Skip to content

Working with Model Catalogs

Edit this page

Every Symfony AI platform resolves a model name like gpt-5-mini into a Model object — a model name, its Capability set, and default options. The component that performs this lookup is the model catalog. Each bridge ships its own catalog, hand-curated for the models that provider was known to offer when the bridge was released.

That works well until a provider ships a model your installed bridge has never heard of. Because the catalog is bundled with the bridge, its lifecycle is tied to the Symfony AI release cycle: a freshly released model — or a local model served by Ollama or LM Studio — is simply not in the catalog yet, and invoking it fails. This page describes the ways to deal with that, from a one-off per-call override to a catalog that refreshes on its own schedule.

Understanding the Catalog Lifecycle

A model catalog is just an implementation of ModelCatalogInterface. When you call $platform->invoke('gpt-5-mini', $messages), the platform asks its catalog for the model matching that name and uses the capabilities and options the catalog returns.

There is no single "right" way to keep that catalog current — it depends on how much you want to own. The approaches below trade off control against convenience:

  1. Configure it yourself — add or override individual models, or pass a fully defined model per call and skip the catalog entirely. Best when you have a handful of custom or local models, or want to source model definitions from somewhere else (a database, a feature flag, ...).
  2. Use a continuously updated catalog — back the catalog with the community models.dev registry, shipped as a Composer package you update independently of the framework. Best when you want broad, fresh coverage without curating models by hand.

You can mix them: a models.dev-backed catalog for the long tail, plus a per-call override or a bundle entry for the one model that is not in any registry yet.

Approach 1: Pass a Model Instance Per Call

The most direct escape hatch is to hand a fully defined model instance to Platform::invoke() instead of a name. This bypasses the catalog lookup completely, so the model never has to exist in any catalog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

$model = new Gpt('gpt-newest', [
    Capability::INPUT_MESSAGES,
    Capability::OUTPUT_TEXT,
    Capability::TOOL_CALLING,
], ['temperature' => 0.5]);

$result = $platform->invoke($model, new MessageBag(
    Message::ofUser('What is the Symfony framework?'),
));

A string keeps the usual catalog-based routing; a Model instance bypasses it. This is the foundation for decoupling from the catalog entirely — if your model definitions live in a database or are toggled by configuration, build the instance from that source and pass it in.

Note

You must pass a bridge-specific model subclass (Gpt, Claude, Gemini, ...), not the base Model. Model clients, result converters, and contract normalizers dispatch on the concrete class, so a bare Model has no client to handle it. The platform routes the instance to the first provider whose model clients accept it.

Approach 2: Add Models via the AI Bundle Configuration

In a Symfony application using the AI Bundle, you can extend a platform's built-in catalog declaratively with the model configuration key — no per-call override and no custom catalog service. This is the natural fit for local models (LM Studio, Ollama) or a model that has not been added to the bridge yet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/packages/ai.yaml
ai:
    platform:
        lmstudio:
            host_url: '%env(LMSTUDIO_HOST_URL)%'

    model:
        lmstudio:
            qwen3-coder-next:
                class: 'Symfony\AI\Platform\Bridge\Generic\CompletionsModel'
                capabilities:
                    - !php/const Symfony\AI\Platform\Capability::INPUT_MESSAGES
                    - !php/const Symfony\AI\Platform\Capability::OUTPUT_TEXT
                    - !php/const Symfony\AI\Platform\Capability::OUTPUT_STREAMING
                    - !php/const Symfony\AI\Platform\Capability::TOOL_CALLING

    agent:
        coder:
            platform: 'ai.platform.lmstudio'
            model: 'qwen3-coder-next'

Models are keyed by platform name. For each one you provide the model class (it must extend Model; for generic OpenAI-compatible platforms use CompletionsModel for chat models or EmbeddingsModel for embeddings) and the list of capabilities it supports.

The configured models are merged into the built-in catalog and take precedence over models with the same name, so the same key also lets you override the capabilities of an existing model.

Approach 3: Use a Continuously Updated Catalog

