diff --git a/internal/modelgateway/README.en.md b/internal/modelgateway/README.en.md index 5e7b49b3b..f83380f5f 100644 --- a/internal/modelgateway/README.en.md +++ b/internal/modelgateway/README.en.md @@ -25,8 +25,8 @@ SSE events flush continuously; error events inside HTTP200 are sanitized too. Re the upstream and a blocked downstream write. Each watcher has one request owner, cancellation and a join. The Anthropic surface targets Claude Code 2.1.303. POST messages accepts no query or exactly -beta=true; the version is fixed to 2023-06-01 and beta tokens are limited to the nine in -anthropic.go. Duplicate or unknown tokens fail. Other client headers are dropped; the CLI's +beta=true; the version is fixed to 2023-06-01 and beta tokens are limited to the claudeBetaHeaders +set in anthropic.go. Duplicate or unknown tokens fail. Other client headers are dropped; the CLI's browser-access marker is validated and discarded. GET models (no query or limit=1000) rechecks current authorization and returns only the frozen model without an upstream call, key decryption or inference-attempt record. Optional endpoints such as count_tokens remain explicitly unsupported. diff --git a/internal/modelgateway/README.md b/internal/modelgateway/README.md index f9bdd13ef..1e0394ce2 100644 --- a/internal/modelgateway/README.md +++ b/internal/modelgateway/README.md @@ -23,7 +23,7 @@ 每个授权观察任务归属于一个请求,取消后必须等待它退出。 Anthropic 兼容面固定到 Claude Code 2.1.303:推理只接受 POST messages 的空 query 或 -精确 beta=true;协议版本固定 2023-06-01,beta token 仅允许 anthropic.go 列出的九项, +精确 beta=true;协议版本固定 2023-06-01,beta token 仅允许 anthropic.go 中 claudeBetaHeaders 列出的值, 重复或未知值拒绝。其他客户端头不转发;CLI 的 browser-access 标记只验证并丢弃。 GET models(空 query 或 limit=1000)在重新核验当前授权后返回绑定的唯一模型,不请求上游、 不解密 Key、不登记推理 attempt。count_tokens 等可选接口保持未实现并明确拒绝。 diff --git a/internal/modelgateway/anthropic_test.go b/internal/modelgateway/anthropic_test.go index 9e2b883de..a0367fd5e 100644 --- a/internal/modelgateway/anthropic_test.go +++ b/internal/modelgateway/anthropic_test.go @@ -19,7 +19,8 @@ const pinnedClaudeBetas = "claude-code-20250219,interleaved-thinking-2025-05-14, // 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 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", // 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", @@ -48,6 +49,30 @@ func TestPinnedCLIClaudeModelBetas(t *testing.T) { } } +// 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()) + } +} + // 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()