diff --git a/.changeset/large-vans-applaud.md b/.changeset/large-vans-applaud.md new file mode 100644 index 000000000000..7256a80f3407 --- /dev/null +++ b/.changeset/large-vans-applaud.md @@ -0,0 +1,21 @@ +--- +'@ai-sdk/openai-compatible': patch +'@ai-sdk/amazon-bedrock': patch +'@ai-sdk/google-vertex': patch +'@ai-sdk/huggingface': patch +'@ai-sdk/perplexity': patch +'@ai-sdk/anthropic': patch +'@ai-sdk/deepseek': patch +'@ai-sdk/provider': patch +'@ai-sdk/mistral': patch +'@ai-sdk/cohere': patch +'@ai-sdk/google': patch +'@ai-sdk/openai': patch +'@ai-sdk/azure': patch +'@ai-sdk/groq': patch +'@ai-sdk/rsc': patch +'@ai-sdk/xai': patch +'ai': patch +--- + +feat: extended token usage diff --git a/content/cookbook/05-node/45-stream-object-record-token-usage.mdx b/content/cookbook/05-node/45-stream-object-record-token-usage.mdx index 8a5414181543..c58ead5125ba 100644 --- a/content/cookbook/05-node/45-stream-object-record-token-usage.mdx +++ b/content/cookbook/05-node/45-stream-object-record-token-usage.mdx @@ -39,7 +39,7 @@ const result = streamObject({ The [`streamObject`](/docs/reference/ai-sdk-core/stream-object) result contains a `usage` promise that resolves to the total token usage. ```ts file='index.ts' highlight={"29,32"} -import { streamObject, TokenUsage } from 'ai'; +import { streamObject, LanguageModelUsage } from 'ai'; import { z } from 'zod'; const result = streamObject({ @@ -55,21 +55,21 @@ const result = streamObject({ }); // your custom function to record token usage: -function recordTokenUsage({ +function recordUsage({ inputTokens, outputTokens, totalTokens, -}: TokenUsage) { +}: LanguageModelUsage) { console.log('Prompt tokens:', inputTokens); console.log('Completion tokens:', outputTokens); console.log('Total tokens:', totalTokens); } // use as promise: -result.usage.then(recordTokenUsage); +result.usage.then(recordUsage); // use with async/await: -recordTokenUsage(await result.usage); +recordUsage(await result.usage); // note: the stream needs to be consumed because of backpressure for await (const partialObject of result.partialObjectStream) { diff --git a/content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx b/content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx index 1a337f61931f..028b72eb8e43 100644 --- a/content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +++ b/content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx @@ -763,31 +763,70 @@ To see `generateText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -804,31 +843,70 @@ To see `generateText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -941,31 +1019,70 @@ To see `generateText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -1317,30 +1434,69 @@ To see `generateText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -1348,7 +1504,7 @@ To see `generateText` in action, check out [these examples](#examples). }, { name: 'totalUsage', - type: 'CompletionTokenUsage', + type: 'LanguageModelUsage', description: 'The total token usage of all steps. When there are multiple steps, the usage is the sum of all step usages.', properties: [ @@ -1617,31 +1773,70 @@ To see `generateText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, diff --git a/content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx b/content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx index 158f7b6ec8bc..8191abaee26b 100644 --- a/content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +++ b/content/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx @@ -1019,31 +1019,70 @@ To see `streamText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -1225,31 +1264,70 @@ To see `streamText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, @@ -1579,39 +1657,76 @@ To see `streamText` in action, check out [these examples](#examples). type: 'Promise', description: 'The total token usage of the generated response. When there are multiple steps, the usage is the sum of all step usages. Automatically consumes the stream.', properties: [ - { - type: 'LanguageModelUsage', - parameters: [ - { - name: 'inputTokens', - type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', - }, - { - name: 'outputTokens', - type: 'number | undefined', - description: 'The number of output (completion) tokens used.', - }, - { - name: 'totalTokens', - type: 'number | undefined', - description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', - }, - { - name: 'reasoningTokens', - type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', - }, - { - name: 'cachedInputTokens', - type: 'number | undefined', - isOptional: true, - description: 'The number of cached input tokens.', - }, - ], - }, + { + type: 'LanguageModelUsage', + parameters: [ + { + name: 'inputTokens', + type: 'number | undefined', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], + }, + { + name: 'outputTokens', + type: 'number | undefined', + description: 'The number of total output (completion) tokens used.', + }, + { + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', + description: + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], + }, + { + name: 'totalTokens', + type: 'number | undefined', + description: 'The total number of tokens used.', + }, + { + name: 'raw', + type: 'object | undefined', + isOptional: true, + description: 'Raw usage information from the provider. This is the provider\'s original usage information and may include additional fields.', + }, + ], + }, ], }, { @@ -1929,30 +2044,67 @@ To see `streamText` in action, check out [these examples](#examples). { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: 'Raw usage information from the provider. This is the provider\'s original usage information and may include additional fields.', }, ], }, @@ -2366,36 +2518,73 @@ To see `streamText` in action, check out [these examples](#examples). type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ - { + { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: 'Raw usage information from the provider. This is the provider\'s original usage information and may include additional fields.', }, ], }, @@ -2443,36 +2632,73 @@ To see `streamText` in action, check out [these examples](#examples). type: 'LanguageModelUsage', description: 'The total token usage of the generated text.', properties: [ - { + { type: 'LanguageModelUsage', parameters: [ { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: 'Raw usage information from the provider. This is the provider\'s original usage information and may include additional fields.', }, ], }, diff --git a/content/docs/07-reference/01-ai-sdk-core/03-generate-object.mdx b/content/docs/07-reference/01-ai-sdk-core/03-generate-object.mdx index 02ad9048bb63..5f384be36a43 100644 --- a/content/docs/07-reference/01-ai-sdk-core/03-generate-object.mdx +++ b/content/docs/07-reference/01-ai-sdk-core/03-generate-object.mdx @@ -595,30 +595,69 @@ To see `generateObject` in action, check out the [additional examples](#more-exa { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, diff --git a/content/docs/07-reference/01-ai-sdk-core/04-stream-object.mdx b/content/docs/07-reference/01-ai-sdk-core/04-stream-object.mdx index 61e7d2f0070d..380374f40852 100644 --- a/content/docs/07-reference/01-ai-sdk-core/04-stream-object.mdx +++ b/content/docs/07-reference/01-ai-sdk-core/04-stream-object.mdx @@ -602,44 +602,76 @@ To see `streamObject` in action, check out the [additional examples](#more-examp type: 'OnFinishResult', parameters: [ { - name: 'usage', type: 'LanguageModelUsage', - description: 'The token usage of the generated text.', - properties: [ + parameters: [ { - type: 'LanguageModelUsage', - parameters: [ + name: 'inputTokens', + type: 'number | undefined', + description: + 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ { - name: 'inputTokens', + name: 'noCacheTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: + 'The number of non-cached input (prompt) tokens used.', }, { - name: 'outputTokens', + name: 'cacheReadTokens', type: 'number | undefined', description: - 'The number of output (completion) tokens used.', + 'The number of cached input (prompt) tokens read.', }, { - name: 'totalTokens', + name: 'cacheWriteTokens', type: 'number | undefined', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'The number of cached input (prompt) tokens written.', }, + ], + }, + { + name: 'outputTokens', + type: 'number | undefined', + description: + 'The number of total output (completion) tokens used.', + }, + { + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', + description: + 'Detailed information about the output (completion) tokens.', + properties: [ { - name: 'reasoningTokens', + name: 'textTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The number of text tokens used.', }, { - name: 'cachedInputTokens', + name: 'reasoningTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of cached input tokens.', + description: 'The number of reasoning tokens used.', }, ], }, + { + name: 'totalTokens', + type: 'number | undefined', + description: 'The total number of tokens used.', + }, + { + name: 'raw', + type: 'object | undefined', + isOptional: true, + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", + }, ], }, { @@ -726,30 +758,69 @@ To see `streamObject` in action, check out the [additional examples](#more-examp { name: 'inputTokens', type: 'number | undefined', - description: 'The number of input (prompt) tokens used.', + description: 'The total number of input (prompt) tokens used.', + }, + { + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', + description: + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], }, { name: 'outputTokens', type: 'number | undefined', - description: 'The number of output (completion) tokens used.', + description: + 'The number of total output (completion) tokens used.', }, { - name: 'totalTokens', - type: 'number | undefined', + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', description: - 'The total number of tokens as reported by the provider. This number might be different from the sum of inputTokens and outputTokens and e.g. include reasoning tokens or other overhead.', + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { - name: 'reasoningTokens', + name: 'totalTokens', type: 'number | undefined', - isOptional: true, - description: 'The number of reasoning tokens used.', + description: 'The total number of tokens used.', }, { - name: 'cachedInputTokens', - type: 'number | undefined', + name: 'raw', + type: 'object | undefined', isOptional: true, - description: 'The number of cached input tokens.', + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, diff --git a/content/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx b/content/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx index f9aff7720fbf..a886a698173f 100644 --- a/content/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx +++ b/content/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx @@ -410,27 +410,76 @@ To see `streamUI` in action, check out [these examples](#examples). parameters: [ { name: 'usage', - type: 'TokenUsage', + type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { - type: 'TokenUsage', + type: 'LanguageModelUsage', parameters: [ { - name: 'promptTokens', - type: 'number', - description: 'The total number of tokens in the prompt.', + name: 'inputTokens', + type: 'number | undefined', + description: 'The total number of input (prompt) tokens used.', }, { - name: 'completionTokens', - type: 'number', + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', description: - 'The total number of tokens in the completion.', + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], + }, + { + name: 'outputTokens', + type: 'number | undefined', + description: 'The number of total output (completion) tokens used.', + }, + { + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', + description: + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { name: 'totalTokens', - type: 'number', - description: 'The total number of tokens generated.', + type: 'number | undefined', + description: 'The total number of tokens used.', + }, + { + name: 'raw', + type: 'object | undefined', + isOptional: true, + description: 'Raw usage information from the provider. This is the provider\'s original usage information and may include additional fields.', }, ], }, @@ -469,7 +518,8 @@ To see `streamUI` in action, check out [these examples](#examples). }, ], }, - ]} + +]} /> ## Returns @@ -585,27 +635,79 @@ To see `streamUI` in action, check out [these examples](#examples). }, { name: 'usage', - type: 'TokenUsage', + type: 'LanguageModelUsage', description: 'The token usage of the generated text.', properties: [ { - type: 'TokenUsage', + type: 'LanguageModelUsage', parameters: [ { - name: 'promptTokens', - type: 'number', - description: 'The total number of tokens in the prompt.', + name: 'inputTokens', + type: 'number | undefined', + description: + 'The total number of input (prompt) tokens used.', }, { - name: 'completionTokens', - type: 'number', + name: 'inputTokenDetails', + type: 'LanguageModelInputTokenDetails', description: - 'The total number of tokens in the completion.', + 'Detailed information about the input (prompt) tokens. See also: cached tokens and non-cached tokens.', + properties: [ + { + name: 'noCacheTokens', + type: 'number | undefined', + description: + 'The number of non-cached input (prompt) tokens used.', + }, + { + name: 'cacheReadTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens read.', + }, + { + name: 'cacheWriteTokens', + type: 'number | undefined', + description: + 'The number of cached input (prompt) tokens written.', + }, + ], + }, + { + name: 'outputTokens', + type: 'number | undefined', + description: + 'The number of total output (completion) tokens used.', + }, + { + name: 'outputTokenDetails', + type: 'LanguageModelOutputTokenDetails', + description: + 'Detailed information about the output (completion) tokens.', + properties: [ + { + name: 'textTokens', + type: 'number | undefined', + description: 'The number of text tokens used.', + }, + { + name: 'reasoningTokens', + type: 'number | undefined', + description: 'The number of reasoning tokens used.', + }, + ], }, { name: 'totalTokens', - type: 'number', - description: 'The total number of tokens generated.', + type: 'number | undefined', + description: 'The total number of tokens used.', + }, + { + name: 'raw', + type: 'object | undefined', + isOptional: true, + description: + "Raw usage information from the provider. This is the provider's original usage information and may include additional fields.", }, ], }, diff --git a/content/docs/08-migration-guides/24-migration-guide-6-0.mdx b/content/docs/08-migration-guides/24-migration-guide-6-0.mdx index 1873363676b7..b056b9d9d727 100644 --- a/content/docs/08-migration-guides/24-migration-guide-6-0.mdx +++ b/content/docs/08-migration-guides/24-migration-guide-6-0.mdx @@ -85,6 +85,13 @@ const modelMessages = convertToModelMessages(messages); // ModelMessage[] They will be removed in a future version. You can use `generateText` and `streamText` with an `output` setting instead. +### `cachedInputTokens` and `reasoningTokens` in `LanguageModelUsage` Deprecation + +`cachedInputTokens` and `reasoningTokens` in `LanguageModelUsage` have been deprecated. + +You can replace `cachedInputTokens` with `inputTokenDetails.cacheReadTokens` +and `reasoningTokens` with `outputTokenDetails.reasoningTokens`. + ### `ToolCallOptions` to `ToolExecutionOptions` Rename The `ToolCallOptions` type has been renamed to `ToolExecutionOptions` diff --git a/examples/ai-core/src/benchmark/stream-text-benchmark.ts b/examples/ai-core/src/benchmark/stream-text-benchmark.ts index 1ca7dcac9402..d9fd2a39c294 100644 --- a/examples/ai-core/src/benchmark/stream-text-benchmark.ts +++ b/examples/ai-core/src/benchmark/stream-text-benchmark.ts @@ -68,9 +68,17 @@ const generateLongContent = (tokens: number, includeTools = false) => { type: 'finish', finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: tokens, - totalTokens: tokens + 10, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: tokens, + text: tokens, + reasoning: undefined, + }, }, }); diff --git a/examples/ai-core/src/generate-object/mock-error.ts b/examples/ai-core/src/generate-object/mock-error.ts index 27e06220382a..d5e801e0b78a 100644 --- a/examples/ai-core/src/generate-object/mock-error.ts +++ b/examples/ai-core/src/generate-object/mock-error.ts @@ -17,9 +17,17 @@ async function main() { }, finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, }), }), diff --git a/examples/ai-core/src/generate-object/mock-repair-add-close.ts b/examples/ai-core/src/generate-object/mock-repair-add-close.ts index cad2fea9d279..2d05387d4605 100644 --- a/examples/ai-core/src/generate-object/mock-repair-add-close.ts +++ b/examples/ai-core/src/generate-object/mock-repair-add-close.ts @@ -8,9 +8,17 @@ async function main() { model: new MockLanguageModelV3({ doGenerate: async () => ({ usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, warnings: [], finishReason: 'tool-calls', diff --git a/examples/ai-core/src/generate-object/mock.ts b/examples/ai-core/src/generate-object/mock.ts index 49c6d89706fc..cd64f01f3f2a 100644 --- a/examples/ai-core/src/generate-object/mock.ts +++ b/examples/ai-core/src/generate-object/mock.ts @@ -10,9 +10,17 @@ async function main() { content: [{ type: 'text', text: `{"content":"Hello, world!"}` }], finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, warnings: [], }), diff --git a/examples/ai-core/src/generate-text/mock-invalid-tool-call.ts b/examples/ai-core/src/generate-text/mock-invalid-tool-call.ts index 3d571bd4f22c..55b89e402d85 100644 --- a/examples/ai-core/src/generate-text/mock-invalid-tool-call.ts +++ b/examples/ai-core/src/generate-text/mock-invalid-tool-call.ts @@ -26,9 +26,17 @@ async function main() { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ diff --git a/examples/ai-core/src/generate-text/mock-tool-call-repair-change-tool.ts b/examples/ai-core/src/generate-text/mock-tool-call-repair-change-tool.ts index 47357a751f93..945ac83f86ab 100644 --- a/examples/ai-core/src/generate-text/mock-tool-call-repair-change-tool.ts +++ b/examples/ai-core/src/generate-text/mock-tool-call-repair-change-tool.ts @@ -9,9 +9,17 @@ async function main() { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ diff --git a/examples/ai-core/src/generate-text/mock-tool-call-repair-reask.ts b/examples/ai-core/src/generate-text/mock-tool-call-repair-reask.ts index b6d2fc8859ab..bfa1a4b60cce 100644 --- a/examples/ai-core/src/generate-text/mock-tool-call-repair-reask.ts +++ b/examples/ai-core/src/generate-text/mock-tool-call-repair-reask.ts @@ -10,9 +10,17 @@ async function main() { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ diff --git a/examples/ai-core/src/generate-text/mock-tool-call-repair-structured-model.ts b/examples/ai-core/src/generate-text/mock-tool-call-repair-structured-model.ts index 45e0e8da24d7..32be24958cf5 100644 --- a/examples/ai-core/src/generate-text/mock-tool-call-repair-structured-model.ts +++ b/examples/ai-core/src/generate-text/mock-tool-call-repair-structured-model.ts @@ -10,9 +10,17 @@ async function main() { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ diff --git a/examples/ai-core/src/generate-text/mock.ts b/examples/ai-core/src/generate-text/mock.ts index cc55605ae8ff..1a064342ebe4 100644 --- a/examples/ai-core/src/generate-text/mock.ts +++ b/examples/ai-core/src/generate-text/mock.ts @@ -9,9 +9,17 @@ async function main() { content: [{ type: 'text', text: `Hello, world!` }], finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, warnings: [], }), diff --git a/examples/ai-core/src/generate-text/openai.ts b/examples/ai-core/src/generate-text/openai.ts index 907269fa719e..2aeca7a856d6 100644 --- a/examples/ai-core/src/generate-text/openai.ts +++ b/examples/ai-core/src/generate-text/openai.ts @@ -11,4 +11,5 @@ run(async () => { }); print('Content:', result.content); + print('Usage:', result.usage); }); diff --git a/examples/ai-core/src/stream-object/mock.ts b/examples/ai-core/src/stream-object/mock.ts index e1469c939303..81eea16f8ae7 100644 --- a/examples/ai-core/src/stream-object/mock.ts +++ b/examples/ai-core/src/stream-object/mock.ts @@ -21,9 +21,17 @@ async function main() { finishReason: 'stop', logprobs: undefined, usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, }, ]), diff --git a/examples/ai-core/src/stream-text/mock-tool-call-repair-change-tool.ts b/examples/ai-core/src/stream-text/mock-tool-call-repair-change-tool.ts index 89220131fb90..180ac8f3f531 100644 --- a/examples/ai-core/src/stream-text/mock-tool-call-repair-change-tool.ts +++ b/examples/ai-core/src/stream-text/mock-tool-call-repair-change-tool.ts @@ -20,9 +20,17 @@ async function main() { finishReason: 'tool-calls', logprobs: undefined, usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, }, ]), diff --git a/examples/ai-core/src/stream-text/mock.ts b/examples/ai-core/src/stream-text/mock.ts index 95be03af2721..345d0454505f 100644 --- a/examples/ai-core/src/stream-text/mock.ts +++ b/examples/ai-core/src/stream-text/mock.ts @@ -17,9 +17,17 @@ async function main() { finishReason: 'stop', logprobs: undefined, usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, }, ]), diff --git a/examples/ai-core/src/stream-text/openai.ts b/examples/ai-core/src/stream-text/openai.ts index 3fa3a826058a..71db507dddd2 100644 --- a/examples/ai-core/src/stream-text/openai.ts +++ b/examples/ai-core/src/stream-text/openai.ts @@ -2,12 +2,16 @@ import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { printFullStream } from '../lib/print-full-stream'; import { run } from '../lib/run'; +import { print } from '../lib/print'; run(async () => { const result = streamText({ model: openai('gpt-5-nano'), prompt: 'Invent a new holiday and describe its traditions.', + maxRetries: 0, }); printFullStream({ result }); + + print('Usage:', await result.usage); }); diff --git a/examples/ai-core/src/stream-text/smooth-stream-chinese.ts b/examples/ai-core/src/stream-text/smooth-stream-chinese.ts index c5d89aae3f4d..39e9afc724a5 100644 --- a/examples/ai-core/src/stream-text/smooth-stream-chinese.ts +++ b/examples/ai-core/src/stream-text/smooth-stream-chinese.ts @@ -19,9 +19,17 @@ async function main() { finishReason: 'stop', logprobs: undefined, usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, }, ], diff --git a/examples/ai-core/src/stream-text/smooth-stream-japanese.ts b/examples/ai-core/src/stream-text/smooth-stream-japanese.ts index 414d8ac8a325..04ab484be9c2 100644 --- a/examples/ai-core/src/stream-text/smooth-stream-japanese.ts +++ b/examples/ai-core/src/stream-text/smooth-stream-japanese.ts @@ -18,9 +18,17 @@ async function main() { finishReason: 'stop', logprobs: undefined, usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, }, ], diff --git a/examples/next-openai/app/api/test-invalid-tool-call/route.ts b/examples/next-openai/app/api/test-invalid-tool-call/route.ts index 4501884b329f..9d9ee121cd35 100644 --- a/examples/next-openai/app/api/test-invalid-tool-call/route.ts +++ b/examples/next-openai/app/api/test-invalid-tool-call/route.ts @@ -82,9 +82,17 @@ export async function POST(req: Request) { type: 'finish', finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, }, ]), diff --git a/packages/ai/internal/index.ts b/packages/ai/internal/index.ts index f01efa73a2ab..8c9e0844b3ad 100644 --- a/packages/ai/internal/index.ts +++ b/packages/ai/internal/index.ts @@ -7,3 +7,4 @@ export { prepareToolsAndToolChoice } from '../src/prompt/prepare-tools-and-tool- export { standardizePrompt } from '../src/prompt/standardize-prompt'; export { prepareCallSettings } from '../src/prompt/prepare-call-settings'; export { prepareRetries } from '../src/util/prepare-retries'; +export { asLanguageModelUsage } from '../src/types/usage'; diff --git a/packages/ai/src/agent/create-agent-ui-stream-response.test.ts b/packages/ai/src/agent/create-agent-ui-stream-response.test.ts index e517cf851aee..ca2e4baa5fe5 100644 --- a/packages/ai/src/agent/create-agent-ui-stream-response.test.ts +++ b/packages/ai/src/agent/create-agent-ui-stream-response.test.ts @@ -44,9 +44,17 @@ describe('createAgentUIStreamResponse', () => { type: 'finish', finishReason: 'stop', usage: { - inputTokens: 10, - outputTokens: 10, - totalTokens: 20, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, providerMetadata: { testProvider: { testKey: 'testValue' }, diff --git a/packages/ai/src/agent/tool-loop-agent.test.ts b/packages/ai/src/agent/tool-loop-agent.test.ts index 39fa85da0c0f..df9e1f317948 100644 --- a/packages/ai/src/agent/tool-loop-agent.test.ts +++ b/packages/ai/src/agent/tool-loop-agent.test.ts @@ -19,10 +19,17 @@ describe('ToolLoopAgent', () => { finishReason: 'stop', usage: { cachedInputTokens: undefined, - inputTokens: 3, - outputTokens: 10, - reasoningTokens: undefined, - totalTokens: 13, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, warnings: [], }; @@ -261,11 +268,17 @@ describe('ToolLoopAgent', () => { type: 'finish', finishReason: 'stop', usage: { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }, providerMetadata: { testProvider: { testKey: 'testValue' }, diff --git a/packages/ai/src/generate-object/__snapshots__/stream-object.test.ts.snap b/packages/ai/src/generate-object/__snapshots__/stream-object.test.ts.snap index f4a1deda8a2d..ae84b9a2da48 100644 --- a/packages/ai/src/generate-object/__snapshots__/stream-object.test.ts.snap +++ b/packages/ai/src/generate-object/__snapshots__/stream-object.test.ts.snap @@ -19,8 +19,18 @@ exports[`streamObject > output = "object" > options.onFinish > should be called }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -41,8 +51,18 @@ exports[`streamObject > output = "object" > options.onFinish > should be called }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -109,8 +129,18 @@ exports[`streamObject > output = "object" > result.fullStream > should send full "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, diff --git a/packages/ai/src/generate-object/generate-object.test.ts b/packages/ai/src/generate-object/generate-object.test.ts index eb3fe7b5d8d8..ce09510eda23 100644 --- a/packages/ai/src/generate-object/generate-object.test.ts +++ b/packages/ai/src/generate-object/generate-object.test.ts @@ -31,11 +31,17 @@ vi.mock('../version', () => { const dummyResponseValues = { finishReason: 'stop' as const, usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, response: { id: 'id-1', timestamp: new Date(123), modelId: 'm-1' }, warnings: [], @@ -709,7 +715,16 @@ describe('generateObject', () => { }, usage: { inputTokens: 10, + inputTokenDetails: { + noCacheTokens: 10, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, outputTokens: 20, + outputTokenDetails: { + textTokens: 20, + reasoningTokens: undefined, + }, totalTokens: 30, reasoningTokens: undefined, cachedInputTokens: undefined, diff --git a/packages/ai/src/generate-object/generate-object.ts b/packages/ai/src/generate-object/generate-object.ts index 83de0bb4f8dd..2f01fbe05a7a 100644 --- a/packages/ai/src/generate-object/generate-object.ts +++ b/packages/ai/src/generate-object/generate-object.ts @@ -32,7 +32,7 @@ import { import { LanguageModelRequestMetadata } from '../types/language-model-request-metadata'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { ProviderMetadata } from '../types/provider-metadata'; -import { LanguageModelUsage } from '../types/usage'; +import { asLanguageModelUsage, LanguageModelUsage } from '../types/usage'; import { DownloadFunction } from '../util/download/download-function'; import { prepareHeaders } from '../util/prepare-headers'; import { prepareRetries } from '../util/prepare-retries'; @@ -366,7 +366,7 @@ via tool or schema description. message: 'No object generated: the model did not return a response.', response: responseData, - usage: result.usage, + usage: asLanguageModelUsage(result.usage), finishReason: result.finishReason, }); } @@ -387,15 +387,17 @@ via tool or schema description. ), // TODO rename telemetry attributes to inputTokens and outputTokens - 'ai.usage.promptTokens': result.usage.inputTokens, - 'ai.usage.completionTokens': result.usage.outputTokens, + 'ai.usage.promptTokens': result.usage.inputTokens.total, + 'ai.usage.completionTokens': + result.usage.outputTokens.total, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [result.finishReason], 'gen_ai.response.id': responseData.id, 'gen_ai.response.model': responseData.modelId, - 'gen_ai.usage.input_tokens': result.usage.inputTokens, - 'gen_ai.usage.output_tokens': result.usage.outputTokens, + 'gen_ai.usage.input_tokens': result.usage.inputTokens.total, + 'gen_ai.usage.output_tokens': + result.usage.outputTokens.total, }, }), ); @@ -412,7 +414,7 @@ via tool or schema description. result = generateResult.objectText; finishReason = generateResult.finishReason; - usage = generateResult.usage; + usage = asLanguageModelUsage(generateResult.usage); warnings = generateResult.warnings; resultProviderMetadata = generateResult.providerMetadata; request = generateResult.request ?? {}; diff --git a/packages/ai/src/generate-object/stream-object.test.ts b/packages/ai/src/generate-object/stream-object.test.ts index c3a6e1c4606a..73558f177489 100644 --- a/packages/ai/src/generate-object/stream-object.test.ts +++ b/packages/ai/src/generate-object/stream-object.test.ts @@ -3,6 +3,7 @@ import { SharedV3Warning, LanguageModelV3StreamPart, TypeValidationError, + LanguageModelV3Usage, } from '@ai-sdk/provider'; import { jsonSchema } from '@ai-sdk/provider-utils'; import { @@ -22,15 +23,21 @@ import { MockTracer } from '../test/mock-tracer'; import { AsyncIterableStream } from '../util/async-iterable-stream'; import { streamObject } from './stream-object'; import { StreamObjectResult } from './stream-object-result'; - -const testUsage = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, - reasoningTokens: undefined, - cachedInputTokens: undefined, +import { asLanguageModelUsage } from '../types/usage'; + +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }; - function createTestModel({ warnings = [], stream = convertArrayToReadableStream([ @@ -351,8 +358,18 @@ describe('streamObject', () => { expect(await result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, } @@ -874,7 +891,7 @@ describe('streamObject', () => { timestamp: new Date(123), modelId: 'model-1', }, - usage: testUsage, + usage: asLanguageModelUsage(testUsage), finishReason: 'stop', }); } @@ -918,7 +935,7 @@ describe('streamObject', () => { timestamp: new Date(123), modelId: 'model-1', }, - usage: testUsage, + usage: asLanguageModelUsage(testUsage), finishReason: 'stop', }); } @@ -959,7 +976,7 @@ describe('streamObject', () => { timestamp: new Date(123), modelId: 'model-1', }, - usage: testUsage, + usage: asLanguageModelUsage(testUsage), finishReason: 'stop', }); } @@ -1802,7 +1819,7 @@ describe('streamObject', () => { timestamp: new Date(0), modelId: 'mock-model-id', }, - usage: testUsage, + usage: asLanguageModelUsage(testUsage), finishReason: 'stop', }); } diff --git a/packages/ai/src/generate-object/stream-object.ts b/packages/ai/src/generate-object/stream-object.ts index e62f9a85a39b..283692a3765e 100644 --- a/packages/ai/src/generate-object/stream-object.ts +++ b/packages/ai/src/generate-object/stream-object.ts @@ -1,10 +1,10 @@ import { JSONValue, - SharedV3Warning, LanguageModelV3FinishReason, LanguageModelV3StreamPart, LanguageModelV3Usage, SharedV3ProviderMetadata, + SharedV3Warning, } from '@ai-sdk/provider'; import { createIdGenerator, @@ -39,7 +39,11 @@ import { import { LanguageModelRequestMetadata } from '../types/language-model-request-metadata'; import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata'; import { ProviderMetadata } from '../types/provider-metadata'; -import { LanguageModelUsage } from '../types/usage'; +import { + asLanguageModelUsage, + createNullLanguageModelUsage, + LanguageModelUsage, +} from '../types/usage'; import { DeepPartial, isDeepEqualData, parsePartialJson } from '../util'; import { AsyncIterableStream, @@ -568,11 +572,7 @@ class DefaultStreamObjectResult // store information for onFinish callback: let warnings: SharedV3Warning[] | undefined; - let usage: LanguageModelUsage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: LanguageModelUsage = createNullLanguageModelUsage(); let finishReason: LanguageModelV3FinishReason | undefined; let providerMetadata: ProviderMetadata | undefined; let object: RESULT | undefined; @@ -699,7 +699,7 @@ class DefaultStreamObjectResult finishReason = chunk.finishReason; // store usage and metadata for promises and onFinish callback: - usage = chunk.usage; + usage = asLanguageModelUsage(chunk.usage); providerMetadata = chunk.providerMetadata; controller.enqueue({ diff --git a/packages/ai/src/generate-text/__snapshots__/generate-text.test.ts.snap b/packages/ai/src/generate-text/__snapshots__/generate-text.test.ts.snap index d18ac2fd5879..261c05aa1946 100644 --- a/packages/ai/src/generate-text/__snapshots__/generate-text.test.ts.snap +++ b/packages/ai/src/generate-text/__snapshots__/generate-text.test.ts.snap @@ -137,8 +137,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -206,8 +216,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -219,15 +239,34 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 13, + }, "inputTokens": 13, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 15, + }, "outputTokens": 15, "reasoningTokens": undefined, "totalTokens": 28, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -304,8 +343,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -373,8 +422,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > callb }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -496,8 +555,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > resul }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -565,8 +634,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result > resul }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -644,8 +723,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -713,8 +802,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -836,8 +935,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -905,8 +1014,18 @@ exports[`generateText > options.stopWhen > 2 steps: initial, tool-result with pr }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1167,8 +1286,18 @@ exports[`generateText > result.steps > should add the reasoning from the model r }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1242,8 +1371,18 @@ exports[`generateText > result.steps > should contain files 1`] = ` }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1309,8 +1448,18 @@ exports[`generateText > result.steps > should contain sources 1`] = ` }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, diff --git a/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap b/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap index 5ce43d519ecd..8bfff1362cb8 100644 --- a/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap +++ b/packages/ai/src/generate-text/__snapshots__/stream-text.test.ts.snap @@ -13,11 +13,11 @@ exports[`streamText > options.stopWhen > 2 steps: initial, tool-result > should "ai.response.finishReason": "stop", "ai.response.text": "Hello, world!", "ai.settings.maxRetries": 2, - "ai.usage.cachedInputTokens": 3, + "ai.usage.cachedInputTokens": 0, "ai.usage.inputTokens": 6, "ai.usage.outputTokens": 20, "ai.usage.reasoningTokens": 10, - "ai.usage.totalTokens": 36, + "ai.usage.totalTokens": 26, "operation.name": "ai.streamText", }, "events": [], @@ -102,11 +102,11 @@ exports[`streamText > options.stopWhen > 2 steps: initial, tool-result > should "ai.response.text": "Hello, world!", "ai.response.timestamp": "1970-01-01T00:00:01.000Z", "ai.settings.maxRetries": 2, - "ai.usage.cachedInputTokens": 3, + "ai.usage.cachedInputTokens": 0, "ai.usage.inputTokens": 3, "ai.usage.outputTokens": 10, "ai.usage.reasoningTokens": 10, - "ai.usage.totalTokens": 23, + "ai.usage.totalTokens": 13, "gen_ai.request.model": "mock-model-id", "gen_ai.response.finish_reasons": [ "stop", @@ -280,8 +280,18 @@ exports[`streamText > result.fullStream > should send delayed asynchronous tool "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -290,7 +300,16 @@ exports[`streamText > result.fullStream > should send delayed asynchronous tool "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -337,8 +356,18 @@ exports[`streamText > result.fullStream > should send tool calls 1`] = ` "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -347,7 +376,16 @@ exports[`streamText > result.fullStream > should send tool calls 1`] = ` "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -400,8 +438,18 @@ exports[`streamText > result.fullStream > should send tool results 1`] = ` "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -410,7 +458,16 @@ exports[`streamText > result.fullStream > should send tool results 1`] = ` "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -992,8 +1049,18 @@ exports[`streamText > tools with custom schema > should send tool calls 1`] = ` "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1002,7 +1069,16 @@ exports[`streamText > tools with custom schema > should send tool calls 1`] = ` "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, diff --git a/packages/ai/src/generate-text/generate-text.test.ts b/packages/ai/src/generate-text/generate-text.test.ts index c81eb8f4aa42..f434ef7257d1 100644 --- a/packages/ai/src/generate-text/generate-text.test.ts +++ b/packages/ai/src/generate-text/generate-text.test.ts @@ -4,6 +4,7 @@ import { LanguageModelV3FunctionTool, LanguageModelV3Prompt, LanguageModelV3ProviderTool, + LanguageModelV3Usage, } from '@ai-sdk/provider'; import { dynamicTool, @@ -40,12 +41,18 @@ vi.mock('../version', () => { }; }); -const testUsage = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, - reasoningTokens: undefined, - cachedInputTokens: undefined, +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }; const dummyResponseValues = { @@ -884,8 +891,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -920,15 +937,34 @@ describe('generateText', () => { ], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -996,11 +1032,17 @@ describe('generateText', () => { ], finishReason: 'tool-calls', usage: { - inputTokens: 10, - outputTokens: 5, - totalTokens: 15, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 5, + text: 5, + reasoning: undefined, + }, }, response: { id: 'test-id-1-from-model', @@ -1070,26 +1112,45 @@ describe('generateText', () => { it('result.totalUsage should sum token usage', () => { expect(result.totalUsage).toMatchInlineSnapshot(` - { - "cachedInputTokens": undefined, - "inputTokens": 13, - "outputTokens": 15, - "reasoningTokens": undefined, - "totalTokens": 28, - } - `); + { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 13, + }, + "inputTokens": 13, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 15, + }, + "outputTokens": 15, + "reasoningTokens": undefined, + "totalTokens": 28, + } + `); }); it('result.usage should contain token usage from final step', async () => { expect(result.usage).toMatchInlineSnapshot(` - { - "cachedInputTokens": undefined, - "inputTokens": 3, - "outputTokens": 10, - "reasoningTokens": undefined, - "totalTokens": 13, - } - `); + { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, + "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, + "outputTokens": 10, + "raw": undefined, + "reasoningTokens": undefined, + "totalTokens": 13, + } + `); }); it('result.steps should contain all steps', () => { @@ -1143,11 +1204,17 @@ describe('generateText', () => { ], finishReason: 'tool-calls', usage: { - inputTokens: 10, - outputTokens: 5, - totalTokens: 15, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 5, + text: 5, + reasoning: undefined, + }, }, response: { id: 'test-id-1-from-model', @@ -1305,8 +1372,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -1374,8 +1451,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1488,8 +1575,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -1557,8 +1654,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1692,26 +1799,45 @@ describe('generateText', () => { it('result.totalUsage should sum token usage', () => { expect(result.totalUsage).toMatchInlineSnapshot(` - { - "cachedInputTokens": undefined, - "inputTokens": 13, - "outputTokens": 15, - "reasoningTokens": undefined, - "totalTokens": 28, - } - `); + { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 13, + }, + "inputTokens": 13, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 15, + }, + "outputTokens": 15, + "reasoningTokens": undefined, + "totalTokens": 28, + } + `); }); it('result.usage should contain token usage from final step', async () => { expect(result.usage).toMatchInlineSnapshot(` - { - "cachedInputTokens": undefined, - "inputTokens": 3, - "outputTokens": 10, - "reasoningTokens": undefined, - "totalTokens": 13, - } - `); + { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, + "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, + "outputTokens": 10, + "raw": undefined, + "reasoningTokens": undefined, + "totalTokens": 13, + } + `); }); it('result.steps should contain all steps', () => { @@ -1763,11 +1889,17 @@ describe('generateText', () => { ], finishReason: 'tool-calls', usage: { - inputTokens: 10, - outputTokens: 5, - totalTokens: 15, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 5, + text: 5, + reasoning: undefined, + }, }, response: { id: 'test-id-1-from-model', @@ -1885,8 +2017,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -1964,8 +2106,18 @@ describe('generateText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 5, + }, "outputTokens": 5, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 15, }, @@ -3452,9 +3604,17 @@ describe('generateText', () => { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ @@ -3587,9 +3747,17 @@ describe('generateText', () => { doGenerate: async () => ({ warnings: [], usage: { - inputTokens: 10, - outputTokens: 20, - totalTokens: 30, + inputTokens: { + total: 10, + noCache: 10, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 20, + text: 20, + reasoning: undefined, + }, }, finishReason: 'tool-calls', content: [ @@ -3737,8 +3905,20 @@ describe('generateText', () => { "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 10, + }, "inputTokens": 10, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 20, + }, "outputTokens": 20, + "raw": undefined, + "reasoningTokens": undefined, "totalTokens": 30, }, "warnings": [], diff --git a/packages/ai/src/generate-text/generate-text.ts b/packages/ai/src/generate-text/generate-text.ts index b13ccb4ce81d..9ff956e81acd 100644 --- a/packages/ai/src/generate-text/generate-text.ts +++ b/packages/ai/src/generate-text/generate-text.ts @@ -30,7 +30,11 @@ import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attribu import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { LanguageModel, ToolChoice } from '../types'; -import { addLanguageModelUsage, LanguageModelUsage } from '../types/usage'; +import { + addLanguageModelUsage, + asLanguageModelUsage, + LanguageModelUsage, +} from '../types/usage'; import { asArray } from '../util/as-array'; import { DownloadFunction } from '../util/download/download-function'; import { prepareRetries } from '../util/prepare-retries'; @@ -519,15 +523,18 @@ A function that attempts to repair a tool call that failed to parse. ), // TODO rename telemetry attributes to inputTokens and outputTokens - 'ai.usage.promptTokens': result.usage.inputTokens, - 'ai.usage.completionTokens': result.usage.outputTokens, + 'ai.usage.promptTokens': result.usage.inputTokens.total, + 'ai.usage.completionTokens': + result.usage.outputTokens.total, // standardized gen-ai llm span attributes: 'gen_ai.response.finish_reasons': [result.finishReason], 'gen_ai.response.id': responseData.id, 'gen_ai.response.model': responseData.modelId, - 'gen_ai.usage.input_tokens': result.usage.inputTokens, - 'gen_ai.usage.output_tokens': result.usage.outputTokens, + 'gen_ai.usage.input_tokens': + result.usage.inputTokens.total, + 'gen_ai.usage.output_tokens': + result.usage.outputTokens.total, }, }), ); @@ -661,7 +668,7 @@ A function that attempts to repair a tool call that failed to parse. const currentStepResult: StepResult = new DefaultStepResult({ content: stepContent, finishReason: currentModelResponse.finishReason, - usage: currentModelResponse.usage, + usage: asLanguageModelUsage(currentModelResponse.usage), warnings: currentModelResponse.warnings, providerMetadata: currentModelResponse.providerMetadata, request: currentModelResponse.request ?? {}, @@ -711,9 +718,10 @@ A function that attempts to repair a tool call that failed to parse. ), // TODO rename telemetry attributes to inputTokens and outputTokens - 'ai.usage.promptTokens': currentModelResponse.usage.inputTokens, + 'ai.usage.promptTokens': + currentModelResponse.usage.inputTokens.total, 'ai.usage.completionTokens': - currentModelResponse.usage.outputTokens, + currentModelResponse.usage.outputTokens.total, }, }), ); diff --git a/packages/ai/src/generate-text/output.test.ts b/packages/ai/src/generate-text/output.test.ts index 5ea08a2b0590..d7b3fb6239a4 100644 --- a/packages/ai/src/generate-text/output.test.ts +++ b/packages/ai/src/generate-text/output.test.ts @@ -12,7 +12,16 @@ const context = { }, usage: { inputTokens: 1, + inputTokenDetails: { + noCacheTokens: 1, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, outputTokens: 2, + outputTokenDetails: { + reasoningTokens: undefined, + textTokens: 2, + }, totalTokens: 3, reasoningTokens: undefined, cachedInputTokens: undefined, diff --git a/packages/ai/src/generate-text/run-tools-transformation.test.ts b/packages/ai/src/generate-text/run-tools-transformation.test.ts index 59f2630bb18c..fc9e32d15c57 100644 --- a/packages/ai/src/generate-text/run-tools-transformation.test.ts +++ b/packages/ai/src/generate-text/run-tools-transformation.test.ts @@ -1,4 +1,7 @@ -import { LanguageModelV3StreamPart } from '@ai-sdk/provider'; +import { + LanguageModelV3StreamPart, + LanguageModelV3Usage, +} from '@ai-sdk/provider'; import { delay, tool } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream, @@ -11,13 +14,20 @@ import { NoSuchToolError } from '../error/no-such-tool-error'; import { MockTracer } from '../test/mock-tracer'; import { runToolsTransformation } from './run-tools-transformation'; -const testUsage = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, - reasoningTokens: undefined, - cachedInputTokens: undefined, +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }; + describe('runToolsTransformation', () => { it('should forward text parts', async () => { const inputStream: ReadableStream = @@ -68,8 +78,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -143,8 +163,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -218,8 +248,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -297,8 +337,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -381,8 +431,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -514,8 +574,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -610,8 +680,18 @@ describe('runToolsTransformation', () => { "type": "finish", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, diff --git a/packages/ai/src/generate-text/run-tools-transformation.ts b/packages/ai/src/generate-text/run-tools-transformation.ts index 6fe39f8b1b3f..c83195ffcad7 100644 --- a/packages/ai/src/generate-text/run-tools-transformation.ts +++ b/packages/ai/src/generate-text/run-tools-transformation.ts @@ -1,4 +1,4 @@ -import { SharedV3Warning, LanguageModelV3StreamPart } from '@ai-sdk/provider'; +import { LanguageModelV3StreamPart, SharedV3Warning } from '@ai-sdk/provider'; import { getErrorMessage, IdGenerator, @@ -9,6 +9,7 @@ import { Tracer } from '@opentelemetry/api'; import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { FinishReason, LanguageModelUsage, ProviderMetadata } from '../types'; import { Source } from '../types/language-model'; +import { asLanguageModelUsage } from '../types/usage'; import { executeToolCall } from './execute-tool-call'; import { DefaultGeneratedFileWithType, GeneratedFile } from './generated-file'; import { isApprovalNeeded } from './is-approval-needed'; @@ -210,7 +211,7 @@ export function runToolsTransformation({ finishChunk = { type: 'finish', finishReason: chunk.finishReason, - usage: chunk.usage, + usage: asLanguageModelUsage(chunk.usage), providerMetadata: chunk.providerMetadata, }; break; diff --git a/packages/ai/src/generate-text/stream-text.test.ts b/packages/ai/src/generate-text/stream-text.test.ts index b635a13073a9..caad0f760afe 100644 --- a/packages/ai/src/generate-text/stream-text.test.ts +++ b/packages/ai/src/generate-text/stream-text.test.ts @@ -7,6 +7,7 @@ import { LanguageModelV3ProviderTool, LanguageModelV3StreamPart, SharedV3ProviderMetadata, + LanguageModelV3Usage, } from '@ai-sdk/provider'; import { delay, @@ -47,6 +48,10 @@ import { streamText, StreamTextOnFinishCallback } from './stream-text'; import { StreamTextResult, TextStreamPart } from './stream-text-result'; import { ToolSet } from './tool-set'; import { features } from 'node:process'; +import { + asLanguageModelUsage, + createNullLanguageModelUsage, +} from '../types/usage'; const defaultSettings = () => ({ @@ -59,20 +64,32 @@ const defaultSettings = () => onError: () => {}, }) as const; -const testUsage = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, - reasoningTokens: undefined, - cachedInputTokens: undefined, +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: undefined, + }, }; -const testUsage2 = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 23, - reasoningTokens: 10, - cachedInputTokens: 3, +const testUsage2: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: 0, + cacheWrite: 0, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: 10, + }, }; function createTestModel({ @@ -507,8 +524,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -517,7 +544,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -716,8 +752,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -726,7 +772,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -804,8 +859,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -814,7 +879,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -886,8 +960,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -896,7 +980,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -991,8 +1084,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1001,7 +1104,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -1240,21 +1352,40 @@ describe('streamText', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, }, { "finishReason": "tool-calls", "totalUsage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "type": "finish", }, @@ -1425,8 +1556,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -1435,7 +1576,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -1549,7 +1699,7 @@ describe('streamText', () => { expect.objectContaining({ finishReason: 'error', text: 'Hello', - usage: testUsage, + usage: asLanguageModelUsage(testUsage), }), ); }); @@ -3179,7 +3329,15 @@ describe('streamText', () => { { type: 'finish', finishReason: 'stop', - usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, + usage: { + inputTokens: { + total: 10, + noCache: 10, + cacheRead: 0, + cacheWrite: 0, + }, + outputTokens: { total: 5, text: 5, reasoning: 0 }, + }, }, ]), }), @@ -3787,8 +3945,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -3797,7 +3965,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -3889,8 +4066,18 @@ describe('streamText', () => { expect(await result.usage).toMatchInlineSnapshot(` { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, } @@ -4294,8 +4481,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -4368,8 +4565,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -4448,8 +4655,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -4979,8 +5196,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5015,15 +5242,34 @@ describe('streamText', () => { ], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5190,8 +5436,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5203,15 +5459,34 @@ describe('streamText', () => { "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5378,8 +5653,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5391,15 +5676,34 @@ describe('streamText', () => { "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5826,8 +6130,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -5870,21 +6184,40 @@ describe('streamText', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, }, "type": "finish", }, @@ -6051,8 +6384,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -6124,11 +6467,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -6137,18 +6490,37 @@ describe('streamText', () => { "toolCalls": [], "toolResults": [], "totalUsage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], } @@ -6236,8 +6608,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -6309,11 +6691,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -6330,25 +6722,43 @@ describe('streamText', () => { it('result.totalUsage should contain total token usage', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, } `); }); it('result.usage should contain token usage from final step', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` - { - "cachedInputTokens": 3, - "inputTokens": 6, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 36, - } - `); + { + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, + "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, + "outputTokens": 20, + "reasoningTokens": 10, + "totalTokens": 26, + } + `); }); it('result.finishReason should contain finish reason from final step', async () => { @@ -6440,8 +6850,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -6513,11 +6933,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -6971,8 +7401,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -7039,11 +7479,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -7155,8 +7605,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -7223,11 +7683,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -7408,8 +7878,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -7452,21 +7932,40 @@ describe('streamText', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, }, "type": "finish", }, @@ -7633,8 +8132,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -7706,11 +8215,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -7719,18 +8238,37 @@ describe('streamText', () => { "toolCalls": [], "toolResults": [], "totalUsage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], } @@ -7818,8 +8356,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -7891,11 +8439,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -7908,25 +8466,43 @@ describe('streamText', () => { it('result.totalUsage should contain total token usage', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, "outputTokens": 20, "reasoningTokens": 10, - "totalTokens": 36, + "totalTokens": 26, } `); }); it('result.usage should contain token usage from final step', async () => { expect(await result.totalUsage).toMatchInlineSnapshot(` - { - "cachedInputTokens": 3, - "inputTokens": 6, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 36, - } - `); + { + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 6, + }, + "inputTokens": 6, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 20, + }, + "outputTokens": 20, + "reasoningTokens": 10, + "totalTokens": 26, + } + `); }); it('result.finishReason should contain finish reason from final step', async () => { @@ -8018,8 +8594,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -8091,11 +8677,21 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:01.000Z, }, "usage": { - "cachedInputTokens": 3, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": 10, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 10, - "totalTokens": 23, + "totalTokens": 13, }, "warnings": [], }, @@ -8168,11 +8764,11 @@ describe('streamText', () => { "ai.response.finishReason": "stop", "ai.response.text": "Hello, world!", "ai.settings.maxRetries": 2, - "ai.usage.cachedInputTokens": 3, + "ai.usage.cachedInputTokens": 0, "ai.usage.inputTokens": 6, "ai.usage.outputTokens": 20, "ai.usage.reasoningTokens": 10, - "ai.usage.totalTokens": 36, + "ai.usage.totalTokens": 26, "operation.name": "ai.streamText", }, "events": [], @@ -8257,11 +8853,11 @@ describe('streamText', () => { "ai.response.text": "Hello, world!", "ai.response.timestamp": "1970-01-01T00:00:01.000Z", "ai.settings.maxRetries": 2, - "ai.usage.cachedInputTokens": 3, + "ai.usage.cachedInputTokens": 0, "ai.usage.inputTokens": 3, "ai.usage.outputTokens": 10, "ai.usage.reasoningTokens": 10, - "ai.usage.totalTokens": 23, + "ai.usage.totalTokens": 13, "gen_ai.request.model": "mock-model-id", "gen_ai.response.finish_reasons": [ "stop", @@ -8535,8 +9131,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -8625,8 +9231,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -8889,8 +9505,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -8899,7 +9525,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -9116,8 +9751,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -9126,7 +9771,16 @@ describe('streamText', () => { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -9923,8 +10577,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -9933,7 +10597,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -10013,8 +10686,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -10219,7 +10902,16 @@ describe('streamText', () => { if (chunk.type === 'finish') { chunk.totalUsage = { inputTokens: 200, + inputTokenDetails: { + noCacheTokens: 200, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, outputTokens: 300, + outputTokenDetails: { + reasoningTokens: undefined, + textTokens: 300, + }, totalTokens: undefined, reasoningTokens: undefined, cachedInputTokens: undefined, @@ -10231,13 +10923,24 @@ describe('streamText', () => { prompt: 'test-input', }); - expect(await result.totalUsage).toStrictEqual({ - inputTokens: 200, - outputTokens: 300, - totalTokens: undefined, - reasoningTokens: undefined, - cachedInputTokens: undefined, - }); + expect(await result.totalUsage).toMatchInlineSnapshot(` + { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 200, + }, + "inputTokens": 200, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 300, + }, + "outputTokens": 300, + "reasoningTokens": undefined, + "totalTokens": undefined, + } + `); }); it('result.finishReason should be transformed', async () => { @@ -10479,8 +11182,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -10805,8 +11518,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -10841,15 +11564,34 @@ describe('streamText', () => { ], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -10992,8 +11734,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -11254,13 +12006,7 @@ describe('streamText', () => { type: 'finish-step', finishReason: 'stop', providerMetadata: undefined, - usage: { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - reasoningTokens: undefined, - cachedInputTokens: undefined, - }, + usage: createNullLanguageModelUsage(), response: { id: 'response-id', modelId: 'mock-model-id', @@ -11271,13 +12017,7 @@ describe('streamText', () => { controller.enqueue({ type: 'finish', finishReason: 'stop', - totalUsage: { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - reasoningTokens: undefined, - cachedInputTokens: undefined, - }, + totalUsage: createNullLanguageModelUsage(), }); return; @@ -11300,11 +12040,17 @@ describe('streamText', () => { type: 'finish', finishReason: 'stop', usage: { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - reasoningTokens: undefined, - cachedInputTokens: undefined, + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, }, }, ]), @@ -11344,20 +12090,36 @@ describe('streamText', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": undefined, + }, "inputTokens": undefined, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": undefined, + }, "outputTokens": undefined, - "reasoningTokens": undefined, + "raw": undefined, "totalTokens": undefined, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": undefined, + }, "inputTokens": undefined, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": undefined, + }, "outputTokens": undefined, - "reasoningTokens": undefined, + "raw": undefined, "totalTokens": undefined, }, "type": "finish", @@ -11425,10 +12187,18 @@ describe('streamText', () => { "timestamp": 1970-01-01T00:00:00.000Z, }, "usage": { - "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": undefined, + }, "inputTokens": undefined, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": undefined, + }, "outputTokens": undefined, - "reasoningTokens": undefined, + "raw": undefined, "totalTokens": undefined, }, "warnings": [], @@ -11887,8 +12657,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -11900,15 +12680,34 @@ describe('streamText', () => { "toolResults": [], "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -12779,8 +13578,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -12789,7 +13598,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -12891,8 +13709,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -13197,8 +14025,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -13255,8 +14093,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -13700,8 +14548,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -13710,7 +14568,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -13918,8 +14785,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -13928,7 +14805,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -14105,8 +14991,18 @@ describe('streamText', () => { }, "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -14266,8 +15162,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -14276,7 +15182,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -14621,8 +15536,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -14631,7 +15556,16 @@ describe('streamText', () => { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -14858,8 +15792,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -14868,7 +15812,16 @@ describe('streamText', () => { "finishReason": "tool-calls", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -15266,8 +16219,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -15276,7 +16239,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -15563,8 +16535,18 @@ describe('streamText', () => { "type": "finish-step", "usage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": undefined, "totalTokens": 13, }, @@ -15573,7 +16555,16 @@ describe('streamText', () => { "finishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, @@ -15806,65 +16797,84 @@ describe('streamText', () => { it('should include the tool denied in the full stream', async () => { expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` - [ - { - "type": "start", - }, - { - "toolCallId": "call-1", - "toolName": "tool1", - "type": "tool-output-denied", - }, - { - "request": {}, - "type": "start-step", - "warnings": [], - }, - { - "id": "1", - "type": "text-start", - }, - { - "id": "1", - "providerMetadata": undefined, - "text": "Hello, world!", - "type": "text-delta", - }, - { - "id": "1", - "type": "text-end", - }, - { - "finishReason": "stop", - "providerMetadata": undefined, - "response": { - "headers": undefined, - "id": "id-0", - "modelId": "mock-model-id", - "timestamp": 1970-01-01T00:00:00.000Z, + [ + { + "type": "start", }, - "type": "finish-step", - "usage": { - "cachedInputTokens": undefined, - "inputTokens": 3, - "outputTokens": 10, - "reasoningTokens": undefined, - "totalTokens": 13, + { + "toolCallId": "call-1", + "toolName": "tool1", + "type": "tool-output-denied", }, - }, - { - "finishReason": "stop", - "totalUsage": { - "cachedInputTokens": undefined, - "inputTokens": 3, - "outputTokens": 10, - "reasoningTokens": undefined, - "totalTokens": 13, + { + "request": {}, + "type": "start-step", + "warnings": [], }, - "type": "finish", - }, - ] - `); + { + "id": "1", + "type": "text-start", + }, + { + "id": "1", + "providerMetadata": undefined, + "text": "Hello, world!", + "type": "text-delta", + }, + { + "id": "1", + "type": "text-end", + }, + { + "finishReason": "stop", + "providerMetadata": undefined, + "response": { + "headers": undefined, + "id": "id-0", + "modelId": "mock-model-id", + "timestamp": 1970-01-01T00:00:00.000Z, + }, + "type": "finish-step", + "usage": { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, + "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, + "outputTokens": 10, + "raw": undefined, + "reasoningTokens": undefined, + "totalTokens": 13, + }, + }, + { + "finishReason": "stop", + "totalUsage": { + "cachedInputTokens": undefined, + "inputTokenDetails": { + "cacheReadTokens": undefined, + "cacheWriteTokens": undefined, + "noCacheTokens": 3, + }, + "inputTokens": 3, + "outputTokenDetails": { + "reasoningTokens": undefined, + "textTokens": 10, + }, + "outputTokens": 10, + "reasoningTokens": undefined, + "totalTokens": 13, + }, + "type": "finish", + }, + ] + `); }); it('should include the tool denied in the ui message stream', async () => { diff --git a/packages/ai/src/generate-text/stream-text.ts b/packages/ai/src/generate-text/stream-text.ts index 7f85024ee86d..cd000b5346fd 100644 --- a/packages/ai/src/generate-text/stream-text.ts +++ b/packages/ai/src/generate-text/stream-text.ts @@ -40,7 +40,11 @@ import { ToolChoice, } from '../types/language-model'; import { ProviderMetadata } from '../types/provider-metadata'; -import { addLanguageModelUsage, LanguageModelUsage } from '../types/usage'; +import { + addLanguageModelUsage, + createNullLanguageModelUsage, + LanguageModelUsage, +} from '../types/usage'; import { UIMessage } from '../ui'; import { createUIMessageStreamResponse } from '../ui-message-stream/create-ui-message-stream-response'; import { getResponseUIMessageId } from '../ui-message-stream/get-response-ui-message-id'; @@ -894,11 +898,8 @@ class DefaultStreamTextResult // derived: const finishReason = recordedFinishReason ?? 'unknown'; - const totalUsage = recordedTotalUsage ?? { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + const totalUsage = + recordedTotalUsage ?? createNullLanguageModelUsage(); // from finish: self._finishReason.resolve(finishReason); @@ -1296,11 +1297,7 @@ class DefaultStreamTextResult const activeToolCallToolNames: Record = {}; let stepFinishReason: FinishReason = 'unknown'; - let stepUsage: LanguageModelUsage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let stepUsage: LanguageModelUsage = createNullLanguageModelUsage(); let stepProviderMetadata: ProviderMetadata | undefined; let stepFirstChunk = true; let stepResponse: { id: string; timestamp: Date; modelId: string } = { @@ -1638,11 +1635,7 @@ class DefaultStreamTextResult await streamStep({ currentStep: 0, responseMessages: initialResponseMessages, - usage: { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }, + usage: createNullLanguageModelUsage(), }); }, }).catch(error => { diff --git a/packages/ai/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap b/packages/ai/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap index 94b83f34af52..1c0b9f00bca3 100644 --- a/packages/ai/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap +++ b/packages/ai/src/middleware/__snapshots__/simulate-streaming-middleware.test.ts.snap @@ -21,21 +21,40 @@ exports[`simulateStreamingMiddleware > should handle empty text response 1`] = ` }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, diff --git a/packages/ai/src/middleware/extract-reasoning-middleware.test.ts b/packages/ai/src/middleware/extract-reasoning-middleware.test.ts index 316b69402c2b..7bcfab8ad20f 100644 --- a/packages/ai/src/middleware/extract-reasoning-middleware.test.ts +++ b/packages/ai/src/middleware/extract-reasoning-middleware.test.ts @@ -1,19 +1,26 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; import { convertArrayToReadableStream, convertAsyncIterableToArray, } from '@ai-sdk/provider-utils/test'; +import { describe, expect, it } from 'vitest'; import { generateText, streamText } from '../generate-text'; import { wrapLanguageModel } from '../middleware/wrap-language-model'; import { MockLanguageModelV3 } from '../test/mock-language-model-v3'; import { extractReasoningMiddleware } from './extract-reasoning-middleware'; -import { describe, it, expect } from 'vitest'; - -const testUsage = { - inputTokens: 5, - outputTokens: 10, - totalTokens: 18, - reasoningTokens: 3, - cachedInputTokens: undefined, + +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 5, + noCache: 5, + cacheRead: 0, + cacheWrite: 0, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: 3, + }, }; describe('extractReasoningMiddleware', () => { @@ -340,21 +347,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -471,21 +497,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -579,21 +624,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -704,21 +768,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -780,21 +863,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -870,21 +972,40 @@ describe('extractReasoningMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, diff --git a/packages/ai/src/middleware/simulate-streaming-middleware.test.ts b/packages/ai/src/middleware/simulate-streaming-middleware.test.ts index a2f5d30bed59..86ae54bc6c4b 100644 --- a/packages/ai/src/middleware/simulate-streaming-middleware.test.ts +++ b/packages/ai/src/middleware/simulate-streaming-middleware.test.ts @@ -1,3 +1,4 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; import { jsonSchema, tool } from '@ai-sdk/provider-utils'; import { convertAsyncIterableToArray, @@ -19,12 +20,18 @@ const DEFAULT_SETTINGs = { }, }; -const testUsage = { - inputTokens: 5, - outputTokens: 10, - totalTokens: 18, - reasoningTokens: 3, - cachedInputTokens: undefined, +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 5, + noCache: 5, + cacheRead: 0, + cacheWrite: 0, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: 3, + }, }; describe('simulateStreamingMiddleware', () => { @@ -96,21 +103,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -196,21 +222,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -342,21 +387,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -470,21 +534,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -601,21 +684,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "tool-calls", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, @@ -684,21 +786,40 @@ describe('simulateStreamingMiddleware', () => { }, "type": "finish-step", "usage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, + "raw": undefined, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, }, { "finishReason": "stop", "totalUsage": { - "cachedInputTokens": undefined, + "cachedInputTokens": 0, + "inputTokenDetails": { + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "noCacheTokens": 5, + }, "inputTokens": 5, + "outputTokenDetails": { + "reasoningTokens": 3, + "textTokens": 10, + }, "outputTokens": 10, "reasoningTokens": 3, - "totalTokens": 18, + "totalTokens": 15, }, "type": "finish", }, diff --git a/packages/ai/src/model/as-language-model-v3.test.ts b/packages/ai/src/model/as-language-model-v3.test.ts index 353585cdfddb..28f3b68e260f 100644 --- a/packages/ai/src/model/as-language-model-v3.test.ts +++ b/packages/ai/src/model/as-language-model-v3.test.ts @@ -1,9 +1,13 @@ import { LanguageModelV2 } from '@ai-sdk/provider'; -import { asLanguageModelV3 } from './as-language-model-v3'; -import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; -import { MockLanguageModelV3 } from '../test/mock-language-model-v3'; +import { + convertArrayToReadableStream, + convertReadableStreamToArray, +} from '@ai-sdk/provider-utils/test'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as logWarningsModule from '../logger/log-warnings'; +import { MockLanguageModelV2 } from '../test/mock-language-model-v2'; +import { MockLanguageModelV3 } from '../test/mock-language-model-v3'; +import { asLanguageModelV3 } from './as-language-model-v3'; describe('asLanguageModelV3', () => { let logWarningSpy: ReturnType; @@ -159,24 +163,87 @@ describe('asLanguageModelV3', () => { expect(response.content).toHaveLength(1); expect(response.finishReason).toBe('stop'); - expect(response.usage.inputTokens).toBe(10); + expect(response.usage.inputTokens.total).toBe(10); + expect(response.usage.outputTokens.total).toBe(5); }); - it('should make doStream method callable', async () => { - const mockStream = new ReadableStream(); - const v2Model = new MockLanguageModelV2({ - provider: 'test-provider', - modelId: 'test-model-id', - doStream: async () => ({ stream: mockStream }), - }); + describe('doStream', () => { + it('should convert v2 stream to v3 stream', async () => { + const v2Model = new MockLanguageModelV2({ + doStream: async ({ prompt }) => { + return { + stream: convertArrayToReadableStream([ + { type: 'text-start', id: '1' }, + { type: 'text-delta', id: '1', delta: 'Hello' }, + { type: 'text-delta', id: '1', delta: ', ' }, + { type: 'text-delta', id: '1', delta: `world!` }, + { type: 'text-end', id: '1' }, + { + type: 'finish', + finishReason: 'stop', + usage: { + inputTokens: 3, + outputTokens: 10, + totalTokens: 13, + reasoningTokens: 2, + cachedInputTokens: 4, + }, + }, + ]), + }; + }, + }); - const result = asLanguageModelV3(v2Model); + const { stream } = await asLanguageModelV3(v2Model).doStream({ + prompt: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }], + }); - const { stream } = await result.doStream({ - prompt: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }], + expect(await convertReadableStreamToArray(stream)) + .toMatchInlineSnapshot(` + [ + { + "id": "1", + "type": "text-start", + }, + { + "delta": "Hello", + "id": "1", + "type": "text-delta", + }, + { + "delta": ", ", + "id": "1", + "type": "text-delta", + }, + { + "delta": "world!", + "id": "1", + "type": "text-delta", + }, + { + "id": "1", + "type": "text-end", + }, + { + "finishReason": "stop", + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 4, + "cacheWrite": undefined, + "noCache": undefined, + "total": 3, + }, + "outputTokens": { + "reasoning": 2, + "text": undefined, + "total": 10, + }, + }, + }, + ] + `); }); - - expect(stream).toBe(mockStream); }); it('should preserve prototype methods when using class instances', async () => { @@ -317,7 +384,7 @@ describe('asLanguageModelV3', () => { prompt: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }], }); - expect(response.usage.reasoningTokens).toBe(5); + expect(response.usage?.outputTokens?.reasoning).toBe(5); }); it('should handle response with cached input tokens in usage', async () => { @@ -343,7 +410,7 @@ describe('asLanguageModelV3', () => { prompt: [{ role: 'user', content: [{ type: 'text', text: 'test' }] }], }); - expect(response.usage.cachedInputTokens).toBe(8); + expect(response.usage?.inputTokens?.cacheRead).toBe(8); }); it('should handle response with different finish reasons', async () => { diff --git a/packages/ai/src/model/as-language-model-v3.ts b/packages/ai/src/model/as-language-model-v3.ts index 7ad4976c18bf..f3751ce56be7 100644 --- a/packages/ai/src/model/as-language-model-v3.ts +++ b/packages/ai/src/model/as-language-model-v3.ts @@ -1,4 +1,11 @@ -import { LanguageModelV2, LanguageModelV3 } from '@ai-sdk/provider'; +import { + LanguageModelV2, + LanguageModelV2StreamPart, + LanguageModelV2Usage, + LanguageModelV3, + LanguageModelV3StreamPart, + LanguageModelV3Usage, +} from '@ai-sdk/provider'; import { logV2CompatibilityWarning } from '../util/log-v2-compatibility-warning'; export function asLanguageModelV3( @@ -17,8 +24,67 @@ export function asLanguageModelV3( // and support all relevant v3 properties: return new Proxy(model, { get(target, prop: keyof LanguageModelV2) { - if (prop === 'specificationVersion') return 'v3'; - return target[prop]; + switch (prop) { + case 'specificationVersion': + return 'v3'; + case 'doGenerate': + return async (...args: Parameters) => { + const result = await target.doGenerate(...args); + return { + ...result, + usage: convertV2UsageToV3(result.usage), + }; + }; + case 'doStream': + return async (...args: Parameters) => { + const result = await target.doStream(...args); + return { + ...result, + stream: convertV2StreamToV3(result.stream), + }; + }; + default: + return target[prop]; + } }, }) as unknown as LanguageModelV3; } + +function convertV2StreamToV3( + stream: ReadableStream, +): ReadableStream { + return stream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + switch (chunk.type) { + case 'finish': + controller.enqueue({ + ...chunk, + usage: convertV2UsageToV3(chunk.usage), + }); + break; + default: + // TODO: AI SDK 6 - no casting (stream parts need to be mapped) + controller.enqueue(chunk as LanguageModelV3StreamPart); + break; + } + }, + }), + ); +} + +function convertV2UsageToV3(usage: LanguageModelV2Usage): LanguageModelV3Usage { + return { + inputTokens: { + total: usage.inputTokens, + noCache: undefined, + cacheRead: usage.cachedInputTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.outputTokens, + text: undefined, + reasoning: usage.reasoningTokens, + }, + }; +} diff --git a/packages/ai/src/types/usage.ts b/packages/ai/src/types/usage.ts index 16a868e883fc..ad1e843b8724 100644 --- a/packages/ai/src/types/usage.ts +++ b/packages/ai/src/types/usage.ts @@ -1,10 +1,81 @@ -import { LanguageModelV3Usage } from '@ai-sdk/provider'; -import { ImageModelV3Usage } from '@ai-sdk/provider'; +import { + ImageModelV3Usage, + JSONObject, + LanguageModelV3Usage, +} from '@ai-sdk/provider'; /** -Represents the number of tokens used in a prompt and completion. + * Represents the number of tokens used in a prompt and completion. */ -export type LanguageModelUsage = LanguageModelV3Usage; +export type LanguageModelUsage = { + /** + * The total number of input (prompt) tokens used. + */ + inputTokens: number | undefined; + + /** + * Detailed information about the input tokens. + */ + inputTokenDetails: { + /** + * The number of non-cached input (prompt) tokens used. + */ + noCacheTokens: number | undefined; + + /** + * The number of cached input (prompt) tokens read. + */ + cacheReadTokens: number | undefined; + + /** + * The number of cached input (prompt) tokens written. + */ + cacheWriteTokens: number | undefined; + }; + + /** + * The number of total output (completion) tokens used. + */ + outputTokens: number | undefined; + + /** + * Detailed information about the output tokens. + */ + outputTokenDetails: { + /** + * The number of text tokens used. + */ + textTokens: number | undefined; + + /** + * The number of reasoning tokens used. + */ + reasoningTokens: number | undefined; + }; + + /** + * The total number of tokens used. + */ + totalTokens: number | undefined; + + /** + * @deprecated Use outputTokenDetails.reasoning instead. + */ + reasoningTokens?: number | undefined; + + /** + * @deprecated Use inputTokenDetails.cacheRead instead. + */ + cachedInputTokens?: number | undefined; + + /** + * Raw usage information from the provider. + * + * This is the usage information in the shape that the provider returns. + * It can include additional information that is not part of the standard usage information. + */ + raw?: JSONObject; +}; /** Represents the number of tokens used in an embedding. @@ -17,13 +88,80 @@ The number of tokens used in the embedding. tokens: number; }; +export function asLanguageModelUsage( + usage: LanguageModelV3Usage, +): LanguageModelUsage { + return { + inputTokens: usage.inputTokens.total, + inputTokenDetails: { + noCacheTokens: usage.inputTokens.noCache, + cacheReadTokens: usage.inputTokens.cacheRead, + cacheWriteTokens: usage.inputTokens.cacheWrite, + }, + outputTokens: usage.outputTokens.total, + outputTokenDetails: { + textTokens: usage.outputTokens.text, + reasoningTokens: usage.outputTokens.reasoning, + }, + totalTokens: addTokenCounts( + usage.inputTokens.total, + usage.outputTokens.total, + ), + raw: usage.raw, + reasoningTokens: usage.outputTokens.reasoning, + cachedInputTokens: usage.inputTokens.cacheRead, + }; +} + +export function createNullLanguageModelUsage(): LanguageModelUsage { + return { + inputTokens: undefined, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokens: undefined, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, + totalTokens: undefined, + raw: undefined, + }; +} + export function addLanguageModelUsage( usage1: LanguageModelUsage, usage2: LanguageModelUsage, ): LanguageModelUsage { return { inputTokens: addTokenCounts(usage1.inputTokens, usage2.inputTokens), + inputTokenDetails: { + noCacheTokens: addTokenCounts( + usage1.inputTokenDetails?.noCacheTokens, + usage2.inputTokenDetails?.noCacheTokens, + ), + cacheReadTokens: addTokenCounts( + usage1.inputTokenDetails?.cacheReadTokens, + usage2.inputTokenDetails?.cacheReadTokens, + ), + cacheWriteTokens: addTokenCounts( + usage1.inputTokenDetails?.cacheWriteTokens, + usage2.inputTokenDetails?.cacheWriteTokens, + ), + }, outputTokens: addTokenCounts(usage1.outputTokens, usage2.outputTokens), + outputTokenDetails: { + textTokens: addTokenCounts( + usage1.outputTokenDetails?.textTokens, + usage2.outputTokenDetails?.textTokens, + ), + reasoningTokens: addTokenCounts( + usage1.outputTokenDetails?.reasoningTokens, + usage2.outputTokenDetails?.reasoningTokens, + ), + }, totalTokens: addTokenCounts(usage1.totalTokens, usage2.totalTokens), reasoningTokens: addTokenCounts( usage1.reasoningTokens, diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts index 5753329891c0..18b15f71851f 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts @@ -283,10 +283,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 34, - "totalTokens": 38, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 34, + "total": 34, + }, + "raw": { + "inputTokens": 4, + "outputTokens": 34, + "totalTokens": 38, + }, }, }, ] @@ -383,9 +395,18 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -541,9 +562,18 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -590,9 +620,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -639,9 +678,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -688,9 +736,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -737,9 +794,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -773,9 +839,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -914,10 +989,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 34, - "totalTokens": 38, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 34, + "total": 34, + }, + "raw": { + "inputTokens": 4, + "outputTokens": 34, + "totalTokens": 38, + }, }, }, ] @@ -1138,10 +1225,24 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 2, - "inputTokens": 4, - "outputTokens": 34, - "totalTokens": 38, + "inputTokens": { + "cacheRead": 2, + "cacheWrite": 3, + "noCache": 2, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 34, + "total": 34, + }, + "raw": { + "cacheReadInputTokens": 2, + "cacheWriteInputTokens": 3, + "inputTokens": 4, + "outputTokens": 34, + "totalTokens": 38, + }, }, }, ] @@ -1276,9 +1377,18 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1346,9 +1456,18 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1417,9 +1536,18 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1558,9 +1686,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1631,9 +1768,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1705,10 +1851,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 100, - "outputTokens": 20, - "totalTokens": 120, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 100, + "total": 100, + }, + "outputTokens": { + "reasoning": undefined, + "text": 20, + "total": 20, + }, + "raw": { + "inputTokens": 100, + "outputTokens": 20, + "totalTokens": 120, + }, }, }, ] @@ -1792,9 +1950,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1898,9 +2065,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2031,9 +2207,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2166,10 +2351,22 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 500, - "outputTokens": 100, - "totalTokens": 600, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 500, + "total": 500, + }, + "outputTokens": { + "reasoning": undefined, + "text": 100, + "total": 100, + }, + "raw": { + "inputTokens": 500, + "outputTokens": 100, + "totalTokens": 600, + }, }, }, ] @@ -2325,10 +2522,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 34, - "totalTokens": 38, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 34, + "total": 34, + }, + "raw": { + "inputTokens": 4, + "outputTokens": 34, + "totalTokens": 38, + }, } `); }); @@ -2908,10 +3117,24 @@ describe('doGenerate', () => { `); expect(response.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 2, - "inputTokens": 4, - "outputTokens": 34, - "totalTokens": 38, + "inputTokens": { + "cacheRead": 2, + "cacheWrite": 3, + "noCache": 2, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 34, + "total": 34, + }, + "raw": { + "cacheReadInputTokens": 2, + "cacheWriteInputTokens": 3, + "inputTokens": 4, + "outputTokens": 34, + "totalTokens": 38, + }, } `); }); diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index f23f02d1087d..c7cb552c1b21 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -6,7 +6,6 @@ import { LanguageModelV3FinishReason, LanguageModelV3Reasoning, LanguageModelV3StreamPart, - LanguageModelV3Usage, SharedV3ProviderMetadata, LanguageModelV3FunctionTool, } from '@ai-sdk/provider'; @@ -32,6 +31,7 @@ import { bedrockProviderOptions, } from './bedrock-chat-options'; import { BedrockErrorSchema } from './bedrock-error'; +import { BedrockUsage, convertBedrockUsage } from './convert-bedrock-usage'; import { createBedrockEventStreamResponseHandler } from './bedrock-event-stream-response-handler'; import { prepareTools } from './bedrock-prepare-tools'; import { convertToBedrockChatMessages } from './convert-to-bedrock-chat-messages'; @@ -453,12 +453,7 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { response.stopReason as BedrockStopReason, isJsonResponseFromTool, ), - usage: { - inputTokens: response.usage?.inputTokens, - outputTokens: response.usage?.outputTokens, - totalTokens: response.usage?.inputTokens + response.usage?.outputTokens, - cachedInputTokens: response.usage?.cacheReadInputTokens ?? undefined, - }, + usage: convertBedrockUsage(response.usage), response: { // TODO add id, timestamp, etc headers: responseHeaders, @@ -493,11 +488,7 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: BedrockUsage | undefined = undefined; let providerMetadata: SharedV3ProviderMetadata | undefined = undefined; let isJsonResponseFromTool = false; @@ -568,15 +559,9 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { } if (value.metadata) { - usage.inputTokens = - value.metadata.usage?.inputTokens ?? usage.inputTokens; - usage.outputTokens = - value.metadata.usage?.outputTokens ?? usage.outputTokens; - usage.totalTokens = - (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0); - usage.cachedInputTokens = - value.metadata.usage?.cacheReadInputTokens ?? - usage.cachedInputTokens; + if (value.metadata.usage) { + usage = value.metadata.usage; + } const cacheUsage = value.metadata.usage?.cacheWriteInputTokens != null @@ -813,7 +798,7 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertBedrockUsage(usage), ...(providerMetadata && { providerMetadata }), }); }, diff --git a/packages/amazon-bedrock/src/convert-bedrock-usage.ts b/packages/amazon-bedrock/src/convert-bedrock-usage.ts new file mode 100644 index 000000000000..6acabadbbfac --- /dev/null +++ b/packages/amazon-bedrock/src/convert-bedrock-usage.ts @@ -0,0 +1,50 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type BedrockUsage = { + inputTokens: number; + outputTokens: number; + totalTokens?: number; + cacheReadInputTokens?: number | null; + cacheWriteInputTokens?: number | null; +}; + +export function convertBedrockUsage( + usage: BedrockUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const inputTokens = usage.inputTokens; + const outputTokens = usage.outputTokens; + const cacheReadTokens = usage.cacheReadInputTokens ?? 0; + const cacheWriteTokens = usage.cacheWriteInputTokens ?? 0; + + return { + inputTokens: { + total: inputTokens, + noCache: inputTokens - cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: cacheWriteTokens, + }, + outputTokens: { + total: outputTokens, + text: outputTokens, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap b/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap index 0c4b604ae910..50a32ad9ffde 100644 --- a/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap +++ b/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap @@ -618,10 +618,32 @@ Both files have been exported and are ready for download!", "modelId": "claude-sonnet-4-5-20250929", }, "usage": { - "cachedInputTokens": 0, - "inputTokens": 12546, - "outputTokens": 1359, - "totalTokens": 13905, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 12546, + "total": 12546, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 1359, + }, + "raw": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0, + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 12546, + "output_tokens": 1359, + "server_tool_use": { + "web_fetch_requests": 0, + "web_search_requests": 0, + }, + "service_tier": "standard", + }, }, "warnings": [], } @@ -4909,10 +4931,23 @@ The presentation has", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 2751, - "outputTokens": 5558, - "totalTokens": 8309, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 2751, + "total": 2751, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 5558, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 2751, + "output_tokens": 5558, + }, }, }, ] @@ -9953,10 +9988,23 @@ Both", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 2273, - "outputTokens": 2479, - "totalTokens": 4752, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 2273, + "total": 2273, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 2479, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 2273, + "output_tokens": 2479, + }, }, }, { @@ -11251,10 +11299,23 @@ The Fibonacci sequence starts with 0, ", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 2263, - "outputTokens": 771, - "totalTokens": 3034, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 2263, + "total": 2263, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 771, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 2263, + "output_tokens": 771, + }, }, }, ] @@ -11872,10 +11933,23 @@ exports[`AnthropicMessagesLanguageModel > doStream > json schema response format }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 313, - "outputTokens": 305, - "totalTokens": 618, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 313, + "total": 313, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 305, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 313, + "output_tokens": 305, + }, }, }, ] @@ -11977,10 +12051,23 @@ It simply echoed back", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 589, - "outputTokens": 83, - "totalTokens": 672, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 589, + "total": 589, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 83, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 589, + "output_tokens": 83, + }, }, }, ] @@ -12168,10 +12255,23 @@ exports[`AnthropicMessagesLanguageModel > doStream > tool search tool > bm25 var }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 1040, - "outputTokens": 41, - "totalTokens": 1081, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 699, + "total": 699, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 158, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 1040, + "output_tokens": 41, + }, }, }, { @@ -12253,10 +12353,23 @@ exports[`AnthropicMessagesLanguageModel > doStream > tool search tool > bm25 var }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 1040, - "outputTokens": 41, - "totalTokens": 1081, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 1040, + "total": 1040, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 41, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 1040, + "output_tokens": 41, + }, }, }, ] @@ -12446,10 +12559,23 @@ exports[`AnthropicMessagesLanguageModel > doStream > tool search tool > regex va }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 1071, - "outputTokens": 67, - "totalTokens": 1138, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 722, + "total": 722, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 163, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 1071, + "output_tokens": 67, + }, }, }, { @@ -12560,10 +12686,23 @@ The weather in", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 1071, - "outputTokens": 67, - "totalTokens": 1138, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 1071, + "total": 1071, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 67, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 1071, + "output_tokens": 67, + }, }, }, ] @@ -12960,10 +13099,23 @@ The page also discusses", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 868, - "outputTokens": 446, - "totalTokens": 1314, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 868, + "total": 868, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 446, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 868, + "output_tokens": 446, + }, }, }, ] @@ -13695,10 +13847,23 @@ The main tech story today appears to be Apple's significant", }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 2037, - "outputTokens": 795, - "totalTokens": 2832, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 2037, + "total": 2037, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 795, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 2037, + "output_tokens": 795, + }, }, }, ] diff --git a/packages/anthropic/src/anthropic-messages-language-model.test.ts b/packages/anthropic/src/anthropic-messages-language-model.test.ts index 63119f579c6a..be0ce707b1c6 100644 --- a/packages/anthropic/src/anthropic-messages-language-model.test.ts +++ b/packages/anthropic/src/anthropic-messages-language-model.test.ts @@ -680,10 +680,21 @@ describe('AnthropicMessagesLanguageModel', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 5, + }, + "raw": { + "input_tokens": 20, + "output_tokens": 5, + }, } `); }); @@ -1183,10 +1194,23 @@ describe('AnthropicMessagesLanguageModel', () => { "modelId": "claude-3-haiku-20240307", }, "usage": { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 50, - "totalTokens": 70, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": 10, + "noCache": 20, + "total": 35, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 50, + }, + "raw": { + "cache_creation_input_tokens": 10, + "cache_read_input_tokens": 5, + "input_tokens": 20, + "output_tokens": 50, + }, }, "warnings": [], } @@ -1329,10 +1353,27 @@ describe('AnthropicMessagesLanguageModel', () => { "modelId": "claude-3-haiku-20240307", }, "usage": { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 50, - "totalTokens": 70, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": 10, + "noCache": 20, + "total": 35, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 50, + }, + "raw": { + "cache_creation": { + "ephemeral_1h_input_tokens": 10, + "ephemeral_5m_input_tokens": 0, + }, + "cache_creation_input_tokens": 10, + "cache_read_input_tokens": 5, + "input_tokens": 20, + "output_tokens": 50, + }, }, "warnings": [], } @@ -3598,10 +3639,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 849, - "outputTokens": 47, - "totalTokens": 896, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 849, + "total": 849, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 47, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 849, + "output_tokens": 47, + }, }, }, ] @@ -3731,10 +3785,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 849, - "outputTokens": 47, - "totalTokens": 896, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 849, + "total": 849, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 47, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 849, + "output_tokens": 47, + }, }, }, ] @@ -3909,10 +3976,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 843, - "outputTokens": 28, - "totalTokens": 871, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 843, + "total": 843, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 28, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 843, + "output_tokens": 28, + }, }, }, ] @@ -4088,10 +4168,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4188,10 +4281,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4270,10 +4376,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4338,10 +4457,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4474,10 +4606,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 441, - "outputTokens": 65, - "totalTokens": 506, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 441, + "total": 441, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 65, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 441, + "output_tokens": 65, + }, }, }, ] @@ -4717,10 +4862,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 5, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": 10, + "noCache": 17, + "total": 32, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 10, + "cache_read_input_tokens": 5, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4796,10 +4954,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 5, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": 10, + "noCache": 17, + "total": 32, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 10, + "cache_read_input_tokens": 5, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4895,10 +5066,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -4944,10 +5128,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -5180,10 +5377,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 227, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 17, + "output_tokens": 227, + }, }, }, ] @@ -5373,10 +5583,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 441, - "outputTokens": 65, - "totalTokens": 506, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 441, + "total": 441, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 65, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 441, + "output_tokens": 65, + }, }, }, ] @@ -5472,10 +5695,23 @@ describe('AnthropicMessagesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 565, - "outputTokens": 48, - "totalTokens": 613, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": 0, + "noCache": 565, + "total": 565, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": 48, + }, + "raw": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 565, + "output_tokens": 48, + }, }, }, ] diff --git a/packages/anthropic/src/anthropic-messages-language-model.ts b/packages/anthropic/src/anthropic-messages-language-model.ts index f6a21702cadf..08e895c1643c 100644 --- a/packages/anthropic/src/anthropic-messages-language-model.ts +++ b/packages/anthropic/src/anthropic-messages-language-model.ts @@ -9,7 +9,6 @@ import { LanguageModelV3Source, LanguageModelV3StreamPart, LanguageModelV3ToolCall, - LanguageModelV3Usage, SharedV3ProviderMetadata, SharedV3Warning, } from '@ai-sdk/provider'; @@ -30,6 +29,11 @@ import { import { anthropicFailedResponseHandler } from './anthropic-error'; import { AnthropicMessageMetadata } from './anthropic-message-metadata'; import { + AnthropicMessagesUsage, + convertAnthropicMessagesUsage, +} from './convert-anthropic-messages-usage'; +import { + AnthropicContextManagementConfig, AnthropicContainer, anthropicMessagesChunkSchema, anthropicMessagesResponseSchema, @@ -933,12 +937,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { finishReason: response.stop_reason, isJsonResponseFromTool, }), - usage: { - inputTokens: response.usage.input_tokens, - outputTokens: response.usage.output_tokens, - totalTokens: response.usage.input_tokens + response.usage.output_tokens, - cachedInputTokens: response.usage.cache_read_input_tokens ?? undefined, - }, + usage: convertAnthropicMessagesUsage(response.usage), request: { body: args }, response: { id: response.id ?? undefined, @@ -1006,10 +1005,11 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, + const usage: AnthropicMessagesUsage = { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, }; const contentBlocks: Record< @@ -1600,9 +1600,11 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { } case 'message_start': { - usage.inputTokens = value.message.usage.input_tokens; - usage.cachedInputTokens = - value.message.usage.cache_read_input_tokens ?? undefined; + usage.input_tokens = value.message.usage.input_tokens; + usage.cache_read_input_tokens = + value.message.usage.cache_read_input_tokens ?? 0; + usage.cache_creation_input_tokens = + value.message.usage.cache_creation_input_tokens ?? 0; rawUsage = { ...(value.message.usage as JSONObject), @@ -1621,9 +1623,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { } case 'message_delta': { - usage.outputTokens = value.usage.output_tokens; - usage.totalTokens = - (usage.inputTokens ?? 0) + (value.usage.output_tokens ?? 0); + usage.output_tokens = value.usage.output_tokens; finishReason = mapAnthropicStopReason({ finishReason: value.delta.stop_reason, @@ -1663,7 +1663,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertAnthropicMessagesUsage(usage), providerMetadata: { anthropic: { usage: (rawUsage as JSONObject) ?? null, diff --git a/packages/anthropic/src/convert-anthropic-messages-usage.ts b/packages/anthropic/src/convert-anthropic-messages-usage.ts new file mode 100644 index 000000000000..26b6afdbabbc --- /dev/null +++ b/packages/anthropic/src/convert-anthropic-messages-usage.ts @@ -0,0 +1,32 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type AnthropicMessagesUsage = { + input_tokens: number; + output_tokens: number; + cache_creation_input_tokens?: number | null; + cache_read_input_tokens?: number | null; +}; + +export function convertAnthropicMessagesUsage( + usage: AnthropicMessagesUsage, +): LanguageModelV3Usage { + const inputTokens = usage.input_tokens; + const outputTokens = usage.output_tokens; + const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0; + const cacheReadTokens = usage.cache_read_input_tokens ?? 0; + + return { + inputTokens: { + total: inputTokens + cacheCreationTokens + cacheReadTokens, + noCache: inputTokens, + cacheRead: cacheReadTokens, + cacheWrite: cacheCreationTokens, + }, + outputTokens: { + total: outputTokens, + text: undefined, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/azure/src/__snapshots__/azure-openai-provider.test.ts.snap b/packages/azure/src/__snapshots__/azure-openai-provider.test.ts.snap index 42a8ba9b2e98..461d0b3ac2ef 100644 --- a/packages/azure/src/__snapshots__/azure-openai-provider.test.ts.snap +++ b/packages/azure/src/__snapshots__/azure-openai-provider.test.ts.snap @@ -2081,11 +2081,27 @@ Error message: Unexpected end of JSON input], }, "type": "finish", "usage": { - "cachedInputTokens": 1280, - "inputTokens": 2143, - "outputTokens": 919, - "reasoningTokens": 576, - "totalTokens": 3062, + "inputTokens": { + "cacheRead": 1280, + "cacheWrite": undefined, + "noCache": 863, + "total": 2143, + }, + "outputTokens": { + "reasoning": 576, + "text": 343, + "total": 919, + }, + "raw": { + "input_tokens": 2143, + "input_tokens_details": { + "cached_tokens": 1280, + }, + "output_tokens": 919, + "output_tokens_details": { + "reasoning_tokens": 576, + }, + }, }, }, ] @@ -2633,11 +2649,27 @@ providers and models, and which ones are available in the AI SDK.", }, "type": "finish", "usage": { - "cachedInputTokens": 2304, - "inputTokens": 3748, - "outputTokens": 543, - "reasoningTokens": 448, - "totalTokens": 4291, + "inputTokens": { + "cacheRead": 2304, + "cacheWrite": undefined, + "noCache": 1444, + "total": 3748, + }, + "outputTokens": { + "reasoning": 448, + "text": 95, + "total": 543, + }, + "raw": { + "input_tokens": 3748, + "input_tokens_details": { + "cached_tokens": 2304, + }, + "output_tokens": 543, + "output_tokens_details": { + "reasoning_tokens": 448, + }, + }, }, }, ] @@ -3158,11 +3190,27 @@ exports[`responses > file search tool > should stream file search results withou }, "type": "finish", "usage": { - "cachedInputTokens": 2304, - "inputTokens": 3737, - "outputTokens": 621, - "reasoningTokens": 512, - "totalTokens": 4358, + "inputTokens": { + "cacheRead": 2304, + "cacheWrite": undefined, + "noCache": 1433, + "total": 3737, + }, + "outputTokens": { + "reasoning": 512, + "text": 109, + "total": 621, + }, + "raw": { + "input_tokens": 3737, + "input_tokens_details": { + "cached_tokens": 2304, + }, + "output_tokens": 621, + "output_tokens_details": { + "reasoning_tokens": 512, + }, + }, }, }, ] @@ -5044,11 +5092,27 @@ Error message: Unexpected end of JSON input], }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 5093, - "outputTokens": 369, - "reasoningTokens": 0, - "totalTokens": 5462, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 5093, + "total": 5093, + }, + "outputTokens": { + "reasoning": 0, + "text": 369, + "total": 369, + }, + "raw": { + "input_tokens": 5093, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 369, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] diff --git a/packages/azure/src/azure-openai-provider.test.ts b/packages/azure/src/azure-openai-provider.test.ts index 6aef7a413bb6..28cf7d390fd4 100644 --- a/packages/azure/src/azure-openai-provider.test.ts +++ b/packages/azure/src/azure-openai-provider.test.ts @@ -1366,11 +1366,27 @@ describe('responses', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 234, - "inputTokens": 543, - "outputTokens": 478, - "reasoningTokens": 123, - "totalTokens": 1021, + "inputTokens": { + "cacheRead": 234, + "cacheWrite": undefined, + "noCache": 309, + "total": 543, + }, + "outputTokens": { + "reasoning": 123, + "text": 355, + "total": 478, + }, + "raw": { + "input_tokens": 543, + "input_tokens_details": { + "cached_tokens": 234, + }, + "output_tokens": 478, + "output_tokens_details": { + "reasoning_tokens": 123, + }, + }, }, }, ] @@ -1498,11 +1514,27 @@ describe('responses', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 0, - "outputTokens": 0, - "reasoningTokens": 0, - "totalTokens": 0, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 0, + "total": 0, + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0, + }, + "raw": { + "input_tokens": 0, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 0, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -1594,11 +1626,27 @@ describe('responses', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 50, - "outputTokens": 25, - "reasoningTokens": 0, - "totalTokens": 75, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 50, + "total": 50, + }, + "outputTokens": { + "reasoning": 0, + "text": 25, + "total": 25, + }, + "raw": { + "input_tokens": 50, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 25, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] diff --git a/packages/cohere/src/cohere-chat-language-model.test.ts b/packages/cohere/src/cohere-chat-language-model.test.ts index ba5bd48a4a87..9496cbed1636 100644 --- a/packages/cohere/src/cohere-chat-language-model.test.ts +++ b/packages/cohere/src/cohere-chat-language-model.test.ts @@ -145,9 +145,21 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "input_tokens": 20, + "output_tokens": 5, + }, } `); }); @@ -916,9 +928,21 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 34, - "outputTokens": 12, - "totalTokens": 46, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": undefined, + "text": 12, + "total": 12, + }, + "raw": { + "input_tokens": 34, + "output_tokens": 12, + }, }, }, ] @@ -1013,9 +1037,21 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 34, - "outputTokens": 12, - "totalTokens": 46, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": undefined, + "text": 12, + "total": 12, + }, + "raw": { + "input_tokens": 34, + "output_tokens": 12, + }, }, }, ] @@ -1145,9 +1181,21 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 893, - "outputTokens": 62, - "totalTokens": 955, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 893, + "total": 893, + }, + "outputTokens": { + "reasoning": undefined, + "text": 62, + "total": 62, + }, + "raw": { + "input_tokens": 893, + "output_tokens": 62, + }, }, }, ] @@ -1192,9 +1240,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1372,9 +1429,21 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 5, - "totalTokens": 15, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "input_tokens": 10, + "output_tokens": 5, + }, }, }, ] diff --git a/packages/cohere/src/cohere-chat-language-model.ts b/packages/cohere/src/cohere-chat-language-model.ts index f2f505e8da9b..29b109ead7a2 100644 --- a/packages/cohere/src/cohere-chat-language-model.ts +++ b/packages/cohere/src/cohere-chat-language-model.ts @@ -5,7 +5,6 @@ import { LanguageModelV3FinishReason, LanguageModelV3Prompt, LanguageModelV3StreamPart, - LanguageModelV3Usage, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; import { @@ -24,6 +23,7 @@ import { cohereChatModelOptions, } from './cohere-chat-options'; import { cohereFailedResponseHandler } from './cohere-error'; +import { CohereUsageTokens, convertCohereUsage } from './convert-cohere-usage'; import { convertToCohereChatPrompt } from './convert-to-cohere-chat-prompt'; import { mapCohereFinishReason } from './map-cohere-finish-reason'; import { prepareTools } from './cohere-prepare-tools'; @@ -204,13 +204,7 @@ export class CohereChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapCohereFinishReason(response.finish_reason), - usage: { - inputTokens: response.usage.tokens.input_tokens, - outputTokens: response.usage.tokens.output_tokens, - totalTokens: - response.usage.tokens.input_tokens + - response.usage.tokens.output_tokens, - }, + usage: convertCohereUsage(response.usage.tokens), request: { body: args }, response: { // TODO timestamp, model id @@ -240,11 +234,7 @@ export class CohereChatLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: CohereUsageTokens | undefined = undefined; let pendingToolCall: { id: string; @@ -410,11 +400,7 @@ export class CohereChatLanguageModel implements LanguageModelV3 { case 'message-end': { finishReason = mapCohereFinishReason(value.delta.finish_reason); - const tokens = value.delta.usage.tokens; - - usage.inputTokens = tokens.input_tokens; - usage.outputTokens = tokens.output_tokens; - usage.totalTokens = tokens.input_tokens + tokens.output_tokens; + usage = value.delta.usage.tokens; return; } @@ -428,7 +414,7 @@ export class CohereChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertCohereUsage(usage), }); }, }), diff --git a/packages/cohere/src/convert-cohere-usage.ts b/packages/cohere/src/convert-cohere-usage.ts new file mode 100644 index 000000000000..0f83a63ab85a --- /dev/null +++ b/packages/cohere/src/convert-cohere-usage.ts @@ -0,0 +1,45 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type CohereUsageTokens = { + input_tokens: number; + output_tokens: number; +}; + +export function convertCohereUsage( + tokens: CohereUsageTokens | undefined | null, +): LanguageModelV3Usage { + if (tokens == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const inputTokens = tokens.input_tokens; + const outputTokens = tokens.output_tokens; + + return { + inputTokens: { + total: inputTokens, + noCache: inputTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: outputTokens, + text: outputTokens, + reasoning: undefined, + }, + raw: tokens, + }; +} diff --git a/packages/deepseek/src/chat/__snapshots__/deepseek-chat-language-model.test.ts.snap b/packages/deepseek/src/chat/__snapshots__/deepseek-chat-language-model.test.ts.snap index 32166ce96daf..1d9bc5515cae 100644 --- a/packages/deepseek/src/chat/__snapshots__/deepseek-chat-language-model.test.ts.snap +++ b/packages/deepseek/src/chat/__snapshots__/deepseek-chat-language-model.test.ts.snap @@ -86,11 +86,27 @@ But the question is phrased: "How many 'r's are in the word 'strawberry'?" So an "timestamp": 2025-12-02T07:35:03.000Z, }, "usage": { - "cachedInputTokens": 0, - "inputTokens": 18, - "outputTokens": 345, - "reasoningTokens": 315, - "totalTokens": 363, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 315, + "text": 30, + "total": 345, + }, + "raw": { + "completion_tokens": 345, + "completion_tokens_details": { + "reasoning_tokens": 315, + }, + "prompt_cache_hit_tokens": 0, + "prompt_cache_miss_tokens": 18, + "prompt_tokens": 18, + "total_tokens": 363, + }, }, "warnings": [], } @@ -198,11 +214,24 @@ exports[`DeepSeekChatLanguageModel > doGenerate > text > should extract text con "timestamp": 2025-12-02T06:18:36.000Z, }, "usage": { - "cachedInputTokens": 0, - "inputTokens": 13, - "outputTokens": 300, - "reasoningTokens": undefined, - "totalTokens": 313, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 13, + "total": 13, + }, + "outputTokens": { + "reasoning": 0, + "text": 300, + "total": 300, + }, + "raw": { + "completion_tokens": 300, + "prompt_cache_hit_tokens": 0, + "prompt_cache_miss_tokens": 13, + "prompt_tokens": 13, + "total_tokens": 313, + }, }, "warnings": [], } @@ -327,11 +356,27 @@ exports[`DeepSeekChatLanguageModel > doGenerate > tool call > json response form "timestamp": 2025-12-02T13:15:41.000Z, }, "usage": { - "cachedInputTokens": 320, - "inputTokens": 495, - "outputTokens": 144, - "reasoningTokens": 118, - "totalTokens": 639, + "inputTokens": { + "cacheRead": 320, + "cacheWrite": undefined, + "noCache": 175, + "total": 495, + }, + "outputTokens": { + "reasoning": 118, + "text": 26, + "total": 144, + }, + "raw": { + "completion_tokens": 144, + "completion_tokens_details": { + "reasoning_tokens": 118, + }, + "prompt_cache_hit_tokens": 320, + "prompt_cache_miss_tokens": 175, + "prompt_tokens": 495, + "total_tokens": 639, + }, }, "warnings": [], } @@ -455,11 +500,27 @@ exports[`DeepSeekChatLanguageModel > doGenerate > tool call > should extract too "timestamp": 2025-12-02T08:57:25.000Z, }, "usage": { - "cachedInputTokens": 320, - "inputTokens": 339, - "outputTokens": 92, - "reasoningTokens": 48, - "totalTokens": 431, + "inputTokens": { + "cacheRead": 320, + "cacheWrite": undefined, + "noCache": 19, + "total": 339, + }, + "outputTokens": { + "reasoning": 48, + "text": 44, + "total": 92, + }, + "raw": { + "completion_tokens": 92, + "completion_tokens_details": { + "reasoning_tokens": 48, + }, + "prompt_cache_hit_tokens": 320, + "prompt_cache_miss_tokens": 19, + "prompt_tokens": 339, + "total_tokens": 431, + }, }, "warnings": [], } @@ -1610,11 +1671,27 @@ exports[`DeepSeekChatLanguageModel > doStream > reasoning > should stream reason }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 18, - "outputTokens": 219, - "reasoningTokens": 205, - "totalTokens": 237, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 205, + "text": 14, + "total": 219, + }, + "raw": { + "completion_tokens": 219, + "completion_tokens_details": { + "reasoning_tokens": 205, + }, + "prompt_cache_hit_tokens": 0, + "prompt_cache_miss_tokens": 18, + "prompt_tokens": 18, + "total_tokens": 237, + }, }, }, ] @@ -3668,11 +3745,24 @@ exports[`DeepSeekChatLanguageModel > doStream > text > should stream text 1`] = }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 13, - "outputTokens": 400, - "reasoningTokens": undefined, - "totalTokens": 413, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 13, + "total": 13, + }, + "outputTokens": { + "reasoning": 0, + "text": 400, + "total": 400, + }, + "raw": { + "completion_tokens": 400, + "prompt_cache_hit_tokens": 0, + "prompt_cache_miss_tokens": 13, + "prompt_tokens": 13, + "total_tokens": 413, + }, }, }, ] @@ -3968,11 +4058,27 @@ exports[`DeepSeekChatLanguageModel > doStream > tool call > should stream tool c }, "type": "finish", "usage": { - "cachedInputTokens": 320, - "inputTokens": 339, - "outputTokens": 83, - "reasoningTokens": 39, - "totalTokens": 422, + "inputTokens": { + "cacheRead": 320, + "cacheWrite": undefined, + "noCache": 19, + "total": 339, + }, + "outputTokens": { + "reasoning": 39, + "text": 44, + "total": 83, + }, + "raw": { + "completion_tokens": 83, + "completion_tokens_details": { + "reasoning_tokens": 39, + }, + "prompt_cache_hit_tokens": 320, + "prompt_cache_miss_tokens": 19, + "prompt_tokens": 339, + "total_tokens": 422, + }, }, }, ] diff --git a/packages/deepseek/src/chat/convert-to-deepseek-usage.ts b/packages/deepseek/src/chat/convert-to-deepseek-usage.ts new file mode 100644 index 000000000000..af336d708fad --- /dev/null +++ b/packages/deepseek/src/chat/convert-to-deepseek-usage.ts @@ -0,0 +1,56 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export function convertDeepSeekUsage( + usage: + | { + prompt_tokens?: number | null | undefined; + completion_tokens?: number | null | undefined; + prompt_cache_hit_tokens?: number | null | undefined; + completion_tokens_details?: + | { + reasoning_tokens?: number | null | undefined; + } + | null + | undefined; + } + | undefined + | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + const cacheReadTokens = usage.prompt_cache_hit_tokens ?? 0; + const reasoningTokens = + usage.completion_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens - cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/deepseek/src/chat/deepseek-chat-language-model.ts b/packages/deepseek/src/chat/deepseek-chat-language-model.ts index ee4ffa04d455..e33c87318e14 100644 --- a/packages/deepseek/src/chat/deepseek-chat-language-model.ts +++ b/packages/deepseek/src/chat/deepseek-chat-language-model.ts @@ -21,6 +21,7 @@ import { ResponseHandler, } from '@ai-sdk/provider-utils'; import { convertToDeepSeekChatMessages } from './convert-to-deepseek-chat-messages'; +import { convertDeepSeekUsage } from './convert-to-deepseek-usage'; import { deepseekChatChunkSchema, deepseekChatResponseSchema, @@ -194,16 +195,7 @@ export class DeepSeekChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapDeepSeekFinishReason(choice.finish_reason), - usage: { - inputTokens: responseBody.usage?.prompt_tokens ?? undefined, - outputTokens: responseBody.usage?.completion_tokens ?? undefined, - totalTokens: responseBody.usage?.total_tokens ?? undefined, - reasoningTokens: - responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? - undefined, - cachedInputTokens: - responseBody.usage?.prompt_cache_hit_tokens ?? undefined, - }, + usage: convertDeepSeekUsage(responseBody.usage), providerMetadata: { [this.providerOptionsName]: { promptCacheHitTokens: responseBody.usage?.prompt_cache_hit_tokens, @@ -510,15 +502,7 @@ export class DeepSeekChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage: { - inputTokens: usage?.prompt_tokens ?? undefined, - outputTokens: usage?.completion_tokens ?? undefined, - totalTokens: usage?.total_tokens ?? undefined, - reasoningTokens: - usage?.completion_tokens_details?.reasoning_tokens ?? - undefined, - cachedInputTokens: usage?.prompt_cache_hit_tokens ?? undefined, - }, + usage: convertDeepSeekUsage(usage), providerMetadata: { [providerOptionsName]: { promptCacheHitTokens: diff --git a/packages/google/src/convert-google-generative-ai-usage.ts b/packages/google/src/convert-google-generative-ai-usage.ts new file mode 100644 index 000000000000..716eaafa972a --- /dev/null +++ b/packages/google/src/convert-google-generative-ai-usage.ts @@ -0,0 +1,51 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type GoogleGenerativeAIUsageMetadata = { + promptTokenCount?: number | null; + candidatesTokenCount?: number | null; + totalTokenCount?: number | null; + cachedContentTokenCount?: number | null; + thoughtsTokenCount?: number | null; + trafficType?: string | null; +}; + +export function convertGoogleGenerativeAIUsage( + usage: GoogleGenerativeAIUsageMetadata | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.promptTokenCount ?? 0; + const candidatesTokens = usage.candidatesTokenCount ?? 0; + const cachedContentTokens = usage.cachedContentTokenCount ?? 0; + const thoughtsTokens = usage.thoughtsTokenCount ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens - cachedContentTokens, + cacheRead: cachedContentTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: candidatesTokens + thoughtsTokens, + text: candidatesTokens, + reasoning: thoughtsTokens, + }, + raw: usage, + }; +} diff --git a/packages/google/src/google-generative-ai-language-model.test.ts b/packages/google/src/google-generative-ai-language-model.test.ts index 5fe09ae7e24a..350a6dc5466d 100644 --- a/packages/google/src/google-generative-ai-language-model.test.ts +++ b/packages/google/src/google-generative-ai-language-model.test.ts @@ -354,11 +354,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "reasoningTokens": undefined, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 5, + "total": 5, + }, + "raw": { + "candidatesTokenCount": 5, + "promptTokenCount": 20, + "totalTokenCount": 25, + }, } `); }); @@ -2064,11 +2075,23 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 10, - "outputTokens": 15, - "reasoningTokens": 8, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 8, + "text": 15, + "total": 23, + }, + "raw": { + "candidatesTokenCount": 15, + "promptTokenCount": 10, + "thoughtsTokenCount": 8, + "totalTokenCount": 25, + }, } `); }); @@ -2351,11 +2374,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 294, - "outputTokens": 233, - "reasoningTokens": undefined, - "totalTokens": 527, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 294, + "total": 294, + }, + "outputTokens": { + "reasoning": 0, + "text": 233, + "total": 233, + }, + "raw": { + "candidatesTokenCount": 233, + "promptTokenCount": 294, + "totalTokenCount": 527, + }, }, }, ] @@ -2873,11 +2907,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 294, - "outputTokens": 233, - "reasoningTokens": undefined, - "totalTokens": 527, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 294, + "total": 294, + }, + "outputTokens": { + "reasoning": 0, + "text": 233, + "total": 233, + }, + "raw": { + "candidatesTokenCount": 233, + "promptTokenCount": 294, + "totalTokenCount": 527, + }, }, }, ] @@ -3233,11 +3278,23 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 14, - "outputTokens": 18, - "reasoningTokens": 142, - "totalTokens": 174, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 14, + "total": 14, + }, + "outputTokens": { + "reasoning": 142, + "text": 18, + "total": 160, + }, + "raw": { + "candidatesTokenCount": 18, + "promptTokenCount": 14, + "thoughtsTokenCount": 142, + "totalTokenCount": 174, + }, }, }, ] @@ -3373,9 +3430,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] diff --git a/packages/google/src/google-generative-ai-language-model.ts b/packages/google/src/google-generative-ai-language-model.ts index 58dde4b4283b..1789acc7a797 100644 --- a/packages/google/src/google-generative-ai-language-model.ts +++ b/packages/google/src/google-generative-ai-language-model.ts @@ -5,7 +5,6 @@ import { LanguageModelV3FinishReason, LanguageModelV3Source, LanguageModelV3StreamPart, - LanguageModelV3Usage, SharedV3ProviderMetadata, } from '@ai-sdk/provider'; import { @@ -24,6 +23,10 @@ import { zodSchema, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; +import { + convertGoogleGenerativeAIUsage, + GoogleGenerativeAIUsageMetadata, +} from './convert-google-generative-ai-usage'; import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema'; import { convertToGoogleGenerativeAIMessages } from './convert-to-google-generative-ai-messages'; import { getModelPath } from './get-model-path'; @@ -291,13 +294,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 { finishReason: candidate.finishReason, hasToolCalls: content.some(part => part.type === 'tool-call'), }), - usage: { - inputTokens: usageMetadata?.promptTokenCount ?? undefined, - outputTokens: usageMetadata?.candidatesTokenCount ?? undefined, - totalTokens: usageMetadata?.totalTokenCount ?? undefined, - reasoningTokens: usageMetadata?.thoughtsTokenCount ?? undefined, - cachedInputTokens: usageMetadata?.cachedContentTokenCount ?? undefined, - }, + usage: convertGoogleGenerativeAIUsage(usageMetadata), warnings, providerMetadata: { google: { @@ -341,11 +338,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: GoogleGenerativeAIUsageMetadata | undefined = undefined; let providerMetadata: SharedV3ProviderMetadata | undefined = undefined; const generateId = this.config.generateId; @@ -386,14 +379,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 { const usageMetadata = value.usageMetadata; if (usageMetadata != null) { - usage.inputTokens = usageMetadata.promptTokenCount ?? undefined; - usage.outputTokens = - usageMetadata.candidatesTokenCount ?? undefined; - usage.totalTokens = usageMetadata.totalTokenCount ?? undefined; - usage.reasoningTokens = - usageMetadata.thoughtsTokenCount ?? undefined; - usage.cachedInputTokens = - usageMetadata.cachedContentTokenCount ?? undefined; + usage = usageMetadata; } const candidate = value.candidates?.[0]; @@ -625,7 +611,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertGoogleGenerativeAIUsage(usage), providerMetadata, }); }, diff --git a/packages/groq/src/convert-groq-usage.ts b/packages/groq/src/convert-groq-usage.ts new file mode 100644 index 000000000000..e955838c8719 --- /dev/null +++ b/packages/groq/src/convert-groq-usage.ts @@ -0,0 +1,52 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export function convertGroqUsage( + usage: + | { + prompt_tokens?: number | null | undefined; + completion_tokens?: number | null | undefined; + prompt_tokens_details?: + | { + cached_tokens?: number | null | undefined; + } + | null + | undefined; + } + | undefined + | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/groq/src/groq-chat-language-model.test.ts b/packages/groq/src/groq-chat-language-model.test.ts index 132cd9567c3c..eae0a5bc1994 100644 --- a/packages/groq/src/groq-chat-language-model.test.ts +++ b/packages/groq/src/groq-chat-language-model.test.ts @@ -141,10 +141,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -181,10 +193,21 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": undefined, - "totalTokens": 20, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 0, + "total": 0, + }, + "raw": { + "prompt_tokens": 20, + "total_tokens": 20, + }, } `); }); @@ -207,10 +230,25 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 15, - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 15, + }, + "total_tokens": 25, + }, } `); }); @@ -852,10 +890,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "totalTokens": 457, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": undefined, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -936,10 +986,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "totalTokens": 457, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": undefined, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1067,10 +1129,22 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "totalTokens": 457, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": undefined, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1203,10 +1277,22 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "totalTokens": 457, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": undefined, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1330,10 +1416,18 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1410,10 +1504,22 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "totalTokens": 457, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": undefined, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1451,10 +1557,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1490,10 +1604,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -1691,10 +1813,22 @@ describe('doStream with raw chunks', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 10, - "outputTokens": 5, - "totalTokens": 15, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 10, + "total_tokens": 15, + }, }, }, ] diff --git a/packages/groq/src/groq-chat-language-model.ts b/packages/groq/src/groq-chat-language-model.ts index 50eaa3930a87..620c7a669b55 100644 --- a/packages/groq/src/groq-chat-language-model.ts +++ b/packages/groq/src/groq-chat-language-model.ts @@ -1,13 +1,12 @@ import { InvalidResponseDataError, LanguageModelV3, - SharedV3Warning, LanguageModelV3Content, LanguageModelV3FinishReason, - LanguageModelV3Prompt, LanguageModelV3StreamPart, LanguageModelV3Usage, SharedV3ProviderMetadata, + SharedV3Warning, } from '@ai-sdk/provider'; import { FetchFunction, @@ -21,6 +20,7 @@ import { postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; +import { convertGroqUsage } from './convert-groq-usage'; import { convertToGroqChatMessages } from './convert-to-groq-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { GroqChatModelId, groqProviderOptions } from './groq-chat-options'; @@ -217,13 +217,7 @@ export class GroqChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapGroqFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage?.prompt_tokens ?? undefined, - outputTokens: response.usage?.completion_tokens ?? undefined, - totalTokens: response.usage?.total_tokens ?? undefined, - cachedInputTokens: - response.usage?.prompt_tokens_details?.cached_tokens ?? undefined, - }, + usage: convertGroqUsage(response.usage), response: { ...getResponseMetadata(response), headers: responseHeaders, @@ -269,12 +263,18 @@ export class GroqChatLanguageModel implements LanguageModelV3 { }> = []; let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - cachedInputTokens: undefined, - }; + let usage: + | { + prompt_tokens?: number | null | undefined; + completion_tokens?: number | null | undefined; + prompt_tokens_details?: + | { + cached_tokens?: number | null | undefined; + } + | null + | undefined; + } + | undefined = undefined; let isFirstChunk = true; let isActiveText = false; let isActiveReasoning = false; @@ -322,13 +322,7 @@ export class GroqChatLanguageModel implements LanguageModelV3 { } if (value.x_groq?.usage != null) { - usage.inputTokens = value.x_groq.usage.prompt_tokens ?? undefined; - usage.outputTokens = - value.x_groq.usage.completion_tokens ?? undefined; - usage.totalTokens = value.x_groq.usage.total_tokens ?? undefined; - usage.cachedInputTokens = - value.x_groq.usage.prompt_tokens_details?.cached_tokens ?? - undefined; + usage = value.x_groq.usage; } const choice = value.choices[0]; @@ -504,7 +498,7 @@ export class GroqChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertGroqUsage(usage), ...(providerMetadata != null ? { providerMetadata } : {}), }); }, diff --git a/packages/huggingface/src/responses/convert-huggingface-responses-usage.ts b/packages/huggingface/src/responses/convert-huggingface-responses-usage.ts new file mode 100644 index 000000000000..1923bb34e55b --- /dev/null +++ b/packages/huggingface/src/responses/convert-huggingface-responses-usage.ts @@ -0,0 +1,54 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type HuggingFaceResponsesUsage = { + input_tokens: number; + input_tokens_details?: { + cached_tokens?: number; + }; + output_tokens: number; + output_tokens_details?: { + reasoning_tokens?: number; + }; + total_tokens: number; +}; + +export function convertHuggingFaceResponsesUsage( + usage: HuggingFaceResponsesUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const inputTokens = usage.input_tokens; + const outputTokens = usage.output_tokens; + const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: inputTokens, + noCache: inputTokens - cachedTokens, + cacheRead: cachedTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: outputTokens, + text: outputTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/huggingface/src/responses/huggingface-responses-language-model.test.ts b/packages/huggingface/src/responses/huggingface-responses-language-model.test.ts index 3401331599aa..fb1f67835cf5 100644 --- a/packages/huggingface/src/responses/huggingface-responses-language-model.test.ts +++ b/packages/huggingface/src/responses/huggingface-responses-language-model.test.ts @@ -100,9 +100,22 @@ describe('HuggingFaceResponsesLanguageModel', () => { expect(result.usage).toMatchInlineSnapshot(` { - "inputTokens": 12, - "outputTokens": 25, - "totalTokens": 37, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 12, + "total": 12, + }, + "outputTokens": { + "reasoning": 0, + "text": 25, + "total": 25, + }, + "raw": { + "input_tokens": 12, + "output_tokens": 25, + "total_tokens": 37, + }, } `); }); @@ -197,9 +210,18 @@ describe('HuggingFaceResponsesLanguageModel', () => { expect(result.usage).toMatchInlineSnapshot(` { - "inputTokens": 0, - "outputTokens": 0, - "totalTokens": 0, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, } `); }); @@ -533,9 +555,22 @@ describe('HuggingFaceResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 12, - "outputTokens": 25, - "totalTokens": 37, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 12, + "total": 12, + }, + "outputTokens": { + "reasoning": 0, + "text": 25, + "total": 25, + }, + "raw": { + "input_tokens": 12, + "output_tokens": 25, + "total_tokens": 37, + }, }, }, ] @@ -564,9 +599,18 @@ describe('HuggingFaceResponsesLanguageModel', () => { expect(finishChunk?.usage).toMatchInlineSnapshot(` { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, } `); }); @@ -953,9 +997,22 @@ describe('HuggingFaceResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 20, - "outputTokens": 15, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 15, + "total": 15, + }, + "raw": { + "input_tokens": 20, + "output_tokens": 15, + "total_tokens": 35, + }, }, }, ] @@ -1261,9 +1318,22 @@ describe('HuggingFaceResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 20, - "totalTokens": 30, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "input_tokens": 10, + "output_tokens": 20, + "total_tokens": 30, + }, }, }, ] diff --git a/packages/huggingface/src/responses/huggingface-responses-language-model.ts b/packages/huggingface/src/responses/huggingface-responses-language-model.ts index 5233ee4b05a0..d2b02d1b1ee8 100644 --- a/packages/huggingface/src/responses/huggingface-responses-language-model.ts +++ b/packages/huggingface/src/responses/huggingface-responses-language-model.ts @@ -5,7 +5,6 @@ import { LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, } from '@ai-sdk/provider'; import { combineHeaders, @@ -19,6 +18,10 @@ import { import { z } from 'zod/v4'; import { HuggingFaceConfig } from '../huggingface-config'; import { huggingfaceFailedResponseHandler } from '../huggingface-error'; +import { + HuggingFaceResponsesUsage, + convertHuggingFaceResponsesUsage, +} from './convert-huggingface-responses-usage'; import { convertToHuggingFaceResponsesMessages } from './convert-to-huggingface-responses-messages'; import { mapHuggingFaceResponsesFinishReason } from './map-huggingface-responses-finish-reason'; import { HuggingFaceResponsesModelId } from './huggingface-responses-settings'; @@ -303,14 +306,7 @@ export class HuggingFaceResponsesLanguageModel implements LanguageModelV3 { finishReason: mapHuggingFaceResponsesFinishReason( response.incomplete_details?.reason ?? 'stop', ), - usage: { - inputTokens: response.usage?.input_tokens ?? 0, - outputTokens: response.usage?.output_tokens ?? 0, - totalTokens: - response.usage?.total_tokens ?? - (response.usage?.input_tokens ?? 0) + - (response.usage?.output_tokens ?? 0), - }, + usage: convertHuggingFaceResponsesUsage(response.usage), request: { body }, response: { id: response.id, @@ -355,11 +351,7 @@ export class HuggingFaceResponsesLanguageModel implements LanguageModelV3 { let finishReason: LanguageModelV3FinishReason = 'unknown'; let responseId: string | null = null; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: HuggingFaceResponsesUsage | undefined = undefined; return { stream: response.pipeThrough( @@ -465,12 +457,7 @@ export class HuggingFaceResponsesLanguageModel implements LanguageModelV3 { value.response.incomplete_details?.reason ?? 'stop', ); if (value.response.usage) { - usage.inputTokens = value.response.usage.input_tokens; - usage.outputTokens = value.response.usage.output_tokens; - usage.totalTokens = - value.response.usage.total_tokens ?? - value.response.usage.input_tokens + - value.response.usage.output_tokens; + usage = value.response.usage; } return; } @@ -506,7 +493,7 @@ export class HuggingFaceResponsesLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertHuggingFaceResponsesUsage(usage), providerMetadata: { huggingface: { responseId, diff --git a/packages/mistral/src/convert-mistral-usage.ts b/packages/mistral/src/convert-mistral-usage.ts new file mode 100644 index 000000000000..de5a5fdf94e7 --- /dev/null +++ b/packages/mistral/src/convert-mistral-usage.ts @@ -0,0 +1,46 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type MistralUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; +}; + +export function convertMistralUsage( + usage: MistralUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens; + const completionTokens = usage.completion_tokens; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/mistral/src/mistral-chat-language-model.test.ts b/packages/mistral/src/mistral-chat-language-model.test.ts index 62f2daf28a9f..4eecbf80bc88 100644 --- a/packages/mistral/src/mistral-chat-language-model.test.ts +++ b/packages/mistral/src/mistral-chat-language-model.test.ts @@ -336,9 +336,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -814,9 +827,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 4, - "outputTokens": 32, - "totalTokens": 36, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 32, + "total": 32, + }, + "raw": { + "completion_tokens": 32, + "prompt_tokens": 4, + "total_tokens": 36, + }, }, }, ] @@ -876,9 +902,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 4, - "outputTokens": 32, - "totalTokens": 36, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 32, + "total": 32, + }, + "raw": { + "completion_tokens": 32, + "prompt_tokens": 4, + "total_tokens": 36, + }, }, }, ] @@ -957,9 +996,22 @@ describe('doStream', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "inputTokens": 183, - "outputTokens": 133, - "totalTokens": 316, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 183, + "total": 183, + }, + "outputTokens": { + "reasoning": undefined, + "text": 133, + "total": 133, + }, + "raw": { + "completion_tokens": 133, + "prompt_tokens": 183, + "total_tokens": 316, + }, }, }, ] @@ -1123,9 +1175,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 4, - "outputTokens": 32, - "totalTokens": 36, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": undefined, + "text": 32, + "total": 32, + }, + "raw": { + "completion_tokens": 32, + "prompt_tokens": 4, + "total_tokens": 36, + }, }, }, ] @@ -1190,9 +1255,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 5, - "outputTokens": 20, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 5, + "total": 5, + }, + "outputTokens": { + "reasoning": undefined, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 5, + "total_tokens": 25, + }, }, }, ] @@ -1285,9 +1363,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 30, - "totalTokens": 40, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 30, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "prompt_tokens": 10, + "total_tokens": 40, + }, }, }, ] @@ -1407,9 +1498,22 @@ describe('doStream with raw chunks', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 5, - "totalTokens": 15, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 10, + "total_tokens": 15, + }, }, }, ] diff --git a/packages/mistral/src/mistral-chat-language-model.ts b/packages/mistral/src/mistral-chat-language-model.ts index 4458dd62ad8d..1ad916270f78 100644 --- a/packages/mistral/src/mistral-chat-language-model.ts +++ b/packages/mistral/src/mistral-chat-language-model.ts @@ -4,7 +4,6 @@ import { LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, } from '@ai-sdk/provider'; import { combineHeaders, @@ -18,6 +17,7 @@ import { postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; +import { MistralUsage, convertMistralUsage } from './convert-mistral-usage'; import { convertToMistralChatMessages } from './convert-to-mistral-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapMistralFinishReason } from './map-mistral-finish-reason'; @@ -237,11 +237,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapMistralFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage.prompt_tokens, - outputTokens: response.usage.completion_tokens, - totalTokens: response.usage.total_tokens, - }, + usage: convertMistralUsage(response.usage), request: { body }, response: { ...getResponseMetadata(response), @@ -271,11 +267,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: MistralUsage | undefined = undefined; let isFirstChunk = true; let activeText = false; @@ -316,9 +308,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { } if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens; - usage.outputTokens = value.usage.completion_tokens; - usage.totalTokens = value.usage.total_tokens; + usage = value.usage; } const choice = value.choices[0]; @@ -426,7 +416,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertMistralUsage(usage), }); }, }), diff --git a/packages/openai-compatible/src/chat/convert-openai-compatible-chat-usage.ts b/packages/openai-compatible/src/chat/convert-openai-compatible-chat-usage.ts new file mode 100644 index 000000000000..7982309cd99a --- /dev/null +++ b/packages/openai-compatible/src/chat/convert-openai-compatible-chat-usage.ts @@ -0,0 +1,55 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export function convertOpenAICompatibleChatUsage( + usage: + | { + prompt_tokens?: number | null; + completion_tokens?: number | null; + prompt_tokens_details?: { + cached_tokens?: number | null; + } | null; + completion_tokens_details?: { + reasoning_tokens?: number | null; + } | null; + } + | undefined + | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = + usage.completion_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens - cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts index 8522fb68fbcc..64b967c52a6f 100644 --- a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts +++ b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts @@ -259,11 +259,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "reasoningTokens": undefined, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -327,11 +338,21 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": 20, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0, + }, + "raw": { + "prompt_tokens": 20, + "total_tokens": 20, + }, } `); }); @@ -948,11 +969,30 @@ describe('doGenerate', () => { expect(result.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 30, - "reasoningTokens": 10, - "totalTokens": 50, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": undefined, + "noCache": 15, + "total": 20, + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "accepted_prediction_tokens": 15, + "reasoning_tokens": 10, + "rejected_prediction_tokens": 5, + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5, + }, + "total_tokens": 50, + }, } `); expect(result.providerMetadata).toMatchInlineSnapshot(` @@ -1003,11 +1043,28 @@ describe('doGenerate', () => { expect(result.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 30, - "reasoningTokens": 10, - "totalTokens": 50, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": undefined, + "noCache": 15, + "total": 20, + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5, + }, + "total_tokens": 50, + }, } `); }); @@ -1125,11 +1182,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": 457, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1215,11 +1283,21 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + }, }, }, ] @@ -1305,11 +1383,21 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + }, }, }, ] @@ -1381,11 +1469,21 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + }, }, }, ] @@ -1516,11 +1614,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": 457, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1656,11 +1765,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": 457, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1787,11 +1907,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 226, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": 246, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 226, + "total": 226, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 226, + "total_tokens": 246, + }, }, }, ] @@ -1871,11 +2002,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": 457, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1949,11 +2091,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 18, - "outputTokens": 439, - "reasoningTokens": undefined, - "totalTokens": 457, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 18, + "total": 18, + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439, + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18, + "total_tokens": 457, + }, }, }, ] @@ -1991,11 +2144,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2033,11 +2193,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2224,11 +2391,29 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 30, - "reasoningTokens": 10, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": undefined, + "noCache": 15, + "total": 20, + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "accepted_prediction_tokens": 15, + "reasoning_tokens": 10, + "rejected_prediction_tokens": 5, + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5, + }, + }, }, } `); @@ -2286,11 +2471,28 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 5, - "inputTokens": 20, - "outputTokens": 30, - "reasoningTokens": 10, - "totalTokens": 50, + "inputTokens": { + "cacheRead": 5, + "cacheWrite": undefined, + "noCache": 15, + "total": 20, + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5, + }, + "total_tokens": 50, + }, }, } `); @@ -2497,11 +2699,18 @@ describe('raw chunks', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": undefined, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] diff --git a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts index f246038ca0a1..25c59a4061f7 100644 --- a/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts +++ b/packages/openai-compatible/src/chat/openai-compatible-chat-language-model.ts @@ -2,11 +2,11 @@ import { APICallError, InvalidResponseDataError, LanguageModelV3, - SharedV3Warning, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, SharedV3ProviderMetadata, + SharedV3Warning, } from '@ai-sdk/provider'; import { combineHeaders, @@ -22,18 +22,18 @@ import { ResponseHandler, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; +import { + defaultOpenAICompatibleErrorStructure, + ProviderErrorStructure, +} from '../openai-compatible-error'; +import { convertOpenAICompatibleChatUsage } from './convert-openai-compatible-chat-usage'; import { convertToOpenAICompatibleChatMessages } from './convert-to-openai-compatible-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason'; import { OpenAICompatibleChatModelId, openaiCompatibleProviderOptions, - OpenAICompatibleProviderOptions, } from './openai-compatible-chat-options'; -import { - defaultOpenAICompatibleErrorStructure, - ProviderErrorStructure, -} from '../openai-compatible-error'; import { MetadataExtractor } from './openai-compatible-metadata-extractor'; import { prepareTools } from './openai-compatible-prepare-tools'; @@ -286,16 +286,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason), - usage: { - inputTokens: responseBody.usage?.prompt_tokens ?? undefined, - outputTokens: responseBody.usage?.completion_tokens ?? undefined, - totalTokens: responseBody.usage?.total_tokens ?? undefined, - reasoningTokens: - responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? - undefined, - cachedInputTokens: - responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined, - }, + usage: convertOpenAICompatibleChatUsage(responseBody.usage), providerMetadata, request: { body }, response: { @@ -351,31 +342,8 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { }> = []; let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: { - completionTokens: number | undefined; - completionTokensDetails: { - reasoningTokens: number | undefined; - acceptedPredictionTokens: number | undefined; - rejectedPredictionTokens: number | undefined; - }; - promptTokens: number | undefined; - promptTokensDetails: { - cachedTokens: number | undefined; - }; - totalTokens: number | undefined; - } = { - completionTokens: undefined, - completionTokensDetails: { - reasoningTokens: undefined, - acceptedPredictionTokens: undefined, - rejectedPredictionTokens: undefined, - }, - promptTokens: undefined, - promptTokensDetails: { - cachedTokens: undefined, - }, - totalTokens: undefined, - }; + let usage: z.infer | undefined = + undefined; let isFirstChunk = true; const providerOptionsName = this.providerOptionsName; let isActiveReasoning = false; @@ -391,7 +359,6 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'stream-start', warnings }); }, - // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { @@ -404,17 +371,23 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'error', error: chunk.error }); return; } - const value = chunk.value; metadataExtractor?.processChunk(chunk.rawValue); // handle error chunks: - if ('error' in value) { + if ('error' in chunk.value) { finishReason = 'error'; - controller.enqueue({ type: 'error', error: value.error.message }); + controller.enqueue({ + type: 'error', + error: chunk.value.error.message, + }); return; } + // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX + // remove this workaround when the issue is fixed + const value = chunk.value as z.infer; + if (isFirstChunk) { isFirstChunk = false; @@ -425,37 +398,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { } if (value.usage != null) { - const { - prompt_tokens, - completion_tokens, - total_tokens, - prompt_tokens_details, - completion_tokens_details, - } = value.usage; - - usage.promptTokens = prompt_tokens ?? undefined; - usage.completionTokens = completion_tokens ?? undefined; - usage.totalTokens = total_tokens ?? undefined; - if (completion_tokens_details?.reasoning_tokens != null) { - usage.completionTokensDetails.reasoningTokens = - completion_tokens_details?.reasoning_tokens; - } - if ( - completion_tokens_details?.accepted_prediction_tokens != null - ) { - usage.completionTokensDetails.acceptedPredictionTokens = - completion_tokens_details?.accepted_prediction_tokens; - } - if ( - completion_tokens_details?.rejected_prediction_tokens != null - ) { - usage.completionTokensDetails.rejectedPredictionTokens = - completion_tokens_details?.rejected_prediction_tokens; - } - if (prompt_tokens_details?.cached_tokens != null) { - usage.promptTokensDetails.cachedTokens = - prompt_tokens_details?.cached_tokens; - } + usage = value.usage; } const choice = value.choices[0]; @@ -647,30 +590,24 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { ...metadataExtractor?.buildMetadata(), }; if ( - usage.completionTokensDetails.acceptedPredictionTokens != null + usage?.completion_tokens_details?.accepted_prediction_tokens != + null ) { providerMetadata[providerOptionsName].acceptedPredictionTokens = - usage.completionTokensDetails.acceptedPredictionTokens; + usage?.completion_tokens_details?.accepted_prediction_tokens; } if ( - usage.completionTokensDetails.rejectedPredictionTokens != null + usage?.completion_tokens_details?.rejected_prediction_tokens != + null ) { providerMetadata[providerOptionsName].rejectedPredictionTokens = - usage.completionTokensDetails.rejectedPredictionTokens; + usage?.completion_tokens_details?.rejected_prediction_tokens; } controller.enqueue({ type: 'finish', finishReason, - usage: { - inputTokens: usage.promptTokens ?? undefined, - outputTokens: usage.completionTokens ?? undefined, - totalTokens: usage.totalTokens ?? undefined, - reasoningTokens: - usage.completionTokensDetails.reasoningTokens ?? undefined, - cachedInputTokens: - usage.promptTokensDetails.cachedTokens ?? undefined, - }, + usage: convertOpenAICompatibleChatUsage(usage), providerMetadata, }); }, @@ -733,46 +670,44 @@ const OpenAICompatibleChatResponseSchema = z.object({ usage: openaiCompatibleTokenUsageSchema, }); +const chunkBaseSchema = z.object({ + id: z.string().nullish(), + created: z.number().nullish(), + model: z.string().nullish(), + choices: z.array( + z.object({ + delta: z + .object({ + role: z.enum(['assistant']).nullish(), + content: z.string().nullish(), + // Most openai-compatible models set `reasoning_content`, but some + // providers serving `gpt-oss` set `reasoning`. See #7866 + reasoning_content: z.string().nullish(), + reasoning: z.string().nullish(), + tool_calls: z + .array( + z.object({ + index: z.number(), + id: z.string().nullish(), + function: z.object({ + name: z.string().nullish(), + arguments: z.string().nullish(), + }), + }), + ) + .nullish(), + }) + .nullish(), + finish_reason: z.string().nullish(), + }), + ), + usage: openaiCompatibleTokenUsageSchema, +}); + // limited version of the schema, focussed on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const createOpenAICompatibleChatChunkSchema = < ERROR_SCHEMA extends z.core.$ZodType, >( errorSchema: ERROR_SCHEMA, -) => - z.union([ - z.object({ - id: z.string().nullish(), - created: z.number().nullish(), - model: z.string().nullish(), - choices: z.array( - z.object({ - delta: z - .object({ - role: z.enum(['assistant']).nullish(), - content: z.string().nullish(), - // Most openai-compatible models set `reasoning_content`, but some - // providers serving `gpt-oss` set `reasoning`. See #7866 - reasoning_content: z.string().nullish(), - reasoning: z.string().nullish(), - tool_calls: z - .array( - z.object({ - index: z.number(), - id: z.string().nullish(), - function: z.object({ - name: z.string().nullish(), - arguments: z.string().nullish(), - }), - }), - ) - .nullish(), - }) - .nullish(), - finish_reason: z.string().nullish(), - }), - ), - usage: openaiCompatibleTokenUsageSchema, - }), - errorSchema, - ]); +) => z.union([chunkBaseSchema, errorSchema]); diff --git a/packages/openai-compatible/src/completion/convert-openai-compatible-completion-usage.ts b/packages/openai-compatible/src/completion/convert-openai-compatible-completion-usage.ts new file mode 100644 index 000000000000..bc78ab031b3e --- /dev/null +++ b/packages/openai-compatible/src/completion/convert-openai-compatible-completion-usage.ts @@ -0,0 +1,46 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export function convertOpenAICompatibleCompletionUsage( + usage: + | { + prompt_tokens?: number | null; + completion_tokens?: number | null; + } + | undefined + | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.test.ts b/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.test.ts index 7fa69b8f4229..edd36d877312 100644 --- a/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.test.ts +++ b/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.test.ts @@ -136,9 +136,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -448,9 +461,22 @@ describe('doStream', () => { "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 362, - "totalTokens": 372, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 362, + "total": 362, + }, + "raw": { + "completion_tokens": 362, + "prompt_tokens": 10, + "total_tokens": 372, + }, }, }, ] @@ -490,9 +516,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -527,9 +562,18 @@ describe('doStream', () => { "finishReason": "error", "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] diff --git a/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.ts b/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.ts index 59b79dcdb1e4..5732998f759b 100644 --- a/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.ts +++ b/packages/openai-compatible/src/completion/openai-compatible-completion-language-model.ts @@ -1,11 +1,10 @@ import { APICallError, LanguageModelV3, - SharedV3Warning, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, + SharedV3Warning, } from '@ai-sdk/provider'; import { combineHeaders, @@ -23,6 +22,7 @@ import { defaultOpenAICompatibleErrorStructure, ProviderErrorStructure, } from '../openai-compatible-error'; +import { convertOpenAICompatibleCompletionUsage } from './convert-openai-compatible-completion-usage'; import { convertToOpenAICompatibleCompletionPrompt } from './convert-to-openai-compatible-completion-prompt'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason'; @@ -197,11 +197,7 @@ export class OpenAICompatibleCompletionLanguageModel return { content, - usage: { - inputTokens: response.usage?.prompt_tokens ?? undefined, - outputTokens: response.usage?.completion_tokens ?? undefined, - totalTokens: response.usage?.total_tokens ?? undefined, - }, + usage: convertOpenAICompatibleCompletionUsage(response.usage), finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason), request: { body: args }, response: { @@ -244,11 +240,13 @@ export class OpenAICompatibleCompletionLanguageModel }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: + | { + prompt_tokens: number | undefined; + completion_tokens: number | undefined; + total_tokens: number | undefined; + } + | undefined = undefined; let isFirstChunk = true; return { @@ -297,9 +295,7 @@ export class OpenAICompatibleCompletionLanguageModel } if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens ?? undefined; - usage.outputTokens = value.usage.completion_tokens ?? undefined; - usage.totalTokens = value.usage.total_tokens ?? undefined; + usage = value.usage; } const choice = value.choices[0]; @@ -327,7 +323,7 @@ export class OpenAICompatibleCompletionLanguageModel controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertOpenAICompatibleCompletionUsage(usage), }); }, }), diff --git a/packages/openai/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap b/packages/openai/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap index 4c03978d78c8..6f0d3f2e7957 100644 --- a/packages/openai/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap +++ b/packages/openai/src/chat/__snapshots__/openai-chat-language-model.test.ts.snap @@ -55,11 +55,30 @@ exports[`doStream > should set .modelId for model-router request 1`] = ` }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 15, - "outputTokens": 78, - "reasoningTokens": 64, - "totalTokens": 93, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 64, + "text": 14, + "total": 78, + }, + "raw": { + "completion_tokens": 78, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "reasoning_tokens": 64, + "rejected_prediction_tokens": 0, + }, + "prompt_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 93, + }, }, }, ] diff --git a/packages/openai/src/chat/convert-openai-chat-usage.ts b/packages/openai/src/chat/convert-openai-chat-usage.ts new file mode 100644 index 000000000000..9e62acd1b933 --- /dev/null +++ b/packages/openai/src/chat/convert-openai-chat-usage.ts @@ -0,0 +1,57 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type OpenAIChatUsage = { + prompt_tokens?: number | null; + completion_tokens?: number | null; + total_tokens?: number | null; + prompt_tokens_details?: { + cached_tokens?: number | null; + } | null; + completion_tokens_details?: { + reasoning_tokens?: number | null; + accepted_prediction_tokens?: number | null; + rejected_prediction_tokens?: number | null; + } | null; +}; + +export function convertOpenAIChatUsage( + usage: OpenAIChatUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = + usage.completion_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens - cachedTokens, + cacheRead: cachedTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/openai/src/chat/openai-chat-language-model.test.ts b/packages/openai/src/chat/openai-chat-language-model.test.ts index d6235601154f..abec089317ec 100644 --- a/packages/openai/src/chat/openai-chat-language-model.test.ts +++ b/packages/openai/src/chat/openai-chat-language-model.test.ts @@ -257,11 +257,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "reasoningTokens": undefined, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -370,11 +381,21 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": 20, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0, + }, + "raw": { + "prompt_tokens": 20, + "total_tokens": 20, + }, } `); }); @@ -1126,11 +1147,25 @@ describe('doGenerate', () => { expect(result.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 1152, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 1152, + "cacheWrite": undefined, + "noCache": -1137, + "total": 15, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 1152, + }, + "total_tokens": 35, + }, } `); }); @@ -1268,11 +1303,25 @@ describe('doGenerate', () => { expect(result.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 10, + "text": 10, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, } `); }); @@ -1889,11 +1938,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "reasoningTokens": undefined, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": 0, + "text": 227, + "total": 227, + }, + "raw": { + "completion_tokens": 227, + "prompt_tokens": 17, + "total_tokens": 244, + }, }, }, ] @@ -2075,11 +2135,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 53, - "outputTokens": 17, - "reasoningTokens": undefined, - "totalTokens": 70, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 53, + "total": 53, + }, + "outputTokens": { + "reasoning": 0, + "text": 17, + "total": 17, + }, + "raw": { + "completion_tokens": 17, + "prompt_tokens": 53, + "total_tokens": 70, + }, }, }, ] @@ -2215,11 +2286,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 53, - "outputTokens": 17, - "reasoningTokens": undefined, - "totalTokens": 70, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 53, + "total": 53, + }, + "outputTokens": { + "reasoning": 0, + "text": 17, + "total": 17, + }, + "raw": { + "completion_tokens": 17, + "prompt_tokens": 53, + "total_tokens": 70, + }, }, }, ] @@ -2359,11 +2441,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 226, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": 246, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 226, + "total": 226, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 226, + "total_tokens": 246, + }, }, }, ] @@ -2443,11 +2536,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 53, - "outputTokens": 17, - "reasoningTokens": undefined, - "totalTokens": 70, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 53, + "total": 53, + }, + "outputTokens": { + "reasoning": 0, + "text": 17, + "total": 17, + }, + "raw": { + "completion_tokens": 17, + "prompt_tokens": 53, + "total_tokens": 70, + }, }, }, ] @@ -2491,9 +2595,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2531,9 +2644,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -2694,11 +2816,25 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 1152, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 1152, + "cacheWrite": undefined, + "noCache": -1137, + "total": 15, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 15, + "prompt_tokens_details": { + "cached_tokens": 1152, + }, + "total_tokens": 35, + }, }, } `); @@ -2742,11 +2878,26 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "accepted_prediction_tokens": 123, + "rejected_prediction_tokens": 456, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, }, } `); @@ -2927,11 +3078,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 17, - "outputTokens": 227, - "reasoningTokens": undefined, - "totalTokens": 244, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 17, + "total": 17, + }, + "outputTokens": { + "reasoning": 0, + "text": 227, + "total": 227, + }, + "raw": { + "completion_tokens": 227, + "prompt_tokens": 17, + "total_tokens": 244, + }, }, }, ] @@ -2996,11 +3158,25 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 10, + "text": 10, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, }, }, ] diff --git a/packages/openai/src/chat/openai-chat-language-model.ts b/packages/openai/src/chat/openai-chat-language-model.ts index ff594826f642..ce53c63d80cb 100644 --- a/packages/openai/src/chat/openai-chat-language-model.ts +++ b/packages/openai/src/chat/openai-chat-language-model.ts @@ -5,7 +5,6 @@ import { LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, SharedV3ProviderMetadata, SharedV3Warning, } from '@ai-sdk/provider'; @@ -22,6 +21,10 @@ import { } from '@ai-sdk/provider-utils'; import { openaiFailedResponseHandler } from '../openai-error'; import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities'; +import { + convertOpenAIChatUsage, + OpenAIChatUsage, +} from './convert-openai-chat-usage'; import { convertToOpenAIChatMessages } from './convert-to-openai-chat-messages'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAIFinishReason } from './map-openai-finish-reason'; @@ -375,13 +378,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapOpenAIFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage?.prompt_tokens ?? undefined, - outputTokens: response.usage?.completion_tokens ?? undefined, - totalTokens: response.usage?.total_tokens ?? undefined, - reasoningTokens: completionTokenDetails?.reasoning_tokens ?? undefined, - cachedInputTokens: promptTokenDetails?.cached_tokens ?? undefined, - }, + usage: convertOpenAIChatUsage(response.usage), request: { body }, response: { ...getResponseMetadata(response), @@ -432,11 +429,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 { }> = []; let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: OpenAIChatUsage | undefined = undefined; let metadataExtracted = false; let isActiveText = false; @@ -488,14 +481,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 { } if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens ?? undefined; - usage.outputTokens = value.usage.completion_tokens ?? undefined; - usage.totalTokens = value.usage.total_tokens ?? undefined; - usage.reasoningTokens = - value.usage.completion_tokens_details?.reasoning_tokens ?? - undefined; - usage.cachedInputTokens = - value.usage.prompt_tokens_details?.cached_tokens ?? undefined; + usage = value.usage; if ( value.usage.completion_tokens_details @@ -684,7 +670,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertOpenAIChatUsage(usage), ...(providerMetadata != null ? { providerMetadata } : {}), }); }, diff --git a/packages/openai/src/completion/convert-openai-completion-usage.ts b/packages/openai/src/completion/convert-openai-completion-usage.ts new file mode 100644 index 000000000000..3b4dc14dfa95 --- /dev/null +++ b/packages/openai/src/completion/convert-openai-completion-usage.ts @@ -0,0 +1,46 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type OpenAICompletionUsage = { + prompt_tokens?: number | null; + completion_tokens?: number | null; + total_tokens?: number | null; +}; + +export function convertOpenAICompletionUsage( + usage: OpenAICompletionUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + + return { + inputTokens: { + total: usage.prompt_tokens ?? undefined, + noCache: promptTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.completion_tokens ?? undefined, + text: completionTokens, + reasoning: undefined, + }, + raw: usage, + }; +} diff --git a/packages/openai/src/completion/openai-completion-language-model.test.ts b/packages/openai/src/completion/openai-completion-language-model.test.ts index f97d43705466..b24a44b230d5 100644 --- a/packages/openai/src/completion/openai-completion-language-model.test.ts +++ b/packages/openai/src/completion/openai-completion-language-model.test.ts @@ -133,9 +133,22 @@ describe('doGenerate', () => { expect(usage).toMatchInlineSnapshot(` { - "inputTokens": 20, - "outputTokens": 5, - "totalTokens": 25, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": undefined, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -468,9 +481,22 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 362, - "totalTokens": 372, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": undefined, + "text": 362, + "total": 362, + }, + "raw": { + "completion_tokens": 362, + "prompt_tokens": 10, + "total_tokens": 372, + }, }, }, ] @@ -514,9 +540,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -554,9 +589,18 @@ describe('doStream', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] diff --git a/packages/openai/src/completion/openai-completion-language-model.ts b/packages/openai/src/completion/openai-completion-language-model.ts index 50a73f5c8de0..be8e978760c1 100644 --- a/packages/openai/src/completion/openai-completion-language-model.ts +++ b/packages/openai/src/completion/openai-completion-language-model.ts @@ -3,7 +3,6 @@ import { SharedV3Warning, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, SharedV3ProviderMetadata, } from '@ai-sdk/provider'; import { @@ -16,6 +15,10 @@ import { postJsonToApi, } from '@ai-sdk/provider-utils'; import { openaiFailedResponseHandler } from '../openai-error'; +import { + convertOpenAICompletionUsage, + OpenAICompletionUsage, +} from './convert-openai-completion-usage'; import { convertToOpenAICompletionPrompt } from './convert-to-openai-completion-prompt'; import { getResponseMetadata } from './get-response-metadata'; import { mapOpenAIFinishReason } from './map-openai-finish-reason'; @@ -188,11 +191,7 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 { return { content: [{ type: 'text', text: choice.text }], - usage: { - inputTokens: response.usage?.prompt_tokens, - outputTokens: response.usage?.completion_tokens, - totalTokens: response.usage?.total_tokens, - }, + usage: convertOpenAICompletionUsage(response.usage), finishReason: mapOpenAIFinishReason(choice.finish_reason), request: { body: args }, response: { @@ -236,11 +235,7 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 { let finishReason: LanguageModelV3FinishReason = 'unknown'; const providerMetadata: SharedV3ProviderMetadata = { openai: {} }; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: OpenAICompletionUsage | undefined = undefined; let isFirstChunk = true; return { @@ -286,9 +281,7 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 { } if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens; - usage.outputTokens = value.usage.completion_tokens; - usage.totalTokens = value.usage.total_tokens; + usage = value.usage; } const choice = value.choices[0]; @@ -319,7 +312,7 @@ export class OpenAICompletionLanguageModel implements LanguageModelV3 { type: 'finish', finishReason, providerMetadata, - usage, + usage: convertOpenAICompletionUsage(usage), }); }, }), diff --git a/packages/openai/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap b/packages/openai/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap index f88bb6345f64..ce193ca07daa 100644 --- a/packages/openai/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap +++ b/packages/openai/src/responses/__snapshots__/openai-responses-language-model.test.ts.snap @@ -823,11 +823,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > apply_patch tool streaming > }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 642, - "outputTokens": 67, - "reasoningTokens": 0, - "totalTokens": 709, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 642, + "total": 642, + }, + "outputTokens": { + "reasoning": 0, + "text": 67, + "total": 67, + }, + "raw": { + "input_tokens": 642, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 67, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -2899,11 +2915,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > code interpreter tool > shoul }, "type": "finish", "usage": { - "cachedInputTokens": 2944, - "inputTokens": 6047, - "outputTokens": 1623, - "reasoningTokens": 1408, - "totalTokens": 7670, + "inputTokens": { + "cacheRead": 2944, + "cacheWrite": undefined, + "noCache": 3103, + "total": 6047, + }, + "outputTokens": { + "reasoning": 1408, + "text": 215, + "total": 1623, + }, + "raw": { + "input_tokens": 6047, + "input_tokens_details": { + "cached_tokens": 2944, + }, + "output_tokens": 1623, + "output_tokens_details": { + "reasoning_tokens": 1408, + }, + }, }, }, ] @@ -3451,11 +3483,27 @@ providers and models, and which ones are available in the AI SDK.", }, "type": "finish", "usage": { - "cachedInputTokens": 2304, - "inputTokens": 3748, - "outputTokens": 543, - "reasoningTokens": 448, - "totalTokens": 4291, + "inputTokens": { + "cacheRead": 2304, + "cacheWrite": undefined, + "noCache": 1444, + "total": 3748, + }, + "outputTokens": { + "reasoning": 448, + "text": 95, + "total": 543, + }, + "raw": { + "input_tokens": 3748, + "input_tokens_details": { + "cached_tokens": 2304, + }, + "output_tokens": 543, + "output_tokens_details": { + "reasoning_tokens": 448, + }, + }, }, }, ] @@ -3976,11 +4024,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > file search tool > should str }, "type": "finish", "usage": { - "cachedInputTokens": 2304, - "inputTokens": 3737, - "outputTokens": 621, - "reasoningTokens": 512, - "totalTokens": 4358, + "inputTokens": { + "cacheRead": 2304, + "cacheWrite": undefined, + "noCache": 1433, + "total": 3737, + }, + "outputTokens": { + "reasoning": 512, + "text": 109, + "total": 621, + }, + "raw": { + "input_tokens": 3737, + "input_tokens_details": { + "cached_tokens": 2304, + }, + "output_tokens": 621, + "output_tokens_details": { + "reasoning_tokens": 512, + }, + }, }, }, ] @@ -4070,11 +4134,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > image generation tool > shoul }, "type": "finish", "usage": { - "cachedInputTokens": 1920, - "inputTokens": 2941, - "outputTokens": 1249, - "reasoningTokens": 1024, - "totalTokens": 4190, + "inputTokens": { + "cacheRead": 1920, + "cacheWrite": undefined, + "noCache": 1021, + "total": 2941, + }, + "outputTokens": { + "reasoning": 1024, + "text": 225, + "total": 1249, + }, + "raw": { + "input_tokens": 2941, + "input_tokens_details": { + "cached_tokens": 1920, + }, + "output_tokens": 1249, + "output_tokens_details": { + "reasoning_tokens": 1024, + }, + }, }, }, ] @@ -4133,11 +4213,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > local shell tool > should str }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 407, - "outputTokens": 151, - "reasoningTokens": 128, - "totalTokens": 558, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 407, + "total": 407, + }, + "outputTokens": { + "reasoning": 128, + "text": 23, + "total": 151, + }, + "raw": { + "input_tokens": 407, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 151, + "output_tokens_details": { + "reasoning_tokens": 128, + }, + }, }, }, ] @@ -6083,11 +6179,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > mcp tool > should stream mcp }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 11791, - "outputTokens": 963, - "reasoningTokens": 512, - "totalTokens": 12754, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 11791, + "total": 11791, + }, + "outputTokens": { + "reasoning": 512, + "text": 451, + "total": 963, + }, + "raw": { + "input_tokens": 11791, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 963, + "output_tokens_details": { + "reasoning_tokens": 512, + }, + }, }, }, ] @@ -6978,11 +7090,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > shell tool > should stream sh }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 331, - "outputTokens": 166, - "reasoningTokens": 0, - "totalTokens": 497, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 331, + "total": 331, + }, + "outputTokens": { + "reasoning": 0, + "text": 166, + "total": 166, + }, + "raw": { + "input_tokens": 331, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 166, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -7060,11 +7188,27 @@ exports[`OpenAIResponsesLanguageModel > doStream > web search tool > should hand }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 50, - "outputTokens": 25, - "reasoningTokens": 0, - "totalTokens": 75, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 50, + "total": 50, + }, + "outputTokens": { + "reasoning": 0, + "text": 25, + "total": 25, + }, + "raw": { + "input_tokens": 50, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 25, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -7550,11 +7694,27 @@ Would you like me to pull live updates or focus on a specific topic (arts,", }, "type": "finish", "usage": { - "cachedInputTokens": 34560, - "inputTokens": 60093, - "outputTokens": 4080, - "reasoningTokens": 3648, - "totalTokens": 64173, + "inputTokens": { + "cacheRead": 34560, + "cacheWrite": undefined, + "noCache": 25533, + "total": 60093, + }, + "outputTokens": { + "reasoning": 3648, + "text": 432, + "total": 4080, + }, + "raw": { + "input_tokens": 60093, + "input_tokens_details": { + "cached_tokens": 34560, + }, + "output_tokens": 4080, + "output_tokens_details": { + "reasoning_tokens": 3648, + }, + }, }, }, ] diff --git a/packages/openai/src/responses/convert-openai-responses-usage.ts b/packages/openai/src/responses/convert-openai-responses-usage.ts new file mode 100644 index 000000000000..b40b9dec69b9 --- /dev/null +++ b/packages/openai/src/responses/convert-openai-responses-usage.ts @@ -0,0 +1,53 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export type OpenAIResponsesUsage = { + input_tokens: number; + output_tokens: number; + input_tokens_details?: { + cached_tokens?: number | null; + } | null; + output_tokens_details?: { + reasoning_tokens?: number | null; + } | null; +}; + +export function convertOpenAIResponsesUsage( + usage: OpenAIResponsesUsage | undefined | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const inputTokens = usage.input_tokens; + const outputTokens = usage.output_tokens; + const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: inputTokens, + noCache: inputTokens - cachedTokens, + cacheRead: cachedTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: outputTokens, + text: outputTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/openai/src/responses/openai-responses-language-model.test.ts b/packages/openai/src/responses/openai-responses-language-model.test.ts index 4ee5aa0661d0..34c8552b81a9 100644 --- a/packages/openai/src/responses/openai-responses-language-model.test.ts +++ b/packages/openai/src/responses/openai-responses-language-model.test.ts @@ -187,11 +187,27 @@ describe('OpenAIResponsesLanguageModel', () => { expect(result.usage).toMatchInlineSnapshot(` { - "cachedInputTokens": 234, - "inputTokens": 345, - "outputTokens": 538, - "reasoningTokens": 123, - "totalTokens": 883, + "inputTokens": { + "cacheRead": 234, + "cacheWrite": undefined, + "noCache": 111, + "total": 345, + }, + "outputTokens": { + "reasoning": 123, + "text": 415, + "total": 538, + }, + "raw": { + "input_tokens": 345, + "input_tokens_details": { + "cached_tokens": 234, + }, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 123, + }, + }, } `); }); @@ -3506,11 +3522,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 234, - "inputTokens": 543, - "outputTokens": 478, - "reasoningTokens": 123, - "totalTokens": 1021, + "inputTokens": { + "cacheRead": 234, + "cacheWrite": undefined, + "noCache": 309, + "total": 543, + }, + "outputTokens": { + "reasoning": 123, + "text": 355, + "total": 478, + }, + "raw": { + "input_tokens": 543, + "input_tokens_details": { + "cached_tokens": 234, + }, + "output_tokens": 478, + "output_tokens_details": { + "reasoning_tokens": 123, + }, + }, }, }, ] @@ -3582,11 +3614,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 0, - "outputTokens": 0, - "reasoningTokens": 0, - "totalTokens": 0, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 0, + "total": 0, + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0, + }, + "raw": { + "input_tokens": 0, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 0, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -3712,11 +3760,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 0, - "outputTokens": 0, - "reasoningTokens": 0, - "totalTokens": 0, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 0, + "total": 0, + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0, + }, + "raw": { + "input_tokens": 0, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 0, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -3815,11 +3879,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 15, - "outputTokens": 263, - "reasoningTokens": 256, - "totalTokens": 278, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 256, + "text": 7, + "total": 263, + }, + "raw": { + "input_tokens": 15, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 263, + "output_tokens_details": { + "reasoning_tokens": 256, + }, + }, }, }, ] @@ -3926,11 +4006,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 12, - "outputTokens": 2, - "reasoningTokens": 0, - "totalTokens": 14, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 12, + "total": 12, + }, + "outputTokens": { + "reasoning": 0, + "text": 2, + "total": 2, + }, + "raw": { + "input_tokens": 12, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 2, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -4226,9 +4322,18 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] @@ -4404,11 +4509,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 34, - "outputTokens": 538, - "reasoningTokens": 320, - "totalTokens": 572, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": 320, + "text": 218, + "total": 538, + }, + "raw": { + "input_tokens": 34, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 320, + }, + }, }, }, ] @@ -4520,11 +4641,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 34, - "outputTokens": 538, - "reasoningTokens": 320, - "totalTokens": 572, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": 320, + "text": 218, + "total": 538, + }, + "raw": { + "input_tokens": 34, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 320, + }, + }, }, }, ] @@ -4707,11 +4844,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 34, - "outputTokens": 538, - "reasoningTokens": 320, - "totalTokens": 572, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": 320, + "text": 218, + "total": 538, + }, + "raw": { + "input_tokens": 34, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 320, + }, + }, }, }, ] @@ -4825,11 +4978,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 34, - "outputTokens": 538, - "reasoningTokens": 320, - "totalTokens": 572, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 34, + "total": 34, + }, + "outputTokens": { + "reasoning": 320, + "text": 218, + "total": 538, + }, + "raw": { + "input_tokens": 34, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 320, + }, + }, }, }, ] @@ -5094,11 +5263,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 45, - "outputTokens": 628, - "reasoningTokens": 420, - "totalTokens": 673, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 45, + "total": 45, + }, + "outputTokens": { + "reasoning": 420, + "text": 208, + "total": 628, + }, + "raw": { + "input_tokens": 45, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 628, + "output_tokens_details": { + "reasoning_tokens": 420, + }, + }, }, }, ] @@ -5366,11 +5551,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 100, - "outputTokens": 50, - "reasoningTokens": 0, - "totalTokens": 150, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 100, + "total": 100, + }, + "outputTokens": { + "reasoning": 0, + "text": 50, + "total": 50, + }, + "raw": { + "input_tokens": 100, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 50, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -5459,11 +5660,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 0, - "inputTokens": 50, - "outputTokens": 25, - "reasoningTokens": 0, - "totalTokens": 75, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 50, + "total": 50, + }, + "outputTokens": { + "reasoning": 0, + "text": 25, + "total": 25, + }, + "raw": { + "input_tokens": 50, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 25, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + }, }, }, ] @@ -5656,11 +5873,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 2944, - "inputTokens": 6047, - "outputTokens": 1623, - "reasoningTokens": 1408, - "totalTokens": 7670, + "inputTokens": { + "cacheRead": 2944, + "cacheWrite": undefined, + "noCache": 3103, + "total": 6047, + }, + "outputTokens": { + "reasoning": 1408, + "text": 215, + "total": 1623, + }, + "raw": { + "input_tokens": 6047, + "input_tokens_details": { + "cached_tokens": 2944, + }, + "output_tokens": 1623, + "output_tokens_details": { + "reasoning_tokens": 1408, + }, + }, }, }, ] @@ -5844,11 +6077,27 @@ describe('OpenAIResponsesLanguageModel', () => { }, "type": "finish", "usage": { - "cachedInputTokens": 2944, - "inputTokens": 6047, - "outputTokens": 1623, - "reasoningTokens": 1408, - "totalTokens": 7670, + "inputTokens": { + "cacheRead": 2944, + "cacheWrite": undefined, + "noCache": 3103, + "total": 6047, + }, + "outputTokens": { + "reasoning": 1408, + "text": 215, + "total": 1623, + }, + "raw": { + "input_tokens": 6047, + "input_tokens_details": { + "cached_tokens": 2944, + }, + "output_tokens": 1623, + "output_tokens_details": { + "reasoning_tokens": 1408, + }, + }, }, }, ] diff --git a/packages/openai/src/responses/openai-responses-language-model.ts b/packages/openai/src/responses/openai-responses-language-model.ts index be6f4565de2f..c4d3c9621d39 100644 --- a/packages/openai/src/responses/openai-responses-language-model.ts +++ b/packages/openai/src/responses/openai-responses-language-model.ts @@ -6,7 +6,6 @@ import { LanguageModelV3FinishReason, LanguageModelV3ProviderTool, LanguageModelV3StreamPart, - LanguageModelV3Usage, SharedV3ProviderMetadata, JSONValue, } from '@ai-sdk/provider'; @@ -34,6 +33,10 @@ import { localShellInputSchema } from '../tool/local-shell'; import { shellInputSchema } from '../tool/shell'; import { webSearchOutputSchema } from '../tool/web-search'; import { mcpOutputSchema } from '../tool/mcp'; +import { + convertOpenAIResponsesUsage, + OpenAIResponsesUsage, +} from './convert-openai-responses-usage'; import { convertToOpenAIResponsesInput } from './convert-to-openai-responses-input'; import { mapOpenAIResponseFinishReason } from './map-openai-responses-finish-reason'; import { @@ -832,15 +835,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { finishReason: response.incomplete_details?.reason, hasFunctionCall, }), - usage: { - inputTokens: usage.input_tokens, - outputTokens: usage.output_tokens, - totalTokens: usage.input_tokens + usage.output_tokens, - reasoningTokens: - usage.output_tokens_details?.reasoning_tokens ?? undefined, - cachedInputTokens: - usage.input_tokens_details?.cached_tokens ?? undefined, - }, + usage: convertOpenAIResponsesUsage(usage), request: { body }, response: { id: response.id, @@ -887,11 +882,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { const providerKey = this.config.provider.replace('.responses', ''); // can be 'openai' or 'azure'. provider is 'openai.responses' or 'azure.responses'. let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: OpenAIResponsesUsage | undefined = undefined; const logprobs: Array = []; let responseId: string | null = null; const ongoingToolCalls: Record< @@ -1535,17 +1526,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { finishReason: value.response.incomplete_details?.reason, hasFunctionCall, }); - usage.inputTokens = value.response.usage.input_tokens; - usage.outputTokens = value.response.usage.output_tokens; - usage.totalTokens = - value.response.usage.input_tokens + - value.response.usage.output_tokens; - usage.reasoningTokens = - value.response.usage.output_tokens_details?.reasoning_tokens ?? - undefined; - usage.cachedInputTokens = - value.response.usage.input_tokens_details?.cached_tokens ?? - undefined; + usage = value.response.usage; if (typeof value.response.service_tier === 'string') { serviceTier = value.response.service_tier; } @@ -1644,7 +1625,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertOpenAIResponsesUsage(usage), providerMetadata, }); }, diff --git a/packages/perplexity/src/convert-perplexity-usage.ts b/packages/perplexity/src/convert-perplexity-usage.ts new file mode 100644 index 000000000000..adce0c73de63 --- /dev/null +++ b/packages/perplexity/src/convert-perplexity-usage.ts @@ -0,0 +1,48 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; + +export function convertPerplexityUsage( + usage: + | { + prompt_tokens?: number | null | undefined; + completion_tokens?: number | null | undefined; + reasoning_tokens?: number | null | undefined; + } + | undefined + | null, +): LanguageModelV3Usage { + if (usage == null) { + return { + inputTokens: { + total: undefined, + noCache: undefined, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined, + }, + raw: undefined, + }; + } + + const promptTokens = usage.prompt_tokens ?? 0; + const completionTokens = usage.completion_tokens ?? 0; + const reasoningTokens = usage.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: promptTokens, + noCache: promptTokens, + cacheRead: undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: completionTokens, + text: completionTokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/perplexity/src/perplexity-language-model.test.ts b/packages/perplexity/src/perplexity-language-model.test.ts index 95d9ee0297ec..7bc043d27b52 100644 --- a/packages/perplexity/src/perplexity-language-model.test.ts +++ b/packages/perplexity/src/perplexity-language-model.test.ts @@ -106,7 +106,25 @@ describe('PerplexityLanguageModel', () => { ] `); - expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20 }); + expect(result.usage).toMatchInlineSnapshot(` + { + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 10, + }, + } + `); expect({ id: result.response?.id, @@ -324,21 +342,41 @@ describe('PerplexityLanguageModel', () => { prompt: TEST_PROMPT, }); - expect(result.usage).toEqual({ - inputTokens: 10, - outputTokens: 20, - reasoningTokens: 50, - }); - - expect(result.providerMetadata).toEqual({ - perplexity: { - images: null, - usage: { - citationTokens: 30, - numSearchQueries: 40, + expect(result.usage).toMatchInlineSnapshot(` + { + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, }, - }, - }); + "outputTokens": { + "reasoning": 50, + "text": -30, + "total": 20, + }, + "raw": { + "citation_tokens": 30, + "completion_tokens": 20, + "num_search_queries": 40, + "prompt_tokens": 10, + "reasoning_tokens": 50, + }, + } + `); + expect(result.providerMetadata).toMatchInlineSnapshot( + ` + { + "perplexity": { + "images": null, + "usage": { + "citationTokens": 30, + "numSearchQueries": 40, + }, + }, + } + `, + ); }); it('should pass headers from provider and request', async () => { @@ -506,10 +544,21 @@ describe('PerplexityLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 10, + }, }, }, ] @@ -589,10 +638,21 @@ describe('PerplexityLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 10, + }, }, }, ] @@ -688,10 +748,21 @@ describe('PerplexityLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 10, - "outputTokens": 20, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 10, + }, }, }, ] @@ -765,10 +836,24 @@ describe('PerplexityLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": 11, - "outputTokens": 21, - "reasoningTokens": 50, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": 11, + "total": 11, + }, + "outputTokens": { + "reasoning": 50, + "text": -29, + "total": 21, + }, + "raw": { + "citation_tokens": 30, + "completion_tokens": 21, + "num_search_queries": 40, + "prompt_tokens": 11, + "reasoning_tokens": 50, + }, }, }, ] @@ -980,10 +1065,18 @@ describe('PerplexityLanguageModel', () => { }, "type": "finish", "usage": { - "inputTokens": undefined, - "outputTokens": undefined, - "reasoningTokens": undefined, - "totalTokens": undefined, + "inputTokens": { + "cacheRead": undefined, + "cacheWrite": undefined, + "noCache": undefined, + "total": undefined, + }, + "outputTokens": { + "reasoning": undefined, + "text": undefined, + "total": undefined, + }, + "raw": undefined, }, }, ] diff --git a/packages/perplexity/src/perplexity-language-model.ts b/packages/perplexity/src/perplexity-language-model.ts index e2c4aa8466c2..c0e994cb2fe1 100644 --- a/packages/perplexity/src/perplexity-language-model.ts +++ b/packages/perplexity/src/perplexity-language-model.ts @@ -1,10 +1,9 @@ import { LanguageModelV3, - SharedV3Warning, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, - LanguageModelV3Usage, + SharedV3Warning, } from '@ai-sdk/provider'; import { FetchFunction, @@ -16,6 +15,7 @@ import { postJsonToApi, } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; +import { convertPerplexityUsage } from './convert-perplexity-usage'; import { convertToPerplexityMessages } from './convert-to-perplexity-messages'; import { mapPerplexityFinishReason } from './map-perplexity-finish-reason'; import { PerplexityLanguageModelId } from './perplexity-language-model-options'; @@ -154,12 +154,7 @@ export class PerplexityLanguageModel implements LanguageModelV3 { return { content, finishReason: mapPerplexityFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage?.prompt_tokens, - outputTokens: response.usage?.completion_tokens, - totalTokens: response.usage?.total_tokens ?? undefined, - reasoningTokens: response.usage?.reasoning_tokens ?? undefined, - }, + usage: convertPerplexityUsage(response.usage), request: { body }, response: { ...getResponseMetadata(response), @@ -208,12 +203,13 @@ export class PerplexityLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - reasoningTokens: undefined, - }; + let usage: + | { + prompt_tokens: number | undefined; + completion_tokens: number | undefined; + reasoning_tokens?: number | null | undefined; + } + | undefined = undefined; const providerMetadata: { perplexity: { @@ -284,9 +280,7 @@ export class PerplexityLanguageModel implements LanguageModelV3 { } if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens; - usage.outputTokens = value.usage.completion_tokens; - usage.reasoningTokens = value.usage.reasoning_tokens ?? undefined; + usage = value.usage; providerMetadata.perplexity.usage = { citationTokens: value.usage.citation_tokens ?? null, @@ -337,7 +331,7 @@ export class PerplexityLanguageModel implements LanguageModelV3 { controller.enqueue({ type: 'finish', finishReason, - usage, + usage: convertPerplexityUsage(usage), providerMetadata, }); }, diff --git a/packages/provider/src/language-model/v3/language-model-v3-usage.ts b/packages/provider/src/language-model/v3/language-model-v3-usage.ts index 11e1cae7a25b..dbd2a174fc4d 100644 --- a/packages/provider/src/language-model/v3/language-model-v3-usage.ts +++ b/packages/provider/src/language-model/v3/language-model-v3-usage.ts @@ -1,34 +1,59 @@ -/** -Usage information for a language model call. +import { JSONObject } from '../../json-value'; -If your API return additional usage information, you can add it to the -provider metadata under your provider's key. +/** + * Usage information for a language model call. */ export type LanguageModelV3Usage = { /** -The number of input (prompt) tokens used. + * Information about the input tokens. */ - inputTokens: number | undefined; + inputTokens: { + /** + *The total number of input (prompt) tokens used. + */ + total: number | undefined; - /** -The number of output (completion) tokens used. - */ - outputTokens: number | undefined; + /** + * The number of non-cached input (prompt) tokens used. + */ + noCache: number | undefined; - /** -The total number of tokens as reported by the provider. -This number might be different from the sum of `inputTokens` and `outputTokens` -and e.g. include reasoning tokens or other overhead. - */ - totalTokens: number | undefined; + /** + * The number of cached input (prompt) tokens read. + */ + cacheRead: number | undefined; + + /** + * The number of cached input (prompt) tokens written. + */ + cacheWrite: number | undefined; + }; /** -The number of reasoning tokens used. + * Information about the output tokens. */ - reasoningTokens?: number | undefined; + outputTokens: { + /** + * The total number of output (completion) tokens used. + */ + total: number | undefined; + + /** + * The number of text tokens used. + */ + text: number | undefined; + + /** + * The number of reasoning tokens used. + */ + reasoning: number | undefined; + }; /** -The number of cached input tokens. + * Raw usage information from the provider. + * + * This is the usage information in the shape that the provider returns. + * It can include additional information that is not part of the standard usage information. */ - cachedInputTokens?: number | undefined; + raw?: JSONObject; }; diff --git a/packages/rsc/src/stream-ui/stream-ui.tsx b/packages/rsc/src/stream-ui/stream-ui.tsx index 1982df4e84dc..8a90a2770b20 100644 --- a/packages/rsc/src/stream-ui/stream-ui.tsx +++ b/packages/rsc/src/stream-ui/stream-ui.tsx @@ -1,34 +1,39 @@ -import { LanguageModelV3, SharedV3Warning } from '@ai-sdk/provider'; +import { + LanguageModelV3, + LanguageModelV3Usage, + SharedV3Warning, +} from '@ai-sdk/provider'; import { InferSchema, ProviderOptions, safeParseJSON, } from '@ai-sdk/provider-utils'; -import { ReactNode } from 'react'; -import * as z3 from 'zod/v3'; -import * as z4 from 'zod/v4'; import { + CallSettings, CallWarning, FinishReason, - LanguageModelUsage, - ToolChoice, - Prompt, - CallSettings, InvalidToolInputError, + LanguageModelUsage, NoSuchToolError, + Prompt, Schema, + ToolChoice, } from 'ai'; import { - standardizePrompt, - prepareToolsAndToolChoice, - prepareRetries, - prepareCallSettings, + asLanguageModelUsage, convertToLanguageModelPrompt, + prepareCallSettings, + prepareRetries, + prepareToolsAndToolChoice, + standardizePrompt, } from 'ai/internal'; +import { ReactNode } from 'react'; +import * as z3 from 'zod/v3'; +import * as z4 from 'zod/v4'; +import { createStreamableUI } from '../streamable-ui/create-streamable-ui'; import { createResolvablePromise } from '../util/create-resolvable-promise'; import { isAsyncGenerator } from '../util/is-async-generator'; import { isGenerator } from '../util/is-generator'; -import { createStreamableUI } from '../streamable-ui/create-streamable-ui'; type Streamable = ReactNode | Promise; @@ -200,7 +205,7 @@ functionality that can be fully encapsulated in the provider. let finishEvent: { finishReason: FinishReason; - usage: LanguageModelUsage; + usage: LanguageModelV3Usage; warnings?: CallWarning[]; response?: { headers?: Record; @@ -394,6 +399,7 @@ functionality that can be fully encapsulated in the provider. if (finishEvent && onFinish) { await onFinish({ ...finishEvent, + usage: asLanguageModelUsage(finishEvent.usage), value: ui.value, }); } diff --git a/packages/rsc/src/stream-ui/stream-ui.ui.test.tsx b/packages/rsc/src/stream-ui/stream-ui.ui.test.tsx index c5f4c0fe1f2a..2eb746fb12b9 100644 --- a/packages/rsc/src/stream-ui/stream-ui.ui.test.tsx +++ b/packages/rsc/src/stream-ui/stream-ui.ui.test.tsx @@ -1,10 +1,11 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; import { delay } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream } from '@ai-sdk/provider-utils/test'; -import { LanguageModelUsage } from 'ai'; +import { asLanguageModelUsage } from 'ai/internal'; import { MockLanguageModelV3 } from 'ai/test'; +import { beforeEach, describe, expect, it } from 'vitest'; import { z } from 'zod/v4'; import { streamUI } from './stream-ui'; -import { describe, it, expect, beforeEach } from 'vitest'; async function recursiveResolve(val: any): Promise { if (val && typeof val === 'object' && typeof val.then === 'function') { @@ -52,10 +53,18 @@ async function simulateFlightServerRender(node: React.ReactNode) { return traverse(node); } -const testUsage: LanguageModelUsage = { - inputTokens: 3, - outputTokens: 10, - totalTokens: 13, +const testUsage: LanguageModelV3Usage = { + inputTokens: { + total: 3, + noCache: 3, + cacheRead: 0, + cacheWrite: 0, + }, + outputTokens: { + total: 10, + text: 10, + reasoning: 0, + }, }; const mockTextModel = new MockLanguageModelV3({ @@ -221,7 +230,7 @@ describe('rsc - streamUI() onFinish callback', () => { }); it('should contain token usage', () => { - expect(result.usage).toStrictEqual(testUsage); + expect(result.usage).toStrictEqual(asLanguageModelUsage(testUsage)); }); it('should contain finish reason', async () => { diff --git a/packages/xai/src/convert-xai-chat-usage.ts b/packages/xai/src/convert-xai-chat-usage.ts new file mode 100644 index 000000000000..aa53a3aef98f --- /dev/null +++ b/packages/xai/src/convert-xai-chat-usage.ts @@ -0,0 +1,23 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; +import { XaiChatUsage } from './xai-chat-language-model'; + +export function convertXaiChatUsage(usage: XaiChatUsage): LanguageModelV3Usage { + const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = + usage.completion_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: usage.prompt_tokens, + noCache: usage.prompt_tokens - cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.completion_tokens, + text: usage.completion_tokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/xai/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap b/packages/xai/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap index dea206506966..a48f3fc5e4c8 100644 --- a/packages/xai/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap +++ b/packages/xai/src/responses/__snapshots__/xai-responses-language-model.test.ts.snap @@ -3764,10 +3764,30 @@ Sonora's style emerged from indigenous Yaqui and Mayo tribes, who used desert fo "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 216, - "outputTokens": 863, - "reasoningTokens": 237, - "totalTokens": 1079, + "inputTokens": { + "cacheRead": 192, + "cacheWrite": undefined, + "noCache": 24, + "total": 216, + }, + "outputTokens": { + "reasoning": 237, + "text": 626, + "total": 863, + }, + "raw": { + "input_tokens": 216, + "input_tokens_details": { + "cached_tokens": 192, + }, + "num_server_side_tools_used": 0, + "num_sources_used": 0, + "output_tokens": 863, + "output_tokens_details": { + "reasoning_tokens": 237, + }, + "total_tokens": 1079, + }, }, }, ] @@ -7062,10 +7082,30 @@ Sonoran's style is practical for its hot, dry climate—dishes are quick to prep "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 216, - "outputTokens": 831, - "reasoningTokens": 253, - "totalTokens": 1047, + "inputTokens": { + "cacheRead": 192, + "cacheWrite": undefined, + "noCache": 24, + "total": 216, + }, + "outputTokens": { + "reasoning": 253, + "text": 578, + "total": 831, + }, + "raw": { + "input_tokens": 216, + "input_tokens_details": { + "cached_tokens": 192, + }, + "num_server_side_tools_used": 0, + "num_sources_used": 0, + "output_tokens": 831, + "output_tokens_details": { + "reasoning_tokens": 253, + }, + "total_tokens": 1047, + }, }, }, ] @@ -10477,10 +10517,30 @@ Sonoran cuisine reflects Indigenous roots from tribes like the Tohono O'odham an "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 216, - "outputTokens": 923, - "reasoningTokens": 323, - "totalTokens": 1139, + "inputTokens": { + "cacheRead": 192, + "cacheWrite": undefined, + "noCache": 24, + "total": 216, + }, + "outputTokens": { + "reasoning": 323, + "text": 600, + "total": 923, + }, + "raw": { + "input_tokens": 216, + "input_tokens_details": { + "cached_tokens": 192, + }, + "num_server_side_tools_used": 0, + "num_sources_used": 0, + "output_tokens": 923, + "output_tokens_details": { + "reasoning_tokens": 323, + }, + "total_tokens": 1139, + }, }, }, ] @@ -11917,10 +11977,30 @@ Note: "XAI" can also refer to "Explainable AI," a field in machine learning that "finishReason": "stop", "type": "finish", "usage": { - "inputTokens": 1875, - "outputTokens": 695, - "reasoningTokens": 397, - "totalTokens": 2570, + "inputTokens": { + "cacheRead": 1578, + "cacheWrite": undefined, + "noCache": 297, + "total": 1875, + }, + "outputTokens": { + "reasoning": 397, + "text": 298, + "total": 695, + }, + "raw": { + "input_tokens": 1875, + "input_tokens_details": { + "cached_tokens": 1578, + }, + "num_server_side_tools_used": 1, + "num_sources_used": 0, + "output_tokens": 695, + "output_tokens_details": { + "reasoning_tokens": 397, + }, + "total_tokens": 2570, + }, }, }, ] diff --git a/packages/xai/src/responses/convert-xai-responses-usage.ts b/packages/xai/src/responses/convert-xai-responses-usage.ts new file mode 100644 index 000000000000..823ac6e7ceee --- /dev/null +++ b/packages/xai/src/responses/convert-xai-responses-usage.ts @@ -0,0 +1,24 @@ +import { LanguageModelV3Usage } from '@ai-sdk/provider'; +import { XaiResponsesUsage } from './xai-responses-api'; + +export function convertXaiResponsesUsage( + usage: XaiResponsesUsage, +): LanguageModelV3Usage { + const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? 0; + const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0; + + return { + inputTokens: { + total: usage.input_tokens, + noCache: usage.input_tokens - cacheReadTokens, + cacheRead: cacheReadTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.output_tokens, + text: usage.output_tokens - reasoningTokens, + reasoning: reasoningTokens, + }, + raw: usage, + }; +} diff --git a/packages/xai/src/responses/xai-responses-language-model.test.ts b/packages/xai/src/responses/xai-responses-language-model.test.ts index a714e7c8c91f..2eb3aeac15e5 100644 --- a/packages/xai/src/responses/xai-responses-language-model.test.ts +++ b/packages/xai/src/responses/xai-responses-language-model.test.ts @@ -133,10 +133,25 @@ describe('XaiResponsesLanguageModel', () => { expect(result.usage).toMatchInlineSnapshot(` { - "inputTokens": 345, - "outputTokens": 538, - "reasoningTokens": 123, - "totalTokens": 883, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 345, + "total": 345, + }, + "outputTokens": { + "reasoning": 123, + "text": 415, + "total": 538, + }, + "raw": { + "input_tokens": 345, + "output_tokens": 538, + "output_tokens_details": { + "reasoning_tokens": 123, + }, + "total_tokens": 883, + }, } `); }); diff --git a/packages/xai/src/responses/xai-responses-language-model.ts b/packages/xai/src/responses/xai-responses-language-model.ts index 46aeb9c744d4..e0f15aed2a1f 100644 --- a/packages/xai/src/responses/xai-responses-language-model.ts +++ b/packages/xai/src/responses/xai-responses-language-model.ts @@ -1,10 +1,10 @@ import { LanguageModelV3, - SharedV3Warning, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3StreamPart, LanguageModelV3Usage, + SharedV3Warning, } from '@ai-sdk/provider'; import { combineHeaders, @@ -17,17 +17,18 @@ import { } from '@ai-sdk/provider-utils'; import { z } from 'zod/v4'; import { getResponseMetadata } from '../get-response-metadata'; +import { xaiFailedResponseHandler } from '../xai-error'; +import { convertToXaiResponsesInput } from './convert-to-xai-responses-input'; +import { convertXaiResponsesUsage } from './convert-xai-responses-usage'; +import { mapXaiResponsesFinishReason } from './map-xai-responses-finish-reason'; import { xaiResponsesChunkSchema, xaiResponsesResponseSchema, } from './xai-responses-api'; -import { mapXaiResponsesFinishReason } from './map-xai-responses-finish-reason'; import { XaiResponsesModelId, xaiResponsesProviderOptions, } from './xai-responses-options'; -import { xaiFailedResponseHandler } from '../xai-error'; -import { convertToXaiResponsesInput } from './convert-to-xai-responses-input'; import { prepareResponsesTools } from './xai-responses-prepare-tools'; type XaiResponsesConfig = { @@ -270,12 +271,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { return { content, finishReason: mapXaiResponsesFinishReason(response.status), - usage: { - inputTokens: response.usage.input_tokens, - outputTokens: response.usage.output_tokens, - totalTokens: response.usage.total_tokens, - reasoningTokens: response.usage.output_tokens_details?.reasoning_tokens, - }, + usage: convertXaiResponsesUsage(response.usage), request: { body }, response: { ...getResponseMetadata(response), @@ -314,11 +310,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - }; + let usage: LanguageModelV3Usage | undefined = undefined; let isFirstChunk = true; const contentBlocks: Record = {}; const seenToolCalls = new Set(); @@ -454,11 +446,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { const response = event.response; if (response.usage) { - usage.inputTokens = response.usage.input_tokens; - usage.outputTokens = response.usage.output_tokens; - usage.totalTokens = response.usage.total_tokens; - usage.reasoningTokens = - response.usage.output_tokens_details?.reasoning_tokens; + usage = convertXaiResponsesUsage(response.usage); } if (response.status) { @@ -620,7 +608,7 @@ export class XaiResponsesLanguageModel implements LanguageModelV3 { } } - controller.enqueue({ type: 'finish', finishReason, usage }); + controller.enqueue({ type: 'finish', finishReason, usage: usage! }); }, }), ), diff --git a/packages/xai/src/xai-chat-language-model.test.ts b/packages/xai/src/xai-chat-language-model.test.ts index 068b9b3dd13e..a27cfa6893e7 100644 --- a/packages/xai/src/xai-chat-language-model.test.ts +++ b/packages/xai/src/xai-chat-language-model.test.ts @@ -190,11 +190,22 @@ describe('XaiChatLanguageModel', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 20, - "outputTokens": 5, - "reasoningTokens": undefined, - "totalTokens": 25, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 20, + "total": 20, + }, + "outputTokens": { + "reasoning": 0, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 20, + "total_tokens": 25, + }, } `); }); @@ -858,11 +869,22 @@ describe('XaiChatLanguageModel', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 32, - "reasoningTokens": undefined, - "totalTokens": 36, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": 0, + "text": 32, + "total": 32, + }, + "raw": { + "completion_tokens": 32, + "prompt_tokens": 4, + "total_tokens": 36, + }, }, }, ] @@ -922,11 +944,22 @@ describe('XaiChatLanguageModel', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 32, - "reasoningTokens": undefined, - "totalTokens": 36, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": 0, + "text": 32, + "total": 32, + }, + "raw": { + "completion_tokens": 32, + "prompt_tokens": 4, + "total_tokens": 36, + }, }, }, ] @@ -1001,11 +1034,22 @@ describe('XaiChatLanguageModel', () => { "finishReason": "tool-calls", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 183, - "outputTokens": 133, - "reasoningTokens": undefined, - "totalTokens": 316, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 183, + "total": 183, + }, + "outputTokens": { + "reasoning": 0, + "text": 133, + "total": 133, + }, + "raw": { + "completion_tokens": 133, + "prompt_tokens": 183, + "total_tokens": 316, + }, }, }, ] @@ -1188,11 +1232,22 @@ describe('XaiChatLanguageModel', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 4, - "outputTokens": 30, - "reasoningTokens": undefined, - "totalTokens": 34, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 4, + "total": 4, + }, + "outputTokens": { + "reasoning": 0, + "text": 30, + "total": 30, + }, + "raw": { + "completion_tokens": 30, + "prompt_tokens": 4, + "total_tokens": 34, + }, }, }, ] @@ -1315,11 +1370,25 @@ describe('XaiChatLanguageModel', () => { expect(usage).toMatchInlineSnapshot(` { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 10, + "text": 10, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, } `); }); @@ -1398,11 +1467,25 @@ describe('XaiChatLanguageModel', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 10, + "text": 10, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, }, }, ] @@ -1489,11 +1572,25 @@ describe('XaiChatLanguageModel', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 15, - "outputTokens": 20, - "reasoningTokens": 10, - "totalTokens": 35, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 15, + "total": 15, + }, + "outputTokens": { + "reasoning": 10, + "text": 10, + "total": 20, + }, + "raw": { + "completion_tokens": 20, + "completion_tokens_details": { + "reasoning_tokens": 10, + }, + "prompt_tokens": 15, + "total_tokens": 35, + }, }, }, ] @@ -1633,11 +1730,22 @@ describe('doStream with raw chunks', () => { "finishReason": "stop", "type": "finish", "usage": { - "cachedInputTokens": undefined, - "inputTokens": 10, - "outputTokens": 5, - "reasoningTokens": undefined, - "totalTokens": 15, + "inputTokens": { + "cacheRead": 0, + "cacheWrite": undefined, + "noCache": 10, + "total": 10, + }, + "outputTokens": { + "reasoning": 0, + "text": 5, + "total": 5, + }, + "raw": { + "completion_tokens": 5, + "prompt_tokens": 10, + "total_tokens": 15, + }, }, }, ] diff --git a/packages/xai/src/xai-chat-language-model.ts b/packages/xai/src/xai-chat-language-model.ts index 52903c353643..95eebea28f73 100644 --- a/packages/xai/src/xai-chat-language-model.ts +++ b/packages/xai/src/xai-chat-language-model.ts @@ -22,6 +22,7 @@ import { mapXaiFinishReason } from './map-xai-finish-reason'; import { XaiChatModelId, xaiProviderOptions } from './xai-chat-options'; import { xaiFailedResponseHandler } from './xai-error'; import { prepareTools } from './xai-prepare-tools'; +import { convertXaiChatUsage } from './convert-xai-chat-usage'; type XaiChatConfig = { provider: string; @@ -263,16 +264,7 @@ export class XaiChatLanguageModel implements LanguageModelV3 { return { content, finishReason: mapXaiFinishReason(choice.finish_reason), - usage: { - inputTokens: response.usage.prompt_tokens, - outputTokens: response.usage.completion_tokens, - totalTokens: response.usage.total_tokens, - reasoningTokens: - response.usage.completion_tokens_details?.reasoning_tokens ?? - undefined, - cachedInputTokens: - response.usage.prompt_tokens_details?.cached_tokens ?? undefined, - }, + usage: convertXaiChatUsage(response.usage), request: { body }, response: { ...getResponseMetadata(response), @@ -307,13 +299,7 @@ export class XaiChatLanguageModel implements LanguageModelV3 { }); let finishReason: LanguageModelV3FinishReason = 'unknown'; - const usage: LanguageModelV3Usage = { - inputTokens: undefined, - outputTokens: undefined, - totalTokens: undefined, - reasoningTokens: undefined, - cachedInputTokens: undefined, - }; + let usage: LanguageModelV3Usage | undefined = undefined; let isFirstChunk = true; const contentBlocks: Record = {}; const lastReasoningDeltas: Record = {}; @@ -366,14 +352,7 @@ export class XaiChatLanguageModel implements LanguageModelV3 { // update usage if present if (value.usage != null) { - usage.inputTokens = value.usage.prompt_tokens; - usage.outputTokens = value.usage.completion_tokens; - usage.totalTokens = value.usage.total_tokens; - usage.reasoningTokens = - value.usage.completion_tokens_details?.reasoning_tokens ?? - undefined; - usage.cachedInputTokens = - value.usage.prompt_tokens_details?.cached_tokens ?? undefined; + usage = convertXaiChatUsage(value.usage); } const choice = value.choices[0]; @@ -490,7 +469,7 @@ export class XaiChatLanguageModel implements LanguageModelV3 { }); } - controller.enqueue({ type: 'finish', finishReason, usage }); + controller.enqueue({ type: 'finish', finishReason, usage: usage! }); }, }), ), @@ -523,6 +502,8 @@ const xaiUsageSchema = z.object({ .nullish(), }); +export type XaiChatUsage = z.infer; + const xaiChatResponseSchema = z.object({ id: z.string().nullish(), created: z.number().nullish(),