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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions open-sse/config/cliFingerprints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,13 @@ export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
headerOrder: [
"Host",
"Content-Type",
"x-api-key",
"Authorization",
"anthropic-version",
"anthropic-beta",
"anthropic-dangerous-direct-browser-access",
"x-app",
"User-Agent",
"X-Claude-Code-Session-Id",
"x-client-request-id",
"X-Stainless-Retry-Count",
"X-Stainless-Timeout",
"X-Stainless-Lang",
Expand All @@ -117,7 +116,6 @@ export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
"X-Stainless-Runtime",
"X-Stainless-Runtime-Version",
"Accept",
"accept-language",
"accept-encoding",
"Connection",
],
Expand All @@ -130,7 +128,6 @@ export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
"metadata",
"max_tokens",
"thinking",
"context_management",
"output_config",
"stream",
],
Expand Down
328 changes: 216 additions & 112 deletions open-sse/services/claudeCodeCompatible.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions open-sse/utils/cacheControlPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export function isClaudeCodeClient(userAgent: string | null | undefined): boolea

// Claude Code user agents
if (ua.includes("claude-code") || ua.includes("claude_code")) return true;
if (ua.includes("claude-cli/")) return true;
if (ua.includes("sdk-cli")) return true;
if (ua.includes("anthropic") && ua.includes("cli")) return true;

return false;
Expand Down
1 change: 1 addition & 0 deletions tests/unit/cache-control-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("Cache Control Policy", () => {
assert.equal(isClaudeCodeClient("claude-code/0.1.0"), true);
assert.equal(isClaudeCodeClient("claude_code/0.1.0"), true);
assert.equal(isClaudeCodeClient("Anthropic CLI/1.0"), true);
assert.equal(isClaudeCodeClient("claude-cli/2.1.113 (external, sdk-cli)"), true);
});

test("rejects non-Claude clients", () => {
Expand Down
40 changes: 20 additions & 20 deletions tests/unit/cc-compatible-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,16 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t
assert.equal(payload.messages[0].content.at(-1).cache_control, undefined);
assert.equal(payload.messages[1].content.at(-1).cache_control, undefined);
assert.equal(payload.messages[2].content.at(-1).cache_control, undefined);
assert.equal(payload.system.length, 4);
assert.equal(payload.system.at(-1).text, "sys");
assert.equal(payload.system.length, 3);
assert.match(payload.system[0].text, /Claude Agent SDK/);
assert.equal(payload.system[0].cache_control, undefined);
assert.match(
payload.system[1].text,
/interactive agent that helps users with software engineering tasks/
);
assert.equal(payload.system[1].cache_control, undefined);
assert.equal(payload.system[2].text, "sys");
assert.equal(payload.system[2].cache_control, undefined);
assert.equal(payload.system[3].cache_control, undefined);
assert.equal(payload.tools.length, 1);
assert.deepEqual(payload.tools[0], {
name: "lookup_weather",
Expand All @@ -139,7 +143,7 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t
},
});
assert.deepEqual(payload.tool_choice, { type: "any" });
assert.equal(payload.context_management.edits[0].type, "clear_thinking_20251015");
assert.equal(payload.context_management, undefined);
assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1");
});

Expand Down Expand Up @@ -212,8 +216,7 @@ test("buildClaudeCodeCompatibleRequest preserves Claude cache markers when reque
preserveCacheControl: true,
});

assert.equal(payload.system[0].cache_control, undefined);
assert.deepEqual(payload.system.at(-1).cache_control, { type: "ephemeral", ttl: "5m" });
assert.deepEqual(payload.system[0].cache_control, { type: "ephemeral", ttl: "5m" });
assert.deepEqual(payload.messages[0].content[0].cache_control, { type: "ephemeral" });
assert.deepEqual(payload.messages[1].content[0].cache_control, {
type: "ephemeral",
Expand Down Expand Up @@ -294,11 +297,8 @@ test("buildClaudeCodeCompatibleRequest keeps built-in system blocks untagged whe
preserveCacheControl: true,
});

assert.equal(payload.system[0].cache_control, undefined);
assert.equal(payload.system[1].cache_control, undefined);
assert.equal(payload.system[2].cache_control, undefined);
assert.deepEqual(payload.system[3].cache_control, { type: "ephemeral" });
assert.deepEqual(payload.system[4].cache_control, { type: "ephemeral", ttl: "1h" });
assert.deepEqual(payload.system[0].cache_control, { type: "ephemeral" });
assert.deepEqual(payload.system[1].cache_control, { type: "ephemeral", ttl: "1h" });
});

