feature #1765 [Platform][Mistral] Add token usage extraction for embeddings (wachterjohannes)

This PR was merged into the main branch.

Discussion
----------

[Platform][Mistral] Add token usage extraction for embeddings

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Docs?         | no
| Issues        | -
| License       | MIT

## Description

This PR adds token usage extraction support for Mistral embeddings — the first embedding bridge to support this.

### Changes

1. **`Mistral\Embeddings\TokenUsageExtractor`** — New class implementing `TokenUsageExtractorInterface`. Extracts `prompt_tokens`, `total_tokens`, and rate limit headers (`x-ratelimit-limit-tokens-minute`, `x-ratelimit-limit-tokens-month`) from Mistral embedding responses. No `completionTokens` since embeddings don't produce completions.

2. **`Mistral\Embeddings\ResultConverter`** — `getTokenUsageExtractor()` now returns the new extractor instead of `null`.

3. **Updated Mistral embeddings example** to display token usage.

### Usage

```php
$result = $platform->invoke('mistral-embed', 'Some text to embed');

$tokenUsage = $result->getMetadata()->get('token_usage');
$tokenUsage->getPromptTokens();  // e.g. 15
$tokenUsage->getTotalTokens();   // e.g. 15
```

Commits
-------

81caade2 [Platform][Mistral] Add token usage extraction for embeddings
This commit is contained in:
Christopher Hertel
2026-03-15 21:47:32 +01:00
5 changed files with 136 additions and 2 deletions

View File

@@ -23,3 +23,4 @@ $result = $platform->invoke('mistral-embed', <<<TEXT
TEXT);
print_vectors($result);
print_token_usage($result->getMetadata()->get('token_usage'));

View File

@@ -1,6 +1,11 @@
CHANGELOG
=========
0.7
---
* Add token usage extraction for embeddings
0.1
---

View File

@@ -53,8 +53,8 @@ final class ResultConverter implements ResultConverterInterface
);
}
public function getTokenUsageExtractor(): ?TokenUsageExtractorInterface
public function getTokenUsageExtractor(): TokenUsageExtractorInterface
{
return null;
return new TokenUsageExtractor();
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\AI\Platform\Bridge\Mistral\Embeddings;
use Symfony\AI\Platform\Result\RawResultInterface;
use Symfony\AI\Platform\TokenUsage\TokenUsage;
use Symfony\AI\Platform\TokenUsage\TokenUsageExtractorInterface;
use Symfony\AI\Platform\TokenUsage\TokenUsageInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Johannes Wachter <johannes@sulu.io>
*/
final class TokenUsageExtractor implements TokenUsageExtractorInterface
{
public function extract(RawResultInterface $rawResult, array $options = []): ?TokenUsageInterface
{
$rawResponse = $rawResult->getObject();
if (!$rawResponse instanceof ResponseInterface) {
return null;
}
$headers = $rawResponse->getHeaders(false);
$remainingTokensMinute = $headers['x-ratelimit-limit-tokens-minute'][0] ?? null;
$remainingTokensMonth = $headers['x-ratelimit-limit-tokens-month'][0] ?? null;
$content = $rawResult->getData();
return new TokenUsage(
promptTokens: $content['usage']['prompt_tokens'] ?? null,
remainingTokensMinute: null !== $remainingTokensMinute ? (int) $remainingTokensMinute : null,
remainingTokensMonth: null !== $remainingTokensMonth ? (int) $remainingTokensMonth : null,
totalTokens: $content['usage']['total_tokens'] ?? null,
);
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Embeddings;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Mistral\Embeddings\TokenUsageExtractor;
use Symfony\AI\Platform\Result\InMemoryRawResult;
use Symfony\AI\Platform\TokenUsage\TokenUsage;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Johannes Wachter <johannes@sulu.io>
*/
final class TokenUsageExtractorTest extends TestCase
{
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,
'total_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->assertSame(10, $tokenUsage->getTotalTokens());
}
public function testItHandlesMissingUsageFields()
{
$extractor = new TokenUsageExtractor();
$result = new InMemoryRawResult([
'usage' => [
'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;
}
}