diff --git a/deptrac.yaml b/deptrac.yaml index 86cde6cc2..4b25f36e5 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -306,11 +306,4 @@ deptrac: - PlatformComponent # Baseline of known violations to be skipped for now skip_violations: - Symfony\AI\Platform\Bridge\Anthropic\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\DeepSeek\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\Gemini\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\Mistral\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\OpenAi\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] Symfony\AI\Platform\Bridge\Perplexity\SearchResultProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\Perplexity\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] - Symfony\AI\Platform\Bridge\VertexAi\TokenOutputProcessor: [Symfony\AI\Agent\OutputProcessorInterface, Symfony\AI\Agent\Output] diff --git a/docs/bundles/ai-bundle.rst b/docs/bundles/ai-bundle.rst index c451466aa..8e81f6258 100644 --- a/docs/bundles/ai-bundle.rst +++ b/docs/bundles/ai-bundle.rst @@ -67,7 +67,6 @@ Advanced Example with Multiple Agents agent: rag: platform: 'ai.platform.azure.gpt_deployment' - track_token_usage: true # Enable tracking of token usage for the agent, default is true model: 'gpt-4o-mini' memory: 'You have access to conversation history and user preferences' # Optional: static memory content prompt: # The system prompt configuration @@ -899,8 +898,8 @@ Token Usage Tracking Token usage tracking is a feature provided by some of the Platform's bridges, for monitoring and analyzing the consumption of tokens by your agents. This feature is particularly useful for understanding costs and performance. -When enabled, the agent will automatically track token usage information and add it -to the result metadata. The tracked information includes: +In case a Platform bridge supports token usage tracking, the Platform will automatically track token usage information +and add it to the result metadata. The tracked information includes: * **Prompt tokens**: Number of tokens used in the input/prompt * **Completion tokens**: Number of tokens generated in the response @@ -932,19 +931,6 @@ The token usage information can be accessed from the result metadata:: } } -Disable Tracking -~~~~~~~~~~~~~~~~ - -To disable token usage tracking for an agent, set the ``track_token_usage`` option to ``false``: - -.. code-block:: yaml - - ai: - agent: - my_agent: - model: 'gpt-4o-mini' - track_token_usage: false - Vectorizers ----------- diff --git a/examples/anthropic/token-metadata.php b/examples/anthropic/token-metadata.php index 4852d773e..af49860e4 100644 --- a/examples/anthropic/token-metadata.php +++ b/examples/anthropic/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory; -use Symfony\AI\Platform\Bridge\Anthropic\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('ANTHROPIC_API_KEY'), http_client()); -$agent = new Agent($platform, 'claude-sonnet-4-5-20250929', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'claude-sonnet-4-5-20250929'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 8a7b5156e..07ab8fdad 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -14,8 +14,8 @@ use Symfony\AI\Agent\Exception\ExceptionInterface as AgentException; use Symfony\AI\Platform\Exception\ExceptionInterface as PlatformException; use Symfony\AI\Platform\Metadata\Metadata; -use Symfony\AI\Platform\Metadata\TokenUsage; use Symfony\AI\Platform\Result\DeferredResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; use Symfony\AI\Store\Exception\ExceptionInterface as StoreException; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Logger\ConsoleLogger; diff --git a/examples/deepseek/token-metadata.php b/examples/deepseek/token-metadata.php index 5c5fa2ce3..93ec97363 100644 --- a/examples/deepseek/token-metadata.php +++ b/examples/deepseek/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory; -use Symfony\AI\Platform\Bridge\DeepSeek\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('DEEPSEEK_API_KEY'), http_client()); -$agent = new Agent($platform, 'deepseek-chat', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'deepseek-chat'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/gemini/token-metadata.php b/examples/gemini/token-metadata.php index f0922dd9b..3ea0212bb 100644 --- a/examples/gemini/token-metadata.php +++ b/examples/gemini/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory; -use Symfony\AI\Platform\Bridge\Gemini\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('GEMINI_API_KEY'), http_client()); -$agent = new Agent($platform, 'gemini-2.0-flash', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'gemini-2.0-flash'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/mistral/token-metadata.php b/examples/mistral/token-metadata.php index cf2d7ea7e..ab8c3ee78 100644 --- a/examples/mistral/token-metadata.php +++ b/examples/mistral/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory; -use Symfony\AI\Platform\Bridge\Mistral\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('MISTRAL_API_KEY'), http_client()); -$agent = new Agent($platform, 'mistral-large-latest', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'mistral-large-latest'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), diff --git a/examples/openai/token-metadata.php b/examples/openai/token-metadata.php index 1020f013f..a89bb9518 100644 --- a/examples/openai/token-metadata.php +++ b/examples/openai/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; -use Symfony\AI\Platform\Bridge\OpenAi\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client()); -$agent = new Agent($platform, 'gpt-4o-mini', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'gpt-4o-mini'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), Message::ofUser('What is the Symfony framework?'), diff --git a/examples/perplexity/token-metadata.php b/examples/perplexity/token-metadata.php index 0c8659f44..760168e28 100644 --- a/examples/perplexity/token-metadata.php +++ b/examples/perplexity/token-metadata.php @@ -11,14 +11,13 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\Perplexity\PlatformFactory; -use Symfony\AI\Platform\Bridge\Perplexity\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; $platform = PlatformFactory::create(env('PERPLEXITY_API_KEY'), http_client()); -$agent = new Agent($platform, 'sonar', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'sonar'); $messages = new MessageBag( Message::forSystem('You are a pirate and you write funny.'), diff --git a/examples/vertexai/token-metadata.php b/examples/vertexai/token-metadata.php index e6726e67f..e469b7b68 100644 --- a/examples/vertexai/token-metadata.php +++ b/examples/vertexai/token-metadata.php @@ -11,7 +11,6 @@ use Symfony\AI\Agent\Agent; use Symfony\AI\Platform\Bridge\VertexAi\PlatformFactory; -use Symfony\AI\Platform\Bridge\VertexAi\TokenOutputProcessor; use Symfony\AI\Platform\Message\Message; use Symfony\AI\Platform\Message\MessageBag; @@ -19,7 +18,7 @@ $platform = PlatformFactory::create(env('GOOGLE_CLOUD_LOCATION'), env('GOOGLE_CLOUD_PROJECT'), adc_aware_http_client()); -$agent = new Agent($platform, 'gemini-2.0-flash-lite', outputProcessors: [new TokenOutputProcessor()]); +$agent = new Agent($platform, 'gemini-2.0-flash-lite'); $messages = new MessageBag( Message::forSystem('You are an expert assistant in animal study.'), Message::ofUser('What does a cat usually eat?'), diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index 2510a963a..47017848b 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -314,10 +314,6 @@ ->info('Service name of platform') ->defaultValue(PlatformInterface::class) ->end() - ->booleanNode('track_token_usage') - ->info('Enable tracking of token usage for the agent') - ->defaultTrue() - ->end() ->variableNode('model') ->validate() ->ifTrue(function ($v) { diff --git a/src/ai-bundle/config/services.php b/src/ai-bundle/config/services.php index a6d87cda6..05ed4e36d 100644 --- a/src/ai-bundle/config/services.php +++ b/src/ai-bundle/config/services.php @@ -28,7 +28,6 @@ use Symfony\AI\Platform\Bridge\Albert\ModelCatalog as AlbertModelCatalog; use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract; use Symfony\AI\Platform\Bridge\Anthropic\ModelCatalog as AnthropicModelCatalog; -use Symfony\AI\Platform\Bridge\Anthropic\TokenOutputProcessor as AnthropicTokenOutputProcessor; use Symfony\AI\Platform\Bridge\Azure\OpenAi\ModelCatalog as AzureOpenAiModelCatalog; use Symfony\AI\Platform\Bridge\Cartesia\ModelCatalog as CartesiaModelCatalog; use Symfony\AI\Platform\Bridge\Cerebras\ModelCatalog as CerebrasModelCatalog; @@ -38,28 +37,23 @@ use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog; use Symfony\AI\Platform\Bridge\Gemini\Contract\GeminiContract; use Symfony\AI\Platform\Bridge\Gemini\ModelCatalog as GeminiModelCatalog; -use Symfony\AI\Platform\Bridge\Gemini\TokenOutputProcessor as GeminiTokenOutputProcessor; use Symfony\AI\Platform\Bridge\HuggingFace\Contract\HuggingFaceContract; use Symfony\AI\Platform\Bridge\HuggingFace\ModelCatalog as HuggingFaceModelCatalog; use Symfony\AI\Platform\Bridge\LmStudio\ModelCatalog as LmStudioModelCatalog; use Symfony\AI\Platform\Bridge\Meta\ModelCatalog as MetaModelCatalog; use Symfony\AI\Platform\Bridge\Mistral\ModelCatalog as MistralModelCatalog; -use Symfony\AI\Platform\Bridge\Mistral\TokenOutputProcessor as MistralTokenOutputProcessor; use Symfony\AI\Platform\Bridge\Ollama\Contract\OllamaContract; use Symfony\AI\Platform\Bridge\Ollama\ModelCatalog as OllamaModelCatalog; use Symfony\AI\Platform\Bridge\OpenAi\Contract\OpenAiContract; use Symfony\AI\Platform\Bridge\OpenAi\ModelCatalog as OpenAiModelCatalog; -use Symfony\AI\Platform\Bridge\OpenAi\TokenOutputProcessor as OpenAiTokenOutputProcessor; use Symfony\AI\Platform\Bridge\OpenRouter\ModelCatalog as OpenRouterModelCatalog; use Symfony\AI\Platform\Bridge\Perplexity\Contract\PerplexityContract; use Symfony\AI\Platform\Bridge\Perplexity\ModelCatalog as PerplexityModelCatalog; use Symfony\AI\Platform\Bridge\Perplexity\SearchResultProcessor as PerplexitySearchResultProcessor; -use Symfony\AI\Platform\Bridge\Perplexity\TokenOutputProcessor as PerplexityTokenOutputProcessor; use Symfony\AI\Platform\Bridge\Replicate\ModelCatalog as ReplicateModelCatalog; use Symfony\AI\Platform\Bridge\Scaleway\ModelCatalog as ScalewayModelCatalog; use Symfony\AI\Platform\Bridge\VertexAi\Contract\GeminiContract as VertexAiGeminiContract; use Symfony\AI\Platform\Bridge\VertexAi\ModelCatalog as VertexAiModelCatalog; -use Symfony\AI\Platform\Bridge\VertexAi\TokenOutputProcessor as VertexAiTokenOutputProcessor; use Symfony\AI\Platform\Bridge\Voyage\ModelCatalog as VoyageModelCatalog; use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Contract\JsonSchema\DescriptionParser; @@ -191,14 +185,6 @@ ]) ->tag('data_collector') - // token usage processors - ->set('ai.platform.token_usage_processor.anthropic', AnthropicTokenOutputProcessor::class) - ->set('ai.platform.token_usage_processor.gemini', GeminiTokenOutputProcessor::class) - ->set('ai.platform.token_usage_processor.mistral', MistralTokenOutputProcessor::class) - ->set('ai.platform.token_usage_processor.openai', OpenAiTokenOutputProcessor::class) - ->set('ai.platform.token_usage_processor.perplexity', PerplexityTokenOutputProcessor::class) - ->set('ai.platform.token_usage_processor.vertexai', VertexAiTokenOutputProcessor::class) - // search result processors ->set('ai.platform.search_result_processor.perplexity', PerplexitySearchResultProcessor::class) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index f7d879523..d0981b267 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -910,28 +910,6 @@ private function processAgentConfig(string $name, array $config, ContainerBuilde } } - // TOKEN USAGE TRACKING - if ($config['track_token_usage'] ?? true) { - $platformServiceId = $config['platform']; - - if ($container->hasAlias($platformServiceId)) { - $platformServiceId = (string) $container->getAlias($platformServiceId); - } - - if (str_starts_with($platformServiceId, 'ai.platform.')) { - $platform = u($platformServiceId)->after('ai.platform.')->toString(); - - if (str_contains($platform, 'azure')) { - $platform = 'azure'; - } - - if ($container->hasDefinition('ai.platform.token_usage_processor.'.$platform)) { - $container->getDefinition('ai.platform.token_usage_processor.'.$platform) - ->addTag('ai.agent.output_processor', ['agent' => $agentId, 'priority' => -30]); - } - } - } - // SYSTEM PROMPT if (isset($config['prompt'])) { $includeTools = isset($config['prompt']['include_tools']) && $config['prompt']['include_tools']; diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index f543d749f..0689d85af 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -4012,43 +4012,6 @@ public function testElevenLabsPlatformWithApiCatalogCanBeRegistered() $this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy')); } - #[TestDox('Token usage processor tags use the correct agent ID')] - public function testTokenUsageProcessorTags() - { - $container = $this->buildContainer([ - 'ai' => [ - 'platform' => [ - 'openai' => [ - 'api_key' => 'sk-test_key', - ], - ], - 'agent' => [ - 'tracked_agent' => [ - 'platform' => 'ai.platform.openai', - 'model' => 'gpt-4', - 'track_token_usage' => true, - ], - ], - ], - ]); - - $agentId = 'ai.agent.tracked_agent'; - - // Token usage processor must exist for OpenAI platform - $tokenUsageProcessor = $container->getDefinition('ai.platform.token_usage_processor.openai'); - $outputTags = $tokenUsageProcessor->getTag('ai.agent.output_processor'); - - $foundTag = false; - foreach ($outputTags as $tag) { - if (($tag['agent'] ?? '') === $agentId) { - $foundTag = true; - break; - } - } - - $this->assertTrue($foundTag, 'Token usage processor should have output tag with full agent ID'); - } - public function testOpenAiPlatformWithDefaultRegion() { $container = $this->buildContainer([ @@ -7082,7 +7045,6 @@ private function getFullConfig(): array 'nested' => ['options' => ['work' => 'too']], ], ], - 'track_token_usage' => true, 'prompt' => [ 'text' => 'You are a helpful assistant.', 'include_tools' => true, diff --git a/src/platform/src/Bridge/Anthropic/ResultConverter.php b/src/platform/src/Bridge/Anthropic/ResultConverter.php index c7515de6e..a3379d686 100644 --- a/src/platform/src/Bridge/Anthropic/ResultConverter.php +++ b/src/platform/src/Bridge/Anthropic/ResultConverter.php @@ -77,6 +77,11 @@ public function convert(RawHttpResult|RawResultInterface $result, array $options return new TextResult($data['content'][0]['text']); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + private function convertStream(RawResultInterface $result): \Generator { foreach ($result->getDataStream() as $data) { diff --git a/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php b/src/platform/src/Bridge/Anthropic/TokenUsageExtractor.php similarity index 55% rename from src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php rename to src/platform/src/Bridge/Anthropic/TokenUsageExtractor.php index 6a744e35e..783eb26d3 100644 --- a/src/platform/src/Bridge/Anthropic/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Anthropic/TokenUsageExtractor.php @@ -11,33 +11,23 @@ namespace Symfony\AI\Platform\Bridge\Anthropic; -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; -final class TokenOutputProcessor implements OutputProcessorInterface +final class TokenUsageExtractor implements TokenUsageExtractorInterface { - public function processOutput(Output $output): void + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsage { - if ($output->getResult() instanceof StreamResult) { + if ($options['stream'] ?? false) { // Streams have to be handled manually as the tokens are part of the streamed chunks - return; + return null; } - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } - - $metadata = $output->getResult()->getMetadata(); - $content = $rawResponse->toArray(false); + $content = $rawResult->getData(); if (!\array_key_exists('usage', $content)) { - $metadata->add('token_usage', new TokenUsage()); - - return; + return null; } $usage = $content['usage']; @@ -46,11 +36,11 @@ public function processOutput(Output $output): void $cachedTokens = ($usage['cache_creation_input_tokens'] ?? 0) + ($usage['cache_read_input_tokens'] ?? 0); } - $metadata->add('token_usage', new TokenUsage( + return 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/Azure/Meta/LlamaResultConverter.php b/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php index f9cf2226c..43ffa62d5 100644 --- a/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php @@ -17,6 +17,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Christopher Hertel @@ -38,4 +39,9 @@ public function convert(RawResultInterface $result, array $options = []): TextRe return new TextResult($data['choices'][0]['message']['content']); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php index 61b49d0b1..417422096 100644 --- a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php @@ -20,6 +20,7 @@ use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Björn Altmann @@ -55,4 +56,9 @@ public function convert(RawResultInterface|RawBedrockResult $result, array $opti return new TextResult($data['content'][0]['text']); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php b/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php index a1e0e5cf5..8330fd0ab 100644 --- a/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Björn Altmann @@ -39,4 +40,9 @@ public function convert(RawResultInterface|RawBedrockResult $result, array $opti return new TextResult($data['generation']); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php b/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php index bf2c1169d..3383a245a 100644 --- a/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php @@ -19,6 +19,7 @@ use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Björn Altmann @@ -54,4 +55,9 @@ public function convert(RawResultInterface|RawBedrockResult $result, array $opti return new TextResult($data['output']['message']['content'][0]['text']); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Cartesia/CartesiaResultConverter.php b/src/platform/src/Bridge/Cartesia/CartesiaResultConverter.php index 6248ea8b2..5708cb319 100644 --- a/src/platform/src/Bridge/Cartesia/CartesiaResultConverter.php +++ b/src/platform/src/Bridge/Cartesia/CartesiaResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** @@ -41,4 +42,9 @@ public function convert(RawResultInterface $result, array $options = []): Result default => throw new RuntimeException('Unsupported Cartesia response.'), }; } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Cerebras/ResultConverter.php b/src/platform/src/Bridge/Cerebras/ResultConverter.php index 99d5f0f09..7e6c33591 100644 --- a/src/platform/src/Bridge/Cerebras/ResultConverter.php +++ b/src/platform/src/Bridge/Cerebras/ResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\StreamResult; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Junaid Farooq @@ -48,6 +49,11 @@ public function convert(RawResultInterface $result, array $options = []): Result return new TextResult($data['choices'][0]['message']['content']); } + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } + private function convertStream(RawResultInterface $result): \Generator { foreach ($result->getDataStream() as $data) { diff --git a/src/platform/src/Bridge/Decart/DecartResultConverter.php b/src/platform/src/Bridge/Decart/DecartResultConverter.php index 2735bfc31..e7347214f 100644 --- a/src/platform/src/Bridge/Decart/DecartResultConverter.php +++ b/src/platform/src/Bridge/Decart/DecartResultConverter.php @@ -16,6 +16,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** @@ -37,4 +38,9 @@ public function convert(RawResultInterface $result, array $options = []): Result return new BinaryResult($response->getContent(), $headers['content-type'][0]); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/DeepSeek/ResultConverter.php b/src/platform/src/Bridge/DeepSeek/ResultConverter.php index 35f64ddee..406a6e631 100644 --- a/src/platform/src/Bridge/DeepSeek/ResultConverter.php +++ b/src/platform/src/Bridge/DeepSeek/ResultConverter.php @@ -59,6 +59,11 @@ public function convert(RawResultInterface $result, array $options = []): Result return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + private function convertStream(RawResultInterface $result): \Generator { $toolCalls = []; diff --git a/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php b/src/platform/src/Bridge/DeepSeek/TokenUsageExtractor.php similarity index 50% rename from src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php rename to src/platform/src/Bridge/DeepSeek/TokenUsageExtractor.php index b449675fd..a80d74dc8 100644 --- a/src/platform/src/Bridge/DeepSeek/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/DeepSeek/TokenUsageExtractor.php @@ -11,39 +11,34 @@ namespace Symfony\AI\Platform\Bridge\DeepSeek; -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; /** * @author Oskar Stark */ -final class TokenOutputProcessor implements OutputProcessorInterface +final class TokenUsageExtractor implements TokenUsageExtractorInterface { - public function processOutput(Output $output): void + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface { - if ($output->getResult() instanceof StreamResult) { + if ($options['stream'] ?? false) { // Streams have to be handled manually as the tokens are part of the streamed chunks - return; + return null; } - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } + $content = $rawResult->getData(); - $metadata = $output->getResult()->getMetadata(); - $content = $rawResponse->toArray(false); + if (!\array_key_exists('usage', $content)) { + return null; + } - $tokenUsage = new TokenUsage( + return 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/DockerModelRunner/Completions/ResultConverter.php b/src/platform/src/Bridge/DockerModelRunner/Completions/ResultConverter.php index 5b208dd5a..85932b563 100644 --- a/src/platform/src/Bridge/DockerModelRunner/Completions/ResultConverter.php +++ b/src/platform/src/Bridge/DockerModelRunner/Completions/ResultConverter.php @@ -26,6 +26,7 @@ use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Mathieu Santostefano @@ -67,6 +68,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } + private function convertStream(RawResultInterface $result): \Generator { $toolCalls = []; diff --git a/src/platform/src/Bridge/DockerModelRunner/Embeddings/ResultConverter.php b/src/platform/src/Bridge/DockerModelRunner/Embeddings/ResultConverter.php index a63af9b55..a2aa7c74e 100644 --- a/src/platform/src/Bridge/DockerModelRunner/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/DockerModelRunner/Embeddings/ResultConverter.php @@ -50,4 +50,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php b/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php index 70d561c4f..9625193b9 100644 --- a/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php +++ b/src/platform/src/Bridge/ElevenLabs/ElevenLabsResultConverter.php @@ -50,6 +50,11 @@ public function convert(RawHttpResult|RawResultInterface $result, array $options }; } + public function getTokenUsageExtractor(): null + { + return null; + } + private function convertToGenerator(ResponseInterface $response): \Generator { foreach ($this->httpClient->stream($response) as $chunk) { diff --git a/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php index 1f858718f..9e9d92a08 100644 --- a/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php @@ -44,4 +44,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index 54cb7b1f4..9663e2a1f 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -67,6 +67,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + private function convertStream(RawResultInterface $result): \Generator { foreach ($result->getDataStream() as $data) { diff --git a/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php b/src/platform/src/Bridge/Gemini/Gemini/TokenUsageExtractor.php similarity index 52% rename from src/platform/src/Bridge/Gemini/TokenOutputProcessor.php rename to src/platform/src/Bridge/Gemini/Gemini/TokenUsageExtractor.php index 2dc7d7b4f..ec1a9ab21 100644 --- a/src/platform/src/Bridge/Gemini/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Gemini/Gemini/TokenUsageExtractor.php @@ -9,32 +9,29 @@ * file that was distributed with this source code. */ -namespace Symfony\AI\Platform\Bridge\Gemini; +namespace Symfony\AI\Platform\Bridge\Gemini\Gemini; -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; -final class TokenOutputProcessor implements OutputProcessorInterface +final class TokenUsageExtractor implements TokenUsageExtractorInterface { - public function processOutput(Output $output): void + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface { - if ($output->getResult() instanceof StreamResult) { + if ($options['stream'] ?? false) { // Streams have to be handled manually as the tokens are part of the streamed chunks - return; + return null; } - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } + $content = $rawResult->getData(); - $metadata = $output->getResult()->getMetadata(); - $content = $rawResponse->toArray(false); + if (!\array_key_exists('usageMetadata', $content)) { + return null; + } - $tokenUsage = new TokenUsage( + return new TokenUsage( promptTokens: $content['usageMetadata']['promptTokenCount'] ?? null, completionTokens: $content['usageMetadata']['candidatesTokenCount'] ?? null, thinkingTokens: $content['usageMetadata']['thoughtsTokenCount'] ?? null, @@ -42,7 +39,5 @@ public function processOutput(Output $output): void cachedTokens: $content['usageMetadata']['cachedContentTokenCount'] ?? null, totalTokens: $content['usageMetadata']['totalTokenCount'] ?? null, ); - - $metadata->add('token_usage', $tokenUsage); } } diff --git a/src/platform/src/Bridge/Generic/Completions/ResultConverter.php b/src/platform/src/Bridge/Generic/Completions/ResultConverter.php index f9e800998..d5b704990 100644 --- a/src/platform/src/Bridge/Generic/Completions/ResultConverter.php +++ b/src/platform/src/Bridge/Generic/Completions/ResultConverter.php @@ -27,6 +27,7 @@ use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * This default implementation is based on the OpenAI GPT completion API. @@ -82,6 +83,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } + private function convertStream(RawResultInterface|RawHttpResult $result): \Generator { $toolCalls = []; diff --git a/src/platform/src/Bridge/Generic/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Generic/Embeddings/ResultConverter.php index 26869d0ac..4a65cd523 100644 --- a/src/platform/src/Bridge/Generic/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Generic/Embeddings/ResultConverter.php @@ -20,6 +20,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; /** @@ -66,4 +67,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/HuggingFace/ResultConverter.php b/src/platform/src/Bridge/HuggingFace/ResultConverter.php index 0af31d68e..2f86943d8 100644 --- a/src/platform/src/Bridge/HuggingFace/ResultConverter.php +++ b/src/platform/src/Bridge/HuggingFace/ResultConverter.php @@ -31,6 +31,7 @@ use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; /** @@ -96,4 +97,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options default => throw new RuntimeException(\sprintf('Unsupported task: %s', $task)), }; } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php index af5de3547..b6455875f 100644 --- a/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; /** @@ -51,4 +52,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options ), ); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php index 987d8b9c4..4ca9ee948 100644 --- a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php +++ b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php @@ -60,6 +60,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + private function convertStream(RawResultInterface $result): \Generator { $toolCalls = []; diff --git a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php b/src/platform/src/Bridge/Mistral/Llm/TokenUsageExtractor.php similarity index 63% rename from src/platform/src/Bridge/Mistral/TokenOutputProcessor.php rename to src/platform/src/Bridge/Mistral/Llm/TokenUsageExtractor.php index 4206adf4e..f32391c32 100644 --- a/src/platform/src/Bridge/Mistral/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Mistral/Llm/TokenUsageExtractor.php @@ -9,47 +9,43 @@ * file that was distributed with this source code. */ -namespace Symfony\AI\Platform\Bridge\Mistral; +namespace Symfony\AI\Platform\Bridge\Mistral\Llm; -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** * @author Quentin Fahrner */ -final class TokenOutputProcessor implements OutputProcessorInterface +final class TokenUsageExtractor implements TokenUsageExtractorInterface { - public function processOutput(Output $output): void + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsage { - if ($output->getResult() instanceof StreamResult) { + if ($options['stream'] ?? false) { // Streams have to be handled manually as the tokens are part of the streamed chunks - return; + return null; } - $rawResponse = $output->getResult()->getRawResult()?->getObject(); + $rawResponse = $rawResult->getObject(); if (!$rawResponse instanceof ResponseInterface) { - return; + return null; } - $metadata = $output->getResult()->getMetadata(); $headers = $rawResponse->getHeaders(false); $remainingTokensMinute = $headers['x-ratelimit-limit-tokens-minute'][0] ?? null; $remainingTokensMonth = $headers['x-ratelimit-limit-tokens-month'][0] ?? null; - $content = $rawResponse->toArray(false); + $content = $rawResult->getData(); - $tokenUsage = new TokenUsage( + return 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, ); - - $metadata->add('token_usage', $tokenUsage); } } diff --git a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php index cf62c61e1..25988d82b 100644 --- a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php +++ b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php @@ -21,6 +21,7 @@ use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; /** @@ -46,10 +47,15 @@ public function convert(RawResultInterface $result, array $options = []): Result : $this->doConvertCompletion($data); } + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } + /** * @param array $data */ - public function doConvertCompletion(array $data): ResultInterface + private function doConvertCompletion(array $data): ResultInterface { if (!isset($data['message'])) { throw new RuntimeException('Response does not contain message.'); @@ -75,7 +81,7 @@ public function doConvertCompletion(array $data): ResultInterface /** * @param array $data */ - public function doConvertEmbeddings(array $data): ResultInterface + private function doConvertEmbeddings(array $data): ResultInterface { if ([] === $data['embeddings']) { throw new RuntimeException('Response does not contain embeddings.'); diff --git a/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php b/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php index e95381d22..7e3383078 100644 --- a/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\ResultConverterInterface; use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @see https://platform.openai.com/docs/api-reference/images/create @@ -52,4 +53,9 @@ public function convert(RawResultInterface $result, array $options = []): Result return new ImageResult($image['revised_prompt'] ?? null, ...$images); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php index cae4321fd..5723388ac 100644 --- a/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; /** @@ -49,4 +50,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php index 8be0ba6b3..b11e17ff2 100644 --- a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php @@ -93,6 +93,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($results) ? array_pop($results) : new ChoiceResult(...$results); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + /** * @param array $output * diff --git a/src/platform/src/Bridge/OpenAi/Gpt/TokenUsageExtractor.php b/src/platform/src/Bridge/OpenAi/Gpt/TokenUsageExtractor.php new file mode 100644 index 000000000..6ff1bf5ef --- /dev/null +++ b/src/platform/src/Bridge/OpenAi/Gpt/TokenUsageExtractor.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\OpenAi\Gpt; + +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Denis Zunke + */ +final class TokenUsageExtractor implements TokenUsageExtractorInterface +{ + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsage + { + if ($options['stream'] ?? false) { + // Streams have to be handled manually as the tokens are part of the streamed chunks + return null; + } + + $rawResponse = $rawResult->getObject(); + if (!$rawResponse instanceof ResponseInterface) { + return null; + } + + $content = $rawResult->getData(); + + if (!\array_key_exists('usage', $content)) { + return null; + } + + $remainingTokens = $rawResponse->getHeaders(false)['x-ratelimit-remaining-tokens'][0] ?? null; + + return new TokenUsage( + promptTokens: $content['usage']['input_tokens'] ?? null, + completionTokens: $content['usage']['output_tokens'] ?? null, + thinkingTokens: $content['usage']['output_tokens_details']['reasoning_tokens'] ?? null, + cachedTokens: $content['usage']['input_tokens_details']['cached_tokens'] ?? null, + remainingTokens: null !== $remainingTokens ? (int) $remainingTokens : null, + totalTokens: $content['usage']['total_tokens'] ?? null, + ); + } +} diff --git a/src/platform/src/Bridge/OpenAi/TextToSpeech/ResultConverter.php b/src/platform/src/Bridge/OpenAi/TextToSpeech/ResultConverter.php index 1f93f2e9a..93ab16ae6 100644 --- a/src/platform/src/Bridge/OpenAi/TextToSpeech/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/TextToSpeech/ResultConverter.php @@ -18,12 +18,13 @@ use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\ResultConverterInterface as BaseResponseConverter; +use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Christopher Hertel */ -final class ResultConverter implements BaseResponseConverter +final class ResultConverter implements ResultConverterInterface { public function supports(Model $model): bool { @@ -40,4 +41,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return new BinaryResult($result->getObject()->getContent()); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php b/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php deleted file mode 100644 index c77e62cc8..000000000 --- a/src/platform/src/Bridge/OpenAi/TokenOutputProcessor.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Bridge\OpenAi; - -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * @author Denis Zunke - */ -final class TokenOutputProcessor implements OutputProcessorInterface -{ - public function processOutput(Output $output): void - { - if ($output->getResult() instanceof StreamResult) { - // Streams have to be handled manually as the tokens are part of the streamed chunks - return; - } - - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } - - $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, - ); - - $metadata->add('token_usage', $tokenUsage); - } -} diff --git a/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php index 97ad909ba..6f56be1d3 100644 --- a/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php @@ -16,12 +16,13 @@ use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\TextResult; -use Symfony\AI\Platform\ResultConverterInterface as BaseResponseConverter; +use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** * @author Christopher Hertel */ -final class ResultConverter implements BaseResponseConverter +final class ResultConverter implements ResultConverterInterface { public function supports(Model $model): bool { @@ -34,4 +35,9 @@ public function convert(RawResultInterface $result, array $options = []): Result return new TextResult($data['text']); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } } diff --git a/src/platform/src/Bridge/Perplexity/ResultConverter.php b/src/platform/src/Bridge/Perplexity/ResultConverter.php index 147886d7f..fe894f0db 100644 --- a/src/platform/src/Bridge/Perplexity/ResultConverter.php +++ b/src/platform/src/Bridge/Perplexity/ResultConverter.php @@ -50,6 +50,11 @@ public function convert(RawResultInterface $result, array $options = []): Result return $result; } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + private function convertStream(RawResultInterface $result): \Generator { $searchResults = $citations = []; diff --git a/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php b/src/platform/src/Bridge/Perplexity/TokenUsageExtractor.php similarity index 50% rename from src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php rename to src/platform/src/Bridge/Perplexity/TokenUsageExtractor.php index b27ea6dfd..a97ff5564 100644 --- a/src/platform/src/Bridge/Perplexity/TokenOutputProcessor.php +++ b/src/platform/src/Bridge/Perplexity/TokenUsageExtractor.php @@ -11,39 +11,34 @@ namespace Symfony\AI\Platform\Bridge\Perplexity; -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; /** * @author Mathieu Santostefano */ -final class TokenOutputProcessor implements OutputProcessorInterface +final class TokenUsageExtractor implements TokenUsageExtractorInterface { - public function processOutput(Output $output): void + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface { - if ($output->getResult() instanceof StreamResult) { + if ($options['stream'] ?? false) { // Streams have to be handled manually as the tokens are part of the streamed chunks - return; + return null; } - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } + $content = $rawResult->getData(); - $content = $rawResponse->toArray(false); + if (!\array_key_exists('usage', $content)) { + return null; + } - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = new TokenUsage( + return 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/Replicate/LlamaResultConverter.php b/src/platform/src/Bridge/Replicate/LlamaResultConverter.php index 8c5ff8cbf..ca4d11ecd 100644 --- a/src/platform/src/Bridge/Replicate/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Replicate/LlamaResultConverter.php @@ -39,4 +39,9 @@ public function convert(RawResultInterface $result, array $options = []): Result return new TextResult(implode('', $data['output'])); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/Scaleway/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Scaleway/Embeddings/ResultConverter.php index 97cdc6134..60354f447 100644 --- a/src/platform/src/Bridge/Scaleway/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Scaleway/Embeddings/ResultConverter.php @@ -49,4 +49,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/Scaleway/Llm/ResultConverter.php b/src/platform/src/Bridge/Scaleway/Llm/ResultConverter.php index fbfc6b549..4cb6689aa 100644 --- a/src/platform/src/Bridge/Scaleway/Llm/ResultConverter.php +++ b/src/platform/src/Bridge/Scaleway/Llm/ResultConverter.php @@ -54,6 +54,11 @@ public function convert(RawResultInterface $result, array $options = []): Result return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): null + { + return null; + } + private function convertStream(RawResultInterface $result): \Generator { $toolCalls = []; diff --git a/src/platform/src/Bridge/TransformersPhp/ResultConverter.php b/src/platform/src/Bridge/TransformersPhp/ResultConverter.php index 587844dbd..6eca50569 100644 --- a/src/platform/src/Bridge/TransformersPhp/ResultConverter.php +++ b/src/platform/src/Bridge/TransformersPhp/ResultConverter.php @@ -18,7 +18,7 @@ use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; -final readonly class ResultConverter implements ResultConverterInterface +final class ResultConverter implements ResultConverterInterface { public function supports(Model $model): bool { @@ -37,4 +37,9 @@ public function convert(RawResultInterface $result, array $options = []): TextRe return new ObjectResult($data); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/VertexAi/Embeddings/ResultConverter.php b/src/platform/src/Bridge/VertexAi/Embeddings/ResultConverter.php index 8c637170a..52feb4129 100644 --- a/src/platform/src/Bridge/VertexAi/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/VertexAi/Embeddings/ResultConverter.php @@ -47,4 +47,9 @@ public function convert(RawResultInterface $result, array $options = []): Vector ), ); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php index 16a70e391..db3f25d18 100644 --- a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php @@ -67,6 +67,11 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } + public function getTokenUsageExtractor(): TokenUsageExtractor + { + return new TokenUsageExtractor(); + } + /** * @throws TransportExceptionInterface */ diff --git a/src/platform/src/Bridge/VertexAi/Gemini/TokenUsageExtractor.php b/src/platform/src/Bridge/VertexAi/Gemini/TokenUsageExtractor.php new file mode 100644 index 000000000..4e96b1b8d --- /dev/null +++ b/src/platform/src/Bridge/VertexAi/Gemini/TokenUsageExtractor.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\VertexAi\Gemini; + +use Symfony\AI\Platform\Result\RawResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; + +/** + * @author Junaid Farooq + */ +final class TokenUsageExtractor implements TokenUsageExtractorInterface +{ + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface + { + if ($options['stream'] ?? false) { + $lastChunk = null; + + foreach ($rawResult->getDataStream() as $chunk) { + // Store last event that contains usage metadata + if (isset($chunk['usageMetadata'])) { + $lastChunk = $chunk; + } + } + + if ($lastChunk) { + return $this->extractUsageMetadata($lastChunk['usageMetadata']); + } + + return null; + } + + $content = $rawResult->getData(); + + if (!\array_key_exists('usageMetadata', $content)) { + return null; + } + + return $this->extractUsageMetadata($content['usageMetadata']); + } + + /** + * @param array{ + * promptTokenCount?: int, + * candidatesTokenCount?: int, + * thoughtsTokenCount?: int, + * cachedContentTokenCount?: int, + * totalTokenCount?: int + * } $usage + */ + private function extractUsageMetadata(array $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/Bridge/VertexAi/TokenOutputProcessor.php b/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php deleted file mode 100644 index 2c23bc33c..000000000 --- a/src/platform/src/Bridge/VertexAi/TokenOutputProcessor.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Bridge\VertexAi; - -use Symfony\AI\Agent\Output; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * @author Junaid Farooq - */ -final class TokenOutputProcessor implements OutputProcessorInterface -{ - /** - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws DecodingExceptionInterface - * @throws ClientExceptionInterface - */ - public function processOutput(Output $output): void - { - $metadata = $output->getResult()->getMetadata(); - - if ($output->getResult() instanceof StreamResult) { - $lastChunk = null; - - foreach ($output->getResult()->getContent() as $chunk) { - // Store last event that contains usage metadata - if (isset($chunk['usageMetadata'])) { - $lastChunk = $chunk; - } - } - - if ($lastChunk) { - $metadata->add('token_usage', $this->extractUsageMetadata($lastChunk['usageMetadata'])); - } - - return; - } - - $rawResponse = $output->getResult()->getRawResult()?->getObject(); - if (!$rawResponse instanceof ResponseInterface) { - return; - } - - $content = $rawResponse->toArray(false); - - $metadata->add('token_usage', $this->extractUsageMetadata($content['usageMetadata'] ?? [])); - } - - /** - * @param array{ - * promptTokenCount?: int, - * candidatesTokenCount?: int, - * thoughtsTokenCount?: int, - * cachedContentTokenCount?: int, - * totalTokenCount?: int - * } $usage - */ - private function extractUsageMetadata(array $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/Bridge/Voyage/ResultConverter.php b/src/platform/src/Bridge/Voyage/ResultConverter.php index e4619cf06..8f37bd181 100644 --- a/src/platform/src/Bridge/Voyage/ResultConverter.php +++ b/src/platform/src/Bridge/Voyage/ResultConverter.php @@ -44,4 +44,9 @@ public function convert(RawResultInterface $result, array $options = []): Result ), ); } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Result/DeferredResult.php b/src/platform/src/Result/DeferredResult.php index ea9ce05cd..a2fc41beb 100644 --- a/src/platform/src/Result/DeferredResult.php +++ b/src/platform/src/Result/DeferredResult.php @@ -50,11 +50,17 @@ public function getResult(): ResultInterface $this->convertedResult->setRawResult($this->rawResult); } + $this->convertedResult->getMetadata()->set($this->getMetadata()->all()); + + if (null !== $tokenUsageExtractor = $this->resultConverter->getTokenUsageExtractor()) { + if (null !== $tokenUsage = $tokenUsageExtractor->extract($this->rawResult, $this->options)) { + $this->convertedResult->getMetadata()->add('token_usage', $tokenUsage); + } + } + $this->isConverted = true; } - $this->convertedResult->getMetadata()->set($this->getMetadata()->all()); - return $this->convertedResult; } diff --git a/src/platform/src/ResultConverterInterface.php b/src/platform/src/ResultConverterInterface.php index 71f605f3d..14a6771a1 100644 --- a/src/platform/src/ResultConverterInterface.php +++ b/src/platform/src/ResultConverterInterface.php @@ -14,8 +14,11 @@ use Symfony\AI\Platform\Exception\ExceptionInterface; use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\ResultInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; /** + * Implementations handle the conversion of result data into structured objects. + * * @author Christopher Hertel */ interface ResultConverterInterface @@ -23,9 +26,16 @@ interface ResultConverterInterface public function supports(Model $model): bool; /** + * Converts the main result data into a ResultInterface instance. + * * @param array $options * * @throws ExceptionInterface */ public function convert(RawResultInterface $result, array $options = []): ResultInterface; + + /** + * Returns a TokenUsageExtractorInterface instance if available, null otherwise. + */ + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface; } diff --git a/src/platform/src/StructuredOutput/ResultConverter.php b/src/platform/src/StructuredOutput/ResultConverter.php index 2e2c5f5d3..c9fd0e82e 100644 --- a/src/platform/src/StructuredOutput/ResultConverter.php +++ b/src/platform/src/StructuredOutput/ResultConverter.php @@ -18,6 +18,7 @@ use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -58,4 +59,9 @@ public function convert(RawResultInterface $result, array $options = []): Result return $objectResult; } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return $this->innerConverter->getTokenUsageExtractor(); + } } diff --git a/src/platform/src/Test/PlainConverter.php b/src/platform/src/Test/PlainConverter.php index df139ce7f..866264615 100644 --- a/src/platform/src/Test/PlainConverter.php +++ b/src/platform/src/Test/PlainConverter.php @@ -32,4 +32,9 @@ public function convert(RawResultInterface $result, array $options = []): Result { return $this->result; } + + public function getTokenUsageExtractor(): null + { + return null; + } } diff --git a/src/platform/src/Metadata/TokenUsage.php b/src/platform/src/TokenUsage/TokenUsage.php similarity index 97% rename from src/platform/src/Metadata/TokenUsage.php rename to src/platform/src/TokenUsage/TokenUsage.php index 8c2383d1b..7160e8e02 100644 --- a/src/platform/src/Metadata/TokenUsage.php +++ b/src/platform/src/TokenUsage/TokenUsage.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\AI\Platform\Metadata; +namespace Symfony\AI\Platform\TokenUsage; /** * @author Junaid Farooq diff --git a/src/platform/src/Metadata/TokenUsageAggregation.php b/src/platform/src/TokenUsage/TokenUsageAggregation.php similarity index 98% rename from src/platform/src/Metadata/TokenUsageAggregation.php rename to src/platform/src/TokenUsage/TokenUsageAggregation.php index 299f183ec..96e323a50 100644 --- a/src/platform/src/Metadata/TokenUsageAggregation.php +++ b/src/platform/src/TokenUsage/TokenUsageAggregation.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\AI\Platform\Metadata; +namespace Symfony\AI\Platform\TokenUsage; /** * @author Christopher Hertel diff --git a/src/platform/src/TokenUsage/TokenUsageExtractorInterface.php b/src/platform/src/TokenUsage/TokenUsageExtractorInterface.php new file mode 100644 index 000000000..b28e82a05 --- /dev/null +++ b/src/platform/src/TokenUsage/TokenUsageExtractorInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\TokenUsage; + +use Symfony\AI\Platform\Result\RawResultInterface; + +/** + * Implementations handle the extraction of token usage data from raw results. + * + * @author Christopher Hertel + */ +interface TokenUsageExtractorInterface +{ + /** + * @param array $options + */ + public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface; +} diff --git a/src/platform/src/Metadata/TokenUsageInterface.php b/src/platform/src/TokenUsage/TokenUsageInterface.php similarity index 94% rename from src/platform/src/Metadata/TokenUsageInterface.php rename to src/platform/src/TokenUsage/TokenUsageInterface.php index 077f16911..330e5df0d 100644 --- a/src/platform/src/Metadata/TokenUsageInterface.php +++ b/src/platform/src/TokenUsage/TokenUsageInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\AI\Platform\Metadata; +namespace Symfony\AI\Platform\TokenUsage; /** * @author Christopher Hertel diff --git a/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php deleted file mode 100644 index 45664b7ae..000000000 --- a/src/platform/tests/Bridge/Anthropic/TokenOutputProcessorTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Bridge\Anthropic; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\Anthropic\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -final class TokenOutputProcessorTest extends TestCase -{ - public function testItHandlesStreamResponsesWithoutProcessing() - { - $processor = new TokenOutputProcessor(); - $streamResult = new StreamResult((static function () { yield 'test'; })()); - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItAddsRemainingTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $textResult->setRawResult($this->createRawResult()); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->getRemainingTokens()); - } - - public function testItAddsUsageTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - 'input_tokens' => 10, - 'output_tokens' => 20, - 'server_tool_use' => [ - 'web_search_requests' => 30, - ], - 'cache_creation_input_tokens' => 40, - 'cache_read_input_tokens' => 50, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - // Missing some fields - 'input_tokens' => 10, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertNull($tokenUsage->getRemainingTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - private function createRawResult(array $data = []): RawHttpResult - { - $rawResponse = $this->createStub(ResponseInterface::class); - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('claude-3-5-sonnet-latest', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/Anthropic/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/Anthropic/TokenUsageExtractorTest.php new file mode 100644 index 000000000..fd912e419 --- /dev/null +++ b/src/platform/tests/Bridge/Anthropic/TokenUsageExtractorTest.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 Bridge\Anthropic; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Anthropic\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; + +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(['some' => 'data']))); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'input_tokens' => 10, + 'output_tokens' => 20, + 'server_tool_use' => [ + 'web_search_requests' => 30, + ], + 'cache_creation_input_tokens' => 40, + 'cache_read_input_tokens' => 50, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $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() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + // Missing some fields + 'input_tokens' => 10, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } +} diff --git a/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php deleted file mode 100644 index d7d552698..000000000 --- a/src/platform/tests/Bridge/Gemini/TokenOutputProcessorTest.php +++ /dev/null @@ -1,145 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Bridge\Gemini; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\Gemini\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -final class TokenOutputProcessorTest extends TestCase -{ - public function testItHandlesStreamResponsesWithoutProcessing() - { - $processor = new TokenOutputProcessor(); - $streamResult = new StreamResult((static function () { yield 'test'; })()); - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItAddsRemainingTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $textResult->setRawResult($this->createRawResult()); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->getRemainingTokens()); - } - - public function testItAddsUsageTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usageMetadata' => [ - 'promptTokenCount' => 10, - 'candidatesTokenCount' => 20, - 'totalTokenCount' => 50, - 'thoughtsTokenCount' => 20, - 'cachedContentTokenCount' => 40, - 'toolUsePromptTokenCount' => 5, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usageMetadata' => [ - // Missing some fields - 'promptTokenCount' => 10, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertNull($tokenUsage->getRemainingTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - private function createRawResult(array $data = []): RawHttpResult - { - $rawResponse = $this->createStub(ResponseInterface::class); - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('gemini-2.5-pro', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/Gemini/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/Gemini/TokenUsageExtractorTest.php new file mode 100644 index 000000000..588d842a1 --- /dev/null +++ b/src/platform/tests/Bridge/Gemini/TokenUsageExtractorTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Gemini; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Gemini\Gemini\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; + +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(['some' => 'data']))); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usageMetadata' => [ + 'promptTokenCount' => 10, + 'candidatesTokenCount' => 20, + 'totalTokenCount' => 50, + 'thoughtsTokenCount' => 20, + 'cachedContentTokenCount' => 40, + 'toolUsePromptTokenCount' => 5, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $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() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usageMetadata' => [ + // Missing some fields + 'promptTokenCount' => 10, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } +} diff --git a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php deleted file mode 100644 index ca42f6b14..000000000 --- a/src/platform/tests/Bridge/Mistral/TokenOutputProcessorTest.php +++ /dev/null @@ -1,147 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Bridge\Mistral; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\Mistral\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -final class TokenOutputProcessorTest extends TestCase -{ - public function testItHandlesStreamResponsesWithoutProcessing() - { - $processor = new TokenOutputProcessor(); - $streamResult = new StreamResult((static function () { yield 'test'; })()); - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItAddsRemainingTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $textResult->setRawResult($this->createRawResponse()); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->getRemainingTokensMinute()); - $this->assertSame(1000000, $tokenUsage->getRemainingTokensMonth()); - } - - public function testItAddsUsageTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResponse = $this->createRawResponse([ - 'usage' => [ - 'prompt_tokens' => 10, - 'completion_tokens' => 20, - 'total_tokens' => 30, - ], - ]); - - $textResult->setRawResult($rawResponse); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResponse = $this->createRawResponse([ - 'usage' => [ - // Missing some fields - 'prompt_tokens' => 10, - ], - ]); - - $textResult->setRawResult($rawResponse); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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 - { - $rawResponse = $this->createStub(ResponseInterface::class); - $rawResponse->method('getHeaders')->willReturn([ - 'x-ratelimit-limit-tokens-minute' => ['1000'], - 'x-ratelimit-limit-tokens-month' => ['1000000'], - ]); - - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('ministral-3b-latest', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/Mistral/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/Mistral/TokenUsageExtractorTest.php new file mode 100644 index 000000000..ee2757798 --- /dev/null +++ b/src/platform/tests/Bridge/Mistral/TokenUsageExtractorTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Mistral; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Mistral\Llm\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(['some' => 'data']))); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'prompt_tokens' => 10, + 'completion_tokens' => 20, + 'total_tokens' => 30, + ], + ], object: $this->createResponseObject()); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $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() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + // Missing some fields + 'prompt_tokens' => 10, + ], + ], object: $this->createResponseObject()); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $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 createResponseObject(): ResponseInterface|MockObject + { + $response = $this->createStub(ResponseInterface::class); + $response->method('getHeaders')->willReturn([ + 'x-ratelimit-limit-tokens-minute' => ['1000'], + 'x-ratelimit-limit-tokens-month' => ['1000000'], + ]); + + return $response; + } +} diff --git a/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php deleted file mode 100644 index 7e46efeb2..000000000 --- a/src/platform/tests/Bridge/OpenAi/TokenOutputProcessorTest.php +++ /dev/null @@ -1,150 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Bridge\OpenAi; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\OpenAi\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -final class TokenOutputProcessorTest extends TestCase -{ - public function testItHandlesStreamResponsesWithoutProcessing() - { - $processor = new TokenOutputProcessor(); - $streamResult = new StreamResult((static function () { yield 'test'; })()); - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItAddsRemainingTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $textResult->setRawResult($this->createRawResult()); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(1000, $tokenUsage->getRemainingTokens()); - } - - public function testItAddsUsageTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - 'prompt_tokens' => 10, - 'completion_tokens' => 20, - 'total_tokens' => 50, - 'completion_tokens_details' => [ - 'reasoning_tokens' => 20, - ], - 'prompt_tokens_details' => [ - 'cached_tokens' => 40, - ], - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - // Missing some fields - 'prompt_tokens' => 10, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertSame(1000, $tokenUsage->getRemainingTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - private function createRawResult(array $data = []): RawHttpResult - { - $rawResponse = $this->createStub(ResponseInterface::class); - $rawResponse->method('getHeaders')->willReturn([ - 'x-ratelimit-remaining-tokens' => ['1000'], - ]); - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('gpt-4o', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/OpenAi/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/OpenAi/TokenUsageExtractorTest.php new file mode 100644 index 000000000..ff746aa98 --- /dev/null +++ b/src/platform/tests/Bridge/OpenAi/TokenUsageExtractorTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\OpenAi; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\OpenAi\Gpt\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(['some' => 'data']))); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'input_tokens' => 10, + 'output_tokens' => 20, + 'total_tokens' => 50, + 'output_tokens_details' => [ + 'reasoning_tokens' => 20, + ], + 'input_tokens_details' => [ + 'cached_tokens' => 40, + ], + ], + ], object: $this->createResponseObject()); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $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() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + // Missing some fields + 'input_tokens' => 10, + ], + ], object: $this->createResponseObject()); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(1000, $tokenUsage->getRemainingTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } + + private function createResponseObject(): ResponseInterface|MockObject + { + $response = $this->createStub(ResponseInterface::class); + $response->method('getHeaders')->willReturn([ + 'x-ratelimit-remaining-tokens' => ['1000'], + ]); + + return $response; + } +} diff --git a/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php deleted file mode 100644 index cb640dc9a..000000000 --- a/src/platform/tests/Bridge/Perplexity/TokenOutputProcessorTest.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Bridge\Perplexity; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\Perplexity\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * @author Mathieu Santostefano - */ -final class TokenOutputProcessorTest extends TestCase -{ - public function testItHandlesStreamResponsesWithoutProcessing() - { - $processor = new TokenOutputProcessor(); - $streamResult = new StreamResult((static function () { yield 'test'; })()); - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $this->assertCount(0, $metadata); - } - - public function testItAddsUsageTokensToMetadata() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - 'prompt_tokens' => 10, - 'completion_tokens' => 20, - 'total_tokens' => 50, - 'reasoning_tokens' => 20, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertSame(20, $tokenUsage->getCompletionTokens()); - $this->assertSame(20, $tokenUsage->getThinkingTokens()); - $this->assertSame(50, $tokenUsage->getTotalTokens()); - } - - public function testItHandlesMissingUsageFields() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - - $rawResult = $this->createRawResult([ - 'usage' => [ - // Missing some fields - 'prompt_tokens' => 10, - ], - ]); - - $textResult->setRawResult($rawResult); - - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getThinkingTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - private function createRawResult(array $data = []): RawHttpResult - { - $rawResponse = $this->createStub(ResponseInterface::class); - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('sonar', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/Perplexity/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/Perplexity/TokenUsageExtractorTest.php new file mode 100644 index 000000000..ab54e87a2 --- /dev/null +++ b/src/platform/tests/Bridge/Perplexity/TokenUsageExtractorTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\Perplexity; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Perplexity\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; + +/** + * @author Mathieu Santostefano + */ +final class TokenUsageExtractorTest extends TestCase +{ + public function testItHandlesStreamResponsesWithoutProcessing() + { + $extractor = new TokenUsageExtractor(); + + $this->assertNull($extractor->extract(new InMemoryRawResult(), ['stream' => true])); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + 'prompt_tokens' => 10, + 'completion_tokens' => 20, + 'total_tokens' => 50, + 'reasoning_tokens' => 20, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); + } + + public function testItHandlesMissingUsageFields() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usage' => [ + // Missing some fields + 'prompt_tokens' => 10, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } +} diff --git a/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php b/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php deleted file mode 100644 index ca5d1edd3..000000000 --- a/src/platform/tests/Bridge/VertexAi/TokenOutputProcessorTest.php +++ /dev/null @@ -1,162 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Bridge\VertexAi; - -use PHPUnit\Framework\TestCase; -use Symfony\AI\Agent\Output; -use Symfony\AI\Platform\Bridge\VertexAi\TokenOutputProcessor; -use Symfony\AI\Platform\Message\MessageBag; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Result\RawHttpResult; -use Symfony\AI\Platform\Result\ResultInterface; -use Symfony\AI\Platform\Result\StreamResult; -use Symfony\AI\Platform\Result\TextResult; -use Symfony\Contracts\HttpClient\ResponseInterface; - -final class TokenOutputProcessorTest extends TestCase -{ - public function testItDoesNothingWithoutRawResponse() - { - $processor = new TokenOutputProcessor(); - $textResult = new TextResult('test'); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $this->assertCount(0, $output->getResult()->getMetadata()); - } - - public function testItAddsUsageTokensToMetadata() - { - $textResult = new TextResult('test'); - - $rawResponse = $this->createRawResponse([ - 'usageMetadata' => [ - 'promptTokenCount' => 10, - 'candidatesTokenCount' => 20, - 'thoughtsTokenCount' => 20, - 'totalTokenCount' => 50, - ], - ]); - - $textResult->setRawResult($rawResponse); - $processor = new TokenOutputProcessor(); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertSame(20, $tokenUsage->getCompletionTokens()); - $this->assertSame(20, $tokenUsage->getThinkingTokens()); - $this->assertSame(50, $tokenUsage->getTotalTokens()); - } - - public function testItHandlesMissingUsageFields() - { - $textResult = new TextResult('test'); - - $rawResponse = $this->createRawResponse([ - 'usageMetadata' => [ - 'promptTokenCount' => 10, - ], - ]); - - $textResult->setRawResult($rawResponse); - $processor = new TokenOutputProcessor(); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertSame(10, $tokenUsage->getPromptTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getThinkingTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - public function testItAddsEmptyTokenUsageWhenUsageMetadataNotPresent() - { - $textResult = new TextResult('test'); - $rawResponse = $this->createRawResponse(['other' => 'data']); - $textResult->setRawResult($rawResponse); - $processor = new TokenOutputProcessor(); - $output = $this->createOutput($textResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $this->assertNull($tokenUsage->getPromptTokens()); - $this->assertNull($tokenUsage->getCompletionTokens()); - $this->assertNull($tokenUsage->getThinkingTokens()); - $this->assertNull($tokenUsage->getTotalTokens()); - } - - public function testItHandlesStreamResults() - { - $processor = new TokenOutputProcessor(); - $chunks = [ - ['content' => 'chunk1'], - ['content' => 'chunk2', 'usageMetadata' => [ - 'promptTokenCount' => 15, - 'candidatesTokenCount' => 25, - 'totalTokenCount' => 40, - ]], - ]; - - $streamResult = new StreamResult((function () use ($chunks) { - foreach ($chunks as $chunk) { - yield $chunk; - } - })()); - - $output = $this->createOutput($streamResult); - - $processor->processOutput($output); - - $metadata = $output->getResult()->getMetadata(); - $tokenUsage = $metadata->get('token_usage'); - - $this->assertCount(1, $metadata); - $this->assertInstanceOf(TokenUsage::class, $tokenUsage); - $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 - { - $rawResponse = $this->createStub(ResponseInterface::class); - - $rawResponse->method('toArray')->willReturn($data); - - return new RawHttpResult($rawResponse); - } - - private function createOutput(ResultInterface $result): Output - { - return new Output('gemini-2.5-pro', $result, new MessageBag(), []); - } -} diff --git a/src/platform/tests/Bridge/VertexAi/TokenUsageExtractorTest.php b/src/platform/tests/Bridge/VertexAi/TokenUsageExtractorTest.php new file mode 100644 index 000000000..1bd068af9 --- /dev/null +++ b/src/platform/tests/Bridge/VertexAi/TokenUsageExtractorTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Tests\Bridge\VertexAi; + +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\VertexAi\Gemini\TokenUsageExtractor; +use Symfony\AI\Platform\Result\InMemoryRawResult; +use Symfony\AI\Platform\TokenUsage\TokenUsage; + +final class TokenUsageExtractorTest extends TestCase +{ + public function testItDoesNothingWithoutUsageData() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult(['other' => 'data']); + + $this->assertNull($extractor->extract($result)); + } + + public function testItExtractsTokenUsage() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usageMetadata' => [ + 'promptTokenCount' => 10, + 'candidatesTokenCount' => 20, + 'thoughtsTokenCount' => 20, + 'totalTokenCount' => 50, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertSame(20, $tokenUsage->getCompletionTokens()); + $this->assertSame(20, $tokenUsage->getThinkingTokens()); + $this->assertSame(50, $tokenUsage->getTotalTokens()); + } + + public function testItHandlesMissingUsageFields() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult([ + 'usageMetadata' => [ + 'promptTokenCount' => 10, + ], + ]); + + $tokenUsage = $extractor->extract($result); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(10, $tokenUsage->getPromptTokens()); + $this->assertNull($tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertNull($tokenUsage->getTotalTokens()); + } + + public function testItHandlesStreamResults() + { + $extractor = new TokenUsageExtractor(); + $result = new InMemoryRawResult(dataStream: [ + ['content' => 'chunk1'], + ['content' => 'chunk2', 'usageMetadata' => [ + 'promptTokenCount' => 15, + 'candidatesTokenCount' => 25, + 'totalTokenCount' => 40, + ]], + ]); + + $tokenUsage = $extractor->extract($result, ['stream' => true]); + + $this->assertInstanceOf(TokenUsage::class, $tokenUsage); + $this->assertSame(15, $tokenUsage->getPromptTokens()); + $this->assertSame(25, $tokenUsage->getCompletionTokens()); + $this->assertNull($tokenUsage->getThinkingTokens()); + $this->assertSame(40, $tokenUsage->getTotalTokens()); + } +} diff --git a/src/platform/tests/Metadata/TokenUsageAggregationTest.php b/src/platform/tests/Metadata/TokenUsageAggregationTest.php index d5fed8529..f6ff3490d 100644 --- a/src/platform/tests/Metadata/TokenUsageAggregationTest.php +++ b/src/platform/tests/Metadata/TokenUsageAggregationTest.php @@ -12,8 +12,8 @@ namespace Symfony\AI\Platform\Tests\Metadata; use PHPUnit\Framework\TestCase; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Metadata\TokenUsageAggregation; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageAggregation; class TokenUsageAggregationTest extends TestCase { diff --git a/src/platform/tests/Metadata/TokenUsageTest.php b/src/platform/tests/Metadata/TokenUsageTest.php index 92a0b0537..29c582988 100644 --- a/src/platform/tests/Metadata/TokenUsageTest.php +++ b/src/platform/tests/Metadata/TokenUsageTest.php @@ -12,8 +12,8 @@ namespace Symfony\AI\Platform\Tests\Metadata; use PHPUnit\Framework\TestCase; -use Symfony\AI\Platform\Metadata\TokenUsage; -use Symfony\AI\Platform\Metadata\TokenUsageInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsage; +use Symfony\AI\Platform\TokenUsage\TokenUsageInterface; class TokenUsageTest extends TestCase { diff --git a/src/store/tests/Double/PlatformTestHandler.php b/src/store/tests/Double/PlatformTestHandler.php index bc0e923e0..fd84410ff 100644 --- a/src/store/tests/Double/PlatformTestHandler.php +++ b/src/store/tests/Double/PlatformTestHandler.php @@ -21,6 +21,7 @@ use Symfony\AI\Platform\Result\ResultInterface; use Symfony\AI\Platform\Result\VectorResult; use Symfony\AI\Platform\ResultConverterInterface; +use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface; use Symfony\AI\Platform\Vector\Vector; use Symfony\Component\HttpClient\Response\MockResponse; @@ -56,4 +57,9 @@ public function convert(RawResultInterface $result, array $options = []): Result { return $this->create ?? new VectorResult(new Vector([1, 2, 3])); } + + public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface + { + return null; + } }