diff --git a/packages/domain/src/provider-event.ts b/packages/domain/src/provider-event.ts
index b98325c7..891130e8 100644
--- a/packages/domain/src/provider-event.ts
+++ b/packages/domain/src/provider-event.ts
@@ -292,6 +292,14 @@ export const threadEventTokenUsageBreakdownSchema = z.object({
totalTokens: z.number(),
inputTokens: z.number(),
cachedInputTokens: z.number(),
+ /**
+ * The part of the prompt written to the prompt cache on this request, when
+ * the provider reports it. Cache writes and cache reads are priced very
+ * differently (Anthropic: 1.25x vs 0.1x base input), so a consumer pricing
+ * usage needs them apart. For claude-code this is the cache-write share of
+ * `cachedInputTokens`; codex reports it separately.
+ */
+ cacheWriteInputTokens: z.number().optional(),
outputTokens: z.number(),
reasoningOutputTokens: z.number(),
});
diff --git a/packages/provider-bridge-protocol/src/bridge-kit/adapter-utils.ts b/packages/provider-bridge-protocol/src/bridge-kit/adapter-utils.ts
index 89769ba4..a72c665b 100644
--- a/packages/provider-bridge-protocol/src/bridge-kit/adapter-utils.ts
+++ b/packages/provider-bridge-protocol/src/bridge-kit/adapter-utils.ts
@@ -361,6 +361,16 @@ export function addTokenUsage(
totalTokens: total.totalTokens + last.totalTokens,
inputTokens: total.inputTokens + last.inputTokens,
cachedInputTokens: total.cachedInputTokens + last.cachedInputTokens,
+ // Optional on the wire: stay absent until some request reports it, so a
+ // provider that never does is not shown as writing zero.
+ ...(total.cacheWriteInputTokens === undefined &&
+ last.cacheWriteInputTokens === undefined
+ ? {}
+ : {
+ cacheWriteInputTokens:
+ (total.cacheWriteInputTokens ?? 0) +
+ (last.cacheWriteInputTokens ?? 0),
+ }),
outputTokens: total.outputTokens + last.outputTokens,
reasoningOutputTokens:
total.reasoningOutputTokens + last.reasoningOutputTokens,
diff --git a/plugins/provider-claude-code/src/sdk-extraction.ts b/plugins/provider-claude-code/src/sdk-extraction.ts
index 8f58a8a4..6c7c1aa1 100644
--- a/plugins/provider-claude-code/src/sdk-extraction.ts
+++ b/plugins/provider-claude-code/src/sdk-extraction.ts
@@ -337,6 +337,7 @@ function toTokenUsageBreakdown(
totalTokens: inputTokens + outputTokens + cachedInputTokens,
inputTokens,
cachedInputTokens,
+ cacheWriteInputTokens: cacheCreationTokens,
outputTokens,
reasoningOutputTokens: 0,
};
diff --git a/plugins/provider-codex/src/delta-translation.ts b/plugins/provider-codex/src/delta-translation.ts
index 6be48511..fd88271e 100644
--- a/plugins/provider-codex/src/delta-translation.ts
+++ b/plugins/provider-codex/src/delta-translation.ts
@@ -1230,6 +1230,7 @@ export function translateCodexEventToDeltas(
totalTokens: tokenUsage.total.totalTokens,
inputTokens: tokenUsage.total.inputTokens,
cachedInputTokens: tokenUsage.total.cachedInputTokens,
+ cacheWriteInputTokens: tokenUsage.total.cacheWriteInputTokens,
outputTokens: tokenUsage.total.outputTokens,
reasoningOutputTokens: tokenUsage.total.reasoningOutputTokens,
},
@@ -1237,6 +1238,7 @@ export function translateCodexEventToDeltas(
totalTokens: tokenUsage.last.totalTokens,
inputTokens: tokenUsage.last.inputTokens,
cachedInputTokens: tokenUsage.last.cachedInputTokens,
+ cacheWriteInputTokens: tokenUsage.last.cacheWriteInputTokens,
outputTokens: tokenUsage.last.outputTokens,
reasoningOutputTokens: tokenUsage.last.reasoningOutputTokens,
},
diff --git a/plugins/provider-codex/src/schemas.ts b/plugins/provider-codex/src/schemas.ts
index add605da..6ceae54e 100644
--- a/plugins/provider-codex/src/schemas.ts
+++ b/plugins/provider-codex/src/schemas.ts
@@ -559,6 +559,9 @@ const codexTokenUsageBreakdownSchema = z
totalTokens: z.number(),
inputTokens: z.number(),
cachedInputTokens: z.number(),
+ // In the app-server's TokenUsageBreakdown; optional so an older codex that
+ // omits it still parses.
+ cacheWriteInputTokens: z.number().optional(),
outputTokens: z.number(),
reasoningOutputTokens: z.number(),
})
What happens
Token usage cannot be priced correctly, because prompt-cache writes are reported as cache reads (claude-code) or dropped (codex). Anthropic prices a cache write at 1.25x base input and a cache read at 0.1x, so the two need to be kept apart.
Measured on one operator's claude-code sessions over 3 days: cache writes were 1.5% of the tokens bb reports as
cachedInputTokens, but 16% of what those tokens cost. Any spend view built onthread/tokenUsage/updatedunderprices claude-code by that much, and shows ~100% "cached".Why
plugins/provider-claude-code/src/sdk-extraction.ts:334:const cachedInputTokens = cacheReadTokens + cacheCreationTokens;. The write/read split from the SDK is lost here.TokenUsageBreakdowncarriescacheWriteInputTokens(see the generatedschema/v2/TokenUsageBreakdown.ts), butplugins/provider-codex/src/delta-translation.ts:1232copies every field except it, andschemas.tsdoesn't declare it.threadEventTokenUsageBreakdownSchemainpackages/domain/src/provider-event.tshas no field for it.Fix (patch below, against
main8d32c61). Additive and non-breakingcacheWriteInputTokenson the shared breakdown. Existing fields keep their meaning, sototalTokensand every current consumer are unchanged.cache_creation_input_tokens(the cache-write share ofcachedInputTokens).totalandlast.addTokenUsagesums it into claude-code's running total. It stays absent until some request reports it, so a provider that never reports it isn't shown as writing zero.Tests
codex
delta-translation.test.ts75/75; claude-code 348/348; provider-bridge-protocol 238/238; domain 201/201. Typecheck green for all four packages.Related: nothing documents whether
inputTokensincludes cached tokens. codex's does and claude-code's doesn't, which consumers must currently infer. That isn't changed here.Patch