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: 4 additions & 1 deletion docs/models/model-gateway.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 3 additions & 1 deletion docs/models/model-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。它们同样在受限白名单中。
升级版本仍须先验证实际协议面,不能按客户端传入内容自动放开。

| 方法与路径 | 查询 | 行为 |
Expand Down
8 changes: 6 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.
// 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,
Expand All @@ -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.
Expand Down
50 changes: 29 additions & 21 deletions internal/modelgateway/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
})
}
}

Expand Down
Loading