From 8ff3f7d028c8eba92718fab694262803a3ff37a4 Mon Sep 17 00:00:00 2001 From: Matthew Evanusa Date: Thu, 20 Aug 2026 15:49:46 -0700 Subject: [PATCH] fix(translate): add qwen/qwen3.8-max output-token cap (32768) qwen/qwen3.8-max was onboarded to the catalog (#907) and promoted to the high cluster as claude_code's rank-1 AA Agentic Index arm, but never got a modelMaxOutputTokens entry. Claude Code requests max_tokens=64000, which the emit layer then clamped to the 8192 OSS fallback; Qwen3-Max thinking consumes most of that budget, so every agentic turn ended in finish_reason=length and the client entered an auto-resume loop until it surfaced 'Claude's response exceeded the 64000 output token maximum' (prod session 5842e58e-0d13-4901-addd-8afb5b31965d, 2026-08-20 ~22:10Z, two consecutive turns truncated at ~2.9k visible output tokens). Qwen3-Max family serves 32k output on Fireworks (qwen3p8-max); add the cap entry so large requests pass through instead of being truncated 4x. Same bug class as the kimi-k3 fix; other still-absent OSS rows (deepseek-v4-pro/flash, minimax-m3) keep the 8192 fallback until their real ceilings are verified. --- internal/translate/default_max_tokens_test.go | 19 +++++++++++++++++++ internal/translate/envelope.go | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/internal/translate/default_max_tokens_test.go b/internal/translate/default_max_tokens_test.go index 9505dcd56..b9d96c921 100644 --- a/internal/translate/default_max_tokens_test.go +++ b/internal/translate/default_max_tokens_test.go @@ -178,3 +178,22 @@ func TestOpenAISameFormat_ExplicitMaxTokensClampsToKimiK3Ceiling(t *testing.T) { out := parseAndEmit(t, body, "openai", opts) assert.Equal(t, float64(32000), out["max_tokens"]) } + +// Regression: qwen/qwen3.8-max was absent from modelMaxOutputTokens, so a +// Claude Code turn requesting max_tokens=64000 was clamped to the 8192 +// fallback. Qwen3-Max serves 32k output on Fireworks (qwen3p8-max); every +// agentic turn ended in finish_reason=length and an auto-resume loop until +// the client surfaced "response exceeded the 64000 output token maximum". +func TestOpenAISameFormat_ExplicitMaxTokensClampsToQwen38MaxCeiling(t *testing.T) { + body := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}],"max_tokens":64000}`) + opts := translate.EmitOptions{ + TargetModel: "qwen/qwen3.8-max", + Capabilities: router.Lookup("qwen/qwen3.8-max"), + } + out := parseAndEmit(t, body, "openai", opts) + assert.Equal(t, float64(32768), out["max_tokens"]) + + body = []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}],"max_tokens":16384}`) + out = parseAndEmit(t, body, "openai", opts) + assert.Equal(t, float64(16384), out["max_tokens"]) +} diff --git a/internal/translate/envelope.go b/internal/translate/envelope.go index f994b5654..4aa2424c6 100644 --- a/internal/translate/envelope.go +++ b/internal/translate/envelope.go @@ -1201,6 +1201,10 @@ var modelMaxOutputTokens = map[string]int{ // Keyed by full catalog ID, since decision.Model keeps the vendor prefix. // Other OSS rows are still absent and so inherit the 8192 fallback. "moonshotai/kimi-k3": 131072, + // Qwen3-Max family serves 32k output on Fireworks (qwen3p8-max). Without + // this entry a Claude Code max_tokens=64000 turn was clamped to the 8192 + // fallback and truncated every agentic turn (finish_reason=length loop). + "qwen/qwen3.8-max": 32768, } const defaultMaxOutputTokenCap = 8192