Skip to content

Gateway /model command sends provider-prefixed slug as raw model name for non-curated models (HTTP 400 Unknown Model) #7922

@notaidong

Description

@notaidong

Summary

When switching to a model that isn't in the _PROVIDER_MODELS curated list via /model <provider>/<model> from a messaging gateway (Telegram/Discord), Hermes stores the raw provider/model slug as the session override and sends it verbatim as the model field in the HTTP request body, causing the provider API to reject it with HTTP 400.

Environment

  • Hermes Agent v0.8.0 (2026.4.8)
  • Python 3.11.15, Ubuntu 24.04 LTS
  • Platform: Telegram gateway (polling mode)
  • Provider: Z.AI Coding Plan (https://api.z.ai/api/coding/paas/v4/)

Steps to reproduce

  1. Configure Z.AI Coding Plan credentials in ~/.hermes/.env:
GLM_API_KEY=<your-zai-coding-plan-key>
GLM_BASE_URL=https://api.z.ai/api/coding/paas/v4/
  1. Run hermes gateway restart.
  2. In Telegram, send: /model zai/glm-5.1
  3. Hermes acknowledges the switch with a warning:
Model switched to zai/glm-5.1
Provider: auto
Warning: zai/glm-5.1 was not found in this provider's model listing. It may still work if your plan supports it.
  Similar models: glm-5.1, glm-5, glm-4.7
(session only -- add --global to persist)
  1. Send any message (e.g. hi).
  2. The agent fails with:
Non-retryable error (HTTP 400): HTTP 400: Unknown Model, please check the model code.
Error code: 400 - {'error': {'code': '1211', 'message': 'Unknown Model, please check the model code.'}}

Expected vs actual

Expected Actual
HTTP body model field "glm-5.1" "zai/glm-5.1"
API response 200 with completion 400 Unknown Model (code 1211)

Evidence: raw request dump

From ~/.hermes/sessions/request_dump_*.json (credentials redacted):

{
  "timestamp": "2026-04-11T19:41:24.796138",
  "reason": "non_retryable_client_error",
  "request": {
    "method": "POST",
    "url": "https://api.z.ai/api/coding/paas/v4/chat/completions",
    "headers": {
      "Authorization": "Bearer <REDACTED>",
      "Content-Type": "application/json"
    },
    "body": {
      "model": "zai/glm-5.1",
      "messages": ["..."]
    }
  },
  "error": {
    "status_code": 400,
    "body": {"code": "1211", "message": "Unknown Model, please check the model code."}
  }
}

The URL correctly resolves to the Z.AI Coding Plan endpoint, but body.model retains the zai/ provider prefix.

Root cause (two related issues)

1. Stale curated list. hermes_cli/models.py around line 125:

"zai": [
    "glm-5",
    "glm-5-turbo",
    "glm-4.7",
    "glm-4.5",
    "glm-4.5-flash",
],

Z.AI /models endpoint currently returns 7 models: glm-4.5, glm-4.5-air, glm-4.6, glm-4.7, glm-5, glm-5-turbo, glm-5.1. The curated list is missing glm-5.1, glm-4.6, and glm-4.5-air. glm-5.1 was released around 2026-03-28 per its created timestamp from Z.AI.

2. Normalize bypass when model is not in curated list (the actual bug). When the user sends /model zai/glm-5.1, _handle_model_command in gateway/run.py (around line 3579) stores a session override. Because glm-5.1 is not in _PROVIDER_MODELS["zai"], the switch flow treats the input as "unrecognized but API-probed OK" — validate_requested_model() performs a live API probe to confirm the model exists, so the switch succeeds with only a warning, but the session override stores the raw zai/glm-5.1 slug. When Hermes later builds the HTTP request body, it reads the override and uses it directly without calling normalize_model_for_provider() to strip the zai/ prefix. Result: body.model = "zai/glm-5.1".

If glm-5.1 were in the curated list, the switch flow would normalize the name to bare glm-5.1 before storing. Since it isn't, normalization is bypassed.

The --provider flag works around this because /model glm-5.1 --provider zai takes a different branch that stores the bare model name from the start.

Verified workarounds

  1. Explicit --provider flag: /model glm-5.1 --provider zai — ✅ verified on v0.8.0.
  2. Patch _PROVIDER_MODELS["zai"] to include glm-5.1 (and ideally glm-4.6, glm-4.5-air). After this patch, /model glm-5.1 (bare, without --provider) also works correctly — ✅ verified on v0.8.0. Note this is a local mitigation that fixes the symptom for specific models but does not address the underlying normalize bypass; any newly released model will hit the same issue until the curated list is updated again.

Proposed fixes

Option A (recommended, minimal) — In _handle_model_command, after _switch_model returns, call normalize_model_for_provider() on the resolved model before storing it into _session_model_overrides. This ensures the stored override always has the bare model name regardless of curated-list membership.

Option B (defense in depth) — Normalize at the HTTP request builder so no matter how the override got there, the outbound body.model is always bare.

Option C (root fix for the stale-list symptom) — Populate _PROVIDER_MODELS[provider] dynamically via probe_api_models() or similar at startup, instead of hardcoding. Complements A and B.

Options A + C together would fix both the normalize bypass and the staleness.

Additional note

This class of bug will resurface whenever a provider adds new models faster than the hardcoded curated list can be refreshed. Z.AI, Kimi, MiniMax etc. release new models roughly monthly; the curated list cannot realistically keep pace without either (a) dynamic refresh or (b) graceful handling of unknown-but-API-probed models.

Thanks for the great project!

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/gatewayGateway runner, session dispatch, deliveryprovider/zaiZAI providertype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions