Skip to content

fix(model_switch): group custom_providers by endpoint in /model picker#9210

Closed
davidvv wants to merge 2 commits into
NousResearch:mainfrom
davidvv:fix/model-picker-custom-providers
Closed

fix(model_switch): group custom_providers by endpoint in /model picker#9210
davidvv wants to merge 2 commits into
NousResearch:mainfrom
davidvv:fix/model-picker-custom-providers

Conversation

@davidvv

@davidvv davidvv commented Apr 13, 2026

Copy link
Copy Markdown

Fixes the /model command producing no output (or no picker) when the active configuration uses custom_providers with an Ollama or other OpenAI-compatible endpoint.

Root cause

list_authenticated_providers() had two problems in section 4 (custom_providers handling):

1. Missing current_base_url parameter
The function had no way to tell which custom entry corresponds to the active endpoint. As a result is_current was always False and the slug was a name-derived hash (e.g. ollama-glm-5-1) that did not match the config model.provider value (custom). Switching via the picker therefore sent the wrong provider slug to switch_model, causing credential resolution to fail.

2. One picker entry per config entry instead of grouping by endpoint
A single Ollama host with 4 model entries appeared as 4 separate single-model providers in the picker instead of one group with 4 models to choose from.

3. Silent TypeError at call site (same symptom on older gateway builds)
Both call sites in gateway/run.py already passed custom_providers=... as a keyword arg, but the old signature did not declare that parameter — resulting in a TypeError that was silently swallowed by the surrounding try/except, leaving providers = [] and the picker never opening.

Fix

  • Add current_base_url: str = parameter.
  • Replace section 4 with a grouping pass: entries are bucketed by (base_url, api_key). When a bucket matches the active endpoint (base_url == current_base_url) the slug is set to current_provider so that credential resolution in the existing switch_model pipeline works without any extra lookup.
  • Display name is derived from the first entry in the group, stripping per-model suffixes (Ollama — GLM 5.1Ollama). All model names from matching entries are merged into a single list.
  • Pass current_base_url through both list_authenticated_providers call sites in gateway/run.py.

Result

Before: 0 providers shown, picker never opened (TypeError swallowed).
After: 1 provider group Ollama with all 4 models, correctly marked as current, inline keyboard picker opens and model switching works.

list_authenticated_providers() had two issues with custom_providers:

1. Missing current_base_url parameter — the function had no way to tell
   which custom entry matches the active endpoint, so is_current was
   always False and the slug was a name-derived hash that did not match
   the config provider value (e.g. "custom"). Switching via the picker
   therefore resolved wrong credentials.

2. One entry per config item instead of grouping by endpoint — a single
   Ollama host with 4 model entries appeared as 4 separate providers in
   the picker, each with 1 model, instead of one group with 4 models.

Fix:
- Add current_base_url: str = "" parameter.
- Replace section 4 with a grouping pass: entries are bucketed by
  (base_url, api_key). When a bucket matches the active endpoint
  (base_url == current_base_url) the slug is set to current_provider
  so that credential resolution in the existing switch pipeline works
  without any extra lookup. Display name is derived from the first
  entry in the group, stripping per-model suffixes ("Ollama — GLM" ->
  "Ollama"). Models from all entries in the group are merged into a
  single list.
- Pass current_base_url through both list_authenticated_providers call
  sites in gateway/run.py (_handle_model_command picker + text list).

Tested against a config with 4 Ollama cloud models on one endpoint:
before: 0 providers shown (TypeError swallowed), picker never opened.
after:  1 provider group "Ollama" with all 4 models, correctly marked
        as current, inline keyboard picker opens and switching works.
Copilot AI review requested due to automatic review settings April 13, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the /model interactive picker failing (often silently) when the active config uses custom_providers pointing at an OpenAI-compatible endpoint (e.g., Ollama), by correctly identifying the active endpoint and grouping custom provider entries by endpoint.

Changes:

  • Extend list_authenticated_providers() to accept current_base_url and use it to mark the active custom endpoint correctly.
  • Replace per-entry custom provider listing with grouping by (base_url, api_key) and merging models into a single picker entry per endpoint.
  • Pass current_base_url into list_authenticated_providers() from both /model call sites in gateway/run.py.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
