Skip to content

Commit dfd2cb4

Browse files
committed
add changeset, defensive clamp, regroup cache tests
- patch changeset for #509 fix - clamp noCache at 0 if cached_tokens > prompt_tokens - group prompt_tokens_details cases under their own describe block - add intent comment explaining the OpenAI-style cached_tokens convention Made-with: Cursor
1 parent 40b3b5e commit dfd2cb4

3 files changed

Lines changed: 76 additions & 42 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"workers-ai-provider": patch
3+
---
4+
5+
Map `inputTokens.cacheRead` and `inputTokens.noCache` from Workers AI's `usage.prompt_tokens_details.cached_tokens` instead of always reporting them as `undefined`. This makes prompt-cache hits visible to consumers that compute pricing or telemetry from `LanguageModelV3Usage` (`generateText`/`streamText` `result.usage`).
6+
7+
`cached_tokens` is treated as `cacheRead`; `cacheWrite` remains `undefined` because the OpenAI-style usage shape Workers AI returns does not distinguish cache reads from writes.
8+
9+
Closes [#509](https://github.com/cloudflare/ai/issues/509).

packages/workers-ai-provider/src/map-workersai-usage.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { LanguageModelV3Usage } from "@ai-sdk/provider";
33
/**
44
* Map Workers AI usage data to the AI SDK V3 usage format.
55
* Accepts any object that may have a `usage` property with token counts.
6+
*
7+
* Workers AI mirrors the OpenAI usage shape, including
8+
* `prompt_tokens_details.cached_tokens` for prompt-cache hits. OpenAI-style
9+
* responses don't distinguish cache reads from cache writes, so we treat
10+
* `cached_tokens` as `cacheRead` and leave `cacheWrite` undefined.
611
*/
712
export function mapWorkersAIUsage(
813
output: Record<string, unknown> | AiTextGenerationOutput | AiTextToImageOutput,
@@ -24,6 +29,11 @@ export function mapWorkersAIUsage(
2429
const completionTokens = usage.completion_tokens ?? 0;
2530
const cachedTokens = usage.prompt_tokens_details?.cached_tokens;
2631

32+
// Clamp at 0 in case the provider ever reports cached_tokens > prompt_tokens;
33+
// the v3 spec expects non-negative counts.
34+
const noCache =
35+
cachedTokens !== undefined ? Math.max(0, promptTokens - cachedTokens) : undefined;
36+
2737
return {
2838
outputTokens: {
2939
total: completionTokens,
@@ -32,7 +42,7 @@ export function mapWorkersAIUsage(
3242
},
3343
inputTokens: {
3444
total: promptTokens,
35-
noCache: cachedTokens !== undefined ? promptTokens - cachedTokens : undefined,
45+
noCache,
3646
cacheRead: cachedTokens,
3747
cacheWrite: undefined,
3848
},

packages/workers-ai-provider/test/map-workersai-usage.test.ts

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -71,55 +71,70 @@ describe("mapWorkersAIUsage", () => {
7171
expect(result.outputTokens.total).toBe(10);
7272
});
7373

74-
it("should map cacheRead and noCache when prompt_tokens_details is present", () => {
75-
const result = mapWorkersAIUsage({
76-
usage: {
77-
prompt_tokens: 6377,
78-
completion_tokens: 349,
79-
prompt_tokens_details: { cached_tokens: 2861 },
80-
},
74+
describe("with prompt_tokens_details", () => {
75+
it("maps cached_tokens to cacheRead and computes noCache", () => {
76+
const result = mapWorkersAIUsage({
77+
usage: {
78+
prompt_tokens: 6377,
79+
completion_tokens: 349,
80+
prompt_tokens_details: { cached_tokens: 2861 },
81+
},
82+
});
83+
84+
expect(result.inputTokens.cacheRead).toBe(2861);
85+
expect(result.inputTokens.noCache).toBe(6377 - 2861);
86+
expect(result.inputTokens.cacheWrite).toBeUndefined();
8187
});
8288

83-
expect(result.inputTokens.cacheRead).toBe(2861);
84-
expect(result.inputTokens.noCache).toBe(6377 - 2861);
85-
expect(result.inputTokens.cacheWrite).toBeUndefined();
86-
});
87-
88-
it("should handle cached_tokens of 0 (all tokens uncached)", () => {
89-
const result = mapWorkersAIUsage({
90-
usage: {
91-
prompt_tokens: 100,
92-
completion_tokens: 50,
93-
prompt_tokens_details: { cached_tokens: 0 },
94-
},
89+
it("treats cached_tokens=0 as a real signal (not 'unknown')", () => {
90+
const result = mapWorkersAIUsage({
91+
usage: {
92+
prompt_tokens: 100,
93+
completion_tokens: 50,
94+
prompt_tokens_details: { cached_tokens: 0 },
95+
},
96+
});
97+
98+
expect(result.inputTokens.cacheRead).toBe(0);
99+
expect(result.inputTokens.noCache).toBe(100);
95100
});
96101

97-
expect(result.inputTokens.cacheRead).toBe(0);
98-
expect(result.inputTokens.noCache).toBe(100 - 0);
99-
});
102+
it("falls back to undefined when cached_tokens is absent", () => {
103+
const result = mapWorkersAIUsage({
104+
usage: {
105+
prompt_tokens: 100,
106+
completion_tokens: 50,
107+
prompt_tokens_details: {},
108+
},
109+
});
110+
111+
expect(result.inputTokens.cacheRead).toBeUndefined();
112+
expect(result.inputTokens.noCache).toBeUndefined();
113+
});
100114

101-
it("should handle prompt_tokens_details with missing cached_tokens", () => {
102-
const result = mapWorkersAIUsage({
103-
usage: {
104-
prompt_tokens: 100,
105-
completion_tokens: 50,
106-
prompt_tokens_details: {},
107-
},
115+
it("clamps noCache at 0 when cached_tokens > prompt_tokens", () => {
116+
const result = mapWorkersAIUsage({
117+
usage: {
118+
prompt_tokens: 100,
119+
completion_tokens: 10,
120+
prompt_tokens_details: { cached_tokens: 150 },
121+
},
122+
});
123+
124+
expect(result.inputTokens.noCache).toBe(0);
125+
expect(result.inputTokens.cacheRead).toBe(150);
108126
});
109127

110-
expect(result.inputTokens.cacheRead).toBeUndefined();
111-
expect(result.inputTokens.noCache).toBeUndefined();
112-
});
128+
it("does not let cache fields affect raw.total", () => {
129+
const result = mapWorkersAIUsage({
130+
usage: {
131+
prompt_tokens: 1000,
132+
completion_tokens: 200,
133+
prompt_tokens_details: { cached_tokens: 800 },
134+
},
135+
});
113136

114-
it("should compute raw total correctly regardless of cache fields", () => {
115-
const result = mapWorkersAIUsage({
116-
usage: {
117-
prompt_tokens: 1000,
118-
completion_tokens: 200,
119-
prompt_tokens_details: { cached_tokens: 800 },
120-
},
137+
expect(result.raw).toEqual({ total: 1200 });
121138
});
122-
123-
expect(result.raw).toEqual({ total: 1000 + 200 });
124139
});
125140
});

0 commit comments

Comments
 (0)