Skip to content

Commit 829ba9c

Browse files
authored
Merge pull request #1516 from QwenLM/mingholy/fix/runtime-timeout
feat: add runtime-aware fetch options for Anthropic and OpenAI providers
2 parents 8d0f785 + 4a0e555 commit 829ba9c

File tree

6 files changed

+249
-37
lines changed

6 files changed

+249
-37
lines changed

packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type RawMessageStreamEvent = Anthropic.RawMessageStreamEvent;
2828
import { RequestTokenEstimator } from '../../utils/request-tokenizer/index.js';
2929
import { safeJsonParse } from '../../utils/safeJsonParse.js';
3030
import { AnthropicContentConverter } from './converter.js';
31+
import { buildRuntimeFetchOptions } from '../../utils/runtimeFetchOptions.js';
3132

3233
type StreamingBlockState = {
3334
type: string;
@@ -54,13 +55,17 @@ export class AnthropicContentGenerator implements ContentGenerator {
5455
) {
5556
const defaultHeaders = this.buildHeaders();
5657
const baseURL = contentGeneratorConfig.baseUrl;
58+
// Configure runtime options to ensure user-configured timeout works as expected
59+
// bodyTimeout is always disabled (0) to let Anthropic SDK timeout control the request
60+
const runtimeOptions = buildRuntimeFetchOptions('anthropic');
5761

5862
this.client = new Anthropic({
5963
apiKey: contentGeneratorConfig.apiKey,
6064
baseURL,
6165
timeout: contentGeneratorConfig.timeout,
6266
maxRetries: contentGeneratorConfig.maxRetries,
6367
defaultHeaders,
68+
...runtimeOptions,
6469
});
6570

6671
this.converter = new AnthropicContentConverter(

packages/core/src/core/openaiContentGenerator/provider/dashscope.test.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type { ContentGeneratorConfig } from '../../contentGenerator.js';
1919
import { AuthType } from '../../contentGenerator.js';
2020
import type { ChatCompletionToolWithCache } from './types.js';
2121
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
22+
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
23+
import type { OpenAIRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
2224

2325
// Mock OpenAI
2426
vi.mock('openai', () => ({
@@ -32,13 +34,22 @@ vi.mock('openai', () => ({
3234
})),
3335
}));
3436

37+
vi.mock('../../../utils/runtimeFetchOptions.js', () => ({
38+
buildRuntimeFetchOptions: vi.fn(),
39+
}));
40+
3541
describe('DashScopeOpenAICompatibleProvider', () => {
3642
let provider: DashScopeOpenAICompatibleProvider;
3743
let mockContentGeneratorConfig: ContentGeneratorConfig;
3844
let mockCliConfig: Config;
3945

4046
beforeEach(() => {
4147
vi.clearAllMocks();
48+
const mockedBuildRuntimeFetchOptions =
49+
buildRuntimeFetchOptions as unknown as MockedFunction<
50+
(sdkType: 'openai') => OpenAIRuntimeFetchOptions
51+
>;
52+
mockedBuildRuntimeFetchOptions.mockReturnValue(undefined);
4253

4354
// Mock ContentGeneratorConfig
4455
mockContentGeneratorConfig = {
@@ -185,18 +196,20 @@ describe('DashScopeOpenAICompatibleProvider', () => {
185196
it('should create OpenAI client with DashScope configuration', () => {
186197
const client = provider.buildClient();
187198

188-
expect(OpenAI).toHaveBeenCalledWith({
189-
apiKey: 'test-api-key',
190-
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
191-
timeout: 60000,
192-
maxRetries: 2,
193-
defaultHeaders: {
194-
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
195-
'X-DashScope-CacheControl': 'enable',
196-
'X-DashScope-UserAgent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
197-
'X-DashScope-AuthType': AuthType.QWEN_OAUTH,
198-
},
199-
});
199+
expect(OpenAI).toHaveBeenCalledWith(
200+
expect.objectContaining({
201+
apiKey: 'test-api-key',
202+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
203+
timeout: 60000,
204+
maxRetries: 2,
205+
defaultHeaders: {
206+
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
207+
'X-DashScope-CacheControl': 'enable',
208+
'X-DashScope-UserAgent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
209+
'X-DashScope-AuthType': AuthType.QWEN_OAUTH,
210+
},
211+
}),
212+
);
200213

201214
expect(client).toBeDefined();
202215
});
@@ -207,13 +220,15 @@ describe('DashScopeOpenAICompatibleProvider', () => {
207220

208221
provider.buildClient();
209222

210-
expect(OpenAI).toHaveBeenCalledWith({
211-
apiKey: 'test-api-key',
212-
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
213-
timeout: DEFAULT_TIMEOUT,
214-
maxRetries: DEFAULT_MAX_RETRIES,
215-
defaultHeaders: expect.any(Object),
216-
});
223+
expect(OpenAI).toHaveBeenCalledWith(
224+
expect.objectContaining({
225+
apiKey: 'test-api-key',
226+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
227+
timeout: DEFAULT_TIMEOUT,
228+
maxRetries: DEFAULT_MAX_RETRIES,
229+
defaultHeaders: expect.any(Object),
230+
}),
231+
);
217232
});
218233
});
219234

packages/core/src/core/openaiContentGenerator/provider/dashscope.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ChatCompletionContentPartWithCache,
1717
ChatCompletionToolWithCache,
1818
} from './types.js';
19+
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
1920

2021
export class DashScopeOpenAICompatibleProvider
2122
implements OpenAICompatibleProvider
@@ -68,12 +69,16 @@ export class DashScopeOpenAICompatibleProvider
6869
maxRetries = DEFAULT_MAX_RETRIES,
6970
} = this.contentGeneratorConfig;
7071
const defaultHeaders = this.buildHeaders();
72+
// Configure fetch options to ensure user-configured timeout works as expected
73+
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
74+
const fetchOptions = buildRuntimeFetchOptions('openai');
7175
return new OpenAI({
7276
apiKey,
7377
baseURL: baseUrl,
7478
timeout,
7579
maxRetries,
7680
defaultHeaders,
81+
...(fetchOptions ? { fetchOptions } : {}),
7782
});
7883
}
7984

packages/core/src/core/openaiContentGenerator/provider/default.test.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { DefaultOpenAICompatibleProvider } from './default.js';
1717
import type { Config } from '../../../config/config.js';
1818
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
1919
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
20+
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
21+
import type { OpenAIRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
2022

2123
// Mock OpenAI
2224
vi.mock('openai', () => ({
@@ -30,13 +32,22 @@ vi.mock('openai', () => ({
3032
})),
3133
}));
3234

35+
vi.mock('../../../utils/runtimeFetchOptions.js', () => ({
36+
buildRuntimeFetchOptions: vi.fn(),
37+
}));
38+
3339
describe('DefaultOpenAICompatibleProvider', () => {
3440
let provider: DefaultOpenAICompatibleProvider;
3541
let mockContentGeneratorConfig: ContentGeneratorConfig;
3642
let mockCliConfig: Config;
3743

3844
beforeEach(() => {
3945
vi.clearAllMocks();
46+
const mockedBuildRuntimeFetchOptions =
47+
buildRuntimeFetchOptions as unknown as MockedFunction<
48+
(sdkType: 'openai') => OpenAIRuntimeFetchOptions
49+
>;
50+
mockedBuildRuntimeFetchOptions.mockReturnValue(undefined);
4051

4152
// Mock ContentGeneratorConfig
4253
mockContentGeneratorConfig = {
@@ -112,15 +123,17 @@ describe('DefaultOpenAICompatibleProvider', () => {
112123
it('should create OpenAI client with correct configuration', () => {
113124
const client = provider.buildClient();
114125

115-
expect(OpenAI).toHaveBeenCalledWith({
116-
apiKey: 'test-api-key',
117-
baseURL: 'https://api.openai.com/v1',
118-
timeout: 60000,
119-
maxRetries: 2,
120-
defaultHeaders: {
121-
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
122-
},
123-
});
126+
expect(OpenAI).toHaveBeenCalledWith(
127+
expect.objectContaining({
128+
apiKey: 'test-api-key',
129+
baseURL: 'https://api.openai.com/v1',
130+
timeout: 60000,
131+
maxRetries: 2,
132+
defaultHeaders: {
133+
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
134+
},
135+
}),
136+
);
124137

125138
expect(client).toBeDefined();
126139
});
@@ -131,15 +144,17 @@ describe('DefaultOpenAICompatibleProvider', () => {
131144

132145
provider.buildClient();
133146

134-
expect(OpenAI).toHaveBeenCalledWith({
135-
apiKey: 'test-api-key',
136-
baseURL: 'https://api.openai.com/v1',
137-
timeout: DEFAULT_TIMEOUT,
138-
maxRetries: DEFAULT_MAX_RETRIES,
139-
defaultHeaders: {
140-
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
141-
},
142-
});
147+
expect(OpenAI).toHaveBeenCalledWith(
148+
expect.objectContaining({
149+
apiKey: 'test-api-key',
150+
baseURL: 'https://api.openai.com/v1',
151+
timeout: DEFAULT_TIMEOUT,
152+
maxRetries: DEFAULT_MAX_RETRIES,
153+
defaultHeaders: {
154+
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
155+
},
156+
}),
157+
);
143158
});
144159

145160
it('should include custom headers from buildHeaders', () => {

packages/core/src/core/openaiContentGenerator/provider/default.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Config } from '../../../config/config.js';
44
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
55
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
66
import type { OpenAICompatibleProvider } from './types.js';
7+
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
78

89
/**
910
* Default provider for standard OpenAI-compatible APIs
@@ -43,12 +44,16 @@ export class DefaultOpenAICompatibleProvider
4344
maxRetries = DEFAULT_MAX_RETRIES,
4445
} = this.contentGeneratorConfig;
4546
const defaultHeaders = this.buildHeaders();
47+
// Configure fetch options to ensure user-configured timeout works as expected
48+
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
49+
const fetchOptions = buildRuntimeFetchOptions('openai');
4650
return new OpenAI({
4751
apiKey,
4852
baseURL: baseUrl,
4953
timeout,
5054
maxRetries,
5155
defaultHeaders,
56+
...(fetchOptions ? { fetchOptions } : {}),
5257
});
5358
}
5459

0 commit comments

Comments
 (0)