Skip to content

feat(config): support per-model reasoning_effort overrides - #51377

Closed
ScotterMonk wants to merge 1 commit into
NousResearch:mainfrom
ScotterMonk:feat/per-model-reasoning-overrides
Closed

feat(config): support per-model reasoning_effort overrides#51377
ScotterMonk wants to merge 1 commit into
NousResearch:mainfrom
ScotterMonk:feat/per-model-reasoning-overrides

Conversation

@ScotterMonk

Copy link
Copy Markdown
Contributor

Summary

Adds agent.reasoning_overrides dict to config.yaml, letting users configure a per-model reasoning_effort that overrides the global agent.reasoning_effort. This solves the common pain point of wanting xhigh reasoning for Claude Opus but medium for Gemini Flash — without changing it manually each time.

agent:
  reasoning_effort: "medium"       # global default
  reasoning_overrides:
    "openrouter/anthropic/claude-opus-4.5": "xhigh"
    "openai/gpt-5": "low"
    "claude-sonnet-4.6": "high"    # bare model name also works

The helper (resolve_per_model_reasoning_effort) is spelling-tolerant: keys match regardless of provider prefix or dots-vs-dashes normalization. Users can write claude-opus-4.5, claude-opus-4-5, anthropic/claude-opus-4.5, or openrouter/anthropic/claude-opus-4.5 — all match the same model.

Why this approach (not custom_providers[].models)

