Skip to content

Commit 61d5c59

Browse files
committed
ref(platform): OllamaCatalog
1 parent f9da9c0 commit 61d5c59

File tree

11 files changed

+257
-27
lines changed

11 files changed

+257
-27
lines changed

docs/components/platform.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ You can also combine size variants with query parameters::
7777
// Get model with size variant and query parameters
7878
$model = $catalog->getModel('qwen3:32b?temperature=0.5&top_p=0.9');
7979

80+
Custom models
81+
~~~~~~~~~~~~~
82+
83+
For providers like Ollama, you can use custom models (built on top of ``Modelfile``), as those models are not listed in
84+
the default catalog, you can use the built-in ``OllamaApiCatalog`` to query the model information from the API rather
85+
than the default catalog::
86+
87+
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
88+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
89+
use Symfony\AI\Platform\Message\Message;
90+
use Symfony\AI\Platform\Message\MessageBag;
91+
92+
$platform = PlatformFactory::create('http://127.0.0.11434', HttpClient::create(), new OllamaApiCatalog(
93+
'http://127.0.0.11434',
94+
HttpClient::create(),
95+
));
96+
97+
$platform->invoke('your_custom_model_name', new MessageBag(
98+
Message::ofUser(...)
99+
));
100+
101+
When using the bundle, the usage of ``OllamaApiCatalog`` is available via the ``api_catalog`` option::
102+
103+
ai:
104+
platform:
105+
ollama:
106+
api_catalog: true
107+
80108
Supported Models & Platforms
81109
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82110

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
13+
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory;
14+
use Symfony\AI\Platform\Message\Message;
15+
use Symfony\AI\Platform\Message\MessageBag;
16+
17+
require_once dirname(__DIR__).'/bootstrap.php';
18+
19+
$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client(), new OllamaApiCatalog(
20+
env('OLLAMA_HOST_URL'),
21+
http_client(),
22+
));
23+
24+
$messages = new MessageBag(
25+
Message::forSystem('You are a helpful assistant.'),
26+
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
27+
);
28+
29+
try {
30+
$result = $platform->invoke(env('OLLAMA_LLM'), $messages);
31+
echo $result->asText().\PHP_EOL;
32+
} catch (InvalidArgumentException $e) {
33+
echo $e->getMessage()."\nMaybe use a different model?\n";
34+
}

src/ai-bundle/config/options.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@
160160
->defaultValue('http_client')
161161
->info('Service ID of the HTTP client to use')
162162
->end()
163+
->booleanNode('api_catalog')
164+
->info('If set, the Ollama API will be used to build the catalog and retrieve models information, using this option leads to additional HTTP calls')
165+
->end()
163166
->end()
164167
->end()
165168
->arrayNode('cerebras')

src/ai-bundle/src/AiBundle.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory as HuggingFacePlatformFactory;
5757
use Symfony\AI\Platform\Bridge\LmStudio\PlatformFactory as LmStudioPlatformFactory;
5858
use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory;
59+
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
5960
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory;
6061
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory as OpenAiPlatformFactory;
6162
use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory;
@@ -566,21 +567,31 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
566567
}
567568

568569
if ('ollama' === $type) {
569-
$platformId = 'ai.platform.ollama';
570+
if (\array_key_exists('api_catalog', $platform)) {
571+
$catalogDefinition = (new Definition(OllamaApiCatalog::class))
572+
->setLazy(true)
573+
->setArguments([
574+
$platform['host_url'],
575+
new Reference('http_client'),
576+
]);
577+
578+
$container->setDefinition('ai.platform.model_catalog.ollama', $catalogDefinition);
579+
}
580+
570581
$definition = (new Definition(Platform::class))
571582
->setFactory(OllamaPlatformFactory::class.'::create')
572583
->setLazy(true)
573-
->addTag('proxy', ['interface' => PlatformInterface::class])
574584
->setArguments([
575585
$platform['host_url'],
576586
new Reference($platform['http_client'], ContainerInterface::NULL_ON_INVALID_REFERENCE),
577587
new Reference('ai.platform.model_catalog.ollama'),
578588
new Reference('ai.platform.contract.ollama'),
579589
new Reference('event_dispatcher'),
580590
])
591+
->addTag('proxy', ['interface' => PlatformInterface::class])
581592
->addTag('ai.platform', ['name' => 'ollama']);
582593

583-
$container->setDefinition($platformId, $definition);
594+
$container->setDefinition('ai.platform.ollama', $definition);
584595

585596
return;
586597
}

