> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instructorphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Model catalog

## One identity and one source

Polyglot identifies an offering by the exact pair `(driver, wire model)`. A profile owns the
offering's support status, context and output limits, modalities, capabilities, source, and
catalog version. `LLMConfig` and connection presets deliberately do not duplicate those facts.

```php theme={null}
use Cognesy\Polyglot\Inference\Models\ModelCatalog;
use Cognesy\Polyglot\Inference\Models\SupportStatus;

$profile = ModelCatalog::discover()->find('qwen', 'qwen3.8-max');

if ($profile->capabilities->jsonSchema === SupportStatus::Supported) {
    // This exact route supports native JSON Schema.
}
// @doctest id="2e14"
```

`find()` never guesses by provider or model-family pattern. A missing pair returns a
`ModelProfile` whose status and feature facts are `unknown` and whose numeric facts are `null`.
Custom and newly released models therefore remain executable without being falsely advertised
as supported or unsupported.

Reasoning uses the same exact record. `capabilities.reasoning` declares the supported selection
kinds, exact provider effort values, effective effort, mapping quality, budget range, default
behavior, and whether reasoning content and token counts are visible. Omitting that object means
reasoning support is unknown. A `quality` value of `lossy` says the provider value would produce a
different effective effort; typed requests reject that mapping by default.

## Optional request-scoped resolution

Ordinary inference does not discover, load, or consult a model catalog. Compose the runtime once
per service or worker and reuse it across isolated inference objects:

```php theme={null}
use Cognesy\Polyglot\Inference\Creation\InferenceDriverRegistry;
use Cognesy\Polyglot\Inference\Inference;
use Cognesy\Polyglot\Inference\InferenceRuntime;

$drivers = InferenceDriverRegistry::default();
$runtime = InferenceRuntime::fromConfig($config, drivers: $drivers);

$first = Inference::fromRuntime($runtime)->withMessages($messages)->create();
$second = Inference::fromRuntime($runtime)->withMessages($otherMessages)->create();
// @doctest id="b8d2"
```

Pass a catalog only when the application wants local capability enforcement, metadata, or facts
needed by a selected processor:

```php theme={null}
use Cognesy\Polyglot\Inference\InferenceRuntime;
use Cognesy\Polyglot\Inference\Models\ModelCatalog;

$catalog = ModelCatalog::discover()->overlay(
    ModelCatalog::fromPaths('/home/app/.config/instructor/models'),
);

$runtime = InferenceRuntime::fromConfig($config, models: $catalog);
// @doctest id="0f63"
```

With this explicit composition, `InferenceRuntime` resolves the exact profile after applying a
request-level model override. The transient profile travels with the in-process request so local
policy, translation, and telemetry describe the model actually executed. It is excluded from
request serialization.

`discover()` resolves model directories through the same application, monorepo, and vendor
locations as LLM presets. It creates a reusable catalog without reading any model records.
`find()` reads only the exact offering's YAML file, using the first matching directory.
Application `config/llm/models` takes precedence over packaged records. Overlays replace whole
records; omitted facts in a replacement become unknown rather than inheriting packaged values.

Discovered catalogs are memoized by application root. `discover($projectRoot)` selects a project
explicitly without changing the global base path; `fromPaths($projectModels, $packageModels)`
creates an independent catalog with explicit ordered roots. Each catalog caches hydrated profiles
and missing keys. Reusing it across hundreds of inference objects avoids repeated file reads and
hydration. `count()`, iteration, `forDriver()`, and `toArray()` explicitly enumerate records.

Profiles are immutable. A catalog instance has no automatic reload; create a new instance to
change its configured scope. The shared config reader caches parsed source by path and mtime;
tests or development tools rewriting files within one timestamp tick can explicitly call
`Config::flushSourceCache()`. Deploy records with the application's immutable release directory,
not by rebuilding a directory active requests are reading. Runtime performs no network lookup,
model-family matching, catalog writes, database query, or background refresh.

Before a provider body is rendered, an in-process preflight step reads only facts explicitly
attached to that request. It does not make an LLM or network request. Explicitly unsupported
streaming, tools, tool choice, JSON Object, and reasoning selections are rejected. Missing or
unknown facts make no local support assertion, so custom models remain executable.

A semantic change such as JSON Schema to JSON Object requires both an exact supported fallback
fact and `LLMConfig::$allowLossyFallback === true`. The default is `false`. Approved changes are
recorded on the effective request and included in the `InferenceRequested` event; adapters never
silently remove or weaken requested behavior. An adapter can still reject an operation it cannot
encode, which is a protocol limitation rather than a catalog capability decision.

## Catalog files

A runtime offering is one named YAML record. For example,
`config/llm/models/openai-compatible/acme-1.yaml`:

