# AI catalog — model tiers, control rules, and OUR defaults

Durable rules for `ai.model_definition` / `ai.model_config` / `ai.api` / `ai.offering` and the
resolvers in this directory. The DB is truth; this file states the *intent* behind the rows so an
agent stops "correcting" deliberate decisions back to provider behavior.

---

## 🚨 RULE 1 — OUR default is deliberately LOWER than the provider's. Never "fix" it upward.

> **Providers set their default high because they are paid by the token.** For most of our work,
> the provider's default is more thinking than the task needs — we pay for reasoning nobody reads.
> So the Matrx default is set BELOW the provider's on purpose.

**The trap this rule exists to stop:** an agent reads our `reasoning_effort` default of `low` or
`medium`, checks the Anthropic/OpenAI/Google docs, sees the provider default is `high`, concludes
our row is wrong, and "fixes" it. **That is backwards.** The provider's default is precisely what we
are overriding. If a caller passes nothing, the provider would give you `high` — *that is why we
send our own value.*

- **A Matrx default that differs from the provider's is EVIDENCE OF A DECISION, not a bug.**
  Do not change one because it disagrees with a provider doc. It is *supposed* to disagree.
- Changing a default is a **product/cost decision** — Arman's call, never an agent's.
- `default: null` is NOT the same as "use a sane default." It means we send nothing and the
  **provider's** expensive default applies. On a paid reasoning model, a null default is a silent
  cost leak, not a neutral choice.
- The same logic governs every knob a provider bills for (verbosity, max tokens, tool budgets), not
  just `reasoning_effort`.

## 🚨 RULE 2 — Expensive Claude models are SPLIT into a base and a `-max` listing.

The deep reasoning tiers (`xhigh`, `max`) are **not** available on the base listing. They are the
product of the separate premium `-max` model, mirroring what several providers now do.

| Listing | `reasoning_effort` enum | `effort_ceiling` |
|---|---|---|
| `claude-opus-5`, `claude-sonnet-5`, `claude-fable-5`, `claude-opus-4-8` (and older base rows) | `auto, none, low, medium, high` | `"high"` |
| `claude-opus-5-max`, `claude-sonnet-5-max`, `claude-fable-5-max`, `claude-opus-4-8-max` | `xhigh, max` | none (uncapped) |

Enforced at **two independent layers** (house doctrine — a single gate is a patch):
1. **UI/enum** — the base offering's rules simply do not list `xhigh`/`max`.
2. **Engine** — `processors.py::anthropic_thinking` (mode=adaptive) honors
   `processor_config.effort_ceiling`; anything above it clamps DOWN with a loud
   `Adjustment(action="effort_ceiling")` so a raw API caller cannot bypass the UI cap.

**So: a base Claude listing resolving `xhigh` → `high` is CORRECT.** If a golden fixture, doc, or
test asserts `xhigh` at a base listing, the *fixture is stale* — regenerate it; do not remove the
ceiling. (This exact confusion produced a 108-case false alarm in `validate_catalog_parity.py` that
sat unexplained for two weeks — see the Change log.)

## 🚨 RULE 3 — `ui_values` is what the MODEL accepts, and the engine enforces it on a
processor's resolved value.

A processor's effort maps are keyed by model **family** (`flash`, `pro`, `budget`), and a family is
not a model. `ui_values` on the offering is the only per-**model** statement of what the provider
will accept, so a processor's RESOLVED provider value is checked against it before it goes on the
wire (`ProcessorContext.reconcile_supported`, consumed today by `google_thinking` mode `gemini_3`).

- Outside the declared set → reconciled to the **nearest supported intensity** in the
  `ai.setting.canonical_values` order (ties break toward the LATER/more-intense position), with a
  loud `Adjustment(action="unsupported_value")` and a yellow banner. It never raises and never
  kills a request — the house rule is *a guard that CAN reconcile MUST reconcile*.
