Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/platform/src/ModelCatalog/AbstractModelCatalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ public function getModel(string $modelName): Model
$parsed = self::parseModelName($modelName);
$actualModelName = $parsed['name'];
$options = $parsed['options'];
$modelBaseName = $parsed['baseName'];

if (!isset($this->models[$actualModelName])) {
if (!isset($this->models[$actualModelName]) && !isset($this->models[$modelBaseName])) {
throw new ModelNotFoundException(\sprintf('Model "%s" not found.', $actualModelName));
}

$modelConfig = $this->models[$actualModelName];
$modelConfig = $this->models[$modelName] ?? $this->models[$modelBaseName];
$modelClass = $modelConfig['class'];

if (!class_exists($modelClass)) {
Expand All @@ -64,11 +65,11 @@ public function getModels(): array
}

/**
* Extracts model name and options from a model name string that may contain query parameters.
* Extracts model name, base model name (without size suffix) and options from a model name string that may contain query parameters.
*
* @param string $modelName The model name, potentially with query parameters (e.g., "model-name?param=value&other=123")
* @param string $modelName The model name, potentially with size suffix and query parameters (e.g., "model-name:32b?param=value&other=123")
*
* @return array{name: string, options: array<string, mixed>} An array containing the model name and parsed options
* @return array{name: string, baseName: string, options: array<string, mixed>} An array containing the model name and parsed options
*/
protected static function parseModelName(string $modelName): array
{
Expand All @@ -89,6 +90,7 @@ protected static function parseModelName(string $modelName): array

return [
'name' => $actualModelName,
'baseName' => explode(':', $actualModelName, 2)[0],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the basename?

Copy link
Contributor Author

@sonnymilton sonnymilton Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the basename fallback to avoid duplicating identical model registrations for every size suffix (e.g. qwen3:32b, qwen3:64m).
Almost every model supported by OLLAMA contains variants with size suffix. For example, the qwen3 model supports over 50 different variants. https://ollama.com/library/qwen3/tags
Registering each one of them would lead to a huge increase of the catalog

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but what are you doing with the baseName in user land?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t expose or use the baseName in user land.
It’s only an internal fallback inside AbstractModelCatalog::getModel().
From the perspective of user land, they always pass the full name (e.g. qwen3:32b) and get back a model with that exact name. The baseName is just used internally to resolve the model configuration when the variant isn’t explicitly registered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'baseName' => explode(':', $actualModelName, 2)[0],

so we don't need it here, right? we will end up with model name qwen3:32b which is fine

Copy link
Contributor Author

@sonnymilton sonnymilton Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since :size suffixes are not a cross-vendor pattern (only use Ollama), it might be more appropriate to keep the fallback logic in the Symfony\AI\Platform\Bridge\Ollama\ModelCatalog::getModel() (like in the first version of this PR 853d902).

'options' => $options,
];
}
Expand Down
2 changes: 2 additions & 0 deletions src/platform/tests/Bridge/Ollama/ModelCatalogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static function modelsProvider(): iterable
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]];
Expand All @@ -45,6 +46,7 @@ public static function modelsProvider(): iterable
yield 'nomic-embed-text' => ['nomic-embed-text', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]];
yield 'bge-m3' => ['bge-m3', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]];
yield 'all-minilm' => ['all-minilm', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]];
yield 'all-minilm:33m' => ['all-minilm:33m', Ollama::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED, Capability::INPUT_MULTIPLE]];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for this model

}

protected function createModelCatalog(): ModelCatalogInterface
Expand Down
26 changes: 26 additions & 0 deletions src/platform/tests/ModelCatalog/AbstractModelCatalogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ public function testNumericStringsAreConvertedRecursively()
$this->assertIsInt($options['a']['e']);
}

public function testGetModelWithSizeSuffixResolvesToBaseModel()
{
$catalog = $this->createTestCatalog();
$model = $catalog->getModel('test-model:32b');

$this->assertSame('test-model:32b', $model->getName());
$this->assertSame([], $model->getOptions());
}

public function testGetModelWithSizeSuffixAndQueryParameters()
{
$catalog = $this->createTestCatalog();
$model = $catalog->getModel('test-model:64m?max_tokens=100&temperature=0.5');

$this->assertSame('test-model:64m', $model->getName());

$options = $model->getOptions();
$this->assertArrayHasKey('max_tokens', $options);
$this->assertIsInt($options['max_tokens']);
$this->assertSame(100, $options['max_tokens']);

$this->assertArrayHasKey('temperature', $options);
$this->assertIsFloat($options['temperature']);
$this->assertSame(0.5, $options['temperature']);
}

private function createTestCatalog(): AbstractModelCatalog
{
return new class extends AbstractModelCatalog {
Expand Down