diff --git a/src/app/api/insights/chat/route.ts b/src/app/api/insights/chat/route.ts index aedc94be7..f418a1c7e 100644 --- a/src/app/api/insights/chat/route.ts +++ b/src/app/api/insights/chat/route.ts @@ -567,6 +567,10 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}. Fetch user: userPrompt, temperature: AI_BUDGETS.coach.temperature, maxTokens: AI_BUDGETS.coach.maxTokens, + // v1.20.1 — thread the request's abort signal so a mid-generation + // client disconnect tears the upstream provider call down instead of + // paying the full token cost into a closed connection. + signal: request.signal, }), }); result = fallback.result; diff --git a/src/lib/ai/anthropic-client.ts b/src/lib/ai/anthropic-client.ts index 97162de03..00c704701 100644 --- a/src/lib/ai/anthropic-client.ts +++ b/src/lib/ai/anthropic-client.ts @@ -259,7 +259,9 @@ export class AnthropicClient implements AIProvider { // 60 s ceiling — see openai-client.ts for the rationale. // v1.11.2 — base URL is user/admin-overridable; pin the connect-time DNS // check so a private/metadata address is rejected (SSRF/rebinding). - { timeoutMs: 60_000, requirePublicHost: true }, + // v1.20.1 — compose the caller's cancel signal (Coach SSE disconnect) so + // a mid-generation abort tears the upstream call down early. + { timeoutMs: 60_000, requirePublicHost: true, signal: params.signal }, ); if (!res.ok) { diff --git a/src/lib/ai/codex-client.ts b/src/lib/ai/codex-client.ts index 2be9fcf8c..dcae2020a 100644 --- a/src/lib/ai/codex-client.ts +++ b/src/lib/ai/codex-client.ts @@ -467,7 +467,9 @@ export class CodexClient implements AIProvider { // clients use so a long generation is not clipped by the 15 s // default while still bounding a tar-pit upstream. }, - { timeoutMs: 60_000 }, + // v1.20.1 — compose the caller's cancel signal (Coach SSE disconnect) so + // a mid-generation abort tears the upstream call down early. + { timeoutMs: 60_000, signal: params.signal }, ); } diff --git a/src/lib/ai/local-client.ts b/src/lib/ai/local-client.ts index 477d9bebc..fa41db7d7 100644 --- a/src/lib/ai/local-client.ts +++ b/src/lib/ai/local-client.ts @@ -125,7 +125,13 @@ export class LocalOpenAICompatibleClient implements AIProvider { ...(params.responseFormat === "json" ? { format: "json" } : {}), }), }, - { timeoutMs: 60_000, requirePublicHost: !allowPrivate }, + // v1.20.1 — compose the caller's cancel signal (Coach SSE disconnect) so + // a mid-generation abort tears the upstream call down early. + { + timeoutMs: 60_000, + requirePublicHost: !allowPrivate, + signal: params.signal, + }, ); if (!res.ok) { diff --git a/src/lib/ai/openai-client.ts b/src/lib/ai/openai-client.ts index e4a8c808d..f84cda5f4 100644 --- a/src/lib/ai/openai-client.ts +++ b/src/lib/ai/openai-client.ts @@ -90,7 +90,9 @@ export class OpenAIClient implements AIProvider { // v1.11.2 — the base URL is user/admin-overridable (BYO gateway), so pin // the connect-time DNS check: a base URL resolving to a private/metadata // address is rejected, closing the SSRF/rebinding surface. - { timeoutMs: 60_000, requirePublicHost: true }, + // v1.20.1 — compose the caller's cancel signal (Coach SSE disconnect) so + // a mid-generation abort tears the upstream call down early. + { timeoutMs: 60_000, requirePublicHost: true, signal: params.signal }, ); if (!res.ok) { diff --git a/src/lib/ai/types.ts b/src/lib/ai/types.ts index 0e1cc8028..318becc18 100644 --- a/src/lib/ai/types.ts +++ b/src/lib/ai/types.ts @@ -271,6 +271,15 @@ export interface CompletionParams { * Mapped per provider; omitted from the wire when unset. */ toolChoice?: "auto" | "none"; + /** + * v1.20.1 — optional caller-owned cancellation signal. Threaded onto the + * client's `safeFetch` so a mid-generation client disconnect (the Coach SSE + * route wires `request.signal` here) tears the upstream provider request + * down instead of running it to completion and paying the full token cost + * into a closed connection. Composed with the per-client timeout inside + * `safeFetch`. Omitted → behaviour is unchanged (timeout-only). + */ + signal?: AbortSignal; } export interface CompletionResult { @@ -313,6 +322,8 @@ export function singleUserTurn(p: { documents?: CompletionDocument[]; tools?: AiToolDef[]; toolChoice?: "auto" | "none"; + /** v1.20.1 — caller-owned cancel signal threaded to the client fetch. */ + signal?: AbortSignal; }): CompletionParams { const images = p.images ?? []; const documents = p.documents ?? []; @@ -349,6 +360,7 @@ export function singleUserTurn(p: { responseFormat: p.responseFormat, tools: p.tools, toolChoice: p.toolChoice, + signal: p.signal, }; }