fix(claude): distinguish claude-sonnet-5-5 from sonnet-5 in model checks

- Add `isClaudeSonnet55Model` helper and prevent Sonnet 5 matching on Sonnet 5.5 prefixes
- Include Claude Sonnet 5.5 in progress display checks

Closes: #6193
This commit is contained in:
Luis Pater
2026-09-29 21:21:58 +08:00
parent cb9a0b5ce8
commit a0cc065d4d
3 changed files with 121 additions and 1 deletions

View File

@@ -302,15 +302,23 @@ func isClaudeOpus55Model(model string) bool {
return model == "claude-opus-5-5" || strings.HasPrefix(model, "claude-opus-5-5[")
}
func isClaudeSonnet55Model(model string) bool {
model = claudeCanonicalModel(model)
return model == "claude-sonnet-5-5" || strings.HasPrefix(model, "claude-sonnet-5-5-") || strings.HasPrefix(model, "claude-sonnet-5-5[")
}
func isClaudeSonnet5Model(model string) bool {
model = claudeCanonicalModel(model)
if isClaudeSonnet55Model(model) {
return false
}
return model == "claude-sonnet-5" || strings.HasPrefix(model, "claude-sonnet-5-") || strings.HasPrefix(model, "claude-sonnet-5[")
}
// claudeModelUsesProgressDisplay reports the 2.1.280 interactive CLI models that
// send thinking.display=updates unless the caller already chose a display mode.
func claudeModelUsesProgressDisplay(model string) bool {
return isClaudeOpus55Model(model) || isClaudeFable51Model(model) || isClaudeSonnet5Model(model)
return isClaudeOpus55Model(model) || isClaudeFable51Model(model) || isClaudeSonnet5Model(model) || isClaudeSonnet55Model(model)
}
// applyClaudeCloakThinkingDisplay fills the latest CLI display only when the

View File

@@ -8139,6 +8139,11 @@ func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) {
body: `{"model":"claude-sonnet-5"}`,
want: constants + ",mid-conversation-system-2026-04-07,mid-conversation-system-clear-at-2026-08-21,effort-2025-11-24",
},
{
name: "claude-sonnet-5-5 carries tool changes beta unlike sonnet-5",
body: `{"model":"claude-sonnet-5-5"}`,
want: constants + ",mid-conversation-system-2026-04-07,mid-conversation-tool-changes-2026-07-01,mid-conversation-system-clear-at-2026-08-21,effort-2025-11-24",
},
{
name: "claude-opus-4-8 accepts role=system",
body: `{"model":"claude-opus-4-8"}`,

View File

@@ -0,0 +1,107 @@
package executor
import (
"bytes"
"net/http"
"strings"
"testing"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
)
// TestIssue6193_Sonnet55ToolChangeBeta verifies that Sonnet 5.5 receives the
// mid-conversation-tool-changes-2026-07-01 beta, while Sonnet 5 remains excluded,
// and that on the Claude OAuth / CLI-fingerprint path the beta reaches Anthropic.
func TestIssue6193_Sonnet55ToolChangeBeta(t *testing.T) {
// 1. In claudeCodeCLIBetas, Sonnet 5.5 (and dated/bracket variants) must include
// mid-conversation-tool-changes-2026-07-01, while Sonnet 5 must remain excluded.
sonnet55Variants := []string{
"claude-sonnet-5-5",
"claude-sonnet-5-5-20261001",
"claude-sonnet-5-5[1m]",
}
for _, model := range sonnet55Variants {
betas := claudeCodeCLIBetas([]byte(`{"model":"`+model+`"}`), nil, false)
if !strings.Contains(betas, "mid-conversation-tool-changes-2026-07-01") {
t.Fatalf("claudeCodeCLIBetas for %s missing mid-conversation-tool-changes-2026-07-01, got: %s", model, betas)
}
if isClaudeSonnet5Model(model) {
t.Fatalf("isClaudeSonnet5Model(%q) = true, want false", model)
}
if !isClaudeSonnet55Model(model) {
t.Fatalf("isClaudeSonnet55Model(%q) = false, want true", model)
}
if !claudeModelUsesProgressDisplay(model) {
t.Fatalf("claudeModelUsesProgressDisplay(%q) = false, want true", model)
}
}
sonnet5Variants := []string{
"claude-sonnet-5",
"claude-sonnet-5-20260401",
"claude-sonnet-5[1m]",
}
for _, model := range sonnet5Variants {
betas := claudeCodeCLIBetas([]byte(`{"model":"`+model+`"}`), nil, false)
if strings.Contains(betas, "mid-conversation-tool-changes-2026-07-01") {
t.Fatalf("claudeCodeCLIBetas for %s unexpectedly includes mid-conversation-tool-changes-2026-07-01, got: %s", model, betas)
}
if !isClaudeSonnet5Model(model) {
t.Fatalf("isClaudeSonnet5Model(%q) = false, want true", model)
}
if isClaudeSonnet55Model(model) {
t.Fatalf("isClaudeSonnet55Model(%q) = true, want false", model)
}
if !claudeModelUsesProgressDisplay(model) {
t.Fatalf("claudeModelUsesProgressDisplay(%q) = false, want true", model)
}
}
// 2. Outgoing Anthropic-Beta on Claude OAuth / CLI fingerprint path for claude-sonnet-5-5
// must contain mid-conversation-tool-changes-2026-07-01, both when caller sends an explicit beta
// and when caller sends no client beta.
body := []byte(`{
"model": "claude-sonnet-5-5",
"max_tokens": 64,
"tools": [{"name":"lookup_notes","input_schema":{"type":"object"}}],
"messages": [
{"role":"user","content":"hello"},
{"role":"system","content":[{"type":"tool_removal","tool":{"type":"tool_reference","name":"lookup_notes"}}]}
]
}`)
auth := &cliproxyauth.Auth{
ID: "test-oauth",
Metadata: map[string]any{"access_token": "sk-ant-oat-issue-6193"},
}
for _, tc := range []struct {
name string
clientBetas string
}{
{name: "with explicit legacy beta", clientBetas: "mid-conversation-tool-changes-2026-07-01"},
{name: "without client beta", clientBetas: ""},
} {
t.Run(tc.name, func(t *testing.T) {
incomingHeaders := http.Header{}
if tc.clientBetas != "" {
incomingHeaders.Set("Anthropic-Beta", tc.clientBetas)
}
req, err := http.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages", bytes.NewReader(body))
if err != nil {
t.Fatalf("http.NewRequest failed: %v", err)
}
if errApply := applyClaudeHeaders(req, auth, "sk-ant-oat-issue-6193", false, nil, body, &config.Config{}, incomingHeaders, false); errApply != nil {
t.Fatalf("applyClaudeHeaders failed: %v", errApply)
}
upstreamBetas := req.Header.Get("Anthropic-Beta")
if !strings.Contains(upstreamBetas, "mid-conversation-tool-changes-2026-07-01") {
t.Fatalf("applyClaudeHeaders for claude-sonnet-5-5 missing mid-conversation-tool-changes-2026-07-01, got Anthropic-Beta: %s", upstreamBetas)
}
})
}
}