Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions internal/runtime/executor/claude_native_workload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package executor

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"

claudeauth "github.com/router-for-me/CLIProxyAPI/v8/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
"github.com/tidwall/gjson"
)

func TestClaudeNativeWorkloadMarkerKeepsOutboundPrefixIdentical(t *testing.T) {
userID, _ := json.Marshal(`{"device_id":"0000000000000000000000000000000000000000000000000000000000000001","account_uuid":"11111111-2222-4333-8444-555555555555","session_id":"11111111-2222-4333-8444-555555555556"}`)
for _, test := range []struct {
name string
stream bool
ttl string
}{
{name: "execute stream bare ttl", stream: true},
{name: "execute stream one hour ttl", stream: true, ttl: `,"ttl":"1h"`},
{name: "execute bare ttl"},
{name: "execute one hour ttl", ttl: `,"ttl":"1h"`},
} {
t.Run(test.name, func(t *testing.T) {
marker := `{"type":"ephemeral"` + test.ttl + `}`
payload := []byte(fmt.Sprintf(`{"model":"claude-opus-5-5","max_tokens":1000,"tools":[{"name":"read_file","description":"Read a file.","input_schema":{"type":"object","properties":{"path":{"type":"string"}}},"cache_control":%[1]s}],"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.312; cc_entrypoint=sdk-ts; cch=00000;"},{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.","cache_control":%[1]s},{"type":"text","text":"Stable project instructions.","cache_control":%[1]s}],"messages":[{"role":"user","content":[{"type":"text","text":"Stable task.","cache_control":%[1]s}]}],"metadata":{"user_id":%[2]s},"thinking":{"type":"adaptive"},"output_config":{"effort":"medium"},"stream":%[3]t}`, marker, userID, test.stream))
auth := &cliproxyauth.Auth{
ID: "native-workload-fixture",
Attributes: map[string]string{"api_key": "sk-ant-oat-test-oauth-key"},
Metadata: map[string]any{
"account_uuid": "11111111-2222-4333-8444-555555555555",
claudeauth.ClaudeDeviceIDsMetadataKey: []string{
"0000000000000000000000000000000000000000000000000000000000000001",
},
},
}
var bodies [][]byte
var userAgents []string
transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
body, errRead := io.ReadAll(req.Body)
if errRead != nil {
return nil, errRead
}
bodies = append(bodies, body)
userAgents = append(userAgents, req.Header.Get("User-Agent"))
header := http.Header{"Content-Type": {"application/json"}}
response := `{"id":"msg_fixture","type":"message","model":"claude-opus-5-5","role":"assistant","content":[{"type":"text","text":"OK"}],"usage":{"input_tokens":1,"output_tokens":1}}`
if test.stream {
header.Set("Content-Type", "text/event-stream")
response = "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_fixture\",\"model\":\"claude-opus-5-5\",\"role\":\"assistant\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
return &http.Response{StatusCode: http.StatusOK, Header: header, Body: io.NopCloser(strings.NewReader(response)), Request: req}, nil
})
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(transport))
exec := NewClaudeExecutor(&config.Config{})

inboundUserAgents := []string{
"claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276)",
"claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/cron)",
"claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276)",
}
for _, userAgent := range inboundUserAgents {
headers := http.Header{
"User-Agent": {userAgent},
"X-App": {"cli"},
"Anthropic-Beta": {"claude-code-20250219,interleaved-thinking-2025-05-14"},
}
req := cliproxyexecutor.Request{Model: "claude-opus-5-5", Payload: payload}
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude, Stream: test.stream, Headers: headers, OriginalRequest: payload}
if test.stream {
result, errStream := exec.ExecuteStream(ctx, auth, req, opts)
if errStream != nil {
t.Fatal(errStream)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
}
} else if _, errExecute := exec.Execute(ctx, auth, req, opts); errExecute != nil {
t.Fatal(errExecute)
}
}

if len(bodies) != len(inboundUserAgents) {
t.Fatalf("captured %d upstream requests, want %d", len(bodies), len(inboundUserAgents))
}
for i, body := range bodies {
if got, want := gjson.GetBytes(body, "system").Raw, gjson.GetBytes(bodies[0], "system").Raw; got != want {
t.Errorf("request %d (%s) outbound system = %s, want ordinary %s", i, inboundUserAgents[i], got, want)
}
for _, field := range []string{"system.#.cache_control", "messages", "tools"} {
if got, want := gjson.GetBytes(body, field).Raw, gjson.GetBytes(payload, field).Raw; got != want {
t.Errorf("request %d (%s) outbound %s = %s, want inbound %s", i, inboundUserAgents[i], field, got, want)
}
}
if userAgents[i] != inboundUserAgents[i] {
t.Errorf("request %d outbound User-Agent = %q, want caller %q", i, userAgents[i], inboundUserAgents[i])
}
}
})
}
}
2 changes: 1 addition & 1 deletion internal/runtime/executor/helps/claude_client_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (
var (
claudeCodeUserAgentPattern = regexp.MustCompile(`(?i)^claude-cli/`)
claudeCodeUserAgentDetailsPattern = regexp.MustCompile(`(?i)^claude-cli/\S+\s+\(external,\s*([^,)]+)(?:,\s*agent-sdk/([^,)]+))?`)
claudeCodeNativeUserAgentPattern = regexp.MustCompile(`(?i)^claude-cli/[0-9]+\.[0-9]+\.[0-9]+\s+\(external,\s*[^,)]+(?:,\s*agent-sdk/[0-9]+\.[0-9]+\.[0-9]+)?\)$`)
claudeCodeNativeUserAgentPattern = regexp.MustCompile(`(?i)^claude-cli/[0-9]+\.[0-9]+\.[0-9]+\s+\(external,\s*[^,)]+(?:,\s*agent-sdk/[0-9]+\.[0-9]+\.[0-9]+)?(?:,\s*workload/[a-z0-9_-]+)?\)$`)
Comment thread
ericlitman marked this conversation as resolved.
Resolved
)

var claudeCodeSubclientByEntrypoint = map[string]string{
Expand Down
49 changes: 38 additions & 11 deletions internal/runtime/executor/helps/claude_client_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ func TestDetectClaudeCodeRequestClassifiesEntrypoints(t *testing.T) {
{name: "remote", userAgent: "claude-cli/2.1.280 (external, remote)", entrypoint: "remote", subclient: "claude-remote"},
{name: "github-action", userAgent: "claude-cli/2.1.280 (external, claude-code-github-action)", entrypoint: "claude-code-github-action", subclient: "claude-code-gh-action"},
{name: "unknown", userAgent: "claude-cli/2.1.280 (external, copied-client)", entrypoint: "copied-client"},
{name: "sdk-ts-workload", userAgent: "claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/cron)", entrypoint: "sdk-ts", subclient: "claude-code-sdk-ts", agentSDKVersion: "0.3.276", native: true},
{name: "cli-workload", userAgent: "claude-cli/2.1.312 (external, cli, workload/cron)", entrypoint: "cli", subclient: "claude-code-cli", native: true},
{name: "vscode-agent-sdk-workload", userAgent: "claude-cli/2.1.280 (external, claude-vscode, agent-sdk/0.3.220, workload/cron)", entrypoint: "claude-vscode", subclient: "claude-code-vscode", agentSDKVersion: "0.3.220", native: true},
{name: "sdk-cli-daemon-workload-tag", userAgent: "claude-cli/2.1.312 (external, sdk-cli, workload/nightly_sync-2)", entrypoint: "sdk-cli", subclient: "claude-code-cli-sdk", native: true},
{name: "sdk-py-workload", userAgent: "claude-cli/2.1.312 (external, sdk-py, agent-sdk/0.1.0, workload/cron)", entrypoint: "sdk-py", subclient: "claude-code-sdk-py", agentSDKVersion: "0.1.0"},
{name: "desktop-workload", userAgent: "claude-cli/2.1.312 (external, claude-desktop, workload/cron)", entrypoint: "claude-desktop", subclient: "claude-desktop"},
} {
t.Run(test.name, func(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
Expand All @@ -170,17 +176,24 @@ func TestDetectClaudeCodeRequestClassifiesEntrypoints(t *testing.T) {
}

func TestDetectClaudeCodeCountTokensAllowsMissingMetadata(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", "claude-cli/2.1.280 (external, claude-vscode, agent-sdk/0.3.220)")
detection := DetectClaudeCodeRequest(headers, []byte(`{"messages":[]}`), true)
if !detection.Confirmed {
t.Fatalf("detection = %#v, want confirmed", detection)
}
if detection.MetadataUserID {
t.Fatalf("metadata signal = true, want false: %#v", detection)
}
if detection.Subclient != "claude-code-vscode" || detection.AgentSDKVersion != "0.3.220" {
t.Fatalf("count_tokens identity = %#v, want VSCode Agent SDK", detection)
for _, userAgent := range []string{
"claude-cli/2.1.280 (external, claude-vscode, agent-sdk/0.3.220)",
"claude-cli/2.1.280 (external, claude-vscode, agent-sdk/0.3.220, workload/cron)",
} {
t.Run(userAgent, func(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", userAgent)
detection := DetectClaudeCodeRequest(headers, []byte(`{"messages":[]}`), true)
if !detection.Confirmed {
t.Fatalf("detection = %#v, want confirmed", detection)
}
if detection.MetadataUserID {
t.Fatalf("metadata signal = true, want false: %#v", detection)
}
if detection.Subclient != "claude-code-vscode" || detection.AgentSDKVersion != "0.3.220" {
t.Fatalf("count_tokens identity = %#v, want VSCode Agent SDK", detection)
}
})
}
}

Expand Down Expand Up @@ -398,6 +411,12 @@ func TestDetectClaudeCodeRequestRejectsNearMissHaikuHelpers(t *testing.T) {
}
}

func workloadUserAgentHeaders(userAgent string) http.Header {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", userAgent)
return headers
}

func TestDetectClaudeCodeRequestRejectsMalformedNativeSignals(t *testing.T) {
tests := []struct {
name string
Expand All @@ -413,6 +432,14 @@ func TestDetectClaudeCodeRequestRejectsMalformedNativeSignals(t *testing.T) {
{name: "unmeasured next-minor user agent", headers: http.Header{"User-Agent": {"claude-cli/2.2.0 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "implausible future user agent", headers: http.Header{"User-Agent": {"claude-cli/999.0.0 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"claude-code-20250219"}}, userID: validClaudeCodeMetadataUserID},
{name: "unrelated beta", headers: http.Header{"User-Agent": {"claude-cli/2.1.280 (external, cli)"}, "X-App": {"cli"}, "Anthropic-Beta": {"anything"}}, userID: validClaudeCodeMetadataUserID},
{name: "empty workload tag", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/)"), userID: validClaudeCodeMetadataUserID},
{name: "workload before agent sdk", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, workload/cron, agent-sdk/0.3.276)"), userID: validClaudeCodeMetadataUserID},
{name: "duplicate workload", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/cron, workload/cron)"), userID: validClaudeCodeMetadataUserID},
{name: "workload tag with trailing text", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/cron extra)"), userID: validClaudeCodeMetadataUserID},
{name: "workload tag with punctuation", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, workload/cron.v2)"), userID: validClaudeCodeMetadataUserID},
{name: "unknown trailing segment", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, foo/bar)"), userID: validClaudeCodeMetadataUserID},
{name: "client app segment", headers: workloadUserAgentHeaders("claude-cli/2.1.312 (external, sdk-ts, agent-sdk/0.3.276, client-app/t3, workload/cron)"), userID: validClaudeCodeMetadataUserID},
{name: "older patch with workload", headers: workloadUserAgentHeaders("claude-cli/2.1.257 (external, sdk-ts, agent-sdk/0.3.276, workload/cron)"), userID: validClaudeCodeMetadataUserID},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
Loading