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:
diegosouzapw
2026-04-27 00:37:25 -03:00
parent 52d5b86e88
commit 19edb8efa4
2 changed files with 82 additions and 7 deletions

View File

@@ -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)) {

View 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");
});
});