From 0841ba05a80353375db360aaa8f8b55bba1955f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Nov 2025 18:51:48 +0100 Subject: [PATCH] fix: Handle finish_reason 'length' to prevent hang when hitting max_tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the response hits the max_tokens limit, the API returns finish_reason="length". Previously, this fell into the catch-all case which didn't emit pending items, causing llmx to hang with "working" status. Now: - Handle "length" the same as "stop" - emit assistant_item and reasoning_item - Also made catch-all case defensive: emit pending items for any unknown finish_reason 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- llmx-rs/core/src/chat_completions.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/llmx-rs/core/src/chat_completions.rs b/llmx-rs/core/src/chat_completions.rs index bc9b6cbf..9d4f4cd4 100644 --- a/llmx-rs/core/src/chat_completions.rs +++ b/llmx-rs/core/src/chat_completions.rs @@ -867,9 +867,9 @@ async fn process_chat_sse( let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } - "stop" => { - // Regular turn without tool-call. Emit the final assistant message - // as a single OutputItemDone so non-delta consumers see the result. + "stop" | "length" => { + // Regular turn without tool-call, or hit max_tokens limit. + // Emit the final assistant message as a single OutputItemDone so non-delta consumers see the result. if let Some(item) = assistant_item.take() { let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } @@ -878,7 +878,16 @@ async fn process_chat_sse( let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } } - _ => {} + _ => { + // Unknown finish_reason - still emit pending items to avoid hanging + debug!("Unknown finish_reason: {}, emitting pending items", finish_reason); + if let Some(item) = assistant_item.take() { + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + if let Some(item) = reasoning_item.take() { + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + } } // Emit Completed regardless of reason so the agent can advance.