Skip to content
Merged
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
68 changes: 68 additions & 0 deletions apps/gateway/src/routes/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2698,6 +2698,74 @@ describe("createExecute — native protocol passthrough (#217)", () => {
expect(okRow?.provider_model).toBe("claude-x");
});

it("stabilizes Claude Code billing cch on Anthropic native passthrough so prompt cache survives turns", async () => {
const provider = anthropicProvider(NATIVE_RESP);
const execute = createExecute({
defaultProvider: provider,
providers: new Map([["anthro", provider]]),
registry: protocolRegistry({
a: {
providerName: "anthro",
providerModel: "claude-x",
targetProviderProtocol: "anthropic_messages",
},
}),
breaker: breaker(),
catalog: new Map(),
now: clock(),
signal: new AbortController().signal,
nativeProtocolPassthroughEnabled: () => true,
});
const carrier = (cch: string, text: string): NativePassthroughCarrier => ({
protocol: "anthropic_messages",
body: {
model: "claude-x",
max_tokens: 16,
system: [
{
type: "text",
text: `x-anthropic-billing-header: cc_version=2.1.175.baa; cc_entrypoint=cli; cch=${cch};`,
},
{ type: "text", text: "You are Claude Code.", cache_control: { type: "ephemeral" } },
{ type: "text", text: "stable project rules", cache_control: { type: "ephemeral" } },
],
tools: [{ name: "Read", input_schema: { type: "object" } }],
messages: [{ role: "user", content: [{ type: "text", text }] }],
},
raw_body: `raw-${cch}`,
headers: { "x-api-key": "client" },
mutations: {},
});

const first = await execute(
plan(["a"]),
anthropicReq({ native_request: carrier("aaaaa", "first turn") }),
);
const second = await execute(
plan(["a"]),
anthropicReq({ native_request: carrier("bbbbb", "a much longer follow-up turn") }),
);

const firstForwarded = provider.nativePassthrough.mock
.calls[0]?.[0] as NativePassthroughCarrier;
const secondForwarded = provider.nativePassthrough.mock
.calls[1]?.[0] as NativePassthroughCarrier;
const cchOf = (forwarded: NativePassthroughCarrier) =>
String(
((forwarded.body.system as Array<Record<string, unknown>>)[0] as { text?: unknown }).text,
).match(/\bcch=([0-9a-f]{5});/)?.[1];
expect(cchOf(firstForwarded)).toMatch(/^[0-9a-f]{5}$/);
expect(cchOf(secondForwarded)).toBe(cchOf(firstForwarded));
expect(cchOf(firstForwarded)).not.toBe("aaaaa");
expect(cchOf(secondForwarded)).not.toBe("bbbbb");
expect(firstForwarded.raw_body).toBeUndefined();
expect(firstForwarded.mutations.body_shims_applied).toEqual([
"anthropic_billing_cch_stabilized",
]);
expect(first.attempts[0]?.passthrough_mutations).toMatchObject(firstForwarded.mutations);
expect(second.attempts[0]?.passthrough_mutations).toMatchObject(secondForwarded.mutations);
});

