























@@ -3,24 +3,40 @@ import { Stream } from "openai/streaming";
33import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4455const {
6+ buildProviderRequestDispatcherPolicyMock,
67 fetchWithSsrFGuardMock,
78 mergeModelProviderRequestOverridesMock,
89 resolveProviderRequestPolicyConfigMock,
10+ shouldUseEnvHttpProxyForUrlMock,
11+ withTrustedEnvProxyGuardedFetchModeMock,
912} = vi.hoisted(() => ({
13+buildProviderRequestDispatcherPolicyMock: vi.fn<
14+(_request?: unknown) => { mode: "direct" } | undefined
15+>(() => undefined),
1016fetchWithSsrFGuardMock: vi.fn(),
1117mergeModelProviderRequestOverridesMock: vi.fn((current, overrides) => ({
1218 ...current,
1319 ...overrides,
1420})),
1521resolveProviderRequestPolicyConfigMock: vi.fn(() => ({ allowPrivateNetwork: false })),
22+shouldUseEnvHttpProxyForUrlMock: vi.fn(() => false),
23+withTrustedEnvProxyGuardedFetchModeMock: vi.fn((params: Record<string, unknown>) => ({
24+ ...params,
25+mode: "trusted_env_proxy",
26+})),
1627}));
17281829vi.mock("../infra/net/fetch-guard.js", () => ({
1930fetchWithSsrFGuard: fetchWithSsrFGuardMock,
31+withTrustedEnvProxyGuardedFetchMode: withTrustedEnvProxyGuardedFetchModeMock,
32+}));
33+34+vi.mock("../infra/net/proxy-env.js", () => ({
35+shouldUseEnvHttpProxyForUrl: shouldUseEnvHttpProxyForUrlMock,
2036}));
21372238vi.mock("./provider-request-config.js", () => ({
23-buildProviderRequestDispatcherPolicy: vi.fn(() => ({ mode: "direct" })),
39+buildProviderRequestDispatcherPolicy: buildProviderRequestDispatcherPolicyMock,
2440getModelProviderRequestTransport: vi.fn(() => undefined),
2541mergeModelProviderRequestOverrides: mergeModelProviderRequestOverridesMock,
2642resolveProviderRequestPolicyConfig: resolveProviderRequestPolicyConfigMock,
@@ -33,10 +49,13 @@ describe("buildGuardedModelFetch", () => {
3349finalUrl: "https://api.openai.com/v1/responses",
3450release: vi.fn(async () => undefined),
3551});
52+buildProviderRequestDispatcherPolicyMock.mockClear().mockReturnValue(undefined);
3653mergeModelProviderRequestOverridesMock.mockClear();
3754resolveProviderRequestPolicyConfigMock
3855.mockClear()
3956.mockReturnValue({ allowPrivateNetwork: false });
57+shouldUseEnvHttpProxyForUrlMock.mockClear().mockReturnValue(false);
58+withTrustedEnvProxyGuardedFetchModeMock.mockClear();
4059delete process.env.OPENCLAW_DEBUG_PROXY_ENABLED;
4160delete process.env.OPENCLAW_DEBUG_PROXY_URL;
4261delete process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS;
@@ -137,6 +156,63 @@ describe("buildGuardedModelFetch", () => {
137156});
138157});
139158159+it("uses trusted env-proxy mode for provider calls when no explicit dispatcher policy is configured", async () => {
160+shouldUseEnvHttpProxyForUrlMock.mockReturnValueOnce(true);
161+const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");
162+const model = {
163+id: "gpt-5.4",
164+provider: "openai",
165+api: "openai-responses",
166+baseUrl: "https://api.openai.com/v1",
167+} as unknown as Model<"openai-responses">;
168+169+const fetcher = buildGuardedModelFetch(model);
170+await fetcher("https://api.openai.com/v1/responses", { method: "POST" });
171+172+expect(shouldUseEnvHttpProxyForUrlMock).toHaveBeenCalledWith(
173+"https://api.openai.com/v1/responses",
174+);
175+expect(withTrustedEnvProxyGuardedFetchModeMock).toHaveBeenCalledWith(
176+expect.objectContaining({
177+url: "https://api.openai.com/v1/responses",
178+dispatcherPolicy: undefined,
179+policy: {
180+allowRfc2544BenchmarkRange: true,
181+allowIpv6UniqueLocalRange: true,
182+hostnameAllowlist: ["api.openai.com"],
183+},
184+}),
185+);
186+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
187+expect.objectContaining({
188+url: "https://api.openai.com/v1/responses",
189+mode: "trusted_env_proxy",
190+}),
191+);
192+});
193+194+it("keeps explicit provider dispatcher policies in strict guarded-fetch mode", async () => {
195+shouldUseEnvHttpProxyForUrlMock.mockReturnValueOnce(true);
196+buildProviderRequestDispatcherPolicyMock.mockReturnValueOnce({ mode: "direct" });
197+const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");
198+const model = {
199+id: "gpt-5.4",
200+provider: "openai",
201+api: "openai-responses",
202+baseUrl: "https://api.openai.com/v1",
203+} as unknown as Model<"openai-responses">;
204+205+const fetcher = buildGuardedModelFetch(model);
206+await fetcher("https://api.openai.com/v1/responses", { method: "POST" });
207+208+expect(withTrustedEnvProxyGuardedFetchModeMock).not.toHaveBeenCalled();
209+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
210+expect.objectContaining({
211+dispatcherPolicy: { mode: "direct" },
212+}),
213+);
214+});
215+140216it("threads explicit transport timeouts into the shared guarded fetch seam", async () => {
141217const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");
142218const model = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。