Skip to content
Closed
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
7 changes: 5 additions & 2 deletions src/claude/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,13 +442,16 @@ function translateAnthropicRequest(
if (outputConfigFormat) body.text = { format: outputConfigFormat };
let cacheKeySource: ClaudeCacheKeySource = null;
if (isRec(raw.metadata) && typeof raw.metadata.user_id === "string") {
body.user = raw.metadata.user_id;
const userIdHash = createHash("sha256").update(raw.metadata.user_id).digest("hex");
// OpenAI and Azure reject `user` longer than 64 chars, and Claude Code's metadata.user_id
// is a JSON blob well past that; send its 64-char hash instead of the raw value.
body.user = raw.metadata.user_id.length <= 64 ? raw.metadata.user_id : userIdHash;
// OpenAI-side prompt caching is routed by prompt_cache_key (Codex clients send
// their session id; without it consecutive /v1/messages turns reported
// cached_tokens: 0 on the ChatGPT backend — devlog 090). Claude Code's
// metadata.user_id embeds the session uuid, so hashing it yields a stable
// per-session key with a bounded length/charset.
body.prompt_cache_key = createHash("sha256").update(raw.metadata.user_id).digest("hex").slice(0, 32);
body.prompt_cache_key = userIdHash.slice(0, 32);
cacheKeySource = "metadata";
} else if (systemParts.length > 0) {
// Claude Desktop sends no metadata.user_id (H1, devlog 130): without any key the
Expand Down
10 changes: 10 additions & 0 deletions tests/claude-integration/claude-inbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,16 @@ describe("prompt cache key provenance (devlog 130 B3)", () => {
expect(body.prompt_cache_key).toMatch(/^[0-9a-f]{32}$/);
});

test("metadata.user_id longer than 64 chars is hashed into user (OpenAI/Azure limit)", () => {
const userId = JSON.stringify({ device_id: "d".repeat(64), account_uuid: "", session_id: "s".repeat(36) });
const { body } = anthropicToResponsesTranslation({
model: "m", max_tokens: 1, messages,
metadata: { user_id: userId },
});
expect(body.user).toMatch(/^[0-9a-f]{64}$/);
expect(body.prompt_cache_key).toBe((body.user as string).slice(0, 32));
});

test("no metadata + system present: fallback key from system hash, source=system", () => {
const a = anthropicToResponsesTranslation({ model: "m", max_tokens: 1, messages, system: "be nice" });
const b = anthropicToResponsesTranslation({ model: "m", max_tokens: 1, messages, system: "be nice" });
Expand Down
Loading