diff --git a/UPGRADE.md b/UPGRADE.md index de0b83fa..7f6f4692 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,6 +1,20 @@ UPGRADE FROM 0.6 to 0.7 ======================= +AI Bundle +--------- + + * The `api_catalog` option for `Ollama` has been removed as the catalog is now automatically fetched from the Ollama server + * The `api_key` option for `Ollama` is now `null` by default to allow the usage of a `ScopingHttpClient` + * The `endpoint` option for `Ollama` is now `null` by default to allow the usage of a `ScopingHttpClient` + +Platform +----- + + * `ModelCatalog` in `Ollama` has been replaced by `OllamaApiCatalog` + * `OllamaApiCatalog` in `Ollama` has been replaced to `ModelCatalog` + * `Ollama` model is now `final` + Store ----- diff --git a/examples/ollama/chat-llama-with-token-usage.php b/examples/ollama/chat-llama-with-token-usage.php index 3f267306..1107fc39 100644 --- a/examples/ollama/chat-llama-with-token-usage.php +++ b/examples/ollama/chat-llama-with-token-usage.php @@ -15,7 +15,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); try { $result = $platform->invoke(env('OLLAMA_LLM'), new MessageBag( diff --git a/examples/ollama/chat-llama.php b/examples/ollama/chat-llama.php index d1dd67d3..ea95d816 100644 --- a/examples/ollama/chat-llama.php +++ b/examples/ollama/chat-llama.php @@ -15,7 +15,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $messages = new MessageBag( Message::forSystem('You are a helpful assistant.'), diff --git a/examples/ollama/embeddings.php b/examples/ollama/embeddings.php index a4a6deed..c26283f6 100644 --- a/examples/ollama/embeddings.php +++ b/examples/ollama/embeddings.php @@ -13,7 +13,7 @@ use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $response = $platform->invoke(env('OLLAMA_EMBEDDINGS'), << $movie) { } // create embeddings for documents -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $vectorizer = new Vectorizer($platform, env('OLLAMA_EMBEDDINGS'), logger()); $indexer = new DocumentIndexer(new DocumentProcessor($vectorizer, $store, logger: logger())); $indexer->index($documents); diff --git a/examples/ollama/stream-toolcall.php b/examples/ollama/stream-toolcall.php index 8d85303b..31a62194 100644 --- a/examples/ollama/stream-toolcall.php +++ b/examples/ollama/stream-toolcall.php @@ -19,7 +19,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $toolbox = new Toolbox([new Clock()], logger: logger()); $processor = new AgentProcessor($toolbox); diff --git a/examples/ollama/stream-with-token-usage.php b/examples/ollama/stream-with-token-usage.php index 80d36b3a..a437a8e9 100644 --- a/examples/ollama/stream-with-token-usage.php +++ b/examples/ollama/stream-with-token-usage.php @@ -15,7 +15,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $messages = new MessageBag( Message::forSystem('You are a helpful assistant.'), diff --git a/examples/ollama/stream.php b/examples/ollama/stream.php index 5fd4a91b..9b1d361b 100644 --- a/examples/ollama/stream.php +++ b/examples/ollama/stream.php @@ -15,7 +15,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $messages = new MessageBag( Message::forSystem('You are a helpful assistant.'), diff --git a/examples/ollama/structured-output-math.php b/examples/ollama/structured-output-math.php index 76b3b231..655a95ad 100644 --- a/examples/ollama/structured-output-math.php +++ b/examples/ollama/structured-output-math.php @@ -21,7 +21,7 @@ require_once dirname(__DIR__).'/bootstrap.php'; $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new PlatformSubscriber()); -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client(), eventDispatcher: $dispatcher); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client(), eventDispatcher: $dispatcher); $messages = new MessageBag( Message::forSystem('You are a helpful math tutor. Guide the user through the solution step by step.'), Message::ofUser('how can I solve 8x + 7 = -23'), diff --git a/examples/ollama/toolcall.php b/examples/ollama/toolcall.php index 96e57bc7..b355464d 100644 --- a/examples/ollama/toolcall.php +++ b/examples/ollama/toolcall.php @@ -19,7 +19,7 @@ use Symfony\AI\Platform\Message\MessageBag; require_once dirname(__DIR__).'/bootstrap.php'; -$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), httpClient: http_client()); +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), env('OLLAMA_API_KEY'), httpClient: http_client()); $toolbox = new Toolbox([new Clock()], logger: logger()); $processor = new AgentProcessor($toolbox); diff --git a/src/ai-bundle/CHANGELOG.md b/src/ai-bundle/CHANGELOG.md index e458d54a..4449e0db 100644 --- a/src/ai-bundle/CHANGELOG.md +++ b/src/ai-bundle/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.7 +--- + + * The `api_catalog` option for `Ollama` has been removed as the catalog is now automatically fetched from the Ollama server + * The `api_key` option for `Ollama` is now `null` by default to allow the usage of a `ScopingHttpClient` + * The `endpoint` option for `Ollama` is now `null` by default to allow the usage of a `ScopingHttpClient` + 0.6 --- diff --git a/src/ai-bundle/config/platform/ollama.php b/src/ai-bundle/config/platform/ollama.php index 4586f7ad..7f0277f8 100644 --- a/src/ai-bundle/config/platform/ollama.php +++ b/src/ai-bundle/config/platform/ollama.php @@ -16,18 +16,13 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; return (new ArrayNodeDefinition('ollama')) ->children() ->stringNode('endpoint') - ->defaultNull() ->info('Endpoint for Ollama (e.g. "http://127.0.0.1:11434" for local, or a cloud endpoint). If null, the http_client is used as-is and must already be configured with a base URI.') ->end() ->stringNode('api_key') - ->defaultNull() ->info('API key for Ollama Cloud authentication (optional for local usage)') ->end() ->stringNode('http_client') ->defaultValue('http_client') ->info('Service ID of the HTTP client to use. When "endpoint" is null, this client must be pre-configured (e.g. with a base_uri).') ->end() - ->booleanNode('api_catalog') - ->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') - ->end() ->end(); diff --git a/src/ai-bundle/config/services.php b/src/ai-bundle/config/services.php index 0f876eb6..62b9d17e 100644 --- a/src/ai-bundle/config/services.php +++ b/src/ai-bundle/config/services.php @@ -46,7 +46,6 @@ 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\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\OpenRouter\ModelCatalog as OpenRouterModelCatalog; @@ -122,9 +121,6 @@ return static function (ContainerConfigurator $container): void { ->set('ai.platform.model_catalog.lmstudio', LmStudioModelCatalog::class) ->set('ai.platform.model_catalog.meta', MetaModelCatalog::class) ->set('ai.platform.model_catalog.mistral', MistralModelCatalog::class) - ->set('ai.platform.model_catalog.ollama', OllamaModelCatalog::class) - ->lazy() - ->tag('proxy', ['interface' => ModelCatalogInterface::class]) ->set('ai.platform.model_catalog.openai', OpenAiModelCatalog::class) ->set('ai.platform.model_catalog.openrouter', OpenRouterModelCatalog::class) ->set('ai.platform.model_catalog.ovh', OvhModelCatalog::class) diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index a2502934..4d3960a2 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -68,7 +68,7 @@ use Symfony\AI\Platform\Bridge\Generic\PlatformFactory as GenericPlatformFactory use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory as HuggingFacePlatformFactory; use Symfony\AI\Platform\Bridge\LmStudio\PlatformFactory as LmStudioPlatformFactory; use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory; -use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog; +use Symfony\AI\Platform\Bridge\Ollama\ModelCatalog; use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory; use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory as OpenAiPlatformFactory; use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory; @@ -81,7 +81,6 @@ use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory as VoyagePlatformFactory; use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Message\Content\File; -use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Platform; use Symfony\AI\Platform\PlatformInterface; @@ -137,7 +136,6 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpClient\HttpClient; -use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Translation\TranslatableMessage; @@ -849,47 +847,13 @@ final class AiBundle extends AbstractBundle throw new RuntimeException('Ollama platform configuration requires "symfony/ai-ollama-platform" package. Try running "composer require symfony/ai-ollama-platform".'); } - $httpClientReference = new Reference($platform['http_client']); - - if (null !== $platform['endpoint']) { - $defaultOptions = []; - if (null !== ($platform['api_key'] ?? null)) { - $defaultOptions['auth_bearer'] = $platform['api_key']; - } - - $scopedClientDefinition = (new Definition(ScopingHttpClient::class)) - ->setFactory([ScopingHttpClient::class, 'forBaseUri']) - ->setArguments([ - $httpClientReference, - $platform['endpoint'], - $defaultOptions, - ]); - - $container->setDefinition('ai.platform.ollama.scoped_http_client', $scopedClientDefinition); - - $httpClientReference = new Reference('ai.platform.ollama.scoped_http_client'); - } - - if (\array_key_exists('api_catalog', $platform)) { - $catalogDefinition = (new Definition(OllamaApiCatalog::class)) - ->setLazy(true) - ->setArguments([ - $httpClientReference, - ]) - ->addTag('proxy', ['interface' => ModelCatalogInterface::class]) - ; - - $container->setDefinition('ai.platform.model_catalog.ollama', $catalogDefinition); - } - $definition = (new Definition(Platform::class)) ->setFactory(OllamaPlatformFactory::class.'::create') ->setLazy(true) ->setArguments([ - $platform['endpoint'], + $platform['endpoint'] ?? null, $platform['api_key'] ?? null, - $httpClientReference, - new Reference('ai.platform.model_catalog.ollama'), + new Reference($platform['http_client']), new Reference('ai.platform.contract.ollama'), new Reference('event_dispatcher'), ]) diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 011a971d..9950d510 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -38,8 +38,6 @@ use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory; use Symfony\AI\Platform\Bridge\Failover\FailoverPlatform; use Symfony\AI\Platform\Bridge\Failover\FailoverPlatformFactory; -use Symfony\AI\Platform\Bridge\Ollama\ModelCatalog; -use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog; use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory; use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\EventListener\TemplateRendererListener; @@ -3912,35 +3910,24 @@ class AiBundleTest extends TestCase ]); $this->assertTrue($container->hasDefinition('ai.platform.ollama')); - $this->assertTrue($container->hasDefinition('ai.platform.ollama.scoped_http_client')); - $this->assertTrue($container->hasDefinition('ai.platform.model_catalog.ollama')); $definition = $container->getDefinition('ai.platform.ollama'); $this->assertSame([OllamaPlatformFactory::class, 'create'], $definition->getFactory()); $this->assertTrue($definition->isLazy()); - $this->assertCount(6, $definition->getArguments()); + $this->assertCount(5, $definition->getArguments()); $this->assertSame('http://127.0.0.1:11434', $definition->getArgument(0)); $this->assertNull($definition->getArgument(1)); $this->assertInstanceOf(Reference::class, $definition->getArgument(2)); - $this->assertSame('ai.platform.ollama.scoped_http_client', (string) $definition->getArgument(2)); + $this->assertSame('http_client', (string) $definition->getArgument(2)); $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); - $this->assertSame('ai.platform.model_catalog.ollama', (string) $definition->getArgument(3)); + $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(3)); $this->assertInstanceOf(Reference::class, $definition->getArgument(4)); - $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(4)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); - $this->assertSame('event_dispatcher', (string) $definition->getArgument(5)); + $this->assertSame('event_dispatcher', (string) $definition->getArgument(4)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); - $catalogDefinition = $container->getDefinition('ai.platform.model_catalog.ollama'); - $this->assertTrue($catalogDefinition->isLazy()); - $this->assertSame(ModelCatalog::class, $catalogDefinition->getClass()); - - $this->assertTrue($catalogDefinition->hasTag('proxy')); - $this->assertSame([['interface' => ModelCatalogInterface::class]], $catalogDefinition->getTag('proxy')); - // Ollama.com usage (with API key) $container = $this->buildContainer([ 'ai' => [ @@ -3954,35 +3941,24 @@ class AiBundleTest extends TestCase ]); $this->assertTrue($container->hasDefinition('ai.platform.ollama')); - $this->assertTrue($container->hasDefinition('ai.platform.ollama.scoped_http_client')); - $this->assertTrue($container->hasDefinition('ai.platform.model_catalog.ollama')); $definition = $container->getDefinition('ai.platform.ollama'); $this->assertSame([OllamaPlatformFactory::class, 'create'], $definition->getFactory()); $this->assertTrue($definition->isLazy()); - $this->assertCount(6, $definition->getArguments()); + $this->assertCount(5, $definition->getArguments()); $this->assertSame('https://ollama.com', $definition->getArgument(0)); $this->assertSame('foo', $definition->getArgument(1)); $this->assertInstanceOf(Reference::class, $definition->getArgument(2)); - $this->assertSame('ai.platform.ollama.scoped_http_client', (string) $definition->getArgument(2)); + $this->assertSame('http_client', (string) $definition->getArgument(2)); $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); - $this->assertSame('ai.platform.model_catalog.ollama', (string) $definition->getArgument(3)); + $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(3)); $this->assertInstanceOf(Reference::class, $definition->getArgument(4)); - $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(4)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); - $this->assertSame('event_dispatcher', (string) $definition->getArgument(5)); + $this->assertSame('event_dispatcher', (string) $definition->getArgument(4)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); - $catalogDefinition = $container->getDefinition('ai.platform.model_catalog.ollama'); - $this->assertTrue($catalogDefinition->isLazy()); - $this->assertSame(ModelCatalog::class, $catalogDefinition->getClass()); - - $this->assertTrue($catalogDefinition->hasTag('proxy')); - $this->assertSame([['interface' => ModelCatalogInterface::class]], $catalogDefinition->getTag('proxy')); - // Custom HTTPClient $container = $this->buildContainer([ 'ai' => [ @@ -3995,82 +3971,23 @@ class AiBundleTest extends TestCase ]); $this->assertTrue($container->hasDefinition('ai.platform.ollama')); - $this->assertFalse($container->hasDefinition('ai.platform.ollama.scoped_http_client')); - $this->assertTrue($container->hasDefinition('ai.platform.model_catalog.ollama')); $definition = $container->getDefinition('ai.platform.ollama'); $this->assertSame([OllamaPlatformFactory::class, 'create'], $definition->getFactory()); $this->assertTrue($definition->isLazy()); - $this->assertCount(6, $definition->getArguments()); + $this->assertCount(5, $definition->getArguments()); $this->assertNull($definition->getArgument(0)); $this->assertNull($definition->getArgument(1)); $this->assertInstanceOf(Reference::class, $definition->getArgument(2)); $this->assertSame('foo', (string) $definition->getArgument(2)); $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); - $this->assertSame('ai.platform.model_catalog.ollama', (string) $definition->getArgument(3)); + $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(3)); $this->assertInstanceOf(Reference::class, $definition->getArgument(4)); - $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(4)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); - $this->assertSame('event_dispatcher', (string) $definition->getArgument(5)); + $this->assertSame('event_dispatcher', (string) $definition->getArgument(4)); $this->assertTrue($definition->hasTag('proxy')); $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); - - $catalogDefinition = $container->getDefinition('ai.platform.model_catalog.ollama'); - $this->assertTrue($catalogDefinition->isLazy()); - $this->assertSame(ModelCatalog::class, $catalogDefinition->getClass()); - - $this->assertTrue($catalogDefinition->hasTag('proxy')); - $this->assertSame([['interface' => ModelCatalogInterface::class]], $catalogDefinition->getTag('proxy')); - } - - public function testOllamaCanBeCreatedWithCatalogFromApi() - { - $container = $this->buildContainer([ - 'ai' => [ - 'platform' => [ - 'ollama' => [ - 'endpoint' => 'http://127.0.0.1:11434', - 'api_catalog' => true, - ], - ], - ], - ]); - - $this->assertTrue($container->hasDefinition('ai.platform.ollama')); - $this->assertTrue($container->hasDefinition('ai.platform.ollama.scoped_http_client')); - $this->assertTrue($container->hasDefinition('ai.platform.model_catalog.ollama')); - - $definition = $container->getDefinition('ai.platform.ollama'); - $this->assertSame([OllamaPlatformFactory::class, 'create'], $definition->getFactory()); - $this->assertTrue($definition->isLazy()); - - $this->assertCount(6, $definition->getArguments()); - $this->assertSame('http://127.0.0.1:11434', $definition->getArgument(0)); - $this->assertNull($definition->getArgument(1)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(2)); - $this->assertSame('ai.platform.ollama.scoped_http_client', (string) $definition->getArgument(2)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); - $this->assertSame('ai.platform.model_catalog.ollama', (string) $definition->getArgument(3)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(4)); - $this->assertSame('ai.platform.contract.ollama', (string) $definition->getArgument(4)); - $this->assertInstanceOf(Reference::class, $definition->getArgument(5)); - $this->assertSame('event_dispatcher', (string) $definition->getArgument(5)); - - $this->assertTrue($definition->hasTag('proxy')); - $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); - - $catalogDefinition = $container->getDefinition('ai.platform.model_catalog.ollama'); - - $this->assertTrue($catalogDefinition->isLazy()); - $this->assertSame(OllamaApiCatalog::class, $catalogDefinition->getClass()); - $this->assertCount(1, $catalogDefinition->getArguments()); - $this->assertInstanceOf(Reference::class, $catalogDefinition->getArgument(0)); - $this->assertSame('ai.platform.ollama.scoped_http_client', (string) $catalogDefinition->getArgument(0)); - - $this->assertTrue($catalogDefinition->hasTag('proxy')); - $this->assertSame([['interface' => ModelCatalogInterface::class]], $catalogDefinition->getTag('proxy')); } /** diff --git a/src/platform/src/Bridge/Ollama/CHANGELOG.md b/src/platform/src/Bridge/Ollama/CHANGELOG.md index 13154f69..5947b934 100644 --- a/src/platform/src/Bridge/Ollama/CHANGELOG.md +++ b/src/platform/src/Bridge/Ollama/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG --- * Add support for `structured_output` capability in `OllamaApiCatalog` + * Replace `ModelCatalog` by `OllamaApiCatalog` + * Rename `OllamaApiCatalog` to `ModelCatalog` + * [BC BREAK] `Ollama` model is now `final` 0.4 --- diff --git a/src/platform/src/Bridge/Ollama/ModelCatalog.php b/src/platform/src/Bridge/Ollama/ModelCatalog.php index 80c0bb22..9e9c266c 100644 --- a/src/platform/src/Bridge/Ollama/ModelCatalog.php +++ b/src/platform/src/Bridge/Ollama/ModelCatalog.php @@ -12,212 +12,72 @@ namespace Symfony\AI\Platform\Bridge\Ollama; use Symfony\AI\Platform\Capability; -use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelCatalog\AbstractModelCatalog; +use Symfony\AI\Platform\Exception\InvalidArgumentException; +use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Oskar Stark + * @author Guillaume Loulier */ -final class ModelCatalog extends AbstractModelCatalog +final class ModelCatalog implements ModelCatalogInterface { - /** - * @param array, capabilities: list}> $additionalModels - */ - public function __construct(array $additionalModels = []) - { - $defaultModels = [ - 'deepseek-r1' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'gpt-oss' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'llama3.1' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'llama3.2' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'llama3' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'mistral' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'qwen3' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'qwen' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'qwen2' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'qwen2.5' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'qwen2.5-coder' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - Capability::TOOL_CALLING, - ], - ], - 'gemma3n' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'gemma3' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'qwen2.5vl' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'llava' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'phi3' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'gemma2' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'gemma' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'llama2' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_MESSAGES, - Capability::OUTPUT_TEXT, - Capability::OUTPUT_STRUCTURED, - ], - ], - 'nomic-embed-text' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_TEXT, - Capability::INPUT_MULTIPLE, - Capability::EMBEDDINGS, - ], - ], - 'bge-m3' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_TEXT, - Capability::INPUT_MULTIPLE, - Capability::EMBEDDINGS, - ], - ], - 'all-minilm' => [ - 'class' => Ollama::class, - 'capabilities' => [ - Capability::INPUT_TEXT, - Capability::INPUT_MULTIPLE, - Capability::EMBEDDINGS, - ], - ], - ]; + public function __construct( + private readonly HttpClientInterface $httpClient, + ) { + } - $this->models = [ - ...$defaultModels, - ...$additionalModels, - ]; + public function getModel(string $modelName): Ollama + { + $response = $this->httpClient->request('POST', '/api/show', [ + 'json' => [ + 'model' => $modelName, + ], + ]); + + $payload = $response->toArray(); + + if ([] === $payload['capabilities']) { + throw new InvalidArgumentException('The model information could not be retrieved from the Ollama API. Your Ollama server might be too old. Try upgrade it.'); + } + + $capabilities = array_map( + static fn (string $capability): Capability => match ($capability) { + 'embedding' => Capability::EMBEDDINGS, + 'completion' => Capability::INPUT_MESSAGES, + 'tools' => Capability::TOOL_CALLING, + 'thinking' => Capability::THINKING, + 'vision' => Capability::INPUT_IMAGE, + default => throw new InvalidArgumentException(\sprintf('The "%s" capability is not supported', $capability)), + }, + $payload['capabilities'], + ); + + if (!\in_array(Capability::EMBEDDINGS, $capabilities, true)) { + $capabilities[] = Capability::OUTPUT_STRUCTURED; + } + + return new Ollama($modelName, $capabilities); + } + + public function getModels(): array + { + $response = $this->httpClient->request('GET', '/api/tags'); + + $models = $response->toArray(); + + return array_merge(...array_map( + function (array $model): array { + $retrievedModel = $this->getModel($model['name']); + + return [ + $retrievedModel->getName() => [ + 'class' => Ollama::class, + 'capabilities' => $retrievedModel->getCapabilities(), + ], + ]; + }, + $models['models'], + )); } } diff --git a/src/platform/src/Bridge/Ollama/Ollama.php b/src/platform/src/Bridge/Ollama/Ollama.php index 460bc514..883aad68 100644 --- a/src/platform/src/Bridge/Ollama/Ollama.php +++ b/src/platform/src/Bridge/Ollama/Ollama.php @@ -16,6 +16,6 @@ use Symfony\AI\Platform\Model; /** * @author Joshua Behrens */ -class Ollama extends Model +final class Ollama extends Model { } diff --git a/src/platform/src/Bridge/Ollama/OllamaApiCatalog.php b/src/platform/src/Bridge/Ollama/OllamaApiCatalog.php deleted file mode 100644 index 20a970bc..00000000 --- a/src/platform/src/Bridge/Ollama/OllamaApiCatalog.php +++ /dev/null @@ -1,82 +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\Ollama; - -use Symfony\AI\Platform\Capability; -use Symfony\AI\Platform\Exception\InvalidArgumentException; -use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -/** - * @author Guillaume Loulier - */ -final class OllamaApiCatalog implements ModelCatalogInterface -{ - public function __construct( - private readonly HttpClientInterface $httpClient, - ) { - } - - public function getModel(string $modelName): Ollama - { - $response = $this->httpClient->request('POST', '/api/show', [ - 'json' => [ - 'model' => $modelName, - ], - ]); - - $payload = $response->toArray(); - - if ([] === $payload['capabilities']) { - throw new InvalidArgumentException('The model information could not be retrieved from the Ollama API. Your Ollama server might be too old. Try upgrade it.'); - } - - $capabilities = array_map( - static fn (string $capability): Capability => match ($capability) { - 'embedding' => Capability::EMBEDDINGS, - 'completion' => Capability::INPUT_MESSAGES, - 'tools' => Capability::TOOL_CALLING, - 'thinking' => Capability::THINKING, - 'vision' => Capability::INPUT_IMAGE, - default => throw new InvalidArgumentException(\sprintf('The "%s" capability is not supported', $capability)), - }, - $payload['capabilities'], - ); - - if (!\in_array(Capability::EMBEDDINGS, $capabilities, true)) { - $capabilities[] = Capability::OUTPUT_STRUCTURED; - } - - return new Ollama($modelName, $capabilities); - } - - public function getModels(): array - { - $response = $this->httpClient->request('GET', '/api/tags'); - - $models = $response->toArray(); - - return array_merge(...array_map( - function (array $model): array { - $retrievedModel = $this->getModel($model['name']); - - return [ - $retrievedModel->getName() => [ - 'class' => Ollama::class, - 'capabilities' => $retrievedModel->getCapabilities(), - ], - ]; - }, - $models['models'], - )); - } -} diff --git a/src/platform/src/Bridge/Ollama/PlatformFactory.php b/src/platform/src/Bridge/Ollama/PlatformFactory.php index 6d0a409a..05e0b67a 100644 --- a/src/platform/src/Bridge/Ollama/PlatformFactory.php +++ b/src/platform/src/Bridge/Ollama/PlatformFactory.php @@ -13,7 +13,6 @@ namespace Symfony\AI\Platform\Bridge\Ollama; use Symfony\AI\Platform\Bridge\Ollama\Contract\OllamaContract; use Symfony\AI\Platform\Contract; -use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; use Symfony\AI\Platform\Platform; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; @@ -26,10 +25,9 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; final class PlatformFactory { public static function create( - ?string $endpoint = 'http://localhost:11434', + ?string $endpoint = null, #[\SensitiveParameter] ?string $apiKey = null, ?HttpClientInterface $httpClient = null, - ModelCatalogInterface $modelCatalog = new ModelCatalog(), ?Contract $contract = null, ?EventDispatcherInterface $eventDispatcher = null, ): Platform { @@ -47,7 +45,7 @@ final class PlatformFactory return new Platform( [new OllamaClient($httpClient)], [new OllamaResultConverter()], - $modelCatalog, + new ModelCatalog($httpClient), $contract ?? OllamaContract::create(), $eventDispatcher, ); diff --git a/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php b/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php deleted file mode 100644 index 58d6cb7b..00000000 --- a/src/platform/src/Bridge/Ollama/Tests/ModelCatalogTest.php +++ /dev/null @@ -1,57 +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\Ollama\Tests; - -use Symfony\AI\Platform\Bridge\Ollama\ModelCatalog; -use Symfony\AI\Platform\Bridge\Ollama\Ollama; -use Symfony\AI\Platform\Capability; -use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; -use Symfony\AI\Platform\Test\ModelCatalogTestCase; - -/** - * @author Oskar Stark - */ -final class ModelCatalogTest extends ModelCatalogTestCase -{ - public static function modelsProvider(): iterable - { - yield 'deepseek-r1' => ['deepseek-r1', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'gpt-oss' => ['gpt-oss', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'llama3.1' => ['llama3.1', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'llama3.2' => ['llama3.2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'llama3' => ['llama3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'mistral' => ['mistral', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen3' => ['qwen3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen3:32b' => ['qwen3:32b', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen' => ['qwen', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen2' => ['qwen2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen2.5' => ['qwen2.5', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'qwen2.5-coder' => ['qwen2.5-coder', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::TOOL_CALLING]]; - yield 'gemma3n' => ['gemma3n', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'gemma3' => ['gemma3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'qwen2.5vl' => ['qwen2.5vl', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'llava' => ['llava', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'phi3' => ['phi3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'gemma2' => ['gemma2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'gemma' => ['gemma', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'llama2' => ['llama2', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED]]; - yield 'nomic-embed-text' => ['nomic-embed-text', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; - yield 'bge-m3' => ['bge-m3', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; - yield 'all-minilm' => ['all-minilm', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; - yield 'all-minilm:33m' => ['all-minilm:33m', Ollama::class, [Capability::INPUT_TEXT, Capability::EMBEDDINGS, Capability::INPUT_MULTIPLE]]; - } - - protected function createModelCatalog(): ModelCatalogInterface - { - return new ModelCatalog(); - } -} diff --git a/src/platform/src/Bridge/Ollama/Tests/OllamaApiCatalogTest.php b/src/platform/src/Bridge/Ollama/Tests/OllamaApiCatalogTest.php index 8e59152d..082a4cd9 100644 --- a/src/platform/src/Bridge/Ollama/Tests/OllamaApiCatalogTest.php +++ b/src/platform/src/Bridge/Ollama/Tests/OllamaApiCatalogTest.php @@ -12,8 +12,8 @@ namespace Symfony\AI\Platform\Bridge\Ollama\Tests; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Bridge\Ollama\ModelCatalog; use Symfony\AI\Platform\Bridge\Ollama\Ollama; -use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog; use Symfony\AI\Platform\Capability; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\JsonMockResponse; @@ -28,7 +28,7 @@ final class OllamaApiCatalogTest extends TestCase ]), ], 'http://127.0.0.1:11434'); - $modelCatalog = new OllamaApiCatalog($httpClient); + $modelCatalog = new ModelCatalog($httpClient); $model = $modelCatalog->getModel('foo'); @@ -56,7 +56,7 @@ final class OllamaApiCatalogTest extends TestCase ]), ], 'http://127.0.0.1:11434'); - $modelCatalog = new OllamaApiCatalog($httpClient); + $modelCatalog = new ModelCatalog($httpClient); $models = $modelCatalog->getModels(); @@ -88,7 +88,7 @@ final class OllamaApiCatalogTest extends TestCase ]), ], 'http://127.0.0.1:11434'); - $modelCatalog = new OllamaApiCatalog($httpClient); + $modelCatalog = new ModelCatalog($httpClient); $models = $modelCatalog->getModels(); diff --git a/src/platform/src/Bridge/Ollama/Tests/OllamaClientTest.php b/src/platform/src/Bridge/Ollama/Tests/OllamaClientTest.php index be20b3b6..c794fb16 100644 --- a/src/platform/src/Bridge/Ollama/Tests/OllamaClientTest.php +++ b/src/platform/src/Bridge/Ollama/Tests/OllamaClientTest.php @@ -93,6 +93,9 @@ final class OllamaClientTest extends TestCase public function testStreamingIsSupported() { $httpClient = new MockHttpClient([ + new JsonMockResponse([ + 'capabilities' => ['completion'], + ]), new MockResponse('data: '.json_encode([ 'model' => 'llama3.2', 'created_at' => '2025-08-23T10:00:00Z', @@ -124,7 +127,7 @@ final class OllamaClientTest extends TestCase $this->assertInstanceOf(StreamResult::class, $result); $this->assertInstanceOf(\Generator::class, $result->getContent()); - $this->assertSame(1, $httpClient->getRequestsCount()); + $this->assertSame(2, $httpClient->getRequestsCount()); } public function testStreamingConverterWithDirectResponse()