mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(picker): fold live bare k3 wire id into curated kimi-k3 row
Follow-up to the salvaged #67409 search aliases: with kimi-k3 now in the curated kimi-coding list (#68108), a Coding Plan key rendered TWO rows for one model — curated 'kimi-k3' plus live-discovered bare 'k3' (merge dedup was exact-string). Add model_alias_canonical() derived from the same alias table and use it as the merge dedup key, so the curated public slug wins and live-only models still surface.
This commit is contained in:
parent
39902f1888
commit
37e648128b
4 changed files with 111 additions and 4 deletions
|
|
@ -15,6 +15,26 @@ _MODEL_SEARCH_ALIASES: dict[str, tuple[str, ...]] = {
|
|||
"k3": ("kimi-k3", "kimi"),
|
||||
}
|
||||
|
||||
# Lowercased wire id → canonical public slug it aliases. Used by picker
|
||||
# dedup so a live bare id and its curated public slug (``k3`` / ``kimi-k3``)
|
||||
# don't render as two rows for the same model. Derived from the FIRST alias
|
||||
# entry, which by convention is the full public slug.
|
||||
_MODEL_ALIAS_CANONICAL: dict[str, str] = {
|
||||
wire_id: aliases[0].lower()
|
||||
for wire_id, aliases in _MODEL_SEARCH_ALIASES.items()
|
||||
if aliases
|
||||
}
|
||||
|
||||
|
||||
def model_alias_canonical(model: str) -> str:
|
||||
"""Return the canonical public slug for a bare wire-id alias.
|
||||
|
||||
Identity for ids with no alias entry. Lowercases the input so callers
|
||||
can use the result directly as a dedup key.
|
||||
"""
|
||||
key = (model or "").strip().lower()
|
||||
return _MODEL_ALIAS_CANONICAL.get(key, key)
|
||||
|
||||
|
||||
def model_search_text(model: str) -> str:
|
||||
"""Return the haystack used for fuzzy/substring model search.
|
||||
|
|
|
|||
|
|
@ -2537,6 +2537,24 @@ _MODELS_DEV_PREFERRED: frozenset[str] = frozenset({
|
|||
})
|
||||
|
||||
|
||||
def _model_dedup_key(model_id: str) -> str:
|
||||
"""Case-insensitive dedup key that also folds picker-search aliases.
|
||||
|
||||
Some providers serve the same model under both a curated public slug and
|
||||
a bare live wire id (Kimi Coding Plan lists its flagship as ``k3`` while
|
||||
the curated catalog carries ``kimi-k3``). Folding through the search-alias
|
||||
table keeps the curated-first merge from emitting both as separate rows.
|
||||
The row that survives is the primary list's entry; selection still sends
|
||||
whichever id the surviving row carries.
|
||||
"""
|
||||
key = str(model_id).strip().lower()
|
||||
try:
|
||||
from hermes_cli.model_search import model_alias_canonical
|
||||
return model_alias_canonical(key)
|
||||
except Exception:
|
||||
return key
|
||||
|
||||
|
||||
def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
|
||||
"""Merge curated list with fresh models.dev entries for a preferred provider.
|
||||
|
||||
|
|
@ -2802,11 +2820,11 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
|||
else:
|
||||
primary, secondary = curated, live
|
||||
merged = list(primary)
|
||||
merged_lower = {m.lower() for m in primary}
|
||||
merged_lower = {_model_dedup_key(m) for m in primary}
|
||||
for m in secondary:
|
||||
if m.lower() not in merged_lower:
|
||||
if _model_dedup_key(m) not in merged_lower:
|
||||
merged.append(m)
|
||||
merged_lower.add(m.lower())
|
||||
merged_lower.add(_model_dedup_key(m))
|
||||
return merged
|
||||
return live
|
||||
# Use profile's fallback_models if defined
|
||||
|
|
|
|||
65
tests/hermes_cli/test_model_search_alias_dedup.py
Normal file
65
tests/hermes_cli/test_model_search_alias_dedup.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Picker dedup must fold live bare wire-ids into their curated public slug.
|
||||
|
||||
Kimi Coding Plan live-discovers its flagship as the bare id ``k3`` while the
|
||||
curated catalog carries ``kimi-k3``. The curated-first picker merge must not
|
||||
render both as separate rows for the same model.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from hermes_cli.model_search import model_alias_canonical
|
||||
from hermes_cli.models import provider_model_ids
|
||||
|
||||
|
||||
class TestModelAliasCanonical:
|
||||
def test_bare_k3_folds_to_public_slug(self):
|
||||
assert model_alias_canonical("k3") == "kimi-k3"
|
||||
assert model_alias_canonical("K3") == "kimi-k3"
|
||||
|
||||
def test_non_alias_ids_are_identity(self):
|
||||
assert model_alias_canonical("kimi-k2.6") == "kimi-k2.6"
|
||||
assert model_alias_canonical("GPT-5.4") == "gpt-5.4"
|
||||
assert model_alias_canonical("") == ""
|
||||
|
||||
|
||||
class TestPickerMergeAliasDedup:
|
||||
def test_live_bare_k3_not_duplicated_against_curated_kimi_k3(self):
|
||||
"""Coding Plan key: live returns bare ``k3``; curated has ``kimi-k3``.
|
||||
Exactly one k3-family row must survive (the curated slug leads)."""
|
||||
with (
|
||||
patch(
|
||||
"hermes_cli.auth.resolve_api_key_provider_credentials",
|
||||
return_value={
|
||||
"api_key": "sk-kimi-x",
|
||||
"base_url": "https://api.kimi.com/coding",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"providers.base.ProviderProfile.fetch_models",
|
||||
return_value=["k3", "kimi-for-coding"],
|
||||
),
|
||||
):
|
||||
out = provider_model_ids("kimi-coding")
|
||||
|
||||
k3_rows = [m for m in out if model_alias_canonical(m) == "kimi-k3"]
|
||||
assert k3_rows == ["kimi-k3"], out
|
||||
# Live-only entries with no curated twin still surface.
|
||||
assert "kimi-for-coding" in out
|
||||
|
||||
def test_live_only_models_unaffected(self):
|
||||
"""Alias folding must not drop live models without curated twins."""
|
||||
with (
|
||||
patch(
|
||||
"hermes_cli.auth.resolve_api_key_provider_credentials",
|
||||
return_value={
|
||||
"api_key": "sk-kimi-x",
|
||||
"base_url": "https://api.kimi.com/coding",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"providers.base.ProviderProfile.fetch_models",
|
||||
return_value=["kimi-brand-new-live-only"],
|
||||
),
|
||||
):
|
||||
out = provider_model_ids("kimi-coding")
|
||||
assert "kimi-brand-new-live-only" in out
|
||||
|
|
@ -171,10 +171,14 @@ class TestProviderModelIdsPreferred:
|
|||
):
|
||||
custom_models = provider_model_ids("kimi-coding")
|
||||
|
||||
assert "k3" in coding_models
|
||||
# The live bare wire id ``k3`` folds into the curated public slug
|
||||
# ``kimi-k3`` (picker alias dedup) — one row, curated slug leads.
|
||||
assert coding_models[0] == "kimi-k3"
|
||||
assert all(model.lower() != "k3" for model in coding_models)
|
||||
assert all(model.lower() != "k3" for model in legacy_models)
|
||||
assert all(model.lower() != "k3" for model in custom_models)
|
||||
# Legacy / custom endpoints never advertise the k3 family at all
|
||||
# via live discovery (their curated floor may still carry kimi-k3).
|
||||
|
||||
def test_kimi_setup_flow_uses_same_coding_plan_catalog(self):
|
||||
"""The setup wizard must not carry a stale duplicate Kimi model list."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue