vibe-proxy/backend/internal/runtime/executor/claude_signing_test.go
2026-08-24 00:10:41 +02:00

215 lines
13 KiB
Go

package executor
import (
"bytes"
"strings"
"testing"
"github.com/tidwall/gjson"
)
const claudeCCH21220BaseBody = `{"model":"model-a","messages":[{"role":"user","content":[{"type":"text","text":"x"}]}],"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.220.test; cc_entrypoint=sdk-cli; cch=00000;"},{"type":"text","text":"system-x"}],"tools":[],"metadata":{"user_id":"meta-x"},"max_tokens":1,"thinking":{"type":"adaptive","display":"omitted"},"context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]},"output_config":{"effort":"high"},"stream":true}`
func TestSignAnthropicMessagesBody_ClaudeCode21220KnownVectors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
body string
want string
}{
{name: "base", body: claudeCCH21220BaseBody, want: "7ee87"},
{name: "model value ignored", body: strings.Replace(claudeCCH21220BaseBody, `"model":"model-a"`, `"model":"model-b"`, 1), want: "7ee87"},
{name: "max tokens ignored", body: strings.Replace(claudeCCH21220BaseBody, `"max_tokens":1`, `"max_tokens":2`, 1), want: "7ee87"},
{name: "message changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"text":"x"`, `"text":"y"`, 1), want: "b9cc8"},
{name: "system changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"system-x"`, `"system-y"`, 1), want: "a30d3"},
{name: "metadata changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"user_id":"meta-x"`, `"user_id":"meta-y"`, 1), want: "7a89d"},
{name: "thinking changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"thinking":{"type":"adaptive","display":"omitted"}`, `"thinking":{"type":"disabled"}`, 1), want: "7205c"},
{name: "context changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}`, `"context_management":{"edits":[]}`, 1), want: "05073"},
{name: "effort changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"effort":"high"`, `"effort":"low"`, 1), want: "12366"},
{name: "stream changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"stream":true`, `"stream":false`, 1), want: "60400"},
{name: "tool changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"tools":[]`, `"tools":[{"name":"t","description":"d","input_schema":{"type":"object"}}]`, 1), want: "3d78d"},
{name: "extra field changes hash", body: strings.Replace(claudeCCH21220BaseBody, `"stream":true}`, `"stream":true,"extra_top":"extra"}`, 1), want: "2d622"},
{
name: "field order remains significant",
body: `{"stream":true,"output_config":{"effort":"high"},"context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]},"thinking":{"type":"adaptive","display":"omitted"},"max_tokens":1,"metadata":{"user_id":"meta-x"},"tools":[],"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.220.test; cc_entrypoint=sdk-cli; cch=00000;"},{"type":"text","text":"system-x"}],"messages":[{"role":"user","content":[{"type":"text","text":"x"}]}],"model":"model-a"}`,
want: "e5b6c",
},
{name: "nested model value ignored", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","model":"a"}`, 1), want: "0601b"},
{name: "nested max tokens member omitted", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","max_tokens":2}`, 1), want: "7ee87"},
{name: "top level fallbacks member omitted", body: strings.Replace(claudeCCH21220BaseBody, `"stream":true}`, `"stream":true,"fallbacks":[{"model":"fallback-a"}]}`, 1), want: "7ee87"},
{name: "nested fallbacks member omitted", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","fallbacks":[{"model":"nested-a"}]}`, 1), want: "7ee87"},
{name: "top level fallback credit token omitted", body: strings.Replace(claudeCCH21220BaseBody, `"stream":true}`, `"stream":true,"fallback_credit_token":"a"}`, 1), want: "7ee87"},
{name: "nested fallback credit token omitted", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","fallback_credit_token":"a"}`, 1), want: "7ee87"},
{name: "trailing dispatch run keeps native comma", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","max_tokens":999,"fallbacks":[{"model":"fallback-model"}]}`, 1), want: "4589b"},
{name: "model before trailing dispatch run", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","model":"nested-model","max_tokens":999,"fallbacks":[{"model":"fallback-model"}],"fallback_credit_token":"not-a-real-token"}`, 1), want: "2d312"},
{name: "model splits dispatch runs", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","max_tokens":999,"model":"nested-model","fallbacks":[{"model":"fallback-model"}]}`, 1), want: "0601b"},
{name: "ordinary nested member remains", body: strings.Replace(claudeCCH21220BaseBody, `"metadata":{"user_id":"meta-x"}`, `"metadata":{"user_id":"meta-x","plain":"a"}`, 1), want: "8d74c"},
{name: "billing block only", body: `{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.220.test; cc_entrypoint=sdk-cli; cch=00000;"}]}`, want: "f2edb"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
signed, err := signAnthropicMessagesBody([]byte(tt.body))
if err != nil {
t.Fatalf("signAnthropicMessagesBody() error = %v", err)
}
if got := claudeCCHFromBody(t, signed); got != tt.want {
t.Fatalf("cch = %q, want %q\nbody: %s", got, tt.want, signed)
}
})
}
}
func TestSignAnthropicMessagesBody_PreservesFinalSerializedBytes(t *testing.T) {
t.Parallel()
literal := "keep literal cch=00000; in the message"
body := []byte(strings.Replace(claudeCCH21220BaseBody, `"text":"x"`, `"text":"`+literal+`"`, 1))
signed, err := signAnthropicMessagesBody(body)
if err != nil {
t.Fatalf("signAnthropicMessagesBody() error = %v", err)
}
if got := gjson.GetBytes(signed, "messages.0.content.0.text").String(); got != literal {
t.Fatalf("message text = %q, want %q", got, literal)
}
cchOffset, ok := claudeBillingCCHDigitsOffset(signed)
if !ok {
t.Fatal("signed billing CCH not found")
}
unsigned := bytes.Clone(signed)
copy(unsigned[cchOffset:cchOffset+claudeCCHLength], "00000")
if !bytes.Equal(unsigned, body) {
t.Fatalf("signing changed bytes outside CCH\n got: %s\nwant: %s", unsigned, body)
}
}
func TestFinalizeAnthropicMessagesBodyCCH_InsertsMissingPlaceholder(t *testing.T) {
t.Parallel()
body := []byte(strings.Replace(claudeCCH21220BaseBody, " cch=00000;", "", 1))
signed, err := finalizeAnthropicMessagesBodyCCH(body, "")
if err != nil {
t.Fatalf("finalizeAnthropicMessagesBodyCCH() error = %v", err)
}
if got := claudeCCHFromBody(t, signed); got != "7ee87" {
t.Fatalf("cch = %q, want %q", got, "7ee87")
}
billing := gjson.GetBytes(signed, "system.0.text").String()
if !strings.Contains(billing, "cc_entrypoint=sdk-cli; cch=7ee87;") {
t.Fatalf("billing header = %q, want CCH after entrypoint", billing)
}
}
func TestFinalizeAnthropicMessagesBodyCCH_AddsMissingBillingBlock(t *testing.T) {
t.Parallel()
body := []byte(`{"model":"claude-opus-4-6","system":"keep this system text","messages":[{"role":"user","content":"hello"}],"max_tokens":128}`)
fallback := "x-anthropic-billing-header: cc_version=2.1.220.test; cc_entrypoint=sdk-cli; cch=00000;"
signed, err := finalizeAnthropicMessagesBodyCCH(body, fallback)
if err != nil {
t.Fatalf("finalizeAnthropicMessagesBodyCCH() error = %v", err)
}
if got := gjson.GetBytes(signed, "system.0.text").String(); !strings.HasPrefix(got, "x-anthropic-billing-header:") {
t.Fatalf("system.0.text = %q, want billing block", got)
}
if got := gjson.GetBytes(signed, "system.1.text").String(); got != "keep this system text" {
t.Fatalf("system.1.text = %q, want preserved system text", got)
}
if _, ok := claudeBillingCCHDigitsOffset(signed); !ok {
t.Fatalf("generated billing block is missing CCH: %s", signed)
}
}
func TestClaudeCCHSigningEnabled(t *testing.T) {
t.Parallel()
const (
anthropicOrigin = "https://api.anthropic.com/v1/messages?beta=true"
gatewayOrigin = "https://gateway.example/v1/messages?beta=true"
)
tests := []struct {
name string
apiKey string
kind claudeCCHUpstreamKind
cliFingerprint bool
origin string
want bool
}{
{name: "official API key default", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, origin: anthropicOrigin, want: false},
{name: "official API key opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: anthropicOrigin, want: true},
{name: "Kimi API key default", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, origin: "https://api.kimi.com/v1/messages", want: false},
// Native emits cch only for firstParty on api.anthropic.com or for vertex, so an
// opted-in key on any other gateway keeps a cache-stable billing header.
{name: "Kimi API key opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: "https://api.kimi.com/v1/messages", want: false},
{name: "gateway API key opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: gatewayOrigin, want: false},
{name: "anthropic host over http opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: "http://api.anthropic.com/v1/messages", want: false},
{name: "anthropic host explicit 443 opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: "https://api.anthropic.com:443/v1/messages", want: true},
{name: "anthropic lookalike host opt-in", apiKey: "key-123", kind: claudeCCHUpstreamAnthropic, cliFingerprint: true, origin: "https://api.anthropic.com.evil.example/v1/messages", want: false},
// A real OAuth credential signs on every upstream: CPA is the hop that has to
// regenerate the cch a downstream Claude Code could not produce.
{name: "Claude OAuth", apiKey: "sk-ant-oat-custom", kind: claudeCCHUpstreamAnthropic, origin: anthropicOrigin, want: true},
{name: "Claude OAuth custom gateway", apiKey: "sk-ant-oat-custom", kind: claudeCCHUpstreamAnthropic, origin: gatewayOrigin, want: true},
{name: "other provider Claude OAuth", apiKey: "sk-ant-oat-other", kind: claudeCCHUpstreamOther, origin: gatewayOrigin, want: true},
{name: "Vertex provider API key", apiKey: "key-123", kind: claudeCCHUpstreamVertex, origin: "https://us-east5-aiplatform.googleapis.com/v1/projects/p/locations/l/publishers/anthropic/models/m:streamRawPredict", want: true},
{name: "other provider API key", apiKey: "key-123", kind: claudeCCHUpstreamOther, origin: gatewayOrigin, want: false},
{name: "other provider API key opt-in", apiKey: "key-123", kind: claudeCCHUpstreamOther, cliFingerprint: true, origin: gatewayOrigin, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := claudeCCHSigningEnabled(tt.apiKey, tt.kind, tt.cliFingerprint, tt.origin); got != tt.want {
t.Fatalf("claudeCCHSigningEnabled() = %t, want %t", got, tt.want)
}
})
}
}
func TestNormalizeClaudeCCHInput_PreservesRawJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
body string
want string
}{
{name: "model string becomes empty", body: `{"model":"claude","keep":1}`, want: `{"model":"","keep":1}`},
{name: "excluded first member", body: `{"max_tokens":1,"keep":2}`, want: `{"keep":2}`},
{name: "excluded middle member", body: `{"keep":1,"fallbacks":[{"model":"x"}],"tail":2}`, want: `{"keep":1,"tail":2}`},
{name: "excluded last member", body: `{"keep":1,"fallback_credit_token":"secret"}`, want: `{"keep":1}`},
{name: "all members excluded", body: `{"max_tokens":1,"fallbacks":[],"fallback_credit_token":"secret"}`, want: `{}`},
{name: "adjacent excluded members", body: `{"keep":1,"max_tokens":1,"fallbacks":[],"tail":2}`, want: `{"keep":1,"tail":2}`},
{name: "native trailing dispatch run", body: `{"keep":1,"max_tokens":1,"fallbacks":[]}`, want: `{"keep":1,}`},
{name: "nested fields", body: `{"outer":{"model":"x","max_tokens":1,"keep":"y"}}`, want: `{"outer":{"model":"","keep":"y"}}`},
{name: "escaped key text stays inside string", body: `{"text":"literal \"model\":\"x\" and \"max_tokens\":1"}`, want: `{"text":"literal \"model\":\"x\" and \"max_tokens\":1"}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := normalizeClaudeCCHInput([]byte(tt.body))
if err != nil {
t.Fatalf("normalizeClaudeCCHInput() error = %v", err)
}
if string(got) != tt.want {
t.Fatalf("normalized body = %s, want %s", got, tt.want)
}
})
}
}
func claudeCCHFromBody(t *testing.T, body []byte) string {
t.Helper()
offset, ok := claudeBillingCCHDigitsOffset(body)
if !ok {
t.Fatalf("billing CCH not found in body: %s", body)
}
return string(body[offset : offset+claudeCCHLength])
}