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
This commit is contained in:
Rob Colbert 2026-05-18 15:29:59 -04:00
parent 51b3013659
commit 73b0b3149d

View File

@ -85,6 +85,8 @@ interface OpenAiChatIterationResult {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
}; };
// Raw usage object from OpenAI API response (for logging/observability)
rawUsage?: unknown;
} }
export class OpenAiApi extends AiApi { export class OpenAiApi extends AiApi {
@ -230,6 +232,7 @@ export class OpenAiApi extends AiApi {
let accumulatedResponse = ""; let accumulatedResponse = "";
let accumulatedThinking = ""; let accumulatedThinking = "";
let usage: { promptTokens: number; completionTokens: number } | undefined; let usage: { promptTokens: number; completionTokens: number } | undefined;
let rawUsage: unknown;
for await (const chunk of response) { for await (const chunk of response) {
if (options.signal?.aborted) { if (options.signal?.aborted) {
@ -242,6 +245,7 @@ export class OpenAiApi extends AiApi {
promptTokens: chunk.usage.prompt_tokens, promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens, completionTokens: chunk.usage.completion_tokens,
}; };
rawUsage = chunk.usage;
// LOG: dump entire usage chunk from OpenAI generate streaming response // LOG: dump entire usage chunk from OpenAI generate streaming response
this.log.info("OpenAI generate: streaming chunk with usage", { this.log.info("OpenAI generate: streaming chunk with usage", {
usage: JSON.parse(JSON.stringify(chunk.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() // LOG: dump final usage and accumulated stats from generate()
this.log.info("OpenAI generate: complete", { this.log.info("OpenAI generate: complete", {
usage, rawUsage: rawUsage ? JSON.parse(JSON.stringify(rawUsage)) : undefined,
responseLength: accumulatedResponse.length, responseLength: accumulatedResponse.length,
thinkingLength: accumulatedThinking.length, thinkingLength: accumulatedThinking.length,
durationMs, durationMs,
@ -335,7 +339,7 @@ export class OpenAiApi extends AiApi {
// LOG: dump usage from streaming chat iteration // LOG: dump usage from streaming chat iteration
this.log.info("OpenAI chat: streaming iteration usage", { this.log.info("OpenAI chat: streaming iteration usage", {
usage: iteration.usage, rawUsage: iteration.rawUsage ? JSON.parse(JSON.stringify(iteration.rawUsage)) : undefined,
finishReason: iteration.finishReason, finishReason: iteration.finishReason,
}); });
@ -352,7 +356,7 @@ export class OpenAiApi extends AiApi {
}); });
// LOG: dump usage from non-streaming fallback // LOG: dump usage from non-streaming fallback
this.log.info("OpenAI chat: non-streaming fallback usage", { 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, finishReason: iteration.finishReason,
}); });
} }
@ -377,7 +381,7 @@ export class OpenAiApi extends AiApi {
}; };
// LOG: dump final chat response stats // LOG: dump final chat response stats
this.log.info("OpenAI chat: final response", { this.log.info("OpenAI chat: final response", {
usage: iteration.usage, rawUsage: iteration.rawUsage ? JSON.parse(JSON.stringify(iteration.rawUsage)) : undefined,
stats: finalResponse.stats, stats: finalResponse.stats,
finishReason: iteration.finishReason, finishReason: iteration.finishReason,
}); });
@ -470,6 +474,7 @@ export class OpenAiApi extends AiApi {
let finishReason: string | null | undefined; let finishReason: string | null | undefined;
const toolCallMap = new Map<number, StreamingToolCallAccumulator>(); const toolCallMap = new Map<number, StreamingToolCallAccumulator>();
let usage: { promptTokens: number; completionTokens: number } | undefined; let usage: { promptTokens: number; completionTokens: number } | undefined;
let rawUsage: unknown;
for await (const chunk of response) { for await (const chunk of response) {
if (signal?.aborted) { if (signal?.aborted) {
@ -485,6 +490,7 @@ export class OpenAiApi extends AiApi {
promptTokens: chunk.usage.prompt_tokens, promptTokens: chunk.usage.prompt_tokens,
completionTokens: chunk.usage.completion_tokens, completionTokens: chunk.usage.completion_tokens,
}; };
rawUsage = chunk.usage;
// LOG: dump entire usage chunk from OpenAI chat streaming response // LOG: dump entire usage chunk from OpenAI chat streaming response
this.log.info("OpenAI chat stream: streaming chunk with usage", { this.log.info("OpenAI chat stream: streaming chunk with usage", {
usage: JSON.parse(JSON.stringify(chunk.usage)), usage: JSON.parse(JSON.stringify(chunk.usage)),
@ -522,7 +528,7 @@ export class OpenAiApi extends AiApi {
contentDeltaCount, contentDeltaCount,
toolDeltaCount, toolDeltaCount,
finishReason, finishReason,
usage, rawUsage: rawUsage ? JSON.parse(JSON.stringify(rawUsage)) : undefined,
responseLength: content.length, responseLength: content.length,
thinkingLength: thinking.length, thinkingLength: thinking.length,
toolCallCount: toolCallMap.size, toolCallCount: toolCallMap.size,
@ -537,6 +543,7 @@ export class OpenAiApi extends AiApi {
contentDeltaCount, contentDeltaCount,
toolDeltaCount, toolDeltaCount,
usage, usage,
rawUsage,
}; };
} }
@ -580,6 +587,7 @@ export class OpenAiApi extends AiApi {
promptTokens: response.usage.prompt_tokens, promptTokens: response.usage.prompt_tokens,
completionTokens: response.usage.completion_tokens, completionTokens: response.usage.completion_tokens,
} : undefined; } : undefined;
const rawUsage = response.usage;
const assistantToolCalls = (message?.tool_calls || []) const assistantToolCalls = (message?.tool_calls || [])
.filter((tc) => tc.type === "function") .filter((tc) => tc.type === "function")
.map((tc) => ({ .map((tc) => ({
@ -607,6 +615,7 @@ export class OpenAiApi extends AiApi {
contentDeltaCount: content ? 1 : 0, contentDeltaCount: content ? 1 : 0,
toolDeltaCount: assistantToolCalls.length, toolDeltaCount: assistantToolCalls.length,
usage, usage,
rawUsage,
}; };
} }