feat(model-picker): show short description on grouped provider rows
The 7 consolidated provider families (OpenAI, xAI Grok, GitHub Copilot, Google Gemini, Kimi / Moonshot, MiniMax, OpenCode) collapse to one top-level picker row. Previously that row showed only the bare group label (e.g. `OpenAI ▸`); now it carries a short blurb describing the endpoints folded inside (e.g. `OpenAI ▸ (Codex CLI or direct OpenAI API)`). - models.py: extend PROVIDER_GROUPS tuples to (label, description, members); group_providers() emits the description on group rows. - main.py: CLI picker renders `<label> ▸ (<description>)` for group rows. - telegram.py: update the group tuple unpack (button text keeps the member count, which fits inline keyboards better than a long blurb). - tests: assert every group has a non-empty description and the fold emits it. Member-specific detail still lives in each member's tui_desc and shows in the drill-down sub-picker. Slug identity, --provider, /model paths unchanged.
This commit is contained in:
@ -3126,7 +3126,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
||||
group_id = data[4:]
|
||||
try:
|
||||
from hermes_cli.models import PROVIDER_GROUPS
|
||||
_label, member_slugs = PROVIDER_GROUPS.get(group_id, ("", []))
|
||||
_label, _desc, member_slugs = PROVIDER_GROUPS.get(group_id, ("", "", []))
|
||||
except Exception:
|
||||
_label, member_slugs = "", []
|
||||
|
||||
|
||||
@ -2434,7 +2434,8 @@ def select_provider_and_model(args=None):
|
||||
for row in grouped_rows:
|
||||
if row["kind"] == "group":
|
||||
gid = row["group_id"]
|
||||
label = f"{row['label']} ▸"
|
||||
group_desc = row.get("description", "")
|
||||
label = f"{row['label']} ▸ ({group_desc})" if group_desc else f"{row['label']} ▸"
|
||||
key = f"group:{gid}"
|
||||
is_active = bool(active_group) and gid == active_group
|
||||
members = row["members"]
|
||||
|
||||
@ -969,23 +969,26 @@ _PROVIDER_LABELS["custom"] = "Custom endpoint" # special case: not a named prov
|
||||
# display affordance; ``group_providers()`` is the single fold used by all
|
||||
# three picker surfaces so they stay consistent.
|
||||
#
|
||||
# group_id -> (display_label, [member_slug, ...])
|
||||
# group_id -> (display_label, group_description, [member_slug, ...])
|
||||
#
|
||||
# ``group_description`` is a short blurb shown on the collapsed top-level group
|
||||
# row in the interactive pickers (alongside the label). Member-specific detail
|
||||
# lives in each member's ``tui_desc`` and shows in the drill-down sub-picker.
|
||||
# Member order is the order shown inside the group submenu.
|
||||
# ---------------------------------------------------------------------------
|
||||
PROVIDER_GROUPS: dict[str, tuple[str, list[str]]] = {
|
||||
"kimi": ("Kimi / Moonshot", ["kimi-coding", "kimi-coding-cn"]),
|
||||
"minimax": ("MiniMax", ["minimax", "minimax-oauth", "minimax-cn"]),
|
||||
"xai": ("xAI Grok", ["xai", "xai-oauth"]),
|
||||
"google": ("Google Gemini", ["gemini", "google-gemini-cli"]),
|
||||
"openai": ("OpenAI", ["openai-codex", "openai-api"]),
|
||||
"opencode": ("OpenCode", ["opencode-zen", "opencode-go"]),
|
||||
"copilot": ("GitHub Copilot", ["copilot", "copilot-acp"]),
|
||||
PROVIDER_GROUPS: dict[str, tuple[str, str, list[str]]] = {
|
||||
"kimi": ("Kimi / Moonshot", "Coding Plan, Moonshot global & China endpoints", ["kimi-coding", "kimi-coding-cn"]),
|
||||
"minimax": ("MiniMax", "Global, OAuth Coding Plan & China endpoints", ["minimax", "minimax-oauth", "minimax-cn"]),
|
||||
"xai": ("xAI Grok", "Direct API or SuperGrok / Premium+ OAuth", ["xai", "xai-oauth"]),
|
||||
"google": ("Google Gemini", "AI Studio API or OAuth + Code Assist", ["gemini", "google-gemini-cli"]),
|
||||
"openai": ("OpenAI", "Codex CLI or direct OpenAI API", ["openai-codex", "openai-api"]),
|
||||
"opencode": ("OpenCode", "Zen pay-as-you-go or Go subscription", ["opencode-zen", "opencode-go"]),
|
||||
"copilot": ("GitHub Copilot", "GitHub token API or copilot --acp process", ["copilot", "copilot-acp"]),
|
||||
}
|
||||
|
||||
# Reverse index: member slug -> group_id. Built once at import.
|
||||
_SLUG_TO_GROUP: dict[str, str] = {
|
||||
slug: gid for gid, (_label, members) in PROVIDER_GROUPS.items() for slug in members
|
||||
slug: gid for gid, (_label, _desc, members) in PROVIDER_GROUPS.items() for slug in members
|
||||
}
|
||||
|
||||
|
||||
@ -1006,7 +1009,7 @@ def group_providers(slugs):
|
||||
{"kind": "single", "slug": <slug>} # ungrouped, or
|
||||
# 1-member group
|
||||
{"kind": "group", "group_id": <gid>, "label": <label>,
|
||||
"members": [<slug>, ...]} # 2+ members
|
||||
"description": <desc>, "members": [<slug>, ...]} # 2+ members
|
||||
|
||||
Rules:
|
||||
* A group row appears at the position of its FIRST present member, in
|
||||
@ -1023,7 +1026,7 @@ def group_providers(slugs):
|
||||
seen: set[str] = set()
|
||||
# Which present members each group has, in declaration order.
|
||||
group_members: dict[str, list[str]] = {}
|
||||
for gid, (_label, members) in PROVIDER_GROUPS.items():
|
||||
for gid, (_label, _desc, members) in PROVIDER_GROUPS.items():
|
||||
present = [m for m in members if m in set(slugs)]
|
||||
if present:
|
||||
group_members[gid] = present
|
||||
@ -1046,9 +1049,10 @@ def group_providers(slugs):
|
||||
if len(members) <= 1:
|
||||
rows.append({"kind": "single", "slug": members[0]})
|
||||
else:
|
||||
label, _ = PROVIDER_GROUPS[gid]
|
||||
label, desc, _ = PROVIDER_GROUPS[gid]
|
||||
rows.append(
|
||||
{"kind": "group", "group_id": gid, "label": label, "members": list(members)}
|
||||
{"kind": "group", "group_id": gid, "label": label,
|
||||
"description": desc, "members": list(members)}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
@ -29,8 +29,9 @@ def test_groups_reference_real_canonical_slugs():
|
||||
"""Every group member must be an actual provider slug. Guards typos and
|
||||
stale group entries after a provider is renamed/removed."""
|
||||
canonical = {p.slug for p in CANONICAL_PROVIDERS}
|
||||
for gid, (label, members) in PROVIDER_GROUPS.items():
|
||||
for gid, (label, desc, members) in PROVIDER_GROUPS.items():
|
||||
assert label, f"group {gid} has empty label"
|
||||
assert desc, f"group {gid} has empty description"
|
||||
assert len(members) >= 1
|
||||
for m in members:
|
||||
assert m in canonical, f"group {gid} member {m!r} is not a canonical slug"
|
||||
@ -39,14 +40,14 @@ def test_groups_reference_real_canonical_slugs():
|
||||
def test_member_slugs_are_unique_across_groups():
|
||||
"""A slug may belong to at most one group."""
|
||||
seen = {}
|
||||
for gid, (_label, members) in PROVIDER_GROUPS.items():
|
||||
for gid, (_label, _desc, members) in PROVIDER_GROUPS.items():
|
||||
for m in members:
|
||||
assert m not in seen, f"{m!r} in both {seen[m]!r} and {gid!r}"
|
||||
seen[m] = gid
|
||||
|
||||
|
||||
def test_reverse_index_matches_groups():
|
||||
for gid, (_label, members) in PROVIDER_GROUPS.items():
|
||||
for gid, (_label, _desc, members) in PROVIDER_GROUPS.items():
|
||||
for m in members:
|
||||
assert provider_group_for_slug(m) == gid
|
||||
assert provider_group_for_slug("openrouter") == ""
|
||||
@ -66,6 +67,9 @@ def test_multi_member_group_folds_to_one_row():
|
||||
assert row["kind"] == "group"
|
||||
assert row["group_id"] == "minimax"
|
||||
assert row["members"] == ["minimax", "minimax-oauth", "minimax-cn"]
|
||||
# group rows carry the short top-level description from PROVIDER_GROUPS
|
||||
assert row["description"] == PROVIDER_GROUPS["minimax"][1]
|
||||
assert row["description"]
|
||||
|
||||
|
||||
def test_group_appears_at_first_member_position():
|
||||
|
||||
Reference in New Issue
Block a user