Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5475,12 +5475,15 @@ def _apply_model_switch_result(self, result, persist_global: bool) -> None:

if self.agent is not None:
try:
if result.credential_pool is not None:
self._credential_pool = result.credential_pool
self.agent.switch_model(
new_model=result.new_model,
new_provider=result.target_provider,
api_key=result.api_key,
base_url=result.base_url,
api_mode=result.api_mode,
credential_pool=result.credential_pool,
)
except Exception as exc:
_cprint(f" ⚠ Agent swap failed ({exc}); change applied to next session.")
Expand Down Expand Up @@ -5699,12 +5702,15 @@ def _handle_model_switch(self, cmd_original: str):
# Apply to running agent (in-place swap)
if self.agent is not None:
try:
if result.credential_pool is not None:
self._credential_pool = result.credential_pool
self.agent.switch_model(
new_model=result.new_model,
new_provider=result.target_provider,
api_key=result.api_key,
base_url=result.base_url,
api_mode=result.api_mode,
credential_pool=result.credential_pool,
)
except Exception as exc:
_cprint(f" ⚠ Agent swap failed ({exc}); change applied to next session.")
Expand Down
23 changes: 21 additions & 2 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,8 @@ class GatewayRunner:
_restart_detached: bool = False
_restart_via_service: bool = False
_stop_task: Optional[asyncio.Task] = None
_session_model_overrides: Dict[str, Dict[str, str]] = {}
# Values are mostly str but credential_pool is a CredentialPool instance.
_session_model_overrides: Dict[str, Dict[str, Any]] = {}
_session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}
# Stale-code self-check defaults (see _detect_stale_code()). Class-level
# so tests that construct GatewayRunner via ``object.__new__`` without
Expand Down Expand Up @@ -1058,7 +1059,7 @@ def __init__(self, config: Optional[GatewayConfig] = None):

# Per-session model overrides from /model command.
# Key: session_key, Value: dict with model/provider/api_key/base_url/api_mode
self._session_model_overrides: Dict[str, Dict[str, str]] = {}
self._session_model_overrides: Dict[str, Dict[str, Any]] = {}
# Per-session reasoning effort overrides from /reasoning.
# Key: session_key, Value: parsed reasoning config dict.
self._session_reasoning_overrides: Dict[str, Dict[str, Any]] = {}
Expand Down Expand Up @@ -1441,7 +1442,23 @@ def _resolve_session_agent_runtime(
"base_url": override.get("base_url"),
"api_mode": override.get("api_mode"),
}
# Carry credential_pool from override if stored (new behavior)
if override.get("credential_pool") is not None:
override_runtime["credential_pool"] = override["credential_pool"]
if override_runtime.get("api_key"):
# Credential pool fix: /model stores api_key but may lack
# credential_pool (ModelSwitchResult has no such field).
# When the provider is a pool name (e.g. "custom:mimo-sgp-friend"),
# resolve the pool here so the agent can rotate on 429/402.
_prov = override.get("provider", "")
if _prov.startswith("custom:") and "credential_pool" not in override_runtime:
try:
from agent.credential_pool import load_pool
_pool = load_pool(_prov)
if _pool and _pool.has_credentials():
override_runtime["credential_pool"] = _pool
except Exception:
pass
logger.debug(
"Session model override (fast): session=%s config_model=%s -> override_model=%s provider=%s",
resolved_session_key or "", model, override_model,
Expand Down Expand Up @@ -7532,6 +7549,7 @@ async def _on_model_selected(
api_key=result.api_key,
base_url=result.base_url,
api_mode=result.api_mode,
credential_pool=result.credential_pool,
)
except Exception as exc:
logger.warning("In-place model switch failed for cached agent: %s", exc)
Expand All @@ -7553,6 +7571,7 @@ async def _on_model_selected(
"api_key": result.api_key,
"base_url": result.base_url,
"api_mode": result.api_mode,
"credential_pool": result.credential_pool,
}

# Evict cached agent so the next turn creates a fresh agent from the
Expand Down
13 changes: 12 additions & 1 deletion hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
import logging
import re
from dataclasses import dataclass
from typing import List, NamedTuple, Optional
from typing import TYPE_CHECKING, List, NamedTuple, Optional

if TYPE_CHECKING:
from agent.credential_pool import CredentialPool

from hermes_cli.providers import (
custom_provider_slug,
Expand Down Expand Up @@ -246,6 +249,10 @@ class ModelSwitchResult:
capabilities: Optional[ModelCapabilities] = None
model_info: Optional[ModelInfo] = None
is_global: bool = False
# CredentialPool for the resolved target provider, when one was loaded.
# Callers must propagate this into runtime kwargs / session overrides so
# per-provider 429 rotation continues to work after a /model switch.
credential_pool: Optional["CredentialPool"] = None


@dataclass
Expand Down Expand Up @@ -818,6 +825,7 @@ def switch_model(
api_key = current_api_key
base_url = current_base_url
api_mode = ""
_resolved_pool = None

if provider_changed or explicit_provider:
try:
Expand All @@ -828,6 +836,7 @@ def switch_model(
api_key = runtime.get("api_key", "")
base_url = runtime.get("base_url", "")
api_mode = runtime.get("api_mode", "")
_resolved_pool = runtime.get("credential_pool")
except Exception as e:
return ModelSwitchResult(
success=False,
Expand All @@ -853,6 +862,7 @@ def switch_model(
api_key = runtime.get("api_key", "")
base_url = runtime.get("base_url", "")
api_mode = runtime.get("api_mode", "")
_resolved_pool = runtime.get("credential_pool")
except Exception:
pass

Expand Down Expand Up @@ -978,6 +988,7 @@ def switch_model(
capabilities=capabilities,
model_info=model_info,
is_global=is_global,
credential_pool=_resolved_pool,
)


Expand Down
11 changes: 10 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2278,7 +2278,7 @@ def _ensure_lmstudio_runtime_loaded(self, config_context_length: Optional[int] =
except Exception as err:
logger.debug("LM Studio preload skipped: %s", err)

def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mode=''):
def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mode='', credential_pool=None):
"""Switch the model/provider in-place for a live agent.

Called by the /model command handlers (CLI and gateway) after
Expand Down Expand Up @@ -2325,6 +2325,15 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
if api_key:
self.api_key = api_key

# ── Swap credential pool if provided ──
# The /model pipeline resolves pools via load_pool() but the result
# was historically lost in the ModelSwitchResult → session-override
# chain. Callers that resolve the pool themselves can now pass it
# through so the agent can rotate on 429/402 after a mid-session
# model switch.
if credential_pool is not None:
self._credential_pool = credential_pool

# ── Build new client ──
if api_mode == "anthropic_messages":
from agent.anthropic_adapter import (
Expand Down