it("native passthrough strips Anthropic effort when the target model does not support it", async () => {
const provider = anthropicProvider(NATIVE_RESP);
const haikuEntry: CatalogEntry = {
Expand Down
66 changes: 66 additions & 0 deletions apps/gateway/src/routes/execute.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import type {
CircuitBreaker,
ExecuteOutcome,
Expand Down Expand Up @@ -50,6 +51,10 @@ import {
usageFromResponsesResponse,
} from "./payload-capture.js";

const ANTHROPIC_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
const ANTHROPIC_BILLING_CCH_RE = /\bcch=([0-9a-f]{5});/i;
const ANTHROPIC_BILLING_CCH_PLACEHOLDER = "cch=00000;";

// Gateway execution adapter — the `execute` injected into routeRequest. It walks
// the resolved candidate chain (ExecutionPlan.candidate_chain) honoring the
// EXECUTION-fallback rules (CLAUDE.md principle 5, docs/04):
Expand Down Expand Up @@ -535,6 +540,59 @@ function sanitizeAnthropicNativeBody(body: Record<string, unknown>): {
: { body, strippedEmptyTextBlocks: 0 };
}

function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}

function stableJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
if (isRecord(value)) {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}

function stabilizeAnthropicBillingCch(body: Record<string, unknown>): {
body: Record<string, unknown>;
stabilized: boolean;
} {
const system = body.system;
if (!Array.isArray(system)) return { body, stabilized: false };
const first = system[0];
if (!isRecord(first)) return { body, stabilized: false };
const text = first.text;
if (
typeof text !== "string" ||
!text.startsWith(ANTHROPIC_BILLING_HEADER_PREFIX) ||
!ANTHROPIC_BILLING_CCH_RE.test(text)
) {
return { body, stabilized: false };
}

const placeholderText = text.replace(ANTHROPIC_BILLING_CCH_RE, ANTHROPIC_BILLING_CCH_PLACEHOLDER);
const placeholderSystem = [{ ...first, text: placeholderText }, ...system.slice(1)];
// Only cache-prefix material feeds this compatibility hash. Including messages would
// reproduce Claude Code's per-turn rotating cch and defeat prompt caching again.
const cachePrefixFingerprint = stableJson({
model: typeof body.model === "string" ? body.model : null,
system: placeholderSystem,
tools: body.tools ?? null,
});
const stableCch = createHash("sha256").update(cachePrefixFingerprint).digest("hex").slice(0, 5);
const nextText = text.replace(ANTHROPIC_BILLING_CCH_RE, `cch=${stableCch};`);
if (nextText === text) return { body, stabilized: false };
return {
body: {
...body,
system: [{ ...first, text: nextText }, ...system.slice(1)],
},
stabilized: true,
};
}

function prepareNativeRequestForUpstream(
nativeRequest: InternalRequest["native_request"],
providerModel: string,
Expand Down Expand Up @@ -618,6 +676,14 @@ function prepareNativeRequestForUpstream(
mutations.empty_anthropic_text_blocks_stripped = sanitized.strippedEmptyTextBlocks;
}
}
const stableBilling = stabilizeAnthropicBillingCch(body);
if (stableBilling.stabilized) {
body = stableBilling.body;
bodyChanged = true;
if (mutations) {
appendMutationList(mutations, "body_shims_applied", ["anthropic_billing_cch_stabilized"]);
}
}
}

// Lane-forced reasoning override (issue: lane-forced-reasoning): rewrite the
Expand Down
8 changes: 8 additions & 0 deletions implementation-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

---

## 2026-07-06 · Anthropic native passthrough 稳定 Claude Code billing cch(Provider execution / prompt cache,docs/04/05,原则 3/5/7/8)

- **背景(Lukin)**:线上 `claude-fable-5` 请求大多是 Anthropic native passthrough,理论上应保留 Claude Code 原始 body;但 production SQLite 样本显示同一会话里 `system[0]` 的 `x-anthropic-billing-header` 只有 `cch` 每轮变化(`cc_version`/`cc_entrypoint` 稳定),导致 Anthropic prompt cache 的严格前缀匹配被第一块内容打断,缓存读取只覆盖小前缀,成本显著偏高。
- **问题归属**:这不是 Helm 协议转换 correctness bug;原样转发本身成立。这里是对 Claude Code 官方 billing header 与 Anthropic prompt cache 机制冲突的兼容性 workaround:为了自托管网关的成本边界,允许 native passthrough 做一个可审计的最小 body shim。
- **修复边界**:只在 `protocol === anthropic_messages` 且 `system[0]` 明确是 `x-anthropic-billing-header`、含 5 位 hex `cch` 时触发;保留 `cc_version`、`cc_entrypoint`、system/messages/tools 正文和 cache_control,仅把 `cch` 替换成由稳定 cache-prefix 材料(resolved model、system、tools,排除 messages)计算出的 5 位值。没有该 header 的 native 请求完全不变。
- **观测决策**:触发时写入 passthrough mutation `body_shims_applied: ["anthropic_billing_cch_stabilized"]`,并清掉 raw_body 走重序列化,避免 telemetry 显示仍是旧的客户端 raw body。后续线上可按该 mutation 与 `cached_tokens/cache_creation_tokens` 验证成本改善。
- **风险边界**:如果 Anthropic 将来开始强校验官方 `cch` 与整请求字节一致,这个 shim 可能引发上游拒绝;届时可通过 native passthrough flag 或后续 runtime setting 回滚。当前生产证据显示 `cch` 更像缓存/归因指纹而非认证字段。

## 2026-07-06 · Admin 请求详情 payload 改为分段懒加载(Admin requests performance,docs/07/11,原则 1/7)

- **背景(Lukin)**:线上 `/admin/requests/:traceId` 详情页会在首屏同时拉完整 request/response/upstream payload;部分记录超过 1MB,经公网和未压缩 JSON 传输后容易出现长时间白屏/卡顿。
Expand Down
Loading