- **`auto`/`none` are never reconcile targets.** They are postures ("leave unset" / "send
  nothing"), not degrees; snapping `minimal` onto `none` would silently turn *think a little* into
  *do not think* on a model whose floor is merely higher (measured on MiniMax-M3: supports only
  `high`/`xhigh`, and `none` is the nearest by raw distance).
- **It fills a hole; it never overrides a declared policy.** A rule with a `value_map` already
  declares miss behaviour via `on_unmapped`, and `anthropic_thinking` declares `effort_ceiling` —
  both still win. An authored family downgrade stays authored: `gemini-3-pro` maps `medium → low`
  and, since `low` is supported, nothing touches it.

**Why it exists.** `_GOOGLE_3_EFFORT_TO_LEVEL_FLASH` maps `minimal → "minimal"` — true of
gemini-3.5-flash, **false** of gemini-3.7-flash, whose offering declares
`ui_values=[auto,none,low,medium,high]`. 38 live agents stored
`reasoning_effort="minimal"`, nothing compared the map's output to the declaration, and Google 400'd
every one of their calls (*"Thinking level MINIMAL is not supported for this model"*) — the FastFire
spoken-answer grader failed on every card of every session. The same gate also caught two latent
400s already frozen in the `google_thinking_3__pro` golden (raw `thinking_budget` resolving to
`minimal`/`medium` on a listing that supports only `low`/`high`).

## 🚨 RULE 4 — THE EQUIVALENCE LAW: convert to the nearest equivalent; drop only when there
isn't one.

**Arman, 2026-08-17:** *"I should be able to call any model, from any provider, with
configurations from any other model and any other provider in any random way I want, and every
single configuration should properly convert and get properly applied without anything being
dropped or lost — unless it's an additive configuration offering something that the given new
model does not support."* That is the product's number-one selling point.

- **Conversion is the DEFAULT.** `ControlRule.on_unmapped` defaults to `nearest`; `drop` is a
  posture a rule must explicitly declare. (It defaulted to `drop` until 2026-08-17, and **135 of
  the 135** live rules carrying a `value_map` took that default — 1,139 measured (value × model)
  combinations discarded rather than converted. Ask for `21:9` on a model whose map lists only
  `16:9` and you got the model's *default*, announced by one yellow line in a server log.)
- **Equivalence is per-SETTING, in [`equivalence.py`](equivalence.py)** — one registry, one entry
  point (`nearest_equivalent`). Ordered scales (`reasoning_effort`, `resolution`, …) resolve by
  position in `ai.setting.canonical_values`; `aspect_ratio` resolves by **geometry** and never
  flips orientation (a portrait request never becomes a landscape crop); `output_format` /
  `audio_format` resolve by **format family**. A single global "nearest" is wrong: `21:9` is
  nearest `16:9` by ratio, not by list position.
- **A setting with no declared metric returns `None`, and the caller drops LOUDLY.** Converting
  incorrectly is its own violation of the law. `tts_voice` deliberately has no metric — a voice is
  an identity, not a degree, so there is no honest nearest; its resolution is a product decision,
  not a distance function.
- **House postures are not degrees.** `auto`/`none` are never the equivalent of an intensity
  (`minimal → none` would turn *think a little* into *do not think*). They ARE valid targets for
  each other: `none → auto` is a legitimate posture-to-posture translation.

Live after the change: aspect_ratio 153/153, output_format 40/40, resolution 30/30,
reasoning_effort 22/22, reasoning_summary 21/21 convert; audio_format 314/381; `tts_voice` 0/492
(pending the product ruling above).

### Explicit default declaration — `ControlRule.to_default` (Rule 3)

**"Resolves to the model default" is a DECLARED decision, not a lookup miss.** Arman, 2026-08-17:
*"There are instances where it makes sense for the conversion to be to a default, but that has to
be an explicit declaration, and it needs to be something that stands out as opposed to being the
norm."*

- **`ControlRule.to_default: list[Any] | None`** names the canonical values that DELIBERATELY
  resolve to this offering's `default` rather than being converted (`value_map`) or dropped
  (`on_unmapped`). Undeclared, `to_default` is `None` and nothing about it changes today's
  behaviour — it is opt-in per key, per offering.
- **Precedence: `to_default` beats `value_map`/`on_unmapped`.** A declared decision wins over an
  inferred conversion — a value listed in `to_default` never reaches the value_map lookup at all,
  even when a value_map entry for it exists.
- **Resolution:** if the rule has a `default`, that provider value is sent; if not, the key is
  omitted (same shape as an unmapped drop, but for a reason that was WRITTEN DOWN).
- **`Adjustment(action="to_default", expected=True)`** — its own action, greppable and visibly
  distinct from `mapped` / `dropped` / `unsupported_value`. `expected=True` because a DECLARED
  default is not a surprise: per `providers/outbound_params.py::warn_client_about_dropped_settings`
  only `expected=False` drops reach the client as a warning — this one stays silent, same posture
  as a `supported: false` capability gap.
- **Validated like `ui_values`:** every `to_default` member must be a member of the setting's
  `canonical_values` (`validate_rules_against_settings`), and a value listed in BOTH `to_default`
  and `value_map` is a data error — ambiguous, refused at validation.
- **Still open:** nothing yet forces a caller to actually USE `to_default` instead of an implicit
  `default` — that is a data-authoring discipline, not an engine guarantee. The mechanism exists;
  filling in the declared maps (Rule 1) is the next step.

## 🚨 RULE 5 — A PAID PROVIDER SERVICE NEEDS A PRICE, or the WHOLE call records NULL

Some server-side tools are billed by the provider **per call, not per token** — Anthropic's hosted
web search is **$10 per 1,000 searches** (web *fetch* is free; only token costs apply, and we
deliberately record no billing component for it).

- `from_anthropic` counts `usage.server_tool_use.web_search_requests` into
  `TokenUsage.billing_components["service.web_search"]`.
- The price lives in `ai.offering.pricing[].component_prices`, **per MILLION units** like every
  other component price: `$10 / 1,000 searches` is stored as **`10000`**.
- The wire-format → feature → component map is `PROVIDER_SERVICE_COMPONENTS` in
  [`usage_config.py`](../config/usage_config.py). **Add an entry only together with the
  `from_<provider>` code that counts its units** — a component nothing emits prices nothing.

**An unpriced component does not degrade to a token-only estimate — `calculate_cost_breakdown`
returns `None` for the entire call** and stamps `metadata.cost_reconciliation =
"unknown_component_price"`. That refusal is correct (a token-only figure on a call that also
incurred a service fee is a wrong number recorded silently), but it means a missing component price
erases the cost of every run that touches the service. `validate_model_pricing` now raises
`missing_service_price` (severity `error`, visible in `scripts/validate_model_pricing.py`) for any
model whose declared features include the service but whose tier has no price for it.

**Anthropic's cache-write duration split is a breakdown, not the total.**
`cache_creation_input_tokens` is authoritative; `cache_creation.ephemeral_5m/1h_input_tokens` can
come up short on a MERGED usage record (`TokenUsage.__add__` sums the flat total every iteration
but replaces the nested dict when an iteration omits it). Billing trusts the total and attributes
any remainder to the 5m default TTL, warning once per (model, api).

## Adding a model

- New premium Anthropic model → add **both** rows (base + `-max`) and give the base an
  `effort_ceiling` matching the table above. A base row without a ceiling is a defect.
- Set the Matrx `default` explicitly per Rule 1. Do not leave it `null` on a paid reasoning model.
- Regenerate the golden (`scripts/dump_chat_param_golden.py`) and **review the fixture diff** —
  the dumper is a truth-vs-code guard, not a rubber stamp.
- A model whose wire contract is not a turn must resolve to its own API. Google
  Live (`google_live`), Lyria (`google_music_realtime`), embeddings
  (`google_embeddings`), and stored/background Interactions
  (`google_interactions`) are distinct dispatch families; never attach one to
  `google_chat` merely because Google is the provider.

## The golden is derived from LIVE truth, never from a legacy label

`scripts/dump_chat_param_golden.py` classifies chat families from the **retired**
`metadata->legacy->api_class` snapshot, which drifts from the live offering. It now cross-checks the
live `ai.api` name and SKIPS (loudly) anything not on a `*_chat` api. Without that check,
`whisper-large-v3` — an STT model still carrying a `groq_standard` legacy label — was dumped into
the *chat* param golden with STT rules, producing a fixture the chat translator could never
reproduce (1018/1024 cases). **When a legacy label and live truth disagree, live wins.**

**Provider wire values are not product vocabulary.** Canonical `resolution="0.5k"` remains stable
when a provider renames its request literal; the `google_image` native rule maps it to Google's
current `image_size="512"` only for Gemini 3.1 Flash Image models that support it. Other Google
image offerings translate that request to their supported `1K` floor. `GoogleImageGeneration`
independently repairs the retired `0.5K` alias with a loud alarm so stale cached or standalone
rules cannot repeat the paid request failure.

## Change log

- **2026-08-20** — Rule 5 added, after seven `claude-opus-5` runs (2026-08-18 → 08-19, the "Race
  Frontier Floor" agent with `internal_web_search`) recorded `cost = NULL`. The offerings for
  `claude-opus-5` / `claude-opus-5-max` shipped without
  `component_prices["service.web_search"]` — every other web-search-capable Anthropic offering
  already had it — so each call refused to price at all. Both offerings now carry `10000`; the 7
  rows were backfilled through the real billing path (**$5.16** total, of which $0.35 is the
  service fee). Two guards: `validate_model_pricing` → `missing_service_price`, and
  `packages/matrx-ai/tests/test_web_search_billing.py`, which pins the exact recorded
  `raw_usage` of row `3d501a92-24eb-42b8-9815-6dfa595d9780`. The same fixture exposed a second
  defect, also fixed: the Anthropic cache-write duration split read 6,624 tokens against an
  authoritative `cache_creation_input_tokens` of 42,709, silently dropping 36,085 billable
  cache-write tokens on that one call (it fired on all 7).

- **2026-08-18** — Updated native Google image resolution translation from the retired
  `0.5K` wire alias to `512`; added a loud provider-boundary repair so stale catalog snapshots
  cannot send the rejected value.

- **2026-08-17** — Rule 3's explicit-default mechanism added: `ControlRule.to_default` names
  canonical values that DECLARE a resolution to this offering's `default`, distinct from a
  value_map miss and taking precedence over it. Recorded as
  `Adjustment(action="to_default", expected=True)` — silent to the client, same as a declared
  capability gap. Validated like `ui_values` against `canonical_values`; a value in both
  `to_default` and `value_map` is a data error. Closes open item 2 of
  `common-docs/systems/configuration-equivalence/FEATURE.md` (the mechanism now exists; filling in
  the declared maps is separate, ongoing work).
- **2026-08-17** — Rule 4 (THE EQUIVALENCE LAW) added: `on_unmapped` now defaults to `nearest`,
  and equivalence is resolved per setting by [`equivalence.py`](equivalence.py) (ordinal /
  geometry / format family, `None` where no honest metric exists). Recovers 580 of 1,139 live
  (value × model) combinations that were being dropped instead of converted; the remaining 559 are
  `tts_voice` (492, no ordinal equivalence — needs a product ruling) and `audio_format` (67, no
  family relative on that provider). Both golden suites unchanged.
- **2026-08-17** — Rule 3 added: a processor's resolved provider value is now reconciled against
  the offering's declared `ui_values` (`ProcessorContext.reconcile_supported`, wired into
  `google_thinking` mode `gemini_3`). Fixes a total outage of the FastFire spoken-answer grader —
  `reasoning_effort="minimal"` on gemini-3.7-flash shipped `thinking_level: MINIMAL`, which that
  model rejects, 400-ing every call for 38 agents. The `google_thinking_3__pro` golden was
  regenerated: 108 cases, transitions `minimal → low` and `medium → high`, both of them levels that
  listing never supported.
- **2026-08-15** — Added catalog routing for Google's three new non-turn wire
  families: `google_live` → realtime session, `google_music_realtime` → music
  session, and `google_embeddings` → vector call. The Model Config Sync agent
  populated the four current Live models, Lyria, stable
  `gemini-embedding-2` + legacy `gemini-embedding-001`, and the three
  background Interactions agents. The requested `gemini-embedding-2-preview`
  was not recreated: Google shut that preview down on 2026-08-10 and the
  current wire id is `gemini-embedding-2`.

- **2026-08-15** — Gemini Omni Flash became a real `google_interactions`
  dispatch route. The adapter sends multimodal input through
  `interactions.create`, nests `task` under `generation_config.video_config`,
  persists `previous_interaction_id`, downloads URI-delivered video only after
  the Files API reports `ACTIVE`, and bills the real Interactions token usage.
  The offering is system-owned and `token_billed=true` with no synthetic
  `usage_basis`; Google prices 720p video output at 5,792 tokens per second.

- **2026-08-11** — Catalog routing failures are typed
  `CatalogRoutingError` and universally classified as
  `matrx_catalog_error`, non-retryable. An unknown model, unavailable offering,
  dead endpoint/API, or invalid offering pin is deterministic local
  configuration—not a provider outage. This closes the incident where a
  retired Qwen model with no active offering was retried three times and shown
  to the user as `unknown_error`; the exact routing defect now survives into
  the stream and no paid/provider retry is scheduled.

- **2026-08-10 (b)** — **Every catalog deviation from an explicit pin is now
  queryable.** `ResolvedCallProfile.resolution_route` gained `"tier_reroute"`:
  when quality-tier routing (or a deprecated-model reroute) serves a model the
  caller did not name, the rerouted profile carries that marker, which
  `_stamp_offering_usage` writes to `TokenUsage.offering_route` → `cx_request`.
  So the three ways we can call something other than what was asked for —
  `sibling_fallback` (overload reroute), `tier_reroute` (this), and guest
  downgrade — are all recorded on the row, not just in a console banner nobody
  reads. Approved by Arman 2026-08-10 after the silent-reroute incident below.

- **2026-08-10** — TTS quality-tier data fix + segmented-usage fix. The
  `high_quality` tier tag sat ONLY on `gemini-3.1-flash-tts-preview` while
  `gemini-2.5-pro-preview-tts` carried empty tiers — so a caller requesting
  `tts_quality="high_quality"` on an agent pinned to 2.5 PRO was REROUTED to
  flash by `resolve_tts_call_profile` (tier not in the pinned offering's tiers
  → catalog selection → only flash matched). Every 1–2 host podcast silently
  downgraded pro→flash, and flash rendered a correct 2-speaker config as one
  voice. Live metadata now maps `high_quality`→2.5 pro, `fast`→3.1 flash
  (default). **The tier tags on `ai.offering.metadata.tts.quality_tiers` ARE
  the routing — a tier tag on the wrong offering silently reroutes every pinned
  model that requests that tier.** Also: the Google translator now SUMS usage
  across segmented-TTS provider calls (`from_google_async` collects one
  `usage_metadata` per segment) — previously only the final segment was
  recorded, under-billing every long episode (~131 tokens for a full podcast).

- **2026-07-28** — Documented Rules 1 and 2 after the `xhigh`-vs-`high` false alarm. The live DB was
  correct the whole time: every base Claude listing carries `effort_ceiling="high"`, every `-max`
  carries none. The `anthropic_adaptive__v2` golden had been snapshotted from `claude-opus-4-6`
  *before* that model gained its ceiling, so the parity validator screamed at correct data for two
  weeks. Golden regenerated (Section B: 0 diffs, all families); `anthropic_adaptive__v2` correctly
  dedupes away now that opus-4-6 matches the base fixture. Also added the live-api guard to the
  dumper (above). **Open cost finding:** nearly every base chat model still has
  `reasoning_effort.default = null`, so the provider's expensive default applies — the house default
  is unset, which Rule 1 says it must not be.
