models

The model catalog: the models a picker offers for each provider, and what each provider’s own listing adds.

The catalog

CATALOG maps each provider to the model ids a picker offers before anything is asked over the network, in display order.

  • claude is what Claude Code’s --model accepts: the tier names fable, opus, sonnet and haiku, which follow the newest model of each family, then the full ids.
  • anthropic is the same models through the Anthropic API.
  • openai and gemini are the models offered through those APIs.
  • codex is what Codex’s own picker lists.

claude_ids returns the full Claude ids without the tier names. claude_aliases maps each tier name to the id it means here, which is the last id of that family in CATALOG. Adding a newer Opus to the end of the list moves opus to it.


source

claude_aliases

def claude_aliases():

Each Claude Code tier alias and the concrete id it tracks here: the family’s last, so newest, in CATALOG.


source

claude_ids

def claude_ids():

The concrete Claude Code ids in CATALOG, without the tier aliases.

test_eq(claude_aliases()['fable'], 'claude-fable-5-1')
test_eq(claude_aliases()['opus'], 'claude-opus-5-5')
assert 'claude-haiku-5-5' in claude_ids() and 'claude-haiku-5-5' in CATALOG['anthropic']
test_eq(claude_aliases()['haiku'], 'claude-haiku-5-5')
assert 'claude-sonnet-5-5' in claude_ids() and claude_aliases()['sonnet'] == 'claude-sonnet-5-5'
assert 'gpt-6.1-sol' in CATALOG['openai'] and CATALOG['openai'][0] == 'gpt-6-astra'
test_eq(set(claude_aliases()), {'fable', 'opus', 'sonnet', 'haiku'})
for p, ids in CATALOG.items(): test_eq(len(ids), len(set(ids))), p

Live discovery

Each provider that can be asked for its models has one function in LIVE. The function returns the ids the provider lists, or [] when it cannot ask: no key, no file, no network, or an answer it cannot read. It never raises.

  • openai_ids asks OpenAI’s /v1/models with $OPENAI_API_KEY.
  • anthropic_ids asks Anthropic’s /v1/models with $ANTHROPIC_API_KEY. It serves both anthropic and claude, since Claude Code runs the same models.
  • codex_ids asks no server. Codex keeps its picker list in models_cache.json under $CODEX_HOME, which defaults to ~/.codex, and the entries marked visibility: 'list' are the ones it offers. The test below writes a small cache file and reads it back.

source

codex_ids

def codex_ids():

What Codex’s own picker lists, from its models cache, or [].


source

anthropic_ids

def anthropic_ids():

Every id the current Anthropic key can list, or [] when it cannot be asked.


source

openai_ids

def openai_ids():

Every id the current OpenAI key can list, or [] when it cannot be asked.

import json, os, tempfile
with tempfile.TemporaryDirectory() as d:
    cache = {'models': [{'slug': 'gpt-6-astra', 'visibility': 'list'}, {'slug': 'gpt-reserve', 'visibility': 'hide'}, {'slug': 'codex-auto-review', 'visibility': 'hide'}]}
    (Path(d)/'models_cache.json').write_text(json.dumps(cache))
    _old = os.environ.get('CODEX_HOME')
    os.environ['CODEX_HOME'] = d
    try: test_eq(codex_ids(), ['gpt-6-astra'])
    finally:
        if _old is None: del os.environ['CODEX_HOME']
        else: os.environ['CODEX_HOME'] = _old
    os.environ['CODEX_HOME'] = str(Path(d)/'nowhere')
    try: test_eq(codex_ids(), [])
    finally:
        if _old is None: del os.environ['CODEX_HOME']
        else: os.environ['CODEX_HOME'] = _old

What a live listing may add

A provider’s listing holds everything it serves: dated snapshots, fine-tunes, realtime and audio models, embeddings, moderation, image and search models. A picker wants none of these. _current decides which listed ids a picker may show. It hides an id by what kind of model it is, not by which generation it belongs to, so a new generation shows up the day the provider lists it. Older chat generations (gpt-4*, gpt-3.5*, claude-2-* and claude-3-*) appear only with legacy=True. The cell below lists ids it keeps, ids it always hides, and ids it shows only with legacy.

for ok in ('gpt-6-astra', 'gpt-7', 'o5-mini', 'claude-fable-5-1', 'claude-haiku-4-5-20251001', 'gemini-3.5-flash'): assert _current('openai', ok), ok
for hidden in ('gpt-6-astra-2026-09-01', 'gpt-3.5-turbo-0125', 'ft:gpt-3.5-turbo-0125:org::abc', 'ada:ft-org-2023-04-06-20-44-19', 'gpt-realtime', 'gpt-6-audio', 'gpt-image-2', 'gpt-6-transcribe', 'tts-1', 'gpt-live-1', 'gpt-3.5-turbo-instruct', 'text-embedding-4', 'omni-moderation-latest', 'dall-e-3', 'whisper-1', 'davinci-002', 'babbage-002', 'sora-2', 'computer-use-preview', 'gpt-5-search-api', 'gpt-4o-search-preview', 'o3-deep-research', 'chat-latest', ''):
    assert not _current('openai', hidden), hidden
    assert not _current('openai', hidden, legacy=True), hidden
