feat(claude): forward x-claude-code-session-id on OAuth requests
This commit is contained in:
@@ -7,6 +7,7 @@ import { buildClineHeaders } from "../shared/clineAuth.js";
|
|||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
||||||
import { stripUnsupportedParams } from "../translator/concerns/paramSupport.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.
|
// Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults.
|
||||||
const BEARER = { combined: true, header: "Authorization", scheme: "bearer" };
|
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);
|
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
|
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
|
||||||
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
if (this.provider?.startsWith?.("anthropic-compatible-")) {
|
||||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || "";
|
const baseUrl = credentials?.providerSpecificData?.baseUrl || "";
|
||||||
|
|||||||
@@ -25,10 +25,25 @@ function deriveUuid(seed) {
|
|||||||
function generateFakeUserID(sessionId, apiKey) {
|
function generateFakeUserID(sessionId, apiKey) {
|
||||||
const deviceId = apiKey ? createHash("sha256").update(`device:${apiKey}`).digest("hex") : randomBytes(32).toString("hex");
|
const deviceId = apiKey ? createHash("sha256").update(`device:${apiKey}`).digest("hex") : randomBytes(32).toString("hex");
|
||||||
const accountUuid = apiKey ? deriveUuid(`account:${apiKey}`) : randomUUID();
|
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}"}`;
|
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):
|
* Cloak tools before sending to Claude provider (anti-ban):
|
||||||
* - Rename client tools with the CLAUDE_TOOL_SUFFIX ("_ide") in tools[] and messages[]
|
* - Rename client tools with the CLAUDE_TOOL_SUFFIX ("_ide") in tools[] and messages[]
|
||||||
|
|||||||
@@ -95,6 +95,38 @@ describe("DefaultExecutor.buildHeaders() — claude provider", () => {
|
|||||||
const executor = new DefaultExecutor("claude");
|
const executor = new DefaultExecutor("claude");
|
||||||
expect(() => executor.buildHeaders({ apiKey: "sk" }, false)).not.toThrow();
|
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 ────────────────────────────────────
|
// ─── anthropic-compatible header stripping ────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user