hermes - ✅(Solved) Fix Gateway /model picker fails for Gemini and Anthropic providers (validate_requested_model rejects curated models) [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#12532Fetched 2026-04-20 12:18:41
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Timeline (top)
cross-referenced ×2referenced ×2commented ×1

Error Message

Gateway error: 4. Select Google → pick any model (e.g. gemini-2.5-flash) → Error 5. Select Anthropic → pick any model (e.g. claude-opus-4-7) → Error

Root Cause

The CLI and Gateway use completely different code paths for model selection:

CLI (hermes model)Gateway (/model picker)
Code path_model_flow_api_key_provider()_prompt_model_selection()_save_model_choice()_on_model_selected()switch_model()validate_requested_model()
ValidationNone — saves directly to config.yamlCalls validate_requested_model() which probes the live API via fetch_api_models()
Result✅ Works❌ Fails

The Gateway's switch_model() pipeline calls validate_requested_model(), which probes the provider's /v1/models endpoint. This causes two distinct failures:

Fix Action

Fixed

PR fix notes

PR #12585: fix(models): accept Gemini + Anthropic in gateway /model picker (#12532)

Description (problem / solution / changelog)

Fixes #12532.

TL;DR

Gateway /model picker rejects both Gemini and Anthropic models while CLI hermes model works with the same credentials. Two distinct bugs in validate_requested_model:

  • Gemini: /models returns IDs like models/gemini-2.5-flash — curated list and user input use bare gemini-2.5-flash → set membership drops every model.
  • Anthropic: generic probe sends Authorization: Bearer without anthropic-version → Anthropic's native /v1/models returns 4xx → fetch_api_models yields None → lands in the generic "couldn't reach API" hard-reject.

Both fixes mirror existing patterns already in this function (Bedrock / Alibaba catalog fall-through for Anthropic; symmetric normalization for the Gemini prefix drift).

Root cause details

Gemini

Gemini's OpenAI-compat endpoint at generativelanguage.googleapis.com/v1beta/openai/models returns IDs in the native Gemini format (models/gemini-2.5-flash). The curated _PROVIDER_MODELS["gemini"] list and the picker UI both use the bare ID. requested_for_lookup in set(api_models) therefore fails for every known Gemini model.

Anthropic

probe_api_models constructs headers at hermes_cli/models.py:1776-1780:

headers: dict[str, str] = {}
if api_key:
    headers["Authorization"] = f"Bearer {api_key}"
if normalized.startswith(COPILOT_BASE_URL):
    headers.update(copilot_default_headers())

Anthropic's API requires x-api-key (for API keys) or Authorization: Bearer with anthropic-version header — never Bearer alone without a version. So the probe silently 4xxs, returning None, and the request lands in the "couldn't reach API" hard-reject branch.

_fetch_anthropic_models (line 1338) already does the right thing — it's called during provider_model_ids("anthropic") for picker population, but never for per-request validation.

Fix

Two surgical additions to validate_requested_model:

1. Gemini prefix strip — before the set-membership check:

if api_models is not None:
    if normalized == "gemini":
        api_models = [
            m[len("models/"):] if isinstance(m, str) and m.startswith("models/") else m
            for m in api_models
        ]
    if requested_for_lookup in set(api_models):
        ...

The normalized list also drives auto-correct + suggestions, so "Similar models: gemini-2.5-flash" surfaces bare IDs the user can actually type — not the useless models/... literals.

2. Anthropic catalog fall-through — after the existing Bedrock block, same pattern as the #12287 Alibaba fix:

if normalized == "anthropic":
    try:
        catalog = provider_model_ids("anthropic")
    except Exception:
        catalog = []
    if catalog:
        if requested in set(catalog) or requested_for_lookup in set(catalog):
            return {"accepted": True, "persist": True, "recognized": True, "message": None}
        suggestions = get_close_matches(requested, catalog, n=3, cutoff=0.4)
        return {
            "accepted": True,
            "persist": True,
            "recognized": False,
            "message": f"Note: `{requested}` was not found in the Anthropic catalog. "
                       f"It may still work if it's a newer model the catalog doesn't list yet. ...",
        }
    # empty / exception → fall through to the generic reject

provider_model_ids("anthropic") already handles live-fetch (via _fetch_anthropic_models with the correct x-api-key + anthropic-version headers) and falls back to the curated static list on failure.

Behaviour matrix

ScenarioBeforeAfter
/model gemini:gemini-2.5-flash via pickerREJECT — not in {models/gemini-2.5-flash, …}accept (quiet)
/model anthropic:claude-opus-4-7 via pickerREJECT — "couldn't reach Anthropic API"accept (quiet)
Unknown Gemini ID (gemini-hypothetical)reject with models/…-leaked suggestionsreject with bare-ID suggestions
Unknown Anthropic model"couldn't reach Anthropic API"accept with warning + close-match suggestions
Other provider (zai) with unreachable APIreject (unchanged)reject (unchanged)
Other provider with models/-prefixed API responsemismatch on bare ID (unchanged)mismatch on bare ID (unchanged)
Anthropic catalog empty (import failure)hard-rejectfall through to hard-reject
Anthropic catalog raiseshard-rejectfall through to hard-reject

Narrow scope — explicitly not changed

  • probe_api_models auth headers. Still Bearer-only. Teaching the generic probe about Anthropic's header requirements would entangle provider-specific details in the transport layer; the catalog fall-through is less invasive.
  • Other providers that might return models/-prefixed IDs. The strip is gated on normalized == "gemini". Pinned by a custom canary.
  • Reporter's Option A (skip validation entirely when the model was chosen from a curated picker). That's a gateway-side refactor of _on_model_selected/switch_model; this PR keeps the fix at the validator layer so it benefits CLI, gateway, and any future caller uniformly.

Regression coverage

Two new test classes, 11 cases:

TestValidateGeminiModelsPrefix (4 cases):

  • test_bare_id_accepted_despite_models_prefix_from_api — reporter's repro
  • test_all_curated_gemini_ids_resolve — 4 IDs across pro/flash/flash-lite/preview
  • test_unknown_gemini_id_surfaces_suggestions_not_generic_reject — pins that suggestions don't leak the models/ prefix
  • test_prefix_strip_limited_to_gemini_providercustom provider canary

TestValidateAnthropicNoModelsEndpoint (7 cases):

  • test_curated_claude_model_accepted — reporter's repro
  • test_all_tiers_resolve — opus/sonnet/haiku
  • test_unknown_model_accepted_with_warning
  • test_empty_catalog_falls_through_to_generic_reject
  • test_catalog_lookup_exception_falls_through
  • test_unknown_model_includes_close_match_suggestion
  • test_other_providers_still_hard_reject_when_api_unreachablezai canary

6 of 11 fail on clean origin/main (6fb69229) with assert False is True on result["accepted"]. The 5 passing tests pin preserved behaviour (canaries + defensive fall-throughs).

Validation

source venv/bin/activate
python -m pytest \
  tests/hermes_cli/test_model_validation.py::TestValidateGeminiModelsPrefix \
  tests/hermes_cli/test_model_validation.py::TestValidateAnthropicNoModelsEndpoint -q
# 11 passed

Broader model-switch / normalize suites (6 files) → 159 passed, 0 failures.

Relation to prior PRs

This PR uses the exact same fall-through pattern as #12287 (Alibaba DashScope coding endpoint). That pattern in turn mirrors the pre-existing Bedrock branch. Each provider whose /models endpoint is structurally inaccessible gets its own small catalog fall-through — no broad widening of the validator's trust surface.


<sub>Co-authored via LLM assistance; I've reviewed every line and am responsible for correctness.</sub>

Changed files

  • hermes_cli/models.py (modified, +47/-0)
  • tests/hermes_cli/test_model_validation.py (modified, +242/-0)

Code Example

Provider: gemini  Model: google/gemini-2.5-flash
Endpoint: https://generativelanguage.googleapis.com/v1beta/openai
HTTP 404: models/google/gemini-2.5-flash is not found for API version v1main

---

Could not reach the Anthropic API to validate `claude-opus-4-7`.
If the service isn't down, this model may not be valid.

---

from hermes_cli.model_switch import switch_model

# Google — fails on validation
result = switch_model(
    raw_input='gemini-2.5-flash',
    current_provider='openrouter',
    current_model='anthropic/claude-haiku-4.5',
    explicit_provider='gemini',
)
print(result.success, result.error_message)
# False, "Model `gemini-2.5-flash` was not found in this provider's model listing.
#   Similar models: `models/gemini-2.5-flash`, ..."

# Anthropic — fails on validation
result = switch_model(
    raw_input='claude-opus-4-7',
    current_provider='openrouter',
    current_model='anthropic/claude-haiku-4.5',
    explicit_provider='anthropic',
)
print(result.success, result.error_message)
# False, "Could not reach the Anthropic API to validate `claude-opus-4-7`..."
RAW_BUFFERClick to expand / collapse

Bug Description

The Gateway (Telegram/Discord) /model interactive picker fails to switch to non-OpenRouter providers (e.g. Google/Gemini, Anthropic), while the CLI hermes model command works correctly for the same providers with the same credentials.

Root Cause

The CLI and Gateway use completely different code paths for model selection:

CLI (hermes model)Gateway (/model picker)
Code path_model_flow_api_key_provider()_prompt_model_selection()_save_model_choice()_on_model_selected()switch_model()validate_requested_model()
ValidationNone — saves directly to config.yamlCalls validate_requested_model() which probes the live API via fetch_api_models()
Result✅ Works❌ Fails

The Gateway's switch_model() pipeline calls validate_requested_model(), which probes the provider's /v1/models endpoint. This causes two distinct failures:

1. Google/Gemini: Model ID format mismatch

  • Gemini's /v1/models endpoint returns IDs prefixed with models/ (e.g. models/gemini-2.5-flash)
  • But the curated list in _PROVIDER_MODELS["gemini"] uses bare names (e.g. gemini-2.5-flash)
  • validate_requested_model() does an exact match → "gemini-2.5-flash" not in {"models/gemini-2.5-flash", ...}rejected
  • If validation is bypassed, the model works fine with the Gemini OpenAI-compatible endpoint

Gateway log:

Provider: gemini  Model: google/gemini-2.5-flash
Endpoint: https://generativelanguage.googleapis.com/v1beta/openai
HTTP 404: models/google/gemini-2.5-flash is not found for API version v1main

2. Anthropic: No /models endpoint

  • Anthropic's API does not expose a /v1/models listing endpoint
  • fetch_api_models() returns None
  • validate_requested_model() treats None as "could not reach API" → accepted: False

Gateway error:

Could not reach the Anthropic API to validate `claude-opus-4-7`.
If the service isn't down, this model may not be valid.

Steps to Reproduce

  1. Configure both GEMINI_API_KEY and ANTHROPIC_API_KEY in ~/.hermes/.env
  2. Start the gateway: hermes gateway run
  3. In Telegram, type /model — the picker shows OpenRouter, Anthropic, Google
  4. Select Google → pick any model (e.g. gemini-2.5-flash) → Error
  5. Select Anthropic → pick any model (e.g. claude-opus-4-7) → Error
  6. For comparison: run hermes model in CLI → select Google or Anthropic → Works

Expected Behavior

Selecting a model from the Gateway picker should work the same as selecting it from the CLI — models from the curated list should be accepted without requiring live API validation.

Suggested Fix

Either:

Option A (recommended): Skip validate_requested_model() when the model was selected from the curated picker list (the picker already shows only valid, curated models from list_authenticated_providers()).

Option B: Fix validate_requested_model() to handle edge cases:

  • For Gemini: strip models/ prefix when comparing, or prepend models/ to the requested name
  • For Anthropic: when fetch_api_models() returns None, fall back to checking the static curated list instead of rejecting
  • General: if a model exists in _PROVIDER_MODELS[provider], accept it even if API probing fails

Reproducible with code

from hermes_cli.model_switch import switch_model

# Google — fails on validation
result = switch_model(
    raw_input='gemini-2.5-flash',
    current_provider='openrouter',
    current_model='anthropic/claude-haiku-4.5',
    explicit_provider='gemini',
)
print(result.success, result.error_message)
# False, "Model `gemini-2.5-flash` was not found in this provider's model listing.
#   Similar models: `models/gemini-2.5-flash`, ..."

# Anthropic — fails on validation
result = switch_model(
    raw_input='claude-opus-4-7',
    current_provider='openrouter',
    current_model='anthropic/claude-haiku-4.5',
    explicit_provider='anthropic',
)
print(result.success, result.error_message)
# False, "Could not reach the Anthropic API to validate `claude-opus-4-7`..."

Environment

  • Hermes version: 0.10.0
  • OS: macOS (Darwin 25.3.0, arm64)
  • Platform: Telegram Gateway
  • Providers affected: Google/Gemini, Anthropic (likely any non-OpenRouter direct provider)

extent analysis

TL;DR

The most likely fix is to modify the validate_requested_model() function to handle edge cases for non-OpenRouter providers, such as stripping the models/ prefix for Gemini or falling back to the static curated list for Anthropic.

Guidance

  • Modify the validate_requested_model() function to handle the models/ prefix for Gemini by either stripping the prefix or prepending it to the requested model name.
  • For Anthropic, modify the function to fall back to checking the static curated list when fetch_api_models() returns None.
  • Consider skipping validate_requested_model() when the model is selected from the curated picker list, as the picker already shows only valid models.
  • Test the modified function with the provided reproducible code to ensure it works correctly for both Gemini and Anthropic.

Example

def validate_requested_model(provider, model):
    # ...
    if provider == 'gemini':
        # Strip the 'models/' prefix
        api_models = [m.replace('models/', '') for m in api_models]
    # ...
    if provider == 'anthropic' and api_models is None:
        # Fall back to the static curated list
        api_models = _PROVIDER_MODELS[provider]
    # ...

Notes

The provided fix options (A and B) both seem viable, but modifying the validate_requested_model() function to handle edge cases (Option B) might be a more comprehensive solution. However, skipping the validation step when the model is selected from the curated list (Option A) could also work if the curated list is trusted to be accurate.

Recommendation

Apply workaround by modifying the validate_requested_model() function to handle edge cases, as this approach addresses the specific issues with Gemini and Anthropic while potentially improving the overall robustness of the model selection process.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix Gateway /model picker fails for Gemini and Anthropic providers (validate_requested_model rejects curated models) [1 pull requests, 1 comments, 2 participants]