fix(cli): /model switch, session restore and /new re-resolve reasoning effort for the new model

The classic CLI resolves `reasoning_config` once at startup for the launch model and
passes that field into every lazily built agent. `_stage_and_swap_model` (typed /model
and picker), `_restore_session_model` (--resume, /resume) and `new_session` (/new)
moved `self.model` without re-running the chokepoint, so the first turn after a switch
issued before the first message went out with the OLD model's effort — always-thinking
models that accept only their own level set (GLM/ARK: low/high/max) reject that with a
non-retryable HTTP 400. `agent.switch_model` already re-resolves its own copy, so only
the CLI-level field was stale.

- `_resolve_cli_reasoning(cli)` runs `resolve_reasoning_config(CLI_CONFIG, cli.model)`
  and is called BEFORE the agent branch on all three paths, covering the lazy-build case.
- `reasoning_config` joins `_RUNTIME_FIELDS`, so a failed in-place swap and the
  `/model X --once` restore roll it back with the rest of the route (drops the explicit
  one-turn restore line the field loop now covers).
- `/new` resolves after the config-default model reset, so the default model's
  per-model override is kept while a `/reasoning` session override is still dropped.
- Docs: the override list names the switch-before-first-message, resume and /new paths.

Slimmer redo of #96023 (@liuhao1024): same core hunk, without the `--reasoning`-pins-
for-the-whole-run flag (the live-agent path already re-resolves on switch, so the CLI
field follows the same rule) and with the two entry points #96023 missed
(`_restore_session_model`, `_RUNTIME_FIELDS`), which @catecholamin identified on
#112921. #112924 (@li-lizhe) covers the same `_stage_and_swap_model` hunk.

Fixes #112921
Fixes #96012

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: li-lizhe <li-lizhe@noreply.github.com>
This commit is contained in:
teknium1
2026-09-16 10:07:22 -07:00
committed by Teknium
parent d5ff004af7
commit 77f5de23dd
4 changed files with 104 additions and 10 deletions

View File

@@ -18,16 +18,31 @@ from rich.markup import escape as _escape
from utils import base_url_host_matches
# CLI-level fields describing the active model route; snapshotted before a switch / one-turn
# override and restored wholesale on rollback.
# override and restored wholesale on rollback. ``reasoning_config`` rides along because it is
# resolved per model: a failed swap or `/model X --reasoning high --once` must not leave the
# new model's effort behind with the old route.
_RUNTIME_FIELDS = (
"model", "provider", "requested_provider", "_explicit_api_key", "_explicit_base_url",
"api_key", "base_url", "api_mode")
"api_key", "base_url", "api_mode", "reasoning_config")
def _runtime_fields(cli) -> dict:
return {key: getattr(cli, key, None) for key in _RUNTIME_FIELDS}
def _resolve_cli_reasoning(cli) -> None:
"""Re-resolve the CLI-level ``reasoning_config`` for ``cli.model`` through the shared chokepoint
(per-model ``reasoning_overrides`` > global ``agent.reasoning_effort``). Startup resolves it once
for the launch model; every path that moves ``cli.model`` must call this BEFORE the agent branch,
because a lazily built agent inherits this field — an always-thinking model then goes out with
the launch model's effort and 400s (#112921, #96012). ``agent.switch_model`` re-resolves its own
copy for the live-agent path."""
from cli import CLI_CONFIG
from hermes_constants import resolve_reasoning_config
# getattr: tests drive /new unbound on a SimpleNamespace without ``model`` (blank -> config default).
cli.reasoning_config = resolve_reasoning_config(CLI_CONFIG, getattr(cli, "model", None) or "")
def stored_session_route(session_meta, *, current_model, current_provider):
"""The route a resumed session should run on, or ``None`` when the stored one is absent or
already current. Returns ``(model, provider, base_url, api_mode, provider_changed)``; the
@@ -427,8 +442,9 @@ class CLIModelSwitchMixin:
"Credential re-resolution for resumed session provider "
"%s failed; keeping ambient credentials",
stored_provider, exc_info=True)
_resolve_cli_reasoning(self)
# Mid-chat /resume swaps the live agent; on startup --resume _init_agent picks up
# self.model / self.provider.
# self.model / self.provider / self.reasoning_config.
if self.agent is not None:
try:
self.agent.switch_model(
@@ -516,9 +532,6 @@ class CLIModelSwitchMixin:
for key in _RUNTIME_FIELDS:
if key in snapshot:
setattr(self, key, snapshot.get(key))
# `/model X --reasoning high --once` must not leave the effort behind with the model.
if "reasoning_config" in snapshot:
self.reasoning_config = snapshot["reasoning_config"]
agent = getattr(self, "agent", None)
if agent is None:
@@ -602,6 +615,7 @@ class CLIModelSwitchMixin:
self.base_url = result.base_url
if result.api_mode:
self.api_mode = result.api_mode
_resolve_cli_reasoning(self)
if self.agent is not None:
try: