Skip to content

fix(textGeneration): stop saving max_tokens-truncated responses as complete - #2385

Open
gary149 wants to merge 2 commits into
mainfrom
fix/flag-max-tokens-truncation
Open

fix(textGeneration): stop saving max_tokens-truncated responses as complete#2385
gary149 wants to merge 2 commits into
mainfrom
fix/flag-max-tokens-truncation

Conversation

@gary149

@gary149 gary149 commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a model hits its max_tokens budget, the provider returns finish_reason: "length". The OpenAI stream adapters treated "length" identically to a clean "stop" (marking the final token special: true), so generate.ts computed interrupted = false and the truncated message was persisted as if complete. No log, no flag, no signal.

Most visible with reasoning models: GLM-5.2 spent its whole budget inside a <think> block and hit the limit before any answer, leaving the user with a ~160KB reasoning dump cut off mid-code, saved as a "finished" reply (e.g. conversation/6a3bcad5a78f58a9cd8f8def). Reproduced live: the model streamed only reasoning_content, emitted zero content, and terminated with finish_reason: "length".

Fix

  • The stream adapters (openAIChatToTextGenerationStream, openAICompletionToTextGenerationStream, non-streaming single adapter) mark the final output truncated: true when finish_reason === "length".
  • generate.ts forces interrupted = true on a truncated output and logs a warn. It also gates the completion handling on generated_text != null (not truthiness) so empty truncated completions (hidden-reasoning models, tiny budgets) are still flagged + logged rather than silently dropped.
  • The MCP flow (runMcpFlow.ts) tracks finish_reason and sets interrupted on its FinalAnswer accordingly, with a warning.
  • Adds adapter tests (length vs stop, and the empty length-terminated stream).

This makes the persisted interrupted flag and the logs truthful. chat-ui has no dedicated "response was cut off" banner today, so this is backend correctness + ops visibility; a visible banner/continue affordance gated on this signal is a sensible follow-up.

Test plan

  • npx vitest run src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.spec.ts (3 passing)
  • npm run check (0 errors); ESLint + Prettier clean

Split from #2383 (this is the code half).

gary149 added 2 commits June 24, 2026 15:38
When a model hit its max_tokens budget mid-generation, the provider returns
finish_reason: "length". The stream adapters treated "length" identically to a
clean "stop" (marking the final token special), so generate.ts computed
interrupted=false and the truncated message was persisted as if it were complete.
No log, no interrupted flag, no UI signal.

This was especially visible with reasoning models: e.g. GLM-5.2 (max_tokens 49152)
could spend its entire budget on <think> reasoning and never emit a final answer,
leaving the user with a 160KB think-dump cut off mid-code and no way to tell it was
truncated or to continue it.

- Stream adapters (openAIChatToTextGenerationStream, openAICompletionToTextGenerationStream,
  and the non-streaming single adapter) now mark the final output truncated:true when
  finish_reason === "length".
- generate.ts forces interrupted=true on a truncated output and logs a warning.
- The MCP flow tracks finish_reason and emits its FinalAnswer with interrupted set
  accordingly, plus a warning.
- Adds a regression test covering the length vs stop cases.
Gate the completion handling on `generated_text != null` instead of truthiness
so a terminal output with empty text is still finalized. A model can hit
finish_reason "length" before emitting any visible content (hidden-reasoning
models, very small budgets); previously that output was skipped (the special
token was ignored) and the truncation warning never fired. Now the truncation
is flagged + logged for the empty case too.

Note: a genuinely empty body still surfaces as a "no output" error downstream
(content equals the initial empty content), which is the appropriate UX; this
change is about observability and a consistent interrupted flag, not changing
that outcome.

Adds an adapter test for the empty length-terminated stream.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 287b8b2771

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// with empty text is still finalized: a model can hit finish_reason "length" before
// emitting any visible content (hidden-reasoning models, tiny budgets), and that
// truncation must still be flagged + logged rather than silently dropped.
if (output.generated_text != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle empty truncated finals before no-output guard

When a finish_reason: "length" response has no surfaced tokens, this new != null branch emits a FinalAnswer with text: "" and interrupted: true, but the conversation route still checks only messageToWriteTo.content === initialMessageContent in src/routes/conversation/[id]/+server.ts:713 and then appends the generic “No output was generated” error status. This affects the hidden-reasoning/tiny-budget case called out in the comment: the truncation is finalized, but live users still see an error and the persisted update stream contains both an interrupted final answer and an error. Consider teaching that guard about a received truncated/interrupted final answer, or otherwise avoiding the empty finalization path until it can suppress the no-output error.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant