From ec9702da1edb156d17629f02932841eee58de3af Mon Sep 17 00:00:00 2001 From: Giulio Leone Date: Wed, 23 Sep 2026 20:04:40 +0300 Subject: [PATCH] fix(claude): bound Responses user to 64 chars for long metadata.user_id Claude Code sends metadata.user_id as a ~186-char JSON string. It was copied verbatim into the Responses 'user' field, which Azure OpenAI and other OpenAI-compatible backends cap at 64 chars, so every Claude Code request routed to Azure failed with 400 "Invalid 'user': string too long". Keep short ids as-is and send the SHA-256 hex digest (already computed for prompt_cache_key) when the id exceeds 64 chars. Fixes #5705 --- src/claude/inbound.ts | 7 +++++-- tests/claude-integration/claude-inbound.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/claude/inbound.ts b/src/claude/inbound.ts index 489075b1e5b..547a8a1c9b7 100644 --- a/src/claude/inbound.ts +++ b/src/claude/inbound.ts @@ -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 diff --git a/tests/claude-integration/claude-inbound.test.ts b/tests/claude-integration/claude-inbound.test.ts index 2390d7f82d9..db44ac75cd2 100644 --- a/tests/claude-integration/claude-inbound.test.ts +++ b/tests/claude-integration/claude-inbound.test.ts @@ -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" });