turn_recovery.py crossed the 2,000-line gate once the reasoning-replay recovery landed beside it. The stale-encrypted-reasoning rungs and the replay-verdict reset are one topic; they move verbatim to agent/turn_recovery_codex.py.
152 lines
8.2 KiB
Python
152 lines
8.2 KiB
Python
"""Bind a provider route onto a live agent in place, and put the primary route back.
|
|
|
|
``bind_route_entry`` is the fallback chain's swap (client, pool, compressor, reasoning,
|
|
extra_body) shared with per-turn routes such as the voice-chat model; ``reinstall_primary_runtime``
|
|
is the inverse that ``restore_primary_runtime`` runs once its gates pass.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, Optional, Tuple
|
|
|
|
from utils import base_url_host_matches
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def bind_route_entry(agent: Any, entry: dict[str, Any], provider: str, model: str) -> Optional[tuple[str, str]]:
|
|
"""Swap ``agent`` onto ``entry`` (a fallback-chain-shaped dict). Returns ``(old_model,
|
|
old_provider)``, or None when the provider has no usable client. Sets ``_fallback_activated``
|
|
so the next turn's ``restore_primary_runtime`` reverts it; raises on a failed swap."""
|
|
from agent.auxiliary_client import resolve_provider_client
|
|
from agent.chat_completion_helpers import (
|
|
_fallback_api_mode_hint, _fallback_api_mode_resolved, _rebind_fallback_credential_pool,
|
|
_reresolve_fallback_reasoning_config, _rescope_fallback_extra_body, _update_fallback_context_compressor,
|
|
)
|
|
from hermes_cli.fallback_config import resolve_entry_api_key
|
|
# Pass the entry's base_url/api_key so custom endpoints (Ollama Cloud) resolve instead
|
|
# of falling through to OpenRouter defaults.
|
|
base_url_hint = (entry.get("base_url") or "").strip() or None
|
|
api_key_hint = resolve_entry_api_key(entry)
|
|
api_mode_explicit, api_mode = _fallback_api_mode_hint(entry, provider, base_url_hint)
|
|
# Ollama Cloud: OLLAMA_API_KEY from env when the entry has no key. Host match, not
|
|
# substring — GHSA-76xc-57q6-vm5m.
|
|
if base_url_hint and base_url_host_matches(base_url_hint, "ollama.com") and not api_key_hint:
|
|
from agent.secret_scope import get_secret
|
|
api_key_hint = get_secret("OLLAMA_API_KEY") or None
|
|
# raw_codex=True: the main agent needs direct responses.stream() access for Codex providers.
|
|
client, _resolved_model = resolve_provider_client(
|
|
provider, model=model, raw_codex=True, explicit_base_url=base_url_hint, explicit_api_key=api_key_hint,
|
|
api_mode=api_mode)
|
|
if client is None:
|
|
return None
|
|
if provider == "moa":
|
|
# A MoA entry means the preset itself, exactly like ``provider: moa`` in config. The client
|
|
# is the preset's aggregator and only proves the preset resolves; bind the facade with the
|
|
# same pins every other MoA build site uses (#112525, #112623).
|
|
base_url, api_mode = "moa://local", "chat_completions"
|
|
else:
|
|
try:
|
|
from hermes_cli.model_normalize import normalize_model_for_provider
|
|
model = normalize_model_for_provider(model, provider)
|
|
except Exception as norm_err: # health: allow BLE001 -- moved verbatim from try_activate_fallback; a bad catalog entry keeps the raw id
|
|
logger.warning("Could not normalize fallback model %r for provider %r: %s", model, provider, norm_err)
|
|
base_url = str(client.base_url)
|
|
from hermes_cli.providers import is_actual_route
|
|
if is_actual_route(provider, base_url):
|
|
api_mode = "chat_completions"
|
|
elif not api_mode_explicit and api_mode == "chat_completions":
|
|
api_mode = _fallback_api_mode_resolved(agent, provider, model, base_url)
|
|
|
|
old_model, old_provider, old_base_url = agent.model, agent.provider, agent.base_url
|
|
# Clear the per-config context_length override so the new model's own context window is
|
|
# resolved instead of the previous model's stale value (#22387).
|
|
agent._config_context_length = None
|
|
agent.model, agent.provider, agent.requested_provider = model, provider, provider
|
|
agent.base_url, agent.api_mode = base_url, api_mode
|
|
# reasoning_content echo opt-in travels with the active provider; restore_primary_runtime reverts it.
|
|
agent._reasoning_echo_flag = bool(entry.get("reasoning_echo", False))
|
|
if hasattr(agent, "_transport_cache"):
|
|
agent._transport_cache.clear()
|
|
from agent.turn_recovery_codex import reset_codex_reasoning_replay
|
|
reset_codex_reasoning_replay(agent)
|
|
agent._fallback_activated = True
|
|
|
|
_rebind_fallback_credential_pool(agent, provider, model)
|
|
if provider == "moa":
|
|
from agent.moa_loop import bind_moa_runtime
|
|
bind_moa_runtime(agent, model)
|
|
else:
|
|
from agent.client_lifecycle import _swap_fallback_clients
|
|
_swap_fallback_clients(agent, client, provider, model, base_url, api_mode)
|
|
|
|
from agent.agent_runtime_helpers import sync_credential_pool_entry_id
|
|
sync_credential_pool_entry_id(agent)
|
|
|
|
agent._use_prompt_caching, agent._use_native_cache_layout = agent._anthropic_prompt_cache_policy(
|
|
provider=provider, base_url=base_url, api_mode=api_mode, model=model)
|
|
agent._ensure_lmstudio_runtime_loaded() # LM Studio: preload before probing context length
|
|
_update_fallback_context_compressor(agent)
|
|
_reresolve_fallback_reasoning_config(agent)
|
|
_rescope_fallback_extra_body(agent, old_model, old_provider, old_base_url)
|
|
return old_model, old_provider
|
|
|
|
|
|
def reinstall_runtime_snapshot(agent: Any, rt: dict[str, Any]) -> None:
|
|
"""Put ``agent`` on the runtime recorded in ``rt`` (a ``_primary_runtime``-shaped snapshot):
|
|
identity, client, caching flags, compressor, reasoning and prompt identity. Credential pool
|
|
and fallback bookkeeping are the caller's. Raises on failure."""
|
|
from agent.agent_runtime_helpers import (
|
|
_apply_primary_runtime_fields, _rebuild_primary_client, _restore_runtime_capabilities,
|
|
)
|
|
_apply_primary_runtime_fields(agent, rt)
|
|
from agent.turn_recovery_codex import reset_codex_reasoning_replay
|
|
reset_codex_reasoning_replay(agent)
|
|
_restore_runtime_capabilities(agent, rt)
|
|
agent._use_prompt_caching = rt["use_prompt_caching"]
|
|
# Default to native layout for snapshots predating the native-vs-proxy split.
|
|
agent._use_native_cache_layout = rt.get(
|
|
"use_native_cache_layout",
|
|
agent.api_mode == "anthropic_messages" and agent.provider == "anthropic",
|
|
)
|
|
# An operator cache disable (_cache_disabled) must survive snapshot restoration.
|
|
if getattr(agent, "_cache_disabled", False):
|
|
agent._use_prompt_caching = False
|
|
agent._use_native_cache_layout = False
|
|
_rebuild_primary_client(agent, rt, reason="restore_primary")
|
|
agent.context_compressor.update_model(
|
|
model=rt["compressor_model"], context_length=rt["compressor_context_length"],
|
|
base_url=rt["compressor_base_url"], api_key=rt["compressor_api_key"],
|
|
provider=rt["compressor_provider"], api_mode=rt.get("compressor_api_mode", ""),
|
|
)
|
|
# Same rule as fallback activation: refresh an existing verdict only; never-probed sessions stay lazy.
|
|
if getattr(agent, "_compression_feasibility_checked", False) is True:
|
|
from agent.conversation_compression import revalidate_compression_feasibility
|
|
revalidate_compression_feasibility(agent)
|
|
# Older snapshots have no reasoning_config; keep the current value.
|
|
saved_reasoning = rt.get("reasoning_config")
|
|
if saved_reasoning is not None:
|
|
agent.reasoning_config = dict(saved_reasoning)
|
|
# Reset the stale-call circuit breaker: its streak measured the route being left.
|
|
from agent.chat_completion_helpers import _reset_stale_streak, rewrite_prompt_model_identity
|
|
_reset_stale_streak(agent)
|
|
# Undo the identity rewrite so the prompt is byte-identical to the stored copy again
|
|
# (prefix cache match).
|
|
rewrite_prompt_model_identity(agent, rt["model"], rt["provider"])
|
|
|
|
|
|
def reinstall_primary_runtime(
|
|
agent: Any, rt: dict[str, Any], primary_provider: str, primary_model: str, matches_primary,
|
|
load_primary_pool, prefetched_pool=None, prefetched: bool = False,
|
|
) -> None:
|
|
"""Put ``agent`` back on its ``_primary_runtime`` snapshot ``rt`` and clear the fallback state."""
|
|
from agent.agent_runtime_helpers import _rebind_primary_credential_pool
|
|
reinstall_runtime_snapshot(agent, rt)
|
|
_rebind_primary_credential_pool(
|
|
agent, primary_provider, primary_model, matches_primary, load_primary_pool, prefetched_pool, prefetched
|
|
)
|
|
agent._fallback_activated = False
|
|
agent._fallback_index = 0
|
|
agent._rate_limit_backoff_count = 0
|