src/ai-bundle/tests/DependencyInjection/AiBundleTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\AI\AiBundle\AiBundle;
2424
use Symfony\AI\Chat\ChatInterface;
2525
use Symfony\AI\Chat\MessageStoreInterface;
26+
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
2627
use Symfony\AI\Store\Document\Filter\TextContainsFilter;
2728
use Symfony\AI\Store\Document\Loader\InMemoryLoader;
2829
use Symfony\AI\Store\Document\Transformer\TextTrimTransformer;
@@ -583,6 +584,45 @@ public function testConfigurationWithUseAttributeAsKeyWorksWithoutNormalizeKeys(
583584
$this->assertTrue($container->hasDefinition('ai.store.mongodb.Production_DB-v3'));
584585
}
585586

587+
public function testOllamaCanBeCreatedWithCatalogFromApi()
588+
{
589+
$container = $this->buildContainer([
590+
'ai' => [
591+
'platform' => [
592+
'ollama' => [
593+
'api_catalog' => true,
594+
],
595+
],
596+
],
597+
]);
598+
599+
$this->assertTrue($container->hasDefinition('ai.platform.ollama'));
600+
$this->assertTrue($container->hasDefinition('ai.platform.model_catalog.ollama'));
601+
602+
$ollamaDefinition = $container->getDefinition('ai.platform.ollama');
603+
604+
$this->assertTrue($ollamaDefinition->isLazy());
605+
$this->assertCount(5, $ollamaDefinition->getArguments());
606+
$this->assertSame('http://127.0.0.1:11434', $ollamaDefinition->getArgument(0));
607+
$this->assertInstanceOf(Reference::class, $ollamaDefinition->getArgument(1));
608+
$this->assertSame('http_client', (string) $ollamaDefinition->getArgument(1));
609+
$this->assertInstanceOf(Reference::class, $ollamaDefinition->getArgument(2));
610+
$this->assertSame('ai.platform.model_catalog.ollama', (string) $ollamaDefinition->getArgument(2));
611+
$this->assertInstanceOf(Reference::class, $ollamaDefinition->getArgument(3));
612+
$this->assertSame('ai.platform.contract.ollama', (string) $ollamaDefinition->getArgument(3));
613+
$this->assertInstanceOf(Reference::class, $ollamaDefinition->getArgument(4));
614+
$this->assertSame('event_dispatcher', (string) $ollamaDefinition->getArgument(4));
615+
616+
$ollamaCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.ollama');
617+
618+
$this->assertTrue($ollamaCatalogDefinition->isLazy());
619+
$this->assertSame(OllamaApiCatalog::class, $ollamaCatalogDefinition->getClass());
620+
$this->assertCount(2, $ollamaCatalogDefinition->getArguments());
621+
$this->assertSame('http://127.0.0.1:11434', $ollamaCatalogDefinition->getArgument(0));
622+
$this->assertInstanceOf(Reference::class, $ollamaCatalogDefinition->getArgument(1));
623+
$this->assertSame('http_client', (string) $ollamaCatalogDefinition->getArgument(1));
624+
}
625+
586626
/**
587627
* Tests that processor tags use the full agent ID (ai.agent.my_agent) instead of just the agent name (my_agent).
588628
* This regression test prevents issues where processors would not be correctly associated with their agents.

src/platform/src/Bridge/Ollama/ModelCatalog.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ public function __construct(array $additionalModels = [])
218218
],
219219
];
220220

221-
$this->models = array_merge($defaultModels, $additionalModels);
221+
$this->models = [
222+
...$defaultModels,
223+
...$additionalModels,
224+
];
222225
}
223226
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Ollama;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\ModelCatalog\FallbackModelCatalog;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
19+
/**
20+
* @author Guillaume Loulier <[email protected]>
21+
*/
22+
final class OllamaApiCatalog extends FallbackModelCatalog
23+
{
24+
public function __construct(
25+
private readonly string $host,
26+
private readonly HttpClientInterface $httpClient,
27+
) {
28+
parent::__construct();
29+
}
30+
31+
public function getModel(string $modelName): Ollama
32+
{
33+
$model = parent::getModel($modelName);
34+
35+
if (\array_key_exists($model->getName(), $this->models)) {
36+
$finalModel = $this->models[$model->getName()];
37+
38+
return new $finalModel['class'](
39+
$model->getName(),
40+
$finalModel['capabilities'],
41+
$model->getOptions(),
42+
);
43+
}
44+
45+
$response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->host), [
46+
'json' => [
47+
'model' => $model->getName(),
48+
],
49+
]);
50+
51+
$payload = $response->toArray();
52+
53+
if ([] === $payload['capabilities']) {
54+
throw new InvalidArgumentException('The model information could not be retrieved from the Ollama API. Your Ollama server might be too old. Try upgrade it.');
55+
}
56+
57+
$capabilities = array_map(
58+
static fn (string $capability): Capability => match ($capability) {
59+
'embedding' => Capability::EMBEDDINGS,
60+
'completion' => Capability::INPUT_TEXT,
61+
'tools' => Capability::TOOL_CALLING,
62+
'thinking' => Capability::THINKING,
63+
'vision' => Capability::INPUT_IMAGE,
64+
default => throw new InvalidArgumentException(\sprintf('The "%s" capability is not supported', $capability)),
65+
},
66+
$payload['capabilities'],
67+
);
68+
69+
$finalModel = new Ollama($model->getName(), $capabilities, $model->getOptions());
70+
71+
$this->models[$finalModel->getName()] = [
72+
'class' => Ollama::class,
73+
'capabilities' => $finalModel->getCapabilities(),
74+
];
75+
76+
return $finalModel;
77+
}
78+
}

