Skip to content

Model Picker — Python source

Filter every major model by context window, price, modality, and tier — sort by cost, context, or tokens-per-dollar to find the right model for the task.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""Model Picker — filter + rank the AI model catalog.

Language: Python (3.9+, standard library only)
Source:   CosmoDev polyglot showcase port of the Model Picker tool, ported from
          src/lib/modelPicker.ts (the canonical TypeScript implementation).
Live:     https://dev.cosmolabs.org/tools/model-picker
License:  display source — part of CosmoDev's polyglot tool pages.

Design goals:
  - Pure + deterministic; never raises.
  - Functionally equivalent to the TS reference: same inputs -> same outputs.
  - Self-contained: stdlib only (no pip packages).

Port notes: the TS lib imports ``listModels`` from src/lib/ai/models.ts and
lets ``models`` default to the bundled pricing snapshot
(src/data/ai-models.json). A dependency-free port cannot load that file, so
the filter step is inlined here and ``models`` is an explicit **keyword-only**
parameter on both entry points — Python cannot put a required parameter after
an optional one, and silently defaulting the data to ``[]`` would hide bugs.
Sort-key strings ("price" | "context" | "tokensPerDollar" | "released") keep
the TS spellings so the two APIs map 1:1.

Every sort places null/missing values last and breaks ties on id ascending,
so output order is fully deterministic for a given model list.
"""

from __future__ import annotations

import functools
from dataclasses import dataclass
from typing import Dict, List, Literal, Optional

__all__ = [
    "Tier",
    "SortKey",
    "AiModel",
    "ModelFilter",
    "Preset",
    "PRESETS",
    "list_models",
    "pick_models",
]

Tier = Literal["flagship", "balanced", "fast", "budget"]
SortKey = Literal["price", "context", "tokensPerDollar", "released"]


@dataclass
class AiModel:
    """One catalog entry. Fields mirror the TS ``AiModel`` interface; ``None``
    plays the role of TS ``null`` (unpriced / unknown).

    Required fields come first, then everything with a default, because
    dataclass fields without defaults cannot follow ones with defaults.
    """

    id: str
    name: str
    vendor: str
    family: str
    tier: Tier
    context_window: int
    max_output: int
    modalities: List[str]

    # USD per 1M tokens; None for open/unpriced models.
    input_per_m: Optional[float] = None
    output_per_m: Optional[float] = None
    cache_read_per_m: Optional[float] = None
    cache_write_per_m: Optional[float] = None
    knowledge_cutoff: Optional[str] = None
    # ISO date string; None when unknown.
    released: Optional[str] = None
    open_weights: bool = False
    reasoning: bool = False
    tool_call: bool = False


@dataclass
class ModelFilter:
    """Mirrors the TS ``ModelFilter`` interface: every field is optional, and
    ``None`` means "do not apply this constraint" (TS ``undefined``)."""

    vendor: Optional[str] = None
    tier: Optional[Tier] = None
    # Minimum usable context window in tokens.
    min_context: Optional[int] = None
    # Maximum input price (USD per 1M tokens); None-priced models are skipped.
    max_input_per_m: Optional[float] = None
    # Required input modality, e.g. 'image'.
    modality: Optional[str] = None
    # Case-insensitive substring match on id, name, and vendor.
    search: Optional[str] = None


@dataclass
class Preset:
    """A curated entry point: a filter + the sort that makes that filter useful."""

    filter: ModelFilter
    sort: SortKey


PRESETS: Dict[str, Preset] = {
    "long-context": Preset(ModelFilter(min_context=500_000), "context"),
    "cheap-bulk": Preset(ModelFilter(max_input_per_m=1), "price"),
    "flagship": Preset(ModelFilter(tier="flagship"), "tokensPerDollar"),
}


def list_models(models: List[AiModel], filter: Optional[ModelFilter] = None) -> List[AiModel]:
    """Filter the model list. Inline port of ``listModels()`` from
    src/lib/ai/models.ts: ``None`` prices never satisfy ``max_input_per_m``,
    and search lowercases both sides before the substring check."""
    f = filter or ModelFilter()
    q = f.search.lower() if f.search is not None else None
    out: List[AiModel] = []
    for m in models:
        if f.vendor is not None and m.vendor != f.vendor:
            continue
        if f.tier is not None and m.tier != f.tier:
            continue
        if f.min_context is not None and m.context_window < f.min_context:
            continue
        if f.max_input_per_m is not None and (
            m.input_per_m is None or m.input_per_m > f.max_input_per_m
        ):
            continue
        if f.modality is not None and f.modality not in m.modalities:
            continue
        if q is not None and q not in f"{m.id} {m.name} {m.vendor}".lower():
            continue
        out.append(m)
    return out


def _by_id(a: AiModel, b: AiModel) -> int:
    """id ascending — the shared stable tie-break for every sort."""
    if a.id < b.id:
        return -1
    if a.id > b.id:
        return 1
    return 0


def _cmp_price(a: AiModel, b: AiModel) -> int:
    if a.input_per_m is None and b.input_per_m is None:
        return _by_id(a, b)
    if a.input_per_m is None:
        return 1
    if b.input_per_m is None:
        return -1
    if a.input_per_m != b.input_per_m:
        return -1 if a.input_per_m < b.input_per_m else 1
    return _by_id(a, b)


def _cmp_context(a: AiModel, b: AiModel) -> int:
    if a.context_window != b.context_window:
        return -1 if a.context_window > b.context_window else 1
    return _by_id(a, b)


def _cmp_tokens_per_dollar(a: AiModel, b: AiModel) -> int:
    ta = None if a.output_per_m is None else 1_000_000 / a.output_per_m
    tb = None if b.output_per_m is None else 1_000_000 / b.output_per_m
    if ta is None and tb is None:
        return _by_id(a, b)
    if ta is None:
        return 1
    if tb is None:
        return -1
    if ta != tb:
        return 1 if ta < tb else -1  # more tokens per dollar first
    return _by_id(a, b)


def _cmp_released(a: AiModel, b: AiModel) -> int:
    if a.released is None and b.released is None:
        return _by_id(a, b)
    if a.released is None:
        return 1
    if b.released is None:
        return -1
    if a.released != b.released:
        return 1 if a.released < b.released else -1  # newest first
    return _by_id(a, b)


# Sort comparator per key: value order first, None last, id asc on ties.
_COMPARATORS = {
    "price": _cmp_price,
    "context": _cmp_context,
    "tokensPerDollar": _cmp_tokens_per_dollar,
    "released": _cmp_released,
}


def pick_models(
    filter: Optional[ModelFilter] = None,
    sort: SortKey = "price",
    *,
    models: List[AiModel],
) -> List[AiModel]:
    """Filter the model list, then sort it (see ``_COMPARATORS`` for the exact
    order). ``sorted`` is stable, so rows comparing equal — including fully
    identical duplicates — keep their input order, matching
    ``Array.prototype.sort`` in the TS original."""
    ranked = list_models(models, filter)
    ranked.sort(key=functools.cmp_to_key(_COMPARATORS[sort]))
    return ranked

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →