Files
airship/scripts/models.curation.json
T
Nayan 7949bcb02e feat(core): let every backend answer for the models it can run
The picker needs a list, and the three backends disagree about whether they have
one. Claude answers `query.supportedModels()` and OpenCode answers
`client.config.providers()`, both live and both scoped to what you are actually
signed in to. Codex can enumerate nothing, at any layer.

So `listModels` probes the two that can and falls back to a seed for the one
that cannot, and nothing it does throws: a picker that cannot list models is a
degraded menu, and one that takes the session down with it is a bug. Every
failure comes back as a group carrying a `note` that says which — "Not signed
in" and "Probe timed out" are different problems, and only one of them is the
user's to fix.

`packages/protocol/src/models.ts` is the seed, generated from models.dev by
`scripts/gen-models.mjs` and reached through its own `./models` export subpath
so the browser can take the list as a value without pulling zod along. Judgement
lives in `scripts/models.curation.json` rather than in the output — the
mechanical filter admits things like `gpt-realtime`, which is not a coding
model — so the generated module stays purely derived and the taste is what gets
reviewed.

On the daemon, `resolveTarget` settles which backend and which model a turn
runs on: what the turn asked for, else the default resolved for that backend,
else the cross-harness one. Per backend rather than one string, because the
picker can change harness mid-session and a single default would follow it and
hand Codex an id only Claude answers to.

`modelRefusal` is the other half. OpenCode drops a model id that does not name
its provider, which makes asking for one indistinguishable from asking for
nothing — the turn runs on the server's default while the composer goes on
showing what was picked. It reads the turn's own model and deliberately not the
resolved one: `--opencode-model` is already a hard error at parse time, and
`--model` reaches all three backends where a bare id is right for two of them,
so that one warns at launch and runs. What is left is the picker's free-text
box, which had no guard at either end.
2026-08-16 13:05:37 +05:30

49 lines
2.1 KiB
JSON

{
"$comment": [
"Hand-maintained curation for scripts/gen-models.mjs. The generator filters",
"models.dev mechanically; everything that needs judgement lives here, so the",
"generated file stays purely derived and this file is what gets reviewed.",
"",
"Run `make models:refresh` after editing."
],
"since": "2025-08-01",
"$since": "Release-date floor. Cuts o1/o3/o4, gpt-oss and the Claude 3.x/4.0 era without naming them one by one.",
"limit": 10,
"$limit": "Rows kept per harness from models.dev, newest first — `extra` below is pinned on top of these rather than counted against them, so claude ships 14. A picker is a menu, not a scroll: `popover-host.ts` caps and scrolls past roughly this many.",
"deny": [
"-chat-latest$",
"-instant$",
"^openai/gpt-realtime-",
"^openai/gpt-oss-",
"-deep-research$",
"-\\d{8}$"
],
"$deny": [
"Regexes matched against the models.dev id. In order:",
"chat-latest / instant — chat tiers, not agentic; they have no tool loop worth offering here.",
"gpt-realtime — voice; passes the tool_call+reasoning filter but is not a coding model.",
"gpt-oss — open weights, not served on the OpenAI API these harnesses call.",
"deep-research — a hosted research product, not a coding model.",
"-YYYYMMDD — models.dev lists both `claude-opus-4-5` and `claude-opus-4-5-20251101`.",
" The undated id is the better menu row and pins to the same weights."
],
"extra": {
"claude": [
{ "id": "opus", "label": "Opus", "hint": "latest" },
{ "id": "sonnet", "label": "Sonnet", "hint": "latest" },
{ "id": "haiku", "label": "Haiku", "hint": "latest" },
{ "id": "fable", "label": "Fable", "hint": "latest" }
]
},
"$extra": [
"Rows models.dev cannot know about, pinned above the derived ones.",
"`claude --model` accepts these aliases and resolves each to the current",
"model in its family, so they stay correct between refreshes in a way a",
"pinned id cannot. `codex -m` and opencode take no aliases, so they get none."
]
}