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.
This commit is contained in:
tanzhenxin 2026-01-30 10:06:23 +08:00
parent 3c2e29931c
commit 48ca2d043e
2 changed files with 51 additions and 40 deletions

View file

@ -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');
});
});

View file

@ -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 {};
}
}
}