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;
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<number, StreamingToolCallAccumulator>();
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,
};
}