diff --git a/examples/openai/token-metadata.php b/examples/openai/token-metadata.php index d3e32c00..6ad92194 100644 --- a/examples/openai/token-metadata.php +++ b/examples/openai/token-metadata.php @@ -15,6 +15,7 @@ use Symfony\AI\Platform\Bridge\OpenAi\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; +use Symfony\AI\Platform\Result\Metadata\TokenUsage; require_once dirname(__DIR__).'/bootstrap.php'; @@ -33,8 +34,11 @@ ]); $metadata = $result->getMetadata(); +$tokenUsage = $metadata->get('token_usage'); -echo 'Utilized Tokens: '.$metadata['total_tokens'].\PHP_EOL; -echo '-- Prompt Tokens: '.$metadata['prompt_tokens'].\PHP_EOL; -echo '-- Completion Tokens: '.$metadata['completion_tokens'].\PHP_EOL; -echo 'Remaining Tokens: '.$metadata['remaining_tokens'].\PHP_EOL; +assert($tokenUsage instanceof TokenUsage); + +echo 'Utilized Tokens: '.$tokenUsage->totalTokens.\PHP_EOL; +echo '-- Prompt Tokens: '.$tokenUsage->promptTokens.\PHP_EOL; +echo '-- Completion Tokens: '.$tokenUsage->completionTokens.\PHP_EOL; +echo 'Remaining Tokens: '.$tokenUsage->remainingTokens.\PHP_EOL; diff --git a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php b/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php index 6ddb6117..90923a76 100644 --- a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php @@ -13,6 +13,7 @@ use Symfony\AI\Agent\Output; use Symfony\AI\Agent\OutputProcessorInterface; +use Symfony\AI\Platform\Result\Metadata\TokenUsage; use Symfony\AI\Platform\Result\StreamResult; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -34,25 +35,29 @@ public function processOutput(Output $output): void } $metadata = $output->result->getMetadata(); + $headers = $rawResponse->getHeaders(false); - $metadata->add( - 'remaining_tokens_minute', - (int) $rawResponse->getHeaders(false)['x-ratelimit-limit-tokens-minute'][0], - ); - - $metadata->add( - 'remaining_tokens_month', - (int) $rawResponse->getHeaders(false)['x-ratelimit-limit-tokens-month'][0], + $remainingTokensMinute = $headers['x-ratelimit-limit-tokens-minute'][0] ?? null; + $remainingTokensMonth = $headers['x-ratelimit-limit-tokens-month'][0] ?? null; + $tokenUsage = new TokenUsage( + remainingTokensMinute: null !== $remainingTokensMinute ? (int) $remainingTokensMinute : null, + remainingTokensMonth: null !== $remainingTokensMonth ? (int) $remainingTokensMonth : null, ); $content = $rawResponse->toArray(false); if (!\array_key_exists('usage', $content)) { + $metadata->add('token_usage', $tokenUsage); + return; } - $metadata->add('prompt_tokens', $content['usage']['prompt_tokens'] ?? null); - $metadata->add('completion_tokens', $content['usage']['completion_tokens'] ?? null); - $metadata->add('total_tokens', $content['usage']['total_tokens'] ?? null); + $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 614d859f..c46933e7 100644 --- a/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php @@ -13,6 +13,7 @@ use Symfony\AI\Agent\Output; use Symfony\AI\Agent\OutputProcessorInterface; +use Symfony\AI\Platform\Result\Metadata\TokenUsage; use Symfony\AI\Platform\Result\StreamResult; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -35,19 +36,27 @@ public function processOutput(Output $output): void $metadata = $output->result->getMetadata(); - $metadata->add( - 'remaining_tokens', - (int) $rawResponse->getHeaders(false)['x-ratelimit-remaining-tokens'][0], + $remainingTokens = $rawResponse->getHeaders(false)['x-ratelimit-remaining-tokens'][0] ?? null; + $tokenUsage = new TokenUsage( + remainingTokens: null !== $remainingTokens ? (int) $remainingTokens : null, ); $content = $rawResponse->toArray(false); if (!\array_key_exists('usage', $content)) { + $metadata->add('token_usage', $tokenUsage); + return; } - $metadata->add('prompt_tokens', $content['usage']['prompt_tokens'] ?? null); - $metadata->add('completion_tokens', $content['usage']['completion_tokens'] ?? null); - $metadata->add('total_tokens', $content['usage']['total_tokens'] ?? null); + $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/Result/Metadata/TokenUsage.php b/src/platform/src/Result/Metadata/TokenUsage.php new file mode 100644 index 00000000..e8ae5be5 --- /dev/null +++ b/src/platform/src/Result/Metadata/TokenUsage.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Result\Metadata; + +/** + * @author Junaid Farooq + */ +final class TokenUsage implements \JsonSerializable +{ + public function __construct( + public ?int $promptTokens = null, + public ?int $completionTokens = null, + public ?int $thinkingTokens = null, + public ?int $cachedTokens = null, + public ?int $remainingTokens = null, + public ?int $remainingTokensMinute = null, + public ?int $remainingTokensMonth = null, + public ?int $totalTokens = null, + ) { + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'prompt' => $this->promptTokens, + 'completion' => $this->completionTokens, + 'thinking' => $this->thinkingTokens, + 'remaining' => $this->remainingTokens, + 'remaining_tokens_minute' => $this->remainingTokensMinute, + 'remaining_tokens_month' => $this->remainingTokensMonth, + 'total' => $this->totalTokens, + ]; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php index 0b0c007f..0358b3e0 100644 --- a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php @@ -20,6 +20,7 @@ use Symfony\AI\Platform\Message\MessageBagInterface; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\Metadata\Metadata; +use Symfony\AI\Platform\Result\Metadata\TokenUsage; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\StreamResult; @@ -70,9 +71,12 @@ public function testItAddsRemainingTokensToMetadata() $processor->processOutput($output); $metadata = $output->result->getMetadata(); - $this->assertCount(2, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens_minute')); - $this->assertSame(1000000, $metadata->get('remaining_tokens_month')); + $tokenUsage = $metadata->get('token_usage'); + + $this->assertCount(1, $metadata); + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(1000, $tokenUsage->remainingTokensMinute); + $this->assertSame(1000000, $tokenUsage->remainingTokensMonth); } public function testItAddsUsageTokensToMetadata() @@ -95,12 +99,14 @@ public function testItAddsUsageTokensToMetadata() $processor->processOutput($output); $metadata = $output->result->getMetadata(); - $this->assertCount(5, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens_minute')); - $this->assertSame(1000000, $metadata->get('remaining_tokens_month')); - $this->assertSame(10, $metadata->get('prompt_tokens')); - $this->assertSame(20, $metadata->get('completion_tokens')); - $this->assertSame(30, $metadata->get('total_tokens')); + $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); } public function testItHandlesMissingUsageFields() @@ -122,12 +128,14 @@ public function testItHandlesMissingUsageFields() $processor->processOutput($output); $metadata = $output->result->getMetadata(); - $this->assertCount(5, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens_minute')); - $this->assertSame(1000000, $metadata->get('remaining_tokens_month')); - $this->assertSame(10, $metadata->get('prompt_tokens')); - $this->assertNull($metadata->get('completion_tokens')); - $this->assertNull($metadata->get('total_tokens')); + $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); } 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 5aa8e004..328729f3 100644 --- a/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php +++ b/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php @@ -20,6 +20,7 @@ use Symfony\AI\Platform\Message\MessageBagInterface; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\Metadata\Metadata; +use Symfony\AI\Platform\Result\Metadata\TokenUsage; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\StreamResult; @@ -31,6 +32,7 @@ #[UsesClass(TextResult::class)] #[UsesClass(StreamResult::class)] #[UsesClass(Metadata::class)] +#[UsesClass(TokenUsage::class)] #[Small] final class TokenOutputProcessorTest extends TestCase { @@ -70,8 +72,11 @@ public function testItAddsRemainingTokensToMetadata() $processor->processOutput($output); $metadata = $output->result->getMetadata(); + $tokenUsage = $metadata->get('token_usage'); + $this->assertCount(1, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens')); + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(1000, $tokenUsage->remainingTokens); } public function testItAddsUsageTokensToMetadata() @@ -83,7 +88,13 @@ public function testItAddsUsageTokensToMetadata() 'usage' => [ 'prompt_tokens' => 10, 'completion_tokens' => 20, - 'total_tokens' => 30, + 'total_tokens' => 50, + 'completion_tokens_details' => [ + 'reasoning_tokens' => 20, + ], + 'prompt_tokens_details' => [ + 'cached_tokens' => 40, + ], ], ]); @@ -94,11 +105,15 @@ public function testItAddsUsageTokensToMetadata() $processor->processOutput($output); $metadata = $output->result->getMetadata(); - $this->assertCount(4, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens')); - $this->assertSame(10, $metadata->get('prompt_tokens')); - $this->assertSame(20, $metadata->get('completion_tokens')); - $this->assertSame(30, $metadata->get('total_tokens')); + $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); } public function testItHandlesMissingUsageFields() @@ -120,11 +135,13 @@ public function testItHandlesMissingUsageFields() $processor->processOutput($output); $metadata = $output->result->getMetadata(); - $this->assertCount(4, $metadata); - $this->assertSame(1000, $metadata->get('remaining_tokens')); - $this->assertSame(10, $metadata->get('prompt_tokens')); - $this->assertNull($metadata->get('completion_tokens')); - $this->assertNull($metadata->get('total_tokens')); + $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); } private function createRawResult(array $data = []): RawHttpResult