feat(ai): add reasoning token counts to Usage

Add optional reasoning?: number to Usage as a subset of output. Populate
for Anthropic (output_tokens_details.thinking_tokens), OpenAI
Responses/Codex/Azure (output_tokens_details.reasoning_tokens), OpenAI
Completions (completion_tokens_details.reasoning_tokens), and Google
Generative AI / Vertex (thoughtsTokenCount). Bedrock Converse and Mistral
do not return a reasoning breakdown, so they stay unset.

closes #6057
This commit is contained in:
Mario Zechner 2026-06-25 10:34:47 +02:00
parent 5c76ae407d
commit d7868b0998
7 changed files with 23 additions and 0 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added an optional `reasoning` field to `Usage` reporting reasoning/thinking token counts as a subset of `output`. Populated for Anthropic (`output_tokens_details.thinking_tokens`), OpenAI Responses/Codex/Azure (`output_tokens_details.reasoning_tokens`), OpenAI Completions (`completion_tokens_details.reasoning_tokens`), and Google Generative AI / Vertex (`thoughtsTokenCount`). Bedrock Converse and Mistral are not populated because those APIs do not return a reasoning token breakdown ([#6057](https://github.com/earendil-works/pi/issues/6057)).
### Fixed
- Fixed retry classification for provider errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).

View file

@ -699,6 +699,14 @@ export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = (
if (event.usage.cache_creation_input_tokens != null) {
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
}
// Anthropic reports reasoning tokens in `output_tokens_details.thinking_tokens` on the
// final message_delta usage (a subset of output_tokens). SDK 0.91.1 omits the field from
// its Usage type, so read it through a narrow cast. Verified against the live API.
const thinkingTokens = (event.usage as { output_tokens_details?: { thinking_tokens?: number } })
.output_tokens_details?.thinking_tokens;
if (thinkingTokens != null) {
output.usage.reasoning = thinkingTokens;
}
// Anthropic doesn't provide total_tokens, compute from components
output.usage.totalTokens =
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;

View file

@ -221,6 +221,7 @@ export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = (
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
cacheWrite: 0,
reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
cost: {
input: 0,

View file

@ -238,6 +238,7 @@ export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = (
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
cacheWrite: 0,
reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
cost: {
input: 0,

View file

@ -1107,6 +1107,7 @@ function parseChunkUsage(
completion_tokens?: number;
prompt_cache_hit_tokens?: number;
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
completion_tokens_details?: { reasoning_tokens?: number };
},
model: Model<"openai-completions">,
): AssistantMessage["usage"] {
@ -1130,6 +1131,7 @@ function parseChunkUsage(
output: outputTokens,
cacheRead: cacheReadTokens,
cacheWrite: cacheWriteTokens,
reasoning: rawUsage.completion_tokens_details?.reasoning_tokens || 0,
totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};

View file

@ -312,6 +312,7 @@ export async function processResponsesStream<TApi extends Api>(
output: response.usage.output_tokens || 0,
cacheRead: cachedTokens,
cacheWrite: 0,
reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0,
totalTokens: response.usage.total_tokens || 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};

View file

@ -356,6 +356,12 @@ export interface Usage {
cacheWrite: number;
/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */
cacheWrite1h?: number;
/**
* Reasoning/thinking tokens, when the provider reports them. This is a subset of
* `output`: `output` already includes these tokens. Set to a number (possibly 0) by
* providers that expose a reasoning breakdown; left undefined by providers that don't.
*/
reasoning?: number;
totalTokens: number;
cost: {
input: number;