Source code for cadrumo.domain.transactions.model_tier

"""Model capability tiers for the LLM classifier.

Decouples consumer code from the shifting sands of model IDs at each
provider. The operator picks a *capability tier* (``LOW`` /
``MEDIUM`` / ``HIGH``) and a *provider* (``claude`` / ``antigravity`` /
``codex``); :func:`catalogue` returns the current
:class:`ModelProfile` whose ``model_id`` meets the tier floor for
that provider.

Rationale:

- Model IDs change every few months (``sonnet-4.5``, ``gemini-3-pro``,
  ``o3`` -> ``o4``). Hard-coding IDs in CLI flags forces consumers to
  chase them. A stable tier + alias table is a better interface.
- Thinking models (multi-step reasoning, chain-of-thought) classify
  ambiguous transactions more accurately than single-shot models but
  cost more tokens. Surfacing the :class:`ModelCapability` axis lets
  the operator pick a trade-off.
- Classification accuracy on realistic autónomo data is acceptable at
  :attr:`ModelTier.MEDIUM` and above; :attr:`ModelTier.LOW` models are
  prone to ignoring the strict JSON schema or picking the wrong
  classification on ambiguous inputs.
  :data:`MINIMUM_CLASSIFICATION_TIER` enforces this floor.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import IntEnum, StrEnum


[docs] class ModelTier(IntEnum): """Ordered capability tier. Comparison operators work as expected. Attributes: LOW: Cheap / fast models prone to schema drift on ambiguous inputs. MEDIUM: Solid single-shot reasoning; the floor for the classification pipeline. HIGH: Top-tier models with strong multi-step reasoning. """ LOW = 1 MEDIUM = 2 HIGH = 3
[docs] class ModelCapability(StrEnum): """Whether the model natively does multi-step reasoning before answering. Attributes: NON_THINKING: Single-shot models that emit one response without internal chain-of-thought passes. THINKING: Models that perform explicit multi-step reasoning before producing the final answer. """ NON_THINKING = "non_thinking" THINKING = "thinking"
[docs] @dataclass(frozen=True) class ModelProfile: """One concrete model at one provider, tagged with its capability tier. Attributes: provider: Lower-case provider name (``claude`` / ``antigravity`` / ``codex``). alias: Stable human-friendly identifier, e.g. ``claude-sonnet``. The operator uses this on the CLI; the tool maps it to ``model_id``. model_id: Current provider-specific model argument. MAY be empty when the provider CLI defaults to the right model (e.g. ``codex`` picks its own default). tier: Capability tier. capability: Thinking vs non-thinking. """ provider: str alias: str model_id: str tier: ModelTier capability: ModelCapability
# Minimum capability tier permitted for the classification pipeline. The # observable failure mode at ``LOW`` is schema drift (the model returns # prose instead of JSON, or picks a classification outside the allow-list) # on more than a handful of transactions per batch, which defeats the # confidence filter operator relies on downstream. MINIMUM_CLASSIFICATION_TIER: ModelTier = ModelTier.MEDIUM """Minimum :class:`ModelTier` permitted for the classification pipeline. The observable failure mode at :attr:`ModelTier.LOW` is schema drift (the model returns prose instead of JSON, or picks a classification outside the allow-list) on more than a handful of transactions per batch, which defeats the confidence filter downstream consumers rely on.""" # Per-provider catalogue of known models. Keep the list tight and prefer # the current default over exhaustive historical IDs — this is a floor # for operator's choices, not an archive. # # Tier placement rationale: # - claude-haiku, gpt-4o-mini: fast/cheap, ~LOW. # - claude-sonnet, antigravity default, gpt-4o, codex default: solid single-shot, MEDIUM. # - claude-opus, o3/o4, codex-high: top-tier reasoning, HIGH. _CATALOGUE: tuple[ModelProfile, ...] = ( # Claude ModelProfile( provider="claude", alias="claude-haiku", model_id="claude-haiku-4-5", tier=ModelTier.LOW, capability=ModelCapability.NON_THINKING, ), ModelProfile( provider="claude", alias="claude-sonnet", model_id="claude-sonnet-4-6", tier=ModelTier.MEDIUM, capability=ModelCapability.NON_THINKING, ), ModelProfile( provider="claude", alias="claude-opus", model_id="claude-opus-4-7", tier=ModelTier.HIGH, capability=ModelCapability.THINKING, ), # Antigravity (Google's agentic CLI ``agy``, the supported successor to the # retired standalone ``gemini`` CLI). The CLI selects its own current # default model; an empty ``model_id`` omits ``--model`` and lets ``agy`` # choose, mirroring the ``codex-default`` profile. Explicit model pinning is # a follow-up once ``agy models`` is queryable in CI. ModelProfile( provider="antigravity", alias="antigravity-default", model_id="", tier=ModelTier.MEDIUM, capability=ModelCapability.THINKING, ), # Codex — default is the agent's current top model; it does not accept # -m for most deployments. alias "codex-default" maps to empty model_id. ModelProfile( provider="codex", alias="codex-default", model_id="", tier=ModelTier.MEDIUM, capability=ModelCapability.THINKING, ), ModelProfile( provider="codex", alias="codex-o3", model_id="o3", tier=ModelTier.HIGH, capability=ModelCapability.THINKING, ), )
[docs] def catalogue() -> tuple[ModelProfile, ...]: """Return the full known-model catalogue. Returns: A tuple of every registered :class:`ModelProfile`, in the catalogue's declared order. """ return _CATALOGUE
__all__ = [ "MINIMUM_CLASSIFICATION_TIER", "ModelCapability", "ModelProfile", "ModelTier", "catalogue", ]