test("buildClaudeCodeCompatibleRequest does not add cache markers in non-preserve mode", () => {
Expand All @@ -325,10 +325,10 @@ test("buildClaudeCodeCompatibleRequest does not add cache markers in non-preserv
preserveCacheControl: false,
});

assert.equal(payload.system.length, 3);
assert.equal(payload.system[0].cache_control, undefined);
assert.equal(payload.system[1].cache_control, undefined);
assert.equal(payload.system[2].cache_control, undefined);
assert.equal(payload.system[3].cache_control, undefined);
assert.equal(payload.messages[0].content[0].cache_control, undefined);
assert.equal(payload.messages[1].content[0].cache_control, undefined);
assert.equal(payload.messages[2].content[0].cache_control, undefined);
Expand Down Expand Up @@ -436,10 +436,10 @@ test("DefaultExecutor uses CC-compatible path and headers", () => {
);

const headers = executor.buildHeaders(credentials, true);
assert.equal(headers["x-api-key"], "sk-test");
assert.equal(headers.Authorization, "Bearer sk-test");
assert.equal(headers["x-api-key"], undefined);
assert.equal(headers["X-Claude-Code-Session-Id"], "session-3");
assert.equal(headers.Accept, "text/event-stream");
assert.equal(headers.Authorization, undefined);
assert.equal(headers.Accept, "application/json");
});

