mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): price Vercel AI Gateway 1-hour cache writes correctly
Read the 1-hour cache-write count from streaming deltas when present, and add a regression test. Fixes #9210
This commit is contained in:
parent
002fc83852
commit
667fc3dd36
3 changed files with 46 additions and 0 deletions
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
- Added `onProviderStreamEvent` to observe parsed provider stream events before normalization, including provider-specific fields not retained in assistant messages ([#9784](https://github.com/earendil-works/pi/issues/9784)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed 1-hour Anthropic cache writes reported by Vercel AI Gateway in streaming deltas being priced at the 5-minute rate ([#9210](https://github.com/earendil-works/pi/issues/9210)).
|
||||
|
||||
## [0.87.1] - 2026-09-22
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -776,6 +776,13 @@ export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = (
|
|||
if (event.usage.cache_creation_input_tokens != null) {
|
||||
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
|
||||
}
|
||||
// Vercel AI Gateway includes the TTL breakdown in deltas, though the SDK only types it on message_start.
|
||||
const cacheCreation = (
|
||||
event.usage as typeof event.usage & { cache_creation?: { ephemeral_1h_input_tokens?: number } }
|
||||
).cache_creation;
|
||||
if (cacheCreation?.ephemeral_1h_input_tokens != null) {
|
||||
output.usage.cacheWrite1h = cacheCreation.ephemeral_1h_input_tokens;
|
||||
}
|
||||
// Anthropic reports reasoning tokens as a subset of output tokens.
|
||||
const thinkingTokens = event.usage.output_tokens_details?.thinking_tokens;
|
||||
if (thinkingTokens != null) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,41 @@ describe("Anthropic 1h cache write cost", () => {
|
|||
expect(result.usage.cost.cacheWrite).toBeCloseTo(7.75, 10);
|
||||
});
|
||||
|
||||
// Regression for #9210: Vercel AI Gateway sends cache usage in message_delta, not message_start.
|
||||
it("prices 1h cache writes reported only in message_delta", async () => {
|
||||
const model = getModel("vercel-ai-gateway", "anthropic/claude-haiku-4.5");
|
||||
const response = createSseResponse([
|
||||
{
|
||||
event: "message_start",
|
||||
data: JSON.stringify({
|
||||
type: "message_start",
|
||||
message: { id: "msg_test", usage: { input_tokens: 0, output_tokens: 0 } },
|
||||
}),
|
||||
},
|
||||
{
|
||||
event: "message_delta",
|
||||
data: JSON.stringify({
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn" },
|
||||
usage: {
|
||||
input_tokens: 3,
|
||||
output_tokens: 4,
|
||||
cache_creation_input_tokens: 6535,
|
||||
cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 6535 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ event: "message_stop", data: JSON.stringify({ type: "message_stop" }) },
|
||||
]);
|
||||
const result = await streamAnthropic(model, context, {
|
||||
client: createFakeAnthropicClient(response),
|
||||
}).result();
|
||||
|
||||
expect(result.usage.cacheWrite).toBe(6535);
|
||||
expect(result.usage.cacheWrite1h).toBe(6535);
|
||||
expect(result.usage.cost.cacheWrite).toBeCloseTo((6535 * model.cost.input * 2) / 1_000_000, 10);
|
||||
});
|
||||
|
||||
it("falls back to the 5m rate when no breakdown is reported", async () => {
|
||||
const model = getModel("anthropic", "claude-opus-4-8");
|
||||
const response = createSseResponse(eventsWithCacheCreation(undefined));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue