diff --git a/src/constants/claudeCodeCompatibility.ts b/src/constants/claudeCodeCompatibility.ts new file mode 100644 index 00000000..486de747 --- /dev/null +++ b/src/constants/claudeCodeCompatibility.ts @@ -0,0 +1,18 @@ +/** + * Claude Code billing compatibility values based on sub2api. + * Reference: https://github.com/Wei-Shaw/sub2api + * License: LGPL-3.0-or-later, Copyright (c) 2026 Wesley Liddick. + */ +// Keep in sync with the Claude Code version accepted by upstream billing validation. +export const CLAUDE_CODE_COMPAT_VERSION = '2.1.92' +export const CLAUDE_CODE_BILLING_HEADER_PREFIX = 'x-anthropic-billing-header:' + +export function formatClaudeCodeBillingHeader(options: { + fingerprint: string + entrypoint?: string + workload?: string | null +}): string { + const entrypoint = options.entrypoint ?? 'unknown' + const workloadPair = options.workload ? ` cc_workload=${options.workload};` : '' + return `${CLAUDE_CODE_BILLING_HEADER_PREFIX} cc_version=${CLAUDE_CODE_COMPAT_VERSION}.${options.fingerprint}; cc_entrypoint=${entrypoint}; cch=00000;${workloadPair}` +} diff --git a/src/constants/system.test.ts b/src/constants/system.test.ts new file mode 100644 index 00000000..a9b0edbf --- /dev/null +++ b/src/constants/system.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from 'bun:test' +import { getAttributionHeader } from './system.js' + +describe('getAttributionHeader', () => { + test('uses Claude Code compatibility version and always includes CCH placeholder', () => { + const originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT + process.env.CLAUDE_CODE_ENTRYPOINT = 'cli' + + try { + expect(getAttributionHeader('abc')).toBe( + 'x-anthropic-billing-header: cc_version=2.1.92.abc; cc_entrypoint=cli; cch=00000;', + ) + } finally { + if (originalEntrypoint === undefined) delete process.env.CLAUDE_CODE_ENTRYPOINT + else process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint + } + }) +}) diff --git a/src/constants/system.ts b/src/constants/system.ts index 0cd2e765..d6d3efe1 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -1,11 +1,11 @@ // Critical system constants extracted to break circular dependencies -import { feature } from 'bun:bundle' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { logForDebugging } from '../utils/debug.js' import { isEnvDefinedFalsy } from '../utils/envUtils.js' import { getAPIProvider } from '../utils/model/providers.js' import { getWorkload } from '../utils/workloadContext.js' +import { formatClaudeCodeBillingHeader } from './claudeCodeCompatibility.js' const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.` const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.` @@ -61,34 +61,29 @@ function isAttributionHeaderEnabled(): boolean { * Returns a header string with cc_version (including fingerprint) and cc_entrypoint. * Enabled by default, can be disabled via env var or GrowthBook killswitch. * - * When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder. - * Before the request is sent, Bun's native HTTP stack finds this placeholder - * in the request body and overwrites the zeros with a computed hash. The - * server verifies this token to confirm the request came from a real Claude - * Code client. See bun-anthropic/src/http/Attestation.zig for implementation. - * - * We use a placeholder (instead of injecting from Zig) because same-length - * replacement avoids Content-Length changes and buffer reallocation. + * Includes a `cch=00000` placeholder. Before the request is sent, + * signClaudeCodeCCHBody overwrites the zeros with a computed hash. + * The server verifies this token to confirm the request came from a real + * Claude Code client. */ export function getAttributionHeader(fingerprint: string): string { if (!isAttributionHeaderEnabled()) { return '' } - const version = `${MACRO.VERSION}.${fingerprint}` const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown' - // cch=00000 placeholder is overwritten by Bun's HTTP stack with attestation token - const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : '' // cc_workload: turn-scoped hint so the API can route e.g. cron-initiated // requests to a lower QoS pool. Absent = interactive default. Safe re: - // fingerprint (computed from msg chars + version only, line 78 above) and - // cch attestation (placeholder overwritten in serialized body bytes after - // this string is built). Server _parse_cc_header tolerates unknown extra - // fields so old API deploys silently ignore this. - const workload = getWorkload() - const workloadPair = workload ? ` cc_workload=${workload};` : '' - const header = `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint};${cch}${workloadPair}` + // fingerprint (computed from msg chars + version only) and cch signature + // (placeholder overwritten in serialized body bytes after this string is + // built). Server _parse_cc_header tolerates unknown extra fields so old API + // deploys silently ignore this. + const header = formatClaudeCodeBillingHeader({ + fingerprint, + entrypoint, + workload: getWorkload(), + }) logForDebugging(`attribution header ${header}`) return header diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index 99cd1a6a..28d46b0a 100644 --- a/src/server/__tests__/providers.test.ts +++ b/src/server/__tests__/providers.test.ts @@ -8,6 +8,7 @@ import * as path from 'path' import * as os from 'os' import { ProviderService } from '../services/providerService.js' import { handleProvidersApi } from '../api/providers.js' +import { handleProxyRequest } from '../proxy/handler.js' import type { CreateProviderInput } from '../types/provider.js' // ─── Test helpers ───────────────────────────────────────────────────────────── @@ -782,6 +783,58 @@ describe('ProviderService', () => { }) }) + describe('handleProxyRequest', () => { + test('injects Claude Code billing attribution with compat version and signed CCH', async () => { + const originalFetch = globalThis.fetch + const originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT + delete process.env.CLAUDE_CODE_ENTRYPOINT + const calls: Array<{ body: Record }> = [] + globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => { + calls.push({ body: JSON.parse(String(init?.body)) as Record }) + return new Response(JSON.stringify({ + id: 'chatcmpl-1', + object: 'chat.completion', + created: 0, + model: 'gpt-4', + choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + }) as typeof fetch + + try { + const svc = new ProviderService() + const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' })) + await svc.activateProvider(provider.id) + + const req = new Request('http://localhost:3456/proxy/v1/messages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4', + max_tokens: 64, + messages: [{ role: 'user', content: 'hello from proxy' }], + }), + }) + + const res = await handleProxyRequest(req, new URL(req.url)) + expect(res.status).toBe(200) + + const system = calls[0].body.messages as Array> + expect(system[0].role).toBe('system') + expect(system[0].content).toMatch( + /^x-anthropic-billing-header: cc_version=2\.1\.92\.693; cc_entrypoint=unknown; cch=[0-9a-f]{5};$/, + ) + } finally { + globalThis.fetch = originalFetch + if (originalEntrypoint === undefined) delete process.env.CLAUDE_CODE_ENTRYPOINT + else process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint + } + }) + }) + describe('testProvider', () => { test('should use preset default auth for saved no-key Anthropic-compatible providers', async () => { const originalFetch = globalThis.fetch diff --git a/src/server/proxy/claudeCodeAttribution.ts b/src/server/proxy/claudeCodeAttribution.ts new file mode 100644 index 00000000..3151f6ec --- /dev/null +++ b/src/server/proxy/claudeCodeAttribution.ts @@ -0,0 +1,57 @@ +/** + * Proxy-side Claude Code billing attribution based on sub2api gateway behavior. + * Reference: https://github.com/Wei-Shaw/sub2api + * License: LGPL-3.0-or-later, Copyright (c) 2026 Wesley Liddick. + */ +import { + CLAUDE_CODE_BILLING_HEADER_PREFIX, + CLAUDE_CODE_COMPAT_VERSION, + formatClaudeCodeBillingHeader, +} from '../../constants/claudeCodeCompatibility.js' +import { computeFingerprint } from '../../utils/fingerprint.js' +import type { AnthropicRequest } from './transform/types.js' + +export function extractFirstUserText(body: AnthropicRequest): string { + for (const message of body.messages) { + if (message.role !== 'user') continue + + if (typeof message.content === 'string') { + return message.content + } + + const textBlock = message.content.find(block => block.type === 'text') + return textBlock?.type === 'text' ? textBlock.text : '' + } + + return '' +} + +export function ensureClaudeCodeAttribution(body: AnthropicRequest): AnthropicRequest { + if (hasBillingAttribution(body.system)) return body + + const fingerprint = computeFingerprint(extractFirstUserText(body), CLAUDE_CODE_COMPAT_VERSION) + const billingBlock = { + type: 'text' as const, + text: formatClaudeCodeBillingHeader({ + fingerprint, + entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown', + }), + } + + return { + ...body, + system: typeof body.system === 'string' + ? [billingBlock, { type: 'text', text: body.system }] + : [billingBlock, ...(body.system ?? [])], + } +} + +function hasBillingAttribution(system: AnthropicRequest['system']): boolean { + if (typeof system === 'string') { + return system.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX) + } + + return Array.isArray(system) && system.some(block => ( + block?.type === 'text' && typeof block.text === 'string' && block.text.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX) + )) +} diff --git a/src/server/proxy/handler.ts b/src/server/proxy/handler.ts index f3047af5..031d708f 100644 --- a/src/server/proxy/handler.ts +++ b/src/server/proxy/handler.ts @@ -9,7 +9,9 @@ * Original work by Jason Young, MIT License */ +import { signClaudeCodeCCHInTransformedString } from '../../utils/claudeCodeCch.js' import { ProviderService } from '../services/providerService.js' +import { ensureClaudeCodeAttribution } from './claudeCodeAttribution.js' import { anthropicToOpenaiChat } from './transform/anthropicToOpenaiChat.js' import { anthropicToOpenaiResponses } from './transform/anthropicToOpenaiResponses.js' import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js' @@ -79,6 +81,8 @@ export async function handleProxyRequest(req: Request, url: URL): Promise { + test('matches xxHash64 reference values for seed 0', () => { + expect(xxHash64Seeded(encoder.encode(''), 0n).toString(16)).toBe('ef46db3751d8e999') + expect(xxHash64Seeded(encoder.encode('a'), 0n).toString(16)).toBe('d24ec4f1a98c6e5b') + expect(xxHash64Seeded(encoder.encode('hello world'), 0n).toString(16)).toBe('45ab6734b21e6968') + }) +}) + +describe('signClaudeCodeCCHInString', () => { + test('replaces Anthropic system billing placeholder with deterministic 5 hex signature', () => { + const body = '{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.92.693; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"hello from proxy"}]}' + const signed = signClaudeCodeCCHInString(body) + + expect(signed).toMatch(/cch=[0-9a-f]{5};/) + expect(signed).not.toContain('cch=00000;') + }) + + test('does not touch cch placeholder outside structured billing block', () => { + const body = '{"messages":[{"role":"user","content":"please keep x-anthropic-billing-header: cc_version=2.1.92.abc; cc_entrypoint=cli; cch=00000; literal"}]}' + expect(signClaudeCodeCCHInString(body)).toBe(body) + }) + + test('does not leave partial signatures when multiple placeholders exist', () => { + const body = '{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.92.abc; cc_entrypoint=cli; cch=00000;"},{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.92.def; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"hi"}]}' + expect(signClaudeCodeCCHInString(body)).toBe(body) + }) + + test('does not partially sign when user text also contains a placeholder', () => { + const body = '{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.92.abc; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"literal x-anthropic-billing-header: cc_version=2.1.92.user; cc_entrypoint=cli; cch=00000;"}]}' + expect(signClaudeCodeCCHInString(body)).toBe(body) + }) + + test('does not partially sign transformed body when user text also contains a placeholder', () => { + const body = '{"messages":[{"role":"system","content":"x-anthropic-billing-header: cc_version=2.1.92.abc; cc_entrypoint=cli; cch=00000;"},{"role":"user","content":"literal x-anthropic-billing-header: cc_version=2.1.92.user; cc_entrypoint=cli; cch=00000;"}]}' + expect(signClaudeCodeCCHInTransformedString(body)).toBe(body) + }) +}) diff --git a/src/utils/claudeCodeCch.ts b/src/utils/claudeCodeCch.ts new file mode 100644 index 00000000..fbab87f6 --- /dev/null +++ b/src/utils/claudeCodeCch.ts @@ -0,0 +1,226 @@ +/** + * Claude Code CCH signing logic based on sub2api. + * Reference: https://github.com/Wei-Shaw/sub2api + * License: LGPL-3.0-or-later, Copyright (c) 2026 Wesley Liddick. + */ +import { CLAUDE_CODE_BILLING_HEADER_PREFIX } from '../constants/claudeCodeCompatibility.js' + +const CCH_PLACEHOLDER = 'cch=00000;' +const CCH_PLACEHOLDER_RE = /\bcch=00000;/g +const CCH_SEED = 0x6E52736AC806831En +const MASK_64 = 0xffffffffffffffffn +const PRIME64_1 = 11400714785074694791n +const PRIME64_2 = 14029467366897019727n +const PRIME64_3 = 1609587929392839161n +const PRIME64_4 = 9650029242287828579n +const PRIME64_5 = 2870177450012600261n + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +type BillingFieldSelector = (parsed: unknown) => string[] + +export function signClaudeCodeCCHInString(body: string): string { + return signClaudeCodeCCHInStringWithSelector(body, selectAnthropicBillingFields) +} + +export function signClaudeCodeCCHInTransformedString(body: string): string { + return signClaudeCodeCCHInStringWithSelector(body, selectTransformedBillingFields) +} + +function signClaudeCodeCCHInStringWithSelector( + body: string, + selectBillingFields: BillingFieldSelector, +): string { + if (!body.includes(CLAUDE_CODE_BILLING_HEADER_PREFIX) || !body.includes(CCH_PLACEHOLDER)) return body + + let parsed: unknown + try { + parsed = JSON.parse(body) as unknown + } catch { + return body + } + + if (countCCHPlaceholders(body) !== 1) return body + + const billingFields = selectBillingFields(parsed) + const placeholderCount = billingFields.reduce((count, value) => count + countCCHPlaceholders(value), 0) + if (placeholderCount !== 1) return body + + const billingField = billingFields.find(value => countCCHPlaceholders(value) === 1) + if (!billingField) return body + + const fieldLiteral = JSON.stringify(billingField) + if (countLiteralOccurrences(body, fieldLiteral) !== 1) return body + + const cch = computeClaudeCodeCCH(body) + const signedField = billingField.replace(CCH_PLACEHOLDER_RE, `cch=${cch};`) + return body.replace(fieldLiteral, JSON.stringify(signedField)) +} + +export function signClaudeCodeCCHBody(body: BodyInit | null | undefined): BodyInit | null | undefined { + if (typeof body === 'string') { + return signClaudeCodeCCHInString(body) + } + + if (body instanceof Uint8Array) { + const raw = decoder.decode(body) + const signed = signClaudeCodeCCHInString(raw) + return signed === raw ? body : encoder.encode(signed) + } + + return body +} + +function selectAnthropicBillingFields(parsed: unknown): string[] { + if (!isRecord(parsed)) return [] + + const { system } = parsed + if (typeof system === 'string') { + return isBillingHeader(system) ? [system] : [] + } + if (!Array.isArray(system)) return [] + + return system.flatMap(block => ( + isTextBlock(block) && isBillingHeader(block.text) ? [block.text] : [] + )) +} + +function selectTransformedBillingFields(parsed: unknown): string[] { + if (!isRecord(parsed)) return [] + + const fields: string[] = [] + if (typeof parsed.instructions === 'string' && isBillingHeader(parsed.instructions)) { + fields.push(parsed.instructions) + } + + if (Array.isArray(parsed.messages)) { + for (const message of parsed.messages) { + if (!isRecord(message)) continue + if (message.role === 'system' && typeof message.content === 'string' && isBillingHeader(message.content)) { + fields.push(message.content) + } + } + } + + return fields +} + +function isBillingHeader(value: string): boolean { + return value.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX) +} + +function isTextBlock(value: unknown): value is { type: 'text'; text: string } { + return isRecord(value) && value.type === 'text' && typeof value.text === 'string' +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function countCCHPlaceholders(value: string): number { + return value.match(CCH_PLACEHOLDER_RE)?.length ?? 0 +} + +function countLiteralOccurrences(body: string, literal: string): number { + let count = 0 + let index = body.indexOf(literal) + while (index !== -1) { + count += 1 + index = body.indexOf(literal, index + literal.length) + } + return count +} + +function computeClaudeCodeCCH(body: string): string { + return (xxHash64Seeded(encoder.encode(body), CCH_SEED) & 0xfffffn) + .toString(16) + .padStart(5, '0') +} + +export function xxHash64Seeded(data: Uint8Array, seed: bigint): bigint { + let offset = 0 + let h64: bigint + const view = new DataView(data.buffer, data.byteOffset, data.byteLength) + + if (data.length >= 32) { + const limit = data.length - 32 + let v1 = (seed + PRIME64_1 + PRIME64_2) & MASK_64 + let v2 = (seed + PRIME64_2) & MASK_64 + let v3 = seed & MASK_64 + let v4 = (seed - PRIME64_1) & MASK_64 + + while (offset <= limit) { + v1 = xxh64Round(v1, readU64(view, offset)) + offset += 8 + v2 = xxh64Round(v2, readU64(view, offset)) + offset += 8 + v3 = xxh64Round(v3, readU64(view, offset)) + offset += 8 + v4 = xxh64Round(v4, readU64(view, offset)) + offset += 8 + } + + h64 = ( + rotl64(v1, 1n) + + rotl64(v2, 7n) + + rotl64(v3, 12n) + + rotl64(v4, 18n) + ) & MASK_64 + h64 = xxh64MergeRound(h64, v1) + h64 = xxh64MergeRound(h64, v2) + h64 = xxh64MergeRound(h64, v3) + h64 = xxh64MergeRound(h64, v4) + } else { + h64 = (seed + PRIME64_5) & MASK_64 + } + + h64 = (h64 + BigInt(data.length)) & MASK_64 + + while (offset + 8 <= data.length) { + const k1 = xxh64Round(0n, readU64(view, offset)) + h64 ^= k1 + h64 = (rotl64(h64, 27n) * PRIME64_1 + PRIME64_4) & MASK_64 + offset += 8 + } + + if (offset + 4 <= data.length) { + h64 ^= (BigInt(readU32(view, offset)) * PRIME64_1) & MASK_64 + h64 = (rotl64(h64, 23n) * PRIME64_2 + PRIME64_3) & MASK_64 + offset += 4 + } + + while (offset < data.length) { + h64 ^= (BigInt(data[offset]!) * PRIME64_5) & MASK_64 + h64 = (rotl64(h64, 11n) * PRIME64_1) & MASK_64 + offset += 1 + } + + h64 ^= h64 >> 33n + h64 = (h64 * PRIME64_2) & MASK_64 + h64 ^= h64 >> 29n + h64 = (h64 * PRIME64_3) & MASK_64 + h64 ^= h64 >> 32n + return h64 & MASK_64 +} + +function xxh64Round(acc: bigint, input: bigint): bigint { + return (rotl64((acc + input * PRIME64_2) & MASK_64, 31n) * PRIME64_1) & MASK_64 +} + +function xxh64MergeRound(acc: bigint, value: bigint): bigint { + acc ^= xxh64Round(0n, value) + return (acc * PRIME64_1 + PRIME64_4) & MASK_64 +} + +function rotl64(value: bigint, bits: bigint): bigint { + return ((value << bits) | (value >> (64n - bits))) & MASK_64 +} + +function readU64(view: DataView, offset: number): bigint { + return view.getBigUint64(offset, true) +} + +function readU32(view: DataView, offset: number): number { + return view.getUint32(offset, true) +} diff --git a/src/utils/fingerprint.ts b/src/utils/fingerprint.ts index 17906ebd..02c6567e 100644 --- a/src/utils/fingerprint.ts +++ b/src/utils/fingerprint.ts @@ -1,4 +1,10 @@ +/** + * Claude Code billing fingerprint algorithm based on sub2api. + * Reference: https://github.com/Wei-Shaw/sub2api + * License: LGPL-3.0-or-later, Copyright (c) 2026 Wesley Liddick. + */ import { createHash } from 'crypto' +import { CLAUDE_CODE_COMPAT_VERSION } from '../constants/claudeCodeCompatibility.js' import type { AssistantMessage, UserMessage } from '../types/message.js' /** @@ -72,5 +78,5 @@ export function computeFingerprintFromMessages( messages: (UserMessage | AssistantMessage)[], ): string { const firstMessageText = extractFirstMessageText(messages) - return computeFingerprint(firstMessageText, MACRO.VERSION) + return computeFingerprint(firstMessageText, CLAUDE_CODE_COMPAT_VERSION) } diff --git a/src/utils/http.ts b/src/utils/http.ts index 93b87e8e..a4d6052f 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -10,6 +10,7 @@ import { handleOAuth401Error, isClaudeAISubscriber, } from './auth.js' +import { CLAUDE_CODE_COMPAT_VERSION } from '../constants/claudeCodeCompatibility.js' import { getClaudeCodeUserAgent } from './userAgent.js' import { getWorkload } from './workloadContext.js' @@ -31,7 +32,7 @@ export function getUserAgent(): string { // so the read picks up the same setWorkload() value as getAttributionHeader. const workload = getWorkload() const workloadSuffix = workload ? `, workload/${workload}` : '' - return `claude-cli/${MACRO.VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})` + return `claude-cli/${CLAUDE_CODE_COMPAT_VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})` } export function getMCPUserAgent(): string { diff --git a/src/utils/sideQuery.ts b/src/utils/sideQuery.ts index 4c560286..76b7983e 100644 --- a/src/utils/sideQuery.ts +++ b/src/utils/sideQuery.ts @@ -5,6 +5,7 @@ import { setLastApiCompletionTimestamp, } from '../bootstrap/state.js' import { STRUCTURED_OUTPUTS_BETA_HEADER } from '../constants/betas.js' +import { CLAUDE_CODE_COMPAT_VERSION } from '../constants/claudeCodeCompatibility.js' import type { QuerySource } from '../constants/querySource.js' import { getAttributionHeader, @@ -142,7 +143,7 @@ export async function sideQuery(opts: SideQueryOptions): Promise { const messageText = extractFirstUserMessageText(messages) // Compute fingerprint for OAuth attribution - const fingerprint = computeFingerprint(messageText, MACRO.VERSION) + const fingerprint = computeFingerprint(messageText, CLAUDE_CODE_COMPAT_VERSION) const attributionHeader = getAttributionHeader(fingerprint) // Build system as array to keep attribution header in its own block