test("validateProviderApiKey uses CC skeleton request after /models fallback", async () => {
Expand Down Expand Up @@ -478,8 +478,9 @@ test("validateProviderApiKey uses CC skeleton request after /models fallback", a
assert.equal(calls[1].body.model, "claude-sonnet-4-6");
assert.equal(calls[1].body.messages[0].role, "user");
assert.equal(calls[1].body.stream, true);
assert.equal(calls[1].headers["x-api-key"], "sk-test");
assert.equal(calls[1].headers.Accept, "text/event-stream");
assert.equal(calls[1].headers.Authorization, "Bearer sk-test");
assert.equal(calls[1].headers["x-api-key"], undefined);
assert.equal(calls[1].headers.Accept, "application/json");
});

test("handleChatCore forces SSE upstream for CC compatible providers while returning JSON to non-stream clients", async () => {
Expand Down Expand Up @@ -557,7 +558,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur

assert.equal(result.success, true);
assert.equal(calls.length, 1);
assert.equal(calls[0].headers.Accept, "text/event-stream");
assert.equal(calls[0].headers.Accept, "application/json");
assert.equal(calls[0].body.stream, true);
assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false);

Expand Down Expand Up @@ -671,8 +672,7 @@ test("handleChatCore preserves client cache markers for Claude Code requests to

assert.equal(result.success, true);
assert.equal(calls.length, 1);
assert.equal(calls[0].body.system[0].cache_control, undefined);
assert.deepEqual(calls[0].body.system.at(-1).cache_control, {
assert.deepEqual(calls[0].body.system[0].cache_control, {
type: "ephemeral",
ttl: "5m",
});
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/chatcore-translation-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,11 @@ test("chatCore builds Claude Code-compatible upstream requests for CC providers"
});

assert.equal(result.success, true);
assert.equal(call.headers.Accept ?? call.headers.accept, "text/event-stream");
assert.equal(call.headers.Accept ?? call.headers.accept, "application/json");
assert.equal(call.body.stream, true);
assert.equal(call.body.context_management.edits[0].type, "clear_thinking_20251015");
assert.equal(call.body.context_management, undefined);
assert.equal(call.body.system.length, 2);
assert.match(call.body.system[0].text, /Claude Agent SDK/);
assert.equal(typeof call.body.metadata.user_id, "string");
assert.equal(call.body.messages[0].role, "user");
assert.equal(call.body.messages[0].content[0].text, "Ping");
Expand Down
35 changes: 23 additions & 12 deletions tests/unit/claude-code-compatible-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ test("base URL helpers strip messages suffixes and join canonical paths", () =>
);
});

test("buildClaudeCodeCompatibleHeaders emits stream-aware auth headers and session id", () => {
test("buildClaudeCodeCompatibleHeaders emits bearer auth headers and session id", () => {
const streamHeaders = buildClaudeCodeCompatibleHeaders("sk-demo", true, "session-123");
const jsonHeaders = buildClaudeCodeCompatibleHeaders("sk-demo", false);

assert.equal(streamHeaders.Accept, "text/event-stream");
assert.equal(streamHeaders["x-api-key"], "sk-demo");
assert.equal(streamHeaders.Accept, "application/json");
assert.equal(streamHeaders.Authorization, "Bearer sk-demo");
assert.equal(streamHeaders["x-api-key"], undefined);
assert.equal(streamHeaders["X-Claude-Code-Session-Id"], "session-123");
assert.equal(
streamHeaders["X-Stainless-Timeout"],
Expand All @@ -56,13 +57,19 @@ test("buildClaudeCodeCompatibleHeaders emits stream-aware auth headers and sessi
assert.equal(jsonHeaders["X-Claude-Code-Session-Id"], undefined);
});

test("Claude Code compatible beta set stays conservative for third-party proxies", () => {
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("oauth-2025-04-20"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("advanced-tool-use-2025-11-20"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("fast-mode-2026-02-01"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("token-efficient-tools-2026-03-28"));
assert.equal(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("fast-mode-2025-04-01"), false);
assert.equal(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("redact-thinking-2025-06-20"), false);
test("Claude Code compatible beta set matches the stable API-key Claude CLI profile", () => {
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("claude-code-20250219"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("interleaved-thinking-2025-05-14"));
assert.ok(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("effort-2025-11-24"));
assert.equal(CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("oauth-2025-04-20"), false);
assert.equal(
CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("context-management-2025-06-27"),
false
);
assert.equal(
CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA.includes("prompt-caching-scope-2026-01-05"),
false
);
});

test("resolveClaudeCodeCompatibleSessionId prefers explicit session headers and generates a fallback id", () => {
Expand Down Expand Up @@ -91,8 +98,12 @@ test("buildClaudeCodeCompatibleValidationPayload produces the expected smoke-tes
content: [{ type: "text", text: "ok" }],
});
assert.equal(payload.tools.length, 0);
assert.equal(payload.system[0].cache_control, undefined);
assert.equal(payload.system.length, 2);
assert.match(payload.system[0].text, /Claude Agent SDK/);
assert.match(
payload.system[1].text,
/interactive agent that helps users with software engineering tasks/
);
assert.ok(JSON.parse(payload.metadata.user_id).session_id);
assert.ok(payload.system.some((block) => String(block.text).includes(process.cwd())));
assert.ok(CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS > payload.max_tokens);
});
21 changes: 16 additions & 5 deletions tests/unit/claude-code-compatible-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,14 @@ test("buildClaudeCodeCompatibleRequest covers normalized OpenAI-style messages,
content: [{ type: "text", text: "draft answer\nalternate answer" }],
},
]);
assert.equal(payload.system[3].text, "system note");
assert.equal(payload.system[4].text, "developer note");
assert.equal(payload.system.length, 4);
assert.match(payload.system[0].text, /Claude Agent SDK/);
assert.match(
payload.system[1].text,
/interactive agent that helps users with software engineering tasks/
);
assert.equal(payload.system[2].text, "system note");
assert.equal(payload.system[3].text, "developer note");
assert.equal(payload.tools.length, 1);
assert.deepEqual(payload.tools[0], {
name: "lookup_account",
Expand Down Expand Up @@ -157,6 +163,8 @@ test("buildClaudeCodeCompatibleRequest covers Claude-native bodies and cache-con
{ name: "toolA", input_schema: { type: "object" }, cache_control: { type: "ephemeral" } },
],
thinking: { type: "enabled", budget_tokens: 12 },
output_config: { format: "compact" },
metadata: { foo: "bar" },
},
model: "claude-sonnet-4-6",
preserveCacheControl: false,
Expand Down Expand Up @@ -194,11 +202,12 @@ test("buildClaudeCodeCompatibleRequest covers Claude-native bodies and cache-con
assert.equal(stripped.messages.at(-1).role, "user");
assert.equal(stripped.system[0].cache_control, undefined);
assert.equal(stripped.messages[0].content[0].cache_control, undefined);
assert.equal(stripped.system.at(-1).cache_control, undefined);
assert.equal(stripped.tools[0].cache_control, undefined);
assert.equal(preserved.system[0].cache_control, undefined);
assert.deepEqual(stripped.thinking, { type: "enabled", budget_tokens: 12 });
assert.deepEqual(stripped.output_config, { effort: "high", format: "compact" });
assert.equal(stripped.metadata.foo, "bar");
assert.deepEqual(preserved.system[0].cache_control, { type: "ephemeral" });
assert.equal(preserved.messages[0].content[0].cache_control.type, "ephemeral");
assert.equal(preserved.system.at(-1).cache_control.type, "ephemeral");
assert.equal(preserved.tools[0].cache_control.type, "ephemeral");
});

Expand All @@ -217,6 +226,8 @@ test("buildClaudeCodeCompatibleRequest omits tool choice when there are no tools
assert.equal(payload.tools.length, 0);
assert.equal("tool_choice" in payload, false);
assert.equal(payload.output_config.effort, "high");
assert.equal(payload.system.length, 2);
assert.match(payload.system[0].text, /Claude Agent SDK/);
});

test("buildClaudeCodeCompatibleRequest covers string system input, non-array Claude fields and tool choice variants", () => {
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/executor-default-base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,11 @@ test("DefaultExecutor.buildHeaders rotates extra API keys and builds Claude Code

assert.equal(first.Authorization, "Bearer primary");
assert.equal(second.Authorization, "Bearer extra-1");
assert.equal(ccHeaders["x-api-key"], "cc-key");
assert.equal(ccHeaders.Authorization, "Bearer cc-key");
assert.equal(ccHeaders["x-api-key"], undefined);
assert.equal(ccHeaders["anthropic-version"], CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION);
assert.equal(ccHeaders["X-Claude-Code-Session-Id"], "session-1");
assert.equal(ccHeaders.Accept, "text/event-stream");
assert.equal(ccHeaders.Accept, "application/json");
assert.equal(ccJsonHeaders.Accept, "application/json");
});

Expand Down