diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 99e2e9134..f200c268c 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -156,16 +156,16 @@ function firstPositiveNumber(...values: unknown[]): number { return 0; } -function hasCacheReadTokenField(rawUsage: object): boolean { +function hasPositiveCacheReadTokenField(rawUsage: object): boolean { const usageLike = rawUsage as OpenAICompletionsUsageLike; - if (typeof usageLike.cached_tokens === "number") return true; - if (typeof usageLike.prompt_cache_hit_tokens === "number") return true; + if (typeof usageLike.cached_tokens === "number" && usageLike.cached_tokens > 0) return true; + if (typeof usageLike.prompt_cache_hit_tokens === "number" && usageLike.prompt_cache_hit_tokens > 0) return true; const rawPromptTokenDetails = usageLike.prompt_tokens_details; if (typeof rawPromptTokenDetails !== "object" || rawPromptTokenDetails === null) return false; const promptTokenDetails = rawPromptTokenDetails as OpenAICompletionsPromptTokenDetails; - return typeof promptTokenDetails.cached_tokens === "number"; + return typeof promptTokenDetails.cached_tokens === "number" && promptTokenDetails.cached_tokens > 0; } /** @@ -1012,7 +1012,7 @@ const streamOpenAICompletionsOnce = ( const applyUsagePayload = (rawUsage: object): void => { output.usage = parseChunkUsage(rawUsage, model, premiumRequestsTotal); sawUsagePayload = true; - awaitTrailingUsageDetails = !hasCacheReadTokenField(rawUsage); + awaitTrailingUsageDetails = !hasPositiveCacheReadTokenField(rawUsage); }; const timedOpenaiStream = iterateWithIdleTimeout(openaiStream, { idleTimeoutMs, diff --git a/packages/ai/test/openai-stream-terminal-close.test.ts b/packages/ai/test/openai-stream-terminal-close.test.ts index 6f867d3a0..254ef4d2c 100644 --- a/packages/ai/test/openai-stream-terminal-close.test.ts +++ b/packages/ai/test/openai-stream-terminal-close.test.ts @@ -88,11 +88,16 @@ describe("terminal frame without connection close", () => { expect(Date.now() - startedAt).toBeLessThan(2_000); }, 10_000); - it("openai-completions: keeps reading usage-only cache details after finish usage", async () => { + it("openai-completions: ignores zero cache placeholder until trailing positive cache details arrive", async () => { const fetchMock = createNeverClosingFetch([ completionChunk({ choices: [{ index: 0, delta: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], - usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + prompt_tokens_details: { cached_tokens: 0 }, + }, }), completionChunk({ choices: [],