Skip to content

Add per-turn and per-image model overrides - #29923

Closed
deusyu wants to merge 4 commits into
NousResearch:mainfrom
deusyu:codex-model-overrides-29914
Closed

Add per-turn and per-image model overrides#29923
deusyu wants to merge 4 commits into
NousResearch:mainfrom
deusyu:codex-model-overrides-29914

Conversation

@deusyu

@deusyu deusyu commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add /model --once support across CLI, TUI, and gateway, restoring the previous model/runtime after one assistant turn.
  • Add one-call provider and model overrides to image_generate.
  • Preserve existing session/global /model behavior while adding cleanup for pending one-turn overrides.

Fixes #29914

Tests

  • scripts/run_tests.sh tests/hermes_cli/test_model_switch_once_flags.py tests/hermes_cli/test_cli_model_once.py tests/hermes_cli/test_apply_model_switch_result_context.py tests/hermes_cli/test_model_switch_custom_providers.py tests/tools/test_image_generation_plugin_dispatch.py tests/tools/test_image_generation.py tests/gateway/test_model_command_custom_providers.py tests/gateway/test_model_switch_persistence.py tests/gateway/test_session_model_override_routing.py tests/test_tui_gateway_server.py -- -q
  • uv run --no-sync python -m pytest -q tests/hermes_cli/test_model_switch_once_flags.py tests/hermes_cli/test_cli_model_once.py tests/hermes_cli/test_apply_model_switch_result_context.py tests/hermes_cli/test_model_switch_custom_providers.py tests/tools/test_image_generation_plugin_dispatch.py tests/tools/test_image_generation.py tests/gateway/test_model_command_custom_providers.py tests/gateway/test_model_switch_persistence.py tests/gateway/test_session_model_override_routing.py tests/test_tui_gateway_server.py::test_config_set_model_once_keeps_env_and_records_restore tests/test_tui_gateway_server.py::test_config_set_model_once_requires_live_session tests/test_tui_gateway_server.py::test_config_set_model_session_switch_clears_pending_once_restore tests/test_tui_gateway_server.py::test_restore_agent_model_runtime_falls_back_to_switch_model tests/test_tui_gateway_server.py::test_config_set_model_syncs_inference_provider_env tests/test_tui_gateway_server.py::test_config_set_model_global_persists tests/test_tui_gateway_server.py::test_config_set_model_syncs_tui_provider_env
  • uv run --no-sync python -m pytest -q tests/hermes_cli/test_user_providers_model_switch.py tests/hermes_cli/test_model_provider_persistence.py tests/hermes_cli/test_model_switch_variant_tags.py tests/hermes_cli/test_model_switch_context_display.py tests/hermes_cli/test_anthropic_model_flow_stale_oauth.py tests/hermes_cli/test_custom_provider_model_switch.py tests/hermes_cli/test_codex_cli_model_picker.py tests/run_agent/test_switch_model_context.py tests/run_agent/test_switch_model_fallback_prune.py tests/tools/test_image_generation_env.py
  • python3.12 -m compileall hermes_cli/model_switch.py cli.py tui_gateway/server.py gateway/run.py tools/image_generation_tool.py tests/hermes_cli/test_model_switch_once_flags.py tests/hermes_cli/test_cli_model_once.py tests/gateway/test_model_command_custom_providers.py tests/gateway/test_model_switch_persistence.py tests/gateway/test_session_model_override_routing.py tests/tools/test_image_generation_plugin_dispatch.py tests/tools/test_image_generation.py tests/test_tui_gateway_server.py
  • git diff --check

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) tool/vision Vision analysis and image generation P2 Medium — degraded but workaround exists labels May 21, 2026
@lkz-de

lkz-de commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

I checked current main plus nearby image-gen work while validating a local patch here. #29923 already covers the core once-per-call provider / model override surface for image_generate, so I do not think a separate fresh PR to main for that broad feature is justified.

