Originally created by: sosidudku1
Reported internally: the cloud model picker offers only a handful of models. Against the live APIs today the picker shows 12 of 305 usable OpenRouter models and 13 of 337 aimlapi models.
Three independent causes.
MAX_PICKS cut the list to 12 (OpenRouter) and 32 (aimlapi) after the curated ids. That cap made sense for an unfilterable list; it silently hides most of the catalog. Both caps are gone. Curated ids keep their hand-picked order, the OpenRouter tail keeps its score order, and the aimlapi tail is now sorted alphabetically instead of following map insertion order.
Two breaking changes on their side, neither of which failed loudly:
openai/chat-completions instead of chat-completion, so the equality check matched zero rows;features array that advertised tool support was removed entirely, so the "can this model call tools" check rejected everything that survived.The net effect was a live refresh that always returned nothing and fell back to the 13-entry offline list, which also explains the price unknown labels in the picker.
Type matching is now suffix-based (*/chat-completions, plus the older spellings), which survives another vendor-prefix rename while still excluding the video, image, anthropic/messages, and responses/submit families that share the same payload.
Both fetchers now distinguish "the API says this model has no tools" from "the API says nothing". Only the former drops a model. This is the generic form of the aimlapi breakage: a provider dropping a capability field can no longer empty our catalog. OpenRouter has the same guard even though its supported_parameters is present today.
The second commit addresses review findings. The 12-row viewport around the cursor now lives in renderPickList itself, so the OpenRouter and aimlapi picker phases window their rows instead of painting the full 300+ row catalog into the terminal, with a position counter once the list outgrows the window. Both fetchers skip null and scalar rows in data, so one bad row can no longer throw and drag the whole live catalog into the static fallback. The fallback tests now reset the module-level cache (vi.resetModules plus a dynamic import) and assert the exact offline list, so they test the fallback rather than a leftover cache. The stale JSDoc describing the removed type/features filter is rewritten, and a comment explains why vision silence deliberately still reads as "no vision".
Ran the parsers against the live payloads captured from both providers: 337 aimlapi picks (was 13) and 305 OpenRouter picks (was 12).
Tests: 16 new across both commits. Cap removal on both providers, offline fallback on network error and on a malformed payload (against a fresh module cache), null rows skipped, the current aimlapi shape verbatim (including the non-chat families that must stay out), explicit no-tools still dropped, silent-API models kept, rows without a usable id ignored, alphabetical tail ordering, and 12-row windowing of 300+ model lists in every wizard picker phase. tsc clean. Full suite shows the same pre-existing failures as main.
Full catalog does not mean every id the providers return: scoreChat keeps the deliberate exclusions of anthropic/* and /gemini/i, and the catalog tests pin both. The recorded reason is the tool transport, not model preference. The cloud path drives models through the OpenAI chat-completions wire shape with native tools, and the adapter seam explicitly marks Anthropic and Gemini adapters as future work (src/llm/provider/adapters/tool-call-adapter.ts, since 5c7c05e). Claude additionally sits on a different surface at aimlapi: its rows are typed anthropic/messages and 404 on /v1/chat/completions. Gemini started out score-boosted and was removed in c0a73b4 together with the recovery handling for cloud models that emit tool calls as plain content, which points at tool-call reliability through the OpenAI-shape adapter. So this PR ships the full set of models the runtime can actually drive today: full minus Claude and Gemini. When dedicated adapters land, lifting the two filters should be part of that change.
Until the picker gains text filtering, these lists are long. That work is the companion branch; merging this one first is fine, since the wizard picker now windows its rows too.
Originally posted by: sosidudku1
Thanks for the thorough pass. All addressed in the new commit:
A) Entry resolution is an id-to-entry Map now, keyed on the picks array reference and rebuilt only when a refresh swaps the array (the offline fallback is memoized so its reference is stable too). On top of that, option labels are lazy getters, so only the 12 rows the viewport paints get formatted; key handling reads ids and lengths only. Same effect as passing an offset, without the signature churn.
B)
renderPickList+PICK_WINDOWmoved tosrc/tui/components/wizard-pick-list.tsx. While splitting I foundproviders-wizard-key-bindings.tswas over the cap too (335), so the phase helpers moved toproviders-wizard-phases.ts; every touched file is back under 300.C) Both regressions fixed. The counter is unconditional again and sits right after the movement keys (
j/k move (5/30) · ...), pinned by a test on the 3-row kind list. The cursor clamps in state via a sharedclampCursorused by movement, page jumps and selection; a test shrinks the catalog under cursor 25 and asserts Enter picks the highlighted last row. The wizard has no list-changed event (the cache swaps in the background), so clamping at every key event is the reducer-level equivalent.D) PgUp/PgDn jump one
PICK_WINDOWand Home/End hit the edges, both in the cloud pick phases and in the openai-compatible discovered list, with tests for the jumps, the tail clamp, andjstaying a printable character in the compat list. Typed filtering for the wizard pickers is the next PR in this series.E) Comment rewritten to say what silence actually costs:
supportsVisionis the capability bit behindProviderCapabilities.vision, which turnsvision.describeintoVisionUnsupportedErrorfor live-only ids, not a hidden badge. Ontags: I pulled the live/v1/modelspayload; across all 337 chat rows tags carry onlyplayground:*grouping andtier:*pricing labels, and known-vision rows (the gpt-4o family) have no vision marker, so there is no capability signal to derive. The field was unread, so it is removed, with the finding documented next tofeatures.F) Agreed, the PR description now carries a scope note (added above in the description).
Unrelated heads-up:
llm-panel-selectors.test.tsfails on main too (static qwen3.7-max pricing is 1.25/3.75, the test expects $2.5/$7.5); same before and after this branch.Ticket changed by: Ooooze