From 1214b13757528c4f16127b9a5d9040ed3d53f78b Mon Sep 17 00:00:00 2001 From: "Ruihao (Ray) Zhang" Date: Thu, 22 Oct 2026 01:48:31 +0000 Subject: [PATCH] test: pin the ACP and claude-haiku-5-5 beta combination in the gateway The docs describe the pinned CLI, started through Agent SDK 0.3.303 and ACP adapter 0.94.0, declaring the direct run's betas plus the SDK's two additions. No test covered that union, only each half. Also record the SDK and adapter in the allowlist's observation note. --- internal/modelgateway/anthropic.go | 5 ++-- internal/modelgateway/anthropic_test.go | 33 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/internal/modelgateway/anthropic.go b/internal/modelgateway/anthropic.go index 06e9f0c51..dfa634380 100644 --- a/internal/modelgateway/anthropic.go +++ b/internal/modelgateway/anthropic.go @@ -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, diff --git a/internal/modelgateway/anthropic_test.go b/internal/modelgateway/anthropic_test.go index a0367fd5e..a1252487e 100644 --- a/internal/modelgateway/anthropic_test.go +++ b/internal/modelgateway/anthropic_test.go @@ -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", } { @@ -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()