fix(llm): double max_tokens on finish_reason=length before retrying JSON - #2321
fix(llm): double max_tokens on finish_reason=length before retrying JSON#2321smoryan wants to merge 2 commits into
Conversation
Thinking models share one max_tokens budget between reasoning and the final answer, so a truncated completion arrives as 200 OK with finish_reason="length". completeJson treated it like any malformed output and retried with the same max_tokens, which truncates again - the retry could never succeed. On the first truncation, log usage diagnostics (max_tokens, completion chars, token usage) and double max_tokens for the next attempt, capped at 32768. Malformed-only retries (finish_reason=stop) keep the budget unchanged. Covered by tests/unit/llm/length-retry-budget.test.ts: doubling from the default, the 32768 cap, at-most-once doubling across consecutive truncations, and no change when finish_reason is stop.
🤖 Open Code ReviewTarget: PR #2321 🔍 OpenCodeReview found 1 issue(s) in this PR. 1.
|
… when malformedRetries is 0, hoist ceiling constant to module scope - completeJson: the upgraded budget was a silent no-op for callers that pass malformedRetries: 0 (fail-fast parsing) — the while-loop exited before the second attempt could run. The truncation upgrade now grants exactly one additional attempt, independent of the malformed-parse budget. - LENGTH_RETRY_MAX_TOKENS_CEILING hoisted to module scope alongside DEFAULT_MAX_TOKENS (review: re-declared per invocation otherwise). - tests: +1 case locking the malformedRetries: 0 truncation retry. Co-Authored-By: LamzQ <linxlam@foxmail.com>
|
Thanks @Memtensor-AI — both findings addressed in bd9bd17:
llm domain: 86 passed (6 files), tsc clean. |
✅ Automated Test Results: PASSEDAll tests passed (5/5 executed). memos_local_plugin/unit: 5/5. Duration: 3s [advisory, non-gating] AI-generated tests on branch test/auto-gen-0bb1cb61401ceb98-20260902092149: 23/23 passed — these do NOT affect the PR verdict; review the branch manually. Branch: |
Description
Thinking models share one
max_tokensbudget between reasoning and the finalanswer, so a truncated completion arrives as 200 OK with
finish_reason="length"— not as an error.completeJson()treats it likeany malformed output and retries with the same budget, which truncates
again: the retry can never succeed.
On our local deployment this triggered routinely on L3-abstraction calls
(P95 useful output ≈ 4.2k tokens, with reasoning frequently pushing past the
configured budget), burning both attempts of
completeJson(the initial call plus its one malformed-retry) and producingLLM_OUTPUT_MALFORMEDfor what is a budget problem, not a formatting problem.This PR detects the truncation and doubles
max_tokensonce for the retry,capped at 32768.
Related Issue: Fixes #2320
Change
core/llm/client.ts—completeJson():callWithFallback(), ifcompletion.finishReason === "length",log
max_tokens_truncateddiagnostics (op,attempt,maxTokens,completion chars,
usage.completionTokens/usage.totalTokens; theusage block is nullable — not every provider normalizes it);
truncatedOnceguard), still granted when the caller passesmalformedRetries: 0(fail-fast), rebuild the callwith
maxTokens = min(32768, max(2 × maxTokens, DEFAULT_MAX_TOKENS)).The detection sits before JSON parsing so the diagnosis does not
depend on the parse failing, and the once-only guard keeps retries from
ratcheting cost upward. If the configured budget is already ≥ 32768
(the cap), the budget is left untouched — the guard never lowers an
explicit high budget a caller set via
llm.maxTokens(fix(plugin): declare llm.maxTokens and llm.headers as first-class config keys #2248).finish_reason="stop") keep the budgetunchanged.
tests/unit/llm/length-retry-budget.test.ts— new suite (5 tests) driving thereal retry loop through a stub provider:
doubling from the default (1024 → 2048) and the retry parses;
cap at 32768 from 16384;
at-most-once doubling across consecutive truncations (budget stays at
32768, ultimately rejects with
MemosError);budget unchanged when the only problem is malformed output
(
finish_reason="stop").Tests
npx vitest run tests/unit/llm/length-retry-budget.test.ts tests/unit/llm/client.test.ts→ 30 passed (5 new + 25 existing)npx vitest run tests/unit/llm/→ 86 passed (6 files)npx tsc -p tsconfig.json --noEmit→ clean (exit 0)Reproduce: clone
main, apply this PR,cd apps/memos-local-plugin,npm install, run the commands above.Related
llm.maxTokensa first-class config key —this covers the retry behavior that PR did not address: with
finish_reason="length", raising the configured budget up front helps,but a completion that still runs out mid-JSON must not burn its retries
at the same doomed budget.
Type of change
How Has This Been Tested?
npx vitest run tests/unit/llm/→ 86 passed (6 files), incl. 5 new truncation-budget testsChecklist
Environment
28dfb4e)