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

6467 lines
265 KiB
Go

package executor
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/andybalholm/brotli"
"github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd"
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func resetClaudeDeviceProfileCache() {
helps.ResetClaudeDeviceProfileCache()
}
func claudeOAuthTestMetadata() map[string]any {
return map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: []string{
"0000000000000000000000000000000000000000000000000000000000000000",
},
}
}
func malformedClaudeTreeSignatureForClaudeExecutorTest() string {
return base64.StdEncoding.EncodeToString([]byte{0x12, 0xFF, 0xFE, 0xFD})
}
func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request {
t.Helper()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ginCtx, _ := gin.CreateTestContext(recorder)
ginReq := httptest.NewRequest(http.MethodPost, "http://localhost/v1/messages", nil)
ginReq.Header = incoming.Clone()
ginCtx.Request = ginReq
req := httptest.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages", nil)
return req.WithContext(context.WithValue(req.Context(), "gin", ginCtx))
}
func assertClaudeFingerprint(t *testing.T, headers http.Header, userAgent, pkgVersion, runtimeVersion, osName, arch string) {
t.Helper()
if got := headers.Get("User-Agent"); got != userAgent {
t.Fatalf("User-Agent = %q, want %q", got, userAgent)
}
if got := headers.Get("X-Stainless-Package-Version"); got != pkgVersion {
t.Fatalf("X-Stainless-Package-Version = %q, want %q", got, pkgVersion)
}
if got := headers.Get("X-Stainless-Runtime-Version"); got != runtimeVersion {
t.Fatalf("X-Stainless-Runtime-Version = %q, want %q", got, runtimeVersion)
}
if got := headers.Get("X-Stainless-Os"); got != osName {
t.Fatalf("X-Stainless-Os = %q, want %q", got, osName)
}
if got := headers.Get("X-Stainless-Arch"); got != arch {
t.Fatalf("X-Stainless-Arch = %q, want %q", got, arch)
}
}
func TestApplyClaudeHeaders_FastModeBetaIsConditional(t *testing.T) {
baseline := claudeCodeCLIBetas([]byte(`{"model":"claude-opus-5"}`), nil, false)
betasWithoutFastMode := baseline
betasWithFastMode := baseline + "," + claudeFastModeBeta
tests := []struct {
name string
body string
want string
}{
{
name: "omitted speed excludes fast mode beta",
body: `{"model":"claude-opus-5"}`,
want: betasWithoutFastMode,
},
{
name: "fast speed appends fast mode beta",
body: `{"model":"claude-opus-5","speed":"fast"}`,
want: betasWithFastMode,
},
{
name: "explicit body beta appends fast mode beta",
body: `{"model":"claude-opus-5","betas":["fast-mode-2026-02-01"]}`,
want: betasWithFastMode,
},
}
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-fast-mode-beta", "cloak_mode": "always"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
extraBetas, body := extractAndRemoveBetas([]byte(tt.body))
req := newClaudeHeaderTestRequest(t, nil)
if errApply := applyClaudeHeaders(req, auth, "key-fast-mode-beta", false, extraBetas, body, nil, nil, false); errApply != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errApply)
}
if got := req.Header.Get("Anthropic-Beta"); got != tt.want {
t.Fatalf("Anthropic-Beta = %q, want %q", got, tt.want)
}
})
}
}
func assertClaudeCredentialIdentity(t *testing.T, body []byte, headers http.Header, deviceIDs []string, accountUUID string) {
t.Helper()
userID := gjson.GetBytes(body, "metadata.user_id").String()
deviceID := gjson.Get(userID, "device_id").String()
inPool := false
for _, candidate := range deviceIDs {
if deviceID == candidate {
inPool = true
break
}
}
if !inPool {
t.Fatalf("device_id = %q, want selected credential device pool entry", deviceID)
}
if got := gjson.Get(userID, "account_uuid").String(); got != accountUUID {
t.Fatalf("account_uuid = %q, want selected credential account %q", got, accountUUID)
}
sessionID := gjson.Get(userID, "session_id").String()
if sessionID == "" || sessionID != headers.Get("X-Claude-Code-Session-Id") {
t.Fatalf("metadata session_id = %q, header session ID = %q", sessionID, headers.Get("X-Claude-Code-Session-Id"))
}
resigned, errResign := finalizeAnthropicMessagesBodyCCH(body, "")
if errResign != nil {
t.Fatalf("re-finalize Claude CCH: %v", errResign)
}
if !bytes.Equal(resigned, body) {
t.Fatal("Claude CCH was calculated before final credential metadata rewrite")
}
}
// assertClaudeCountTokensIdentity pins the count_tokens shape captured from real
// Claude Code 2.1.220: the endpoint carries no metadata whatsoever. Anthropic
// rejects the field there with "metadata: Extra inputs are not permitted", so the
// credential identity travels only on the header and on the Messages endpoint.
func assertClaudeCountTokensIdentity(t *testing.T, body []byte, headers http.Header) {
t.Helper()
if got := gjson.GetBytes(body, "metadata"); got.Exists() {
t.Fatalf("count_tokens metadata = %s, want it absent", got.Raw)
}
if got := headers.Get("X-Claude-Code-Session-Id"); got == "" {
t.Fatal("count_tokens is missing X-Claude-Code-Session-Id")
}
resigned, errResign := finalizeAnthropicMessagesBodyCCH(body, "")
if errResign != nil {
t.Fatalf("re-finalize Claude CCH: %v", errResign)
}
if !bytes.Equal(resigned, body) {
t.Fatal("count_tokens CCH was calculated before the final body rewrite")
}
}
func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.70 (external, cli)",
PackageVersion: "0.80.0",
RuntimeVersion: "v24.5.0",
OS: "MacOS",
Arch: "arm64",
Timeout: "900",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-baseline",
Attributes: map[string]string{
"api_key": "key-baseline",
"cloak_mode": "always",
"header:User-Agent": "evil-client/9.9",
"header:X-Stainless-Os": "Linux",
"header:X-Stainless-Arch": "x64",
"header:X-Stainless-Package-Version": "9.9.9",
},
}
incoming := http.Header{
"User-Agent": []string{"curl/8.7.1"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
}
req := newClaudeHeaderTestRequest(t, incoming)
applyClaudeHeaders(req, auth, "key-baseline", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, req.Header, "evil-client/9.9", "9.9.9", "v24.5.0", "Linux", "x64")
if got := req.Header.Get("X-Stainless-Timeout"); got != "900" {
t.Fatalf("X-Stainless-Timeout = %q, want %q", got, "900")
}
}
func TestApplyClaudeHeaders_RejectsUnmeasuredClaudeCLIFingerprints(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-upgrade",
Attributes: map[string]string{
"api_key": "key-upgrade",
"cloak_mode": "always",
},
}
firstReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.74.0"},
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(firstReq, auth, "key-upgrade", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, firstReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"lobe-chat/1.0"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-upgrade", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
higherReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.63 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.75.0"},
"X-Stainless-Runtime-Version": []string{"v24.4.0"},
"X-Stainless-Os": []string{"MacOS"},
"X-Stainless-Arch": []string{"arm64"},
})
applyClaudeHeaders(higherReq, auth, "key-upgrade", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, higherReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
lowerReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.61 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.73.0"},
"X-Stainless-Runtime-Version": []string{"v24.2.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(lowerReq, auth, "key-upgrade", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
}
func TestApplyClaudeHeaders_DoesNotDowngradeConfiguredBaselineOnFirstClaudeClient(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.70 (external, cli)",
PackageVersion: "0.80.0",
RuntimeVersion: "v24.5.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-baseline-floor",
Attributes: map[string]string{
"api_key": "key-baseline-floor",
},
}
olderClaudeReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.74.0"},
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(olderClaudeReq, auth, "key-baseline-floor", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, olderClaudeReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
newerClaudeReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.71 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.81.0"},
"X-Stainless-Runtime-Version": []string{"v24.6.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(newerClaudeReq, auth, "key-baseline-floor", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
}
func TestApplyClaudeHeaders_UpgradesCachedSoftwareFingerprintWhenBaselineAdvances(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
oldCfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.70 (external, cli)",
PackageVersion: "0.80.0",
RuntimeVersion: "v24.5.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
newCfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.77 (external, cli)",
PackageVersion: "0.87.0",
RuntimeVersion: "v24.8.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-baseline-reload",
Attributes: map[string]string{
"api_key": "key-baseline-reload",
"cloak_mode": "always",
},
}
officialReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.71 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.81.0"},
"X-Stainless-Runtime-Version": []string{"v24.6.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(officialReq, auth, "key-baseline-reload", false, nil, nil, oldCfg, nil, true)
assertClaudeFingerprint(t, officialReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-baseline-reload", false, nil, nil, newCfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.77 (external, cli)", "0.87.0", "v24.8.0", "MacOS", "arm64")
}
func TestApplyClaudeHeaders_LearnsOfficialFingerprintAfterCustomBaselineFallback(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "my-gateway/1.0",
PackageVersion: "custom-pkg",
RuntimeVersion: "custom-runtime",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-custom-baseline-learning",
Attributes: map[string]string{
"api_key": "key-custom-baseline-learning",
"cloak_mode": "always",
},
}
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-custom-baseline-learning", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "my-gateway/1.0", "custom-pkg", "custom-runtime", "MacOS", "arm64")
officialReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.77 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.87.0"},
"X-Stainless-Runtime-Version": []string{"v24.8.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(officialReq, auth, "key-custom-baseline-learning", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, officialReq.Header, "my-gateway/1.0", "custom-pkg", "custom-runtime", "MacOS", "arm64")
postLearningThirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(postLearningThirdPartyReq, auth, "key-custom-baseline-learning", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, postLearningThirdPartyReq.Header, "my-gateway/1.0", "custom-pkg", "custom-runtime", "MacOS", "arm64")
}
func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-racy-upgrade",
Attributes: map[string]string{
"api_key": "key-racy-upgrade",
},
}
lowPaused := make(chan struct{})
releaseLow := make(chan struct{})
var pauseOnce sync.Once
var releaseOnce sync.Once
helps.ClaudeDeviceProfileBeforeCandidateStore = func(candidate helps.ClaudeDeviceProfile) {
if candidate.UserAgent != "claude-cli/2.1.60 (external, cli)" {
return
}
pause := false
pauseOnce.Do(func() {
pause = true
close(lowPaused)
})
if pause {
<-releaseLow
}
}
t.Cleanup(func() {
helps.ClaudeDeviceProfileBeforeCandidateStore = nil
releaseOnce.Do(func() { close(releaseLow) })
})
lowResultCh := make(chan helps.ClaudeDeviceProfile, 1)
go func() {
lowResultCh <- helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"claude-cli/2.1.60 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.70.0"},
"X-Stainless-Runtime-Version": []string{"v22.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
}, cfg)
}()
select {
case <-lowPaused:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for lower candidate to pause before storing")
}
highResult := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"claude-cli/2.1.60 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.70.0"},
"X-Stainless-Runtime-Version": []string{"v22.0.0"},
"X-Stainless-Os": []string{"MacOS"},
"X-Stainless-Arch": []string{"arm64"},
}, cfg)
releaseOnce.Do(func() { close(releaseLow) })
select {
case lowResult := <-lowResultCh:
if lowResult.UserAgent != "claude-cli/2.1.60 (external, cli)" {
t.Fatalf("lowResult.UserAgent = %q, want %q", lowResult.UserAgent, "claude-cli/2.1.60 (external, cli)")
}
if lowResult.PackageVersion != "0.70.0" {
t.Fatalf("lowResult.PackageVersion = %q, want %q", lowResult.PackageVersion, "0.70.0")
}
if lowResult.OS != "MacOS" || lowResult.Arch != "arm64" {
t.Fatalf("lowResult platform = %s/%s, want %s/%s", lowResult.OS, lowResult.Arch, "MacOS", "arm64")
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for lower candidate result")
}
if highResult.UserAgent != "claude-cli/2.1.60 (external, cli)" {
t.Fatalf("highResult.UserAgent = %q, want %q", highResult.UserAgent, "claude-cli/2.1.60 (external, cli)")
}
if highResult.OS != "MacOS" || highResult.Arch != "arm64" {
t.Fatalf("highResult platform = %s/%s, want %s/%s", highResult.OS, highResult.Arch, "MacOS", "arm64")
}
cached := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
"User-Agent": []string{"curl/8.7.1"},
}, cfg)
if cached.UserAgent != "claude-cli/2.1.60 (external, cli)" {
t.Fatalf("cached.UserAgent = %q, want %q", cached.UserAgent, "claude-cli/2.1.60 (external, cli)")
}
if cached.PackageVersion != "0.70.0" {
t.Fatalf("cached.PackageVersion = %q, want %q", cached.PackageVersion, "0.70.0")
}
if cached.OS != "MacOS" || cached.Arch != "arm64" {
t.Fatalf("cached platform = %s/%s, want %s/%s", cached.OS, cached.Arch, "MacOS", "arm64")
}
}
func TestApplyClaudeHeaders_ThirdPartyBaselineThenOfficialUpgradeKeepsPinnedPlatform(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := true
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.70 (external, cli)",
PackageVersion: "0.80.0",
RuntimeVersion: "v24.5.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-third-party-then-official",
Attributes: map[string]string{
"api_key": "key-third-party-then-official",
"cloak_mode": "always",
},
}
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-third-party-then-official", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
officialReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.77 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.87.0"},
"X-Stainless-Runtime-Version": []string{"v24.8.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(officialReq, auth, "key-third-party-then-official", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, officialReq.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
}
func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := false
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
OS: "MacOS",
Arch: "arm64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-disable-stability",
Attributes: map[string]string{
"api_key": "key-disable-stability",
"cloak_mode": "always",
},
}
firstReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.74.0"},
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(firstReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, firstReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"lobe-chat/1.0"},
"X-Stainless-Package-Version": []string{"0.10.0"},
"X-Stainless-Runtime-Version": []string{"v18.0.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(thirdPartyReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", helps.MapStainlessOS(), helps.MapStainlessArch())
lowerReq := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.61 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.73.0"},
"X-Stainless-Runtime-Version": []string{"v24.2.0"},
"X-Stainless-Os": []string{"Windows"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(lowerReq, auth, "key-disable-stability", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, lowerReq.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "MacOS", "arm64")
}
func TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := false
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-legacy-ua-override",
Attributes: map[string]string{
"api_key": "key-legacy-ua-override",
"header:User-Agent": "config-ua/1.0",
},
}
req := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
"X-Stainless-Package-Version": []string{"0.74.0"},
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
"X-Stainless-Os": []string{"Linux"},
"X-Stainless-Arch": []string{"x64"},
})
applyClaudeHeaders(req, auth, "key-legacy-ua-override", false, nil, nil, cfg, nil, true)
assertClaudeFingerprint(t, req.Header, "config-ua/1.0", "0.70.0", "v22.0.0", helps.MapStainlessOS(), helps.MapStainlessArch())
}
func TestApplyClaudeHeaders_LegacyThirdPartyUsesStableConfiguredOSArch(t *testing.T) {
resetClaudeDeviceProfileCache()
stabilize := false
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
OS: "Windows",
Arch: "x64",
StabilizeDeviceProfile: &stabilize,
},
}
auth := &cliproxyauth.Auth{
ID: "auth-legacy-runtime-os-arch",
Attributes: map[string]string{
"api_key": "key-legacy-runtime-os-arch",
"cloak_mode": "always",
},
}
req := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
})
applyClaudeHeaders(req, auth, "key-legacy-runtime-os-arch", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, req.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "Windows", "x64")
}
func TestApplyClaudeHeaders_UnsetStabilizationUsesStableConfiguredOSArch(t *testing.T) {
resetClaudeDeviceProfileCache()
cfg := &config.Config{
ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
UserAgent: "claude-cli/2.1.60 (external, cli)",
PackageVersion: "0.70.0",
RuntimeVersion: "v22.0.0",
OS: "Linux",
Arch: "x64",
},
}
auth := &cliproxyauth.Auth{
ID: "auth-unset-runtime-os-arch",
Attributes: map[string]string{
"api_key": "key-unset-runtime-os-arch",
"cloak_mode": "always",
},
}
req := newClaudeHeaderTestRequest(t, http.Header{
"User-Agent": []string{"curl/8.7.1"},
})
applyClaudeHeaders(req, auth, "key-unset-runtime-os-arch", false, nil, nil, cfg, nil, false)
assertClaudeFingerprint(t, req.Header, "claude-cli/2.1.60 (external, cli)", "0.70.0", "v22.0.0", "Linux", "x64")
}
func TestApplyClaudeHeaders_UsesOAuthAuthorizationAndBrowserFingerprint(t *testing.T) {
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "sk-ant-oat-header-test"}}
req := newClaudeHeaderTestRequest(t, nil)
if errHeaders := applyClaudeHeaders(req, auth, "sk-ant-oat-header-test", false, nil, nil, &config.Config{}, nil, false, "11111111-2222-4333-8444-555555555555"); errHeaders != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errHeaders)
}
if got := req.Header.Get("Authorization"); got != "Bearer sk-ant-oat-header-test" {
t.Fatalf("Authorization = %q, want OAuth bearer", got)
}
if got := req.Header.Get("x-api-key"); got != "" {
t.Fatalf("x-api-key = %q, want empty for OAuth", got)
}
if got := req.Header.Get("Anthropic-Dangerous-Direct-Browser-Access"); got != "true" {
t.Fatalf("Anthropic-Dangerous-Direct-Browser-Access = %q, want true", got)
}
if got := req.Header.Get("Anthropic-Beta"); !strings.Contains(got, "oauth-2025-04-20") {
t.Fatalf("Anthropic-Beta = %q, want OAuth beta", got)
}
}
func TestApplyClaudeHeaders_EmptyAPIKey_OmitsAuthHeaders(t *testing.T) {
auth := &cliproxyauth.Auth{
Provider: "claude",
Attributes: map[string]string{
"auth_kind": "apikey",
"base_url": "https://custom-claude.example.com",
"header:Custom-Token": "custom-secret",
},
}
req, err := http.NewRequest(http.MethodPost, "https://custom-claude.example.com/v1/messages", nil)
if err != nil {
t.Fatalf("NewRequest() error = %v", err)
}
// Preset preexisting client headers to ensure they get stripped for empty API key
req.Header.Set("Authorization", "Bearer preexisting-bearer")
req.Header.Set("x-api-key", "preexisting-key")
if errHeaders := applyClaudeHeaders(req, auth, "", false, nil, nil, &config.Config{}, nil, false); errHeaders != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errHeaders)
}
if got := req.Header.Get("Authorization"); got != "" {
t.Fatalf("Authorization = %q, want empty for empty API key", got)
}
if got := req.Header.Get("x-api-key"); got != "" {
t.Fatalf("x-api-key = %q, want empty for empty API key", got)
}
if got := req.Header.Get("Custom-Token"); got != "custom-secret" {
t.Fatalf("Custom-Token = %q, want custom-secret", got)
}
// Also verify PrepareRequest
req2, _ := http.NewRequest(http.MethodPost, "https://custom-claude.example.com/v1/messages", nil)
req2.Header.Set("Authorization", "Bearer preexisting-bearer")
req2.Header.Set("x-api-key", "preexisting-key")
exec := &ClaudeExecutor{}
if errPrep := exec.PrepareRequest(req2, auth); errPrep != nil {
t.Fatalf("PrepareRequest() error = %v", errPrep)
}
if got := req2.Header.Get("Authorization"); got != "" {
t.Fatalf("PrepareRequest Authorization = %q, want empty", got)
}
if got := req2.Header.Get("x-api-key"); got != "" {
t.Fatalf("PrepareRequest x-api-key = %q, want empty", got)
}
if got := req2.Header.Get("Custom-Token"); got != "custom-secret" {
t.Fatalf("PrepareRequest Custom-Token = %q, want custom-secret", got)
}
}
func TestClaudeExecutor_NonClaudeRequestUsesClaudeCode220CLIFingerprint(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-sdk-fingerprint",
"base_url": server.URL,
"cloak_mode": "always",
}}
payload := []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":[{"type":"text","text":"x"}]}]}`)
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
assertClaudeFingerprint(t, seenHeaders, "claude-cli/2.1.220 (external, cli)", "0.94.0", "v26.3.0", helps.MapStainlessOS(), helps.MapStainlessArch())
if got := seenHeaders.Get("X-App"); got != "cli" {
t.Fatalf("X-App = %q, want cli", got)
}
if want := claudeCodeCLIBetas(payload, nil, false); seenHeaders.Get("Anthropic-Beta") != want {
t.Fatalf("Anthropic-Beta = %q, want %q", seenHeaders.Get("Anthropic-Beta"), want)
}
system := gjson.GetBytes(seenBody, "system").Array()
if len(system) != 2 {
t.Fatalf("system block count = %d, want 2: %s", len(system), seenBody)
}
if got := system[0].Get("text").String(); got != "x-anthropic-billing-header: cc_version=2.1.220.04c; cc_entrypoint=cli;" {
t.Fatalf("billing header = %q, want 2.1.220 CLI fingerprint", got)
}
if got := system[1].Get("text").String(); got != claudeCodeCLIIdentity {
t.Fatalf("system[1].text = %q, want official CLI identity", got)
}
if got := system[1].Get("cache_control.type").String(); got != "ephemeral" {
t.Fatalf("system[1].cache_control.type = %q, want ephemeral", got)
}
// This credential is an API key, and native only selects the 1h cache pool for
// OAuth. The body ttl therefore has to stay absent, matching the fact that
// claudeCodeCLIBetas does not emit extended-cache-ttl-2025-04-11 here either.
if system[1].Get("cache_control.ttl").Exists() {
t.Fatalf("API-key request must not carry a 1h body ttl: %s", system[1].Raw)
}
if betas := seenHeaders.Get("Anthropic-Beta"); strings.Contains(betas, claudeExtendedCacheTTLBeta) {
t.Fatalf("API-key request must not declare extended-cache-ttl: %s", betas)
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("messages[0].content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "x", "")
userID := gjson.GetBytes(seenBody, "metadata.user_id").String()
if !helps.IsValidUserID(userID) {
t.Fatalf("metadata.user_id = %q, want Claude Code 2.1.220 JSON shape", userID)
}
if got, want := gjson.Get(userID, "session_id").String(), seenHeaders.Get("X-Claude-Code-Session-Id"); got != want {
t.Fatalf("metadata session_id = %q, header session ID = %q", got, want)
}
}
func TestClaudeExecutor_ConfirmedClaudeCodeRequestPreservesInteractiveIdentity(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
const sessionID = "11111111-2222-4333-8444-555555555555"
const userID = `{"device_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","account_uuid":"","session_id":"11111111-2222-4333-8444-555555555555"}`
payload := []byte(`{"model":"claude-opus-4-6","system":[{"type":"text","text":"interactive-system","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":"x"}],"metadata":{"user_id":` + fmt.Sprintf("%q", userID) + `}}`)
incoming := http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24"},
"X-Claude-Code-Session-Id": {sessionID},
"X-Stainless-Package-Version": {"0.94.0"},
"X-Stainless-Runtime-Version": {"v26.3.0"},
"X-Stainless-Os": {"MacOS"},
"X-Stainless-Arch": {"arm64"},
}
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-confirmed-client",
"base_url": server.URL,
}}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: incoming,
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
assertClaudeFingerprint(t, seenHeaders, "claude-cli/2.1.220 (external, cli)", "0.94.0", "v26.3.0", "MacOS", "arm64")
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != "interactive-system" {
t.Fatalf("system.0.text = %q, want confirmed client system preserved", got)
}
if got := gjson.GetBytes(seenBody, "system.#").Int(); got != 1 {
t.Fatalf("system block count = %d, want 1", got)
}
if got := gjson.GetBytes(seenBody, "metadata.user_id").String(); got != userID {
t.Fatalf("metadata.user_id = %q, want preserved %q", got, userID)
}
if got := seenHeaders.Get("Anthropic-Beta"); got != incoming.Get("Anthropic-Beta") {
t.Fatalf("Anthropic-Beta = %q, want preserved %q", got, incoming.Get("Anthropic-Beta"))
}
}
func TestClaudeExecutor_ConfirmedClaudeCodeWithoutCacheControlPreservesContent(t *testing.T) {
tests := []struct {
name string
stream bool
}{
{name: "non-stream"},
{name: "stream", stream: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
if tt.stream {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("event: message_stop\n" + `data: {"type":"message_stop"}` + "\n\n"))
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
const sessionID = "11111111-2222-4333-8444-555555555555"
const userID = `{"device_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","account_uuid":"","session_id":"11111111-2222-4333-8444-555555555555"}`
payload := []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":"x"}],"metadata":{"user_id":` + fmt.Sprintf("%q", userID) + `}}`)
incoming := http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219"},
"X-Claude-Code-Session-Id": {sessionID},
}
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-confirmed-markerless",
"base_url": server.URL,
}}
req := cliproxyexecutor.Request{Model: "claude-opus-4-6", Payload: payload}
opts := cliproxyexecutor.Options{
Stream: tt.stream,
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: incoming,
}
if tt.stream {
result, errStream := executor.ExecuteStream(context.Background(), auth, req, opts)
if errStream != nil {
t.Fatalf("ExecuteStream() error = %v", errStream)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("stream chunk error = %v", chunk.Err)
}
}
} else if _, errExecute := executor.Execute(context.Background(), auth, req, opts); errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
content := gjson.GetBytes(seenBody, "messages.0.content")
if content.Type != gjson.String || content.String() != "x" {
t.Fatalf("messages.0.content = %s, want native string content preserved; body=%s", content.Raw, seenBody)
}
if gjson.GetBytes(seenBody, "messages.0.content.0.cache_control").Exists() {
t.Fatalf("confirmed markerless native request received synthetic cache_control: %s", seenBody)
}
})
}
}
func TestClaudeExecutor_ConfirmedVSCodeAgentSDKRequestPreservesIdentity(t *testing.T) {
helps.ResetClaudeDeviceProfileCache()
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
const sessionID = "22222222-3333-4444-8555-666666666666"
const userID = `{"device_id":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","account_uuid":"","session_id":"22222222-3333-4444-8555-666666666666"}`
const vscodeUA = "claude-cli/2.1.220 (external, claude-vscode, agent-sdk/0.3.220)"
const billingHeader = "x-anthropic-billing-header: cc_version=2.1.220.04c; cc_entrypoint=claude-vscode;"
payload := []byte(`{"model":"claude-opus-4-6","system":[{"type":"text","text":` + fmt.Sprintf("%q", billingHeader) + `},{"type":"text","text":"You are a Claude agent, built on Anthropic's Claude Agent SDK.","cache_control":{"type":"ephemeral","ttl":"1h"}},{"type":"text","text":"vscode-agent-system"}],"messages":[{"role":"user","content":"x"}],"metadata":{"user_id":` + fmt.Sprintf("%q", userID) + `}}`)
incoming := http.Header{
"User-Agent": {vscodeUA},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,interleaved-thinking-2025-05-14"},
"Anthropic-Dangerous-Direct-Browser-Access": {"true"},
"X-Claude-Code-Session-Id": {sessionID},
"X-Stainless-Package-Version": {"0.94.0"},
"X-Stainless-Runtime-Version": {"v26.3.0"},
"X-Stainless-Os": {"MacOS"},
"X-Stainless-Arch": {"arm64"},
}
stabilize := true
executor := NewClaudeExecutor(&config.Config{ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{StabilizeDeviceProfile: &stabilize}})
auth := &cliproxyauth.Auth{ID: "auth-vscode-agent-sdk", Attributes: map[string]string{
"api_key": "key-vscode-agent-sdk",
"base_url": server.URL,
}}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: incoming,
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
assertClaudeFingerprint(t, seenHeaders, vscodeUA, "0.94.0", "v26.3.0", "MacOS", "arm64")
if got := seenHeaders.Get("Anthropic-Dangerous-Direct-Browser-Access"); got != "true" {
t.Fatalf("Anthropic-Dangerous-Direct-Browser-Access = %q, want preserved true", got)
}
if got := seenHeaders.Get("X-Claude-Code-Session-Id"); got != sessionID {
t.Fatalf("X-Claude-Code-Session-Id = %q, want preserved %q", got, sessionID)
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != billingHeader {
t.Fatalf("system.0.text = %q, want VSCode attribution preserved", got)
}
if got := gjson.GetBytes(seenBody, "system.1.text").String(); got != "You are a Claude agent, built on Anthropic's Claude Agent SDK." {
t.Fatalf("system.1.text = %q, want VSCode Agent SDK identity preserved", got)
}
if got := gjson.GetBytes(seenBody, "system.1.cache_control.ttl").String(); got != "1h" {
t.Fatalf("system.1.cache_control.ttl = %q, want preserved 1h", got)
}
if got := gjson.GetBytes(seenBody, "system.2.text").String(); got != "vscode-agent-system" {
t.Fatalf("system.2.text = %q, want VSCode Agent SDK system preserved", got)
}
if got := gjson.GetBytes(seenBody, "system.#").Int(); got != 3 {
t.Fatalf("system block count = %d, want 3", got)
}
if got := gjson.GetBytes(seenBody, "metadata.user_id").String(); got != userID {
t.Fatalf("metadata.user_id = %q, want preserved %q", got, userID)
}
}
func TestClaudeExecutor_CopiedVSCodeAgentSDKHeadersWithoutMetadataAreCloaked(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
payload := []byte(`{"model":"claude-opus-5","system":"spoofed-system","messages":[{"role":"user","content":"x"}]}`)
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-spoofed-client",
"base_url": server.URL,
"cloak_mode": "always",
}}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-5",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, claude-vscode, agent-sdk/0.3.220)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219"},
},
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if got := seenHeaders.Get("User-Agent"); got != "claude-cli/2.1.220 (external, cli)" {
t.Fatalf("User-Agent = %q, want CLI cloak", got)
}
if got := gjson.GetBytes(seenBody, "system.#").Int(); got != 2 {
t.Fatalf("system block count = %d, want billing and CLI identity only", got)
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("messages[0].content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "x", "")
assertClaudeMidConversationSystemMessage(t, seenBody, 1, "spoofed-system", "")
}
func TestClaudeExecutor_AgentSDKEntrypointWithStrongSignalsUsesCLICloak(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
payload := []byte(`{"model":"claude-opus-4-6","system":"agent-sdk-system","messages":[{"role":"user","content":"x"}],"metadata":{"user_id":"agent-sdk-user"}}`)
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-agent-sdk-client",
"base_url": server.URL,
"cloak_mode": "always",
}}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, sdk-ts, agent-sdk/0.3.220)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219"},
},
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if got := seenHeaders.Get("User-Agent"); got != "claude-cli/2.1.220 (external, cli)" {
t.Fatalf("User-Agent = %q, want CLI cloak", got)
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); !strings.Contains(got, "cc_entrypoint=cli;") {
t.Fatalf("billing attribution = %q, want cli", got)
}
if got := gjson.GetBytes(seenBody, "system.1.text").String(); got != claudeCodeCLIIdentity {
t.Fatalf("system.1.text = %q, want official CLI identity", got)
}
}
func TestClaudeExecutor_ConfirmedVSCodeOAuthPreservesToolNames(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
const userID = `{"device_id":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","account_uuid":"","session_id":"33333333-4444-4555-8666-777777777777"}`
payload := []byte(`{"model":"claude-opus-4-6","system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.220.04c; cc_entrypoint=claude-vscode; cch=00000;"}],"tools":[{"name":"bash","description":"known native name must pass through","input_schema":{"type":"object"}},{"name":"search_web","description":"unknown native name must pass through","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":"x"}],"metadata":{"user_id":` + fmt.Sprintf("%q", userID) + `}}`)
deviceIDs := []string{
"0000000000000000000000000000000000000000000000000000000000000000",
}
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Attributes: map[string]string{
"api_key": "sk-ant-oat-native-vscode",
"base_url": server.URL,
},
Metadata: map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"claude_device_ids": deviceIDs,
"cloak_mode": "always",
},
}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
OriginalRequest: payload,
Headers: http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, claude-vscode, agent-sdk/0.3.220)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219"},
"X-Stainless-Package-Version": {"0.94.0"},
"X-Stainless-Runtime-Version": {"v26.3.0"},
},
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if got := gjson.GetBytes(seenBody, "tools.0.name").String(); got != "bash" {
t.Fatalf("tools.0.name = %q, want confirmed native known name preserved", got)
}
if got := gjson.GetBytes(seenBody, "tools.1.name").String(); got != "search_web" {
t.Fatalf("tools.1.name = %q, want confirmed native unknown name preserved", got)
}
assertClaudeCredentialIdentity(t, seenBody, seenHeaders, deviceIDs, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
upstreamUserID := gjson.GetBytes(seenBody, "metadata.user_id").String()
if upstreamDeviceID := gjson.Get(upstreamUserID, "device_id").String(); upstreamDeviceID == strings.Repeat("c", 64) {
t.Fatalf("device_id = %q, want native device replaced by credential pool", upstreamDeviceID)
}
if got := gjson.Get(upstreamUserID, "session_id").String(); got != "33333333-4444-4555-8666-777777777777" {
t.Fatalf("session_id = %q, want downstream agent session", got)
}
if got := seenHeaders.Get("X-Claude-Code-Session-Id"); got != "33333333-4444-4555-8666-777777777777" {
t.Fatalf("X-Claude-Code-Session-Id = %q, want downstream agent session", got)
}
}
func TestClaudeDeviceProfileStabilizationEnabled_DefaultFalse(t *testing.T) {
if helps.ClaudeDeviceProfileStabilizationEnabled(nil) {
t.Fatal("expected nil config to default to disabled stabilization")
}
if helps.ClaudeDeviceProfileStabilizationEnabled(&config.Config{}) {
t.Fatal("expected unset stabilize-device-profile to default to disabled stabilization")
}
}
func TestApplyClaudeToolPrefix(t *testing.T) {
input := []byte(`{"tools":[{"name":"alpha"},{"name":"proxy_bravo"}],"tool_choice":{"type":"tool","name":"charlie"},"messages":[{"role":"assistant","content":[{"type":"tool_use","name":"delta","id":"t1","input":{}}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_alpha" {
t.Fatalf("tools.0.name = %q, want %q", got, "proxy_alpha")
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_bravo" {
t.Fatalf("tools.1.name = %q, want %q", got, "proxy_bravo")
}
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != "proxy_charlie" {
t.Fatalf("tool_choice.name = %q, want %q", got, "proxy_charlie")
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "proxy_delta" {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, "proxy_delta")
}
}
func TestApplyClaudeToolPrefix_WithToolReference(t *testing.T) {
input := []byte(`{"tools":[{"name":"alpha"}],"messages":[{"role":"user","content":[{"type":"tool_reference","tool_name":"beta"},{"type":"tool_reference","tool_name":"proxy_gamma"}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
if got := gjson.GetBytes(out, "messages.0.content.0.tool_name").String(); got != "proxy_beta" {
t.Fatalf("messages.0.content.0.tool_name = %q, want %q", got, "proxy_beta")
}
if got := gjson.GetBytes(out, "messages.0.content.1.tool_name").String(); got != "proxy_gamma" {
t.Fatalf("messages.0.content.1.tool_name = %q, want %q", got, "proxy_gamma")
}
}
func TestSanitizeClaudeWebSearchDomains(t *testing.T) {
// Mirrors the litellm payload from issue #2681: a non-empty allowed_domains
// alongside an empty blocked_domains, which Anthropic rejects as ambiguous.
input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search","allowed_domains":["anthropic.com"],"blocked_domains":[],"max_uses":8}]}`)
out := sanitizeClaudeWebSearchDomains(input)
if gjson.GetBytes(out, "tools.0.blocked_domains").Exists() {
t.Fatalf("empty blocked_domains should be removed: %s", string(out))
}
if got := gjson.GetBytes(out, "tools.0.allowed_domains").Array(); len(got) != 1 || got[0].String() != "anthropic.com" {
t.Fatalf("non-empty allowed_domains should be preserved: %s", string(out))
}
if got := gjson.GetBytes(out, "tools.0.max_uses").Int(); got != 8 {
t.Fatalf("max_uses should be preserved: got %d", got)
}
}
func TestSanitizeClaudeWebSearchDomains_LeavesNonBuiltinAndNonEmpty(t *testing.T) {
// Empty arrays on non-web_search tools must be left untouched.
input := []byte(`{"tools":[{"type":"custom","name":"x","blocked_domains":[]},{"type":"web_search_20250305","name":"web_search","blocked_domains":["evil.com"]}]}`)
out := sanitizeClaudeWebSearchDomains(input)
if !gjson.GetBytes(out, "tools.0.blocked_domains").Exists() {
t.Fatalf("non-web_search tool fields should be untouched: %s", string(out))
}
if got := gjson.GetBytes(out, "tools.1.blocked_domains").Array(); len(got) != 1 || got[0].String() != "evil.com" {
t.Fatalf("non-empty blocked_domains should be preserved: %s", string(out))
}
}
func TestApplyClaudeToolPrefix_SkipsBuiltinTools(t *testing.T) {
input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search"},{"name":"my_custom_tool","input_schema":{"type":"object"}}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "web_search" {
t.Fatalf("built-in tool name should not be prefixed: tools.0.name = %q, want %q", got, "web_search")
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_my_custom_tool" {
t.Fatalf("custom tool should be prefixed: tools.1.name = %q, want %q", got, "proxy_my_custom_tool")
}
}
func TestApplyClaudeToolPrefix_BuiltinToolSkipped(t *testing.T) {
body := []byte(`{
"tools": [
{"type": "web_search_20250305", "name": "web_search", "max_uses": 5},
{"name": "Read"}
],
"messages": [
{"role": "user", "content": [
{"type": "tool_use", "name": "web_search", "id": "ws1", "input": {}},
{"type": "tool_use", "name": "Read", "id": "r1", "input": {}}
]}
]
}`)
out := applyClaudeToolPrefix(body, "proxy_")
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "web_search" {
t.Fatalf("tools.0.name = %q, want %q", got, "web_search")
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "web_search" {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, "web_search")
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_Read" {
t.Fatalf("tools.1.name = %q, want %q", got, "proxy_Read")
}
if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != "proxy_Read" {
t.Fatalf("messages.0.content.1.name = %q, want %q", got, "proxy_Read")
}
}
func TestApplyClaudeToolPrefix_KnownBuiltinInHistoryOnly(t *testing.T) {
body := []byte(`{
"tools": [
{"name": "Read"}
],
"messages": [
{"role": "user", "content": [
{"type": "tool_use", "name": "web_search", "id": "ws1", "input": {}}
]}
]
}`)
out := applyClaudeToolPrefix(body, "proxy_")
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "web_search" {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, "web_search")
}
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Read" {
t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Read")
}
}
func TestApplyClaudeToolPrefix_CustomToolsPrefixed(t *testing.T) {
body := []byte(`{
"tools": [{"name": "Read"}, {"name": "Write"}],
"messages": [
{"role": "user", "content": [
{"type": "tool_use", "name": "Read", "id": "r1", "input": {}},
{"type": "tool_use", "name": "Write", "id": "w1", "input": {}}
]}
]
}`)
out := applyClaudeToolPrefix(body, "proxy_")
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Read" {
t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Read")
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "proxy_Write" {
t.Fatalf("tools.1.name = %q, want %q", got, "proxy_Write")
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != "proxy_Read" {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, "proxy_Read")
}
if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != "proxy_Write" {
t.Fatalf("messages.0.content.1.name = %q, want %q", got, "proxy_Write")
}
}
func TestApplyClaudeToolPrefix_ToolChoiceBuiltin(t *testing.T) {
body := []byte(`{
"tools": [
{"type": "web_search_20250305", "name": "web_search"},
{"name": "Read"}
],
"tool_choice": {"type": "tool", "name": "web_search"}
}`)
out := applyClaudeToolPrefix(body, "proxy_")
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != "web_search" {
t.Fatalf("tool_choice.name = %q, want %q", got, "web_search")
}
}
func TestApplyClaudeToolPrefix_KnownFallbackBuiltinsRemainUnprefixed(t *testing.T) {
for _, builtin := range []string{"web_search", "code_execution", "text_editor", "computer"} {
t.Run(builtin, func(t *testing.T) {
input := []byte(fmt.Sprintf(`{
"tools":[{"name":"Read"}],
"tool_choice":{"type":"tool","name":%q},
"messages":[{"role":"assistant","content":[{"type":"tool_use","name":%q,"id":"toolu_1","input":{}},{"type":"tool_reference","tool_name":%q},{"type":"tool_result","tool_use_id":"toolu_1","content":[{"type":"tool_reference","tool_name":%q}]}]}]
}`, builtin, builtin, builtin, builtin))
out := applyClaudeToolPrefix(input, "proxy_")
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != builtin {
t.Fatalf("tool_choice.name = %q, want %q", got, builtin)
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != builtin {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, builtin)
}
if got := gjson.GetBytes(out, "messages.0.content.1.tool_name").String(); got != builtin {
t.Fatalf("messages.0.content.1.tool_name = %q, want %q", got, builtin)
}
if got := gjson.GetBytes(out, "messages.0.content.2.content.0.tool_name").String(); got != builtin {
t.Fatalf("messages.0.content.2.content.0.tool_name = %q, want %q", got, builtin)
}
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "proxy_Read" {
t.Fatalf("tools.0.name = %q, want %q", got, "proxy_Read")
}
})
}
}
func TestStripClaudeToolPrefixFromResponse(t *testing.T) {
input := []byte(`{"content":[{"type":"tool_use","name":"proxy_alpha","id":"t1","input":{}},{"type":"tool_use","name":"bravo","id":"t2","input":{}}]}`)
out := stripClaudeToolPrefixFromResponse(input, "proxy_")
if got := gjson.GetBytes(out, "content.0.name").String(); got != "alpha" {
t.Fatalf("content.0.name = %q, want %q", got, "alpha")
}
if got := gjson.GetBytes(out, "content.1.name").String(); got != "bravo" {
t.Fatalf("content.1.name = %q, want %q", got, "bravo")
}
}
func TestStripClaudeToolPrefixFromResponse_WithToolReference(t *testing.T) {
input := []byte(`{"content":[{"type":"tool_reference","tool_name":"proxy_alpha"},{"type":"tool_reference","tool_name":"bravo"}]}`)
out := stripClaudeToolPrefixFromResponse(input, "proxy_")
if got := gjson.GetBytes(out, "content.0.tool_name").String(); got != "alpha" {
t.Fatalf("content.0.tool_name = %q, want %q", got, "alpha")
}
if got := gjson.GetBytes(out, "content.1.tool_name").String(); got != "bravo" {
t.Fatalf("content.1.tool_name = %q, want %q", got, "bravo")
}
}
func TestStripClaudeToolPrefixFromStreamLine(t *testing.T) {
line := []byte(`data: {"type":"content_block_start","content_block":{"type":"tool_use","name":"proxy_alpha","id":"t1"},"index":0}`)
out := stripClaudeToolPrefixFromStreamLine(line, "proxy_")
payload := bytes.TrimSpace(out)
if bytes.HasPrefix(payload, []byte("data:")) {
payload = bytes.TrimSpace(payload[len("data:"):])
}
if got := gjson.GetBytes(payload, "content_block.name").String(); got != "alpha" {
t.Fatalf("content_block.name = %q, want %q", got, "alpha")
}
}
func TestStripClaudeToolPrefixFromStreamLine_WithToolReference(t *testing.T) {
line := []byte(`data: {"type":"content_block_start","content_block":{"type":"tool_reference","tool_name":"proxy_beta"},"index":0}`)
out := stripClaudeToolPrefixFromStreamLine(line, "proxy_")
payload := bytes.TrimSpace(out)
if bytes.HasPrefix(payload, []byte("data:")) {
payload = bytes.TrimSpace(payload[len("data:"):])
}
if got := gjson.GetBytes(payload, "content_block.tool_name").String(); got != "beta" {
t.Fatalf("content_block.tool_name = %q, want %q", got, "beta")
}
}
func TestApplyClaudeToolPrefix_PreservesNestedMCPToolReference(t *testing.T) {
input := []byte(`{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_123","content":[{"type":"tool_reference","tool_name":"mcp__nia__manage_resource"}]}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
got := gjson.GetBytes(out, "messages.0.content.0.content.0.tool_name").String()
if got != "mcp__nia__manage_resource" {
t.Fatalf("nested tool_reference tool_name = %q, want MCP name preserved", got)
}
}
func TestClaudeExecutor_ExecuteStripsOpenAIEncryptedThinkingBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"codex reasoning","signature":"gAAAAABopenai-encrypted-content"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if strings.Contains(string(seenBody), "gAAAAABopenai-encrypted-content") || strings.Contains(string(seenBody), "codex reasoning") {
t.Fatalf("invalid thinking block was forwarded: %s", string(seenBody))
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("messages.0.content length = %d, want 1: %s", len(content), string(seenBody))
}
if got := content[0].Get("text").String(); got != "Answer" {
t.Fatalf("remaining content text = %q, want Answer", got)
}
}
func TestClaudeExecutor_ExecuteStripsForeignToolUseSignaturesBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{
"type":"tool_use",
"id":"toolu_1",
"name":"lookup",
"input":{"q":"x"},
"signature":"skip_thought_signature_validator",
"thought_signature":"skip_thought_signature_validator",
"extra_content":{"google":{"thought_signature":"skip_thought_signature_validator"}}
}
]},
{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
toolUse := gjson.GetBytes(seenBody, "messages.0.content.0")
if !toolUse.Get("type").Exists() || toolUse.Get("type").String() != "tool_use" {
t.Fatalf("tool_use block was not preserved: %s", string(seenBody))
}
for _, path := range []string{"signature", "thought_signature", "extra_content"} {
if toolUse.Get(path).Exists() {
t.Fatalf("foreign tool_use signature field %s was forwarded: %s", path, string(seenBody))
}
}
}
func TestShouldSanitizeClaudeMessagesForUpstream_OnlyClaudeFamily(t *testing.T) {
cases := []struct {
model string
want bool
}{
{model: "claude-sonnet-4-5", want: true},
{model: "claude-3-5-sonnet-20241022", want: true},
{model: "kimi-k2.5", want: false},
{model: "mimo-v2", want: false},
{model: "gemini-3.5-flash", want: false},
}
for _, tc := range cases {
t.Run(tc.model, func(t *testing.T) {
got := shouldSanitizeClaudeMessagesForUpstream(tc.model)
if got != tc.want {
t.Errorf("shouldSanitizeClaudeMessagesForUpstream(%q) = %v, want %v", tc.model, got, tc.want)
}
})
}
}
func TestSanitizeClaudeMessagesForClaudeUpstream_BypassesUnknownModelSignatureMatrix(t *testing.T) {
rawSignature := "skip_thought_signature_validator"
body := []byte(`{
"model": "kimi-k2.5",
"messages": [
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "keep", "signature": "` + rawSignature + `"},
{"type": "text", "text": "hello"},
{"type": "tool_use", "id": "call_123", "name": "get_weather", "input": {}, "signature": "` + rawSignature + `"}
]
}
]
}`)
output := sanitizeClaudeMessagesForClaudeUpstreamWithDebug(context.Background(), body, "kimi-k2.5")
parts := gjson.GetBytes(output, "messages.0.content").Array()
if len(parts) != 3 {
t.Fatalf("content length = %d, want 3 when sanitizer is bypassed: %s", len(parts), output)
}
if got := parts[0].Get("signature").String(); got != rawSignature {
t.Fatalf("thinking signature = %q, want preserved %q", got, rawSignature)
}
if got := parts[2].Get("signature").String(); got != rawSignature {
t.Fatalf("tool_use signature = %q, want preserved %q", got, rawSignature)
}
}
func TestClaudeExecutor_ExecuteBypassesSignatureSanitizerForUnknownModel(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"mimo-v2","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"keep reasoning","signature":""},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "mimo-v2",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if !strings.Contains(string(seenBody), "keep reasoning") {
t.Fatalf("unknown-model thinking block should bypass Claude sanitizer: %s", string(seenBody))
}
}
func TestClaudeExecutor_ExecuteStripsMalformedEPrefixThinkingBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
malformedSignature := malformedClaudeTreeSignatureForClaudeExecutorTest()
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"bad reasoning","signature":"` + malformedSignature + `"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if strings.Contains(string(seenBody), malformedSignature) || strings.Contains(string(seenBody), "bad reasoning") {
t.Fatalf("malformed E-prefix thinking block was forwarded: %s", string(seenBody))
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("messages.0.content length = %d, want 1: %s", len(content), string(seenBody))
}
if got := content[0].Get("text").String(); got != "Answer" {
t.Fatalf("remaining content text = %q, want Answer", got)
}
}
func TestClaudeExecutor_ExecuteStripsInvalidBase64ThinkingBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"bad reasoning","signature":"E!!!invalid!!!"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if strings.Contains(string(seenBody), "E!!!invalid!!!") || strings.Contains(string(seenBody), "bad reasoning") {
t.Fatalf("invalid-base64 thinking block was forwarded: %s", string(seenBody))
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("messages.0.content length = %d, want 1: %s", len(content), string(seenBody))
}
}
func TestClaudeExecutor_ExecuteStripsEmptySignatureEmptyTextThinking(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","text":"","signature":""},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
content := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("messages.0.content length = %d, want 1: %s", len(content), string(seenBody))
}
if got := content[0].Get("type").String(); got != "text" {
t.Fatalf("remaining content type = %q, want text: %s", got, string(seenBody))
}
if got := content[0].Get("text").String(); got != "Answer" {
t.Fatalf("remaining content text = %q, want Answer: %s", got, string(seenBody))
}
}
func TestClaudeExecutor_ExecuteStreamStripsOpenAIEncryptedThinkingBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"message_stop\"}\n\n"))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"codex reasoning","signature":"gAAAAABopenai-encrypted-content"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("ExecuteStream() error = %v", err)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("unexpected chunk error: %v", chunk.Err)
}
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if strings.Contains(string(seenBody), "gAAAAABopenai-encrypted-content") || strings.Contains(string(seenBody), "codex reasoning") {
t.Fatalf("invalid thinking block was forwarded: %s", string(seenBody))
}
}
func claudeOAuthCancellationTestMetadata() map[string]any {
return map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: []string{
"0000000000000000000000000000000000000000000000000000000000000000",
},
}
}
func TestClaudeExecutor_ExecuteStreamOAuthStartupCancellationIsRequestScoped(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
close(started)
<-release
}))
defer server.Close()
defer close(release)
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-stream-startup-cancellation",
Attributes: map[string]string{
"api_key": "sk-ant-oat-stream-startup-cancellation",
"base_url": server.URL,
},
Metadata: claudeOAuthCancellationTestMetadata(),
}
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
_, errStream := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{
Model: "claude-opus-5",
Payload: []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hello"}],"stream":true}`),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
errCh <- errStream
}()
<-started
cancel()
select {
case errStream := <-errCh:
if !errors.Is(errStream, context.Canceled) {
t.Fatalf("ExecuteStream() error = %v, want context.Canceled", errStream)
}
var requestErr cliproxyexecutor.RequestScopedError
if !errors.As(errStream, &requestErr) || requestErr == nil || !requestErr.IsRequestScoped() {
t.Fatalf("ExecuteStream() error = %T %v, want request-scoped cancellation", errStream, errStream)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for startup cancellation")
}
}
func TestClaudeExecutor_ExecuteStreamOAuthCancellationIsRequestScoped(t *testing.T) {
started := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("data"))
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
close(started)
<-r.Context().Done()
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-stream-cancellation",
Attributes: map[string]string{
"api_key": "sk-ant-oat-stream-cancellation",
"base_url": server.URL,
},
Metadata: claudeOAuthCancellationTestMetadata(),
}
payload := []byte(`{"model":"claude-opus-5","system":"system prompt","messages":[{"role":"user","content":"hello"}],"stream":true}`)
ctx, cancel := context.WithCancel(context.Background())
result, errStream := executor.ExecuteStream(ctx, auth, cliproxyexecutor.Request{
Model: "claude-opus-5",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errStream != nil {
cancel()
t.Fatalf("ExecuteStream() error = %v", errStream)
}
<-started
cancel()
var cancellationErr error
deadline := time.After(2 * time.Second)
for cancellationErr == nil {
select {
case chunk, ok := <-result.Chunks:
if !ok {
t.Fatal("stream closed without a cancellation result")
}
cancellationErr = chunk.Err
case <-deadline:
t.Fatal("timed out waiting for cancellation result")
}
}
if !errors.Is(cancellationErr, context.Canceled) {
t.Fatalf("stream error = %v, want context.Canceled", cancellationErr)
}
var requestErr cliproxyexecutor.RequestScopedError
if !errors.As(cancellationErr, &requestErr) || requestErr == nil || !requestErr.IsRequestScoped() {
t.Fatalf("stream error = %T %v, want request-scoped cancellation", cancellationErr, cancellationErr)
}
var statusErr interface{ StatusCode() int }
if errors.As(cancellationErr, &statusErr) {
t.Fatalf("stream cancellation unexpectedly exposes HTTP status %d", statusErr.StatusCode())
}
for range result.Chunks {
}
}
func TestClaudeExecutor_ExecuteStreamDirectPassthroughEmitsCompleteSSEEvents(t *testing.T) {
firstData := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}`
secondData := `{"type":"message_stop"}`
upstreamStream := "event: content_block_delta\n" +
"data: " + firstData + "\n" +
"\n" +
"event: message_stop\n" +
"data: " + secondData + "\n" +
"\n"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte(upstreamStream))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("ExecuteStream() error = %v", err)
}
var payloads []string
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("unexpected chunk error: %v", chunk.Err)
}
payloads = append(payloads, string(chunk.Payload))
}
want := []string{
"event: content_block_delta\n" + "data: " + firstData + "\n\n",
"event: message_stop\n" + "data: " + secondData + "\n\n",
}
if len(payloads) != len(want) {
t.Fatalf("payload count = %d, want %d: %#v", len(payloads), len(want), payloads)
}
for i := range want {
if payloads[i] != want[i] {
t.Fatalf("payload[%d] = %q, want %q", i, payloads[i], want[i])
}
}
}
// TestClaudeExecutor_ExecuteStreamDecodesCompressedSSE guards the dependency that
// lets CPA advertise the real client's Accept-Encoding on streaming requests:
// once compression is offered the upstream may compress the SSE body, so the
// streaming success path must decode it and still emit event boundaries intact.
func TestClaudeExecutor_ExecuteStreamDecodesCompressedSSE(t *testing.T) {
firstData := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}`
secondData := `{"type":"message_stop"}`
upstreamStream := "event: content_block_delta\n" +
"data: " + firstData + "\n" +
"\n" +
"event: message_stop\n" +
"data: " + secondData + "\n" +
"\n"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Content-Encoding", "gzip")
gzipWriter := gzip.NewWriter(w)
if _, errWrite := gzipWriter.Write([]byte(upstreamStream)); errWrite != nil {
t.Errorf("gzip write: %v", errWrite)
}
if errClose := gzipWriter.Close(); errClose != nil {
t.Errorf("gzip close: %v", errClose)
}
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("ExecuteStream() error = %v", err)
}
var payloads []string
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("unexpected chunk error: %v", chunk.Err)
}
payloads = append(payloads, string(chunk.Payload))
}
want := []string{
"event: content_block_delta\n" + "data: " + firstData + "\n\n",
"event: message_stop\n" + "data: " + secondData + "\n\n",
}
if len(payloads) != len(want) {
t.Fatalf("payload count = %d, want %d: %#v", len(payloads), len(want), payloads)
}
for i := range want {
if payloads[i] != want[i] {
t.Fatalf("payload[%d] = %q, want %q", i, payloads[i], want[i])
}
}
}
func TestClaudeExecutor_CountTokensExcludesInvalidOpenAIThinking(t *testing.T) {
executor := NewClaudeExecutor(&config.Config{})
countTokens := func(payload []byte) int64 {
t.Helper()
resp, err := executor.CountTokens(context.Background(), nil, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("CountTokens() error = %v", err)
}
return gjson.GetBytes(resp.Payload, "input_tokens").Int()
}
withInvalidThinking := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"codex reasoning","signature":"gAAAAABopenai-encrypted-content"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
withoutInvalidThinking := []byte(`{
"messages": [
{"role":"assistant","content":[{"type":"text","text":"Answer"}]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
if got, want := countTokens(withInvalidThinking), countTokens(withoutInvalidThinking); got != want {
t.Fatalf("count with invalid thinking = %d, want sanitized count %d", got, want)
}
}
func TestClaudeCountTokensBetasForCredentialMatchesNativeOAuth220(t *testing.T) {
want := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,token-counting-2024-11-01"
if got := claudeCountTokensBetasForCredential(true); got != want {
t.Fatalf("OAuth count_tokens betas = %q, want %q", got, want)
}
wantAPIKey := "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,token-counting-2024-11-01"
if got := claudeCountTokensBetasForCredential(false); got != wantAPIKey {
t.Fatalf("API-key count_tokens betas = %q, want %q", got, wantAPIKey)
}
if got := withClaudeCountTokensOAuthBeta(wantAPIKey); got != want {
t.Fatalf("confirmed-client count_tokens betas = %q, want %q", got, want)
}
}
func TestShouldUseClaudeUpstreamTokenCount(t *testing.T) {
tests := []struct {
name string
apiKey string
baseURL string
want bool
}{
{name: "official OAuth", apiKey: "sk-ant-oat-official", baseURL: "https://api.anthropic.com", want: true},
{name: "official API key", apiKey: "key-official", baseURL: "https://api.anthropic.com:443", want: true},
{name: "custom OAuth", apiKey: "sk-ant-oat-custom", baseURL: "https://gateway.example"},
{name: "custom API key", apiKey: "key-custom", baseURL: "https://gateway.example"},
{name: "lookalike host", apiKey: "sk-ant-oat-lookalike", baseURL: "https://api.anthropic.com.example"},
{name: "insecure official host", apiKey: "sk-ant-oat-http", baseURL: "http://api.anthropic.com"},
{name: "missing credential", baseURL: "https://api.anthropic.com"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := shouldUseClaudeUpstreamTokenCount(test.apiKey, test.baseURL); got != test.want {
t.Fatalf("shouldUseClaudeUpstreamTokenCount() = %v, want %v", got, test.want)
}
})
}
}
func TestClaudeExecutor_LegacySystemReminderAcrossMessagesAndStream(t *testing.T) {
var mu sync.Mutex
captured := make(map[string][]byte)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if strings.Contains(r.URL.Path, "count_tokens") {
t.Errorf("custom OAuth count_tokens unexpectedly reached upstream: %s", r.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
return
}
kind := "messages"
if gjson.GetBytes(body, "stream").Bool() {
kind = "stream"
}
mu.Lock()
captured[kind] = bytes.Clone(body)
mu.Unlock()
switch kind {
case "stream":
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
default:
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_legacy","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-legacy-reminder-paths",
Attributes: map[string]string{
"api_key": "sk-ant-oat-legacy-reminder-paths",
"base_url": server.URL,
},
Metadata: map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: []string{
"0000000000000000000000000000000000000000000000000000000000000000",
},
},
}
makePayload := func(userText string, stream bool) []byte {
streamField := ""
if stream {
streamField = `,"stream":true`
}
return []byte(`{"model":"claude-opus-4-6","system":"legacy-system-prompt","messages":[{"role":"user","content":` + fmt.Sprintf("%q", userText) + `}]` + streamField + `}`)
}
if _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6", Payload: makePayload("messages-user", false),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude}); errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
countResp, errCount := executor.CountTokens(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6", Payload: makePayload("count-user", false),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errCount != nil {
t.Fatalf("CountTokens() error = %v", errCount)
}
if got := gjson.GetBytes(countResp.Payload, "input_tokens").Int(); got <= 0 {
t.Fatalf("local count_tokens input_tokens = %d, want positive estimate", got)
}
streamResult, errStream := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6", Payload: makePayload("stream-user", true),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errStream != nil {
t.Fatalf("ExecuteStream() error = %v", errStream)
}
for chunk := range streamResult.Chunks {
if chunk.Err != nil {
t.Fatalf("stream chunk error = %v", chunk.Err)
}
}
mu.Lock()
bodies := map[string][]byte{
"messages": bytes.Clone(captured["messages"]),
"stream": bytes.Clone(captured["stream"]),
}
mu.Unlock()
for kind, wantUser := range map[string]string{"messages": "messages-user", "stream": "stream-user"} {
body := bodies[kind]
if len(body) == 0 {
t.Fatalf("missing %s upstream capture", kind)
}
assertClaudeLegacySystemReminderLayout(t, body, "legacy-system-prompt", wantUser, "1h")
if _, ok := claudeBillingCCHDigitsOffset(body); !ok {
t.Fatalf("%s body is missing final CCH", kind)
}
}
}
func TestClaudeExecutor_CountTokensUpstreamCloakNeverPreservesCustomTool(t *testing.T) {
var upstreamBody []byte
var upstreamHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upstreamBody, _ = io.ReadAll(r.Body)
upstreamHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"input_tokens":7}`))
}))
defer server.Close()
deviceIDs := []string{
"0000000000000000000000000000000000000000000000000000000000000000",
}
auth := &cliproxyauth.Auth{
ID: "oauth-never-count-tokens",
Attributes: map[string]string{
"api_key": "sk-ant-oat-never-count-tokens",
"base_url": server.URL,
},
Metadata: map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: deviceIDs,
"cloak_mode": "never",
},
}
payload := []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":"search"}],"tools":[{"name":"search_web","input_schema":{"type":"object"}}]}`)
executor := NewClaudeExecutor(&config.Config{})
_, errCount := executor.countTokensUpstream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Metadata: map[string]any{
cliproxyexecutor.ExecutionSessionMetadataKey: "count-never-agent-conversation",
},
})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
if got := gjson.GetBytes(upstreamBody, "tools.0.name").String(); got != "search_web" {
t.Fatalf("count_tokens tool name = %q, want cloak=never passthrough", got)
}
assertClaudeCountTokensIdentity(t, upstreamBody, upstreamHeaders)
}
func TestClaudeExecutor_CountTokensUpstreamConfirmedVSCodePreservesCustomTool(t *testing.T) {
var upstreamName string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
upstreamName = gjson.GetBytes(body, "tools.0.name").String()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"input_tokens":7}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-mcp-native-count-tokens",
Attributes: map[string]string{
"api_key": "sk-ant-oat-mcp-native-count-tokens",
"base_url": server.URL,
},
Metadata: map[string]any{
"cloak_mode": "always",
},
}
payload := []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":"search"}],"tools":[{"name":"search_web","input_schema":{"type":"object"}}]}`)
_, errCount := executor.countTokensUpstream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Headers: http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, claude-vscode, agent-sdk/0.3.220)"},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219"},
},
})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
if upstreamName != "search_web" {
t.Fatalf("confirmed VSCode count_tokens tool name = %q, want unchanged", upstreamName)
}
}
func TestClaudeExecutor_CountTokensCloakMatchesMeasuredDirectAnthropicShape(t *testing.T) {
var upstreamBody []byte
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
var errRead error
upstreamBody, errRead = io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"input_tokens":34}`)), Request: req}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "sk-ant-oat-cloaked-count-shape"}}
payload := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":[{"type":"text","text":"x"}]}],"tools":[{"name":"search_web","input_schema":{"type":"object"}}],"metadata":{"user_id":"remove"},"context_management":{"edits":[]},"diagnostics":{"previous_message_id":"remove"}}`)
_, errCount := NewClaudeExecutor(&config.Config{}).countTokensUpstream(ctx, auth, cliproxyexecutor.Request{Model: "claude-opus-5", Payload: payload}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
if got := gjson.GetBytes(upstreamBody, "system"); got.Exists() {
t.Fatalf("cloaked direct count system = %s, want absent", got.Raw)
}
for _, field := range []string{"metadata", "context_management", "diagnostics", "betas"} {
if got := gjson.GetBytes(upstreamBody, field); got.Exists() {
t.Fatalf("cloaked direct count %s = %s, want absent", field, got.Raw)
}
}
if got := gjson.GetBytes(upstreamBody, "tools.0.name").String(); !helps.IsClaudeMCPToolName(got) {
t.Fatalf("cloaked direct count tool = %q, want OAuth MCP alias", got)
}
}
// TestClaudeExecutor_CountTokensCloakRelocatesCallerSystemAndObfuscates asserts
// that a cloaked direct-Anthropic count_tokens request keeps Claude Code's
// measured shape (no system field) while still accounting for the caller's
// system prompt and honouring sensitive-word obfuscation.
func TestClaudeExecutor_CountTokensCloakRelocatesCallerSystemAndObfuscates(t *testing.T) {
const callerSystem = "third party ACMECORP orchestrator rules"
const sensitiveWord = "ACMECORP"
testCases := []struct {
name string
model string
wantSystemMsg bool
}{
{name: "mid conversation system role", model: "claude-opus-5", wantSystemMsg: true},
{name: "legacy system reminder", model: "claude-sonnet-4-5", wantSystemMsg: false},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var upstreamBody []byte
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
var errRead error
upstreamBody, errRead = io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"input_tokens":34}`)),
Request: req,
}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "sk-ant-oat-count-relocate",
"cloak_sensitive_words": sensitiveWord,
}}
payload := []byte(`{"model":"` + testCase.model + `","system":[{"type":"text","text":"` + callerSystem + `"}],` +
`"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"tools":[]}`)
_, errCount := NewClaudeExecutor(&config.Config{}).countTokensUpstream(ctx, auth,
cliproxyexecutor.Request{Model: testCase.model, Payload: payload},
cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
// Claude Code's count_tokens never carries a system field.
if got := gjson.GetBytes(upstreamBody, "system"); got.Exists() {
t.Fatalf("cloaked count system = %s, want absent", got.Raw)
}
// The caller's system prompt must still be counted, relocated into messages.
// Compare decoded text so JSON escaping does not affect the assertions.
var decodedTexts []string
sawSystemRole := false
gjson.GetBytes(upstreamBody, "messages").ForEach(func(_, message gjson.Result) bool {
if message.Get("role").String() == "system" {
sawSystemRole = true
}
message.Get("content").ForEach(func(_, block gjson.Result) bool {
decodedTexts = append(decodedTexts, block.Get("text").String())
return true
})
return true
})
joinedTexts := strings.Join(decodedTexts, "\n")
if !strings.Contains(joinedTexts, "orchestrator rules") {
t.Fatalf("caller system prompt was dropped from the counted body: %s", upstreamBody)
}
if testCase.wantSystemMsg {
if !sawSystemRole {
t.Fatalf("expected a mid-conversation system message, got %s", upstreamBody)
}
} else if !strings.Contains(joinedTexts, "<system-reminder>") {
t.Fatalf("expected a legacy system reminder, got %s", upstreamBody)
}
// Sensitive words must not reach Anthropic verbatim on this endpoint either.
if strings.Contains(joinedTexts, sensitiveWord) {
t.Fatalf("sensitive word %q leaked to count_tokens: %s", sensitiveWord, upstreamBody)
}
})
}
}
// TestClaudeExecutor_CountTokensCloakStrictModeDropsCallerSystem mirrors the
// Messages path: strict mode keeps only Claude Code identity, so a caller's
// system prompt must not be reintroduced into the counted body.
func TestClaudeExecutor_CountTokensCloakStrictModeDropsCallerSystem(t *testing.T) {
var upstreamBody []byte
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
var errRead error
upstreamBody, errRead = io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"input_tokens":34}`)),
Request: req,
}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "sk-ant-oat-count-strict",
"cloak_strict_mode": "true",
}}
payload := []byte(`{"model":"claude-opus-5","system":[{"type":"text","text":"caller only secret directive"}],` +
`"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"tools":[]}`)
_, errCount := NewClaudeExecutor(&config.Config{}).countTokensUpstream(ctx, auth,
cliproxyexecutor.Request{Model: "claude-opus-5", Payload: payload},
cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
if got := gjson.GetBytes(upstreamBody, "system"); got.Exists() {
t.Fatalf("strict cloaked count system = %s, want absent", got.Raw)
}
if strings.Contains(string(upstreamBody), "secret directive") {
t.Fatalf("strict mode must not forward the caller system prompt: %s", upstreamBody)
}
}
func TestClaudeExecutor_CountTokensConfirmedNativePreservesMeasuredOAuthBody(t *testing.T) {
var upstreamBody []byte
var upstreamHeaders http.Header
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
var errRead error
upstreamBody, errRead = io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
upstreamHeaders = req.Header.Clone()
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"input_tokens":34}`)),
Request: req,
}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "sk-ant-oat-native-count-shape"}}
payload := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":[{"type":"text","text":"x"}]}],"tools":[]}`)
incomingBetas := "claude-code-20250219,interleaved-thinking-2025-05-14,context-management-2025-06-27,token-counting-2024-11-01"
wantBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,token-counting-2024-11-01"
_, errCount := executor.countTokensUpstream(ctx, auth, cliproxyexecutor.Request{Model: "claude-opus-5", Payload: payload}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Headers: http.Header{
"User-Agent": {"claude-cli/2.1.220 (external, cli)"},
"X-App": {"cli"},
"Anthropic-Beta": {incomingBetas},
},
})
if errCount != nil {
t.Fatalf("countTokensUpstream() error = %v", errCount)
}
if !bytes.Equal(upstreamBody, payload) {
t.Fatalf("confirmed native count body changed\n got: %s\nwant: %s", upstreamBody, payload)
}
for _, field := range []string{"system", "metadata", "context_management", "betas"} {
if got := gjson.GetBytes(upstreamBody, field); got.Exists() {
t.Fatalf("confirmed native count body %s = %s, want absent", field, got.Raw)
}
}
if got := strings.Join(upstreamHeaders["anthropic-beta"], ","); got != wantBetas {
t.Fatalf("confirmed native count beta = %q, want %q", got, wantBetas)
}
if got := upstreamHeaders.Get("X-Stainless-Timeout"); got != "" {
t.Fatalf("confirmed native count timeout = %q, want absent", got)
}
}
func TestClaudeExecutor_CountTokensCountsLocallyWithoutUpstreamRequest(t *testing.T) {
payload := []byte(`{
"system":"client system instructions",
"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]
}`)
const expectedCount int64 = 7
testCases := []struct {
name string
apiKey string
}{
{name: "custom API key", apiKey: "key-123"},
{name: "custom OAuth", apiKey: "sk-ant-oat-custom"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected upstream count_tokens request: %s", r.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": testCase.apiKey,
"base_url": server.URL,
}}
resp, errCount := executor.CountTokens(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-sonnet-4-5",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if errCount != nil {
t.Fatalf("CountTokens() error = %v", errCount)
}
if got := gjson.GetBytes(resp.Payload, "input_tokens").Int(); got != expectedCount {
t.Fatalf("input_tokens = %d, want %d; payload = %s", got, expectedCount, resp.Payload)
}
})
}
executor := NewClaudeExecutor(&config.Config{})
resp, err := executor.CountTokens(context.Background(), nil, cliproxyexecutor.Request{
Model: "claude-sonnet-4-5",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
ResponseFormat: sdktranslator.FormatGemini,
})
if err != nil {
t.Fatalf("CountTokens() Gemini response error = %v", err)
}
if got := gjson.GetBytes(resp.Payload, "totalTokens").Int(); got != expectedCount {
t.Fatalf("Gemini totalTokens = %d, want %d; payload = %s", got, expectedCount, resp.Payload)
}
if got := gjson.GetBytes(resp.Payload, "promptTokensDetails.0.tokenCount").Int(); got != expectedCount {
t.Fatalf("Gemini prompt token detail = %d, want %d; payload = %s", got, expectedCount, resp.Payload)
}
}
func TestClaudeExecutor_CountTokensRejectsInvalidRequests(t *testing.T) {
testCases := []struct {
name string
payload string
}{
{name: "invalid JSON", payload: `not-json`},
{name: "non-object", payload: `[]`},
{name: "missing messages", payload: `{}`},
{name: "empty messages", payload: `{"messages":[]}`},
{name: "non-array messages", payload: `{"messages":"invalid"}`},
{name: "invalid role", payload: `{"messages":[{"role":"system","content":"hello"}]}`},
{name: "invalid content", payload: `{"messages":[{"role":"user","content":42}]}`},
{name: "non-object content block", payload: `{"messages":[{"role":"user","content":[42]}]}`},
{name: "untyped content block", payload: `{"messages":[{"role":"user","content":[{"text":"hello"}]}]}`},
}
executor := NewClaudeExecutor(&config.Config{})
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
_, err := executor.CountTokens(context.Background(), nil, cliproxyexecutor.Request{
Model: "claude-sonnet-4-5",
Payload: []byte(testCase.payload),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
assertStatusErr(t, err, http.StatusBadRequest)
requestErr, ok := err.(cliproxyexecutor.RequestScopedError)
if !ok || !requestErr.IsRequestScoped() {
t.Fatalf("error %T is not request-scoped", err)
}
})
}
}
func TestClaudeExecutor_CountTokensRebuildsMidSystemMessagesBeforeValidation(t *testing.T) {
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"rebuild_mid_system_message": "true",
}}
payload := []byte(`{
"system":"Top rule",
"messages":[
{"role":"user","content":"hello"},
{"role":"system","content":"Mid rule"},
{"role":"assistant","content":"answer"}
]
}`)
resp, err := executor.CountTokens(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-sonnet-4-5",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if err != nil {
t.Fatalf("CountTokens() error = %v", err)
}
if got := gjson.GetBytes(resp.Payload, "input_tokens").Int(); got <= 0 {
t.Fatalf("input_tokens = %d, want positive count; payload = %s", got, resp.Payload)
}
}
func TestClaudeExecutor_ReusesUserIDAcrossModelsWhenCacheEnabled(t *testing.T) {
var userIDs []string
var requestModels []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
userID := gjson.GetBytes(body, "metadata.user_id").String()
model := gjson.GetBytes(body, "model").String()
userIDs = append(userIDs, userID)
requestModels = append(requestModels, model)
t.Logf("HTTP Server received request: model=%s, user_id=%s, url=%s", model, userID, r.URL.String())
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
t.Logf("End-to-end test: Fake HTTP server started at %s", server.URL)
cacheEnabled := true
executor := NewClaudeExecutor(&config.Config{
ClaudeKey: []config.ClaudeKey{
{
APIKey: "key-123",
BaseURL: server.URL,
Cloak: &config.CloakConfig{
CacheUserID: &cacheEnabled,
},
},
},
})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
models := []string{"claude-3-5-sonnet", "claude-3-5-haiku"}
for _, model := range models {
t.Logf("Sending request for model: %s", model)
modelPayload, _ := sjson.SetBytes(payload, "model", model)
if _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: model,
Payload: modelPayload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
}); err != nil {
t.Fatalf("Execute(%s) error: %v", model, err)
}
}
if len(userIDs) != 2 {
t.Fatalf("expected 2 requests, got %d", len(userIDs))
}
if userIDs[0] == "" || userIDs[1] == "" {
t.Fatal("expected user_id to be populated")
}
t.Logf("user_id[0] (model=%s): %s", requestModels[0], userIDs[0])
t.Logf("user_id[1] (model=%s): %s", requestModels[1], userIDs[1])
if userIDs[0] != userIDs[1] {
t.Fatalf("expected user_id to be reused across models, got %q and %q", userIDs[0], userIDs[1])
}
if !helps.IsValidUserID(userIDs[0]) {
t.Fatalf("user_id %q is not valid", userIDs[0])
}
t.Logf("✓ End-to-end test passed: Same user_id (%s) was used for both models", userIDs[0])
}
func TestClaudeExecutor_DefaultDoesNotInjectUserID(t *testing.T) {
var userIDs []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
userIDs = append(userIDs, gjson.GetBytes(body, "metadata.user_id").String())
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
for i := 0; i < 2; i++ {
if _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
}); err != nil {
t.Fatalf("Execute call %d error: %v", i, err)
}
}
if len(userIDs) != 2 {
t.Fatalf("expected 2 requests, got %d", len(userIDs))
}
if userIDs[0] != "" || userIDs[1] != "" {
t.Fatalf("default API-key requests must preserve caller metadata without injecting user_id, got %q and %q", userIDs[0], userIDs[1])
}
}
func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsEmptyClaudeStream(t *testing.T) {
_, err := executeOpenAIChatCompletionThroughClaude(t, "")
if err == nil {
t.Fatal("Execute error = nil, want empty stream error")
}
assertStatusErr(t, err, http.StatusBadGateway)
if !strings.Contains(err.Error(), "empty stream response") {
t.Fatalf("Execute error = %q, want empty stream response", err.Error())
}
}
func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsClaudeErrorEvent(t *testing.T) {
body := `data: {"type":"error","error":{"type":"overloaded_error","message":"upstream overloaded"}}` + "\n"
_, err := executeOpenAIChatCompletionThroughClaude(t, body)
if err == nil {
t.Fatal("Execute error = nil, want upstream error event")
}
assertStatusErr(t, err, http.StatusBadGateway)
if !strings.Contains(err.Error(), "upstream overloaded") {
t.Fatalf("Execute error = %q, want upstream overloaded", err.Error())
}
}
func TestClaudeExecutor_ExecuteOpenAINonStreamRejectsIncompleteClaudeStream(t *testing.T) {
body := strings.Join([]string{
`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022"}}`,
`data: {"type":"message_stop"}`,
``,
}, "\n")
_, err := executeOpenAIChatCompletionThroughClaude(t, body)
if err == nil {
t.Fatal("Execute error = nil, want incomplete stream error")
}
assertStatusErr(t, err, http.StatusBadGateway)
if !strings.Contains(err.Error(), "ended before message completion") {
t.Fatalf("Execute error = %q, want incomplete stream error", err.Error())
}
}
func TestClaudeExecutor_ExecuteOpenAINonStreamConvertsValidClaudeStream(t *testing.T) {
body := strings.Join([]string{
`event: message_start`,
`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022"}}`,
`event: content_block_delta`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`,
`event: message_delta`,
`data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":2,"output_tokens":1}}`,
`event: message_stop`,
`data: {"type":"message_stop"}`,
``,
}, "\n")
resp, err := executeOpenAIChatCompletionThroughClaude(t, body)
if err != nil {
t.Fatalf("Execute error: %v", err)
}
if got := gjson.GetBytes(resp.Payload, "id").String(); got != "msg_123" {
t.Fatalf("response id = %q, want msg_123; payload=%s", got, string(resp.Payload))
}
if got := gjson.GetBytes(resp.Payload, "model").String(); got != "claude-3-5-sonnet-20241022" {
t.Fatalf("response model = %q, want claude-3-5-sonnet-20241022", got)
}
if got := gjson.GetBytes(resp.Payload, "choices.0.message.content").String(); got != "ok" {
t.Fatalf("response content = %q, want ok", got)
}
if got := gjson.GetBytes(resp.Payload, "usage.total_tokens").Int(); got != 3 {
t.Fatalf("usage.total_tokens = %d, want 3", got)
}
}
func TestClaudeExecutor_ExecuteTransportMatchesResponseFormat(t *testing.T) {
const model = "claude-3-5-sonnet-20241022"
streamResponse := strings.Join([]string{
`event: message_start`,
`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022"}}`,
`event: content_block_delta`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`,
`event: message_delta`,
`data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":2,"output_tokens":1}}`,
`event: message_stop`,
`data: {"type":"message_stop"}`,
``,
}, "\n")
jsonResponse := `{"id":"msg_123","type":"message","role":"assistant","model":"claude-3-5-sonnet-20241022","content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":2,"output_tokens":1}}`
tests := []struct {
name string
sourceFormat sdktranslator.Format
responseFormat sdktranslator.Format
wantStream bool
}{
{name: "OpenAI to OpenAI uses SSE", sourceFormat: sdktranslator.FormatOpenAI, responseFormat: sdktranslator.FormatOpenAI, wantStream: true},
{name: "OpenAI to Claude uses JSON", sourceFormat: sdktranslator.FormatOpenAI, responseFormat: sdktranslator.FormatClaude, wantStream: false},
{name: "Claude to OpenAI uses SSE", sourceFormat: sdktranslator.FormatClaude, responseFormat: sdktranslator.FormatOpenAI, wantStream: true},
{name: "Claude to Claude uses JSON", sourceFormat: sdktranslator.FormatClaude, responseFormat: sdktranslator.FormatClaude, wantStream: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
if tt.wantStream {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte(streamResponse))
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(jsonResponse))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{
Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: []config.PayloadModelRule{{Name: model, Protocol: "claude"}},
Params: map[string]any{"stream": !tt.wantStream},
}},
},
})
attributes := map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}
if tt.wantStream {
attributes["header:Accept"] = "application/json"
attributes["header:Accept-Encoding"] = "gzip, deflate, br, zstd"
}
auth := &cliproxyauth.Auth{Attributes: attributes}
payload := []byte(`{"model":"claude-3-5-sonnet-20241022","stream":false,"messages":[{"role":"user","content":"hi"}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: model,
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: tt.sourceFormat,
ResponseFormat: tt.responseFormat,
Headers: http.Header{
"Anthropic-Beta": []string{"client-beta"},
},
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
stream := gjson.GetBytes(seenBody, "stream")
if !stream.Exists() || stream.Bool() != tt.wantStream {
t.Fatalf("upstream stream = %s, want %t; body=%s", stream.Raw, tt.wantStream, string(seenBody))
}
wantAccept := "application/json"
wantEncoding := "gzip, deflate, br, zstd"
if tt.wantStream {
wantAccept = "text/event-stream"
wantEncoding = "identity"
}
if got := seenHeaders.Get("Accept"); got != wantAccept {
t.Fatalf("Accept = %q, want %q", got, wantAccept)
}
if got := seenHeaders.Get("Accept-Encoding"); got != wantEncoding {
t.Fatalf("Accept-Encoding = %q, want %q", got, wantEncoding)
}
if got := seenHeaders.Get("Anthropic-Beta"); !strings.Contains(got, "client-beta") {
t.Fatalf("Anthropic-Beta = %q, want client beta preserved", got)
}
})
}
}
func executeOpenAIChatCompletionThroughClaude(t *testing.T, upstreamBody string) (cliproxyexecutor.Response, error) {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte(upstreamBody))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":"hi"}]}`)
return executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
})
}
func assertStatusErr(t *testing.T, err error, want int) {
t.Helper()
status, ok := err.(interface{ StatusCode() int })
if !ok {
t.Fatalf("error %T does not expose StatusCode", err)
}
if got := status.StatusCode(); got != want {
t.Fatalf("StatusCode() = %d, want %d", got, want)
}
}
func TestStripClaudeToolPrefixFromResponse_NestedToolReference(t *testing.T) {
input := []byte(`{"content":[{"type":"tool_result","tool_use_id":"toolu_123","content":[{"type":"tool_reference","tool_name":"proxy_mcp__nia__manage_resource"}]}]}`)
out := stripClaudeToolPrefixFromResponse(input, "proxy_")
got := gjson.GetBytes(out, "content.0.content.0.tool_name").String()
if got != "mcp__nia__manage_resource" {
t.Fatalf("nested tool_reference tool_name = %q, want %q", got, "mcp__nia__manage_resource")
}
}
func TestApplyClaudeToolPrefix_NestedToolReferenceWithStringContent(t *testing.T) {
// tool_result.content can be a string - should not be processed
input := []byte(`{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_123","content":"plain string result"}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
got := gjson.GetBytes(out, "messages.0.content.0.content").String()
if got != "plain string result" {
t.Fatalf("string content should remain unchanged = %q", got)
}
}
func TestApplyClaudeToolPrefix_SkipsBuiltinToolReference(t *testing.T) {
input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search"}],"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"tool_reference","tool_name":"web_search"}]}]}]}`)
out := applyClaudeToolPrefix(input, "proxy_")
got := gjson.GetBytes(out, "messages.0.content.0.content.0.tool_name").String()
if got != "web_search" {
t.Fatalf("built-in tool_reference should not be prefixed, got %q", got)
}
}
func TestNormalizeCacheControlTTL_DowngradesLaterOneHourBlocks(t *testing.T) {
payload := []byte(`{
"tools": [{"name":"t1","cache_control":{"type":"ephemeral","ttl":"1h"}}],
"system": [{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}],
"messages": [{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]
}`)
out := normalizeCacheControlTTL(payload)
if got := gjson.GetBytes(out, "tools.0.cache_control.ttl").String(); got != "1h" {
t.Fatalf("tools.0.cache_control.ttl = %q, want %q", got, "1h")
}
if gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").Exists() {
t.Fatalf("messages.0.content.0.cache_control.ttl should be removed after a default-5m block")
}
}
func TestNormalizeCacheControlTTL_PreservesOriginalBytesWhenNoChange(t *testing.T) {
// Payload where no TTL normalization is needed (all blocks use 1h with no
// preceding 5m block). The text intentionally contains HTML chars (<, >, &)
// that json.Marshal would escape to \u003c etc., altering byte identity.
payload := []byte(`{"tools":[{"name":"t1","cache_control":{"type":"ephemeral","ttl":"1h"}}],"system":[{"type":"text","text":"<system-reminder>foo & bar</system-reminder>","cache_control":{"type":"ephemeral","ttl":"1h"}}],"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`)
out := normalizeCacheControlTTL(payload)
if !bytes.Equal(out, payload) {
t.Fatalf("normalizeCacheControlTTL altered bytes when no change was needed.\noriginal: %s\ngot: %s", payload, out)
}
}
func TestNormalizeCacheControlTTL_PreservesKeyOrderWhenModified(t *testing.T) {
payload := []byte(`{"model":"m","messages":[{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral","ttl":"1h"}}]}],"tools":[{"name":"t1","cache_control":{"type":"ephemeral"}}],"system":[{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}]}`)
out := normalizeCacheControlTTL(payload)
if gjson.GetBytes(out, "messages.0.content.0.cache_control.ttl").Exists() {
t.Fatalf("messages.0.content.0.cache_control.ttl should be removed after a default-5m block")
}
outStr := string(out)
idxModel := strings.Index(outStr, `"model"`)
idxMessages := strings.Index(outStr, `"messages"`)
idxTools := strings.Index(outStr, `"tools"`)
idxSystem := strings.Index(outStr, `"system"`)
if idxModel == -1 || idxMessages == -1 || idxTools == -1 || idxSystem == -1 {
t.Fatalf("failed to locate top-level keys in output: %s", outStr)
}
if !(idxModel < idxMessages && idxMessages < idxTools && idxTools < idxSystem) {
t.Fatalf("top-level key order changed:\noriginal: %s\ngot: %s", payload, out)
}
}
func TestEnforceCacheControlLimit_StripsNonLastToolBeforeMessages(t *testing.T) {
payload := []byte(`{
"tools": [
{"name":"t1","cache_control":{"type":"ephemeral"}},
{"name":"t2","cache_control":{"type":"ephemeral"}}
],
"system": [{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}],
"messages": [
{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral"}}]},
{"role":"user","content":[{"type":"text","text":"u2","cache_control":{"type":"ephemeral"}}]}
]
}`)
out := enforceCacheControlLimit(payload, 4)
if got := countCacheControls(out); got != 4 {
t.Fatalf("cache_control count = %d, want 4", got)
}
if gjson.GetBytes(out, "tools.0.cache_control").Exists() {
t.Fatalf("tools.0.cache_control should be removed first (non-last tool)")
}
if !gjson.GetBytes(out, "tools.1.cache_control").Exists() {
t.Fatalf("tools.1.cache_control (last tool) should be preserved")
}
if !gjson.GetBytes(out, "messages.0.content.0.cache_control").Exists() || !gjson.GetBytes(out, "messages.1.content.0.cache_control").Exists() {
t.Fatalf("message cache_control blocks should be preserved when non-last tool removal is enough")
}
}
func TestEnforceCacheControlLimit_PreservesKeyOrderWhenModified(t *testing.T) {
payload := []byte(`{"model":"m","messages":[{"role":"user","content":[{"type":"text","text":"u1","cache_control":{"type":"ephemeral"}},{"type":"text","text":"u2","cache_control":{"type":"ephemeral"}}]}],"tools":[{"name":"t1","cache_control":{"type":"ephemeral"}},{"name":"t2","cache_control":{"type":"ephemeral"}}],"system":[{"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}]}`)
out := enforceCacheControlLimit(payload, 4)
if got := countCacheControls(out); got != 4 {
t.Fatalf("cache_control count = %d, want 4", got)
}
if gjson.GetBytes(out, "tools.0.cache_control").Exists() {
t.Fatalf("tools.0.cache_control should be removed first (non-last tool)")
}
outStr := string(out)
idxModel := strings.Index(outStr, `"model"`)
idxMessages := strings.Index(outStr, `"messages"`)
idxTools := strings.Index(outStr, `"tools"`)
idxSystem := strings.Index(outStr, `"system"`)
if idxModel == -1 || idxMessages == -1 || idxTools == -1 || idxSystem == -1 {
t.Fatalf("failed to locate top-level keys in output: %s", outStr)
}
if !(idxModel < idxMessages && idxMessages < idxTools && idxTools < idxSystem) {
t.Fatalf("top-level key order changed:\noriginal: %s\ngot: %s", payload, out)
}
}
func TestEnforceCacheControlLimit_ToolOnlyPayloadStillRespectsLimit(t *testing.T) {
payload := []byte(`{
"tools": [
{"name":"t1","cache_control":{"type":"ephemeral"}},
{"name":"t2","cache_control":{"type":"ephemeral"}},
{"name":"t3","cache_control":{"type":"ephemeral"}},
{"name":"t4","cache_control":{"type":"ephemeral"}},
{"name":"t5","cache_control":{"type":"ephemeral"}}
]
}`)
out := enforceCacheControlLimit(payload, 4)
if got := countCacheControls(out); got != 4 {
t.Fatalf("cache_control count = %d, want 4", got)
}
if gjson.GetBytes(out, "tools.0.cache_control").Exists() {
t.Fatalf("tools.0.cache_control should be removed to satisfy max=4")
}
if !gjson.GetBytes(out, "tools.4.cache_control").Exists() {
t.Fatalf("last tool cache_control should be preserved when possible")
}
}
func TestClaudeExecutor_ExecuteSanitizesSignaturesBeforeUpstream(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-sonnet-4-5","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{
"model": "claude-sonnet-4-5",
"max_tokens": 16,
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"drop this","signature":""},
{"type":"text","text":"I will run git status."},
{"type":"tool_use","id":"Bash-1","name":"Bash","input":{"command":"git status"},"signature":"bad","thoughtSignature":"bad2","model":"claude-opus-4-1"}
]},
{"role":"user","content":[{"type":"tool_result","tool_use_id":"Bash-1","content":"ok"}]}
]
}`)
if _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-sonnet-4-5",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
Stream: false,
}); err != nil {
t.Fatalf("Execute error: %v", err)
}
parts := gjson.GetBytes(seenBody, "messages.0.content").Array()
if len(parts) != 2 {
t.Fatalf("messages.0.content length = %d, want 2; body=%s", len(parts), seenBody)
}
if parts[0].Get("type").String() != "text" {
t.Fatalf("first remaining part = %s, want text", parts[0].Raw)
}
toolUse := parts[1]
if toolUse.Get("type").String() != "tool_use" {
t.Fatalf("second remaining part = %s, want tool_use", toolUse.Raw)
}
for _, path := range []string{"signature", "thoughtSignature", "model"} {
if toolUse.Get(path).Exists() {
t.Fatalf("tool_use.%s should be removed before upstream: %s", path, seenBody)
}
}
}
func TestClaudeExecutor_Execute_InvalidGzipErrorBodyReturnsDecodeMessage(t *testing.T) {
testClaudeExecutorInvalidCompressedErrorBody(t, func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error {
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
return err
})
}
func TestClaudeExecutor_ExecuteStream_InvalidGzipErrorBodyReturnsDecodeMessage(t *testing.T) {
testClaudeExecutorInvalidCompressedErrorBody(t, func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error {
_, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
return err
})
}
func testClaudeExecutorInvalidCompressedErrorBody(
t *testing.T,
invoke func(executor *ClaudeExecutor, auth *cliproxyauth.Auth, payload []byte) error,
) {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Encoding", "gzip")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("not-a-valid-gzip-stream"))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
err := invoke(executor, auth, payload)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "failed to decode error response body") {
t.Fatalf("expected decode failure message, got: %v", err)
}
if statusProvider, ok := err.(interface{ StatusCode() int }); !ok || statusProvider.StatusCode() != http.StatusBadRequest {
t.Fatalf("expected status code 400, got: %v", err)
}
}
func TestEnsureModelMaxTokens_UsesRegisteredMaxCompletionTokens(t *testing.T) {
reg := registry.GetGlobalRegistry()
clientID := "test-claude-max-completion-tokens-client"
modelID := "test-claude-max-completion-tokens-model"
reg.RegisterClient(clientID, "claude", []*registry.ModelInfo{{
ID: modelID,
Type: "claude",
OwnedBy: "anthropic",
Object: "model",
Created: time.Now().Unix(),
MaxCompletionTokens: 4096,
UserDefined: true,
}})
defer reg.UnregisterClient(clientID)
input := []byte(`{"model":"test-claude-max-completion-tokens-model","messages":[{"role":"user","content":"hi"}]}`)
out := ensureModelMaxTokens(input, modelID)
if got := gjson.GetBytes(out, "max_tokens").Int(); got != 4096 {
t.Fatalf("max_tokens = %d, want %d", got, 4096)
}
}
func TestEnsureModelMaxTokens_DefaultsMissingValue(t *testing.T) {
reg := registry.GetGlobalRegistry()
clientID := "test-claude-default-max-tokens-client"
modelID := "test-claude-default-max-tokens-model"
reg.RegisterClient(clientID, "claude", []*registry.ModelInfo{{
ID: modelID,
Type: "claude",
OwnedBy: "anthropic",
Object: "model",
Created: time.Now().Unix(),
UserDefined: true,
}})
defer reg.UnregisterClient(clientID)
input := []byte(`{"model":"test-claude-default-max-tokens-model","messages":[{"role":"user","content":"hi"}]}`)
out := ensureModelMaxTokens(input, modelID)
if got := gjson.GetBytes(out, "max_tokens").Int(); got != defaultModelMaxTokens {
t.Fatalf("max_tokens = %d, want %d", got, defaultModelMaxTokens)
}
}
func TestEnsureModelMaxTokens_PreservesExplicitValue(t *testing.T) {
reg := registry.GetGlobalRegistry()
clientID := "test-claude-preserve-max-tokens-client"
modelID := "test-claude-preserve-max-tokens-model"
reg.RegisterClient(clientID, "claude", []*registry.ModelInfo{{
ID: modelID,
Type: "claude",
OwnedBy: "anthropic",
Object: "model",
Created: time.Now().Unix(),
MaxCompletionTokens: 4096,
UserDefined: true,
}})
defer reg.UnregisterClient(clientID)
input := []byte(`{"model":"test-claude-preserve-max-tokens-model","max_tokens":2048,"messages":[{"role":"user","content":"hi"}]}`)
out := ensureModelMaxTokens(input, modelID)
if got := gjson.GetBytes(out, "max_tokens").Int(); got != 2048 {
t.Fatalf("max_tokens = %d, want %d", got, 2048)
}
}
func TestEnsureModelMaxTokens_SkipsUnregisteredModel(t *testing.T) {
input := []byte(`{"model":"test-claude-unregistered-model","messages":[{"role":"user","content":"hi"}]}`)
out := ensureModelMaxTokens(input, "test-claude-unregistered-model")
if gjson.GetBytes(out, "max_tokens").Exists() {
t.Fatalf("max_tokens should remain unset, got %s", gjson.GetBytes(out, "max_tokens").Raw)
}
}
// TestClaudeExecutor_ExecuteStream_SetsIdentityAcceptEncoding verifies that streaming
// requests use Accept-Encoding: identity so the upstream cannot respond with a
// compressed SSE body that would silently break the line scanner.
func TestClaudeExecutor_ExecuteStream_SetsIdentityAcceptEncoding(t *testing.T) {
var gotEncoding, gotAccept string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotEncoding = r.Header.Get("Accept-Encoding")
gotAccept = r.Header.Get("Accept")
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"message_stop\"}\n\n"))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err != nil {
t.Fatalf("ExecuteStream error: %v", err)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("unexpected chunk error: %v", chunk.Err)
}
}
if gotEncoding != "identity" {
t.Errorf("Accept-Encoding = %q, want %q", gotEncoding, "identity")
}
if gotAccept != "text/event-stream" {
t.Errorf("Accept = %q, want %q", gotAccept, "text/event-stream")
}
}
// TestClaudeExecutor_Execute_SetsCompressedAcceptEncoding verifies that non-streaming
// requests keep the full accept-encoding to allow response compression (which
// decodeResponseBody handles correctly).
func TestClaudeExecutor_Execute_SetsCompressedAcceptEncoding(t *testing.T) {
var gotEncoding, gotAccept string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotEncoding = r.Header.Get("Accept-Encoding")
gotAccept = r.Header.Get("Accept")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet-20241022","role":"assistant","content":[{"type":"text","text":"hi"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
if gotEncoding != "gzip, deflate, br, zstd" {
t.Errorf("Accept-Encoding = %q, want %q", gotEncoding, "gzip, deflate, br, zstd")
}
if gotAccept != "application/json" {
t.Errorf("Accept = %q, want %q", gotAccept, "application/json")
}
}
// TestClaudeExecutor_ExecuteStream_GzipSuccessBodyDecoded verifies that a streaming
// HTTP 200 response with Content-Encoding: gzip is correctly decompressed before
// the line scanner runs, so SSE chunks are not silently dropped.
func TestClaudeExecutor_ExecuteStream_GzipSuccessBodyDecoded(t *testing.T) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte("data: {\"type\":\"message_stop\"}\n"))
_ = gz.Close()
compressedBody := buf.Bytes()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Content-Encoding", "gzip")
_, _ = w.Write(compressedBody)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err != nil {
t.Fatalf("ExecuteStream error: %v", err)
}
var combined strings.Builder
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("chunk error: %v", chunk.Err)
}
combined.Write(chunk.Payload)
}
if combined.Len() == 0 {
t.Fatal("expected at least one chunk from gzip-encoded SSE body, got none (body was not decompressed)")
}
if !strings.Contains(combined.String(), "message_stop") {
t.Errorf("expected SSE content in chunks, got: %q", combined.String())
}
}
func TestDecodeResponseBodyStackedRepeatedHeaders(t *testing.T) {
payload := []byte("stacked Claude response")
var gzipOutput bytes.Buffer
gzipWriter := gzip.NewWriter(&gzipOutput)
if _, errWrite := gzipWriter.Write(payload); errWrite != nil {
t.Fatal(errWrite)
}
if errClose := gzipWriter.Close(); errClose != nil {
t.Fatal(errClose)
}
var brotliOutput bytes.Buffer
brotliWriter := brotli.NewWriter(&brotliOutput)
if _, errWrite := brotliWriter.Write(gzipOutput.Bytes()); errWrite != nil {
t.Fatal(errWrite)
}
if errClose := brotliWriter.Close(); errClose != nil {
t.Fatal(errClose)
}
header := make(http.Header)
header.Add("Content-Encoding", "gzip")
header.Add("Content-Encoding", "br")
decoded, errDecode := decodeResponseBody(io.NopCloser(bytes.NewReader(brotliOutput.Bytes())), claudeResponseContentEncoding(header))
if errDecode != nil {
t.Fatal(errDecode)
}
defer decoded.Close()
got, errRead := io.ReadAll(decoded)
if errRead != nil {
t.Fatal(errRead)
}
if !bytes.Equal(got, payload) {
t.Fatalf("decoded body = %q, want %q", got, payload)
}
}
// TestDecodeResponseBody_MagicByteGzipNoHeader verifies that decodeResponseBody
// detects gzip-compressed content via magic bytes even when Content-Encoding is absent.
func TestDecodeResponseBody_MagicByteGzipNoHeader(t *testing.T) {
const plaintext = "data: {\"type\":\"message_stop\"}\n"
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte(plaintext))
_ = gz.Close()
rc := io.NopCloser(&buf)
decoded, err := decodeResponseBody(rc, "")
if err != nil {
t.Fatalf("decodeResponseBody error: %v", err)
}
defer decoded.Close()
got, err := io.ReadAll(decoded)
if err != nil {
t.Fatalf("ReadAll error: %v", err)
}
if string(got) != plaintext {
t.Errorf("decoded = %q, want %q", got, plaintext)
}
}
// TestDecodeResponseBody_MagicByteZstdNoHeader verifies that decodeResponseBody
// detects zstd-compressed content via magic bytes even when Content-Encoding is absent.
func TestDecodeResponseBody_MagicByteZstdNoHeader(t *testing.T) {
const plaintext = "data: {\"type\":\"message_stop\"}\n"
var buf bytes.Buffer
enc, err := zstd.NewWriter(&buf)
if err != nil {
t.Fatalf("zstd.NewWriter: %v", err)
}
_, _ = enc.Write([]byte(plaintext))
_ = enc.Close()
rc := io.NopCloser(&buf)
decoded, err := decodeResponseBody(rc, "")
if err != nil {
t.Fatalf("decodeResponseBody error: %v", err)
}
defer decoded.Close()
got, err := io.ReadAll(decoded)
if err != nil {
t.Fatalf("ReadAll error: %v", err)
}
if string(got) != plaintext {
t.Errorf("decoded = %q, want %q", got, plaintext)
}
}
// TestDecodeResponseBody_PlainTextNoHeader verifies that decodeResponseBody returns
// plain text untouched when Content-Encoding is absent and no magic bytes match.
func TestDecodeResponseBody_PlainTextNoHeader(t *testing.T) {
const plaintext = "data: {\"type\":\"message_stop\"}\n"
rc := io.NopCloser(strings.NewReader(plaintext))
decoded, err := decodeResponseBody(rc, "")
if err != nil {
t.Fatalf("decodeResponseBody error: %v", err)
}
defer decoded.Close()
got, err := io.ReadAll(decoded)
if err != nil {
t.Fatalf("ReadAll error: %v", err)
}
if string(got) != plaintext {
t.Errorf("decoded = %q, want %q", got, plaintext)
}
}
// TestClaudeExecutor_ExecuteStream_GzipNoContentEncodingHeader verifies the full
// pipeline: when the upstream returns a gzip-compressed SSE body WITHOUT setting
// Content-Encoding (a misbehaving upstream), the magic-byte sniff in
// decodeResponseBody still decompresses it, so chunks reach the caller.
func TestClaudeExecutor_ExecuteStream_GzipNoContentEncodingHeader(t *testing.T) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte("data: {\"type\":\"message_stop\"}\n"))
_ = gz.Close()
compressedBody := buf.Bytes()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
// Intentionally omit Content-Encoding to simulate misbehaving upstream.
_, _ = w.Write(compressedBody)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err != nil {
t.Fatalf("ExecuteStream error: %v", err)
}
var combined strings.Builder
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("chunk error: %v", chunk.Err)
}
combined.Write(chunk.Payload)
}
if combined.Len() == 0 {
t.Fatal("expected chunks from gzip body without Content-Encoding header, got none (magic-byte sniff failed)")
}
if !strings.Contains(combined.String(), "message_stop") {
t.Errorf("unexpected chunk content: %q", combined.String())
}
}
// TestClaudeExecutor_Execute_GzipErrorBodyNoContentEncodingHeader verifies that the
// error path (4xx) correctly decompresses a gzip body even when the upstream omits
// the Content-Encoding header. This closes the gap left by PR #1771, which only
// fixed header-declared compression on the error path.
func TestClaudeExecutor_Execute_GzipErrorBodyNoContentEncodingHeader(t *testing.T) {
const errJSON = `{"type":"error","error":{"type":"invalid_request_error","message":"test error"}}`
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte(errJSON))
_ = gz.Close()
compressedBody := buf.Bytes()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Intentionally omit Content-Encoding to simulate misbehaving upstream.
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(compressedBody)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err == nil {
t.Fatal("expected an error for 400 response, got nil")
}
if !strings.Contains(err.Error(), "test error") {
t.Errorf("error message should contain decompressed JSON, got: %q", err.Error())
}
}
// TestClaudeExecutor_ExecuteStream_GzipErrorBodyNoContentEncodingHeader verifies
// the same for the streaming executor: 4xx gzip body without Content-Encoding is
// decoded and the error message is readable.
func TestClaudeExecutor_ExecuteStream_GzipErrorBodyNoContentEncodingHeader(t *testing.T) {
const errJSON = `{"type":"error","error":{"type":"invalid_request_error","message":"stream test error"}}`
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
_, _ = gz.Write([]byte(errJSON))
_ = gz.Close()
compressedBody := buf.Bytes()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Intentionally omit Content-Encoding to simulate misbehaving upstream.
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write(compressedBody)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
_, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err == nil {
t.Fatal("expected an error for 400 response, got nil")
}
if !strings.Contains(err.Error(), "stream test error") {
t.Errorf("error message should contain decompressed JSON, got: %q", err.Error())
}
}
// TestClaudeExecutor_ExecuteStream_AcceptEncodingOverrideCannotBypassIdentity verifies that the
// streaming executor enforces Accept-Encoding: identity regardless of auth.Attributes override.
func TestClaudeExecutor_ExecuteStream_AcceptEncodingOverrideCannotBypassIdentity(t *testing.T) {
var gotEncoding string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotEncoding = r.Header.Get("Accept-Encoding")
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"message_stop\"}\n\n"))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
"header:Accept-Encoding": "gzip, deflate, br, zstd",
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("claude"),
})
if err != nil {
t.Fatalf("ExecuteStream error: %v", err)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("unexpected chunk error: %v", chunk.Err)
}
}
if gotEncoding != "identity" {
t.Errorf("Accept-Encoding = %q; stream path must enforce identity regardless of auth.Attributes override", gotEncoding)
}
}
// assertClaudeMidConversationSystemMessage checks a forwarded caller system prompt.
// wantTTL is "" for the native default marker and "1h" once
// upgradeClaudeCacheControlTTL has run, which only happens for OAuth credentials.
func assertClaudeMidConversationSystemMessage(t *testing.T, body []byte, messageIndex int, wantText, wantTTL string) {
t.Helper()
messagePath := fmt.Sprintf("messages.%d", messageIndex)
if got := gjson.GetBytes(body, messagePath+".role").String(); got != "system" {
t.Fatalf("%s.role = %q, want system", messagePath, got)
}
content := gjson.GetBytes(body, messagePath+".content").Array()
if len(content) != 1 {
t.Fatalf("%s.content has %d blocks, want 1", messagePath, len(content))
}
if got := content[0].Get("text").String(); got != wantText {
t.Fatalf("%s.content.0.text lost caller prompt: got len %d, want len %d", messagePath, len(got), len(wantText))
}
if got := content[0].Get("cache_control.type").String(); got != "ephemeral" {
t.Fatalf("%s.content.0.cache_control.type = %q, want ephemeral", messagePath, got)
}
if got := content[0].Get("cache_control.ttl").String(); got != wantTTL {
t.Fatalf("%s.content.0.cache_control.ttl = %q, want %q: %s", messagePath, got, wantTTL, content[0].Raw)
}
}
func assertClaudeLegacySystemReminderLayout(t *testing.T, body []byte, wantSystem, wantUser, wantTTL string) {
t.Helper()
if got := gjson.GetBytes(body, "system.#").Int(); got != 2 {
t.Fatalf("top-level system block count = %d, want billing and identity only", got)
}
if got := gjson.GetBytes(body, "messages.#").Int(); got != 1 {
t.Fatalf("message count = %d, want one user turn and no role=system", got)
}
content := gjson.GetBytes(body, "messages.0.content").Array()
if len(content) != 3 {
t.Fatalf("user content has %d blocks, want currentDate, caller reminder, and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
if got := content[1].Get("text").String(); got != claudeCallerSystemReminder(wantSystem) {
t.Fatalf("caller reminder lost system prompt: got len %d, want len %d", len(got), len(wantSystem))
}
if content[1].Get("cache_control").Exists() {
t.Fatalf("caller reminder unexpectedly has cache_control: %s", content[1].Raw)
}
assertEphemeralUserTextBlock(t, content[2], wantUser, wantTTL)
}
func assertClaudeCodeCurrentDateBlock(t *testing.T, block gjson.Result) {
t.Helper()
assertClaudeCodeCurrentDateBlockAt(t, block, time.Now())
}
func assertClaudeCodeCurrentDateBlockAt(t *testing.T, block gjson.Result, now time.Time) {
t.Helper()
if got := block.Get("type").String(); got != "text" {
t.Fatalf("currentDate block type = %q, want text", got)
}
if got, want := block.Get("text").String(), claudeCodeCurrentDateReminder(now); got != want {
t.Fatalf("currentDate reminder = %q, want %q", got, want)
}
if block.Get("cache_control").Exists() {
t.Fatalf("currentDate block must not contain cache_control: %s", block.Raw)
}
}
// assertEphemeralUserTextBlock checks the cloaked first-user block. wantTTL is ""
// for the native default marker and "1h" once upgradeClaudeCacheControlTTL has run,
// which only happens for OAuth credentials.
func assertEphemeralUserTextBlock(t *testing.T, block gjson.Result, wantText, wantTTL string) {
t.Helper()
if got := block.Get("type").String(); got != "text" {
t.Fatalf("user block type = %q, want text", got)
}
if got := block.Get("text").String(); got != wantText {
t.Fatalf("user block text = %q, want %q", got, wantText)
}
if got := block.Get("cache_control.type").String(); got != "ephemeral" {
t.Fatalf("user block cache_control.type = %q, want ephemeral", got)
}
if got := block.Get("cache_control.ttl").String(); got != wantTTL {
t.Fatalf("user block cache_control.ttl = %q, want %q: %s", got, wantTTL, block.Raw)
}
}
func TestClaudeBillingFingerprintUsesLatestUserText(t *testing.T) {
const prompt = "CPA_OFFICIAL_BASEURL_CLI_SYSTEM_EMPTY_b82d4e"
payload := []byte(`{"system":"must not seed the build hash","messages":[{"role":"user","content":"old"},{"role":"assistant","content":"answer"},{"role":"user","content":[{"type":"text","text":"<system-reminder>date</system-reminder>"},{"type":"text","text":"` + prompt + `"}]}]}`)
if got := claudeBillingFingerprintMessageText(payload); got != prompt {
t.Fatalf("claudeBillingFingerprintMessageText() = %q, want %q", got, prompt)
}
if got := computeFingerprint(prompt, "2.1.220"); got != "e06" {
t.Fatalf("computeFingerprint() = %q, want official 2.1.220 capture suffix e06", got)
}
}
func TestClaudeCodeLocalDateMatchesNativeLocalCalendarAlgorithm(t *testing.T) {
instant := time.Date(2026, time.July, 31, 15, 30, 0, 0, time.UTC)
kiritimati := time.FixedZone("Kiritimati", 14*60*60)
minusTwelve := time.FixedZone("Etc/GMT+12", -12*60*60)
if got := claudeCodeLocalDate(instant.In(kiritimati)); got != "2026-08-01" {
t.Fatalf("Kiritimati local date = %q, want 2026-08-01", got)
}
if got := claudeCodeLocalDate(instant.In(minusTwelve)); got != "2026-07-31" {
t.Fatalf("GMT-12 local date = %q, want 2026-07-31", got)
}
wantReminder := "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is 2026-08-01.\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n\n"
if got := claudeCodeCurrentDateReminder(instant.In(kiritimati)); got != wantReminder {
t.Fatalf("currentDate reminder = %q, want exact native text %q", got, wantReminder)
}
}
func TestClaudeCodeTimezoneUsesCredentialThenConfiguredProfile(t *testing.T) {
instant := time.Date(2026, time.August, 2, 1, 30, 0, 0, time.UTC)
cfg := &config.Config{ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{Timezone: "Asia/Tokyo"}}
auth := &cliproxyauth.Auth{Metadata: map[string]any{"timezone": "Pacific/Honolulu"}}
if got := claudeCodeLocalDate(instant.In(claudeCodeTimezone(cfg, auth))); got != "2026-08-01" {
t.Fatalf("credential currentDate = %q, want 2026-08-01", got)
}
if got := claudeCodeLocalDate(instant.In(claudeCodeTimezone(cfg, nil))); got != "2026-08-02" {
t.Fatalf("configured currentDate = %q, want 2026-08-02", got)
}
invalidAuth := &cliproxyauth.Auth{Metadata: map[string]any{"timezone": "not/a-timezone"}}
if got := claudeCodeTimezone(cfg, invalidAuth).String(); got != "Asia/Tokyo" {
t.Fatalf("invalid credential timezone = %q, want config fallback", got)
}
invalid := &config.Config{ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{Timezone: "not/a-timezone"}}
if got := claudeCodeTimezone(invalid, nil); got != time.Local {
t.Fatalf("invalid timezone location = %v, want time.Local", got)
}
}
func TestInjectClaudeCodeCurrentDateIsIdempotentAndAlignsFirstUserCache(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hello","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]}`)
first := injectClaudeCodeCurrentDate(payload, fixed)
if !bytes.Contains(first, []byte(`<system-reminder>`)) || bytes.Contains(first, []byte(`\u003csystem-reminder`)) {
t.Fatalf("currentDate angle brackets must match JSON.stringify bytes: %s", first)
}
second := injectClaudeCodeCurrentDate(first, fixed)
if !bytes.Equal(first, second) {
t.Fatalf("currentDate injection is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
content := gjson.GetBytes(first, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("first user content has %d blocks, want 2: %s", len(content), first)
}
if got := content[0].Get("text").String(); got != claudeCodeCurrentDateReminder(fixed) {
t.Fatalf("currentDate text = %q, want exact native reminder", got)
}
if content[0].Get("cache_control").Exists() {
t.Fatalf("currentDate block must not contain cache_control: %s", content[0].Raw)
}
assertEphemeralUserTextBlock(t, content[1], "hello", "")
}
func TestInjectClaudeCodeCurrentDateMovesExistingCopyToFirstBlock(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
dateBlock := buildTextBlock(claudeCodeCurrentDateReminder(fixed), nil)
payload := []byte(`{"messages":[{"role":"user","content":[` +
`{"type":"text","text":"hello"},` + dateBlock + `]}]}`)
out := injectClaudeCodeCurrentDate(payload, fixed)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("content has %d blocks, want one currentDate and user text: %s", len(content), out)
}
assertClaudeCodeCurrentDateBlockAt(t, content[0], fixed)
assertEphemeralUserTextBlock(t, content[1], "hello", "")
}
func TestInjectClaudeCodeCurrentDatePrecedesExistingReminder(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
reminder := "<system-reminder>\ncaller instructions\n</system-reminder>"
payload := []byte(`{"messages":[{"role":"user","content":[` +
buildTextBlock(reminder, nil) + `,` +
`{"type":"text","text":"continue","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]}`)
out := injectClaudeCodeCurrentDate(payload, fixed)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 3 {
t.Fatalf("content has %d blocks, want currentDate, reminder, and user text: %s", len(content), out)
}
assertClaudeCodeCurrentDateBlockAt(t, content[0], fixed)
if got := content[1].Get("text").String(); got != reminder {
t.Fatalf("content[1].text = %q, want standalone reminder", got)
}
assertEphemeralUserTextBlock(t, content[2], "continue", "")
}
func TestInjectClaudeCodeCurrentDateFollowsLeadingToolResults(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{}}]},` +
`{"role":"user","content":[` +
`{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},` +
`{"type":"text","text":"continue"}]}]}`)
first := injectClaudeCodeCurrentDate(payload, fixed)
second := injectClaudeCodeCurrentDate(first, fixed)
if !bytes.Equal(first, second) {
t.Fatalf("currentDate injection is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
content := gjson.GetBytes(first, "messages.1.content").Array()
if len(content) != 3 {
t.Fatalf("content has %d blocks, want tool_result, currentDate, and user text: %s", len(content), first)
}
if got := content[0].Get("type").String(); got != "tool_result" {
t.Fatalf("content[0].type = %q, want tool_result to stay first: %s", got, first)
}
if got := content[0].Get("tool_use_id").String(); got != "toolu_1" {
t.Fatalf("content[0].tool_use_id = %q, want toolu_1", got)
}
assertClaudeCodeCurrentDateBlockAt(t, content[1], fixed)
assertEphemeralUserTextBlock(t, content[2], "continue", "")
}
func TestInjectClaudeCodeCurrentDateFollowsAllLeadingToolResults(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[` +
`{"type":"tool_use","id":"toolu_1","name":"Read","input":{}},` +
`{"type":"tool_use","id":"toolu_2","name":"Read","input":{}}]},` +
`{"role":"user","content":[` +
`{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},` +
`{"type":"tool_result","tool_use_id":"toolu_2","content":"ok"}]}]}`)
out := injectClaudeCodeCurrentDate(payload, fixed)
content := gjson.GetBytes(out, "messages.1.content").Array()
if len(content) != 3 {
t.Fatalf("content has %d blocks, want two tool_results and currentDate: %s", len(content), out)
}
for idx, wantID := range []string{"toolu_1", "toolu_2"} {
if got := content[idx].Get("type").String(); got != "tool_result" {
t.Fatalf("content[%d].type = %q, want tool_result: %s", idx, got, out)
}
if got := content[idx].Get("tool_use_id").String(); got != wantID {
t.Fatalf("content[%d].tool_use_id = %q, want %q", idx, got, wantID)
}
}
assertClaudeCodeCurrentDateBlockAt(t, content[2], fixed)
}
// Test case 1: String system prompt becomes an authoritative mid-conversation
// system message after the first user turn.
func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
system := gjson.GetBytes(out, "system")
if !system.IsArray() {
t.Fatalf("system should be an array, got %s", system.Type)
}
blocks := system.Array()
if len(blocks) != 2 {
t.Fatalf("expected billing and identity blocks only, got %d", len(blocks))
}
if got := blocks[0].Get("text").String(); !strings.Contains(got, "cc_entrypoint=cli;") {
t.Fatalf("blocks[0] should use CLI billing attribution, got %q", got)
}
if blocks[1].Get("text").String() != claudeCodeCLIIdentity {
t.Fatalf("blocks[1] should be official CLI identity, got %q", blocks[1].Get("text").String())
}
if got := blocks[1].Get("cache_control.type").String(); got != "ephemeral" {
t.Fatalf("blocks[1] cache_control.type = %q, want ephemeral", got)
}
if blocks[1].Get("cache_control.ttl").Exists() {
t.Fatalf("blocks[1] cache_control must not carry a default ttl: %s", blocks[1].Raw)
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("messages[0].content has %d blocks, want currentDate and user text: %s", len(content), out)
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
assertClaudeMidConversationSystemMessage(t, out, 1, "You are a helpful assistant.", "")
}
func TestClaudeUsesLegacySystemReminder(t *testing.T) {
tests := map[string]bool{
"claude-opus-4-6": true,
"claude-opus-4-7": true,
"claude-sonnet-5": false,
"prefix/claude-sonnet-4-6": true,
"claude-3-5-haiku-latest": true,
"claude-opus-5": false,
"prefix/claude-opus-4-8": false,
"claude-fable-5": false,
"claude-future-6": false,
"": false,
}
for model, want := range tests {
t.Run(model, func(t *testing.T) {
payload := []byte(`{"model":` + fmt.Sprintf("%q", model) + `}`)
if got := claudeUsesLegacySystemReminder(payload); got != want {
t.Fatalf("claudeUsesLegacySystemReminder(%q) = %v, want %v", model, got, want)
}
})
}
}
func TestCheckSystemInstructionsWithMode_FutureModelDefaultsToMidSystem(t *testing.T) {
payload := []byte(`{"model":"claude-opus-6","system":"future instructions","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
if got := gjson.GetBytes(out, "system.#").Int(); got != 2 {
t.Fatalf("top-level system block count = %d, want 2", got)
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("user content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
assertClaudeMidConversationSystemMessage(t, out, 1, "future instructions", "")
}
func TestCheckSystemInstructionsWithMode_LegacyModelUsesSystemReminder(t *testing.T) {
payload := []byte(`{"model":"claude-opus-4-6","system":"legacy instructions","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
if got := gjson.GetBytes(out, "system.#").Int(); got != 2 {
t.Fatalf("top-level system block count = %d, want billing and identity only", got)
}
if got := gjson.GetBytes(out, "messages.#").Int(); got != 1 {
t.Fatalf("message count = %d, want no role=system insertion", got)
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 3 {
t.Fatalf("user content has %d blocks, want currentDate, caller reminder, and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
if got := content[1].Get("text").String(); got != claudeCallerSystemReminder("legacy instructions") {
t.Fatalf("caller system reminder = %q", got)
}
if content[1].Get("cache_control").Exists() {
t.Fatalf("caller system reminder unexpectedly has cache_control: %s", content[1].Raw)
}
assertEphemeralUserTextBlock(t, content[2], "hi", "")
}
func TestCheckSystemInstructionsWithMode_LegacyModelKeepsSystemBlocksSeparate(t *testing.T) {
payload := []byte(`{"model":"claude-opus-4-6","system":[` +
`{"type":"text","text":"first guidance","cache_control":{"type":"ephemeral","ttl":"1h"}},` +
`{"type":"text","text":"second guidance"}],` +
`"messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 4 {
t.Fatalf("user content has %d blocks, want currentDate, two caller reminders, and user text: %s", len(content), out)
}
assertClaudeCodeCurrentDateBlock(t, content[0])
for idx, want := range []string{"first guidance", "second guidance"} {
block := content[idx+1]
if got := block.Get("text").String(); got != claudeCallerSystemReminder(want) {
t.Fatalf("content[%d].text = %q, want separate caller reminder %q", idx+1, got, want)
}
if block.Get("cache_control").Exists() {
t.Fatalf("content[%d] caller reminder unexpectedly has cache_control: %s", idx+1, block.Raw)
}
}
assertEphemeralUserTextBlock(t, content[3], "hi", "")
}
// Test case 2: Strict mode keeps only the injected Claude Code system blocks.
func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) {
payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, true)
blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
t.Fatalf("strict mode should produce 2 injected blocks, got %d", len(blocks))
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("strict mode content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
}
// Test case 3: Empty string system prompt adds only currentDate before user text.
func TestCheckSystemInstructionsWithMode_EmptyStringSystemIgnored(t *testing.T) {
payload := []byte(`{"system":"","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
t.Fatalf("empty string system should still produce 2 injected blocks, got %d", len(blocks))
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("empty system content has %d blocks, want 2", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
}
// Test case 4: Array system prompt becomes one mid-conversation system message.
func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","system":[{"type":"text","text":"Be concise."}],"messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
t.Fatalf("expected 2 top-level system blocks, got %d", len(blocks))
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("messages[0].content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
assertClaudeMidConversationSystemMessage(t, out, 1, "Be concise.", "")
}
func TestCheckSystemInstructionsWithMode_ArraySystemKeepsBlocksAsSeparateMessages(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","system":[` +
`{"type":"text","text":"first guidance","cache_control":{"type":"ephemeral","ttl":"1h"}},` +
`{"type":"text","text":"second guidance"}],` +
`"messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
t.Fatalf("message count = %d, want user and two separate system messages: %s", got, out)
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("user content has %d blocks, want currentDate and user text: %s", len(content), out)
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
assertClaudeMidConversationSystemMessage(t, out, 1, "first guidance", "")
assertClaudeMidConversationSystemMessage(t, out, 2, "second guidance", "")
}
func TestRelocateClaudeSystemPromptForCountTokensKeepsBlocksSeparate(t *testing.T) {
tests := []struct {
name string
model string
legacy bool
}{
{name: "mid-system model", model: "claude-opus-5"},
{name: "legacy model", model: "claude-opus-4-6", legacy: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
payload := []byte(`{"model":"` + test.model + `","system":[` +
`{"type":"text","text":"first guidance"},` +
`{"type":"text","text":"second guidance"}],` +
`"messages":[{"role":"user","content":"hi"}]}`)
out := relocateClaudeSystemPromptForCountTokens(payload, false)
if gjson.GetBytes(out, "system").Exists() {
t.Fatalf("count_tokens system must be absent: %s", out)
}
if test.legacy {
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 3 {
t.Fatalf("legacy content has %d blocks, want two reminders and user text: %s", len(content), out)
}
if got := content[0].Get("text").String(); got != claudeCallerSystemReminder("first guidance") {
t.Fatalf("first caller reminder = %q", got)
}
if got := content[1].Get("text").String(); got != claudeCallerSystemReminder("second guidance") {
t.Fatalf("second caller reminder = %q", got)
}
if got := content[2].Get("text").String(); got != "hi" {
t.Fatalf("user text = %q, want hi", got)
}
return
}
if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
t.Fatalf("message count = %d, want user and two system messages: %s", got, out)
}
assertClaudeMidConversationSystemMessage(t, out, 1, "first guidance", "")
assertClaudeMidConversationSystemMessage(t, out, 2, "second guidance", "")
})
}
}
// Test case 5: Special characters survive the mid-conversation system move.
func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","system":"Use <xml> tags & \"quotes\" in output.","messages":[{"role":"user","content":"hi"}]}`)
out := checkSystemInstructionsWithMode(payload, false)
wantSystem := `Use <xml> tags & "quotes" in output.`
if got := gjson.GetBytes(out, "system.#").Int(); got != 2 {
t.Fatalf("top-level system block count = %d, want 2", got)
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("messages[0].content has %d blocks, want 2", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hi", "")
assertClaudeMidConversationSystemMessage(t, out, 1, wantSystem, "")
}
func TestCheckSystemInstructionsWithSigningMode_LongPromptIsExactAndIdempotent(t *testing.T) {
wantSystem := "\nPI_SYSTEM_BEGIN\nEmbedded reference: # currentDate\nToday's date is caller-owned text.\n" + strings.Repeat("Preserve tools, policies, and caller semantics exactly.\n", 560) + "PI_SYSTEM_END \n"
payloadMap := map[string]any{
"model": "claude-opus-5",
"system": wantSystem,
"messages": []any{map[string]any{
"role": "user",
"content": "hello",
}},
}
payload, errMarshal := json.Marshal(payloadMap)
if errMarshal != nil {
t.Fatalf("marshal payload: %v", errMarshal)
}
first := checkSystemInstructionsWithSigningMode(payload, false, true, "2.1.220", "cli", "")
second := checkSystemInstructionsWithSigningMode(first, false, true, "2.1.220", "cli", "")
if !bytes.Equal(first, second) {
t.Fatalf("complete cloak layout is not byte-idempotent:\nfirst: %s\nsecond: %s", first, second)
}
if got := gjson.GetBytes(first, "system.#").Int(); got != 2 {
t.Fatalf("top-level system block count = %d, want 2", got)
}
if got := gjson.GetBytes(first, "messages.#").Int(); got != 2 {
t.Fatalf("message count = %d, want user then system", got)
}
content := gjson.GetBytes(first, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("user content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "hello", "")
assertClaudeMidConversationSystemMessage(t, first, 1, wantSystem, "")
if strings.Contains(content[0].Get("text").String(), "PI_SYSTEM_BEGIN") || strings.Contains(content[1].Get("text").String(), "PI_SYSTEM_BEGIN") {
t.Fatal("caller system prompt leaked into the user content blocks")
}
if !bytes.Contains(first, []byte(`<system-reminder>`)) || bytes.Contains(first, []byte(`\u003csystem-reminder`)) {
t.Fatalf("currentDate reminder angle brackets must remain literal JSON bytes")
}
signed, errSign := finalizeAnthropicMessagesBodyCCH(first, "")
if errSign != nil {
t.Fatalf("finalize Claude CCH: %v", errSign)
}
resigned, errResign := finalizeAnthropicMessagesBodyCCH(signed, "")
if errResign != nil {
t.Fatalf("re-finalize Claude CCH: %v", errResign)
}
if !bytes.Equal(signed, resigned) {
t.Fatal("CCH finalization is not byte-idempotent after long prompt preservation")
}
}
func TestClaudeExecutor_CustomBaseURLPreservesBodyByDefault(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if strings.Contains(string(seenBody), "x-anthropic-billing-header:") || strings.Contains(string(seenBody), "cch=") {
t.Fatalf("default custom BaseURL request must not inject billing/CCH: %s", seenBody)
}
}
func TestClaudeExecutor_CustomBaseURLAPIKeyDoesNotEnableCCHSigning(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{
ClaudeKey: []config.ClaudeKey{{
APIKey: "key-123",
BaseURL: server.URL,
ExperimentalCCHSigning: true,
}},
})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
const messageText = "please keep literal cch=00000 in this message"
payload := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"please keep literal cch=00000 in this message"}]}]}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if got := gjson.GetBytes(seenBody, "messages.0.content.0.text").String(); got != messageText {
t.Fatalf("message text = %q, want %q", got, messageText)
}
if strings.Contains(string(seenBody), "x-anthropic-billing-header:") {
t.Fatalf("default custom BaseURL request must not inject a billing header: %s", seenBody)
}
}
func TestClaudeExecutor_CustomBaseURLOAuthGeneratesMissingCCH(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Attributes: map[string]string{
"api_key": "sk-ant-oat-custom-cch",
"base_url": server.URL,
"cloak_mode": "never",
},
Metadata: claudeOAuthTestMetadata(),
}
payload := []byte(`{"model":"claude-opus-4-6","system":"keep original system","messages":[{"role":"user","content":"hello"}],"max_tokens":64}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if _, ok := claudeBillingCCHDigitsOffset(seenBody); !ok {
t.Fatalf("Claude OAuth custom BaseURL body is missing generated CCH: %s", seenBody)
}
if got := gjson.GetBytes(seenBody, "system.1.text").String(); got != "keep original system" {
t.Fatalf("system.1.text = %q, want preserved system text", got)
}
}
func TestClaudeExecutor_RebuildMidSystemMessageDisabledByDefault(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{
ClaudeKey: []config.ClaudeKey{{
APIKey: "key-123",
BaseURL: server.URL,
}},
})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"system":[{"type":"text","text":"Top rule","cache_control":{"type":"ephemeral"}}],"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]},{"role":"system","content":"Mid rule"},{"role":"user","content":[{"type":"text","text":"continue"}]}],"metadata":{"user_id":"{\"device_id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"account_uuid\":\"\",\"session_id\":\"11111111-2222-4333-8444-555555555555\"}"}}`)
ctx := contextWithGinHeaders(map[string]string{
"User-Agent": "claude-cli/2.1.220 (external, cli)",
"X-App": "cli",
"Anthropic-Beta": "claude-code-20250219",
})
_, errExecute := executor.Execute(ctx, auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
if got := gjson.GetBytes(seenBody, "system.0.text").String(); got != "Top rule" {
t.Fatalf("system.0.text = %q, want top-level system preserved", got)
}
if got := gjson.GetBytes(seenBody, `messages.#(role=="system").content`).String(); got != "Mid rule" {
t.Fatalf("mid system message = %q, want original message preserved", got)
}
}
func TestClaudeExecutor_RebuildMidSystemMessageOptInMovesSystemMessages(t *testing.T) {
var seenBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
seenBody = bytes.Clone(body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-3-5-sonnet","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{
ClaudeKey: []config.ClaudeKey{{
APIKey: "key-123",
BaseURL: server.URL,
RebuildMidSystemMessage: true,
}},
})
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"api_key": "key-123",
"base_url": server.URL,
}}
payload := []byte(`{"system":"Top rule","messages":[{"role":"user","content":[{"type":"text","text":"hi"}]},{"role":"system","content":"Mid string rule"},{"role":"assistant","content":[{"type":"text","text":"ok"}]},{"role":"system","content":[{"type":"text","text":"Mid array rule","cache_control":{"type":"ephemeral"}}]},{"role":"user","content":[{"type":"text","text":"continue"}]}],"metadata":{"user_id":"{\"device_id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"account_uuid\":\"\",\"session_id\":\"11111111-2222-4333-8444-555555555555\"}"}}`)
ctx := contextWithGinHeaders(map[string]string{
"User-Agent": "claude-cli/2.1.220 (external, cli)",
"X-App": "cli",
"Anthropic-Beta": "claude-code-20250219",
})
_, errExecute := executor.Execute(ctx, auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("claude")})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if len(seenBody) == 0 {
t.Fatal("expected request body to be captured")
}
system := gjson.GetBytes(seenBody, "system").Array()
if len(system) != 3 {
t.Fatalf("system has %d items, want 3: %s", len(system), gjson.GetBytes(seenBody, "system").Raw)
}
wantTexts := []string{"Top rule", "Mid string rule", "Mid array rule"}
for i, want := range wantTexts {
if got := system[i].Get("text").String(); got != want {
t.Fatalf("system[%d].text = %q, want %q", i, got, want)
}
}
if got := gjson.GetBytes(seenBody, "system.2.cache_control.type").String(); got != "ephemeral" {
t.Fatalf("system.2.cache_control.type = %q, want ephemeral", got)
}
if gjson.GetBytes(seenBody, `messages.#(role=="system")`).Exists() {
t.Fatalf("messages should not contain system role after rebuild: %s", gjson.GetBytes(seenBody, "messages").Raw)
}
if got := gjson.GetBytes(seenBody, "messages.#").Int(); got != 3 {
t.Fatalf("messages count = %d, want 3", got)
}
}
func TestResolveClaudeWirePolicy(t *testing.T) {
tests := []struct {
name string
confirmed bool
mode string
wantCloak bool
}{
{name: "unknown auto", mode: "auto", wantCloak: true},
{name: "unknown always", mode: "always", wantCloak: true},
{name: "unknown never", mode: "never", wantCloak: false},
{name: "confirmed auto", confirmed: true, mode: "auto", wantCloak: false},
{name: "confirmed always", confirmed: true, mode: "always", wantCloak: false},
{name: "confirmed never", confirmed: true, mode: "never", wantCloak: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
auth := &cliproxyauth.Auth{Metadata: map[string]any{"cloak_mode": test.mode}}
policy, _ := resolveClaudeWirePolicy(&config.Config{}, auth, "sk-ant-oat-test", test.confirmed)
if !policy.OAuth {
t.Fatal("resolveClaudeWirePolicy() OAuth = false, want true")
}
if policy.ConfirmedClaudeCode != test.confirmed {
t.Fatalf("ConfirmedClaudeCode = %v, want %v", policy.ConfirmedClaudeCode, test.confirmed)
}
if policy.Cloak != test.wantCloak {
t.Fatalf("Cloak = %v, want %v", policy.Cloak, test.wantCloak)
}
})
}
}
func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmitted(t *testing.T) {
cfg := &config.Config{
ClaudeKey: []config.ClaudeKey{{
APIKey: "key-123",
Cloak: &config.CloakConfig{
StrictMode: true,
SensitiveWords: []string{"proxy"},
},
}},
}
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-123"}}
payload := []byte(`{"system":"proxy rules","messages":[{"role":"user","content":[{"type":"text","text":"proxy access"}]}]}`)
out, cloaked, errCloaking := applyCloaking(
context.Background(),
cfg,
auth,
payload,
"key-123",
false,
false,
)
if errCloaking != nil {
t.Fatalf("applyCloaking() error = %v", errCloaking)
}
if !cloaked {
t.Fatal("applyCloaking() cloaked = false, want true")
}
blocks := gjson.GetBytes(out, "system").Array()
if len(blocks) != 2 {
t.Fatalf("expected strict mode to keep the 2 injected Claude CLI system blocks, got %d", len(blocks))
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("strict mode should add only currentDate before user text, got %d content blocks", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
if got := content[1].Get("text").String(); !strings.Contains(got, "\u200B") {
t.Fatalf("expected configured sensitive word obfuscation to apply, got %q", got)
}
}
func TestNormalizeClaudeSamplingForUpstream_RemovesTemperature(t *testing.T) {
payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"}}`)
out := normalizeClaudeSamplingForUpstream(payload, false)
if gjson.GetBytes(out, "temperature").Exists() {
t.Fatalf("temperature should be removed")
}
}
func TestNormalizeClaudeSamplingForUpstream_RemovesTemperatureWithThinkingEnabled(t *testing.T) {
payload := []byte(`{"temperature":0.2,"thinking":{"type":"enabled","budget_tokens":2048}}`)
out := normalizeClaudeSamplingForUpstream(payload, false)
if gjson.GetBytes(out, "temperature").Exists() {
t.Fatalf("temperature should be removed")
}
}
func TestNormalizeClaudeSamplingForUpstream_RemovesTopPAndTopKForThinking(t *testing.T) {
payload := []byte(`{"temperature":0.2,"top_p":0.9,"top_k":40,"thinking":{"type":"adaptive"}}`)
out := normalizeClaudeSamplingForUpstream(payload, false)
if gjson.GetBytes(out, "temperature").Exists() {
t.Fatalf("temperature should be removed")
}
if gjson.GetBytes(out, "top_p").Exists() {
t.Fatalf("top_p should be removed when thinking is active")
}
if gjson.GetBytes(out, "top_k").Exists() {
t.Fatalf("top_k should be removed when thinking is active")
}
}
func TestNormalizeClaudeSamplingForUpstream_NoThinkingRemovesTemperatureAndTopP(t *testing.T) {
payload := []byte(`{"temperature":0,"top_p":0.9,"top_k":40,"messages":[{"role":"user","content":"hi"}]}`)
out := normalizeClaudeSamplingForUpstream(payload, false)
if gjson.GetBytes(out, "temperature").Exists() {
t.Fatalf("temperature should be removed")
}
if gjson.GetBytes(out, "top_p").Exists() {
t.Fatalf("top_p should be removed")
}
if got := gjson.GetBytes(out, "top_k").Int(); got != 40 {
t.Fatalf("top_k = %v, want 40", got)
}
}
func TestNormalizeClaudeSamplingForUpstream_AfterForcedToolChoiceRemovesTemperature(t *testing.T) {
payload := []byte(`{"temperature":0,"thinking":{"type":"adaptive"},"output_config":{"effort":"max"},"tool_choice":{"type":"any"}}`)
out := disableThinkingIfToolChoiceForced(payload)
out = normalizeClaudeSamplingForUpstream(out, false)
if gjson.GetBytes(out, "thinking").Exists() {
t.Fatalf("thinking should be removed when tool_choice forces tool use")
}
if gjson.GetBytes(out, "temperature").Exists() {
t.Fatalf("temperature should be removed")
}
}
// The measured structured Haiku helper sends "temperature":1, and
// claudeCodeHelperShapeStructured keys on exactly that value. Stripping it would
// make CPA emit a shape no native client produces, so a confirmed native caller
// must keep it.
func TestNormalizeClaudeSamplingForUpstreamNativeKeepsMeasuredHelperTemperature(t *testing.T) {
// Top-level key order and values mirror the measured structured helper.
payload := []byte(`{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":[{"type":"text","text":"helper probe"}]}],"system":[{"type":"text","text":"Return a short title."}],"tools":[],"metadata":{"user_id":"u"},"max_tokens":32000,"thinking":{"type":"disabled"},"temperature":1,"output_config":{"format":{"type":"json_schema"}},"stream":true}`)
if got := gjson.GetBytes(payload, "temperature"); !got.Exists() || got.Num != 1 {
t.Fatalf("measured helper fixture should carry temperature=1, got %q", got.Raw)
}
out := normalizeClaudeSamplingForUpstream(payload, true)
if got := gjson.GetBytes(out, "temperature"); !got.Exists() || got.Num != 1 {
t.Fatalf("confirmed native must preserve the measured temperature, got %q", got.Raw)
}
}
// Anthropic's real constraints, verified against the live API: with thinking
// active temperature must be 1, top_p must be >= 0.95 and top_k must be unset;
// otherwise temperature and top_p cannot both be specified. Preserving the
// native wire must never forward a combination that would 400.
func TestNormalizeClaudeSamplingForUpstreamNativeDropsOnlyRejectedCombinations(t *testing.T) {
tests := []struct {
name string
payload string
keep map[string]float64
dropped []string
}{
{
name: "thinking off keeps every accepted knob",
payload: `{"temperature":0.5,"top_k":40}`,
keep: map[string]float64{"temperature": 0.5, "top_k": 40},
},
{
name: "thinking off drops top_p when temperature is also set",
payload: `{"temperature":0.5,"top_p":0.9}`,
keep: map[string]float64{"temperature": 0.5},
dropped: []string{"top_p"},
},
{
name: "thinking off keeps a lone top_p",
payload: `{"top_p":0.9}`,
keep: map[string]float64{"top_p": 0.9},
},
{
name: "thinking disabled is not thinking",
payload: `{"temperature":1,"thinking":{"type":"disabled"}}`,
keep: map[string]float64{"temperature": 1},
},
{
name: "thinking enabled keeps temperature 1",
payload: `{"temperature":1,"thinking":{"type":"enabled","budget_tokens":1024}}`,
keep: map[string]float64{"temperature": 1},
},
{
name: "thinking enabled drops temperature that is not 1",
payload: `{"temperature":0.5,"thinking":{"type":"enabled","budget_tokens":1024}}`,
dropped: []string{"temperature"},
},
{
name: "thinking enabled keeps top_p at or above 0.95",
payload: `{"top_p":0.99,"thinking":{"type":"enabled","budget_tokens":1024}}`,
keep: map[string]float64{"top_p": 0.99},
},
{
name: "thinking enabled drops top_p below 0.95",
payload: `{"top_p":0.9,"thinking":{"type":"enabled","budget_tokens":1024}}`,
dropped: []string{"top_p"},
},
{
name: "thinking enabled always drops top_k",
payload: `{"top_k":40,"thinking":{"type":"enabled","budget_tokens":1024}}`,
dropped: []string{"top_k"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
out := normalizeClaudeSamplingForUpstream([]byte(tc.payload), true)
for field, want := range tc.keep {
got := gjson.GetBytes(out, field)
if !got.Exists() || got.Num != want {
t.Fatalf("%s = %q, want %v preserved", field, got.Raw, want)
}
}
for _, field := range tc.dropped {
if got := gjson.GetBytes(out, field); got.Exists() {
t.Fatalf("%s = %q, want dropped because Anthropic rejects it", field, got.Raw)
}
}
})
}
}
func TestRemapOAuthToolNames_AllClientNamesUseMCPAliases(t *testing.T) {
for _, original := range []string{"Bash", "bash", "Glob", "glob"} {
t.Run(original, func(t *testing.T) {
body := []byte(`{"tools":[{"name":` + fmt.Sprintf("%q", original) + `,"description":"Run a client tool","input_schema":{"type":"object"}}]}`)
out, reverseMap := remapOAuthToolNames(body)
alias := gjson.GetBytes(out, "tools.0.name").String()
if !helps.IsClaudeMCPToolName(alias) {
t.Fatalf("tools.0.name = %q, want MCP alias", alias)
}
if reverseMap[alias] != original {
t.Fatalf("reverseMap = %v, want %q -> %q", reverseMap, alias, original)
}
resp := []byte(`{"content":[{"type":"tool_use","id":"toolu_01","name":` + fmt.Sprintf("%q", alias) + `,"input":{}}]}`)
reversed, errReverse := reverseRemapOAuthToolNames(resp, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNames() error = %v", errReverse)
}
if got := gjson.GetBytes(reversed, "content.0.name").String(); got != original {
t.Fatalf("content.0.name = %q, want %q", got, original)
}
})
}
}
func TestRemapOAuthToolNames_AllClientToolsAsMCP(t *testing.T) {
body := []byte(`{
"tools":[
{"type":"web_search_20250305","name":"web_search","max_uses":2},
{"name":"bash","description":"client shell tool","input_schema":{"type":"object"}},
{"name":"Read","description":"client read tool","input_schema":{"type":"object"}},
{"name":"mcp__context7__query-docs","description":"existing MCP tool","input_schema":{"type":"object"}},
{"name":"search_web","description":"unknown one","input_schema":{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}},
{"name":"Search_Web","description":"case-distinct unknown","input_schema":{"type":"object"}},
{"name":"search_web","description":"repeated declaration","input_schema":{"type":"object"}}
],
"tool_choice":{"type":"tool","name":"search_web"},
"messages":[
{"role":"assistant","content":[
{"type":"tool_use","id":"toolu_unknown","name":"search_web","input":{"q":"go"}},
{"type":"tool_reference","tool_name":"Search_Web"}
]},
{"role":"user","content":[
{"type":"tool_result","tool_use_id":"toolu_unknown","content":[{"type":"tool_reference","tool_name":"search_web"}]}
]}
]
}`)
out, reverseMap := remapOAuthToolNamesWithOptions(body, claudeMCPAliasOptions{secret: "credential-secret"})
if got := gjson.GetBytes(out, "tools.0.name").String(); got != "web_search" {
t.Fatalf("typed builtin = %q, want unchanged", got)
}
bashAlias := gjson.GetBytes(out, "tools.1.name").String()
readAlias := gjson.GetBytes(out, "tools.2.name").String()
if !helps.IsClaudeMCPToolName(bashAlias) || !helps.IsClaudeMCPToolName(readAlias) {
t.Fatalf("former vetted names did not receive MCP aliases: bash=%q Read=%q", bashAlias, readAlias)
}
if got := gjson.GetBytes(out, "tools.1.description").String(); got != "client shell tool" {
t.Fatalf("bash description = %q, want preserved", got)
}
if got := gjson.GetBytes(out, "tools.1.input_schema.type").String(); got != "object" {
t.Fatalf("bash schema changed: %s", out)
}
if got := gjson.GetBytes(out, "tools.3.name").String(); got != "mcp__context7__query-docs" {
t.Fatalf("existing MCP tool = %q, want unchanged", got)
}
searchAlias := gjson.GetBytes(out, "tools.4.name").String()
caseAlias := gjson.GetBytes(out, "tools.5.name").String()
if !helps.IsClaudeMCPToolName(searchAlias) || !helps.IsClaudeMCPToolName(caseAlias) {
t.Fatalf("generated aliases are invalid: %q, %q", searchAlias, caseAlias)
}
if searchAlias == caseAlias {
t.Fatalf("case-distinct names share alias %q", searchAlias)
}
if got := gjson.GetBytes(out, "tools.6.name").String(); got != searchAlias {
t.Fatalf("repeated declaration alias = %q, want %q", got, searchAlias)
}
if !strings.HasSuffix(searchAlias, "_search_web") || !strings.HasSuffix(caseAlias, "_Search_Web") {
t.Fatalf("generated aliases lost semantic suffixes: %q, %q", searchAlias, caseAlias)
}
if len(searchAlias) > 64 || len(caseAlias) > 64 {
t.Fatalf("generated aliases exceed 64 characters: %q, %q", searchAlias, caseAlias)
}
if got := gjson.GetBytes(out, "tools.4.description").String(); got != "unknown one" {
t.Fatalf("description = %q, want preserved", got)
}
if got := gjson.GetBytes(out, "tools.4.input_schema.required.0").String(); got != "q" {
t.Fatalf("input schema was not preserved: %s", out)
}
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != searchAlias {
t.Fatalf("tool_choice.name = %q, want %q", got, searchAlias)
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != searchAlias {
t.Fatalf("historical tool_use.name = %q, want %q", got, searchAlias)
}
if got := gjson.GetBytes(out, "messages.0.content.0.id").String(); got != "toolu_unknown" {
t.Fatalf("tool_use.id = %q, want unchanged", got)
}
if got := gjson.GetBytes(out, "messages.0.content.1.tool_name").String(); got != caseAlias {
t.Fatalf("tool_reference.tool_name = %q, want %q", got, caseAlias)
}
if got := gjson.GetBytes(out, "messages.1.content.0.content.0.tool_name").String(); got != searchAlias {
t.Fatalf("nested tool_reference.tool_name = %q, want %q", got, searchAlias)
}
if reverseMap[searchAlias] != "search_web" || reverseMap[caseAlias] != "Search_Web" ||
reverseMap[bashAlias] != "bash" || reverseMap[readAlias] != "Read" {
t.Fatalf("reverseMap = %v, want exact client names", reverseMap)
}
response := []byte(fmt.Sprintf(`{"content":[
{"type":"tool_use","id":"toolu_unknown","name":%q,"input":{}},
{"type":"tool_reference","tool_name":%q},
{"type":"tool_result","tool_use_id":"toolu_unknown","content":[{"type":"tool_reference","tool_name":%q}]}
]}`, searchAlias, caseAlias, searchAlias))
restored, errReverse := reverseRemapOAuthToolNames(response, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNames() error = %v", errReverse)
}
if got := gjson.GetBytes(restored, "content.0.name").String(); got != "search_web" {
t.Fatalf("restored tool_use.name = %q, want search_web", got)
}
if got := gjson.GetBytes(restored, "content.1.tool_name").String(); got != "Search_Web" {
t.Fatalf("restored tool_reference.tool_name = %q, want Search_Web", got)
}
if got := gjson.GetBytes(restored, "content.2.content.0.tool_name").String(); got != "search_web" {
t.Fatalf("restored nested tool_reference = %q, want search_web", got)
}
streamLine := []byte(fmt.Sprintf(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_unknown","name":%q,"input":{}}}`, searchAlias))
restoredLine, errReverse := reverseRemapOAuthToolNamesFromStreamLine(streamLine, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNamesFromStreamLine() error = %v", errReverse)
}
if got := gjson.GetBytes(helps.JSONPayload(restoredLine), "content_block.name").String(); got != "search_web" {
t.Fatalf("restored stream name = %q, want search_web: %s", got, restoredLine)
}
}
func TestRemapOAuthToolNames_TypedCustomUsesMCPAlias(t *testing.T) {
body := []byte(`{
"tools":[
{"type":"custom","name":"client_custom","description":"keep","input_schema":{"type":"object","properties":{"value":{"type":"string"}}}},
{"type":"web_search_20250305","name":"web_search","max_uses":2},
{"type":"client_extension_v1","name":"client_extension","description":"extension","input_schema":{"type":"object"}}
],
"tool_choice":{"type":"tool","name":"client_custom"},
"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_custom","name":"client_custom","input":{}}]}]
}`)
out, reverseMap := remapOAuthToolNamesWithOptions(body, claudeMCPAliasOptions{secret: "caller-secret"})
alias := gjson.GetBytes(out, "tools.0.name").String()
if !helps.IsClaudeMCPToolName(alias) {
t.Fatalf("typed custom alias = %q, want MCP name", alias)
}
if gjson.GetBytes(out, "tools.0.type").Exists() {
t.Fatalf("typed custom type was not normalized away: %s", out)
}
if got := gjson.GetBytes(out, "tools.0.description").String(); got != "keep" {
t.Fatalf("typed custom description = %q, want preserved", got)
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "web_search" {
t.Fatalf("server builtin name = %q, want unchanged", got)
}
extensionAlias := gjson.GetBytes(out, "tools.2.name").String()
if !helps.IsClaudeMCPToolName(extensionAlias) || gjson.GetBytes(out, "tools.2.type").Exists() {
t.Fatalf("unknown typed client tool was not normalized: %s", out)
}
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != alias {
t.Fatalf("tool_choice.name = %q, want %q", got, alias)
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != alias {
t.Fatalf("historical tool_use.name = %q, want %q", got, alias)
}
if reverseMap[alias] != "client_custom" || reverseMap[extensionAlias] != "client_extension" {
t.Fatalf("reverseMap = %v, want exact typed client names", reverseMap)
}
}
func TestRemapOAuthToolNames_MCPAliasAvoidsClientCollision(t *testing.T) {
const secret = "credential-secret"
initialCandidate := helps.ClaudeMCPToolAlias(secret, "fetch_url", 0)
body := []byte(fmt.Sprintf(`{"tools":[
{"name":%q,"input_schema":{"type":"object"}},
{"name":"fetch_url","input_schema":{"type":"object"}}
]}`, initialCandidate))
out, reverseMap := remapOAuthToolNamesWithOptions(body, claudeMCPAliasOptions{secret: secret})
if got := gjson.GetBytes(out, "tools.0.name").String(); got != initialCandidate {
t.Fatalf("existing MCP tool = %q, want %q", got, initialCandidate)
}
alias := gjson.GetBytes(out, "tools.1.name").String()
if alias == initialCandidate {
t.Fatalf("generated alias collided with client MCP name %q", alias)
}
if reverseMap[alias] != "fetch_url" {
t.Fatalf("reverseMap = %v, want %q -> fetch_url", reverseMap, alias)
}
}
func TestRemapOAuthToolNames_MCPAliasIsMandatory(t *testing.T) {
body := []byte(`{"tools":[{"name":"search_web","input_schema":{"type":"object"}}]}`)
out, reverseMap := remapOAuthToolNames(body)
alias := gjson.GetBytes(out, "tools.0.name").String()
if !helps.IsClaudeMCPToolName(alias) {
t.Fatalf("tools.0.name = %q, want mandatory MCP alias", alias)
}
if reverseMap[alias] != "search_web" {
t.Fatalf("reverseMap = %v, want alias -> search_web", reverseMap)
}
}
func TestRemapOAuthToolNames_SemanticAliasRestoresLongOriginal(t *testing.T) {
original := "Read.file/with a very long semantic name and Unicode 网页内容 that exceeds the wire limit"
body := []byte(`{"tools":[{"name":` + fmt.Sprintf("%q", original) + `,"input_schema":{"type":"object"}}]}`)
options := claudeMCPAliasOptions{secret: "stable-caller"}
out, reverseMap := remapOAuthToolNamesWithOptions(body, options)
alias := gjson.GetBytes(out, "tools.0.name").String()
if !helps.IsClaudeMCPToolName(alias) || len(alias) > 64 {
t.Fatalf("semantic alias is invalid or too long: len=%d name=%q", len(alias), alias)
}
if !strings.Contains(alias, "_Read_file_with_a_very_long") {
t.Fatalf("semantic alias %q does not expose the truncated original meaning", alias)
}
if reverseMap[alias] != original {
t.Fatalf("reverseMap lost exact original: got %q, want %q", reverseMap[alias], original)
}
second, _ := remapOAuthToolNamesWithOptions(body, options)
if got := gjson.GetBytes(second, "tools.0.name").String(); got != alias {
t.Fatalf("semantic alias is not stable across requests: %q != %q", got, alias)
}
response := []byte(`{"content":[{"type":"tool_use","id":"toolu_1","name":` + fmt.Sprintf("%q", alias) + `,"input":{}}]}`)
restored, errReverse := reverseRemapOAuthToolNames(response, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNames() error = %v", errReverse)
}
if got := gjson.GetBytes(restored, "content.0.name").String(); got != original {
t.Fatalf("restored tool name = %q, want exact original %q", got, original)
}
}
func TestPrepareClaudeOAuthToolNamesForUpstream_PreservesMCPConvention(t *testing.T) {
body := []byte(`{"tools":[
{"name":"search_web","input_schema":{"type":"object"}},
{"name":"mcp__context7__query-docs","input_schema":{"type":"object"}},
{"name":"bash","input_schema":{"type":"object"}}
],"tool_choice":{"type":"tool","name":"search_web"}}`)
out, reverseMap := prepareClaudeOAuthToolNamesForUpstream(body, claudeMCPAliasOptions{secret: "credential-secret"})
alias := gjson.GetBytes(out, "tools.0.name").String()
if !helps.IsClaudeMCPToolName(alias) || strings.HasPrefix(alias, "proxy_") {
t.Fatalf("unknown alias = %q, want bare mcp__ name", alias)
}
if got := gjson.GetBytes(out, "tools.1.name").String(); got != "mcp__context7__query-docs" {
t.Fatalf("existing MCP name = %q, want unchanged", got)
}
bashAlias := gjson.GetBytes(out, "tools.2.name").String()
if !helps.IsClaudeMCPToolName(bashAlias) || strings.HasPrefix(bashAlias, "proxy_") {
t.Fatalf("former vetted tool = %q, want bare MCP alias", bashAlias)
}
if got := gjson.GetBytes(out, "tool_choice.name").String(); got != alias {
t.Fatalf("tool_choice.name = %q, want %q", got, alias)
}
if reverseMap[alias] != "search_web" || reverseMap[bashAlias] != "bash" {
t.Fatalf("reverseMap = %v, want exact alias restoration", reverseMap)
}
}
func TestResolveClaudeMCPAliasOptions(t *testing.T) {
if options := resolveClaudeMCPAliasOptions(context.Background()); options.secret == "" {
t.Fatal("default caller alias secret is empty")
}
gin.SetMode(gin.TestMode)
ginCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
ginCtx.Set("userApiKey", "downstream-caller-one")
callerCtx := context.WithValue(context.Background(), "gin", ginCtx)
firstSecret := resolveClaudeMCPAliasOptions(callerCtx).secret
secondSecret := resolveClaudeMCPAliasOptions(callerCtx).secret
if firstSecret == "" || secondSecret != firstSecret {
t.Fatalf("caller alias secret is unstable: %q != %q", firstSecret, secondSecret)
}
otherGinCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
otherGinCtx.Set("userApiKey", "downstream-caller-two")
otherCtx := context.WithValue(context.Background(), "gin", otherGinCtx)
if otherSecret := resolveClaudeMCPAliasOptions(otherCtx).secret; otherSecret == firstSecret {
t.Fatalf("different downstream callers shared alias secret %q", firstSecret)
}
}
func TestRemapOAuthToolNames_MixedCaseNamesRemainDistinct(t *testing.T) {
body := []byte(`{"tools":[` +
`{"name":"Bash","input_schema":{"type":"object"}},` +
`{"name":"bash","input_schema":{"type":"object"}}` +
`]}`)
out, reverseMap := remapOAuthToolNames(body)
upperAlias := gjson.GetBytes(out, "tools.0.name").String()
lowerAlias := gjson.GetBytes(out, "tools.1.name").String()
if !helps.IsClaudeMCPToolName(upperAlias) || !helps.IsClaudeMCPToolName(lowerAlias) || upperAlias == lowerAlias {
t.Fatalf("mixed-case aliases = %q, %q, want distinct MCP names", upperAlias, lowerAlias)
}
if reverseMap[upperAlias] != "Bash" || reverseMap[lowerAlias] != "bash" {
t.Fatalf("reverseMap = %v, want exact mixed-case names", reverseMap)
}
}
// TestReverseRemapOAuthToolNamesFromStreamLine_HonorsPerRequestMap guards the
// SSE streaming code path against the same mixed-case bug.
func TestReverseRemapOAuthToolNamesFromStreamLine_HonorsPerRequestMap(t *testing.T) {
reverseMap := map[string]string{"Glob": "glob"}
// Bash block was never renamed, must pass through as-is.
bashLine := []byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01","name":"Bash","input":{}}}`)
out, errReverse := reverseRemapOAuthToolNamesFromStreamLine(bashLine, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNamesFromStreamLine() error = %v", errReverse)
}
if !bytes.Contains(out, []byte(`"name":"Bash"`)) {
t.Fatalf("Bash should be preserved, got: %s", string(out))
}
if bytes.Contains(out, []byte(`"name":"bash"`)) {
t.Fatalf("Bash must not be lowercased, got: %s", string(out))
}
// Glob block IS in the reverseMap, must be restored to `glob`.
globLine := []byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_02","name":"Glob","input":{}}}`)
out, errReverse = reverseRemapOAuthToolNamesFromStreamLine(globLine, reverseMap)
if errReverse != nil {
t.Fatalf("reverseRemapOAuthToolNamesFromStreamLine() error = %v", errReverse)
}
if !bytes.Contains(out, []byte(`"name":"glob"`)) {
t.Fatalf("Glob should be restored to glob, got: %s", string(out))
}
}
func TestPrepareClaudeOAuthToolNamesForUpstream_AllCustomToolsWithHistory(t *testing.T) {
body := []byte(`{"tools":[` +
`{"name":"Bash","input_schema":{"type":"object","properties":{"cmd":{"type":"string"}}}},` +
`{"name":"glob","input_schema":{"type":"object","properties":{"filePattern":{"type":"string"}}}}` +
`],"messages":[{"role":"assistant","content":[` +
`{"type":"tool_use","id":"toolu_01","name":"Bash","input":{}},` +
`{"type":"tool_use","id":"toolu_02","name":"glob","input":{}}` +
`]}]}`)
out, reverseMap := prepareClaudeOAuthToolNamesForUpstream(body, claudeMCPAliasOptions{secret: "mixed-case-caller"})
bashAlias := gjson.GetBytes(out, "tools.0.name").String()
globAlias := gjson.GetBytes(out, "tools.1.name").String()
if !helps.IsClaudeMCPToolName(bashAlias) || !helps.IsClaudeMCPToolName(globAlias) || bashAlias == globAlias {
t.Fatalf("tool aliases = %q, %q, want distinct bare MCP names", bashAlias, globAlias)
}
if got := gjson.GetBytes(out, "messages.0.content.0.name").String(); got != bashAlias {
t.Fatalf("messages.0.content.0.name = %q, want %q", got, bashAlias)
}
if got := gjson.GetBytes(out, "messages.0.content.1.name").String(); got != globAlias {
t.Fatalf("messages.0.content.1.name = %q, want %q", got, globAlias)
}
if reverseMap[bashAlias] != "Bash" || reverseMap[globAlias] != "glob" {
t.Fatalf("reverseMap = %v, want exact client names", reverseMap)
}
}
func TestClaudeExecutor_ExecuteOpenAINonStreamRestoresOAuthToolNames(t *testing.T) {
upstreamBody := strings.Join([]string{
`event: message_start`,
`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022","usage":{"input_tokens":10,"output_tokens":1}}}`,
`event: content_block_start`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01","name":"Bash","input":{}}}`,
`event: content_block_delta`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"command\": \"echo hi\"}"}}`,
`event: content_block_stop`,
`data: {"type":"content_block_stop","index":0}`,
`event: message_delta`,
`data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":30}}`,
`event: message_stop`,
`data: {"type":"message_stop"}`,
``,
}, "\n")
type upstreamRequest struct {
toolName string
stream bool
}
upstreamRequests := make(chan upstreamRequest, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, errRead := io.ReadAll(r.Body)
if errRead != nil {
http.Error(w, errRead.Error(), http.StatusBadRequest)
return
}
toolName := gjson.GetBytes(body, "tools.0.name").String()
upstreamRequests <- upstreamRequest{
toolName: toolName,
stream: gjson.GetBytes(body, "stream").Bool(),
}
w.Header().Set("Content-Type", "text/event-stream")
responseBody := strings.Replace(upstreamBody, `"name":"Bash"`, `"name":`+fmt.Sprintf("%q", toolName), 1)
_, _ = w.Write([]byte(responseBody))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Attributes: map[string]string{
"api_key": "sk-ant-oat01-test",
"base_url": server.URL,
},
Metadata: claudeOAuthTestMetadata(),
}
payload := []byte(`{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":"run echo hi"}],` +
`"tools":[{"type":"function","function":{"name":"bash","description":"run shell",` +
`"parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}}]}`)
resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-3-5-sonnet-20241022",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
})
if err != nil {
t.Fatalf("Execute error: %v", err)
}
upstream := <-upstreamRequests
if !upstream.stream {
t.Fatal("upstream stream = false, want true")
}
if !helps.IsClaudeMCPToolName(upstream.toolName) || !strings.HasSuffix(upstream.toolName, "_bash") {
t.Fatalf("upstream tools.0.name = %q, want semantic MCP alias", upstream.toolName)
}
if got := gjson.GetBytes(resp.Payload, "choices.0.message.tool_calls.0.function.name").String(); got != "bash" {
t.Fatalf("tool_calls.0.function.name = %q, want %q; payload=%s", got, "bash", string(resp.Payload))
}
}
func TestClaudeExecutor_ExecuteOAuthCustomToolMCPAliasRoundTrip(t *testing.T) {
var upstreamAlias string
var upstreamBody []byte
var upstreamHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
upstreamBody = bytes.Clone(body)
upstreamHeaders = r.Header.Clone()
upstreamAlias = gjson.GetBytes(body, "tools.0.name").String()
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"id":"msg_1","type":"message","role":"assistant","model":"claude-opus-4-6","content":[{"type":"tool_use","id":"toolu_1","name":%q,"input":{"query":"go"}}],"stop_reason":"tool_use","usage":{"input_tokens":1,"output_tokens":1}}`, upstreamAlias)
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-mcp-round-trip",
Attributes: map[string]string{
"api_key": "sk-ant-oat-mcp-round-trip",
"base_url": server.URL,
},
Metadata: claudeOAuthTestMetadata(),
}
payload := []byte(`{"model":"claude-opus-5","system":"messages-system-prompt","messages":[{"role":"user","content":"search"}],"tools":[{"name":"search_web","description":"search","input_schema":{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}}]}`)
resp, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-5",
Payload: payload,
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
if !helps.IsClaudeMCPToolName(upstreamAlias) || strings.HasPrefix(upstreamAlias, "proxy_") || !strings.HasSuffix(upstreamAlias, "_search_web") {
t.Fatalf("upstream tool name = %q, want semantic mcp__ alias", upstreamAlias)
}
if got := gjson.GetBytes(resp.Payload, "content.0.name").String(); got != "search_web" {
t.Fatalf("client response tool name = %q, want search_web; payload=%s", got, resp.Payload)
}
if _, ok := claudeBillingCCHDigitsOffset(upstreamBody); !ok {
t.Fatalf("Claude OAuth custom BaseURL body is missing CCH: %s", upstreamBody)
}
if got := upstreamHeaders.Get("User-Agent"); got != "claude-cli/2.1.220 (external, cli)" {
t.Fatalf("Messages User-Agent = %q, want CLI identity", got)
}
wantBetas := claudeCodeCLIBetas(payload, nil, true)
if got := upstreamHeaders.Get("Anthropic-Beta"); got != wantBetas {
t.Fatalf("Messages Anthropic-Beta = %q, want %q", got, wantBetas)
}
if got := gjson.GetBytes(upstreamBody, "system.1.text").String(); got != claudeCodeCLIIdentity {
t.Fatalf("Messages system.1.text = %q, want official CLI identity", got)
}
if got := gjson.GetBytes(upstreamBody, "system.#").Int(); got != 2 {
t.Fatalf("Messages top-level system block count = %d, want 2", got)
}
content := gjson.GetBytes(upstreamBody, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("Messages first user content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "search", "1h")
assertClaudeMidConversationSystemMessage(t, upstreamBody, 1, "messages-system-prompt", "1h")
}
func TestClaudeExecutor_ExecuteStreamOAuthCustomToolMCPAliasRoundTrip(t *testing.T) {
var upstreamAlias string
var upstreamBody []byte
var upstreamHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
upstreamBody = bytes.Clone(body)
upstreamHeaders = r.Header.Clone()
upstreamAlias = gjson.GetBytes(body, "tools.0.name").String()
w.Header().Set("Content-Type", "text/event-stream")
_, _ = fmt.Fprintf(w, "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_1\",\"name\":%q,\"input\":{}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n", upstreamAlias)
}))
defer server.Close()
deviceIDs := []string{
"0000000000000000000000000000000000000000000000000000000000000000",
}
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "oauth-mcp-stream-round-trip",
Attributes: map[string]string{
"api_key": "sk-ant-oat-mcp-stream-round-trip",
"base_url": server.URL,
},
Metadata: map[string]any{
"account_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
claudeauth.ClaudeDeviceIDsMetadataKey: deviceIDs,
},
}
payload := []byte(`{"model":"claude-opus-5","system":"stream-system-prompt","messages":[{"role":"user","content":"fetch"}],"tools":[{"name":"fetch_url","description":"fetch","input_schema":{"type":"object"}}],"stream":true}`)
result, errStream := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-5",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Metadata: map[string]any{
cliproxyexecutor.ExecutionSessionMetadataKey: "stream-agent-conversation",
},
})
if errStream != nil {
t.Fatalf("ExecuteStream() error = %v", errStream)
}
var downstream bytes.Buffer
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("stream chunk error = %v", chunk.Err)
}
downstream.Write(chunk.Payload)
}
if !helps.IsClaudeMCPToolName(upstreamAlias) || !strings.HasSuffix(upstreamAlias, "_fetch_url") {
t.Fatalf("upstream tool name = %q, want semantic mcp__ alias", upstreamAlias)
}
if _, ok := claudeBillingCCHDigitsOffset(upstreamBody); !ok {
t.Fatalf("streaming Claude OAuth custom BaseURL body is missing CCH: %s", upstreamBody)
}
if got := upstreamHeaders.Get("User-Agent"); got != "claude-cli/2.1.220 (external, cli)" {
t.Fatalf("streaming User-Agent = %q, want CLI identity", got)
}
wantBetas := claudeCodeCLIBetas(payload, nil, true)
if got := upstreamHeaders.Get("Anthropic-Beta"); got != wantBetas {
t.Fatalf("streaming Anthropic-Beta = %q, want %q", got, wantBetas)
}
if got := gjson.GetBytes(upstreamBody, "system.1.text").String(); got != claudeCodeCLIIdentity {
t.Fatalf("streaming system.1.text = %q, want official CLI identity", got)
}
if got := gjson.GetBytes(upstreamBody, "system.#").Int(); got != 2 {
t.Fatalf("streaming top-level system block count = %d, want 2", got)
}
content := gjson.GetBytes(upstreamBody, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("streaming first user content has %d blocks, want currentDate and user text", len(content))
}
assertClaudeCodeCurrentDateBlock(t, content[0])
assertEphemeralUserTextBlock(t, content[1], "fetch", "1h")
assertClaudeMidConversationSystemMessage(t, upstreamBody, 1, "stream-system-prompt", "1h")
assertClaudeCredentialIdentity(t, upstreamBody, upstreamHeaders, deviceIDs, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
if !strings.Contains(downstream.String(), `"name":"fetch_url"`) {
t.Fatalf("downstream stream did not restore fetch_url: %s", downstream.String())
}
if strings.Contains(downstream.String(), upstreamAlias) {
t.Fatalf("downstream leaked upstream alias %q: %s", upstreamAlias, downstream.String())
}
}
func TestPrependClaudeSystemReminders_FollowsToolResultsAndIsIdempotent(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},{"type":"text","text":"continue"}]}` +
`]}`)
texts := []string{"first guidance", "second guidance"}
first := prependClaudeSystemRemindersToFirstUserMessage(payload, texts)
second := prependClaudeSystemRemindersToFirstUserMessage(first, texts)
if !bytes.Equal(first, second) {
t.Fatalf("caller reminder insertion is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
content := gjson.GetBytes(first, "messages.1.content").Array()
if len(content) != 4 {
t.Fatalf("content has %d blocks, want tool_result, two caller reminders, and user text", len(content))
}
if got := content[0].Get("type").String(); got != "tool_result" {
t.Fatalf("content[0].type = %q, want tool_result", got)
}
for idx, text := range texts {
if got := content[idx+1].Get("text").String(); got != claudeCallerSystemReminder(text) {
t.Fatalf("content[%d].text = %q, want caller reminder %q", idx+1, got, text)
}
}
if got := content[3].Get("text").String(); got != "continue" {
t.Fatalf("content[3].text = %q, want user text", got)
}
}
func TestInsertClaudeMidConversationSystemMessages_FollowsToolResultUserTurn(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]}` +
`]}`)
out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
t.Fatalf("message count = %d, want 3: %s", got, out)
}
blocks := gjson.GetBytes(out, "messages.1.content")
if got := blocks.Get("0.type").String(); got != "tool_result" {
t.Fatalf("first block type = %q, want tool_result: %s", got, out)
}
if got := blocks.Get("0.tool_use_id").String(); got != "toolu_1" {
t.Fatalf("tool_use_id = %q, want toolu_1: %s", got, out)
}
assertClaudeMidConversationSystemMessage(t, out, 2, "guidance", "")
}
func TestInsertClaudeMidConversationSystemMessages_PrecedesExistingAssistantTurn(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"user","content":"hello"},` +
`{"role":"assistant","content":"answer"},` +
`{"role":"user","content":"continue"}` +
`]}`)
out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
roles := gjson.GetBytes(out, "messages.#.role").Array()
wantRoles := []string{"user", "system", "assistant", "user"}
if len(roles) != len(wantRoles) {
t.Fatalf("message count = %d, want %d: %s", len(roles), len(wantRoles), out)
}
for idx, wantRole := range wantRoles {
if got := roles[idx].String(); got != wantRole {
t.Fatalf("messages[%d].role = %q, want %q", idx, got, wantRole)
}
}
assertClaudeMidConversationSystemMessage(t, out, 1, "guidance", "")
}
func TestInsertClaudeMidConversationSystemMessages_FollowsConsecutiveUserRun(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"user","content":"first"},` +
`{"role":"user","content":"second"},` +
`{"role":"assistant","content":"answer"}` +
`]}`)
out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
roles := gjson.GetBytes(out, "messages.#.role").Array()
wantRoles := []string{"user", "user", "system", "assistant"}
if len(roles) != len(wantRoles) {
t.Fatalf("message count = %d, want %d: %s", len(roles), len(wantRoles), out)
}
for idx, wantRole := range wantRoles {
if got := roles[idx].String(); got != wantRole {
t.Fatalf("messages[%d].role = %q, want %q", idx, got, wantRole)
}
}
assertClaudeMidConversationSystemMessage(t, out, 2, "guidance", "")
}
func TestInsertClaudeMidConversationSystemMessages_IsIdempotent(t *testing.T) {
payload := []byte(`{"messages":[{"role":"user","content":"hello"}]}`)
texts := []string{"first guidance", "second guidance"}
first := insertClaudeMidConversationSystemMessages(payload, texts)
second := insertClaudeMidConversationSystemMessages(first, texts)
if !bytes.Equal(first, second) {
t.Fatalf("mid-conversation system insertion is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
if got := gjson.GetBytes(first, "messages.#").Int(); got != 3 {
t.Fatalf("message count = %d, want user and two system messages: %s", got, first)
}
assertClaudeMidConversationSystemMessage(t, first, 1, texts[0], "")
assertClaudeMidConversationSystemMessage(t, first, 2, texts[1], "")
}
// TestClaudeCodeCLIBetas_MatchesObservedClientMatrix pins the Anthropic-Beta
// baseline to Claude Code 2.1.220 behavior captured against api.anthropic.com.
// The OAuth profile was reverified on 2026-08-03 with two distinct accounts.
func TestClaudeCodeCLIBetas_MatchesObservedClientMatrix(t *testing.T) {
const constants = "claude-code-20250219,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
tests := []struct {
name string
body string
requested map[string]bool
oauth bool
want string
}{
{
name: "legacy model without tools omits both conditional betas",
body: `{"model":"claude-opus-4-6"}`,
want: constants + ",effort-2025-11-24",
},
{
name: "context 1m sits right after claude-code, not at the end",
body: `{"model":"claude-opus-4-6"}`,
requested: map[string]bool{claudeContext1MBeta: true},
want: "claude-code-20250219,context-1m-2025-08-07," +
"interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," +
"thinking-token-count-2026-05-13,context-management-2025-06-27," +
"prompt-caching-scope-2026-01-05,effort-2025-11-24",
},
{
name: "opus-5 1m variant reproduces the full observed order",
body: `{"model":"claude-opus-5","tools":[{"name":"Read"}]}`,
requested: map[string]bool{
claudeContext1MBeta: true,
claudeServerSideFallbackBeta: true,
claudeFallbackCreditBeta: true,
},
want: "claude-code-20250219,context-1m-2025-08-07," +
"interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," +
"thinking-token-count-2026-05-13,context-management-2025-06-27," +
"prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," +
"advanced-tool-use-2025-11-20,effort-2025-11-24," +
"server-side-fallback-2026-06-01,fallback-credit-2026-06-01",
},
{
name: "structured outputs trails effort",
body: `{"model":"claude-opus-4-6"}`,
requested: map[string]bool{claudeStructuredOutputsBeta: true},
want: constants + ",effort-2025-11-24,structured-outputs-2025-12-15",
},
{
name: "unknown caller beta is not smuggled into the baseline",
body: `{"model":"claude-opus-4-6"}`,
requested: map[string]bool{"totally-made-up-2030-01-01": true},
want: constants + ",effort-2025-11-24",
},
{
name: "claude-sonnet-5 accepts role=system",
body: `{"model":"claude-sonnet-5"}`,
want: constants + ",mid-conversation-system-2026-04-07,effort-2025-11-24",
},
{
name: "claude-opus-4-8 accepts role=system",
body: `{"model":"claude-opus-4-8"}`,
want: constants + ",mid-conversation-system-2026-04-07,effort-2025-11-24",
},
{
name: "claude-fable-5 accepts role=system",
body: `{"model":"claude-fable-5"}`,
want: constants + ",mid-conversation-system-2026-04-07,effort-2025-11-24",
},
{
name: "claude-opus-4-7 stays on the reminder path",
body: `{"model":"claude-opus-4-7"}`,
want: constants + ",effort-2025-11-24",
},
{
name: "oauth uses advanced tools and the current cache TTL trailer",
body: `{"model":"claude-opus-4-6","tools":[{"name":"Read"}]}`,
oauth: true,
want: "claude-code-20250219,oauth-2025-04-20," +
"interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," +
"thinking-token-count-2026-05-13,context-management-2025-06-27," +
"prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20," +
"effort-2025-11-24,fallback-credit-2026-06-01," +
"extended-cache-ttl-2025-04-11",
},
{
name: "oauth precedes context-1m",
body: `{"model":"claude-opus-5","tools":[{"name":"Read"}]}`,
oauth: true,
requested: map[string]bool{
claudeContext1MBeta: true,
claudeServerSideFallbackBeta: true,
claudeFallbackCreditBeta: true,
},
want: "claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07," +
"interleaved-thinking-2025-05-14,redact-thinking-2026-02-12," +
"thinking-token-count-2026-05-13,context-management-2025-06-27," +
"prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07," +
"advanced-tool-use-2025-11-20,effort-2025-11-24," +
"server-side-fallback-2026-06-01,fallback-credit-2026-06-01," +
"extended-cache-ttl-2025-04-11",
},
{
name: "api key path sends neither oauth beta",
body: `{"model":"claude-opus-4-6"}`,
want: constants + ",effort-2025-11-24",
},
{
name: "claude-haiku-4-5-20251001 stays on the reminder path",
body: `{"model":"claude-haiku-4-5-20251001"}`,
want: constants + ",effort-2025-11-24",
},
{
name: "legacy model with tools adds advanced tool use only",
body: `{"model":"claude-sonnet-4-6","tools":[{"name":"Read"}]}`,
want: constants + ",advanced-tool-use-2025-11-20,effort-2025-11-24",
},
{
name: "role=system model without tools adds mid conversation system only",
body: `{"model":"claude-opus-5"}`,
want: constants + ",mid-conversation-system-2026-04-07,effort-2025-11-24",
},
{
name: "role=system model with tools adds both in wire order",
body: `{"model":"claude-opus-5","tools":[{"name":"Read"}]}`,
want: constants + ",mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24",
},
{
name: "empty tools array does not add advanced tool use",
body: `{"model":"claude-opus-4-6","tools":[]}`,
want: constants + ",effort-2025-11-24",
},
{
name: "unknown future model keeps the optimistic role=system default",
body: `{"model":"claude-future-9"}`,
want: constants + ",mid-conversation-system-2026-04-07,effort-2025-11-24",
},
{
name: "thinking display summarized drops redact-thinking",
body: `{"model":"claude-opus-5","thinking":{"type":"adaptive","display":"summarized"}}`,
want: "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-system-2026-04-07," +
"effort-2025-11-24",
},
{
name: "thinking display omitted drops redact-thinking as well",
body: `{"model":"claude-opus-4-6","thinking":{"type":"enabled","budget_tokens":2048,"display":"omitted"}}`,
want: "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,effort-2025-11-24",
},
{
name: "thinking without display keeps redact-thinking",
body: `{"model":"claude-opus-4-6","thinking":{"type":"adaptive"}}`,
want: constants + ",effort-2025-11-24",
},
{
name: "blank display value keeps redact-thinking",
body: `{"model":"claude-opus-4-6","thinking":{"type":"adaptive","display":" "}}`,
want: constants + ",effort-2025-11-24",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := claudeCodeCLIBetas([]byte(tt.body), tt.requested, tt.oauth); got != tt.want {
t.Fatalf("claudeCodeCLIBetas() = %q, want %q", got, tt.want)
}
})
}
}
// TestApplyClaudeHeaders_StreamTransportNegotiation pins the observed 2.1.220
// behaviour: a streaming request to api.anthropic.com negotiates exactly like a
// non-streaming one, because Anthropic selects SSE from the body. Other
// Anthropic-compatible upstreams keep the conservative SSE contract.
func TestApplyClaudeHeaders_StreamTransportNegotiation(t *testing.T) {
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-stream-accept"}}
body := []byte(`{"model":"claude-opus-4-6","stream":true}`)
directReq := newClaudeHeaderTestRequest(t, http.Header{})
if errApply := applyClaudeHeaders(directReq, auth, "key-stream-accept", true, nil, body, nil, http.Header{}, false); errApply != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errApply)
}
if got, want := directReq.Header.Get("Accept"), "application/json"; got != want {
t.Fatalf("streaming Accept = %q, want %q to match the real client", got, want)
}
if got, want := directReq.Header.Get("Accept-Encoding"), "gzip, deflate, br, zstd"; got != want {
t.Fatalf("streaming Accept-Encoding = %q, want %q to match the real client", got, want)
}
gatewayReq := httptest.NewRequest(http.MethodPost, "https://api.kimi.com/coding/v1/messages", nil)
gatewayReq = gatewayReq.WithContext(directReq.Context())
if errApply := applyClaudeHeaders(gatewayReq, auth, "key-stream-accept", true, nil, body, nil, http.Header{}, false); errApply != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errApply)
}
if got, want := gatewayReq.Header.Get("Accept"), "text/event-stream"; got != want {
t.Fatalf("gateway streaming Accept = %q, want %q", got, want)
}
if got, want := gatewayReq.Header.Get("Accept-Encoding"), "identity"; got != want {
t.Fatalf("gateway streaming Accept-Encoding = %q, want %q", got, want)
}
}
func TestApplyClaudeHeaders_DefaultPreservesCallerBetas(t *testing.T) {
incoming := http.Header{"Anthropic-Beta": []string{"caller-only-beta"}}
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-caller-betas"}}
body := []byte(`{"model":"claude-opus-4-6"}`)
// Default API-key mode preserves caller betas on direct Anthropic.
directReq := newClaudeHeaderTestRequest(t, incoming)
if errApply := applyClaudeHeaders(directReq, auth, "key-caller-betas", false, nil, body, nil, incoming, false); errApply != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errApply)
}
if got := directReq.Header.Get("Anthropic-Beta"); got != "caller-only-beta" {
t.Fatalf("Anthropic-Beta = %q, want caller beta on api.anthropic.com", got)
}
// Other Anthropic-compatible upstreams keep caller betas functional.
gatewayReq := httptest.NewRequest(http.MethodPost, "https://api.kimi.com/coding/v1/messages", nil)
gatewayReq = gatewayReq.WithContext(directReq.Context())
if errApply := applyClaudeHeaders(gatewayReq, auth, "key-caller-betas", false, nil, body, nil, incoming, false); errApply != nil {
t.Fatalf("applyClaudeHeaders() error = %v", errApply)
}
if got := gatewayReq.Header.Get("Anthropic-Beta"); !strings.Contains(got, "caller-only-beta") {
t.Fatalf("Anthropic-Beta = %q, want caller beta preserved on non-Anthropic upstream", got)
}
}
// TestInjectClaudeCodeContextManagement pins the captured 2.1.220 object and
// the thinking and caller-ownership rules that control automatic injection.
func TestInjectClaudeCodeContextManagement(t *testing.T) {
const captured = `{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}`
for _, test := range []struct {
name string
payload string
}{
{name: "enabled thinking", payload: `{"model":"claude-opus-5","thinking":{"type":"enabled"}}`},
{name: "adaptive thinking", payload: `{"model":"claude-opus-5","thinking":{"type":"adaptive"}}`},
} {
t.Run(test.name, func(t *testing.T) {
got, automaticallyInjected := injectClaudeCodeContextManagement([]byte(test.payload))
if !automaticallyInjected {
t.Fatal("automatic context_management injection was not reported")
}
if diff := gjson.GetBytes(got, "context_management").Raw; diff != captured {
t.Fatalf("context_management = %s, want the captured object %s", diff, captured)
}
})
}
callerOwned := []byte(`{"model":"claude-opus-4-6","context_management":{"edits":[]}}`)
callerOwnedGot, automaticallyInjected := injectClaudeCodeContextManagement(callerOwned)
if automaticallyInjected {
t.Error("caller context_management was reported as automatically injected")
}
if !bytes.Equal(callerOwnedGot, callerOwned) {
t.Fatalf("caller context_management was modified: %s", callerOwnedGot)
}
// Anthropic rejects clear_thinking_20251015 unless thinking is enabled or
// adaptive, so an omitted thinking field is as ineligible as an explicit
// disabled one.
for _, test := range []struct {
name string
payload string
}{
{name: "disabled thinking", payload: `{"model":"claude-opus-5","thinking":{"type":"disabled"}}`},
{name: "omitted thinking", payload: `{"model":"claude-opus-4-6"}`},
{name: "unknown thinking", payload: `{"model":"claude-opus-5","thinking":{"type":"unexpected"}}`},
} {
t.Run(test.name, func(t *testing.T) {
ineligible := []byte(test.payload)
got, automaticallyInjected := injectClaudeCodeContextManagement(ineligible)
if automaticallyInjected {
t.Error("ineligible thinking context_management was reported as automatically injected")
}
if !bytes.Equal(got, ineligible) {
t.Errorf("ineligible payload was modified: %s", got)
}
if cm := gjson.GetBytes(got, "context_management"); cm.Exists() {
t.Errorf("context_management = %s, want absent", cm.Raw)
}
})
}
}
// Anthropic rejects a request carrying the clear_thinking_20251015 strategy
// without enabled/adaptive thinking:
//
// `clear_thinking_20251015` strategy requires `thinking` to be enabled or adaptive
//
// This walks the real execute.go ordering, where disableThinkingIfToolChoiceForced
// deletes the thinking field between injection and reconciliation.
func TestClaudeCodeContextManagementNeverOutlivesEligibleThinking(t *testing.T) {
for _, test := range []struct {
name string
payload string
wantCM bool
}{
{
name: "thinking omitted from the start",
payload: `{"model":"claude-opus-5","messages":[]}`,
},
{
name: "forced tool_choice strips thinking after injection",
payload: `{"model":"claude-opus-5","thinking":{"type":"enabled","budget_tokens":1024},"tool_choice":{"type":"any"},"messages":[]}`,
},
{
name: "thinking survives without forced tool_choice",
payload: `{"model":"claude-opus-5","thinking":{"type":"enabled","budget_tokens":1024},"messages":[]}`,
wantCM: true,
},
} {
t.Run(test.name, func(t *testing.T) {
body, injected := injectClaudeCodeContextManagement([]byte(test.payload))
state := claudeCodeContextManagementState{eligible: true, automaticallyInjected: injected}
body = disableThinkingIfToolChoiceForced(body)
body = reconcileClaudeCodeContextManagement(body, state)
thinkingEligible := gjson.GetBytes(body, "thinking.type").String() == "enabled" ||
gjson.GetBytes(body, "thinking.type").String() == "adaptive"
cm := gjson.GetBytes(body, "context_management")
if cm.Exists() && !thinkingEligible {
t.Fatalf("context_management = %s survived ineligible thinking; Anthropic would reject this: %s", cm.Raw, body)
}
if cm.Exists() != test.wantCM {
t.Fatalf("context_management present = %v, want %v; body=%s", cm.Exists(), test.wantCM, body)
}
})
}
}
func TestReconcileClaudeCodeContextManagement(t *testing.T) {
withAutomatic := func(thinkingType string) string {
return `{"thinking":{"type":"` + thinkingType + `"},"context_management":` + claudeCodeContextManagement + `}`
}
for _, test := range []struct {
name string
payload string
state claudeCodeContextManagementState
wantRaw string
}{
{
name: "removes unchanged automatic object when disabled",
payload: withAutomatic("disabled"),
state: claudeCodeContextManagementState{eligible: true, automaticallyInjected: true},
},
{
name: "preserves rule owned automatic object when disabled",
payload: withAutomatic("disabled"),
state: claudeCodeContextManagementState{eligible: true, automaticallyInjected: true, payloadRuleTouched: true},
wantRaw: claudeCodeContextManagement,
},
{
name: "preserves changed automatic object when disabled",
payload: `{"thinking":{"type":"disabled"},"context_management":{"edits":[{"type":"custom"}]}}`,
state: claudeCodeContextManagementState{eligible: true, automaticallyInjected: true},
wantRaw: `{"edits":[{"type":"custom"}]}`,
},
{
name: "adds automatic object when enabled",
payload: `{"thinking":{"type":"enabled"}}`,
state: claudeCodeContextManagementState{eligible: true},
wantRaw: claudeCodeContextManagement,
},
{
name: "adds automatic object when adaptive",
payload: `{"thinking":{"type":"adaptive"}}`,
state: claudeCodeContextManagementState{eligible: true},
wantRaw: claudeCodeContextManagement,
},
{
name: "caller ownership prevents addition",
payload: `{"thinking":{"type":"enabled"}}`,
state: claudeCodeContextManagementState{eligible: true, callerOwned: true},
},
{
name: "payload rule ownership prevents addition",
payload: `{"thinking":{"type":"enabled"}}`,
state: claudeCodeContextManagementState{eligible: true, payloadRuleTouched: true},
},
{
name: "ineligible request prevents addition",
payload: `{"thinking":{"type":"enabled"}}`,
},
{
name: "omitted thinking prevents addition",
payload: `{}`,
state: claudeCodeContextManagementState{eligible: true},
},
{
name: "removes automatic object when thinking was stripped entirely",
payload: `{"context_management":` + claudeCodeContextManagement + `}`,
state: claudeCodeContextManagementState{eligible: true, automaticallyInjected: true},
},
{
name: "keeps caller object when thinking was stripped entirely",
payload: `{"context_management":` + claudeCodeContextManagement + `}`,
state: claudeCodeContextManagementState{eligible: true, callerOwned: true},
wantRaw: claudeCodeContextManagement,
},
{
name: "unknown thinking prevents addition",
payload: `{"thinking":{"type":"unexpected"}}`,
state: claudeCodeContextManagementState{eligible: true},
},
{
name: "invalid thinking prevents addition",
payload: `{"thinking":{"type":123}}`,
state: claudeCodeContextManagementState{eligible: true},
},
} {
t.Run(test.name, func(t *testing.T) {
got := reconcileClaudeCodeContextManagement([]byte(test.payload), test.state)
if raw := gjson.GetBytes(got, "context_management").Raw; raw != test.wantRaw {
t.Fatalf("context_management = %s, want %s; body=%s", raw, test.wantRaw, got)
}
})
}
}
func TestClaudeExecutorPayloadOverrideDisabledThinking(t *testing.T) {
const model = "claude-opus-5"
modelRules := []config.PayloadModelRule{{Name: model, Protocol: "claude"}}
basePayload := []byte(`{"model":"claude-opus-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}`)
for _, test := range []struct {
name string
stream bool
}{
{name: "execute"},
{name: "execute stream", stream: true},
} {
t.Run(test.name, func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "disabled"},
}}}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, test.stream)
if got := gjson.GetBytes(upstreamBody, "thinking.type").String(); got != "disabled" {
t.Fatalf("final upstream thinking.type = %q, want disabled; body=%s", got, upstreamBody)
}
if got := gjson.GetBytes(upstreamBody, "context_management"); got.Exists() {
t.Errorf("final upstream context_management = %s with disabled thinking, want absent", got.Raw)
}
})
}
t.Run("caller context management is preserved", func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "disabled"},
}}}}
payload := []byte(`{"model":"claude-opus-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}],"context_management":{"edits":[{"type":"caller_owned"}]}}`)
upstreamBody := executeClaudeContextManagementRequest(t, cfg, payload, false)
if got := gjson.GetBytes(upstreamBody, "context_management.edits.0.type").String(); got != "caller_owned" {
t.Fatalf("caller context_management type = %q, want caller_owned; body=%s", got, upstreamBody)
}
})
t.Run("payload override replacement is preserved", func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{
"thinking.type": "disabled",
"context_management": map[string]any{"edits": []any{map[string]any{"type": "payload_rule"}}},
},
}}}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, false)
if got := gjson.GetBytes(upstreamBody, "context_management.edits.0.type").String(); got != "payload_rule" {
t.Fatalf("payload-rule context_management type = %q, want payload_rule; body=%s", got, upstreamBody)
}
})
t.Run("exact automatic value remains payload rule owned", func(t *testing.T) {
ownershipConfigs := []struct {
name string
cfg *config.Config
}{
{
name: "default",
cfg: &config.Config{Payload: config.PayloadConfig{
Default: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": json.RawMessage(claudeCodeContextManagement)},
}},
Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "disabled"},
}},
}},
},
{
name: "raw default",
cfg: &config.Config{Payload: config.PayloadConfig{
DefaultRaw: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": claudeCodeContextManagement},
}},
Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "disabled"},
}},
}},
},
{
name: "override",
cfg: &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{
"thinking.type": "disabled",
"context_management": json.RawMessage(claudeCodeContextManagement),
},
}}}},
},
{
name: "raw override",
cfg: &config.Config{Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "disabled"},
}},
OverrideRaw: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"context_management": claudeCodeContextManagement},
}},
}},
},
}
for _, ownership := range ownershipConfigs {
for _, stream := range []bool{false, true} {
name := ownership.name + " execute"
if stream {
name += " stream"
}
t.Run(name, func(t *testing.T) {
upstreamBody := executeClaudeContextManagementRequest(t, ownership.cfg, basePayload, stream)
if got := gjson.GetBytes(upstreamBody, "thinking.type").String(); got != "disabled" {
t.Fatalf("final upstream thinking.type = %q, want disabled; body=%s", got, upstreamBody)
}
if got := gjson.GetBytes(upstreamBody, "context_management").Raw; got != claudeCodeContextManagement {
t.Fatalf("%s context_management = %s, want payload-rule-owned %s; body=%s", ownership.name, got, claudeCodeContextManagement, upstreamBody)
}
})
}
}
})
t.Run("payload filter remains effective", func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Filter: []config.PayloadFilterRule{{
Models: modelRules,
Params: []string{"context_management"},
}}}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, false)
if got := gjson.GetBytes(upstreamBody, "context_management"); got.Exists() {
t.Fatalf("filtered context_management = %s, want absent", got.Raw)
}
})
for _, stream := range []bool{false, true} {
// Anthropic rejects the automatic strategy once forced tool choice has
// stripped thinking:
//
// `clear_thinking_20251015` strategy requires `thinking` to be enabled or adaptive
name := "forced tool choice drops automatic context management execute"
if stream {
name += " stream"
}
t.Run(name, func(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}],"thinking":{"type":"adaptive"},"tool_choice":{"type":"any"}}`)
upstreamBody := executeClaudeContextManagementRequest(t, &config.Config{}, payload, stream)
if got := gjson.GetBytes(upstreamBody, "thinking"); got.Exists() {
t.Fatalf("forced tool choice thinking = %s, want absent", got.Raw)
}
if got := gjson.GetBytes(upstreamBody, "context_management"); got.Exists() {
t.Fatalf("forced tool choice context_management = %s, want absent because Anthropic rejects it without thinking", got.Raw)
}
if got := gjson.GetBytes(upstreamBody, "tool_choice.type").String(); got != "any" {
t.Fatalf("forced tool_choice.type = %q, want any", got)
}
})
}
}
func TestClaudeExecutorPayloadOverrideReenablesThinking(t *testing.T) {
const model = "claude-opus-5"
modelRules := []config.PayloadModelRule{{Name: model, Protocol: "claude"}}
basePayload := []byte(`{"model":"claude-opus-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}],"thinking":{"type":"disabled"}}`)
for _, test := range []struct {
name string
thinkingType string
stream bool
}{
{name: "execute enabled", thinkingType: "enabled"},
{name: "execute adaptive", thinkingType: "adaptive"},
{name: "execute stream enabled", thinkingType: "enabled", stream: true},
{name: "execute stream adaptive", thinkingType: "adaptive", stream: true},
} {
t.Run(test.name, func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": test.thinkingType},
}}}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, test.stream)
if got := gjson.GetBytes(upstreamBody, "thinking.type").String(); got != test.thinkingType {
t.Fatalf("final upstream thinking.type = %q, want %q; body=%s", got, test.thinkingType, upstreamBody)
}
if got := gjson.GetBytes(upstreamBody, "context_management").Raw; got != claudeCodeContextManagement {
t.Fatalf("final upstream context_management = %s, want %s after payload override to %s; body=%s", got, claudeCodeContextManagement, test.thinkingType, upstreamBody)
}
})
}
for _, stream := range []bool{false, true} {
nameSuffix := "execute"
if stream {
nameSuffix = "execute stream"
}
t.Run("caller context management is preserved after re-enabling "+nameSuffix, func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "enabled"},
}}}}
payload := []byte(`{"model":"claude-opus-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}],"thinking":{"type":"disabled"},"context_management":{"edits":[{"type":"caller_owned"}]}}`)
upstreamBody := executeClaudeContextManagementRequest(t, cfg, payload, stream)
if got := gjson.GetBytes(upstreamBody, "context_management.edits.0.type").String(); got != "caller_owned" {
t.Fatalf("caller context_management type = %q, want caller_owned; body=%s", got, upstreamBody)
}
})
t.Run("custom payload rule object is preserved after re-enabling "+nameSuffix, func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{
"thinking.type": "adaptive",
"context_management": map[string]any{"edits": []any{map[string]any{"type": "payload_rule"}}},
},
}}}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, stream)
if got := gjson.GetBytes(upstreamBody, "context_management.edits.0.type").String(); got != "payload_rule" {
t.Fatalf("payload-rule context_management type = %q, want payload_rule; body=%s", got, upstreamBody)
}
})
t.Run("context management filter remains authoritative after re-enabling "+nameSuffix, func(t *testing.T) {
cfg := &config.Config{Payload: config.PayloadConfig{
Override: []config.PayloadRule{{
Models: modelRules,
Params: map[string]any{"thinking.type": "enabled"},
}},
Filter: []config.PayloadFilterRule{{
Models: modelRules,
Params: []string{"context_management"},
}},
}}
upstreamBody := executeClaudeContextManagementRequest(t, cfg, basePayload, stream)
if got := gjson.GetBytes(upstreamBody, "thinking.type").String(); got != "enabled" {
t.Fatalf("final upstream thinking.type = %q, want enabled; body=%s", got, upstreamBody)
}
if got := gjson.GetBytes(upstreamBody, "context_management"); got.Exists() {
t.Fatalf("filtered context_management = %s after re-enabling, want absent; body=%s", got.Raw, upstreamBody)
}
})
}
}
func executeClaudeContextManagementRequest(t *testing.T, cfg *config.Config, payload []byte, stream bool) []byte {
t.Helper()
var upstreamBody []byte
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
var errRead error
upstreamBody, errRead = io.ReadAll(req.Body)
if errRead != nil {
t.Fatal(errRead)
}
contentType := "application/json"
responseBody := `{"id":"msg_test","type":"message","role":"assistant","model":"claude-opus-5","content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`
if stream {
contentType = "text/event-stream"
responseBody = "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_test\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-opus-5\",\"content\":[],\"stop_reason\":null,\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{contentType}},
Body: io.NopCloser(strings.NewReader(responseBody)),
Request: req,
}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
executor := NewClaudeExecutor(cfg)
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-payload-rule", "cloak_mode": "always"}}
request := cliproxyexecutor.Request{Model: "claude-opus-5", Payload: payload}
options := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude}
if stream {
result, errStream := executor.ExecuteStream(ctx, auth, request, options)
if errStream != nil {
t.Fatalf("ExecuteStream() error = %v", errStream)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatalf("stream chunk error = %v", chunk.Err)
}
}
return upstreamBody
}
if _, errExecute := executor.Execute(ctx, auth, request, options); errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
return upstreamBody
}
func TestValidateClaudeCallerSystemBlocksAcceptsTextOnly(t *testing.T) {
tests := []struct {
name string
system string
}{
{name: "string", system: `"S1"`},
{name: "text blocks", system: `[{"type":"text","text":"S1"},{"type":"text","text":"S2"}]`},
{name: "absent", system: ``},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
payload := `{"model":"claude-opus-5"}`
if test.system != "" {
payload = `{"model":"claude-opus-5","system":` + test.system + `}`
}
if err := validateClaudeCallerSystemBlocks(gjson.Get(payload, "system")); err != nil {
t.Fatalf("validateClaudeCallerSystemBlocks() error = %v, want nil", err)
}
})
}
}
// Anthropic rejects every non-text block in both system slots, verified live on
// 2026-08-03: the top-level field answers "system.<i>.type: Input should be
// 'text'" and a role=system message answers "role 'system' supports text,
// tool_addition, and tool_removal blocks only". Cloaking has no third slot, so
// the request has to fail here instead of losing the caller's instructions.
func TestValidateClaudeCallerSystemBlocksRejectsNonTextBlock(t *testing.T) {
tests := []struct {
name string
system string
wantIndex string
wantType string
}{
{
name: "image",
system: `[{"type":"text","text":"S1"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"AAAA"}}]`,
wantIndex: "system.1.type",
wantType: `"image"`,
},
{
name: "responses marker",
system: `[{"type":"input_file"}]`,
wantIndex: "system.0.type",
wantType: `"input_file"`,
},
{
name: "missing type",
system: `[{"text":"S1"}]`,
wantIndex: "system.0.type",
wantType: `"unknown"`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateClaudeCallerSystemBlocks(gjson.Parse(test.system))
if err == nil {
t.Fatal("validateClaudeCallerSystemBlocks() error = nil, want rejection")
}
var statusCoder interface{ StatusCode() int }
if !errors.As(err, &statusCoder) || statusCoder.StatusCode() != http.StatusBadRequest {
t.Fatalf("error status = %v, want 400", err)
}
var scoped interface{ IsRequestScoped() bool }
if !errors.As(err, &scoped) || !scoped.IsRequestScoped() {
t.Fatalf("error %v must be request scoped so no other credential is tried", err)
}
if got := err.Error(); !strings.Contains(got, test.wantIndex) || !strings.Contains(got, test.wantType) {
t.Fatalf("error = %q, want it to name %s and %s", got, test.wantIndex, test.wantType)
}
})
}
}
func TestApplyCloakingRejectsNonTextCallerSystemBlock(t *testing.T) {
cfg := &config.Config{}
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-123", "cloak_mode": "always"}}
payload := []byte(`{"model":"claude-opus-5","system":[{"type":"text","text":"S1"},{"type":"input_image"}],"messages":[{"role":"user","content":[{"type":"text","text":"U1"}]}]}`)
out, cloaked, errCloaking := applyCloaking(context.Background(), cfg, auth, payload, "key-123", false, true)
if errCloaking == nil {
t.Fatal("applyCloaking() error = nil, want rejection")
}
if out != nil {
t.Fatalf("applyCloaking() payload = %s, want nil", out)
}
if cloaked {
t.Fatal("applyCloaking() cloaked = true, want false")
}
}
// Strict mode never forwards caller system prompts, so an unusable block cannot
// lose information and must not fail the request.
func TestApplyCloakingStrictModeIgnoresNonTextCallerSystemBlock(t *testing.T) {
cfg := &config.Config{
ClaudeKey: []config.ClaudeKey{{
APIKey: "key-123",
Cloak: &config.CloakConfig{StrictMode: true},
}},
}
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "key-123"}}
payload := []byte(`{"model":"claude-opus-5","system":[{"type":"input_image"}],"messages":[{"role":"user","content":[{"type":"text","text":"U1"}]}]}`)
out, cloaked, errCloaking := applyCloaking(context.Background(), cfg, auth, payload, "key-123", false, true)
if errCloaking != nil {
t.Fatalf("applyCloaking() error = %v, want nil", errCloaking)
}
if !cloaked {
t.Fatal("applyCloaking() cloaked = false, want true")
}
if got := len(gjson.GetBytes(out, "system").Array()); got != 2 {
t.Fatalf("system blocks = %d, want the 2 Claude Code blocks", got)
}
}
// A cloaked direct-Anthropic count_tokens request relocates caller system blocks
// into messages, so a non-text block has no destination there either and must be
// rejected before any upstream call.
func TestClaudeExecutor_CountTokensRejectsNonTextCallerSystemBlock(t *testing.T) {
upstreamCalled := false
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
upstreamCalled = true
return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"input_tokens":1}`)), Request: req}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
auth := &cliproxyauth.Auth{Attributes: map[string]string{"api_key": "sk-ant-oat-count-system-block"}}
payload := []byte(`{"model":"claude-opus-5","system":[{"type":"text","text":"S1"},{"type":"input_image"}],"messages":[{"role":"user","content":[{"type":"text","text":"x"}]}]}`)
_, errCount := NewClaudeExecutor(&config.Config{}).countTokensUpstream(ctx, auth,
cliproxyexecutor.Request{Model: "claude-opus-5", Payload: payload},
cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errCount == nil {
t.Fatal("countTokensUpstream() error = nil, want rejection")
}
var statusCoder interface{ StatusCode() int }
if !errors.As(errCount, &statusCoder) || statusCoder.StatusCode() != http.StatusBadRequest {
t.Fatalf("countTokensUpstream() error = %v, want 400", errCount)
}
if upstreamCalled {
t.Fatal("countTokensUpstream() called upstream, want local rejection")
}
}
// The native gate selects the 1h cache pool only for OAuth credentials and pushes
// extended-cache-ttl-2025-04-11 exactly when that selection produced a 1h body ttl.
// Body ttl and the beta must therefore always travel together.
func TestClaudeExecutor_CacheTTLIsPairedWithExtendedCacheTTLBeta(t *testing.T) {
tests := []struct {
name string
apiKey string
wantTTL string
wantBeta bool
}{
{
name: "oauth credential selects the 1h pool",
apiKey: "sk-ant-oat-cache-ttl-pairing",
wantTTL: "1h",
wantBeta: true,
},
{
name: "api key credential keeps the default pool",
apiKey: "key-cache-ttl-pairing",
wantTTL: "",
wantBeta: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var seenBody []byte
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenBody, _ = io.ReadAll(r.Body)
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_1","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "cache-ttl-pairing",
Attributes: map[string]string{
"api_key": test.apiKey,
"base_url": server.URL,
"cloak_mode": "always",
},
Metadata: claudeOAuthTestMetadata(),
}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":[{"type":"text","text":"x"}]}]}`),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
gotTTL := gjson.GetBytes(seenBody, "system.1.cache_control.ttl").String()
if gotTTL != test.wantTTL {
t.Fatalf("system[1].cache_control.ttl = %q, want %q: %s", gotTTL, test.wantTTL, seenBody)
}
if got := gjson.GetBytes(seenBody, "system.1.cache_control.type").String(); got != "ephemeral" {
t.Fatalf("system[1].cache_control.type = %q, want ephemeral: %s", got, seenBody)
}
gotBeta := strings.Contains(seenHeaders.Get("Anthropic-Beta"), claudeExtendedCacheTTLBeta)
if gotBeta != test.wantBeta {
t.Fatalf("extended-cache-ttl declared = %v, want %v: %s", gotBeta, test.wantBeta, seenHeaders.Get("Anthropic-Beta"))
}
// The pairing invariant itself: a 1h body ttl without the beta, or the beta
// without a 1h body ttl, is a combination native never produces.
if (gotTTL == "1h") != gotBeta {
t.Fatalf("body ttl %q and extended-cache-ttl beta %v disagree", gotTTL, gotBeta)
}
})
}
}
func TestClaudeExecutor_PreservesNativeAgentAndEnvironmentHeaders(t *testing.T) {
tests := []struct {
name string
incomingHeaders http.Header
wantHeaders map[string]string
wantAbsent []string
}{
{
name: "preserves canonical agent and parent agent headers",
incomingHeaders: http.Header{
"X-Claude-Code-Agent-Id": {"subagent-001"},
"X-Claude-Code-Parent-Agent-Id": {"parent-agent-root"},
},
wantHeaders: map[string]string{
"X-Claude-Code-Agent-Id": "subagent-001",
"X-Claude-Code-Parent-Agent-Id": "parent-agent-root",
},
},
{
name: "preserves lowercased agent and environment headers",
incomingHeaders: http.Header{
"x-claude-code-agent-id": {"agent-xyz"},
"x-claude-remote-container-id": {"container-123"},
"x-claude-remote-session-id": {"remote-sess-456"},
"x-client-app": {"custom-sdk"},
"x-anthropic-additional-protection": {"true"},
},
wantHeaders: map[string]string{
"X-Claude-Code-Agent-Id": "agent-xyz",
"X-Claude-Remote-Container-Id": "container-123",
"X-Claude-Remote-Session-Id": "remote-sess-456",
"X-Client-App": "custom-sdk",
"X-Anthropic-Additional-Protection": "true",
},
},
{
name: "does not fabricate agent header when absent",
incomingHeaders: http.Header{
"User-Agent": {"test-client"},
},
wantAbsent: []string{
"X-Claude-Code-Agent-Id",
"X-Claude-Code-Parent-Agent-Id",
"X-Claude-Remote-Container-Id",
"X-Claude-Remote-Session-Id",
"X-Client-App",
"X-Anthropic-Additional-Protection",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var seenHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenHeaders = r.Header.Clone()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"msg_agent","type":"message","model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
executor := NewClaudeExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "agent-header-test",
Attributes: map[string]string{
"api_key": "sk-ant-test-key",
"base_url": server.URL,
"cloak_mode": "always",
},
Metadata: claudeOAuthTestMetadata(),
}
_, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "claude-opus-4-6",
Payload: []byte(`{"model":"claude-opus-4-6","messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatClaude,
Headers: tt.incomingHeaders,
})
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
for wantKey, wantVal := range tt.wantHeaders {
if got := seenHeaders.Get(wantKey); got != wantVal {
t.Errorf("header %s = %q, want %q", wantKey, got, wantVal)
}
}
for _, absentKey := range tt.wantAbsent {
if got := seenHeaders.Get(absentKey); got != "" {
t.Errorf("header %s = %q, want absent", absentKey, got)
}
}
})
}
}