Skip to content

fix(api_server): respect per-request model override in _create_agent - #22825

Closed
mssteuer wants to merge 1 commit into
NousResearch:mainfrom
mssteuer:fix/api-server-model-override-v2
Closed

mssteuer wants to merge 1 commit into
NousResearch:mainfrom
mssteuer:fix/api-server-model-override-v2

Conversation

@mssteuer

@mssteuer mssteuer commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

When API consumers (e.g. CCC task dispatcher, external UIs) pass a model field in their request body, the API server now correctly uses that model instead of falling back to the gateway default.

Problem

The model field was extracted from the request body but never passed through to _create_agent(). This caused all API-spawned sessions to use the gateway default model regardless of what model was requested.

Impact: Sessions requested on cheaper models (e.g. openrouter/openai/gpt-5.5, litellm/gemini-2.5-flash) silently ran on the gateway default (claude-opus-4-6), burning expensive tokens on the wrong provider. In our setup, ~66 coding sessions in 18 hours ran on Opus when they should have been on OpenRouter.

Root Cause

_handle_chat_completions, _handle_responses, and _handle_runs all extracted the model from the request body, but _create_agent() had no model_override parameter to receive it — the extracted value was used only for status display, never for actual agent creation.

Fix

  • _create_agent() now accepts a model_override parameter
  • When override contains openrouter/ prefix → resolves OpenRouter credentials from providers config
  • When override contains litellm- or litellm/ prefix → routes to LiteLLM proxy
  • _run_agent() forwards model_override to _create_agent()
  • All three API paths (chat completions, responses, runs) now pass body.model as model_override
  • Gateway default alias is filtered out (if body.model == self._model_name, no override is applied)

Tests

Added tests/gateway/test_api_server_model_override.py with 5 regression tests:

  • _create_agent respects override with openrouter prefix (credential resolution)
  • _create_agent respects override with litellm prefix (proxy routing)
  • _create_agent respects plain model name (model swap, keep provider)
  • _create_agent uses gateway default when no override
  • Gateway default model name is not treated as override

All 162 existing API server tests continue to pass.

@mssteuer
mssteuer force-pushed the fix/api-server-model-override-v2 branch from c44cdf0 to 91999eb Compare May 9, 2026 21:20
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery labels May 9, 2026
@alt-glitch

Copy link
Copy Markdown

Duplicate of #19939 — same fix (wire model_override into _create_agent for all API paths). Also related to #5862, #16403, #10773.

@mssteuer

Copy link
Copy Markdown
Contributor Author

Duplicate of #19939 — same fix (wire model_override into _create_agent for all API paths). Also related to #5862, #16403, #10773.

No longer a duplicate. #19939 closed in favor of this more recent one. Please review and consider merging @alt-glitch

Codename-11 added a commit to Codename-11/hermes-agent that referenced this pull request Jun 17, 2026
Desktop/external clients that send a model field in the session chat or
chat/stream request body can now override the gateway default for that
turn. _create_agent() and _run_agent() accept requested_model, which
takes priority over _resolve_gateway_model() when present.

Wired through:
- POST /api/sessions/{id}/chat
- POST /api/sessions/{id}/chat/stream

This enables the desktop composer model picker to hot-swap the model
mid-session without requiring a config.yaml write + gateway restart.

