From 6aea3875ef20ecdd6c4f162f76bf06a31d43bd8a Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 26 Sep 2026 17:15:12 +0700 Subject: [PATCH] feat(claude): forward x-claude-code-session-id on OAuth requests --- open-sse/executors/default.js | 10 +++++++ open-sse/utils/claudeCloaking.js | 17 ++++++++++- tests/unit/claude-header-forwarding.test.js | 32 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index eadf1797..e5c59741 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -7,6 +7,7 @@ import { buildClineHeaders } from "../shared/clineAuth.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js"; +import { extractClaudeSessionIdFromUserId } from "../utils/claudeCloaking.js"; // Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults. const BEARER = { combined: true, header: "Authorization", scheme: "bearer" }; @@ -172,6 +173,15 @@ export class DefaultExecutor extends BaseExecutor { headers["Anthropic-Beta"] = mergeAnthropicBeta(headers["Anthropic-Beta"], clientBeta); } + // Claude OAuth: align x-claude-code-session-id with metadata.user_id.session_id if missing + if (this.provider === "claude" && !headers["x-claude-code-session-id"]) { + const token = credentials?.accessToken || credentials?.apiKey || ""; + if (token.includes("sk-ant-oat")) { + const sid = extractClaudeSessionIdFromUserId(body?.metadata?.user_id); + if (sid) headers["x-claude-code-session-id"] = sid; + } + } + // Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams if (this.provider?.startsWith?.("anthropic-compatible-")) { const baseUrl = credentials?.providerSpecificData?.baseUrl || ""; diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js index 7e3790df..f4b710ff 100644 --- a/open-sse/utils/claudeCloaking.js +++ b/open-sse/utils/claudeCloaking.js @@ -25,10 +25,25 @@ function deriveUuid(seed) { function generateFakeUserID(sessionId, apiKey) { const deviceId = apiKey ? createHash("sha256").update(`device:${apiKey}`).digest("hex") : randomBytes(32).toString("hex"); const accountUuid = apiKey ? deriveUuid(`account:${apiKey}`) : randomUUID(); - const sessionUuid = sessionId || randomUUID(); + const cleanSessionId = typeof sessionId === "string" ? sessionId.replace(/^claude:/i, "").trim() : null; + const sessionUuid = cleanSessionId || randomUUID(); return `{"device_id":"${deviceId}","account_uuid":"${accountUuid}","session_id":"${sessionUuid}"}`; } +export function extractClaudeSessionIdFromUserId(userId) { + if (typeof userId !== "string" || !userId) return null; + if (userId[0] === "{") { + try { + const sid = JSON.parse(userId)?.session_id; + return typeof sid === "string" && sid ? sid.replace(/^claude:/i, "").trim() || null : null; + } catch { + return null; + } + } + const clean = userId.replace(/^claude:/i, "").trim(); + return clean || null; +} + /** * Cloak tools before sending to Claude provider (anti-ban): * - Rename client tools with the CLAUDE_TOOL_SUFFIX ("_ide") in tools[] and messages[] diff --git a/tests/unit/claude-header-forwarding.test.js b/tests/unit/claude-header-forwarding.test.js index 556aafe7..653fa462 100644 --- a/tests/unit/claude-header-forwarding.test.js +++ b/tests/unit/claude-header-forwarding.test.js @@ -95,6 +95,38 @@ describe("DefaultExecutor.buildHeaders() — claude provider", () => { const executor = new DefaultExecutor("claude"); expect(() => executor.buildHeaders({ apiKey: "sk" }, false)).not.toThrow(); }); + + it("sets x-claude-code-session-id from metadata.user_id on Claude OAuth", () => { + const executor = new DefaultExecutor("claude"); + const headers = executor.buildHeaders( + { accessToken: "sk-ant-oat-test-token" }, + true, + undefined, + "claude-opus-5", + { + metadata: { + user_id: '{"device_id":"d","account_uuid":"a","session_id":"sess-abc"}', + }, + } + ); + expect(headers["x-claude-code-session-id"]).toBe("sess-abc"); + }); + + it("omits x-claude-code-session-id for non-OAuth API keys", () => { + const executor = new DefaultExecutor("claude"); + const headers = executor.buildHeaders( + { apiKey: "sk-ant-api03-xxx" }, + true, + undefined, + "claude-opus-5", + { + metadata: { + user_id: '{"device_id":"d","account_uuid":"a","session_id":"sess-abc"}', + }, + } + ); + expect(headers["x-claude-code-session-id"]).toBeUndefined(); + }); }); // ─── anthropic-compatible header stripping ────────────────────────────────────