From d674950c97270a4df62887c691404aba48c021c5 Mon Sep 17 00:00:00 2001 From: Williams Caceres Date: Mon, 3 Aug 2026 22:26:46 -0300 Subject: [PATCH] fix: continuation loop never stops because stream.switches is never incremented SwitchableStream._switches only increments inside switchSource(), which api.chat.ts never calls. The safety check that's supposed to cap auto-continuation at MAX_RESPONSE_SEGMENTS (stream.switches >= MAX_RESPONSE_SEGMENTS) therefore never trips, so a response that keeps returning finishReason: 'length' triggers an infinite continuation loop -- one real LLM call every ~40s, forever, silently consuming API credits, with the UI appearing frozen since nothing ever completes. Fix: track continuation attempts with a local counter that's actually incremented on every continuation, restoring the intended 2-segment cap. Observed in production against DeepSeek (deepseek-chat / deepseek-v4-flash) on a long-running chat that had grown close to the 128k token context cap: 10 consecutive identical 'Reached max token limit (128000): Continuing message (2 switches left)' log lines over 8 minutes, message count frozen, real balance drained on every retry. --- app/routes/api.chat.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 73f9176305b..434d2674531 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -75,6 +75,18 @@ async function chatAction({ context, request }: ActionFunctionArgs) { const stream = new SwitchableStream(); + /* + * FIX (2026-08-04): `stream.switches` is never incremented anywhere in this file + * (SwitchableStream only increments `_switches` inside `switchSource()`, which is + * never called here). That means the `stream.switches >= MAX_RESPONSE_SEGMENTS` + * safety check below never trips, and the "continue on length limit" branch can + * recurse forever for any model/provider that keeps returning finishReason "length" + * (observed in production: infinite loop, one real LLM call every ~40s, forever, + * silently consuming API credits). Tracking our own counter here restores the + * intended cap of MAX_RESPONSE_SEGMENTS. + */ + let continuationSegments = 0; + const cumulativeUsage = { completionTokens: 0, promptTokens: 0, @@ -249,11 +261,16 @@ async function chatAction({ context, request }: ActionFunctionArgs) { return; } - if (stream.switches >= MAX_RESPONSE_SEGMENTS) { + if (continuationSegments >= MAX_RESPONSE_SEGMENTS) { + logger.error( + `Reached max token limit (${MAX_TOKENS}) and used all ${MAX_RESPONSE_SEGMENTS} continuation segments -- stopping instead of looping forever.`, + ); throw Error('Cannot continue message: Maximum segments reached'); } - const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches; + continuationSegments++; + + const switchesLeft = MAX_RESPONSE_SEGMENTS - continuationSegments; logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);