diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 51adf9378e..330385f468 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -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)) { diff --git a/tests/unit/anthropic-cache-fingerprint.test.ts b/tests/unit/anthropic-cache-fingerprint.test.ts new file mode 100644 index 0000000000..d57be63a0d --- /dev/null +++ b/tests/unit/anthropic-cache-fingerprint.test.ts @@ -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"); + }); +});