diff --git a/examples/ollama/list-models.php b/examples/ollama/list-models.php new file mode 100644 index 000000000..c6c6a4b3f --- /dev/null +++ b/examples/ollama/list-models.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory; +use Symfony\AI\Platform\Capability; + +require_once dirname(__DIR__).'/bootstrap.php'; + +$platform = PlatformFactory::create(env('OLLAMA_HOST_URL'), http_client()); +$modelDefinitions = $platform->fetchModelDefinitions(); + +echo "Available models:\n"; +foreach ($modelDefinitions as $modelDefinition) { + $modelCapabilities = array_map( + static fn (Capability $capability): string => $capability->value, + $modelDefinition->getCapabilities() + ); + echo sprintf( + " + %s (%s)\n", + $modelDefinition->getName(), + implode(', ', $modelCapabilities) + ); +} diff --git a/src/platform/src/Bridge/Ollama/OllamaClient.php b/src/platform/src/Bridge/Ollama/OllamaClient.php index f1220ed79..396e88e9f 100644 --- a/src/platform/src/Bridge/Ollama/OllamaClient.php +++ b/src/platform/src/Bridge/Ollama/OllamaClient.php @@ -11,17 +11,32 @@ namespace Symfony\AI\Platform\Bridge\Ollama; +use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; +use Symfony\AI\Platform\ModelDefinition; +use Symfony\AI\Platform\ModelDefinitionsAwareInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Christopher Hertel */ -final readonly class OllamaClient implements ModelClientInterface +final readonly class OllamaClient implements ModelClientInterface, ModelDefinitionsAwareInterface { + /** + * @see https://github.com/ollama/ollama/blob/main/types/model/capability.go + */ + private const CAPABILITY_MAP = [ + 'completion' => [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STRUCTURED], + 'tools' => [Capability::TOOL_CALLING], + // 'insert' => [], + 'vision' => [Capability::INPUT_IMAGE], + 'embedding' => [Capability::INPUT_MULTIPLE], + // 'thinking' => [], + ]; + public function __construct( private HttpClientInterface $httpClient, private string $hostUrl, @@ -35,13 +50,7 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { - $response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->hostUrl), [ - 'json' => [ - 'model' => $model->getName(), - ], - ]); - - $capabilities = $response->toArray()['capabilities'] ?? null; + $capabilities = $this->fetchModelDetails($model->getName())['capabilities'] ?? null; if (null === $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.'); @@ -54,6 +63,61 @@ public function request(Model $model, array|string $payload, array $options = [] }; } + /** + * @return array + */ + public function fetchModelDefinitions(): array + { + $response = $this->httpClient->request('GET', \sprintf('%s/api/tags', $this->hostUrl)); + $models = $response->toArray()['models'] ?? null; + + if (null === $models) { + throw new InvalidArgumentException('The model information could not be retrieved from the Ollama API. Your Ollama server might be too old. Try upgrade it.'); + } + + $modelDefinitions = array_map($this->buildModelDefinition(...), $models); + $modelNames = array_map(static fn (ModelDefinition $modelDefinition): string => $modelDefinition->getName(), $modelDefinitions); + + return array_combine($modelNames, $modelDefinitions); + } + + /** + * @return array{details: array, model_info: array, capabilities: list} + */ + private function fetchModelDetails(string $model): array + { + $response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->hostUrl), [ + 'json' => [ + 'model' => $model, + ], + ]); + + return $response->toArray(); + } + + /** + * @param array{name: string, model: string, modified_at: string, size: int} $model + * + * @see https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models + */ + private function buildModelDefinition(array $model): ModelDefinition + { + $modelInformation = $this->fetchModelDetails($model['model']); + + $capabilities = []; + foreach ($modelInformation['capabilities'] ?? [] as $capability) { + $capabilities = [...$capabilities, ...(self::CAPABILITY_MAP[$capability] ?? [])]; + } + + $meta = [ + 'family' => $modelInformation['details']['family'] ?? null, + 'modified_at' => $model['modified_at'] ?? null, + 'parameter_size' => $modelInformation['model_info']['general.parameter_count'] ?? null, + ]; + + return new ModelDefinition($model['name'], $capabilities, $meta); + } + /** * @param array $payload * @param array $options diff --git a/src/platform/src/ModelDefinition.php b/src/platform/src/ModelDefinition.php new file mode 100644 index 000000000..1092ae461 --- /dev/null +++ b/src/platform/src/ModelDefinition.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform; + +use Symfony\AI\Platform\Exception\InvalidArgumentException; + +final class ModelDefinition +{ + /** + * @param non-empty-string $name + * @param Capability[] $capabilities + * @param array $meta + */ + public function __construct( + private readonly string $name, + private readonly array $capabilities = [], + private readonly array $meta = [], + ) { + if ('' === trim($name)) { + throw new InvalidArgumentException('Model name cannot be empty.'); + } + } + + /** + * @return non-empty-string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return Capability[] + */ + public function getCapabilities(): array + { + return $this->capabilities; + } + + public function supports(Capability $capability): bool + { + return \in_array($capability, $this->capabilities, true); + } + + /** + * @return array + */ + public function getMeta(): array + { + return $this->meta; + } +} diff --git a/src/platform/src/ModelDefinitionsAwareInterface.php b/src/platform/src/ModelDefinitionsAwareInterface.php new file mode 100644 index 000000000..230144438 --- /dev/null +++ b/src/platform/src/ModelDefinitionsAwareInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform; + +interface ModelDefinitionsAwareInterface +{ + /** + * @return array + */ + public function fetchModelDefinitions(): array; +} diff --git a/src/platform/src/Platform.php b/src/platform/src/Platform.php index de5a16dc9..256839e25 100644 --- a/src/platform/src/Platform.php +++ b/src/platform/src/Platform.php @@ -58,6 +58,34 @@ public function invoke(Model $model, array|string|object $input, array $options return $this->convertResult($model, $result, $options); } + /** + * @param string $prefix (optional) filters model names by the given prefix + * + * @return array + */ + public function fetchModelDefinitions(string $prefix = ''): array + { + $allModelDetails = []; + foreach ($this->modelClients as $modelClient) { + if (!$modelClient instanceof ModelDefinitionsAwareInterface) { + continue; + } + $modelDefinitions = $modelClient->fetchModelDefinitions(); + if ('' !== $prefix) { + $modelDefinitions = array_filter( + $modelDefinitions, + static fn (string $name): bool => str_starts_with($name, $prefix), + \ARRAY_FILTER_USE_KEY + ); + } + if ([] !== $modelDefinitions) { + $allModelDetails = [...$allModelDetails, ...$modelDefinitions]; + } + } + + return $allModelDetails; + } + /** * @param array $payload * @param array $options