diff --git a/internal/runtime/executor/claude_executor_request.go b/internal/runtime/executor/claude_executor_request.go index 8b0838ec6..d330f337c 100644 --- a/internal/runtime/executor/claude_executor_request.go +++ b/internal/runtime/executor/claude_executor_request.go @@ -135,7 +135,7 @@ func isManagedClaudeBeta(beta string) bool { // 11 advanced-tool-use-2025-11-20 requests using tool search or another advanced tool-use feature // 12 effort-2025-11-24 effort-supporting models with active thinking // 13 server-side-fallback-2026-06-01 requests with fallbacks or requested -// 14 fallback-credit-2026-06-01 OAuth credentials +// 14 fallback-credit-2026-06-01 requests with fallback tokens, fallbacks, or requested // 15 structured-outputs-2025-12-15 structured output requests // 16 thinking-display-updates-2026-08-18 requests with thinking.display=updates // 17 fast-mode-2026-02-01 speed:fast requests only @@ -177,7 +177,10 @@ func claudeCodeCLIBetas(body []byte, requested map[string]bool, oauthToken bool) if !isProbeOrHelper && (requested[claudeServerSideFallbackBeta] || gjson.GetBytes(body, "fallbacks").Exists()) { betas = append(betas, claudeServerSideFallbackBeta) } - if requested[claudeFallbackCreditBeta] || oauthToken { + shouldIncludeFallbackCredit := requested[claudeFallbackCreditBeta] || + gjson.GetBytes(body, "fallback_credit_token").Exists() || + (oauthToken && gjson.GetBytes(body, "fallbacks").Exists()) + if shouldIncludeFallbackCredit { betas = append(betas, claudeFallbackCreditBeta) } for _, beta := range claudeCodeTrailingBetas { diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 3924d91f9..2cae16f29 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -7990,8 +7990,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20," + - "effort-2025-11-24,fallback-credit-2026-06-01," + - "extended-cache-ttl-2025-04-11", + "effort-2025-11-24,extended-cache-ttl-2025-04-11", }, { name: "oauth precedes context-1m", @@ -8122,7 +8121,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "advisor-tool-2026-03-01,effort-2025-11-24,fallback-credit-2026-06-01," + + "advisor-tool-2026-03-01,effort-2025-11-24," + "afk-mode-2026-01-31,extended-cache-ttl-2025-04-11", }, { @@ -8134,7 +8133,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "effort-2025-11-24,fallback-credit-2026-06-01,fast-mode-2026-02-01," + + "effort-2025-11-24,fast-mode-2026-02-01," + "afk-mode-2026-01-31,extended-cache-ttl-2025-04-11", }, { @@ -8145,7 +8144,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "effort-2025-11-24,fallback-credit-2026-06-01,extended-cache-ttl-2025-04-11", + "effort-2025-11-24,extended-cache-ttl-2025-04-11", }, { name: "thinking display updates emits thinking-display-updates beta and drops redact-thinking", @@ -8168,7 +8167,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "effort-2025-11-24,fallback-credit-2026-06-01", + "effort-2025-11-24", }, { name: "probe request max_tokens=1 omits effort and extended-cache-ttl betas", @@ -8177,8 +8176,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { want: "claude-code-20250219,oauth-2025-04-20," + "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + - "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "fallback-credit-2026-06-01", + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07", }, { name: "haiku model omits effort beta even if requested", @@ -8189,7 +8187,7 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05," + - "fallback-credit-2026-06-01,extended-cache-ttl-2025-04-11", + "extended-cache-ttl-2025-04-11", }, { name: "disabled thinking omits effort beta even if requested", @@ -8200,7 +8198,39 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) { "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + "thinking-token-count-2026-05-13,context-management-2025-06-27," + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + - "fallback-credit-2026-06-01,extended-cache-ttl-2025-04-11", + "extended-cache-ttl-2025-04-11", + }, + { + name: "body with fallback_credit_token automatically adds fallback-credit beta", + body: `{"model":"claude-sonnet-5","fallback_credit_token":"fct_12345"}`, + oauth: true, + want: "claude-code-20250219,oauth-2025-04-20," + + "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + + "thinking-token-count-2026-05-13,context-management-2025-06-27," + + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + + "effort-2025-11-24,fallback-credit-2026-06-01,extended-cache-ttl-2025-04-11", + }, + { + name: "oauth body with fallbacks automatically adds fallback-credit beta", + body: `{"model":"claude-fable-5-1","fallbacks":[{"model":"claude-opus-5"}]}`, + oauth: true, + want: "claude-code-20250219,oauth-2025-04-20," + + "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + + "thinking-token-count-2026-05-13,context-management-2025-06-27," + + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + + "effort-2025-11-24,server-side-fallback-2026-06-01,fallback-credit-2026-06-01," + + "extended-cache-ttl-2025-04-11", + }, + { + name: "requested fallback-credit beta is honored", + body: `{"model":"claude-sonnet-5"}`, + requested: map[string]bool{claudeFallbackCreditBeta: true}, + oauth: true, + want: "claude-code-20250219,oauth-2025-04-20," + + "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," + + "thinking-token-count-2026-05-13,context-management-2025-06-27," + + "prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," + + "effort-2025-11-24,fallback-credit-2026-06-01,extended-cache-ttl-2025-04-11", }, } diff --git a/internal/runtime/executor/claude_fingerprint_policy_test.go b/internal/runtime/executor/claude_fingerprint_policy_test.go index 4fe9c990a..0e86482da 100644 --- a/internal/runtime/executor/claude_fingerprint_policy_test.go +++ b/internal/runtime/executor/claude_fingerprint_policy_test.go @@ -235,7 +235,8 @@ func TestApplyClaudeHeaders_ClaudeCodeCLIProfileUsesOAuthBetasWithoutPretendingT if errReq != nil { t.Fatalf("NewRequest() error = %v", errReq) } - if errHeaders := applyClaudeHeaders(req, auth, "key-third-party", false, nil, []byte(`{"model":"claude-sonnet-5"}`), &config.Config{}, nil, false, "11111111-2222-4333-8444-555555555555"); errHeaders != nil { + payload := []byte(`{"model":"claude-sonnet-5"}`) + if errHeaders := applyClaudeHeaders(req, auth, "key-third-party", false, nil, payload, &config.Config{}, nil, false, "11111111-2222-4333-8444-555555555555"); errHeaders != nil { t.Fatalf("applyClaudeHeaders() error = %v", errHeaders) } if got := req.Header.Get("Authorization"); got != "Bearer key-third-party" { @@ -245,14 +246,8 @@ func TestApplyClaudeHeaders_ClaudeCodeCLIProfileUsesOAuthBetasWithoutPretendingT t.Fatalf("x-api-key = %q, want empty on third-party gateway", got) } betas := req.Header.Get("Anthropic-Beta") - if !strings.Contains(betas, "oauth-2025-04-20") { - t.Fatalf("Anthropic-Beta = %q, want oauth beta", betas) - } - if !strings.Contains(betas, "extended-cache-ttl-2025-04-11") { - t.Fatalf("Anthropic-Beta = %q, want extended-cache-ttl", betas) - } - if !strings.Contains(betas, "fallback-credit-2026-06-01") { - t.Fatalf("Anthropic-Beta = %q, want fallback-credit", betas) + if want := claudeCodeCLIBetas(payload, nil, true); betas != want { + t.Fatalf("Anthropic-Beta = %q, want %q", betas, want) } }