diff --git a/src/lib/ai/anthropic-client.ts b/src/lib/ai/anthropic-client.ts index 5b637ebde..5a110f1e9 100644 --- a/src/lib/ai/anthropic-client.ts +++ b/src/lib/ai/anthropic-client.ts @@ -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) { diff --git a/src/lib/ai/local-client.ts b/src/lib/ai/local-client.ts index 33eeafaa4..d967a4162 100644 --- a/src/lib/ai/local-client.ts +++ b/src/lib/ai/local-client.ts @@ -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) { diff --git a/src/lib/ai/openai-client.ts b/src/lib/ai/openai-client.ts index 5c0882813..f4b17d1e3 100644 --- a/src/lib/ai/openai-client.ts +++ b/src/lib/ai/openai-client.ts @@ -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) {