Skip to content

Commit e9d5bb5

Browse files
lochmuellerchr-hertel
authored andcommitted
[Platform][OpenRouter] Add Embeddings incl. example, fill Model catalog
1 parent 8be61b1 commit e9d5bb5

File tree

9 files changed

+588
-11
lines changed

9 files changed

+588
-11
lines changed

examples/openrouter/chat-gemini.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory;
1313
use Symfony\AI\Platform\Message\Message;
1414
use Symfony\AI\Platform\Message\MessageBag;
15-
use Symfony\AI\Platform\Model;
1615

1716
require_once dirname(__DIR__).'/bootstrap.php';
1817

1918
$platform = PlatformFactory::create(env('OPENROUTER_KEY'), http_client());
20-
// In case free is running into 429 rate limit errors, you can use the paid model:
21-
// $model = 'google/gemini-2.0-flash-lite-001';
2219
$model = 'google/gemini-2.0-flash-exp:free';
20+
// In case free is running into 404 errors, you can use the paid model:
21+
// $model = 'google/gemini-2.0-flash-lite-001';
2322

2423
$messages = new MessageBag(
2524
Message::forSystem('You are a helpful assistant.'),

examples/openrouter/embeddings.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory;
13+
14+
require_once dirname(__DIR__).'/bootstrap.php';
15+
16+
$platform = PlatformFactory::create(env('OPENROUTER_KEY'), http_client());
17+
18+
$result = $platform->invoke('openai/text-embedding-3-small', <<<TEXT
19+
Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and rivers.
20+
The people of Japan were very kind and hardworking. They loved their country very much and took care of it. The
21+
country was very peaceful and prosperous. The people lived happily ever after.
22+
TEXT);
23+
24+
print_vectors($result);

src/platform/src/Bridge/OpenRouter/ModelClient.php renamed to src/platform/src/Bridge/OpenRouter/Completions/ModelClient.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\AI\Platform\Bridge\OpenRouter;
12+
namespace Symfony\AI\Platform\Bridge\OpenRouter\Completions;
1313

1414
use Symfony\AI\Platform\Exception\InvalidArgumentException;
1515
use Symfony\AI\Platform\Model;
@@ -30,6 +30,7 @@ public function __construct(
3030
#[\SensitiveParameter] private readonly string $apiKey,
3131
) {
3232
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
33+
3334
if ('' === $apiKey) {
3435
throw new InvalidArgumentException('The API key must not be empty.');
3536
}

src/platform/src/Bridge/OpenRouter/ResultConverter.php renamed to src/platform/src/Bridge/OpenRouter/Completions/ResultConverter.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\AI\Platform\Bridge\OpenRouter;
12+
namespace Symfony\AI\Platform\Bridge\OpenRouter\Completions;
1313

14+
use Symfony\AI\Platform\Exception\AuthenticationException;
15+
use Symfony\AI\Platform\Exception\BadRequestException;
16+
use Symfony\AI\Platform\Exception\RateLimitExceededException;
1417
use Symfony\AI\Platform\Exception\RuntimeException;
1518
use Symfony\AI\Platform\Model;
1619
use Symfony\AI\Platform\Result\RawResultInterface;
@@ -30,8 +33,24 @@ public function supports(Model $model): bool
3033

3134
public function convert(RawResultInterface $result, array $options = []): ResultInterface
3235
{
36+
$response = $result->getObject();
3337
$data = $result->getData();
3438

39+
if (401 === $response->getStatusCode()) {
40+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'];
41+
throw new AuthenticationException($errorMessage);
42+
}
43+
44+
if (400 === $response->getStatusCode() || 404 === $response->getStatusCode()) {
45+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'] ?? 'Bad Request';
46+
throw new BadRequestException($errorMessage);
47+
}
48+
49+
if (429 === $response->getStatusCode()) {
50+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'] ?? 'Bad Request';
51+
throw new RateLimitExceededException($errorMessage);
52+
}
53+
3554
if (!isset($data['choices'][0]['message'])) {
3655
throw new RuntimeException('Response does not contain message.');
3756
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\OpenRouter;
13+
14+
use Symfony\AI\Platform\Model;
15+
16+
class Embeddings extends Model
17+
{
18+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\OpenRouter\Embeddings;
13+
14+
use Symfony\AI\Platform\Bridge\OpenRouter\Embeddings;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\ModelClientInterface;
18+
use Symfony\AI\Platform\Result\RawHttpResult;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* @author Tim Lochmüller <tim@fruit-lab.de>
23+
*/
24+
final class ModelClient implements ModelClientInterface
25+
{
26+
public function __construct(
27+
private readonly HttpClientInterface $httpClient,
28+
#[\SensitiveParameter] private readonly string $apiKey,
29+
) {
30+
if ('' === $apiKey) {
31+
throw new InvalidArgumentException('The API key must not be empty.');
32+
}
33+
if (!str_starts_with($apiKey, 'sk-')) {
34+
throw new InvalidArgumentException('The API key must start with "sk-".');
35+
}
36+
}
37+
38+
public function supports(Model $model): bool
39+
{
40+
return $model instanceof Embeddings;
41+
}
42+
43+
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
44+
{
45+
return new RawHttpResult($this->httpClient->request('POST', 'https://openrouter.ai/api/v1/embeddings', [
46+
'auth_bearer' => $this->apiKey,
47+
'json' => [
48+
'model' => $model->getName(),
49+
'input' => $payload,
50+
],
51+
]));
52+
}
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\OpenRouter\Embeddings;
13+
14+
use Symfony\AI\Platform\Bridge\OpenRouter\Embeddings;
15+
use Symfony\AI\Platform\Exception\AuthenticationException;
16+
use Symfony\AI\Platform\Exception\BadRequestException;
17+
use Symfony\AI\Platform\Exception\RateLimitExceededException;
18+
use Symfony\AI\Platform\Exception\RuntimeException;
19+
use Symfony\AI\Platform\Model;
20+
use Symfony\AI\Platform\Result\RawResultInterface;
21+
use Symfony\AI\Platform\Result\VectorResult;
22+
use Symfony\AI\Platform\ResultConverterInterface;
23+
use Symfony\AI\Platform\Vector\Vector;
24+
25+
/**
26+
* @author Tim Lochmüller <tim@fruit-lab.de>
27+
*/
28+
final class ResultConverter implements ResultConverterInterface
29+
{
30+
public function supports(Model $model): bool
31+
{
32+
return $model instanceof Embeddings;
33+
}
34+
35+
public function convert(RawResultInterface $result, array $options = []): VectorResult
36+
{
37+
$response = $result->getObject();
38+
$data = $result->getData();
39+
40+
if (401 === $response->getStatusCode()) {
41+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'];
42+
throw new AuthenticationException($errorMessage);
43+
}
44+
45+
if (400 === $response->getStatusCode() || 404 === $response->getStatusCode()) {
46+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'] ?? 'Bad Request';
47+
throw new BadRequestException($errorMessage);
48+
}
49+
50+
if (429 === $response->getStatusCode()) {
51+
$errorMessage = json_decode($response->getContent(false), true)['error']['message'] ?? 'Bad Request';
52+
throw new RateLimitExceededException($errorMessage);
53+
}
54+
55+
if (!isset($data['data'][0]['embedding'])) {
56+
throw new RuntimeException('Response does not contain data.');
57+
}
58+
59+
return new VectorResult(
60+
...array_map(
61+
static fn (array $item): Vector => new Vector($item['embedding']),
62+
$data['data'],
63+
),
64+
);
65+
}
66+
}

0 commit comments

Comments
 (0)