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:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
||||||
|
|
||||||
- Fixed retry classification for provider errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).
|
- Fixed retry classification for provider errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).
|
||||||
|
|||||||
@@ -699,6 +699,14 @@ export const stream: StreamFunction<"anthropic-messages", AnthropicOptions> = (
|
|||||||
if (event.usage.cache_creation_input_tokens != null) {
|
if (event.usage.cache_creation_input_tokens != null) {
|
||||||
output.usage.cacheWrite = event.usage.cache_creation_input_tokens;
|
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
|
// Anthropic doesn't provide total_tokens, compute from components
|
||||||
output.usage.totalTokens =
|
output.usage.totalTokens =
|
||||||
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = (
|
|||||||
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
|
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
|
||||||
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
|
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
|
reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
|
||||||
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
|
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
|
||||||
cost: {
|
cost: {
|
||||||
input: 0,
|
input: 0,
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = (
|
|||||||
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
|
(chunk.usageMetadata.candidatesTokenCount || 0) + (chunk.usageMetadata.thoughtsTokenCount || 0),
|
||||||
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
|
cacheRead: chunk.usageMetadata.cachedContentTokenCount || 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
|
reasoning: chunk.usageMetadata.thoughtsTokenCount || 0,
|
||||||
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
|
totalTokens: chunk.usageMetadata.totalTokenCount || 0,
|
||||||
cost: {
|
cost: {
|
||||||
input: 0,
|
input: 0,
|
||||||
|
|||||||
@@ -1107,6 +1107,7 @@ function parseChunkUsage(
|
|||||||
completion_tokens?: number;
|
completion_tokens?: number;
|
||||||
prompt_cache_hit_tokens?: number;
|
prompt_cache_hit_tokens?: number;
|
||||||
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number };
|
||||||
|
completion_tokens_details?: { reasoning_tokens?: number };
|
||||||
},
|
},
|
||||||
model: Model<"openai-completions">,
|
model: Model<"openai-completions">,
|
||||||
): AssistantMessage["usage"] {
|
): AssistantMessage["usage"] {
|
||||||
@@ -1130,6 +1131,7 @@ function parseChunkUsage(
|
|||||||
output: outputTokens,
|
output: outputTokens,
|
||||||
cacheRead: cacheReadTokens,
|
cacheRead: cacheReadTokens,
|
||||||
cacheWrite: cacheWriteTokens,
|
cacheWrite: cacheWriteTokens,
|
||||||
|
reasoning: rawUsage.completion_tokens_details?.reasoning_tokens || 0,
|
||||||
totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
|
totalTokens: input + outputTokens + cacheReadTokens + cacheWriteTokens,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|||||||
output: response.usage.output_tokens || 0,
|
output: response.usage.output_tokens || 0,
|
||||||
cacheRead: cachedTokens,
|
cacheRead: cachedTokens,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
|
reasoning: response.usage.output_tokens_details?.reasoning_tokens || 0,
|
||||||
totalTokens: response.usage.total_tokens || 0,
|
totalTokens: response.usage.total_tokens || 0,
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -356,6 +356,12 @@ export interface Usage {
|
|||||||
cacheWrite: number;
|
cacheWrite: number;
|
||||||
/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */
|
/** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */
|
||||||
cacheWrite1h?: number;
|
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;
|
totalTokens: number;
|
||||||
cost: {
|
cost: {
|
||||||
input: number;
|
input: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user