Skip to content

fix(cliproxyapi): preserve bare metadata.user_id on Anthropic-shape bodies - #2195

Closed
NomenAK wants to merge 1 commit into
cheaper-inference:release/v3.8.0from
NomenAK:upstream/cliproxyapi-preserve-metadata-user-id
Closed

NomenAK wants to merge 1 commit into
cheaper-inference:release/v3.8.0from
NomenAK:upstream/cliproxyapi-preserve-metadata-user-id

Conversation

@NomenAK

@NomenAK NomenAK commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

#2165 stripped metadata unconditionally on Anthropic-shape bodies routed to CPA's /v1/messages (alongside client_info, prompt_cache_key, safety_identifier, etc.). That removed a useful signal that some BYOK clients (notably Capy) provide : a stable per-user metadata.user_id like user_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 same account_uuid / session_uuid across 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 under metadata (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, file internal/runtime/executor/helps/cloak_utils.go. The generateFakeUserID function builds a Claude Code-shaped identifier user_<64hex>_account_<uuidv4>_session_<uuidv4> where both UUIDs are produced by uuid.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's internal/auth/claude/anthropic_auth.go parses but doesn't persist into ClaudeTokenData).

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 :

func DeterministicFakeUserID(seed string) string {
    h := sha256.Sum256([]byte("cpa-cloak-v1|" + seed))
    return "user_" + hex.EncodeToString(h[:]) +
           "_account_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("a|"+seed)).String() +
           "_session_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("s|"+seed)).String()
}

…and use it whenever the inbound metadata.user_id is 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 :

  • Replace delete transformed.metadata (unconditional) with a conditional check :
    • Keep metadata if it's exactly { user_id: <string> } (one key, string value).
    • Strip it in every other case (no user_id, non-string user_id, extra fields alongside user_id).
  • Strip order for other Capy extras (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)
  • Existing test "strips metadata" rewritten to assert the new conditional behavior.

🤖 Generated with Claude Code

…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>
@NomenAK
NomenAK requested a review from diegosouzapw as a code owner May 12, 2026 10:14

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +251 to +259
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.
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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".

Suggested change
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;
}

@NomenAK

NomenAK commented May 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing after 2026-05-12 empirical bisect.

This PR preserved a bare {user_id: <string>} shape on metadata (and stripped anything else under metadata) so a CPA-side patch could use the inbound user_id as a deterministic seed for the cloaked user_id (account_uuid + session_uuid stable per Capy user → Anthropic prompt cache hits across calls).

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.

@NomenAK NomenAK closed this May 12, 2026
diegosouzapw added a commit that referenced this pull request Jun 30, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant