From 841257c434788534d0e556ae0ae1d97b946b330f Mon Sep 17 00:00:00 2001 From: OmniRoute Ops Date: Tue, 12 May 2026 10:13:36 +0000 Subject: [PATCH] fix(cliproxyapi): preserve bare metadata.user_id on Anthropic-shape bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-#2165 strip removed `metadata` unconditionally. That throws away the inbound `metadata.user_id` field that BYOK clients like Capy send (stable per Capy user — `user_`). CPA's outbound cloak then generates a fresh random UUID per call (cf. `internal/runtime/executor/ helps/cloak_utils.go::generateFakeUserID` in router-for-me/CLIProxyAPI), so Anthropic sees a different `account_uuid` and `session_uuid` for every call from the same BYOK user — no prompt-cache reuse, and the account fails Anthropic's "real Claude Code account" recognition signal that likely gates `^mcp_[^_]` rejection / "Extra usage required" 400s. Pass through a bare `{user_id: }` shape so a future CPA-side patch can use the value as a deterministic seed for the cloaked `user__account__session_` triplet. Strip metadata entirely if Capy's SDK adds anything beyond `user_id` (`session_id`, `client_info`, etc., which Anthropic rejects). No client-visible change today — the change only becomes useful when CPA honors the inbound user_id ; without that patch CPA continues to overwrite with its random fake. The OmniRoute-side change is a no-op-on-its-own prerequisite that costs nothing if CPA never adopts the cooperation. Co-Authored-By: Claude Opus 4.7 (1M context) --- open-sse/executors/cliproxyapi.ts | 22 ++++++++++++++- tests/unit/cliproxyapi-executor.test.ts | 37 +++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/open-sse/executors/cliproxyapi.ts b/open-sse/executors/cliproxyapi.ts index 601d100e037c..7262735db9a5 100644 --- a/open-sse/executors/cliproxyapi.ts +++ b/open-sse/executors/cliproxyapi.ts @@ -236,7 +236,27 @@ export class CliproxyapiExecutor extends BaseExecutor { delete transformed.client_info; delete transformed.prompt_cache_key; delete transformed.safety_identifier; - delete transformed.metadata; + + // Conditional metadata strip. Preserve a bare `{user_id: }` + // shape so a CPA-side patch can use the inbound user_id as a + // deterministic seed for the cloaked Claude-Code-shaped user_id + // (current CPA generates a fresh random UUIDv4 per call → no + // Anthropic prompt-cache reuse across calls from the same BYOK + // user ; passing through a stable Capy `metadata.user_id` lets CPA + // derive a stable `account_uuid`/`session_uuid` triplet from it). + // + // Strip metadata entirely if it carries any field beyond `user_id` + // (Capy's SDK can add `session_id`, `client_info`, etc., which + // Anthropic rejects). + const md = transformed.metadata; + if (md && typeof md === "object") { + const mdRec = md as Record; + const isBareUserId = typeof mdRec.user_id === "string" && Object.keys(mdRec).length === 1; + if (!isBareUserId) { + delete transformed.metadata; + } + // bare {user_id: } shape : keep as-is. + } // Conditional thinking strip: preserve Anthropic-valid shapes // ({type:"enabled"|"disabled", budget_tokens:N}) that applyThinkingBudget diff --git a/tests/unit/cliproxyapi-executor.test.ts b/tests/unit/cliproxyapi-executor.test.ts index 0d65bf243134..0eac00bbfdf6 100644 --- a/tests/unit/cliproxyapi-executor.test.ts +++ b/tests/unit/cliproxyapi-executor.test.ts @@ -356,17 +356,50 @@ describe("CliproxyapiExecutor", () => { assert.equal(result.output_config, undefined); }); - it("strips metadata", () => { + it("preserves bare metadata {user_id} as a CPA cloak-seed", () => { const exec = new CliproxyapiExecutor(); const result = exec.transformRequest( "claude-opus-4-7", - anthropicBody({ metadata: { user_id: "abc" } }), + anthropicBody({ metadata: { user_id: "user_3DGta5gzamzAUr57ZYZEtmc6lHX" } }), + true, + {} + ); + assert.deepEqual(result.metadata, { + user_id: "user_3DGta5gzamzAUr57ZYZEtmc6lHX", + }); + }); + + it("strips metadata when it carries extras beyond user_id", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ + metadata: { user_id: "abc", session_id: "s1", client_info: { name: "Capy" } }, + }), true, {} ); assert.equal(result.metadata, undefined); }); + it("strips metadata when user_id is missing or non-string", () => { + const exec = new CliproxyapiExecutor(); + const result1 = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ metadata: { session_id: "s1" } }), + true, + {} + ); + assert.equal(result1.metadata, undefined); + const result2 = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ metadata: { user_id: 42 } }), + true, + {} + ); + assert.equal(result2.metadata, undefined); + }); + it("strips client_info", () => { const exec = new CliproxyapiExecutor(); const result = exec.transformRequest(