```yaml theme={null}
schemaVersion: 1
version: project-1
profile:
  driver: openai-compatible
  model: acme-1
  status: supported
  limits:
    contextWindow: 128000
    maxOutput: 8192
  modalities:
    inputText: supported
    outputText: supported
  capabilities:
    streaming: supported
  source: project
# @doctest id="1ccc"
```

The path is `<encoded-driver>/<encoded-model>.yaml`, with each component encoded using
`rawurlencode()`; the special components `.` and `..` have their dots percent-encoded as well.
For example, model `openai/gpt-oss-120b` under driver `openrouter` is stored as
`openrouter/openai%2Fgpt-oss-120b.yaml`. The `profile` retains the exact unencoded identity,
which must match its path. `ModelRecordDirectory::relativePath(new ModelKey($driver, $model))`
provides the canonical relative path. Model facts are loaded literally, without environment or
secret substitution.

`schemaVersion` must be integer `1`; `version` is a non-empty data revision. Parsers reject
unknown fields, invalid status types, malformed nested records, and duplicate effort mappings.
Explicit null is permitted for numeric limits and the optional maximum reasoning budget;
other present fields must have their declared type. Missing facts remain unknown. A malformed
or unreadable selected record raises an error; it does not fall back to a lower-priority record.

Every support field is tri-state: `supported`, `unsupported`, or `unknown`. Omitted support and
numeric fields are unknown. Catalog serialization omits unknown and null facts.

Overall `status: supported` means only that the exact `(driver, wire model)` route is intentionally
maintained and the named driver is executable. It does not imply that every modality or feature is
supported. Each nested fact stands on its own; a sparse supported record is valid. A partially
supported distinction that the current schema cannot express stays `unknown`. For example, Qwen's
model record does not claim generic tool-choice support because some choices depend on whether
thinking is enabled, while drivers that cannot render any explicit choice use `unsupported`.

Packaged `source` values describe how the checked-in record was authored:

* `upstream-reviewed` means selected upstream facts were reviewed before being committed;
* `hand-authored` means the maintainer owns the exact assertions directly;
* project records choose their own source label, commonly `project`.

These labels are evidence categories, not automatic freshness guarantees. Capability evidence is
kept deliberately small and tied to a real consumer:

| Fact                             | Required evidence                                                                      | Consumer                                                     |
| -------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| overall status                   | maintained exact driver route and reviewed model identifier                            | Tell metadata and application policy                         |
| limits and modalities            | reviewed upstream/provider facts; modalities must also have a represented message path | Tell metadata and application policy                         |
| streaming and tools              | reviewed model claim plus driver request/response coverage                             | optional preflight and Tell metadata                         |
| tool choice and response formats | final wire-shape coverage for the asserted route; partial support remains unknown      | optional preflight and Tell metadata                         |
| reasoning                        | exact hand-authored or reviewed selection/mapping data plus translation coverage       | reasoning translation, optional preflight, and Tell metadata |

Generic file input is intentionally absent. A future document or PDF capability needs a concrete
message abstraction, at least one provider renderer, and final wire-payload tests before it can be
advertised.

The packaged record directory is generated offline from source and override files:

```bash theme={null}
packages/polyglot/bin/instructor-catalog build
packages/polyglot/bin/instructor-catalog check
packages/polyglot/bin/instructor-catalog validate
# @doctest id="149d"
```

These commands do not fetch provider APIs. Updating upstream facts is an explicit source-review
workflow, while application runtime reads only the selected deterministic record. `--target`
names a generated record directory. Build updates its YAML records and removes obsolete generated
records; do not store unrelated hand-authored files in that output directory.

The source and override JSON catalogs remain offline maintenance inputs. `ModelCatalog::fromArray()`
and `fromFile()` support explicit bulk data loading for tools and callers supplying in-memory
catalogs; runtime discovery never loads a monolithic `models.json` file. Bulk envelopes accept
`schemaVersion: 1` (omission also selects schema 1); generated runtime records require it explicitly.

## Maintaining the packaged catalog

Edit `resources/catalog/models-source.json` for reviewed base facts or
`resources/catalog/models-overrides.json` for hand-authored whole-record replacements. Review the
source diff directly in Git, then rebuild the deterministic runtime records:

```bash theme={null}
just models-build
just models-check
just models-validate
# @doctest id="73a8"
```

All three commands are offline. Build validates both inputs before changing output, writes readable
individual records, and removes obsolete generated records. Check reports stale output without
changing it. Validate parses both inputs and verifies the complete generated record set. There is
no fetch, staging, apply, mapping, or provenance subsystem; upstream research and review happen
outside this package workflow.
