Set max_tokens on non-reasoning Anthropic requests (#816) - #822
Conversation
|
Thanks, I'll take a look at merging and have this ship in the next release. |
With reasoning off (or effort absent) nothing set max_tokens on the per-request CompletionRequestBuilder in rig_stream_factory: the reasoning-ceiling branch was the only writer, and rig 0.41 hard-errors with "`max_tokens` must be set for Anthropic" before the HTTP call for any model id outside its per-model default table — every Claude 5 id. Every non-reasoning turn on such a model failed. Thread a per-agent cap along the same path reasoning travels: build_agent -> AnyAgent.max_tokens -> LoopSpawnConfig -> LoopConfig -> StreamOptions -> the stream builder. The value is the cap the user explicitly configured (CLI --max-tokens > config max_tokens); when nothing is configured, dirge invents its 8192 default ONLY where rig has no per-model default of its own — read off the model's default_max_tokens field, not a duplicated model list — so an unconfigured user on a rig-recognised id (opus-4.x 128k, sonnet-4/haiku-4.5 64k) keeps rig's larger cap instead of a silent cut to 8192. A new request_max_tokens helper decides the request value: a thinking turn keeps the reasoning ceiling (budget_tokens must stay strictly below max_tokens — the v0.24.1 invariant anthropic_ceiling_clears_every_budget pins), a non-reasoning turn on an Anthropic-shaped provider carries the threaded value, and every other provider stays unset, byte-identical to before.
a987943 to
ef5c536
Compare
|
Thanks, if you could address the macos failure, happy to merge it in. |
The trace test helper selects records by sequence number, and SEQ is a process-global counter that starts at zero in every process, so the filter is only sound while a trace file belongs to one process. The sink filename carried only a SystemTime run stamp read as each process starts; under a process-per-test runner two processes launched in the same clock tick read the same stamp, share a file, and then read each other's records back through overlapping sequence ranges. That surfaced as a macOS CI failure in which recs[0] was another test's record: a_tool_call_and_its_result_share_an_id saw kind "tool_end" where it wrote tool_start, and oversized_payloads_are_bounded_and_marked saw a short unmarked excerpt. The two failures came from adjacent PIDs. Adding the PID to the filename makes each file private to its writer. Test-helper only; no production code changes.
|
Rebased onto current The macOS failure isn't from this patch — it's a pre-existing race in the In both,
Fix is one line plus a comment: add the PID to the filename, making each file private to its writer. Test-helper only, no production code. Two concurrent processes can't share a PID, so the race is closed rather than narrowed. Two adjacent things I noticed but deliberately did not change, in case you want them handled differently:
Neither is in scope here; real isolation would mean injecting the sink rather than filtering a shared log, which is a production-code refactor I didn't want to fold into a bug fix. Local verification of the branch as pushed: full suite 5423 passed / 0 failed / 1 ignored, |
|
Thanks for digging in. |
Fixes #816.
Problem
With reasoning off, an Anthropic request carries no
max_tokensand fails before the HTTP call:Reproduced 3/3 on v0.25.0 with
{"provider_type":"anthropic","model":"claude-opus-5","auth":"claude-code","effort":"off"}, and via/model claude-opus-5interactively.Root cause
rig_stream_factory.rssetmax_tokenson the per-request builder only from the reasoning-ceiling branch, so with reasoning off nothing set it. rig's Anthropic model then falls back todefault_max_tokens_for_model(), which recognizes onlyclaude-opus-4*/claude-sonnet-4*/claude-haiku-4-5*— every Claude 5 id yieldsNoneand the request is rejected.The streaming path never carried the agent's
max_tokens:build_agentsets it on theAgentBuilder, but since 0.41AnyAgentInnerstores the model directly rather than a rigAgent, and the per-request builder is constructed independently. rig's per-model default was papering over the gap for recognized ids.Fix
Thread the resolved cap along the same path
reasoningalready travels — no factory signature changes — into a small pure helper:The reasoning ceiling short-circuits first, so the
budget_tokens < max_tokensinvariant from v0.24.1 is untouched (anthropic_ceiling_clears_every_budgetstill passes;adapter.rsis not modified). Gating onturn_reasoning_enabledmeans an OpenAIeffort: highturn — which produces no ceiling — is not capped, and gating onEffortWire::AnthropicBudgetkeeps every non-Anthropic provider byte-identical.Not lowering existing defaults
A first cut used
resolve_max_tokensunconditionally, whoseunwrap_or(8192)erased the difference between "user chose 8192" and "nobody said". That would have cut unconfiguredclaude-opus-4-6from rig's 128,000 to 8,192 — silent truncation for users who never hit the bug. Instead a value is invented only where rig has none, decided by reading rig's own resolved default off the stored model:No model-prefix list is duplicated in dirge, so this tracks rig with no drift. Unconfigured recognized ids keep rig's exact per-model values; an explicitly configured cap is honored (the
max_tokensconfig key and--max-tokensflag reach the streaming path for the first time).Tests
4 new in
provider::tests::max_tokens_816_tests, including a wire-level pin that stands a local capture server up and asserts an unconfigured non-reasoning request forclaude-opus-4-6carries rig's own default (read off the model, not hardcoded) and not 8192. Plus 5 inrig_stream_factory.rscovering the helper across providers and reasoning states, and an extended propagation assertion instream.rs.Full suite: 5414 passed, 0 failed, 1 ignored.
clippy --all-targets -- -D warningsclean,fmt --checkclean.End-to-end: the failing repro above now completes normally.
Disclosures / known adjacent cases
--no-default-features --features no-plugin— thepluginfeature's janet toolchain is unavailable on this host (Build fails on Linux #712). The singlemax_tokens: None,literal added toplugin_hooks_tests.rsis therefore verified only by CI with default features.dispatch.rsone_shot!/btw_querybuilds anAgentBuilderwith nomax_tokens, so one-shot side calls on unrecognized Anthropic ids have the same latent failure. Not addressed here.