From 73b0b3149d90062fbbfbc5cea0da05a8e05f48d3 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Mon, 18 May 2026 15:29:59 -0400 Subject: [PATCH] fix(ai): log raw OpenAI API response objects, not cooked usage - Add rawUsage field to OpenAiChatIterationResult to carry the original OpenAI SDK usage object through the call chain - All downstream chat() logs now dump rawUsage via JSON.parse(JSON.stringify()) instead of our cooked { promptTokens, completionTokens } - This exposes completion_tokens_details.reasoning_tokens and any other fields OpenAI returns that we weren't capturing --- packages/ai/src/openai.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/ai/src/openai.ts b/packages/ai/src/openai.ts index d8034f5..027baac 100644 --- a/packages/ai/src/openai.ts +++ b/packages/ai/src/openai.ts @@ -85,6 +85,8 @@ interface OpenAiChatIterationResult { promptTokens: number; completionTokens: number; }; + // Raw usage object from OpenAI API response (for logging/observability) + rawUsage?: unknown; } export class OpenAiApi extends AiApi { @@ -230,6 +232,7 @@ export class OpenAiApi extends AiApi { let accumulatedResponse = ""; let accumulatedThinking = ""; let usage: { promptTokens: number; completionTokens: number } | undefined; + let rawUsage: unknown; for await (const chunk of response) { if (options.signal?.aborted) { @@ -242,6 +245,7 @@ export class OpenAiApi extends AiApi { promptTokens: chunk.usage.prompt_tokens, completionTokens: chunk.usage.completion_tokens, }; + rawUsage = chunk.usage; // LOG: dump entire usage chunk from OpenAI generate streaming response this.log.info("OpenAI generate: streaming chunk with usage", { usage: JSON.parse(JSON.stringify(chunk.usage)), @@ -277,7 +281,7 @@ export class OpenAiApi extends AiApi { // LOG: dump final usage and accumulated stats from generate() this.log.info("OpenAI generate: complete", { - usage, + rawUsage: rawUsage ? JSON.parse(JSON.stringify(rawUsage)) : undefined, responseLength: accumulatedResponse.length, thinkingLength: accumulatedThinking.length, durationMs, @@ -335,7 +339,7 @@ export class OpenAiApi extends AiApi { // LOG: dump usage from streaming chat iteration this.log.info("OpenAI chat: streaming iteration usage", { - usage: iteration.usage, + rawUsage: iteration.rawUsage ? JSON.parse(JSON.stringify(iteration.rawUsage)) : undefined, finishReason: iteration.finishReason, }); @@ -352,7 +356,7 @@ export class OpenAiApi extends AiApi { }); // LOG: dump usage from non-streaming fallback this.log.info("OpenAI chat: non-streaming fallback usage", { - usage: iteration.usage, + rawUsage: iteration.rawUsage ? JSON.parse(JSON.stringify(iteration.rawUsage)) : undefined, finishReason: iteration.finishReason, }); } @@ -377,7 +381,7 @@ export class OpenAiApi extends AiApi { }; // LOG: dump final chat response stats this.log.info("OpenAI chat: final response", { - usage: iteration.usage, + rawUsage: iteration.rawUsage ? JSON.parse(JSON.stringify(iteration.rawUsage)) : undefined, stats: finalResponse.stats, finishReason: iteration.finishReason, }); @@ -470,6 +474,7 @@ export class OpenAiApi extends AiApi { let finishReason: string | null | undefined; const toolCallMap = new Map(); let usage: { promptTokens: number; completionTokens: number } | undefined; + let rawUsage: unknown; for await (const chunk of response) { if (signal?.aborted) { @@ -485,6 +490,7 @@ export class OpenAiApi extends AiApi { promptTokens: chunk.usage.prompt_tokens, completionTokens: chunk.usage.completion_tokens, }; + rawUsage = chunk.usage; // LOG: dump entire usage chunk from OpenAI chat streaming response this.log.info("OpenAI chat stream: streaming chunk with usage", { usage: JSON.parse(JSON.stringify(chunk.usage)), @@ -522,7 +528,7 @@ export class OpenAiApi extends AiApi { contentDeltaCount, toolDeltaCount, finishReason, - usage, + rawUsage: rawUsage ? JSON.parse(JSON.stringify(rawUsage)) : undefined, responseLength: content.length, thinkingLength: thinking.length, toolCallCount: toolCallMap.size, @@ -537,6 +543,7 @@ export class OpenAiApi extends AiApi { contentDeltaCount, toolDeltaCount, usage, + rawUsage, }; } @@ -580,6 +587,7 @@ export class OpenAiApi extends AiApi { promptTokens: response.usage.prompt_tokens, completionTokens: response.usage.completion_tokens, } : undefined; + const rawUsage = response.usage; const assistantToolCalls = (message?.tool_calls || []) .filter((tc) => tc.type === "function") .map((tc) => ({ @@ -607,6 +615,7 @@ export class OpenAiApi extends AiApi { contentDeltaCount: content ? 1 : 0, toolDeltaCount: assistantToolCalls.length, usage, + rawUsage, }; }