Configuring models one by one does not scale if you want broad, always-current coverage. The models.dev bridge solves this by replacing a bridge's bundled catalog with one sourced from the community models.dev registry, shipped as the standalone symfony/models-dev Composer package — a daily snapshot of providers, models, capabilities, and pricing.

The key benefit is that the catalog lifecycle is decoupled from the framework release cycle. Newly released models arrive with composer update symfony/models-dev, without bumping symfony/ai-platform or editing any catalog by hand.

Install the bridge and the data package:

1
composer require symfony/ai-models-dev-platform symfony/models-dev

The bridge provides a ModelCatalog that reads the models.dev data for a given provider and drops into the matching bridge in place of its bundled catalog. For any OpenAI-compatible provider, pair it with the Generic bridge:

1
2
3
4
5
6
7
8
9
10
use Symfony\AI\Platform\Bridge\Generic\Factory as GenericFactory;
use Symfony\AI\Platform\Bridge\ModelsDev\ModelCatalog;

$platform = GenericFactory::createPlatform(
    baseUrl: 'https://api.deepseek.com',
    apiKey: $_ENV['DEEPSEEK_API_KEY'],
    modelCatalog: new ModelCatalog('deepseek'),
);

$result = $platform->invoke('deepseek-chat', $messages);

For a provider that needs a specialized bridge, pair the catalog with that bridge. The catalog already carries the model class that bridge expects (e.g. Claude for Anthropic), based on the provider's models.dev entry:

1
2
3
4
5
6
7
use Symfony\AI\Platform\Bridge\Anthropic\Factory as AnthropicFactory;
use Symfony\AI\Platform\Bridge\ModelsDev\ModelCatalog;

$platform = AnthropicFactory::createPlatform(
    apiKey: $_ENV['ANTHROPIC_API_KEY'],
    modelCatalog: new ModelCatalog('anthropic'),
);

The Models.dev Platform reference covers the bridge in full, including its Factory, embeddings, streaming, and bundle configuration.

Combining Providers in One Platform

Because each provider is just a regular bridge backed by a models.dev catalog, you can compose several into a single Platform and let it route by model name. ProviderRegistry resolves the API base URL for OpenAI-compatible providers, including ones models.dev does not publish directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\AI\Platform\Bridge\Anthropic\Factory as AnthropicFactory;
use Symfony\AI\Platform\Bridge\Generic\Factory as GenericFactory;
use Symfony\AI\Platform\Bridge\ModelsDev\ModelCatalog;
use Symfony\AI\Platform\Bridge\ModelsDev\ProviderRegistry;
use Symfony\AI\Platform\Platform;

$registry = new ProviderRegistry();

$platform = new Platform([
    GenericFactory::createProvider(
        baseUrl: $registry->getApiBaseUrl('deepseek'),
        apiKey: $_ENV['DEEPSEEK_API_KEY'],
        modelCatalog: new ModelCatalog('deepseek'),
        name: 'deepseek',
    ),
    AnthropicFactory::createProvider(
        apiKey: $_ENV['ANTHROPIC_API_KEY'],
        modelCatalog: new ModelCatalog('anthropic'),
    ),
]);

$platform->invoke('deepseek-chat', $messages);     // → deepseek
$platform->invoke('claude-haiku-4-5', $messages);  // → anthropic

A model id resolves to the first provider (in array order) whose catalog knows it. When several providers expose the same id, order the array so the preferred one comes first.

Choosing an Approach

There is deliberately no single default; pick the smallest tool that covers your case:

  • One or two custom/local models — Approach 2 (bundle config) keeps everything declarative, or Approach 1 if you are not using the bundle.
  • Model definitions from an external source (database, feature flags, an admin UI) — Approach 1, building the Model instance from that source and bypassing the catalog.
  • Broad, always-current coverage across many providers — Approach 3, refreshed on its own cadence with composer update symfony/models-dev.

These compose: a models.dev-backed catalog handles the long tail while a bundle entry or per-call instance covers the one model no registry has yet.

See Also

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version