What still looks missing relative to current provider behavior is narrower:

  • plugins/image_gen/openrouter/__init__.py should thread an explicit per-call model into _resolve_model_chain() so OpenRouter/Nous routes actually honor the override at provider level rather than only at generic dispatch level.
  • the managed Krea fast-path should read the per-call model_override before falling back to configured model.
  • in direct FAL mode, importing the SDK may still need FAL_KEY hydrated from Hermes config when the user keeps secrets only in the Hermes env/config file.

Local verification I ran while checking the overlap:

  • pytest -q tests/tools/test_image_generation.py tests/tools/test_image_generation_image_to_image.py tests/plugins/image_gen/test_openrouter_compat_provider.py -o addopts=115 passed

So my recommendation is: keep #29923 as the main PR, and if helpful I can prepare a focused follow-up patch for those provider-specific gaps rather than open a duplicate broad override PR.

@lkz-de

lkz-de commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

I reproduced a remaining tool-surface gap on current main while testing image editing.

Provider plugins can already honor per-call model selection, but image_generate still drops explicit provider / model args inside _handle_image_generate, so the request can silently route using configured defaults instead of the requested backend/model.

Concrete repro from local testing:

  • call _handle_image_generate({... "provider": "openai-codex", "model": "gpt-image-2-medium", "image_url": ...})
  • expected: dispatch through Codex edit
  • current main behavior: explicit override is not threaded through tool dispatch / managed Krea routing / direct FAL model resolution

I opened a focused follow-up with tests here:

That patch:

  • exposes optional provider / model on the image_generate schema
  • threads them through _handle_image_generate
  • honors them in plugin dispatch, managed Krea routing, and direct FAL model resolution
  • adds focused tests for explicit provider/model dispatch

Verification on the follow-up branch:

  • targeted override tests pass
  • a local end-to-end _handle_image_generate(...) call with explicit provider=openai-codex, model=gpt-image-2-medium, and image_url=... successfully produced an edited image

@teknium1 teknium1 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.

Thanks for carrying the one-turn restore state across CLI, gateway, and TUI surfaces. The current-main premise is real: cli.py:8045 still uses the legacy parser, while tools/image_generation_tool.py:1181 and :1513 keep image backend/model selection user-configured only.

Problems

  • In the TUI path, the new --once branch avoids session["model_override"] (tui_gateway/server.py:2885 in this diff), but _run_prompt_submit() calls _sync_agent_model_with_config() before the turn (tui_gateway/server.py:8945). That helper only skips synchronization when session.get("model_override") is present (:3022), so the intended override can be reverted before its first request.
  • In the gateway path, the temporary override is written before the existing session-store persistence call (gateway/slash_commands.py:1863 in this diff; current persistence is at :1879-1887). The new restore helper restores only the in-memory map, so a restart can rehydrate the temporary override.

Suggested changes

  • Guard TUI pre-turn config synchronization with pending once state and add a test that observes the model actually used for the first turn.
  • Avoid persisting a once override, or restore the persisted value alongside the in-memory value; test gateway restart after the once turn.
  • Coordinate the overlapping image lane with open #59815.

Automated hermes-sweeper review.