Upstream PRs NousResearch#22825, NousResearch#25552, and NousResearch#36110 address similar gaps on /v1/*
paths; this patch is scoped to the /api/sessions/* surface only.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for tracing the model value through all three API endpoints. The direct body-model behavior is still absent for unconfigured values, but current main has since added a different routing seam.

Problems

  • Current main resolves request models through configured model_routes aliases only (gateway/platforms/api_server.py:1207, :2194, :3312, :4285), with explicit session /model precedence in _create_agent (:1291-1334). The proposed parallel model_override path would bypass that architecture.
  • The added raw provider-config lookup and fixed LiteLLM URL do not use the current provider runtime resolver. gateway.run._resolve_runtime_agent_kwargs_for_provider resolves provider runtime fields including API mode and credential pool (gateway/run.py:1906-1924), and the existing route path calls it at gateway/platforms/api_server.py:1305-1311.

Suggested changes

  • Salvage this through the existing model_routes/route flow, preserving session override precedence and using the provider-runtime resolver.
  • Add endpoint-level coverage for the intended unconfigured/direct-request behavior across chat completions, Responses, and Runs.

This is an automated hermes-sweeper review.

Comment thread gateway/platforms/api_server.py Outdated
# This allows API consumers (CCC, external UIs) to select a model
# per-request rather than always using the gateway default.
if model_override:
model = model_override

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Current main resolves a switched provider through _resolve_runtime_agent_kwargs_for_provider (gateway/run.py:1906-1924) so it retains API mode, command arguments, and credential-pool behavior. Please salvage this through the current model_routes provider-resolution seam instead of reading a raw providers map here.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
API consumers could not select a model per request: the body's 'model'
field was only honored when it matched a configured model_routes alias;
anything else silently fell back to the gateway default.

Salvaged through the existing model_routes seam per review, replacing
the earlier parallel model_override path:

- New opt-in flag platforms.api_server.extra.direct_model_requests
  (default off — generic OpenAI clients hardcode model names and
  existing deployments rely on unknown values falling back).
- _resolve_request_route(): configured aliases always win; with the
  flag on, an unconfigured value that differs from the advertised
  model name synthesizes an ephemeral {'model': value} route.
- Reuses _create_agent's route application, so session /model override
  precedence is preserved and provider runtime resolution stays with
  _resolve_runtime_agent_kwargs_for_provider — no raw provider-config
  lookups, no fixed LiteLLM URL.
- Endpoint coverage across chat completions, Responses, and Runs, plus
  default-off fallback and session-override-beats-request tests.
@mssteuer
mssteuer force-pushed the fix/api-server-model-override-v2 branch from 91999eb to ca84221 Compare July 21, 2026 23:30
@mssteuer

Copy link
Copy Markdown
Contributor Author

@teknium1 done — rebuilt through the model_routes seam as suggested; the parallel model_override path is gone.

Force-pushed a single commit (ca84221f7):

  • New opt-in flag platforms.api_server.extra.direct_model_requests (default off — generic OpenAI clients hardcode model names like gpt-4o, and existing deployments rely on unknown values falling back to the gateway default rather than erroring upstream).
  • _resolve_request_route(): configured model_routes aliases always win; with the flag enabled, an unconfigured body model that differs from the advertised name synthesizes an ephemeral {"model": <value>} route. All three endpoints (chat completions, Responses, Runs) call it at the same sites the alias lookup already lived.
  • Architecture preserved: the synthetic route flows through _create_agent's existing route application, so session /model precedence is intact, and provider/credential resolution stays with _resolve_runtime_agent_kwargs_for_provider — the raw provider-config lookup and the fixed LiteLLM URL from the old patch are gone. A synthetic route carries only model, never provider/api_key/base_url.
  • Endpoint-level coverage for the intended behavior: direct model reaches the agent on /v1/chat/completions, /v1/responses, and /v1/runs; unknown model is ignored when the flag is off; configured alias beats passthrough; session /model override beats the request body.

Local verification:

python -m pytest tests/gateway/test_api_server.py -q
# 219 passed
ruff check gateway/platforms/api_server.py tests/gateway/test_api_server.py
# All checks passed!

teknium1 added a commit that referenced this pull request Jul 24, 2026
Follow-ups on the salvaged #54426 routing contract:

- Bare `model` without `provider` on the OpenAI-compatible endpoints
  (/v1/chat/completions, /v1/responses) is now opt-in via
  gateway.platforms.api_server.direct_model_requests (default off) —
  generic OpenAI clients hardcode model names ('gpt-4o', ...) and
  existing deployments rely on those falling back to the gateway
  default. Explicit `provider` requests and the Hermes-native
  session-chat + /v1/runs surfaces are always honored.
  Idea credit: PR #22825 by @mssteuer.
- A model_routes alias with no `model` key can no longer leak the
  alias string as the executing model name (defensive; parse-time
  validation already drops such routes).
- Fix mis-indented _run_agent call args in _handle_session_chat_stream.
- Docs: document the opt-in flag.
@teknium1

Copy link
Copy Markdown
Collaborator

Closing in favor of PR #70853 (salvage of #54426), which now covers per-request model selection across all four API server execution surfaces — including your core idea: the opt-in direct_model_requests config gate for bare model passthrough, default off so generic OpenAI clients keep falling back to the gateway default. You were the earliest submitter of the per-request model override (May 9), and the gate design in #70853 is credited to you in the code comment and PR body. Thanks!

@teknium1 teknium1 closed this Jul 24, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Follow-ups on the salvaged NousResearch#54426 routing contract:

- Bare `model` without `provider` on the OpenAI-compatible endpoints
  (/v1/chat/completions, /v1/responses) is now opt-in via
  gateway.platforms.api_server.direct_model_requests (default off) —
  generic OpenAI clients hardcode model names ('gpt-4o', ...) and
  existing deployments rely on those falling back to the gateway
  default. Explicit `provider` requests and the Hermes-native
  session-chat + /v1/runs surfaces are always honored.
  Idea credit: PR NousResearch#22825 by @mssteuer.
- A model_routes alias with no `model` key can no longer leak the
  alias string as the executing model name (defensive; parse-time
  validation already drops such routes).
- Fix mis-indented _run_agent call args in _handle_session_chat_stream.
- Docs: document the opt-in flag.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
Follow-ups on the salvaged NousResearch#54426 routing contract:

- Bare `model` without `provider` on the OpenAI-compatible endpoints
  (/v1/chat/completions, /v1/responses) is now opt-in via
  gateway.platforms.api_server.direct_model_requests (default off) —
  generic OpenAI clients hardcode model names ('gpt-4o', ...) and
  existing deployments rely on those falling back to the gateway
  default. Explicit `provider` requests and the Hermes-native
  session-chat + /v1/runs surfaces are always honored.
  Idea credit: PR NousResearch#22825 by @mssteuer.
- A model_routes alias with no `model` key can no longer leak the
  alias string as the executing model name (defensive; parse-time
  validation already drops such routes).
- Fix mis-indented _run_agent call args in _handle_session_chat_stream.
- Docs: document the opt-in flag.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
Follow-ups on the salvaged NousResearch#54426 routing contract:

- Bare `model` without `provider` on the OpenAI-compatible endpoints
  (/v1/chat/completions, /v1/responses) is now opt-in via
  gateway.platforms.api_server.direct_model_requests (default off) —
  generic OpenAI clients hardcode model names ('gpt-4o', ...) and
  existing deployments rely on those falling back to the gateway
  default. Explicit `provider` requests and the Hermes-native
  session-chat + /v1/runs surfaces are always honored.
  Idea credit: PR NousResearch#22825 by @mssteuer.
- A model_routes alias with no `model` key can no longer leak the
  alias string as the executing model name (defensive; parse-time
  validation already drops such routes).
- Fix mis-indented _run_agent call args in _handle_session_chat_stream.
- Docs: document the opt-in flag.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants