diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 03bba3861..8a7b5156e 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -107,15 +107,15 @@ function print_token_usage(Metadata $metadata): void $table = new Table(output()); $table->setHeaderTitle('Token Usage'); $table->setRows([ - ['Prompt tokens', $tokenUsage->promptTokens ?? $na], - ['Completion tokens', $tokenUsage->completionTokens ?? $na], - ['Thinking tokens', $tokenUsage->thinkingTokens ?? $na], - ['Tool tokens', $tokenUsage->toolTokens ?? $na], - ['Cached tokens', $tokenUsage->cachedTokens ?? $na], - ['Remaining tokens minute', $tokenUsage->remainingTokensMinute ?? $na], - ['Remaining tokens month', $tokenUsage->remainingTokensMonth ?? $na], - ['Remaining tokens', $tokenUsage->remainingTokens ?? $na], - ['Utilized tokens', $tokenUsage->totalTokens ?? $na], + ['Prompt tokens', $tokenUsage->getPromptTokens() ?? $na], + ['Completion tokens', $tokenUsage->getCompletionTokens() ?? $na], + ['Thinking tokens', $tokenUsage->getThinkingTokens() ?? $na], + ['Tool tokens', $tokenUsage->getToolTokens() ?? $na], + ['Cached tokens', $tokenUsage->getCachedTokens() ?? $na], + ['Remaining tokens minute', $tokenUsage->getRemainingTokensMinute() ?? $na], + ['Remaining tokens month', $tokenUsage->getRemainingTokensMonth() ?? $na], + ['Remaining tokens', $tokenUsage->getRemainingTokens() ?? $na], + ['Total tokens', $tokenUsage->getTotalTokens() ?? $na], ]); $table->render(); } diff --git a/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php b/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php index 6d4aafd10..6a744e35e 100644 --- a/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php @@ -32,28 +32,25 @@ public function processOutput(Output $output): void } $metadata = $output->getResult()->getMetadata(); - - $tokenUsage = new TokenUsage(); - $content = $rawResponse->toArray(false); + if (!\array_key_exists('usage', $content)) { - $metadata->add('token_usage', $tokenUsage); + $metadata->add('token_usage', new TokenUsage()); return; } $usage = $content['usage']; - - $tokenUsage->promptTokens = $usage['input_tokens'] ?? null; - $tokenUsage->completionTokens = $usage['output_tokens'] ?? null; - $tokenUsage->toolTokens = $usage['server_tool_use']['web_search_requests'] ?? null; - $cachedTokens = null; if (\array_key_exists('cache_creation_input_tokens', $usage) || \array_key_exists('cache_read_input_tokens', $usage)) { $cachedTokens = ($usage['cache_creation_input_tokens'] ?? 0) + ($usage['cache_read_input_tokens'] ?? 0); } - $tokenUsage->cachedTokens = $cachedTokens; - $metadata->add('token_usage', $tokenUsage); + $metadata->add('token_usage', new TokenUsage( + promptTokens: $usage['input_tokens'] ?? null, + completionTokens: $usage['output_tokens'] ?? null, + toolTokens: $usage['server_tool_use']['web_search_requests'] ?? null, + cachedTokens: $cachedTokens, + )); } } diff --git a/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php b/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php index fdef48362..b449675fd 100644 --- a/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php @@ -35,23 +35,14 @@ public function processOutput(Output $output): void } $metadata = $output->getResult()->getMetadata(); - - $tokenUsage = new TokenUsage(); - $content = $rawResponse->toArray(false); - if (!\array_key_exists('usage', $content)) { - $metadata->add('token_usage', $tokenUsage); - - return; - } - - $usage = $content['usage']; - - $tokenUsage->promptTokens = $usage['prompt_tokens'] ?? null; - $tokenUsage->completionTokens = $usage['completion_tokens'] ?? null; - $tokenUsage->cachedTokens = $usage['prompt_cache_hit_tokens'] ?? null; - $tokenUsage->totalTokens = $usage['total_tokens'] ?? null; + $tokenUsage = new TokenUsage( + promptTokens: $content['usage']['prompt_tokens'] ?? null, + completionTokens: $content['usage']['completion_tokens'] ?? null, + cachedTokens: $content['usage']['prompt_cache_hit_tokens'] ?? null, + totalTokens: $content['usage']['total_tokens'] ?? null, + ); $metadata->add('token_usage', $tokenUsage); } diff --git a/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php b/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php index 8517c4c5d..2dc7d7b4f 100644 --- a/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php @@ -32,24 +32,16 @@ public function processOutput(Output $output): void } $metadata = $output->getResult()->getMetadata(); - - $tokenUsage = new TokenUsage(); - $content = $rawResponse->toArray(false); - if (!\array_key_exists('usageMetadata', $content)) { - $metadata->add('token_usage', $tokenUsage); - - return; - } - - $usage = $content['usageMetadata']; - $tokenUsage->promptTokens = $usage['promptTokenCount'] ?? null; - $tokenUsage->completionTokens = $usage['candidatesTokenCount'] ?? null; - $tokenUsage->thinkingTokens = $usage['thoughtsTokenCount'] ?? null; - $tokenUsage->toolTokens = $usage['toolUsePromptTokenCount'] ?? null; - $tokenUsage->cachedTokens = $usage['cachedContentTokenCount'] ?? null; - $tokenUsage->totalTokens = $usage['totalTokenCount'] ?? null; + $tokenUsage = new TokenUsage( + promptTokens: $content['usageMetadata']['promptTokenCount'] ?? null, + completionTokens: $content['usageMetadata']['candidatesTokenCount'] ?? null, + thinkingTokens: $content['usageMetadata']['thoughtsTokenCount'] ?? null, + toolTokens: $content['usageMetadata']['toolUsePromptTokenCount'] ?? null, + cachedTokens: $content['usageMetadata']['cachedContentTokenCount'] ?? null, + totalTokens: $content['usageMetadata']['totalTokenCount'] ?? null, + ); $metadata->add('token_usage', $tokenUsage); } diff --git a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php b/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php index a48fbc6ce..4206adf4e 100644 --- a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php @@ -39,25 +39,17 @@ public function processOutput(Output $output): void $remainingTokensMinute = $headers['x-ratelimit-limit-tokens-minute'][0] ?? null; $remainingTokensMonth = $headers['x-ratelimit-limit-tokens-month'][0] ?? null; + + $content = $rawResponse->toArray(false); + $tokenUsage = new TokenUsage( + promptTokens: $content['usage']['prompt_tokens'] ?? null, + completionTokens: $content['usage']['completion_tokens'] ?? null, remainingTokensMinute: null !== $remainingTokensMinute ? (int) $remainingTokensMinute : null, remainingTokensMonth: null !== $remainingTokensMonth ? (int) $remainingTokensMonth : null, + totalTokens: $content['usage']['total_tokens'] ?? null, ); - $content = $rawResponse->toArray(false); - - if (!\array_key_exists('usage', $content)) { - $metadata->add('token_usage', $tokenUsage); - - return; - } - - $usage = $content['usage']; - - $tokenUsage->promptTokens = $usage['prompt_tokens'] ?? null; - $tokenUsage->completionTokens = $usage['completion_tokens'] ?? null; - $tokenUsage->totalTokens = $usage['total_tokens'] ?? null; - $metadata->add('token_usage', $tokenUsage); } } diff --git a/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php b/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php index 6f31d3394..c77e62cc8 100644 --- a/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php @@ -35,28 +35,18 @@ public function processOutput(Output $output): void } $metadata = $output->getResult()->getMetadata(); + $content = $rawResponse->toArray(false); $remainingTokens = $rawResponse->getHeaders(false)['x-ratelimit-remaining-tokens'][0] ?? null; $tokenUsage = new TokenUsage( + promptTokens: $content['usage']['prompt_tokens'] ?? null, + completionTokens: $content['usage']['completion_tokens'] ?? null, + thinkingTokens: $content['usage']['completion_tokens_details']['reasoning_tokens'] ?? null, + cachedTokens: $content['usage']['prompt_tokens_details']['cached_tokens'] ?? null, remainingTokens: null !== $remainingTokens ? (int) $remainingTokens : null, + totalTokens: $content['usage']['total_tokens'] ?? null, ); - $content = $rawResponse->toArray(false); - - if (!\array_key_exists('usage', $content)) { - $metadata->add('token_usage', $tokenUsage); - - return; - } - - $usage = $content['usage']; - - $tokenUsage->promptTokens = $usage['prompt_tokens'] ?? null; - $tokenUsage->completionTokens = $usage['completion_tokens'] ?? null; - $tokenUsage->thinkingTokens = $usage['completion_tokens_details']['reasoning_tokens'] ?? null; - $tokenUsage->cachedTokens = $usage['prompt_tokens_details']['cached_tokens'] ?? null; - $tokenUsage->totalTokens = $usage['total_tokens'] ?? null; - $metadata->add('token_usage', $tokenUsage); } } diff --git a/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php b/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php index e99269c55..b27ea6dfd 100644 --- a/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php @@ -36,18 +36,13 @@ public function processOutput(Output $output): void $content = $rawResponse->toArray(false); - if (!\array_key_exists('usage', $content)) { - return; - } - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = new TokenUsage(); - $usage = $content['usage']; - - $tokenUsage->promptTokens = $usage['prompt_tokens'] ?? null; - $tokenUsage->completionTokens = $usage['completion_tokens'] ?? null; - $tokenUsage->thinkingTokens = $usage['reasoning_tokens'] ?? null; - $tokenUsage->totalTokens = $usage['total_tokens'] ?? null; + $tokenUsage = new TokenUsage( + promptTokens: $content['usage']['prompt_tokens'] ?? null, + completionTokens: $content['usage']['completion_tokens'] ?? null, + thinkingTokens: $content['usage']['reasoning_tokens'] ?? null, + totalTokens: $content['usage']['total_tokens'] ?? null, + ); $metadata->add('token_usage', $tokenUsage); } diff --git a/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php b/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php index 8e4a4115b..2c23bc33c 100644 --- a/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php @@ -13,7 +13,6 @@ use Symfony\AI\Agent\Output; use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\Metadata; use Symfony\AI\Platform\Metadata\TokenUsage; use Symfony\AI\Platform\Result\StreamResult; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; @@ -37,7 +36,6 @@ final class TokenOutputProcessor implements OutputProcessorInterface */ public function processOutput(Output $output): void { - $tokenUsage = new TokenUsage(); $metadata = $output->getResult()->getMetadata(); if ($output->getResult() instanceof StreamResult) { @@ -51,7 +49,7 @@ public function processOutput(Output $output): void } if ($lastChunk) { - $this->extractUsageMetadata($lastChunk['usageMetadata'], $metadata, $tokenUsage); + $metadata->add('token_usage', $this->extractUsageMetadata($lastChunk['usageMetadata'])); } return; @@ -64,13 +62,7 @@ public function processOutput(Output $output): void $content = $rawResponse->toArray(false); - if (!isset($content['usageMetadata'])) { - $metadata->add('token_usage', $tokenUsage); - - return; - } - - $this->extractUsageMetadata($content['usageMetadata'], $metadata, $tokenUsage); + $metadata->add('token_usage', $this->extractUsageMetadata($content['usageMetadata'] ?? [])); } /** @@ -82,14 +74,14 @@ public function processOutput(Output $output): void * totalTokenCount?: int * } $usage */ - private function extractUsageMetadata(array $usage, Metadata $metadata, TokenUsage $tokenUsage): void + private function extractUsageMetadata(array $usage): TokenUsage { - $tokenUsage->promptTokens = $usage['promptTokenCount'] ?? null; - $tokenUsage->completionTokens = $usage['candidatesTokenCount'] ?? null; - $tokenUsage->thinkingTokens = $usage['thoughtsTokenCount'] ?? null; - $tokenUsage->cachedTokens = $usage['cachedContentTokenCount'] ?? null; - $tokenUsage->totalTokens = $usage['totalTokenCount'] ?? null; - - $metadata->add('token_usage', $tokenUsage); + return new TokenUsage( + promptTokens: $usage['promptTokenCount'] ?? null, + completionTokens: $usage['candidatesTokenCount'] ?? null, + thinkingTokens: $usage['thoughtsTokenCount'] ?? null, + cachedTokens: $usage['cachedContentTokenCount'] ?? null, + totalTokens: $usage['totalTokenCount'] ?? null, + ); } } diff --git a/src/platform/src/Metadata/TokenUsage.php b/src/platform/src/Metadata/TokenUsage.php index 7ce4ff308..8c2383d1b 100644 --- a/src/platform/src/Metadata/TokenUsage.php +++ b/src/platform/src/Metadata/TokenUsage.php @@ -14,46 +14,63 @@ /** * @author Junaid Farooq */ -final class TokenUsage implements \JsonSerializable +final class TokenUsage implements TokenUsageInterface { public function __construct( - public ?int $promptTokens = null, - public ?int $completionTokens = null, - public ?int $thinkingTokens = null, - public ?int $toolTokens = null, - public ?int $cachedTokens = null, - public ?int $remainingTokens = null, - public ?int $remainingTokensMinute = null, - public ?int $remainingTokensMonth = null, - public ?int $totalTokens = null, + private readonly ?int $promptTokens = null, + private readonly ?int $completionTokens = null, + private readonly ?int $thinkingTokens = null, + private readonly ?int $toolTokens = null, + private readonly ?int $cachedTokens = null, + private readonly ?int $remainingTokens = null, + private readonly ?int $remainingTokensMinute = null, + private readonly ?int $remainingTokensMonth = null, + private readonly ?int $totalTokens = null, ) { } - /** - * @return array{ - * prompt_tokens: ?int, - * completion_tokens: ?int, - * thinking_tokens: ?int, - * tool_tokens: ?int, - * cached_tokens: ?int, - * remaining_tokens: ?int, - * remaining_tokens_minute: ?int, - * remaining_tokens_month: ?int, - * total_tokens: ?int, - * } - */ - public function jsonSerialize(): array - { - return [ - 'prompt_tokens' => $this->promptTokens, - 'completion_tokens' => $this->completionTokens, - 'thinking_tokens' => $this->thinkingTokens, - 'tool_tokens' => $this->toolTokens, - 'cached_tokens' => $this->cachedTokens, - 'remaining_tokens' => $this->remainingTokens, - 'remaining_tokens_minute' => $this->remainingTokensMinute, - 'remaining_tokens_month' => $this->remainingTokensMonth, - 'total_tokens' => $this->totalTokens, - ]; + public function getPromptTokens(): ?int + { + return $this->promptTokens; + } + + public function getCompletionTokens(): ?int + { + return $this->completionTokens; + } + + public function getThinkingTokens(): ?int + { + return $this->thinkingTokens; + } + + public function getToolTokens(): ?int + { + return $this->toolTokens; + } + + public function getCachedTokens(): ?int + { + return $this->cachedTokens; + } + + public function getRemainingTokens(): ?int + { + return $this->remainingTokens; + } + + public function getRemainingTokensMinute(): ?int + { + return $this->remainingTokensMinute; + } + + public function getRemainingTokensMonth(): ?int + { + return $this->remainingTokensMonth; + } + + public function getTotalTokens(): ?int + { + return $this->totalTokens; } } diff --git a/src/platform/src/Metadata/TokenUsageAggregation.php b/src/platform/src/Metadata/TokenUsageAggregation.php new file mode 100644 index 000000000..299f183ec --- /dev/null +++ b/src/platform/src/Metadata/TokenUsageAggregation.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Metadata; + +/** + * @author Christopher Hertel + */ +final class TokenUsageAggregation implements TokenUsageInterface +{ + /** + * @var TokenUsageInterface[] + */ + private readonly array $tokenUsages; + + public function __construct( + TokenUsageInterface ...$tokenUsages, + ) { + $this->tokenUsages = $tokenUsages; + } + + public function getPromptTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getPromptTokens()); + } + + public function getCompletionTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getCompletionTokens()); + } + + public function getThinkingTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getThinkingTokens()); + } + + public function getToolTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getToolTokens()); + } + + public function getCachedTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getCachedTokens()); + } + + public function getRemainingTokens(): ?int + { + return $this->min(fn (TokenUsageInterface $usage) => $usage->getRemainingTokens()); + } + + public function getRemainingTokensMinute(): ?int + { + return $this->min(fn (TokenUsageInterface $usage) => $usage->getRemainingTokensMinute()); + } + + public function getRemainingTokensMonth(): ?int + { + return $this->min(fn (TokenUsageInterface $usage) => $usage->getRemainingTokensMonth()); + } + + public function getTotalTokens(): ?int + { + return $this->sum(fn (TokenUsageInterface $usage) => $usage->getTotalTokens()); + } + + private function sum(\Closure $mapFunction): ?int + { + $array = array_filter(array_map($mapFunction, $this->tokenUsages)); + + if ([] === $array) { + return null; + } + + return array_sum($array); + } + + private function min(\Closure $mapFunction): ?int + { + $array = array_filter(array_map($mapFunction, $this->tokenUsages)); + + if ([] === $array) { + return null; + } + + return min($array); + } +} diff --git a/src/platform/src/Metadata/TokenUsageInterface.php b/src/platform/src/Metadata/TokenUsageInterface.php new file mode 100644 index 000000000..077f16911 --- /dev/null +++ b/src/platform/src/Metadata/TokenUsageInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Metadata; + +/** + * @author Christopher Hertel + */ +interface TokenUsageInterface +{ + public function getPromptTokens(): ?int; + + public function getCompletionTokens(): ?int; + + public function getThinkingTokens(): ?int; + + public function getToolTokens(): ?int; + + public function getCachedTokens(): ?int; + + public function getRemainingTokens(): ?int; + + public function getRemainingTokensMinute(): ?int; + + public function getRemainingTokensMonth(): ?int; + + public function getTotalTokens(): ?int; +} diff --git a/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php index 63dbeb497..45664b7ae 100644 --- a/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php @@ -64,7 +64,7 @@ public function testItAddsRemainingTokensToMetadata() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->remainingTokens); + $this->assertNull($tokenUsage->getRemainingTokens()); } public function testItAddsUsageTokensToMetadata() @@ -94,13 +94,13 @@ public function testItAddsUsageTokensToMetadata() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(30, $tokenUsage->toolTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertNull($tokenUsage->remainingTokens); - $this->assertNull($tokenUsage->thinkingTokens); - $this->assertSame(90, $tokenUsage->cachedTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(30, $tokenUsage->getToolTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertSame(90, $tokenUsage->getCachedTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -125,10 +125,10 @@ public function testItHandlesMissingUsageFields() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertNull($tokenUsage->remainingTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } private function createRawResult(array $data = []): RawHttpResult diff --git a/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php index d6f77abfe..d7d552698 100644 --- a/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php @@ -64,7 +64,7 @@ public function testItAddsRemainingTokensToMetadata() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->remainingTokens); + $this->assertNull($tokenUsage->getRemainingTokens()); } public function testItAddsUsageTokensToMetadata() @@ -93,13 +93,13 @@ public function testItAddsUsageTokensToMetadata() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(5, $tokenUsage->toolTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertNull($tokenUsage->remainingTokens); - $this->assertSame(20, $tokenUsage->thinkingTokens); - $this->assertSame(40, $tokenUsage->cachedTokens); - $this->assertSame(50, $tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(5, $tokenUsage->getToolTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(40, $tokenUsage->getCachedTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -124,10 +124,10 @@ public function testItHandlesMissingUsageFields() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertNull($tokenUsage->remainingTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } private function createRawResult(array $data = []): RawHttpResult diff --git a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php index c48610178..ca42f6b14 100644 --- a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php @@ -64,8 +64,8 @@ public function testItAddsRemainingTokensToMetadata() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->remainingTokensMinute); - $this->assertSame(1000000, $tokenUsage->remainingTokensMonth); + $this->assertSame(1000, $tokenUsage->getRemainingTokensMinute()); + $this->assertSame(1000000, $tokenUsage->getRemainingTokensMonth()); } public function testItAddsUsageTokensToMetadata() @@ -91,11 +91,11 @@ public function testItAddsUsageTokensToMetadata() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->remainingTokensMinute); - $this->assertSame(1000000, $tokenUsage->remainingTokensMonth); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertSame(30, $tokenUsage->totalTokens); + $this->assertSame(1000, $tokenUsage->getRemainingTokensMinute()); + $this->assertSame(1000000, $tokenUsage->getRemainingTokensMonth()); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(30, $tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -120,11 +120,11 @@ public function testItHandlesMissingUsageFields() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->remainingTokensMinute); - $this->assertSame(1000000, $tokenUsage->remainingTokensMonth); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(1000, $tokenUsage->getRemainingTokensMinute()); + $this->assertSame(1000000, $tokenUsage->getRemainingTokensMonth()); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } private function createRawResponse(array $data = []): RawHttpResult diff --git a/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php index 044536247..7e46efeb2 100644 --- a/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php @@ -64,7 +64,7 @@ public function testItAddsRemainingTokensToMetadata() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->remainingTokens); + $this->assertSame(1000, $tokenUsage->getRemainingTokens()); } public function testItAddsUsageTokensToMetadata() @@ -96,12 +96,12 @@ public function testItAddsUsageTokensToMetadata() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertSame(1000, $tokenUsage->remainingTokens); - $this->assertSame(20, $tokenUsage->thinkingTokens); - $this->assertSame(40, $tokenUsage->cachedTokens); - $this->assertSame(50, $tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(1000, $tokenUsage->getRemainingTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(40, $tokenUsage->getCachedTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -126,10 +126,10 @@ public function testItHandlesMissingUsageFields() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(1000, $tokenUsage->remainingTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(1000, $tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } private function createRawResult(array $data = []): RawHttpResult diff --git a/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php index 50edbfb5b..cb640dc9a 100644 --- a/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php @@ -75,10 +75,10 @@ public function testItAddsUsageTokensToMetadata() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertSame(20, $tokenUsage->thinkingTokens); - $this->assertSame(50, $tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -103,10 +103,10 @@ public function testItHandlesMissingUsageFields() $tokenUsage = $metadata->get('token_usage'); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->thinkingTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } private function createRawResult(array $data = []): RawHttpResult diff --git a/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php index 3399f85f5..ca5d1edd3 100644 --- a/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php @@ -59,10 +59,10 @@ public function testItAddsUsageTokensToMetadata() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertSame(20, $tokenUsage->completionTokens); - $this->assertSame(20, $tokenUsage->thinkingTokens); - $this->assertSame(50, $tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); } public function testItHandlesMissingUsageFields() @@ -86,10 +86,10 @@ public function testItHandlesMissingUsageFields() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->promptTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->thinkingTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } public function testItAddsEmptyTokenUsageWhenUsageMetadataNotPresent() @@ -107,10 +107,10 @@ public function testItAddsEmptyTokenUsageWhenUsageMetadataNotPresent() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->promptTokens); - $this->assertNull($tokenUsage->completionTokens); - $this->assertNull($tokenUsage->thinkingTokens); - $this->assertNull($tokenUsage->totalTokens); + $this->assertNull($tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); } public function testItHandlesStreamResults() @@ -140,10 +140,10 @@ public function testItHandlesStreamResults() $this->assertCount(1, $metadata); $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(15, $tokenUsage->promptTokens); - $this->assertSame(25, $tokenUsage->completionTokens); - $this->assertNull($tokenUsage->thinkingTokens); - $this->assertSame(40, $tokenUsage->totalTokens); + $this->assertSame(15, $tokenUsage->getPromptTokens()); + $this->assertSame(25, $tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertSame(40, $tokenUsage->getTotalTokens()); } private function createRawResponse(array $data = []): RawHttpResult diff --git a/src/platform/tests/Metadata/TokenUsageAggregationTest.php b/src/platform/tests/Metadata/TokenUsageAggregationTest.php new file mode 100644 index 000000000..d5fed8529 --- /dev/null +++ b/src/platform/tests/Metadata/TokenUsageAggregationTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Metadata; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Metadata\TokenUsage; +use Symfony\AI\Platform\Metadata\TokenUsageAggregation; + +class TokenUsageAggregationTest extends TestCase +{ + public function testAggregatesTokenUsageCorrectly() + { + $usage1 = new TokenUsage( + promptTokens: 10, + completionTokens: 20, + thinkingTokens: 5, + toolTokens: 2, + cachedTokens: 1, + remainingTokens: 100, + remainingTokensMinute: 60, + remainingTokensMonth: 1000, + totalTokens: 38 + ); + $usage2 = new TokenUsage( + promptTokens: 5, + completionTokens: 10, + thinkingTokens: 3, + toolTokens: 1, + cachedTokens: 2, + remainingTokens: 80, + remainingTokensMinute: 50, + remainingTokensMonth: 900, + totalTokens: 21 + ); + $aggregation = new TokenUsageAggregation($usage1, $usage2); + + $this->assertSame(15, $aggregation->getPromptTokens()); + $this->assertSame(30, $aggregation->getCompletionTokens()); + $this->assertSame(8, $aggregation->getThinkingTokens()); + $this->assertSame(3, $aggregation->getToolTokens()); + $this->assertSame(3, $aggregation->getCachedTokens()); + $this->assertSame(80, $aggregation->getRemainingTokens()); + $this->assertSame(50, $aggregation->getRemainingTokensMinute()); + $this->assertSame(900, $aggregation->getRemainingTokensMonth()); + $this->assertSame(59, $aggregation->getTotalTokens()); + } + + public function testHandlesNullValues() + { + $usage1 = new TokenUsage(promptTokens: null, completionTokens: null, remainingTokens: null, totalTokens: null); + $usage2 = new TokenUsage(promptTokens: 5, completionTokens: 10, remainingTokens: 25, totalTokens: 21); + $aggregation = new TokenUsageAggregation($usage1, $usage2); + + $this->assertSame(5, $aggregation->getPromptTokens()); + $this->assertSame(10, $aggregation->getCompletionTokens()); + $this->assertSame(25, $aggregation->getRemainingTokens()); + $this->assertSame(21, $aggregation->getTotalTokens()); + } + + public function testHandlesOnlyNullValues() + { + $usage1 = new TokenUsage(); + $usage2 = new TokenUsage(); + $aggregation = new TokenUsageAggregation($usage1, $usage2); + + $this->assertNull($aggregation->getPromptTokens()); + $this->assertNull($aggregation->getCompletionTokens()); + $this->assertNull($aggregation->getRemainingTokens()); + $this->assertNull($aggregation->getTotalTokens()); + } +} diff --git a/src/platform/tests/Metadata/TokenUsageTest.php b/src/platform/tests/Metadata/TokenUsageTest.php new file mode 100644 index 000000000..92a0b0537 --- /dev/null +++ b/src/platform/tests/Metadata/TokenUsageTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Metadata; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Metadata\TokenUsage; +use Symfony\AI\Platform\Metadata\TokenUsageInterface; + +class TokenUsageTest extends TestCase +{ + public function testDefaultValuesAreNull() + { + $usage = new TokenUsage(); + + $this->assertInstanceOf(TokenUsageInterface::class, $usage); + + $this->assertNull($usage->getPromptTokens()); + $this->assertNull($usage->getCompletionTokens()); + $this->assertNull($usage->getThinkingTokens()); + $this->assertNull($usage->getToolTokens()); + $this->assertNull($usage->getCachedTokens()); + $this->assertNull($usage->getRemainingTokens()); + $this->assertNull($usage->getRemainingTokensMinute()); + $this->assertNull($usage->getRemainingTokensMonth()); + $this->assertNull($usage->getTotalTokens()); + } + + public function testValuesAreSetCorrectly() + { + $usage = new TokenUsage(1, 2, 3, 4, 5, 6, 7, 8, 9); + + $this->assertSame(1, $usage->getPromptTokens()); + $this->assertSame(2, $usage->getCompletionTokens()); + $this->assertSame(3, $usage->getThinkingTokens()); + $this->assertSame(4, $usage->getToolTokens()); + $this->assertSame(5, $usage->getCachedTokens()); + $this->assertSame(6, $usage->getRemainingTokens()); + $this->assertSame(7, $usage->getRemainingTokensMinute()); + $this->assertSame(8, $usage->getRemainingTokensMonth()); + $this->assertSame(9, $usage->getTotalTokens()); + } +}