Comment thread tui_gateway/server.py
@@ -2832,7 +2885,7 @@ def _apply_model_switch(
# contamination bug). agent.switch_model() above already mutated the right

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.

_run_prompt_submit() calls _sync_agent_model_with_config() before the turn, and its only opt-out is session["model_override"]. Because this branch suppresses that key for --once, the configured model can replace the one-turn model before run_conversation; preserve a transient guard until the first turn finishes.

Comment thread gateway/slash_commands.py
@@ -1850,6 +1863,14 @@ async def _finish_switch() -> str:
"base_url": result.base_url,

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.

This temporary override is still passed to the unconditional session-store write below. _restore_pending_one_turn_model_override() restores only the in-memory map, so a restart may rehydrate this one-turn choice permanently. Skip the write for --once, or restore the persisted override too.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state 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
@lkz-de

lkz-de commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

I rechecked the current head (003b9acd) against current main, and teknium's two /model --once findings reproduce from my side too:

  • in tui_gateway/server.py, the --once path avoids session["model_override"], so the pre-turn _sync_agent_model_with_config() path can still clobber the one-turn override before first use in fresh/resumed/config-change cases
  • in gateway/slash_commands.py, the temporary override still goes through the existing set_model_override(...) write-through path, so a gateway restart can rehydrate what should have been a one-turn-only choice

Given the review's note to coordinate the image lane with #59815, the cleanest split now looks like:

#59815 is mine — happy to rebase it around whatever lands here first if that helps keep the split low-friction.

teknium1 pushed a commit that referenced this pull request Jul 18, 2026
Adds --once to /model across CLI, TUI, and gateway: switch model for the
next turn only, restoring the previous model in a finally block so
success, exception, and interrupt all revert. Parsing extends
parse_model_flags_detailed(); resolve_persist_behavior() treats --once
as a persistence opt-out; --global + --once is rejected.

Salvaged from PR #29923 (image-generation lane split to #59815 per
review; conflict resolution against current main by the maintainers).
teknium1 added a commit that referenced this pull request Jul 18, 2026
Fixes the two review defects that kept PR #29923 open, plus docs:

- gateway: exclude --once from the session-store write-through. The
  once-override lived only in memory before, but the write-through
  persisted it, so a gateway restart before the finally-restore
  rehydrated a supposedly one-turn model permanently.
- TUI: skip _sync_agent_model_with_config while a one-turn restore is
  pending. The once-model is deliberately not pinned as a session
  model_override, so the config sync saw a model mismatch and clobbered
  the once-override back to the config model before the turn ran.
- tests: real _handle_model_command drive asserting --once never
  touches set_model_override while --session still does; restore-pop
  idempotency.
- docs: /model --once in configuring-models.md with an honest
  prompt-cache cost note (one-shot switch breaks the cached prefix
  twice; wins for short sessions and cheap-to-expensive escalation).
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #67113 — your commit was cherry-picked onto current main with your authorship preserved in git history. Thanks for a well-built feature: the ModelFlagParseResult dataclass refactor with the legacy 5-tuple shim was exactly the right compatibility approach, and the restore-in-finally pattern across all three frontends held up cleanly through review.

Two adjustments landed on top in the salvage:

  • the gateway lane no longer writes the once-override through to the session store (a restart mid-turn used to rehydrate it permanently)
  • the TUI skips the pre-turn config-model sync while a once-override is active (it used to clobber the override before the turn ran)

The per-call image_generate override lane stayed out per the review discussion — that half remains tracked in #59815. Docs for --once are live on the configuring-models page.

@teknium1 teknium1 closed this Jul 18, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Adds --once to /model across CLI, TUI, and gateway: switch model for the
next turn only, restoring the previous model in a finally block so
success, exception, and interrupt all revert. Parsing extends
parse_model_flags_detailed(); resolve_persist_behavior() treats --once
as a persistence opt-out; --global + --once is rejected.

Salvaged from PR NousResearch#29923 (image-generation lane split to NousResearch#59815 per
review; conflict resolution against current main by the maintainers).
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Fixes the two review defects that kept PR NousResearch#29923 open, plus docs:

- gateway: exclude --once from the session-store write-through. The
  once-override lived only in memory before, but the write-through
  persisted it, so a gateway restart before the finally-restore
  rehydrated a supposedly one-turn model permanently.
- TUI: skip _sync_agent_model_with_config while a one-turn restore is
  pending. The once-model is deliberately not pinned as a session
  model_override, so the config sync saw a model mismatch and clobbered
  the once-override back to the config model before the turn ran.
- tests: real _handle_model_command drive asserting --once never
  touches set_model_override while --session still does; restore-pop
  idempotency.
- docs: /model --once in configuring-models.md with an honest
  prompt-cache cost note (one-shot switch breaks the cached prefix
  twice; wins for short sessions and cheap-to-expensive escalation).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) 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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/vision Vision analysis and image generation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add first-class per-turn and per-tool-call model overrides

4 participants