diff --git a/apps/desktop/src/components/model-picker.tsx b/apps/desktop/src/components/model-picker.tsx index a43f02bafe8..be0bf051d03 100644 --- a/apps/desktop/src/components/model-picker.tsx +++ b/apps/desktop/src/components/model-picker.tsx @@ -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({ - - + + {!loading && !error && No models found.} @@ -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 @@ -155,10 +169,18 @@ function ModelResults({ return
No authenticated providers.
} + 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}`} > {model} {locked && Pro} diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 5fb78612773..60d4d797666 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1359,6 +1359,43 @@ def list_authenticated_providers( model_ids = _ids if _ids else (curated.get(hermes_slug, []) or curated.get(pid, [])) except Exception: model_ids = curated.get(hermes_slug, []) or curated.get(pid, []) + elif hermes_slug == "nous": + # Nous serves a large live /v1/models catalog (vendor-prefixed + # models from many providers, returned alphabetically). The + # `hermes model` picker deliberately shows ONLY the curated agentic + # list — augmented with the Portal's free/paid recommendations so + # newly-launched models surface without a CLI release — in curated + # order. Mirror that exactly (see _model_flow_nous in main.py) so + # the GUI picker matches the CLI. Was: falling through to + # cached_provider_model_ids, which dumped the full alphabetical + # catalog; then: curated-only, which dropped the 4 Portal + # recommendations (e.g. stepfun/step-3.7-flash:free). + model_ids = curated.get("nous", []) + try: + from hermes_cli.models import ( + get_pricing_for_provider as _nous_pricing, + check_nous_free_tier as _nous_free, + union_with_portal_free_recommendations as _union_free, + union_with_portal_paid_recommendations as _union_paid, + ) + from hermes_cli.auth import get_provider_auth_state as _nous_state + + _pricing = _nous_pricing("nous") or {} + _portal = "" + try: + _st = _nous_state("nous") or {} + _portal = _st.get("portal_base_url", "") or "" + except Exception: + _portal = "" + if _nous_free(force_fresh=True): + model_ids, _ = _union_free(model_ids, _pricing, _portal) + else: + model_ids, _ = _union_paid(model_ids, _pricing, _portal) + except Exception: + # Portal recommendation fetch failed — fall back to the + # curated list alone (still correct, just may lag newly + # launched models, exactly like an offline CLI run). + pass else: # Unified pathway — see Section 1 rationale. Fall back to the # curated dict (with models.dev merge for preferred providers)