From 956ce7cf785a0676aa7cc24719927bbfa03c69af Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:36:15 -0700 Subject: [PATCH] fix(registry): add Claude Sonnet 5 model metadata --- .../registry/model_registry_safety_test.go | 28 +++++++++++++ internal/registry/models/models.json | 22 +++++++++++ internal/runtime/executor/claude_executor.go | 23 +++++------ .../runtime/executor/claude_executor_test.go | 39 ++++++++++++++----- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/internal/registry/model_registry_safety_test.go b/internal/registry/model_registry_safety_test.go index be5bf790..e84671c5 100644 --- a/internal/registry/model_registry_safety_test.go +++ b/internal/registry/model_registry_safety_test.go @@ -147,3 +147,31 @@ func TestLookupModelInfoReturnsCloneForStaticDefinitions(t *testing.T) { t.Fatalf("expected static lookup clone, got %+v", second) } } + +func TestLookupModelInfoIncludesClaudeSonnet5(t *testing.T) { + model := LookupModelInfo("claude-sonnet-5") + if model == nil { + t.Fatal("expected Claude Sonnet 5 static model") + } + if model.Type != "claude" { + t.Fatalf("Claude Sonnet 5 type = %q, want claude", model.Type) + } + if model.ContextLength != 1000000 { + t.Fatalf("Claude Sonnet 5 context length = %d, want 1000000", model.ContextLength) + } + if model.MaxCompletionTokens != 128000 { + t.Fatalf("Claude Sonnet 5 max completion tokens = %d, want 128000", model.MaxCompletionTokens) + } + if model.Thinking == nil || !model.Thinking.ZeroAllowed || !model.Thinking.DynamicAllowed || model.Thinking.Min != 0 || model.Thinking.Max != 0 { + t.Fatalf("expected Claude Sonnet 5 dynamic level-only thinking with zero allowed, got %+v", model.Thinking) + } + expectedLevels := []string{"low", "medium", "high", "xhigh", "max"} + if len(model.Thinking.Levels) != len(expectedLevels) { + t.Fatalf("Claude Sonnet 5 thinking levels = %+v, want %+v", model.Thinking.Levels, expectedLevels) + } + for i, level := range expectedLevels { + if model.Thinking.Levels[i] != level { + t.Fatalf("Claude Sonnet 5 thinking levels = %+v, want %+v", model.Thinking.Levels, expectedLevels) + } + } +} diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json index 5725cae2..f61f9fff 100644 --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -119,6 +119,28 @@ ] } }, + { + "id": "claude-sonnet-5", + "object": "model", + "created": 1782777600, + "owned_by": "anthropic", + "type": "claude", + "display_name": "Claude Sonnet 5", + "description": "Anthropic's agentic Sonnet model for coding, tool use, and enterprise workflows", + "context_length": 1000000, + "max_completion_tokens": 128000, + "thinking": { + "zero_allowed": true, + "dynamic_allowed": true, + "levels": [ + "low", + "medium", + "high", + "xhigh", + "max" + ] + } + }, { "id": "claude-fable-5", "object": "model", diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index c0ece152..5ece6fbd 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -244,7 +244,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) - body = normalizeClaudeTemperatureForThinking(body) + body = normalizeClaudeSamplingForThinking(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -434,7 +434,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A // Disable thinking if tool_choice forces tool use (Anthropic API constraint) body = disableThinkingIfToolChoiceForced(body) - body = normalizeClaudeTemperatureForThinking(body) + body = normalizeClaudeSamplingForThinking(body) // Auto-inject cache_control if missing (optimization for ClawdBot/clients without caching support) if countCacheControls(body) == 0 { @@ -865,21 +865,18 @@ func disableThinkingIfToolChoiceForced(body []byte) []byte { return body } -// normalizeClaudeTemperatureForThinking keeps Anthropic message requests valid when -// thinking is enabled. Anthropic rejects temperatures other than 1 when -// thinking.type is enabled/adaptive/auto. -func normalizeClaudeTemperatureForThinking(body []byte) []byte { - if !gjson.GetBytes(body, "temperature").Exists() { - return body - } - +// normalizeClaudeSamplingForThinking keeps Anthropic message requests valid when +// thinking is active. Anthropic rejects non-default sampling while thinking is +// enabled/adaptive/auto. +func normalizeClaudeSamplingForThinking(body []byte) []byte { thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String())) switch thinkingType { case "enabled", "adaptive", "auto": - if temp := gjson.GetBytes(body, "temperature"); temp.Exists() && temp.Type == gjson.Number && temp.Float() == 1 { - return body + if temp := gjson.GetBytes(body, "temperature"); temp.Exists() && (temp.Type != gjson.Number || temp.Float() != 1) { + body, _ = sjson.SetBytes(body, "temperature", 1) } - body, _ = sjson.SetBytes(body, "temperature", 1) + body, _ = sjson.DeleteBytes(body, "top_p") + body, _ = sjson.DeleteBytes(body, "top_k") } return body } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 0d960bcf..c78923df 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -2659,37 +2659,58 @@ func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmi } } -func TestNormalizeClaudeTemperatureForThinking_AdaptiveCoercesToOne(t *testing.T) { +func TestNormalizeClaudeSamplingForThinking_AdaptiveCoercesTemperatureToOne(t *testing.T) { payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`) - out := normalizeClaudeTemperatureForThinking(payload) + out := normalizeClaudeSamplingForThinking(payload) if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { t.Fatalf("temperature = %v, want 1", got) } } -func TestNormalizeClaudeTemperatureForThinking_EnabledCoercesToOne(t *testing.T) { +func TestNormalizeClaudeSamplingForThinking_EnabledCoercesTemperatureToOne(t *testing.T) { payload := []byte(`{"temperature":0.2,"thinking":{"type":"enabled","budget_tokens":2048}}`) - out := normalizeClaudeTemperatureForThinking(payload) + out := normalizeClaudeSamplingForThinking(payload) if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { t.Fatalf("temperature = %v, want 1", got) } } -func TestNormalizeClaudeTemperatureForThinking_NoThinkingLeavesTemperatureAlone(t *testing.T) { - payload := []byte(`{"temperature":0,"messages":[{"role":"user","content":"hi"}]}`) - out := normalizeClaudeTemperatureForThinking(payload) +func TestNormalizeClaudeSamplingForThinking_RemovesTopPAndTopK(t *testing.T) { + payload := []byte(`{"temperature":0.2,"top_p":0.9,"top_k":40,"thinking":{"type":"adaptive"}}`) + out := normalizeClaudeSamplingForThinking(payload) + + if got := gjson.GetBytes(out, "temperature").Float(); got != 1 { + t.Fatalf("temperature = %v, want 1", got) + } + if gjson.GetBytes(out, "top_p").Exists() { + t.Fatalf("top_p should be removed when thinking is active") + } + if gjson.GetBytes(out, "top_k").Exists() { + t.Fatalf("top_k should be removed when thinking is active") + } +} + +func TestNormalizeClaudeSamplingForThinking_NoThinkingLeavesTemperatureAlone(t *testing.T) { + payload := []byte(`{"temperature":0,"top_p":0.9,"top_k":40,"messages":[{"role":"user","content":"hi"}]}`) + out := normalizeClaudeSamplingForThinking(payload) if got := gjson.GetBytes(out, "temperature").Float(); got != 0 { t.Fatalf("temperature = %v, want 0", got) } + if got := gjson.GetBytes(out, "top_p").Float(); got != 0.9 { + t.Fatalf("top_p = %v, want 0.9", got) + } + if got := gjson.GetBytes(out, "top_k").Int(); got != 40 { + t.Fatalf("top_k = %v, want 40", got) + } } -func TestNormalizeClaudeTemperatureForThinking_AfterForcedToolChoiceKeepsOriginalTemperature(t *testing.T) { +func TestNormalizeClaudeSamplingForThinking_AfterForcedToolChoiceKeepsOriginalTemperature(t *testing.T) { payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"},"tool_choice":{"type":"any"}}`) out := disableThinkingIfToolChoiceForced(payload) - out = normalizeClaudeTemperatureForThinking(out) + out = normalizeClaudeSamplingForThinking(out) if gjson.GetBytes(out, "thinking").Exists() { t.Fatalf("thinking should be removed when tool_choice forces tool use")