fix(desktop): GUI model picker shows curated Nous list in curated order

Two bugs made the GUI Nous model list diverge from the `hermes model` CLI picker:

1. Backend (model_switch.py): the Nous row in list_authenticated_providers
   fell through to cached_provider_model_ids("nous"), dumping the full live
   /v1/models catalog (~50 vendor-prefixed models, alphabetical). Now it uses
   the curated list AND applies the Portal free/paid recommendation union —
   exactly like _model_flow_nous in main.py — so newly-launched models such as
   stepfun/step-3.7-flash:free surface in curated order. Best-effort: falls
   back to the curated list alone if the Portal fetch fails.

2. Frontend (model-picker.tsx): cmdk's Command had shouldFilter on (default),
   which re-sorts items by fuzzy-match score (≈alphabetical) and ignores array
   order. Set shouldFilter={false} + own the search term and do an
   order-preserving substring filter, so the backend's curated order is shown
   verbatim.
This commit is contained in:
emozilla
2026-05-31 06:17:18 -04:00
parent 3c04527e1e
commit ac2e489074
2 changed files with 64 additions and 5 deletions
+27 -5
View File
@@ -42,6 +42,12 @@ export function ModelPickerDialog({
contentClassName
}: ModelPickerDialogProps) {
const [persistGlobal, setPersistGlobal] = useState(!sessionId)
// Own the search term so we can filter manually. cmdk's built-in
// shouldFilter reorders items by its fuzzy-match score (≈alphabetical with
// an empty query), which destroys the backend's curated order. We disable
// it and do a plain substring filter that preserves array order — matching
// the `hermes model` CLI picker, which shows the curated list verbatim.
const [search, setSearch] = useState('')
const modelOptions = useQuery({
queryKey: ['model-options', sessionId || 'global'],
@@ -88,8 +94,13 @@ export function ModelPickerDialog({
</DialogDescription>
</DialogHeader>
<Command className="rounded-none bg-card">
<CommandInput autoFocus placeholder="Filter providers and models..." />
<Command className="rounded-none bg-card" shouldFilter={false}>
<CommandInput
autoFocus
onValueChange={setSearch}
placeholder="Filter providers and models..."
value={search}
/>
<CommandList className="max-h-96">
{!loading && !error && <CommandEmpty>No models found.</CommandEmpty>}
<ModelResults
@@ -99,6 +110,7 @@ export function ModelPickerDialog({
loading={loading}
onSelectModel={selectModel}
providers={providers}
search={search}
/>
</CommandList>
</Command>
@@ -128,7 +140,8 @@ function ModelResults({
providers,
currentModel,
currentProvider,
onSelectModel
onSelectModel,
search
}: {
loading: boolean
error: string | null
@@ -136,6 +149,7 @@ function ModelResults({
currentModel: string
currentProvider: string
onSelectModel: (provider: ModelOptionProvider, model: string) => void
search: string
}) {
if (loading) {
return <LoadingResults />
@@ -155,10 +169,18 @@ function ModelResults({
return <div className="px-4 py-6 text-sm text-muted-foreground">No authenticated providers.</div>
}
const q = search.trim().toLowerCase()
const matches = (provider: ModelOptionProvider, model: string) =>
!q ||
model.toLowerCase().includes(q) ||
provider.name.toLowerCase().includes(q) ||
provider.slug.toLowerCase().includes(q)
return (
<>
{providers.map(provider => {
const models = provider.models ?? []
// Preserve the backend's curated order — filter in place, no re-sort.
const models = (provider.models ?? []).filter(m => matches(provider, m))
if (models.length === 0) {
return null
@@ -195,7 +217,7 @@ function ModelResults({
onSelectModel(provider, model)
}
}}
value={`${provider.name} ${provider.slug} ${model}`}
value={`${provider.slug}:${model}`}
>
<span className="min-w-0 flex-1 truncate">{model}</span>
{locked && <span className="shrink-0 text-[0.62rem] uppercase tracking-wide opacity-80">Pro</span>}