src/platform/src/Bridge/Ollama/OllamaClient.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\Ollama;
1313

14+
use Symfony\AI\Platform\Capability;
1415
use Symfony\AI\Platform\Exception\InvalidArgumentException;
1516
use Symfony\AI\Platform\Model;
1617
use Symfony\AI\Platform\ModelClientInterface;
@@ -36,21 +37,9 @@ public function supports(Model $model): bool
3637

3738
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
3839
{
39-
$response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->hostUrl), [
40-
'json' => [
41-
'model' => $model->getName(),
42-
],
43-
]);
44-
45-
$capabilities = $response->toArray()['capabilities'] ?? null;
46-
47-
if (null === $capabilities) {
48-
throw new InvalidArgumentException('The model information could not be retrieved from the Ollama API. Your Ollama server might be too old. Try upgrade it.');
49-
}
50-
5140
return match (true) {
52-
\in_array('completion', $capabilities, true) => $this->doCompletionRequest($payload, $options),
53-
\in_array('embedding', $capabilities, true) => $this->doEmbeddingsRequest($model, $payload, $options),
41+
\in_array(Capability::INPUT_MESSAGES, $model->getCapabilities(), true) => $this->doCompletionRequest($payload, $options),
42+
\in_array(Capability::EMBEDDINGS, $model->getCapabilities(), true) => $this->doEmbeddingsRequest($model, $payload, $options),
5443
default => throw new InvalidArgumentException(\sprintf('Unsupported model "%s": "%s".', $model::class, $model->getName())),
5544
};
5645
}

src/platform/src/Capability.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ enum Capability: string
4242
// VOICE
4343
case TEXT_TO_SPEECH = 'text-to-speech';
4444
case SPEECH_TO_TEXT = 'speech-to-text';
45+
46+
// EMBEDDINGS
47+
case EMBEDDINGS = 'embeddings';
48+
49+
// Thinking
50+
case THINKING = 'thinking';
4551
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Tests\Bridge\Ollama;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
16+
use Symfony\AI\Platform\Capability;
17+
use Symfony\Component\HttpClient\MockHttpClient;
18+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
19+
20+
final class OllamaApiCatalogTest extends TestCase
21+
{
22+
public function testModelCatalogCanReturnModelFromApi()
23+
{
24+
$httpClient = new MockHttpClient([
25+
new JsonMockResponse([
26+
'capabilities' => ['completion'],
27+
]),
28+
]);
29+
30+
$modelCatalog = new OllamaApiCatalog('http://127.0.0.1:11434', $httpClient);
31+
32+
$model = $modelCatalog->getModel('foo');
33+
34+
$this->assertSame('foo', $model->getName());
35+
$this->assertSame([
36+
Capability::INPUT_TEXT,
37+
], $model->getCapabilities());
38+
$this->assertSame(1, $httpClient->getRequestsCount());
39+
}
40+
}

0 commit comments

Comments
 (0)