Files
OmniRoute/open-sse/services/claudeCodeFingerprint.ts
diegosouzapw 3b4e7b0e5f feat(claude-code): PR #1188 — Native parity with CCH signing, tool remapping, and API constraints
Squash merge of feat/claude-code-native-parity into release/v3.6.5.

## Wiring

1. base.ts: CCH xxHash64 body signing for anthropic-compatible-cc-* providers,
   applied after CLI fingerprint ordering so the hash covers the final bytes
   sent upstream.

2. chatCore.ts: After buildClaudeCodeCompatibleRequest(), applies synchronous
   parity pipeline steps:
   - remapToolNamesInRequest() — TitleCase tool name mapping (14 tools)
   - enforceThinkingTemperature() — temperature=1 when thinking active
   - disableThinkingIfToolChoiceForced() — remove thinking on forced tool_choice
   Cache-control limit enforcement intentionally omitted from chatCore because
   the billing-header system block counts toward the 4-block cap and would strip
   legitimate client cache markers.

3. xxhash-wasm: installed (was declared in package.json by PR but not installed).

## Test updates

- tests/unit/claude-code-parity.test.mjs: 25 new tests covering CCH signing,
  fingerprint computation, tool remapping, and API constraints.
- tests/unit/cc-compatible-provider.test.mjs: fixed pre-existing assertion that
  expected no cache_control on system blocks (billing header now carries ephemeral
  per PR #1188 design).

Closes #1188
2026-04-12 22:41:12 -03:00

47 lines
1.4 KiB
TypeScript

/**
* Claude Code fingerprint computation.
*
* The billing header includes a 3-char fingerprint derived from:
* SHA256(SALT + msg[4] + msg[7] + msg[20] + version)[:3]
*
* This fingerprint is computed from the first user message text and
* included in cc_version=VERSION.FINGERPRINT in the billing header.
*/
import { createHash } from "node:crypto";
const FINGERPRINT_SALT = "59cf53e54c78";
export function computeFingerprint(firstUserMessageText: string, version: string): string {
const indices = [4, 7, 20];
const chars = indices.map((i) => firstUserMessageText[i] || "0").join("");
const input = `${FINGERPRINT_SALT}${chars}${version}`;
const hash = createHash("sha256").update(input).digest("hex");
return hash.slice(0, 3);
}
export function extractFirstUserMessageText(
messages: Array<{ role?: string; content?: unknown }> | undefined
): string {
if (!Array.isArray(messages)) return "";
for (const msg of messages) {
if (String(msg?.role).toLowerCase() !== "user") continue;
const content = msg?.content;
if (typeof content === "string") return content;
if (Array.isArray(content)) {
for (const block of content) {
if (
block &&
typeof block === "object" &&
"text" in block &&
typeof (block as Record<string, unknown>).text === "string"
) {
return (block as Record<string, unknown>).text as string;
}
}
}
return "";
}
return "";
}