Issue #15511 proposed scoping per-model reasoning to custom_providers, which was closed as "not planned" because it:

  • Only works for custom providers (native provider models can't use it)
  • Requires fabricating a custom_providers entry
  • Couples transport routing with behavioral settings

This PR uses a top-level agent.reasoning_overrides dict — conceptually consistent with agent.reasoning_effort, supports all providers, and the key format (provider/model) matches what users see in /model.

Resolution priority

  1. Session-scoped /reasoning --session override (gateway only — PR fix(gateway): make /reasoning session-scoped by default #15533, unchanged)
  2. Per-model override from agent.reasoning_overrides (spelling-tolerant, this PR)
  3. Global agent.reasoning_effort (existing)
  4. Provider default (unchanged)

Wired into

  • CLI startup (cli.py) — per-model override checked at agent construction
  • Messaging gateway (gateway/run.py::_load_reasoning_config) — same
  • Desktop/TUI (tui_gateway/server.py::_load_reasoning_config) — same
  • Cron scheduler (cron/scheduler.py) — same
  • /model mid-session switch (agent_runtime_helpers.py::switch_model) — re-resolves reasoning_config on switch, and saves it to _primary_runtime for correct fallback recovery
  • Fallback activation (chat_completion_helpers.py::try_activate_fallback) — re-resolves reasoning_config for the fallback model (best-effort, wrapped in try/except)

Files changed (17 total)

Implementation:

  • hermes_constants.py — helper functions
  • hermes_cli/config.py — DEFAULT_CONFIG + version bump
  • cli.py, gateway/run.py, tui_gateway/server.py, cron/scheduler.py — wire-up
  • agent/agent_runtime_helpers.py/model switch + _primary_runtime snapshot
  • agent/chat_completion_helpers.py — fallback re-resolution

Tests (90 new test cases across 6 files)
Docs: cli-config.yaml.example, website/docs/user-guide/configuration.md, docs/PER_MODEL_REASONING.md

Behavioral guarantees

  1. Backwards compatible: empty reasoning_overrides: {} is the default — zero behavior change
  2. Session override preserved: /reasoning --session still wins over per-model overrides
  3. No false positives: gemini-2.0-flash will NOT match override key gemini-flash
  4. Fallback recovery: restore_primary_runtime() returns reasoning_config to primary's value
  5. Best-effort safety: all dynamic-path wire-ups wrapped in try/except

Fixes #21256

@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/cron Cron scheduler and job management comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have labels Jun 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: implements per-model reasoning_effort overrides via a top-level agent.reasoning_overrides dict — the same goal as feature request #21256 (which proposed scoping it under custom_providers). Cross-linking for reviewer context.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approved

Well-designed feature: per-model reasoning_effort overrides in config.yaml. Different models can now have different reasoning effort levels (e.g., Claude Opus at xhigh, GPT-5 at low).

Looks Good

  • Clean implementation: reasoning_overrides dict in agent: config section
  • Model name matching is flexible (dots/dashes interchangeable, provider prefix optional)
  • reasoning_config is properly restored on model switch and fallback activation
  • Config example documents the feature with clear examples
  • Tests cover the resolution logic
  • Large PR (1181 additions) but well-scoped to one feature across the agent transport layer

Reviewed by Hermes Agent

@WompaJango

Copy link
Copy Markdown
Contributor

Building on @alt-glitch's triage note — wanted to address the relationship between this PR, #20594, and the closed #15511 directly.

This PR and #20594 solve different problems:

They're complementary. The top-level agent.reasoning_overrides dict works for all providers (native + custom), not just custom_providers. That's the key difference from #15511's original proposal — no need to fabricate a custom_providers entry just to set a reasoning level.

Active issues that would benefit from this:

Why top-level instead of custom_providers[].models[]:

  1. Native provider models (Anthropic, OpenAI direct) can't use custom_providers
  2. Keeps transport routing and behavioral settings decoupled
  3. Key format (provider/model) matches what users see in /model
  4. No need to maintain a parallel config surface — agent.reasoning_overrides sits right next to agent.reasoning_effort

Happy to address any concerns about the spelling-tolerant matching or test coverage. The helper generates bounded variant sets (dots↔dashes, provider prefix stripping) and is tested with 90 cases including false-positive guards.

Add agent.reasoning_overrides dict to config.yaml. Users can now set
a reasoning_effort per model, overriding the global agent.reasoning_effort.

Example:
  agent:
    reasoning_effort: "medium"       # global default
    reasoning_overrides:
      "openrouter/anthropic/claude-opus-4.5": "xhigh"
      "openai/gpt-5": "low"
      "claude-sonnet-4.6": "high"    # bare model name also works

The helper is spelling-tolerant: override keys match regardless of
provider prefix or dots-vs-dashes normalization, so users can write
keys in any sensible form and they'll match.

Resolution priority:
1. Session-scoped /reasoning --session override (gateway only; unchanged)
2. Per-model override from agent.reasoning_overrides (spelling-tolerant)
3. Global agent.reasoning_effort (existing)
4. Provider default (unchanged)

Wired into:
- CLI startup (cli.py)
- Messaging gateway agent construction (gateway/run.py)
- Desktop/TUI _load_reasoning_config (tui_gateway/server.py)
- Cron job scheduler (cron/scheduler.py)
- /model mid-session switch (agent/agent_runtime_helpers.py)
  + _primary_runtime now tracks reasoning_config for correct fallback recovery
- Fallback activation (agent/chat_completion_helpers.py::try_activate_fallback)
  + Re-resolves reasoning_config for the fallback model (best-effort)

Closes NousResearch#21256 (per-model reasoning_effort defaults).

Note: no hermes config set agent.reasoning_overrides.<model> support;
users edit the YAML directly. _set_nested splits on "." and would
corrupt model keys containing version dots.
@ScotterMonk
ScotterMonk force-pushed the feat/per-model-reasoning-overrides branch from 864dd95 to d17d31b Compare July 14, 2026 11:44
@WompaJango

Copy link
Copy Markdown
Contributor

Quick status update since my earlier comment — this PR has been stable and all-green for three weeks now:

  • CI: All checks passing, mergeable with no conflicts
  • Review: Approved by @tonydwb
  • Community validation: @Methodician confirmed the real-world use case on feat(config): support per-model reasoning_effort defaults via custom_providers #21256 with a live OpenRouter/DeepSeek test — verified that different reasoning levels (high vs xhigh) are supported upstream and succeed in practice. That thread also confirms the exact pain point: /model and /reasoning are separate knobs today, so switching models leaves reasoning stuck at the wrong level.

The design has held up well under review. The top-level agent.reasoning_overrides approach is simpler than the custom_providers[].models[] route proposed in #15511 (closed as not-planned) because:

  1. Works for native providers (Anthropic, OpenAI direct) — not just custom providers
  2. No need to fabricate a custom_providers entry just to set a reasoning level
  3. Sits right next to agent.reasoning_effort — one place to look, not two
  4. Spelling-tolerant matching handles the dots-vs-dashes mess that model naming creates

90 test cases across 6 test files cover the resolution logic, model switch, fallback recovery, and the YAML-boolean-False edge case (where reasoning_effort: false must not be silently coerced back to enabled).

Would appreciate a maintainer review and merge consideration. Happy to address any remaining feedback.

@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #64458 — your commit was cherry-picked onto current main with your authorship preserved in git history (rebase merge, commit d9cdb81). Thanks for the thorough work, @ScotterMonk: the spelling-tolerant matcher, the six-surface wire-up, and the test coverage all survived intact.

On top of your commit we unified the per-surface resolution logic into a single shared chokepoint (resolve_reasoning_config() in hermes_constants.py) and fixed a gateway edge where overrides resolved against the config default model instead of the session's effective model after a session-only /model switch.

Fixes #21256.

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

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(config): support per-model reasoning_effort defaults via custom_providers

5 participants