# ADR-0011: Compile-Time Model Registry with Identifier Normalization

## Status

🔄 Superseded by [ADR-0022](adr-0022.md) (2026-05-06)

The catalog is no longer embedded-only. The compile-time YAML and identifier
normalisation logic described below remain in place, but `ModelRegistry::load()`
now layers optional `~/.omni-dev/models.yaml` and `./.omni-dev/models.yaml`
files over the embedded catalog. See ADR-0022 for the layered loader,
precedence rules, and failure modes.

## Context

omni-dev makes API calls to AI models from multiple providers (Anthropic Claude,
OpenAI, Google Gemini) and must configure each request with the correct parameters:
`max_tokens`, the input context limit, and any provider-specific HTTP beta headers.
These values differ substantially across models — output token limits range from 4,096
to 128,000 — and some models require specific beta headers to unlock enhanced limits
(for example, `anthropic-beta: output-128k-2025-02-19` unlocks 128K output tokens on
Claude Sonnet 3.7).

AWS Bedrock and regional API gateways introduce a further complication: they use
non-canonical identifier formats such as `us.anthropic.claude-3-7-sonnet-20250219-v1:0`
or `anthropic.claude-3-haiku-20240307-v1:0` rather than the Anthropic API's plain
`claude-3-7-sonnet-20250219`. A lookup mechanism must map these variants to their
canonical spec.

Three alternatives were evaluated:

- **Runtime API discovery.** Query each provider's API at startup to learn model
  capabilities. This adds latency, requires network connectivity at startup, and the
  Anthropic Messages API does not expose per-model token limits or required beta
  headers via a discovery endpoint.

- **User-configured limits only.** Require users to specify `max_tokens` and related
  parameters in their omni-dev configuration. This shifts correctness responsibility
  to users, who cannot reasonably be expected to know the right values for every
  model they might use, and creates silent misconfiguration when fields are omitted.

- **Hardcoded per-model constants.** Inline the values as Rust constants in source
  code. Avoids any parsing overhead but hard to maintain: every new model requires a
  code change and a release. Constants also cannot express the structured relationship
  between a model and its beta headers without additional boilerplate.

## Decision

We will embed a YAML model catalog (`src/templates/models.yaml`) at compile time
using `include_str!` and expose it through a singleton `ModelRegistry` initialized
via `std::sync::OnceLock` on first access (`get_model_registry` in
`src/claude/model_config.rs`).

### Catalog structure

The YAML has two top-level sections:

- **`models`** — one entry per model, recording `api_identifier`, `max_output_tokens`,
  `input_context`, `provider`, `tier`, `generation`, `legacy`, and `beta_headers`.
- **`providers`** — one entry per provider, recording `api_base`, `default_model`,
  tier descriptions, and fallback `defaults` used when a model identifier is not
  found in the catalog.

### Identifier lookup

`get_model_spec` resolves an identifier in two steps:

1. **Exact match** — look up the identifier directly in the `HashMap<String, ModelSpec>`
   built at load time.
2. **Normalize then look up** — strip non-canonical parts from the identifier and
   repeat the exact lookup. The normalization in `extract_core_model_identifier`
   removes:
   - Region prefixes: if the string before the first `.` is three characters or fewer
     it is treated as a region code (`us`, `eu`, etc.) and dropped.
   - Provider prefix: the literal prefix `anthropic.` is removed.
   - Version suffix: a trailing `-vN:N` segment (e.g. `-v1:0`) is removed.

   After normalization, `us.anthropic.claude-3-7-sonnet-20250219-v1:0` becomes
   `claude-3-7-sonnet-20250219`, which matches the canonical entry.

### Beta header overrides

Each model entry may declare one or more `beta_headers`. A beta header can override
`max_output_tokens` and/or `input_context` when that header is active. Callers that
send a specific beta header use `get_max_output_tokens_with_beta` or
`get_input_context_with_beta` to obtain the effective limit for that header.

### Fallback chain

When an identifier is not found in the catalog, `get_max_output_tokens` and
`get_input_context` fall back in order:

1. Provider-level defaults in `providers.<name>.defaults` (inferred from whether
   the identifier contains `claude` or `anthropic`).
2. Hardcoded ultimate fallback (`FALLBACK_MAX_OUTPUT_TOKENS = 4096`,
   `FALLBACK_INPUT_CONTEXT = 100_000`).

## Consequences

**Positive:**

- **Correct API parameters without runtime discovery.** Token limits and beta headers
  are always available, even in offline environments or when the provider has no
  discovery endpoint.

- **Zero overhead after first access.** `OnceLock` initializes the registry once;
  subsequent lookups are plain HashMap operations.

- **Single file to update when models change.** Adding a new model or adjusting
  limits requires editing `models.yaml` only; no Rust source changes are needed.

- **Bedrock and regional identifiers are handled transparently.** Callers do not need
  to normalize identifiers before lookup; `get_model_spec` handles both canonical and
  non-canonical formats.

- **Beta header overrides are co-located with model specs.** The YAML entry for a
  model is the single source of truth for both its base limits and any header-unlocked
  extensions.

**Negative:**

- **The catalog is static.** A newly released model is unavailable until `models.yaml`
  is updated and a new version of omni-dev is released. Users running an older binary
  against a new model fall through to provider defaults or the hardcoded fallback,
  which may silently under-cap output.

- **The region-prefix heuristic is approximate.** The normalization treats any prefix
  of three characters or fewer before the first `.` as a region code. An unusual
  identifier format with a short non-region prefix would be incorrectly stripped.

- **A malformed embedded YAML causes a panic at first access.** If `models.yaml` is
  corrupted at compile time, `get_model_registry` panics via `expect`. This is
  accepted because the YAML is validated by the existing test suite at CI time and the
  failure mode is a startup crash rather than silent misconfiguration.

**Neutral:**

- **Adding a new provider requires two YAML edits.** A new provider needs both a
  `models` entry and a `providers` configuration block (for `api_base`, `default_model`,
  `tiers`, and `defaults`).

- **Unknown models always receive a non-zero fallback limit.** The fallback chain
  ensures API calls are never made with a zero `max_tokens`, but the hardcoded fallback
  value (4,096 output tokens) may be smaller than the actual model's capability.
