Skip to content

Commit 7516cc6

Browse files
committed
feature #915 [Platform] Add new OpenRouter models, ModelApiCatalog and openrouter/auto + presets (lochmueller)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Add new OpenRouter models, ModelApiCatalog and openrouter/auto + presets | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Docs? | no | Issues | | License | MIT Features... - Update the static ModelCatalog to the current list of models - New models and embeddings are added (e.g. ChatGPT 5.1, Gemini 3) - Add a ModelApiCatalog to call the OpenRouter API to handle the current list of Models - Add openrouter/auto and presets for ModelCatalog and ModelApiCatalog Commits ------- 435f5f7 [Platform] Add new OpenRouter models, ModelApiCatalog and openrouter/auto + presets
2 parents ba2321e + 435f5f7 commit 7516cc6

File tree

4 files changed

+2899
-363
lines changed

4 files changed

+2899
-363
lines changed

examples/openrouter/chat-gemini.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@
1616
require_once dirname(__DIR__).'/bootstrap.php';
1717

1818
$platform = PlatformFactory::create(env('OPENROUTER_KEY'), http_client());
19-
$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';
2219

2320
$messages = new MessageBag(
2421
Message::forSystem('You are a helpful assistant.'),
2522
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
2623
);
27-
$result = $platform->invoke($model, $messages);
24+
$result = $platform->invoke('google/gemini-2.5-flash-lite', $messages);
2825

2926
echo $result->asText().\PHP_EOL;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Capability;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelCatalog\AbstractModelCatalog;
17+
18+
/**
19+
* Add OpenRouter specific features to the model catalogues.
20+
*
21+
* "openrouter/auto" -> https://openrouter.ai/docs/features/model-routing#auto-router
22+
* "@preset/" -> https://openrouter.ai/docs/features/presets
23+
*
24+
* Modifier are handled by the default parseModelName function
25+
* ":nitro" -> https://openrouter.ai/docs/features/provider-routing#nitro-shortcut
26+
* ":floor" -> https://openrouter.ai/docs/features/provider-routing#floor-price-shortcut
27+
* ":exacto" -> https://openrouter.ai/docs/features/exacto-variant
28+
* ":online" -> https://openrouter.ai/docs/features/web-search
29+
*
30+
* @author Tim Lochmüller <[email protected]>
31+
*/
32+
abstract class AbstractOpenRouterModelCatalog extends AbstractModelCatalog
33+
{
34+
public function __construct()
35+
{
36+
$this->models = [
37+
'openrouter/auto' => [
38+
'class' => Model::class,
39+
'capabilities' => Capability::cases(),
40+
],
41+
'@preset' => [
42+
'class' => Model::class,
43+
'capabilities' => Capability::cases(),
44+
],
45+
];
46+
}
47+
48+
protected function parseModelName(string $modelName): array
49+
{
50+
if (str_starts_with($modelName, '@preset')) {
51+
return [
52+
'name' => $modelName,
53+
'catalogKey' => '@preset',
54+
'options' => [],
55+
];
56+
}
57+
58+
return parent::parseModelName($modelName);
59+
}
60+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Capability;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
19+
/**
20+
* @author Tim Lochmüller <[email protected]>
21+
*/
22+
final class ModelApiCatalog extends AbstractOpenRouterModelCatalog
23+
{
24+
protected bool $modelsAreLoaded = false;
25+
26+
public function __construct(
27+
private readonly HttpClientInterface $httpClient,
28+
) {
29+
parent::__construct();
30+
}
31+
32+
public function getModel(string $modelName): Model
33+
{
34+
$this->preloadRemoteModels();
35+
36+
return parent::getModel($modelName);
37+
}
38+
39+
public function getModels(): array
40+
{
41+
$this->preloadRemoteModels();
42+
43+
return parent::getModels();
44+
}
45+
46+
protected function preloadRemoteModels(): void
47+
{
48+
if (!$this->modelsAreLoaded) {
49+
$this->models = [
50+
...$this->models,
51+
...$this->fetchRemoteModels(),
52+
...$this->fetchRemoteEmbeddings(),
53+
];
54+
$this->modelsAreLoaded = true;
55+
}
56+
}
57+
58+
/**
59+
* @return iterable<string, array{class: class-string<Model>, capabilities: list<Capability::*>}>
60+
*/
61+
protected function fetchRemoteModels(): iterable
62+
{
63+
$responseModels = $this->httpClient->request('GET', 'https://openrouter.ai/api/v1/models');
64+
foreach ($responseModels->toArray()['data'] as $model) {
65+
$capabilities = [];
66+
67+
foreach ($model['architecture']['input_modalities'] as $inputModality) {
68+
switch ($inputModality) {
69+
case 'text':
70+
$capabilities[] = Capability::INPUT_TEXT;
71+
break;
72+
case 'image':
73+
$capabilities[] = Capability::INPUT_IMAGE;
74+
break;
75+
case 'audio':
76+
$capabilities[] = Capability::INPUT_AUDIO;
77+
break;
78+
case 'file':
79+
$capabilities[] = Capability::INPUT_PDF;
80+
break;
81+
case 'video':
82+
$capabilities[] = Capability::INPUT_MULTIMODAL; // Video?
83+
break;
84+
default:
85+
throw new InvalidArgumentException('Unknown model '.$inputModality.' input modality.', 1763717587);
86+
}
87+
}
88+
89+
foreach ($model['architecture']['output_modalities'] as $outputModality) {
90+
switch ($outputModality) {
91+
case 'text':
92+
$capabilities[] = Capability::OUTPUT_TEXT;
93+
break;
94+
case 'image':
95+
$capabilities[] = Capability::OUTPUT_IMAGE;
96+
break;
97+
default:
98+
throw new InvalidArgumentException('Unknown model '.$outputModality.' output modality.', 1763717588);
99+
}
100+
}
101+
102+
yield $model['id'] => [
103+
'class' => Model::class,
104+
'capabilities' => $capabilities,
105+
];
106+
}
107+
}
108+
109+
/**
110+
* @return iterable<string, array{class: class-string<Embeddings>, capabilities: list<Capability::*>}>
111+
*/
112+
protected function fetchRemoteEmbeddings(): iterable
113+
{
114+
$responseEmbeddings = $this->httpClient->request('GET', 'https://openrouter.ai/api/v1/embeddings/models');
115+
foreach ($responseEmbeddings->toArray()['data'] as $embedding) {
116+
yield $embedding['id'] => [
117+
'class' => Embeddings::class,
118+
'capabilities' => [Capability::INPUT_TEXT, Capability::EMBEDDINGS],
119+
];
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)