for old in ('gpt-4.1', 'gpt-4o', 'gpt-3.5-turbo', 'claude-3-5-haiku-latest'):
    assert not _current('openai', old) and _current('openai', old, legacy=True), old

What a picker offers

provider_models(p) is the list a picker shows for provider p. It starts with the CATALOG ids in their order, then adds the ids from the live listing that _current admits and that are not dated snapshots, newest first by version number, with nothing repeated. The live answer is remembered for five minutes with ramabana.core.probed. If the listing fails, the result is the catalog alone. A provider that is neither in CATALOG nor in LIVE gives [].

The first cell below stands in a fake OpenAI listing. The second makes every listing fail. The third fakes Anthropic’s endpoint and shows that claude and anthropic share one request.


source

provider_models

def provider_models(
    p, legacy:bool=False, ttl:int=300
):

Ids a picker offers for provider p: the curated ones in their order, then what live discovery adds.

from ramabana.core import forget_probes
_orig = dict(LIVE)
forget_probes()
LIVE['openai'] = lambda: ['gpt-6-astra', 'gpt-6-nova', 'gpt-6-nova-2026-09-01', 'gpt-6-audio', 'gpt-4.1']
try:
    got = provider_models('openai')
    test_eq(got[:len(CATALOG['openai'])], list(CATALOG['openai']))
    test_eq(got[len(CATALOG['openai']):], ['gpt-6-nova'])
    test_eq(got.count('gpt-6-astra'), 1)
    assert 'gpt-4.1' in provider_models('openai', legacy=True)
    # newest first, by the numbers: alphabetical put gpt-6.1 after every gpt-5
    forget_probes(); LIVE['openai'] = lambda: ['gpt-5.1', 'gpt-6.1-nova', 'gpt-5.10']
    test_eq(provider_models('openai')[len(CATALOG['openai']):], ['gpt-6.1-nova', 'gpt-5.10', 'gpt-5.1'])
finally: LIVE.clear(); LIVE.update(_orig); forget_probes()
def _boom(): raise OSError('no network')
forget_probes()
LIVE.update({p: _boom for p in LIVE})
try:
    for p in CATALOG: test_eq(provider_models(p)[:len(CATALOG[p])], list(CATALOG[p]))
finally: LIVE.clear(); LIVE.update(_orig); forget_probes()
test_eq(provider_models('nobody'), [])
_n, _key, _lst = [], os.environ.get('ANTHROPIC_API_KEY'), _listed
def _fake(*a, **kw): _n.append(1); return ['claude-sonnet-6', 'claude-sonnet-6-20270101', 'claude-sonnet-5-5']
forget_probes(); os.environ['ANTHROPIC_API_KEY'] = 'k'; _listed = _fake
try:
    got = provider_models('claude')
    test_eq(got[:len(CATALOG['claude'])], list(CATALOG['claude']))
    test_eq(got[len(CATALOG['claude']):], ['claude-sonnet-6'])
    assert 'claude-sonnet-6' in provider_models('anthropic') and len(_n) == 1
    forget_probes(); del os.environ['ANTHROPIC_API_KEY']
    test_eq(provider_models('claude'), list(CATALOG['claude']))
finally:
    _listed = _lst; forget_probes()
    if _key is None: os.environ.pop('ANTHROPIC_API_KEY', None)
    else: os.environ['ANTHROPIC_API_KEY'] = _key

Through available_models

ramabana.core.available_models builds its hosted rows from provider_models, and includes a provider only when runtime_available and auth_status say it can be used here. Each row has a value, label, provider and source. With a fake OpenAI key and listing, the OpenAI rows are the catalog followed by the one new model:

import ramabana.models as _m
from ramabana.core import available_models, CLAUDE, CLAUDE_ALIASES, CLAUDE_MODELS
_orig_live, _key = dict(_m.LIVE), os.environ.get('OPENAI_API_KEY')
forget_probes()
_m.LIVE.update({'openai': lambda: ['gpt-6-astra', 'gpt-6-nova', 'gpt-6-nova-2026-09-01', 'gpt-6-audio'], 'anthropic': lambda: [], 'codex': lambda: []})
os.environ['OPENAI_API_KEY'] = 'sk-not-a-real-key'
try:
    rows = available_models()
    test_eq({tuple(sorted(r)) for r in rows}, {('label', 'provider', 'source', 'value')})
    openai = [r['label'] for r in rows if r['provider'] == 'openai']
    test_eq(openai, [*CATALOG['openai'], 'gpt-6-nova'])
    test_eq([r['value'] for r in rows if r['provider'] == 'openai'][0], 'openai/gpt-6-astra')
finally:
    _m.LIVE.clear(); _m.LIVE.update(_orig_live); forget_probes()
    if _key is None: del os.environ['OPENAI_API_KEY']
    else: os.environ['OPENAI_API_KEY'] = _key
test_eq(CLAUDE_MODELS, claude_ids())
assert {'claude-fable-5-1', 'claude-opus-5-5'} <= set(CLAUDE_MODELS)
test_eq(CLAUDE_ALIASES['fable'], 'claude-fable-5-1')
test_eq(CLAUDE['claude/opus'], 'claude-opus-5-5')