Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"lint": "oxlint && oxfmt --check .",
"lint:fix": "oxlint --fix . && oxfmt --write .",
"format": "oxfmt --write .",
"prepublishOnly": "pnpm run build && pnpm test"
"prepublishOnly": "pnpm run build && pnpm test",
"test:prompt": "pnpm run build && node --experimental-strip-types scripts/test-prompt.ts"
},
"devDependencies": {
"@opencode-ai/plugin": "latest",
Expand Down
193 changes: 193 additions & 0 deletions scripts/extract-cch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* Extract the cch value from a real Claude CLI request and compare
* with our xxHash64 implementation to verify seed correctness.
*
* Usage: pnpm run build && node --experimental-strip-types scripts/extract-cch.ts
*/
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http"
import { request as httpsRequest } from "node:https"
import { spawn } from "node:child_process"
import { writeFileSync } from "node:fs"
import { computeCchHash } from "../dist/xxhash64.js"

const PORT = 18899
const TIMEOUT_MS = 30_000

console.log("Starting intercept proxy on port", PORT)
console.log("Will capture one request from Claude CLI and compare cch...\n")

const server = createServer((req: IncomingMessage, res: ServerResponse) => {
// Handle health checks — respond 200 and wait for the real request
if (req.method === "HEAD" || !req.url?.includes("/v1/messages")) {
console.log(`>>> ${req.method} ${req.url} (skipping, not /v1/messages)`)
res.writeHead(200)
res.end()
return
}

const chunks: Buffer[] = []
req.on("data", (chunk: Buffer) => chunks.push(chunk))
req.on("end", () => {
console.log(`\n>>> ${req.method} ${req.url}`)
const bodyStr = Buffer.concat(chunks).toString()

// Extract the cch from the billing header in the body
const cchMatch = bodyStr.match(/cch=([0-9a-f]{5})/)
const realCch = cchMatch ? cchMatch[1] : null

console.log("=== CAPTURED REQUEST ===")
console.log("Body length:", bodyStr.length)
console.log("Real cch from Claude CLI:", realCch)

// Now compute what our implementation produces
// Replace the real cch with 00000 placeholder to simulate what Bun hashes
const bodyWithPlaceholder = bodyStr.replace(/cch=[0-9a-f]{5}/, "cch=00000")
const encoder = new TextEncoder()
const fullBodyHash = computeCchHash(encoder.encode(bodyWithPlaceholder))
console.log("Our xxHash64 (full body):", fullBodyHash)
console.log(
"Match (full body):",
fullBodyHash === realCch ? "YES ✓" : "NO ✗",
)

// Also try hashing with system stripped to billing-only
try {
const parsed = JSON.parse(bodyWithPlaceholder) as {
system?: Array<{ type?: string; text?: string }>
}
if (Array.isArray(parsed.system)) {
const billingOnly = parsed.system.filter(
(e) =>
typeof e.text === "string" &&
e.text.startsWith("x-anthropic-billing-header"),
)
const strippedBody = { ...parsed, system: billingOnly }
const strippedHash = computeCchHash(
encoder.encode(JSON.stringify(strippedBody)),
)
console.log("Our xxHash64 (billing-only system):", strippedHash)
console.log(
"Match (billing-only):",
strippedHash === realCch ? "YES ✓" : "NO ✗",
)

// Also try with no system at all
const noSystem = { ...parsed }
delete (noSystem as Record<string, unknown>).system
const noSystemHash = computeCchHash(
encoder.encode(JSON.stringify(noSystem)),
)
console.log("Our xxHash64 (no system):", noSystemHash)
console.log(
"Match (no system):",
noSystemHash === realCch ? "YES ✓" : "NO ✗",
)
}
} catch {
console.log("Failed to parse body for stripped test")
}

// Save body with placeholder for seed brute-forcing
writeFileSync(
"/tmp/claude-body-placeholder.json",
bodyWithPlaceholder,
"utf-8",
)
writeFileSync("/tmp/claude-cch-real.txt", realCch ?? "", "utf-8")
console.log("\nSaved body to /tmp/claude-body-placeholder.json")
console.log("Saved real cch to /tmp/claude-cch-real.txt")

// Extract version info
const versionMatch = bodyStr.match(/cc_version=([^;]+)/)
const entrypointMatch = bodyStr.match(/cc_entrypoint=([^;]+)/)
console.log("\nBilling header details:")
console.log(" cc_version:", versionMatch?.[1])
console.log(" cc_entrypoint:", entrypointMatch?.[1])

// Extract system block count
try {
const p = JSON.parse(bodyStr) as {
system?: unknown[]
}
console.log(" system block count:", p.system?.length)
if (Array.isArray(p.system)) {
for (let i = 0; i < p.system.length; i++) {
const entry = p.system[i] as {
text?: string
cache_control?: unknown
}
const text =
typeof entry.text === "string" ? entry.text.slice(0, 80) : "?"
const cc = entry.cache_control
? JSON.stringify(entry.cache_control)
: "none"
console.log(` system[${i}]: cache_control=${cc} text="${text}..."`)
}
}
} catch {}

// Forward to real API so claude doesn't error
const proxyOpts = {
hostname: "api.anthropic.com",
path: req.url,
method: req.method,
headers: { ...req.headers, host: "api.anthropic.com" },
}
const proxy = httpsRequest(proxyOpts, (proxyRes) => {
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers)
proxyRes.pipe(res)
proxyRes.on("end", () => {
server.close()
process.exit(0)
})
})
proxy.on("error", () => {
res.writeHead(502)
res.end()
server.close()
process.exit(1)
})
proxy.write(bodyStr)
proxy.end()
})
})