hermes_cli/model_switch.py Adds current_base_url support and groups custom_providers by endpoint for the /model picker output.
gateway/run.py Threads current_base_url through to provider listing so the picker can correctly mark/select the active custom endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/model_switch.py Outdated
for entry in custom_providers:
from collections import OrderedDict
groups: dict = OrderedDict()
for i, entry in enumerate(custom_providers):
Comment on lines +1008 to +1011
"models": [],
"source": "custom_providers",
"api_url": b_url,
}
Comment thread hermes_cli/model_switch.py Outdated
Comment on lines +1024 to +1027
for m in g["models"]:
if m not in existing["models"]:
existing["models"].append(m)
existing["total_models"] = len(existing["models"])
Comment on lines 972 to +1000
# --- 4. Saved custom providers from config ---
# Entries are grouped by (base_url, api_key) so a single Ollama host
# with several models appears as one provider group in the picker.
# When the entry's base_url matches the currently active endpoint the
# slug is set to current_provider so that switching via the picker
# resolves credentials correctly through the existing runtime provider
# logic (no extra credential lookup needed).
if custom_providers and isinstance(custom_providers, list):
for entry in custom_providers:
from collections import OrderedDict
groups: dict = OrderedDict()
for i, entry in enumerate(custom_providers):
if not isinstance(entry, dict):
continue

display_name = (entry.get("name") or "").strip()
api_url = (
entry.get("base_url", "")
or entry.get("url", "")
or entry.get("api", "")
or ""
).strip()
if not display_name or not api_url:
b_url = (
entry.get("base_url") or entry.get("url") or entry.get("api") or ""
).rstrip("/")
a_key = entry.get("api_key") or ""
model_id = (entry.get("model") or "").strip()
if not b_url:
continue

slug = custom_provider_slug(display_name)
if slug in seen_slugs:
continue

models_list = []
default_model = (entry.get("model") or "").strip()
if default_model:
models_list.append(default_model)

results.append({
"slug": slug,
"name": display_name,
"is_current": slug == current_provider,
"is_user_defined": True,
"models": models_list,
"total_models": len(models_list),
"source": "user-config",
"api_url": api_url,
})
seen_slugs.add(slug)
if current_base_url and b_url == current_base_url.rstrip("/"):
slug = current_provider or "custom"
else:
slug = custom_provider_slug(entry.get("name") or b_url)
group_key = (b_url, a_key)
if group_key not in groups:
raw_name = (entry.get("name") or "").strip()
# Strip per-model suffix ("Ollama — GLM 5.1" -> "Ollama")
display_name = raw_name.split("—")[0].strip() if "—" in raw_name else raw_name
- Remove unused enumerate() in section 4 loop
- Normalise source value to "user-config" (consistent with existing taxonomy);
  update docstring to document this
- Apply max_models cap after merging models in the slug-collision path
  (total_models keeps the full deduplicated count)
- Add three regression tests:
    test_list_authenticated_providers_groups_same_endpoint
    test_list_authenticated_providers_current_endpoint_slug_and_is_current
    test_list_authenticated_providers_max_models_cap_after_merge
teknium1 pushed a commit that referenced this pull request Apr 23, 2026
#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing #8216, #11499, #13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR #9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes #9210
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery labels Apr 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Superseded by #14505 which salvaged this fix onto current main.

ulasbilgen pushed a commit to ulasbilgen/hermes-adhd-agent that referenced this pull request May 1, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
aj-nt pushed a commit to aj-nt/hermes-agent that referenced this pull request May 1, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
NousResearch#9210)

Multiple custom_providers entries sharing the same base_url + api_key
are now grouped into a single picker row. A local Ollama host with
per-model display names ("Ollama — GLM 5.1", "Ollama — Qwen3-coder",
"Ollama — Kimi K2", "Ollama — MiniMax M2.7") previously produced four
near-duplicate picker rows that differed only by suffix; now it appears
as one "Ollama" row with four models.

Key changes:
- Grouping key changed from slug-by-name to (base_url, api_key). Names
  frequently differ per model while the endpoint stays the same.
- When the grouped endpoint matches current_base_url, the row's slug is
  set to current_provider so picker-driven switches route through the
  live credential pipeline (no re-resolution needed).
- Per-model suffix is stripped from the display name ("Ollama — X" →
  "Ollama") via em-dash / " - " separators.
- Two groups with different api_keys at the same base_url (or otherwise
  colliding on cleaned name) are disambiguated with a numeric suffix
  (custom:openai, custom:openai-2) so both stay visible.
- current_base_url parameter plumbed through both gateway call sites.

Existing NousResearch#8216, NousResearch#11499, NousResearch#13509 regressions covered (dict/list shapes
of models:, section-3/section-4 dedup, normalized list-format entries).

Salvaged from @davidvv's PR NousResearch#9210 — the underlying code had diverged
~1400 commits since that PR was opened, so this is a reconstruction of
the same approach on current main rather than a clean cherry-pick.
Authorship preserved via --author on this commit.

Closes NousResearch#9210
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants