mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: add Claude Code billing attribution compatibility
This commit is contained in:
parent
abd782494e
commit
df60c77472
18
src/constants/claudeCodeCompatibility.ts
Normal file
18
src/constants/claudeCodeCompatibility.ts
Normal file
@ -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}`
|
||||
}
|
||||
18
src/constants/system.test.ts
Normal file
18
src/constants/system.test.ts
Normal file
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
|
||||
@ -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<string, unknown> }> = []
|
||||
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
|
||||
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<Record<string, string>>
|
||||
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
|
||||
|
||||
57
src/server/proxy/claudeCodeAttribution.ts
Normal file
57
src/server/proxy/claudeCodeAttribution.ts
Normal file
@ -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)
|
||||
))
|
||||
}
|
||||
@ -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<Respon
|
||||
)
|
||||
}
|
||||
|
||||
body = ensureClaudeCodeAttribution(body)
|
||||
|
||||
const isStream = body.stream === true
|
||||
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
||||
|
||||
@ -122,7 +126,7 @@ async function handleOpenaiChat(
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(transformed),
|
||||
body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)),
|
||||
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
||||
})
|
||||
|
||||
@ -186,7 +190,7 @@ async function handleOpenaiResponses(
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(transformed),
|
||||
body: signClaudeCodeCCHInTransformedString(JSON.stringify(transformed)),
|
||||
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
||||
})
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
refreshAndGetAwsCredentials,
|
||||
refreshGcpCredentialsIfNeeded,
|
||||
} from 'src/utils/auth.js'
|
||||
import { signClaudeCodeCCHBody } from 'src/utils/claudeCodeCch.js'
|
||||
import { getUserAgent } from 'src/utils/http.js'
|
||||
import { getSmallFastModel } from 'src/utils/model/model.js'
|
||||
import {
|
||||
@ -422,6 +423,6 @@ function buildFetch(
|
||||
} catch {
|
||||
// never let logging crash the fetch
|
||||
}
|
||||
return inner(input, { ...init, headers })
|
||||
return inner(input, { ...init, headers, body: signClaudeCodeCCHBody(init?.body) })
|
||||
}
|
||||
}
|
||||
|
||||
42
src/utils/claudeCodeCch.test.ts
Normal file
42
src/utils/claudeCodeCch.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { signClaudeCodeCCHInString, signClaudeCodeCCHInTransformedString, xxHash64Seeded } from './claudeCodeCch.js'
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
describe('xxHash64Seeded', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
226
src/utils/claudeCodeCch.ts
Normal file
226
src/utils/claudeCodeCch.ts
Normal file
@ -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<string, unknown> {
|
||||
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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<BetaMessage> {
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user