const timer = setTimeout(() => {
console.log("Timeout - no request captured")
server.close()
process.exit(1)
}, TIMEOUT_MS)

server.listen(PORT, () => {
const child = spawn(
"claude",
["-p", "say hi", "--model", "claude-haiku-4-5"],
{
env: {
...process.env,
ANTHROPIC_API_KEY: "",
ANTHROPIC_BASE_URL: `http://localhost:${PORT}`,
TERM: "dumb",
},
stdio: "ignore",
},
)

child.on("error", (err) => {
console.log("Claude CLI error:", err.message)
clearTimeout(timer)
server.close()
process.exit(1)
})

child.on("close", () => {
setTimeout(() => {
clearTimeout(timer)
server.close()
}, 3000)
})
})
109 changes: 109 additions & 0 deletions scripts/test-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
getCachedCredentials,
initAccounts,
setActiveAccountSource,
} from "../dist/credentials.js"
import { buildRequestHeaders, fetchWithRetry } from "../dist/index.js"
import { readAllClaudeAccounts } from "../dist/keychain.js"
import { transformBody, transformResponseStream } from "../dist/transforms.js"
import { buildBillingHeaderValue } from "../dist/signing.js"
import { config } from "../dist/model-config.js"

const API_URL = "https://api.anthropic.com/v1/messages"
const SYSTEM_IDENTITY =
"You are Claude Code, Anthropic's official CLI for Claude."

const prompt = process.argv[2]
if (!prompt) {
console.error("Usage: pnpm run test:prompt 'your prompt here' [model]")
process.exit(1)
}

const modelId = process.argv[3] ?? "claude-sonnet-4-6"

// Init credentials
const accounts = readAllClaudeAccounts()
if (accounts.length === 0) {
console.error(
"No Claude Code credentials found. Run `claude` to authenticate.",
)
process.exit(1)
}
initAccounts(accounts)
setActiveAccountSource(accounts[0].source)

const creds = getCachedCredentials()
if (!creds) {
console.error("Credentials expired. Run `claude` to refresh.")
process.exit(1)
}

// Build messages
const messages = [{ role: "user", content: prompt }]

const version = process.env.ANTHROPIC_CLI_VERSION ?? config.ccVersion
const billingHeader = buildBillingHeaderValue(messages, version, "cli")

