fix: model picker search matches OpenRouter display names (#7228)
This commit is contained in:
1 parent
e168b67e42
commit
ec4bac9fc9
3 files changed
+101
-2
No files matched your search
+30
-1
@@ -2247,6 +2247,29 @@ def _model_matches_picker_selection(
|
|||||||
return not selected_provider or not candidate_provider or selected_provider == candidate_provider
|
return not selected_provider or not candidate_provider or selected_provider == candidate_provider
|
||||||
|
|
||||||
|
|
||||||
|
def _openrouter_model_display_name(model_id: str) -> str:
|
||||||
|
"""Return the OpenRouter display name (e.g. ``Ox Alpha``) for *model_id*.
|
||||||
|
|
||||||
|
Reads only the local shared metadata disk cache written by hermes-agent
|
||||||
|
(``cache/openrouter_model_metadata.json``) — never touches the network.
|
||||||
|
Falls back to the raw id when the model is unknown or the cache is
|
||||||
|
unavailable, so picker rows are always populated (#7228).
|
||||||
|
"""
|
||||||
|
if not model_id:
|
||||||
|
return model_id
|
||||||
|
try:
|
||||||
|
from agent.model_metadata import _load_model_metadata_disk_cache
|
||||||
|
|
||||||
|
cache = _load_model_metadata_disk_cache() or {}
|
||||||
|
except Exception:
|
||||||
|
return model_id
|
||||||
|
entry = cache.get(model_id)
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return model_id
|
||||||
|
name = str(entry.get("name") or "").strip()
|
||||||
|
return name or model_id
|
||||||
|
|
||||||
|
|
||||||
def _split_picker_overflow_models(
|
def _split_picker_overflow_models(
|
||||||
ordered_models: list[dict],
|
ordered_models: list[dict],
|
||||||
*,
|
*,
|
||||||
@@ -7790,7 +7813,13 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
|
|||||||
for mid, _desc in live_curated:
|
for mid, _desc in live_curated:
|
||||||
if mid and mid not in seen_ids:
|
if mid and mid not in seen_ids:
|
||||||
seen_ids.add(mid)
|
seen_ids.add(mid)
|
||||||
raw_models.append({"id": mid, "label": mid})
|
# Ship the friendly display name (e.g. "Ox Alpha")
|
||||||
|
# from the local OpenRouter metadata cache instead
|
||||||
|
# of the raw id, so the picker search matches what
|
||||||
|
# users see in Hermes Desktop (#7228).
|
||||||
|
raw_models.append(
|
||||||
|
{"id": mid, "label": _openrouter_model_display_name(mid)}
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to load OpenRouter curated catalog from hermes_cli")
|
logger.warning("Failed to load OpenRouter curated catalog from hermes_cli")
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -4572,10 +4572,16 @@ function renderModelDropdown(){
|
|||||||
else if(_prevHasSearch){ for(const k in _groupOpenState) delete _groupOpenState[k]; }
|
else if(_prevHasSearch){ for(const k in _groupOpenState) delete _groupOpenState[k]; }
|
||||||
_prevHasSearch=hasSearch;
|
_prevHasSearch=hasSearch;
|
||||||
const found=new Set();
|
const found=new Set();
|
||||||
|
// Fold whitespace/hyphens/dots on both sides so "ox alpha", "ox-alpha" and
|
||||||
|
// "ox.alpha" all match the same model (OpenRouter display names use spaces,
|
||||||
|
// ids use slashes/hyphens) (#7228).
|
||||||
|
const _foldModelSearch=(s)=>String(s||'').toLowerCase().replace(/[\s._-]+/g,'');
|
||||||
|
const foldTerm=_foldModelSearch(term);
|
||||||
for(const m of _modelData){
|
for(const m of _modelData){
|
||||||
const name=m.name.toLowerCase();
|
const name=m.name.toLowerCase();
|
||||||
const id=m.id.toLowerCase();
|
const id=m.id.toLowerCase();
|
||||||
if(name.includes(term)||id.includes(term)){
|
if(name.includes(term)||id.includes(term)
|
||||||
|
||_foldModelSearch(name).includes(foldTerm)||_foldModelSearch(id).includes(foldTerm)){
|
||||||
found.add(m.value);
|
found.add(m.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Regression check for #7228 — model-picker search vs OpenRouter display names.
|
||||||
|
|
||||||
|
Composer model-picker search was a literal substring on the rendered name and
|
||||||
|
id. OpenRouter overflow rows were labeled with the raw id (``stealth/ox-alpha``)
|
||||||
|
instead of the provider display name (``Ox Alpha``), and spaces were not
|
||||||
|
normalized to hyphens — so typing the name users see in Hermes Desktop
|
||||||
|
(``Ox Alpha``) always yielded "No models found".
|
||||||
|
|
||||||
|
Fixed in three layers (issue requirement):
|
||||||
|
1. backend: /api/models ships the friendly display name from the local
|
||||||
|
OpenRouter metadata disk cache for curated catalog rows;
|
||||||
|
2. frontend: getModelLabel() resolves via the dynamic label map, which is
|
||||||
|
now hydrated with the display name;
|
||||||
|
3. frontend: search folds whitespace/hyphens/dots on both sides so
|
||||||
|
``ox alpha`` == ``ox-alpha`` == ``ox.alpha``.
|
||||||
|
|
||||||
|
Verified at the source level so this stays fast.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).parent.parent
|
||||||
|
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
|
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_has_openrouter_display_name_helper():
|
||||||
|
# The helper must read only the local disk cache (no network).
|
||||||
|
assert "def _openrouter_model_display_name(model_id: str) -> str:" in CONFIG_PY
|
||||||
|
assert "_load_model_metadata_disk_cache" in CONFIG_PY
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_ships_display_name_not_raw_id():
|
||||||
|
# Curated OpenRouter catalog rows must use the friendly display name
|
||||||
|
# instead of the raw id as the picker label (#7228).
|
||||||
|
snippet = CONFIG_PY[CONFIG_PY.index('fetch_openrouter_models as _fetch_or_models'):]
|
||||||
|
snippet = snippet[:snippet.index('except Exception')]
|
||||||
|
assert "_openrouter_model_display_name(mid)" in snippet
|
||||||
|
assert '{"id": mid, "label": mid}' not in snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_models_folds_space_hyphen_dot():
|
||||||
|
# Search must fold whitespace/hyphens/dots on both sides so display names
|
||||||
|
# with spaces match ids with hyphens ("ox alpha" == "ox-alpha").
|
||||||
|
assert "replace(/[\\s._-]+/g,'')" in UI_JS
|
||||||
|
assert "_foldModelSearch(name).includes(foldTerm)" in UI_JS
|
||||||
|
assert "_foldModelSearch(id).includes(foldTerm)" in UI_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_model_label_uses_dynamic_map_first():
|
||||||
|
# getModelLabel() must resolve through the dynamic label map, which is
|
||||||
|
# hydrated from m.label / overflowModel.label (now the display name).
|
||||||
|
idx = UI_JS.index("function getModelLabel(modelId){")
|
||||||
|
# The dynamic-map lookup must appear before the static-label fallback,
|
||||||
|
# so backend display names win over the hardcoded table.
|
||||||
|
assert "_dynamicModelLabels[modelId]" in UI_JS[idx:idx + 900]
|
||||||
|
assert UI_JS[idx:idx + 900].index("_dynamicModelLabels[modelId]") < \
|
||||||
|
(UI_JS[idx:idx + 900].index("STATIC_LABELS") if "STATIC_LABELS" in UI_JS[idx:idx + 900] else 10**9)
|
||||||
|
# The dynamic map must be populated from the backend label, not the raw id.
|
||||||
|
assert "_dynamicModelLabels[m.id]=m.label||m.id" in UI_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_literal_substring_match_still_present():
|
||||||
|
# Raw-id search ("stealth", "stealth/ox-alpha") must keep working.
|
||||||
|
assert "name.includes(term)||id.includes(term)" in UI_JS
|
||||||
Reference in new issue
Block a user