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
106 changes: 106 additions & 0 deletions internal/runtime/executor/claude_executor_t3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package executor

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
"github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps"
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 TestClaudeExecutorT3CodePreservesNativeWire(t *testing.T) {
for _, mode := range []string{"execute", "stream", "count"} {
for _, scheduled := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/scheduled=%t", mode, scheduled), func(t *testing.T) {
helps.ResetClaudeDeviceProfileCache()
var body []byte
var headers http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ = io.ReadAll(r.Body)
headers = r.Header.Clone()
if mode == "count" {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"input_tokens":42}`)
} else if mode == "stream" {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_t3\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5-5\",\"content\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
} else {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"id":"msg_t3","type":"message","role":"assistant","model":"claude-sonnet-5-5","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":1,"output_tokens":1}}`)
}
}))
defer server.Close()
suffix := ""
if scheduled {
suffix = ", workload/cron"
}
userAgent := "claude-cli/2.1.316 (external, t3-code, agent-sdk/0.3.276" + suffix + ")"
incoming := claudeNativeHelperHeaders("claude-code-20250219,"+claudeNativeHelperCoreBetas+",extended-cache-ttl-2025-04-11", "gzip, deflate, br, zstd")
incoming.Set("User-Agent", userAgent)
payload := []byte(`{"model":"claude-sonnet-5-5","max_tokens":1024,"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.316; cc_entrypoint=t3-code;"},{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.","cache_control":{"type":"ephemeral","ttl":"1h"}},{"type":"text","text":"Caller instructions","cache_control":{"type":"ephemeral","ttl":"1h"}}],"tools":[{"name":"Bash","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"metadata":{"user_id":` + fmt.Sprintf("%q", claudeNativeHelperUserID) + `}}`)
executor := NewClaudeExecutor(&config.Config{})
request := cliproxyexecutor.Request{Model: "claude-sonnet-5-5", Payload: payload}
options := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatClaude, OriginalRequest: payload, Headers: incoming}
auth := claudeNativeHelperOAuthAuth(server.URL)
switch mode {
case "execute":
if _, errExecute := executor.Execute(context.Background(), auth, request, options); errExecute != nil {
t.Fatal(errExecute)
}
case "stream":
result, errStream := executor.ExecuteStream(context.Background(), auth, request, options)
if errStream != nil {
t.Fatal(errStream)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
}
case "count":
if _, errCount := executor.countTokensUpstream(context.Background(), auth, request, options); errCount != nil {
t.Fatal(errCount)
}
}
if got := headers.Get("User-Agent"); got != userAgent {
t.Fatalf("User-Agent = %q, want %q", got, userAgent)
}
if mode == "count" {
if got := gjson.GetBytes(body, "system.#").Int(); got != 2 {
t.Fatalf("count system blocks = %d, want 2 (billing excluded)", got)
}
for i := 0; i < 2; i++ {
if got, want := gjson.GetBytes(body, fmt.Sprintf("system.%d", i)).Raw, gjson.GetBytes(payload, fmt.Sprintf("system.%d", i+1)).Raw; got != want {
t.Fatalf("count system block changed: got %s, want %s", got, want)
}
}
} else {
if got := gjson.GetBytes(body, "system.0.text").String(); !strings.Contains(got, "cc_entrypoint=t3-code;") {
t.Fatalf("billing attribution changed: %q", got)
}
if got := gjson.GetBytes(body, "system.#").Int(); got != 3 {
t.Fatalf("system blocks = %d, want 3", got)
}
for _, path := range []string{"system.1", "system.2"} {
if got, want := gjson.GetBytes(body, path).Raw, gjson.GetBytes(payload, path).Raw; got != want {
t.Fatalf("%s changed: got %s, want %s", path, got, want)
}
}
}
for _, path := range []string{"tools", "messages"} {
if got, want := gjson.GetBytes(body, path).Raw, gjson.GetBytes(payload, path).Raw; got != want {
t.Fatalf("%s changed: got %s, want %s", path, got, want)
}
}
})
}
}
}
2 changes: 2 additions & 0 deletions internal/runtime/executor/helps/claude_client_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var claudeCodeSubclientByEntrypoint = map[string]string{
"bench": "claude-code-bench",
"sdk-cli": "claude-code-cli-sdk",
"sdk-ts": "claude-code-sdk-ts",
"t3-code": "t3-code",
"sdk-py": "claude-code-sdk-py",
"claude-vscode": "claude-code-vscode",
"claude-code-github-action": "claude-code-gh-action",
Expand Down Expand Up @@ -67,6 +68,7 @@ var nativeClaudeEntrypoints = map[string]bool{
"cli": true,
"sdk-cli": true,
"sdk-ts": true,
"t3-code": true,
"claude-vscode": true,
}

Expand Down
63 changes: 45 additions & 18 deletions internal/runtime/executor/helps/claude_client_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ func TestDetectClaudeCodeRequestClassifiesEntrypoints(t *testing.T) {
{name: "vscode-agent-sdk", userAgent: "claude-cli/2.1.280 (external, claude-vscode, agent-sdk/0.3.220)", entrypoint: "claude-vscode", subclient: "claude-code-vscode", agentSDKVersion: "0.3.220", native: true},
{name: "sdk-cli", userAgent: "claude-cli/2.1.280 (external, sdk-cli)", entrypoint: "sdk-cli", subclient: "claude-code-cli-sdk", native: true},
{name: "sdk-ts", userAgent: "claude-cli/2.1.280 (external, sdk-ts, agent-sdk/0.3.220)", entrypoint: "sdk-ts", subclient: "claude-code-sdk-ts", agentSDKVersion: "0.3.220", native: true},
{name: "t3-code", userAgent: "claude-cli/2.1.316 (external, t3-code)", entrypoint: "t3-code", subclient: "t3-code", native: true},
{name: "t3-code-sdk", userAgent: "claude-cli/2.1.316 (external, t3-code, agent-sdk/0.3.276)", entrypoint: "t3-code", subclient: "t3-code", agentSDKVersion: "0.3.276", native: true},
{name: "t3-code-scheduled", userAgent: "claude-cli/2.1.316 (external, t3-code, agent-sdk/0.3.276, workload/cron)", entrypoint: "t3-code", subclient: "t3-code", agentSDKVersion: "0.3.276", native: true},
{name: "sdk-py", userAgent: "claude-cli/2.1.280 (external, sdk-py, agent-sdk/0.1.0)", entrypoint: "sdk-py", subclient: "claude-code-sdk-py", agentSDKVersion: "0.1.0"},
{name: "desktop", userAgent: "claude-cli/2.1.280 (external, claude-desktop)", entrypoint: "claude-desktop", subclient: "claude-desktop"},
{name: "desktop-third-party-inference", userAgent: "claude-cli/2.1.280 (external, claude-desktop-3p)", entrypoint: "claude-desktop-3p", subclient: "claude-desktop-3p"},
Expand Down Expand Up @@ -625,26 +628,50 @@ func TestDetectClaudeCodeRequestRecognizesAgentSDKHaiku55Helpers(t *testing.T) {
webSearchBetas := "interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,per-turn-control-2026-07-01,mid-conversation-tool-changes-2026-07-01,inline-tools-2026-09-15,advisor-tool-2026-03-01,effort-2025-11-24,dangerous-tool-use-2026-09-03,extended-cache-ttl-2025-04-11"
webSearchPayload := []byte(`{"max_tokens":1024,"messages":[{"role":"user","content":[{"type":"text","text":"search query"}]}],"metadata":{"user_id":` + string(encodedUserID) + `},"model":"claude-haiku-5-5","output_config":{"effort":"medium"},"stream":true,"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.304.2e0; cc_entrypoint=sdk-ts"},{"type":"text","text":"You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK."},{"type":"text","text":"Perform web search."}],"tool_choice":{"type":"auto"},"tools":[{"type":"web_search_20250305","name":"web_search"}]}`)

for _, tc := range []struct {
name string
betas string
payload []byte
}{
{name: "sdk-ts title helper", betas: titleBetas, payload: titlePayload},
{name: "sdk-ts web_search helper", betas: webSearchBetas, payload: webSearchPayload},
} {
t.Run(tc.name, func(t *testing.T) {
headers := measuredClaudeCodeHelperHeaders(tc.betas)
headers.Set("User-Agent", "claude-cli/2.1.304 (external, sdk-ts, agent-sdk/0.3.276)")
headers.Set("X-Stainless-Package-Version", "0.128.0")
headers.Del("X-Client-Request-Id")
for _, entrypoint := range []string{"sdk-ts", "t3-code"} {
for _, tc := range []struct {
name string
betas string
payload []byte
}{
{name: "sdk-ts title helper", betas: titleBetas, payload: titlePayload},
{name: "sdk-ts web_search helper", betas: webSearchBetas, payload: webSearchPayload},
} {
t.Run(entrypoint+"/"+tc.name, func(t *testing.T) {
payload := []byte(strings.ReplaceAll(string(tc.payload), "cc_entrypoint=sdk-ts", "cc_entrypoint="+entrypoint))
headers := measuredClaudeCodeHelperHeaders(tc.betas)
headers.Set("User-Agent", "claude-cli/2.1.304 (external, "+entrypoint+", agent-sdk/0.3.276)")
headers.Set("X-Stainless-Package-Version", "0.128.0")
headers.Del("X-Client-Request-Id")

detection := DetectClaudeCodeRequest(headers, payload, false)
if !detection.Confirmed || !detection.StrongSignals || !detection.NativeClient || !detection.HelperProfile {
t.Fatalf("detection = %#v, want confirmed Agent SDK Haiku 5.5 helper", detection)
}
if detection.Entrypoint != entrypoint || detection.Subclient != claudeCodeSubclientByEntrypoint[entrypoint] || detection.AgentSDKVersion != "0.3.276" {
t.Fatalf("detection identity = %#v, want matching Agent SDK identity / 0.3.276", detection)
}
})
}
}
}

detection := DetectClaudeCodeRequest(headers, tc.payload, false)
if !detection.Confirmed || !detection.StrongSignals || !detection.NativeClient || !detection.HelperProfile {
t.Fatalf("detection = %#v, want confirmed sdk-ts Haiku 5.5 helper", detection)
func TestDetectT3CodeRequiresNativeSignals(t *testing.T) {
for _, missing := range []string{"X-App", "Anthropic-Beta", "metadata", "version"} {
t.Run(missing, func(t *testing.T) {
headers := confirmedClaudeCodeHeaders()
headers.Set("User-Agent", "claude-cli/2.1.316 (external, t3-code, agent-sdk/0.3.276)")
payload := claudeCodeDetectionPayload(validClaudeCodeMetadataUserID)
switch missing {
case "metadata":
payload = []byte(`{"messages":[]}`)
case "version":
headers.Set("User-Agent", "claude-cli/999.0.0 (external, t3-code)")
default:
headers.Del(missing)
}
if detection.Entrypoint != "sdk-ts" || detection.Subclient != "claude-code-sdk-ts" || detection.AgentSDKVersion != "0.3.276" {
t.Fatalf("detection identity = %#v, want sdk-ts / claude-code-sdk-ts / 0.3.276", detection)
if detection := DetectClaudeCodeRequest(headers, payload, false); detection.Confirmed {
t.Fatalf("confirmed with missing %s: %#v", missing, detection)
}
})
}
Expand Down
Loading