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
22 changes: 21 additions & 1 deletion open-sse/executors/cliproxyapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <string>}`
// 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<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.
}
Comment on lines +251 to +259

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


// Conditional thinking strip: preserve Anthropic-valid shapes
// ({type:"enabled"|"disabled", budget_tokens:N}) that applyThinkingBudget
Expand Down
37 changes: 35 additions & 2 deletions tests/unit/cliproxyapi-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down