mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(claude): stabilize billing header fingerprint for prompt-cache affinity (#1638)
The billing header fingerprint was computed from the first user message text via computeFingerprint(), which changes every conversation turn. This mutated the system[] prefix on each request, invalidating Anthropic's prompt-cache prefix and forcing ~100% cache_create (vs 96% cache_read with stable prefix). Now uses a per-day SHA-256 hash of the date + ccVersion, keeping the billing header format while preserving prompt-cache prefix stability across turns. Includes 6 unit tests.
This commit is contained in:
@@ -13,10 +13,6 @@ import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestD
|
||||
import { supportsXHighEffort } from "../config/providerModels.ts";
|
||||
import { remapToolNamesInRequest } from "../services/claudeCodeToolRemapper.ts";
|
||||
import { obfuscateInBody } from "../services/claudeCodeObfuscation.ts";
|
||||
import {
|
||||
computeFingerprint,
|
||||
extractFirstUserMessageText,
|
||||
} from "../services/claudeCodeFingerprint.ts";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
@@ -466,9 +462,14 @@ export class BaseExecutor {
|
||||
obfuscateInBody(tb);
|
||||
|
||||
const ccVersion = "2.1.114";
|
||||
const messages = tb.messages as Array<{ role?: string; content?: unknown }> | undefined;
|
||||
const msgText = extractFirstUserMessageText(messages);
|
||||
const fp = computeFingerprint(msgText, ccVersion);
|
||||
// Fix #1638: Use a stable fingerprint instead of message-derived one.
|
||||
// The original computeFingerprint() hashed first-user-message chars, which
|
||||
// changes every conversation turn. This mutated the system[] prefix on each
|
||||
// request, invalidating Anthropic's prompt-cache prefix and forcing ~100%
|
||||
// cache_create (vs 96% cache_read with a stable prefix). Using a per-day
|
||||
// hash keeps the billing header format while preserving cache affinity.
|
||||
const dayStamp = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
const fp = createHash("sha256").update(`${dayStamp}${ccVersion}`).digest("hex").slice(0, 3);
|
||||
const billingLine = `x-anthropic-billing-header: cc_version=${ccVersion}.${fp}; cc_entrypoint=cli; cch=00000;`;
|
||||
|
||||
if (Array.isArray(tb.system)) {
|
||||
|
||||
74
tests/unit/anthropic-cache-fingerprint.test.ts
Normal file
74
tests/unit/anthropic-cache-fingerprint.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Tests for Anthropic billing header fingerprint stability (#1638).
|
||||
*
|
||||
* Validates that the billing header fingerprint is stable across different
|
||||
* messages within the same day, preventing prompt-cache prefix invalidation.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// Replicate the stabilized fingerprint logic from base.ts
|
||||
function computeStableFingerprint(ccVersion: string): string {
|
||||
const dayStamp = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
return createHash("sha256").update(`${dayStamp}${ccVersion}`).digest("hex").slice(0, 3);
|
||||
}
|
||||
|
||||
// The old implementation for comparison
|
||||
function computeOldFingerprint(firstUserMessageText: string, version: string): string {
|
||||
const FINGERPRINT_SALT = "59cf53e54c78";
|
||||
const indices = [4, 7, 20];
|
||||
const chars = indices.map((i) => firstUserMessageText[i] || "0").join("");
|
||||
const input = `${FINGERPRINT_SALT}${chars}${version}`;
|
||||
return createHash("sha256").update(input).digest("hex").slice(0, 3);
|
||||
}
|
||||
|
||||
describe("Anthropic billing header fingerprint (#1638)", () => {
|
||||
const ccVersion = "2.1.114";
|
||||
|
||||
it("should produce the same fingerprint for different messages (stable)", () => {
|
||||
const fp1 = computeStableFingerprint(ccVersion);
|
||||
const fp2 = computeStableFingerprint(ccVersion);
|
||||
assert.equal(fp1, fp2, "Same-day fingerprints should be identical");
|
||||
});
|
||||
|
||||
it("should produce a 3-character hex fingerprint", () => {
|
||||
const fp = computeStableFingerprint(ccVersion);
|
||||
assert.equal(fp.length, 3, "Fingerprint should be 3 chars");
|
||||
assert.ok(/^[a-f0-9]{3}$/.test(fp), `Fingerprint '${fp}' should be lowercase hex`);
|
||||
});
|
||||
|
||||
it("old implementation produces DIFFERENT fingerprints for different messages", () => {
|
||||
const msg1 = "Hello, how can I help you with your code?";
|
||||
const msg2 = "Please fix the bug in my application";
|
||||
const fp1 = computeOldFingerprint(msg1, ccVersion);
|
||||
const fp2 = computeOldFingerprint(msg2, ccVersion);
|
||||
assert.notEqual(fp1, fp2, "Old method should differ per message — this was the bug");
|
||||
});
|
||||
|
||||
it("new implementation produces SAME fingerprint regardless of message content", () => {
|
||||
// The new implementation doesn't use message content at all
|
||||
const fp1 = computeStableFingerprint(ccVersion);
|
||||
const fp2 = computeStableFingerprint(ccVersion);
|
||||
const fp3 = computeStableFingerprint(ccVersion);
|
||||
assert.equal(fp1, fp2);
|
||||
assert.equal(fp2, fp3);
|
||||
});
|
||||
|
||||
it("billing header line should be deterministic within the same day", () => {
|
||||
const fp = computeStableFingerprint(ccVersion);
|
||||
const billingLine1 = `x-anthropic-billing-header: cc_version=${ccVersion}.${fp}; cc_entrypoint=cli; cch=00000;`;
|
||||
const billingLine2 = `x-anthropic-billing-header: cc_version=${ccVersion}.${fp}; cc_entrypoint=cli; cch=00000;`;
|
||||
assert.equal(billingLine1, billingLine2, "Billing lines should be byte-identical");
|
||||
});
|
||||
|
||||
it("should produce different fingerprints for different days", () => {
|
||||
// Simulate different days by computing manually
|
||||
const day1 = "2026-04-27";
|
||||
const day2 = "2026-04-28";
|
||||
const fp1 = createHash("sha256").update(`${day1}${ccVersion}`).digest("hex").slice(0, 3);
|
||||
const fp2 = createHash("sha256").update(`${day2}${ccVersion}`).digest("hex").slice(0, 3);
|
||||
// They should be different (extremely high probability with SHA-256)
|
||||
assert.notEqual(fp1, fp2, "Different days should produce different fingerprints");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user