Skip to content

fix(query): keep the truncation retry within the model's output and context limits - #978

Merged
agentforce314 merged 1 commit into
mainfrom
fix/958-escalation-output-limits
Nov 5, 2026
Merged

agentforce314 merged 1 commit into
mainfrom
fix/958-escalation-output-limits

Conversation

@agentforce314

Copy link
Copy Markdown
Owner

Fixes #958. Supersedes #977.

Problem

When a reply is cut off at the output limit, the query loop retries the same request once with max_tokens=64000. Models and gateways that cap output below that reject the retry, and the turn ends with an error instead of continuing.

Fix

The retry still starts at 64,000, then is lowered to whichever of these is known:

  • the model's output ceiling: a catalog row's documented maximum (new ModelConfig.max_output_tokens_ceiling), or settings.modelLimits.maxOutputTokens for a model the catalog doesn't know;
  • CLAUDE_CODE_MAX_OUTPUT_TOKENS;
  • the context window left after the truncated request's input (read from its usage), less a 1,000-token margin.

If the result allows no more output than the truncated attempt had (the max_tokens it sent on the Anthropic wire, or the output it actually produced), the retry is skipped. The loop goes straight to the existing recovery lane, which keeps the partial reply and asks the model to resume. A model with no known limit still retries at 64,000.

Also fixed:

  • Compaction asked for 8,192 output tokens on every model. It now asks for the smaller of 8,192 and the model's normal request size, so summaries for a 4K model are no longer rejected.
  • A malformed modelLimits (a JSON list rather than an object) raised out of the limit lookup. On main that made every Anthropic-wire request for an uncatalogued model fail. It now reads as "nothing configured".
  • modelLimits keys containing a colon (llama3.1:8b) were skipped. They now match like any other key; host-qualified keys behave as before.

The catalog stays authoritative, as the README says: modelLimits never changes the limits of a catalogued model.

Catalog ceilings

max_output_tokens is not a ceiling on most rows. On current Anthropic rows it is the first-attempt value (32K, below the real maximum, so the retry has room). On OpenAI-compatible rows it is advisory and only feeds the auto-compact reservation. So max_output_tokens_ceiling is set only on the 11 legacy rows whose documented maximum is below 64K: Claude 3 and 3.5, gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4 and gpt-3.5-turbo. claude-opus-4-20250514 is left unset because its claude-opus-4 prefix also resolves Opus 4.5 and 4.6, which allow far more.

Why not #977

  • On the Anthropic wire every request already sends max_tokens, so for an uncatalogued model capped at 4,096 the bounded retry was identical to the request that had just been cut off.
  • For catalogued models the cap applied only to the retry (claude-opus-4-8 with maxOutputTokens: 8192 went 32,000 → 8,192 → 32,000), and broad prefix keys such as "claude" would have capped mainstream models.
  • The retry wasn't bounded by the remaining context window.
  • A list-shaped modelLimits raised out of the query loop.

Testing

  • tests/test_query_escalation_limits.py (22 tests) drives the real query() loop through a real OpenAIProvider and AnthropicProvider, with only the network call mocked, and checks the max_tokens each request sends. On main the tests for each bug above fail. The rest guard behaviour that must not change, such as claude-opus-4-8 still retrying 32,000 → 64,000.
  • The 22 test files that touch output limits or compaction: 563 passed. The 22 failures fail identically on a clean origin/main checkout (test_ch04_api_round4 transport lanes, TestStopHooks, test_advisor_effort_wiring, test_agent_loop_compat_model_error).
  • Full suite: 11,016 passed, 33 failed; the same 33 fail on origin/main.

The code comments citing TS behaviour (query.ts skipping the escalation while CLAUDE_CODE_MAX_OUTPUT_TOKENS is set, compact.ts taking the min, the 1,000-token buffer in withRetry.ts) weren't checked against a local copy of the TS source.

Not covered

  • A private model with no modelLimits entry still retries at 64,000. If its endpoint rejects that with an error that doesn't mention max_tokens, the turn still ends. Falling back to the partial reply when the retry is rejected would close that.
  • claude-opus-4-1 still retries at 64,000 because it shares the Opus 4 catalog row.

🤖 Generated with Claude Code

…ontext limits

When a reply hit the output limit, the query loop re-sent the same
request once with max_tokens=64000. Models and gateways that cap output
below that rejected the retry, and the turn ended with an error (#958).

The retry still starts at 64,000, then is lowered to the model's known
output ceiling (a catalog row's documented maximum, or
settings.modelLimits.maxOutputTokens for an uncatalogued model), to
CLAUDE_CODE_MAX_OUTPUT_TOKENS, and to the context window left after the
truncated request's input. When that allows no more output than the
truncated attempt had, the retry is skipped and the loop goes straight
to the recovery lane, which keeps the partial reply. A model with no
known limit still retries at 64,000.

- models/configs: ModelConfig.max_output_tokens_ceiling, set on the
  legacy Claude 3 / 3.5 and GPT-4-era rows whose documented maximum is
  below 64K. Current rows leave it unset: their max_output_tokens is a
  first-attempt or advisory value, not a ceiling.
- models/context: get_max_output_tokens_ceiling and
  get_env_max_output_tokens. The modelLimits lookup reads a non-mapping
  value as unset instead of raising (a JSON list made every
  Anthropic-wire request for an uncatalogued model fail), and matches
  exact keys that contain a colon, such as llama3.1:8b.
- compact: summary requests ask for min(8192, the model's normal
  max_tokens), so a 4K model's summaries are no longer rejected.

The catalog stays authoritative: modelLimits never changes the limits
of a catalogued model.

Supersedes #977.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agentforce314
agentforce314 merged commit e258224 into main Nov 5, 2026
6 of 7 checks passed
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.

Length recovery ignores configured output limit and retries at 64000 tokens

1 participant