fix: Make General proxy timeout authoritative

Non-stream OpenAI-compatible proxy requests still kept the previous 300s timeout floor after the unified network settings merge. That made lower General AI timeout settings ineffective for normal proxy traffic, even though the setting is meant to be the final provider request timeout.

Constraint: Unified network settings must remain the final timeout and proxy override for provider protocol paths.
Rejected: Keep the 300s non-stream floor | it makes low timeout settings ineffective for normal proxy requests.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce per-protocol timeout floors without updating the General network settings contract and tests.
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Live provider request against a real upstream proxy.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-20 17:48:01 +08:00
parent 88ac83d42f
commit 4343b3f039
2 changed files with 152 additions and 2 deletions

View File

@ -30,6 +30,156 @@ describe('proxy network settings', () => {
beforeEach(setup)
afterEach(teardown)
test('uses configured AI request timeout for non-stream upstream requests', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
JSON.stringify({
network: {
aiRequestTimeoutMs: 45_000,
proxy: { mode: 'system', url: '' },
},
}),
'utf-8',
)
const svc = new ProviderService()
const provider = await svc.addProvider({
presetId: 'custom',
name: 'OpenAI Proxy',
baseUrl: 'https://api.example.com',
apiKey: 'sk-test',
apiFormat: 'openai_chat',
models: {
main: 'model-main',
haiku: 'model-main',
sonnet: 'model-main',
opus: 'model-main',
},
})
const originalFetch = globalThis.fetch
const originalTimeout = AbortSignal.timeout
const timeoutCalls: number[] = []
globalThis.fetch = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
return new Response(JSON.stringify({
id: 'chatcmpl-network-timeout',
object: 'chat.completion',
created: 0,
model: 'model-main',
choices: [{
index: 0,
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
AbortSignal.timeout = ((ms: number) => {
timeoutCalls.push(ms)
return originalTimeout(ms)
}) as typeof AbortSignal.timeout
try {
const body = {
model: 'model-main',
max_tokens: 64,
messages: [{ role: 'user', content: 'hello' }],
}
const req = new Request(
`http://localhost:3456/proxy/providers/${provider.id}/v1/messages`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
},
)
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
expect(timeoutCalls).toEqual([45_000])
} finally {
AbortSignal.timeout = originalTimeout
globalThis.fetch = originalFetch
}
})
test('uses configured AI request timeout for non-stream Responses upstream requests', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
JSON.stringify({
network: {
aiRequestTimeoutMs: 45_000,
proxy: { mode: 'system', url: '' },
},
}),
'utf-8',
)
const svc = new ProviderService()
const provider = await svc.addProvider({
presetId: 'custom',
name: 'OpenAI Responses Proxy',
baseUrl: 'https://api.example.com',
apiKey: 'sk-test',
apiFormat: 'openai_responses',
models: {
main: 'model-main',
haiku: 'model-main',
sonnet: 'model-main',
opus: 'model-main',
},
})
const originalFetch = globalThis.fetch
const originalTimeout = AbortSignal.timeout
const timeoutCalls: number[] = []
globalThis.fetch = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
return new Response(JSON.stringify({
id: 'resp-network-timeout',
status: 'completed',
model: 'model-main',
output: [{
type: 'message',
content: [{ type: 'output_text', text: 'ok' }],
}],
usage: { input_tokens: 1, output_tokens: 1 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
AbortSignal.timeout = ((ms: number) => {
timeoutCalls.push(ms)
return originalTimeout(ms)
}) as typeof AbortSignal.timeout
try {
const body = {
model: 'model-main',
max_tokens: 64,
messages: [{ role: 'user', content: 'hello' }],
}
const req = new Request(
`http://localhost:3456/proxy/providers/${provider.id}/v1/messages`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
},
)
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
expect(timeoutCalls).toEqual([45_000])
} finally {
AbortSignal.timeout = originalTimeout
globalThis.fetch = originalFetch
}
})
test('uses configured AI request timeout for streaming upstream requests', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),

View File

@ -134,7 +134,7 @@ async function handleOpenaiChat(
Authorization: `Bearer ${apiKey}`,
},
body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)),
signal: AbortSignal.timeout(isStream ? aiRequestTimeoutMs : Math.max(aiRequestTimeoutMs, 300_000)),
signal: AbortSignal.timeout(aiRequestTimeoutMs),
...proxyOptions,
})
@ -202,7 +202,7 @@ async function handleOpenaiResponses(
Authorization: `Bearer ${apiKey}`,
},
body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)),
signal: AbortSignal.timeout(isStream ? aiRequestTimeoutMs : Math.max(aiRequestTimeoutMs, 300_000)),
signal: AbortSignal.timeout(aiRequestTimeoutMs),
...proxyOptions,
})