const requestBody = {
model: modelId,
max_tokens: 4096,
stream: true,
system: [
{ type: "text", text: billingHeader },
{ type: "text", text: SYSTEM_IDENTITY },
],
messages,
}

const body = transformBody(JSON.stringify(requestBody))

const init: RequestInit = { method: "POST", body }
const headers = buildRequestHeaders(
new URL(API_URL),
init,
creds.accessToken,
modelId,
)
headers.set("content-type", "application/json")

// Send request and stream response
const response = await fetchWithRetry(API_URL, { ...init, body, headers })

if (!response.ok) {
const errorBody = await response.text()
console.error(`API error ${response.status}:`, errorBody)
process.exit(1)
}

const transformed = transformResponseStream(response)
const reader = transformed.body!.getReader()
const decoder = new TextDecoder()

let fullText = ""

while (true) {
const { done, value } = await reader.read()
if (done) break

const chunk = decoder.decode(value, { stream: true })
// Parse SSE events
for (const line of chunk.split("\n")) {
if (!line.startsWith("data: ")) continue
const data = line.slice(6)
if (data === "[DONE]") continue
try {
const event = JSON.parse(data) as {
type?: string
delta?: { type?: string; text?: string }
}
if (event.type === "content_block_delta" && event.delta?.text) {
process.stdout.write(event.delta.text)
fullText += event.delta.text
}
} catch {
// skip non-JSON lines
}
}
}

if (fullText) console.log()
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const SOURCE_FILES = [
"transforms.ts",
"credentials.ts",
"logger.ts",
"xxhash64.ts",
] as const

async function copySourceFiles(tempDir: string): Promise<void> {
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export {
export { isEnable1mContext, type PluginSettings } from "./plugin-config.ts"
export {
buildBillingHeaderValue,
computeCch,
computeVersionSuffix,
extractFirstUserMessageText,
} from "./signing.ts"
Expand Down
2 changes: 1 addition & 1 deletion src/model-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ModelConfig {
}

export const config: ModelConfig = {
ccVersion: "2.1.90",
ccVersion: "2.1.97",
baseBetas: [
"claude-code-20250219",
"oauth-2025-04-20",
Expand Down
23 changes: 4 additions & 19 deletions src/signing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import assert from "node:assert/strict"
import { describe, it } from "node:test"
import {
extractFirstUserMessageText,
computeCch,
computeVersionSuffix,
buildBillingHeaderValue,
} from "./signing.ts"
Expand Down Expand Up @@ -65,20 +64,6 @@ describe("signing", () => {
})
})

describe("computeCch", () => {
it("matches test vector: 'hey' → fa690", () => {
assert.equal(computeCch("hey"), "fa690")
})

it("matches test vector: empty string → e3b0c", () => {
assert.equal(computeCch(""), "e3b0c")
})

it("matches test vector: long message", () => {
assert.equal(computeCch("Hello, how are you doing today?"), "852db")
})
})

describe("computeVersionSuffix", () => {
it("matches test vector: 'hey' + v2.1.37 → 0d9", () => {
assert.equal(computeVersionSuffix("hey", "2.1.37"), "0d9")
Expand Down Expand Up @@ -121,7 +106,7 @@ describe("signing", () => {
)
assert.equal(
result,
"x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=fa690;",
"x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=00000;",
)
})

Expand All @@ -141,13 +126,13 @@ describe("signing", () => {
)
assert.equal(
result,
"x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=fa690;",
"x-anthropic-billing-header: cc_version=2.1.90.b39; cc_entrypoint=cli; cch=00000;",
)
})

it("handles missing user message (hashes empty string)", () => {
it("handles missing user message (uses placeholder cch)", () => {
const result = buildBillingHeaderValue([], "2.1.90", "cli")
assert.ok(result.includes("cch=e3b0c"))
assert.ok(result.includes("cch=00000"))
})

it("uses provided entrypoint", () => {
Expand Down
Loading
Loading