From 48ca2d043ec1038d3f754d439e754296774a3364 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 30 Jan 2026 10:06:23 +0800 Subject: [PATCH] fix(core): avoid passing undici agent to Anthropic SDK Anthropic's Node client uses node-fetch, which expects a Node http(s).Agent. Stop providing undici's Dispatcher as httpAgent; only set httpAgent when a proxy is configured (via https-proxy-agent), otherwise let the SDK use its default. --- .../src/utils/runtimeFetchOptions.test.ts | 28 +++++++-- .../core/src/utils/runtimeFetchOptions.ts | 63 +++++++++---------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/packages/core/src/utils/runtimeFetchOptions.test.ts b/packages/core/src/utils/runtimeFetchOptions.test.ts index 3cb6efbd15..23ff0c7512 100644 --- a/packages/core/src/utils/runtimeFetchOptions.test.ts +++ b/packages/core/src/utils/runtimeFetchOptions.test.ts @@ -30,6 +30,19 @@ vi.mock('undici', () => { }; }); +vi.mock('https-proxy-agent', () => { + class MockHttpsProxyAgent { + proxyUrl: string; + constructor(proxyUrl: string) { + this.proxyUrl = proxyUrl; + } + } + + return { + HttpsProxyAgent: MockHttpsProxyAgent, + }; +}); + describe('buildRuntimeFetchOptions (node runtime)', () => { it('disables undici timeouts for Agent in OpenAI options', () => { const result = buildRuntimeFetchOptions('openai'); @@ -60,17 +73,20 @@ describe('buildRuntimeFetchOptions (node runtime)', () => { }); }); - it('returns httpAgent with disabled timeouts for Anthropic options', () => { + it('returns empty object for Anthropic without proxy (uses SDK default agent)', () => { const result = buildRuntimeFetchOptions('anthropic'); + expect(result).toEqual({}); + }); + + it('returns HttpsProxyAgent for Anthropic with proxy', () => { + const result = buildRuntimeFetchOptions('anthropic', 'http://proxy.local'); + expect(result).toBeDefined(); expect(result && 'httpAgent' in result).toBe(true); - const httpAgent = (result as { httpAgent?: { options?: UndiciOptions } }) + const httpAgent = (result as { httpAgent?: { proxyUrl?: string } }) .httpAgent; - expect(httpAgent?.options).toMatchObject({ - headersTimeout: 0, - bodyTimeout: 0, - }); + expect(httpAgent?.proxyUrl).toBe('http://proxy.local'); }); }); diff --git a/packages/core/src/utils/runtimeFetchOptions.ts b/packages/core/src/utils/runtimeFetchOptions.ts index 8eab8929f5..88f896d609 100644 --- a/packages/core/src/utils/runtimeFetchOptions.ts +++ b/packages/core/src/utils/runtimeFetchOptions.ts @@ -5,6 +5,7 @@ */ import { Agent, ProxyAgent, type Dispatcher } from 'undici'; +import { HttpsProxyAgent } from 'https-proxy-agent'; /** * JavaScript runtime type @@ -113,48 +114,42 @@ export function buildRuntimeFetchOptions( } case 'node': { - // Node.js: Use ProxyAgent when proxy is configured, otherwise Agent. - // undici timeouts are disabled to let SDK timeout control the request. - try { - const dispatcher = createDispatcher(proxyUrl); - if (sdkType === 'openai') { - return { - dispatcher, - }; - } else { - return { - httpAgent: dispatcher, - }; - } - } catch { - // If undici is not available, return appropriate default - if (sdkType === 'openai') { + // Node.js: Use appropriate agent based on SDK type. + // OpenAI SDK uses undici internally, so we use undici Dispatcher. + // Anthropic SDK uses node-fetch internally, so we use Node.js-compatible agents. + if (sdkType === 'openai') { + try { + const dispatcher = createDispatcher(proxyUrl); + return { dispatcher }; + } catch { return undefined; - } else { - return {}; } + } else { + // Anthropic SDK: use HttpsProxyAgent when proxy is set, + // otherwise let SDK use its default agentkeepalive agent. + // The SDK's timeout option controls request-level timeouts. + if (proxyUrl) { + return { httpAgent: new HttpsProxyAgent(proxyUrl) }; + } + return {}; } } default: { - // Unknown runtime: Use ProxyAgent when proxy is configured, otherwise Agent. - try { - const dispatcher = createDispatcher(proxyUrl); - if (sdkType === 'openai') { - return { - dispatcher, - }; - } else { - return { - httpAgent: dispatcher, - }; - } - } catch { - if (sdkType === 'openai') { + // Unknown runtime: treat as Node.js-like environment. + if (sdkType === 'openai') { + try { + const dispatcher = createDispatcher(proxyUrl); + return { dispatcher }; + } catch { return undefined; - } else { - return {}; } + } else { + // Anthropic SDK: use HttpsProxyAgent when proxy is set + if (proxyUrl) { + return { httpAgent: new HttpsProxyAgent(proxyUrl) }; + } + return {}; } } }