Repository navigation
Conversation
…odies The post-cheaper-inference#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_<base62>`). 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: <string>}` shape so a future CPA-side patch can use the value as a deterministic seed for the cloaked `user_<hex>_account_<uuid>_session_<uuid>` 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) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the CliproxyapiExecutor to conditionally preserve the metadata field when it contains only a user_id string, enabling prompt-cache reuse. Previously, this field was always removed. Corresponding unit tests have been added to verify the preservation of valid metadata and the removal of invalid or extra fields. Feedback was provided to improve the robustness of the metadata check by explicitly handling non-object types and arrays to prevent potential API errors.
| const md = transformed.metadata; | ||
| if (md && typeof md === "object") { | ||
| const mdRec = md as Record<string, unknown>; | ||
| const isBareUserId = typeof mdRec.user_id === "string" && Object.keys(mdRec).length === 1; | ||
| if (!isBareUserId) { | ||
| delete transformed.metadata; | ||
| } | ||
| // bare {user_id: <string>} shape : keep as-is. | ||
| } |
There was a problem hiding this comment.
The current implementation only strips metadata if it is an object that doesn't match the {user_id: string} shape. However, if metadata is a non-object (e.g., a string, number, or null), it will be preserved because the if (md && typeof md === "object") condition will be skipped. Since the previous behavior was to unconditionally delete metadata for Anthropic-shaped requests, this change could lead to 400 errors from Anthropic if a client sends a non-object metadata field.
It is safer to ensure metadata is deleted unless it matches the exact allowed shape, and to explicitly exclude arrays which also have a typeof value of "object".
| const md = transformed.metadata; | |
| if (md && typeof md === "object") { | |
| const mdRec = md as Record<string, unknown>; | |
| const isBareUserId = typeof mdRec.user_id === "string" && Object.keys(mdRec).length === 1; | |
| if (!isBareUserId) { | |
| delete transformed.metadata; | |
| } | |
| // bare {user_id: <string>} shape : keep as-is. | |
| } | |
| const md = transformed.metadata; | |
| const mdRec = md as Record<string, unknown>; | |
| const isBareUserId = | |
| md && | |
| typeof md === "object" && | |
| !Array.isArray(md) && | |
| typeof mdRec.user_id === "string" && | |
| Object.keys(mdRec).length === 1; | |
| if (!isBareUserId) { | |
| delete transformed.metadata; | |
| } |
|
Closing after 2026-05-12 empirical bisect. This PR preserved a bare Bisect against live Anthropic via CPA cloak confirmed that the entire metadata strip is unnecessary: Anthropic accepts metadata with arbitrary extra keys without flagging the extras-billing gate. The CPA-side deterministic user_id derivation now lives in router-for-me/CLIProxyAPI#3356 and works from the raw inbound user_id regardless of how cluttered the surrounding metadata object is. Result: the conditional metadata strip is over-engineering with no upstream value. Fork-side: NomenAK/OmniRoute@cleanup/remove-over-engineered-strips drops it in commit 4e9e534. |
…t from 9router#2196) Claude's tool schema requires every tool to carry an explicit `type` discriminator (custom, computer_20241022, ...). Anthropic's first-party API infers "custom" when it is omitted, but strict Anthropic-compatible gateways (e.g. MiniMax) enforce the schema and reject legacy tool payloads lacking `type` with HTTP 400 (#2195). Backfill a missing `type` to "custom" at the pre-dispatch choke point for any Claude-format outbound request (targetFormat === FORMATS.CLAUDE), mirroring the Kiro tool-sanitize block. Tools that already declare a type — including built-in tool types — are left untouched. Extracted as a pure, unit-tested helper (open-sse/handlers/chatCore/claudeToolDefaults.ts). Co-authored-by: warelik <warelik@users.noreply.github.com> Inspired-by: decolua/9router#2196
Summary
#2165 stripped
metadataunconditionally on Anthropic-shape bodies routed to CPA's/v1/messages(alongsideclient_info,prompt_cache_key,safety_identifier, etc.). That removed a useful signal that some BYOK clients (notably Capy) provide : a stable per-usermetadata.user_idlikeuser_3DGta5gzamzAUr57ZYZEtmc6lHX.Stripping it means CPA's outbound cloak (which generates a fresh random UUID per call via
uuid.New()) has no way to make Anthropic see the sameaccount_uuid/session_uuidacross calls from the same end user. No prompt-cache reuse, no per-user session continuity.This PR preserves a bare
{user_id: <string>}metadata shape only. Anything else undermetadata(which Capy's SDK sometimes adds —session_id,client_info, etc.) is still stripped because Anthropic rejects it.Related Issues
Motivation
CPA source :
router-for-me/CLIProxyAPI, fileinternal/runtime/executor/helps/cloak_utils.go. ThegenerateFakeUserIDfunction builds a Claude Code-shaped identifieruser_<64hex>_account_<uuidv4>_session_<uuidv4>where both UUIDs are produced byuuid.New()per call — no caching, no tie-back to the inbound BYOK request, no propagation of the OAuth account UUID captured during the auth handshake (which CPA'sinternal/auth/claude/anthropic_auth.goparses but doesn't persist intoClaudeTokenData).Net effect from Anthropic's perspective : every call from the same Capy user looks like a different identity. The Anthropic prompt cache, which is keyed on account + prefix, never hits across calls in the same conversation.
A small CPA-side patch can derive a deterministic CC-shaped triplet from any seed string :
…and use it whenever the inbound
metadata.user_idis non-empty but doesn't match CPA's strict CC regex. That patch is in flight on the CPA side ; this PR is the OmniRoute-side prerequisite so the seed actually arrives.Today, with stock CPA, this PR is a no-op (CPA still overwrites the inbound user_id). Once the CPA patch ships, the same Capy
user_3DGta5gz…gets a stable Anthropic identity, restoring prompt cache hits and (possibly) clearing the^mcp_[^_]recognition gate without any other rewrite.Changes
open-sse/executors/cliproxyapi.ts:delete transformed.metadata(unconditional) with a conditional check :metadataif it's exactly{ user_id: <string> }(one key, string value).user_id, non-stringuser_id, extra fields alongsideuser_id).client_info,prompt_cache_key,safety_identifier) is unchanged.Validation
npm run test:unit—tests/unit/cliproxyapi-executor.test.ts— 39/39 pass (3 new tests : preserve bare shape, strip when extras present, strip when user_id missing or non-string)🤖 Generated with Claude Code