diff --git a/docs/models/model-gateway.en.md b/docs/models/model-gateway.en.md index 8ed49f50..1c0c71ea 100644 --- a/docs/models/model-gateway.en.md +++ b/docs/models/model-gateway.en.md @@ -81,7 +81,10 @@ browser-access marker never reach the supplier. The same pinned CLI also declares `structured-outputs-2025-12-15` and `thinking-display-updates-2026-08-18` when queries are created through Agent SDK 0.3.303 / -ACP adapter 0.94.0. Both are included in the bounded whitelist. An upgrade must verify its +ACP adapter 0.94.0, and `per-turn-control-2026-07-01` on its first request for +`claude-haiku-5-5`. The pinned OpenCode 1.19.3 declares `thinking-binding-controls-2026-08-01` +and `structured-outputs-2025-11-13` when it requests a Claude model through +`@ai-sdk/anthropic`. All of these are included in the bounded whitelist. An upgrade must verify its observed protocol surface before extending the policy. | Method and path | Query | Behavior | diff --git a/docs/models/model-gateway.md b/docs/models/model-gateway.md index 21391b54..3462134c 100644 --- a/docs/models/model-gateway.md +++ b/docs/models/model-gateway.md @@ -53,7 +53,9 @@ Claude Code 2.1.303 的 Messages 兼容面如下;允许的 beta 标记由 客户端诊断头、会话标识及 browser-access 标记不会转发到供应商。 同一固定 CLI 通过 Agent SDK 0.3.303 / ACP adapter 0.94.0 创建查询时还会声明 -`structured-outputs-2025-12-15` 与 `thinking-display-updates-2026-08-18`;它们同样在受限白名单中。 +`structured-outputs-2025-12-15` 与 `thinking-display-updates-2026-08-18`,对 `claude-haiku-5-5` 的首个请求还会带 +`per-turn-control-2026-07-01`。固定的 OpenCode 1.19.3 经 `@ai-sdk/anthropic` 请求 Claude 模型时声明 +`thinking-binding-controls-2026-08-01` 与 `structured-outputs-2025-11-13`。它们同样在受限白名单中。 升级版本仍须先验证实际协议面,不能按客户端传入内容自动放开。 | 方法与路径 | 查询 | 行为 | diff --git a/internal/modelgateway/anthropic.go b/internal/modelgateway/anthropic.go index 456299ea..06e9f0c5 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. -// A CLI upgrade must exercise its requests before extending this set. +// 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 +// exercise its requests before extending this set. var claudeBetaHeaders = map[string]bool{ "claude-code-20250219": true, "interleaved-thinking-2025-05-14": true, @@ -22,6 +23,9 @@ var claudeBetaHeaders = map[string]bool{ "effort-2025-11-24": true, "structured-outputs-2025-12-15": true, "thinking-display-updates-2026-08-18": true, + "per-turn-control-2026-07-01": true, + "thinking-binding-controls-2026-08-01": true, + "structured-outputs-2025-11-13": true, } // runtimeModelRoute accepts only inference and the frozen binding's local model listing. diff --git a/internal/modelgateway/anthropic_test.go b/internal/modelgateway/anthropic_test.go index 87c4a902..9e2b883d 100644 --- a/internal/modelgateway/anthropic_test.go +++ b/internal/modelgateway/anthropic_test.go @@ -16,27 +16,35 @@ 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" -// Agent SDK query creation enables these additional capabilities on the same pinned native CLI. -func TestPinnedClaudeAgentSDKCapabilities(t *testing.T) { - betas := pinnedClaudeBetas + ",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("SDK capabilities 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("pinned SDK request rejected: status=%d calls=%d", response.Code, calls.Load()) +// 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 through the pinned ACP adapter, with claude-haiku-5-5. + "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", + // 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", + } { + t.Run(name, func(t *testing.T) { + 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 { + t.Error("pinned 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", strings.NewReader("{\"model\":\"fixture-model\",\"messages\":[],\"max_tokens\":16}")) + request.Header.Set("X-Api-Key", 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("pinned %s betas rejected: status=%d calls=%d", name, response.Code, calls.Load()) + } + }) } }