Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/modelgateway/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (

const anthropicVersion = "2023-06-01"

// These protocol capabilities were observed with the pinned Claude 2.1.303 CLI and, for
// Claude models over Anthropic Messages, the pinned OpenCode 1.19.3 CLI. A CLI upgrade must
// These protocol capabilities were observed with the pinned Claude 2.1.303 CLI, including when
// it is started through Agent SDK 0.3.303 / ACP adapter 0.94.0, and, for Claude models over
// Anthropic Messages, the pinned OpenCode 1.19.3 CLI. A CLI, SDK or adapter upgrade must
// exercise its requests before extending this set.
var claudeBetaHeaders = map[string]bool{
"claude-code-20250219": true,
Expand Down
33 changes: 32 additions & 1 deletion internal/modelgateway/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import (

const pinnedClaudeBetas = "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-tool-changes-2026-07-01,effort-2025-11-24"

// The first request the bundled Claude Code 2.1.303 sent for claude-haiku-5-5 when run directly
// with the adapter's alias and model environment, so it carries no Agent SDK additions.
const pinnedClaudeHaikuBetas = "interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,effort-2025-11-24"

// Each pinned CLI's exact first-request header for a Claude model is forwarded unchanged.
func TestPinnedCLIClaudeModelBetas(t *testing.T) {
for name, betas := range map[string]string{
// Claude Code 2.1.303 run directly with the adapter's alias and model environment, with
// claude-haiku-5-5; the Agent SDK's own additions are covered separately below.
"claude": "interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,effort-2025-11-24",
"claude": pinnedClaudeHaikuBetas,
// OpenCode 1.19.3 through @ai-sdk/anthropic, with claude-haiku-5-5.
"opencode": "thinking-binding-controls-2026-08-01,structured-outputs-2025-11-13",
} {
Expand Down Expand Up @@ -73,6 +77,33 @@ func TestPinnedClaudeAgentSDKCapabilities(t *testing.T) {
}
}

// The documented production combination: the same CLI started through Agent SDK 0.3.303 / ACP
// adapter 0.94.0 for claude-haiku-5-5 declares the direct run's betas plus the SDK's additions.
// The header is composed from those two captured sets rather than captured end to end, so this
// pins that the gateway accepts and forwards the union unchanged, not the client's header order.
func TestPinnedClaudeACPHaikuBetaCombination(t *testing.T) {
betas := pinnedClaudeHaikuBetas + ",structured-outputs-2025-12-15,thinking-display-updates-2026-08-18"
var calls atomic.Int64
service, _ := anthropicFixture(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
if r.Header.Get("Anthropic-Beta") != betas || r.URL.RawQuery != "beta=true" {
t.Error("ACP and haiku betas changed while forwarding")
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, "{\"id\":\"m\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"fixture-model\",\"content\":[{\"type\":\"text\",\"text\":\"{}\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":3,\"output_tokens\":1}}")
}))
request := httptest.NewRequest(http.MethodPost, "/runtime/anthropic/v1/messages?beta=true", strings.NewReader("{\"model\":\"fixture-model\",\"messages\":[],\"max_tokens\":16}"))
request.Header.Set("Authorization", "Bearer "+strings.Repeat("r", 40))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Anthropic-Version", anthropicVersion)
request.Header.Set("Anthropic-Beta", betas)
response := httptest.NewRecorder()
service.ModelHandler(response, request)
if response.Code != 200 || calls.Load() != 1 {
t.Fatalf("ACP haiku betas rejected: status=%d calls=%d", response.Code, calls.Load())
}
}

// anthropicFixture keeps the upstream key separate from the workload's short-lived token.
func anthropicFixture(t *testing.T, handler http.Handler) (*Service, *fakePolicy) {
t.Helper()
Expand Down
Loading