fix(ai): bound every provider fetch with a 60 s AbortSignal timeout

The OpenAI, Anthropic, and local OpenAI-compatible clients all used a
bare `fetch()` with no `signal`. A tar-pit upstream (a misconfigured
local Ollama on a stalled GPU, a flaky proxy in front of a hosted
provider, or a deliberately malicious user-configured LOCAL endpoint)
could accept the POST and then hang indefinitely, pinning the request
handler until the connection died on the socket level.

A 60 s ceiling lands well outside any legitimate completion budget
(production timings live in `getEvent()?.addExternalCall` traces) while
capping the worst case.

The Codex client uses an SSE stream with its own loop and stays as-is
for now; a follow-up can thread the same ceiling through the streaming
read once the contract there is settled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-27 00:05:12 +02:00
1 parent 2d5b4cfac1
commit fc83d8541f
3 files changed
+10

No files matched your search

+2
View File
@@ -49,6 +49,8 @@ export class AnthropicClient implements AIProvider {
system: params.systemPrompt,
messages: [{ role: "user", content: wrapForJson(params.userPrompt) }],
}),
// 60 s ceiling — see openai-client.ts for the rationale.
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) {
+4
View File
@@ -50,6 +50,10 @@ export class LocalOpenAICompatibleClient implements AIProvider {
temperature: params.temperature ?? 0.3,
max_tokens: params.maxTokens ?? 1000,
}),
// 60 s ceiling — a misbehaving local endpoint (Ollama on a stalled
// GPU, a tar-pit proxy) shouldn't be able to pin a worker
// indefinitely. Real local completions land well inside this.
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) {
+4
View File
@@ -46,6 +46,10 @@ export class OpenAIClient implements AIProvider {
max_tokens: params.maxTokens ?? 1000,
response_format: { type: "json_object" },
}),
// Cap any single completion at 60 s so a tar-pit upstream cannot
// pin a worker indefinitely. Real completions land well inside this
// budget — see `getEvent()?.addExternalCall` timings in production.
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) {