atlas-iac/services/hermes/scripts/patch_codex_runtime.py

538 lines
22 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Let Hermes use the authenticated Codex app-server with routed settings."""
from __future__ import annotations
import argparse
from pathlib import Path
PROVIDER_BEFORE = ''' if provider == "openai-codex":
try:
creds = resolve_codex_runtime_credentials()
'''
PROVIDER_AFTER = ''' if provider == "openai-codex":
if (
str(model_cfg.get("openai_runtime") or "").strip().lower()
== "codex_app_server"
):
return {
"provider": "openai-codex",
"api_mode": "codex_app_server",
"base_url": DEFAULT_CODEX_BASE_URL,
"api_key": "codex-cli-runtime",
"source": "codex-cli",
"requested_provider": requested_provider,
}
try:
creds = resolve_codex_runtime_credentials()
'''
SESSION_SIGNATURE_BEFORE = ''' def run_turn(
self,
user_input: Any,
*,
turn_timeout: float = 600.0,
'''
SESSION_SIGNATURE_AFTER = ''' def run_turn(
self,
user_input: Any,
*,
model: Optional[str] = None,
effort: Optional[str] = None,
turn_timeout: float = 600.0,
'''
SESSION_REQUEST_BEFORE = ''' ts = self._client.request(
"turn/start",
{
"threadId": self._thread_id,
"input": [{"type": "text", "text": user_input_text}],
},
timeout=10,
)
'''
SESSION_REQUEST_AFTER = ''' turn_params: dict[str, Any] = {
"threadId": self._thread_id,
"input": [{"type": "text", "text": user_input_text}],
"approvalPolicy": "never",
"sandboxPolicy": {"type": "dangerFullAccess"},
}
if model:
turn_params["model"] = model
if effort:
turn_params["effort"] = effort
ts = self._client.request(
"turn/start",
turn_params,
timeout=10,
)
'''
TURN_BEFORE = ''' try:
turn = agent._codex_session.run_turn(user_input=user_message)
'''
TURN_AFTER = ''' reasoning = getattr(agent, "reasoning_config", None)
effort = (
str(reasoning.get("effort") or "").strip()
if isinstance(reasoning, dict)
else ""
)
try:
turn = agent._codex_session.run_turn(
user_input=user_message,
model=str(getattr(agent, "model", "") or "").strip() or None,
effort=effort or None,
)
'''
FALLBACK_RESOLUTION_BEFORE = ''' # Use centralized router for client construction.
# raw_codex=True because the main agent needs direct responses.stream()
# access for Codex providers.
try:
from agent.auxiliary_client import resolve_provider_client
'''
FALLBACK_RESOLUTION_AFTER = ''' # The Codex subscription is exposed through the authenticated local CLI,
# not as a bearer token that the OpenAI-compatible fallback resolver can
# consume. Activate that runtime directly and let conversation_loop hand
# the already-built turn context to codex app-server.
try:
if fb_provider == "openai-codex":
from hermes_cli.runtime_provider import resolve_runtime_provider
runtime = resolve_runtime_provider(
requested=fb_provider,
target_model=fb_model,
)
if runtime.get("api_mode") == "codex_app_server":
try:
from hermes_cli.model_normalize import normalize_model_for_provider
fb_model = normalize_model_for_provider(fb_model, fb_provider)
except Exception as _norm_err:
logger.warning(
"Could not normalize fallback model %r for provider %r: %s",
fb_model, fb_provider, _norm_err,
)
old_model = agent.model
agent._config_context_length = None
agent.model = fb_model
agent.provider = fb_provider
agent.base_url = str(runtime.get("base_url") or "")
agent.api_key = str(runtime.get("api_key") or "codex-cli-runtime")
agent.api_mode = "codex_app_server"
agent.client = None
agent._anthropic_client = None
agent._credential_pool = None
agent._fallback_activated = True
agent._codex_cross_provider_fallback = True
if hasattr(agent, "_transport_cache"):
agent._transport_cache.clear()
rewrite_prompt_model_identity(agent, fb_model, fb_provider)
agent._buffer_status(
f"🔄 Primary model failed — switching to fallback: "
f"{fb_model} via {fb_provider}"
)
logger.info(
"Fallback activated through Codex app-server: %s%s (%s)",
old_model, fb_model, fb_provider,
)
_reset_stale_streak(agent)
return True
from agent.auxiliary_client import resolve_provider_client
'''
FALLBACK_DISPATCH_BEFORE = ''' while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
# Reset per-turn checkpoint dedup so each iteration can take one snapshot
'''
FALLBACK_DISPATCH_AFTER = ''' while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
# A provider failure can activate the CLI-backed Codex runtime after
# this function's initial runtime dispatch. Re-enter the native Codex
# adapter here instead of attempting an OpenAI-compatible request with
# credentials that deliberately do not exist in Hermes' auth store.
if agent.api_mode == "codex_app_server":
fallback_user_message = user_message
if getattr(agent, "_codex_cross_provider_fallback", False):
from agent.codex_runtime import build_cross_provider_codex_prompt
fallback_user_message = build_cross_provider_codex_prompt(
messages,
user_message,
)
agent._codex_cross_provider_fallback = False
return agent._run_codex_app_server_turn(
user_message=fallback_user_message,
original_user_message=original_user_message,
messages=messages,
effective_task_id=effective_task_id,
should_review_memory=_should_review_memory,
)
# Reset per-turn checkpoint dedup so each iteration can take one snapshot
'''
RETRY_FALLBACK_DISPATCH_BEFORE = ''' while retry_count < max_retries:
# ── Nous Portal rate limit guard ──────────────────────
'''
RETRY_FALLBACK_DISPATCH_AFTER = ''' while retry_count < max_retries:
# Fallback activation happens inside this retry loop. Dispatch a
# newly selected Codex app-server route before the next iteration
# tries to build OpenAI-compatible kwargs from the intentionally
# absent bearer-token client.
if agent.api_mode == "codex_app_server":
fallback_user_message = user_message
if getattr(agent, "_codex_cross_provider_fallback", False):
from agent.codex_runtime import build_cross_provider_codex_prompt
fallback_user_message = build_cross_provider_codex_prompt(
messages,
user_message,
)
agent._codex_cross_provider_fallback = False
return agent._run_codex_app_server_turn(
user_message=fallback_user_message,
original_user_message=original_user_message,
messages=messages,
effective_task_id=effective_task_id,
should_review_memory=_should_review_memory,
)
# ── Nous Portal rate limit guard ──────────────────────
'''
STREAM_RECOVERY_BEFORE = ''' # If we have prior messages, roll back to last complete state
if len(messages) > 1:
agent._vprint(f"{agent.log_prefix} ⏪ Rolling back to last complete assistant turn")
rolled_back_messages = agent._get_messages_up_to_last_assistant(messages)
agent._cleanup_task_resources(effective_task_id)
agent._persist_session(messages, conversation_history)
return {
"final_response": "Response truncated due to output length limit",
"messages": rolled_back_messages,
"api_calls": api_call_count,
"completed": False,
"partial": True,
"error": "Response truncated due to output length limit"
}
else:
# First message was truncated - mark as failed
agent._flush_status_buffer()
agent._vprint(f"{agent.log_prefix}❌ First response truncated - cannot recover", force=True)
agent._persist_session(messages, conversation_history)
return {
"final_response": "First response truncated due to output length limit",
"messages": messages,
"api_calls": api_call_count,
"completed": False,
"failed": True,
"error": "First response truncated due to output length limit"
}
'''
STREAM_RECOVERY_AFTER = ''' # A partial-stream stub is a transport failure, not proof that
# the model exhausted its output budget. Retry the unchanged
# turn through the routed alias so Switchyard can choose a
# healthy provider. Never append the incomplete response.
_is_transport_stub = (
getattr(response, "id", "") == PARTIAL_STREAM_STUB_ID
)
if _is_transport_stub and truncated_tool_call_retries < 4:
truncated_tool_call_retries += 1
agent._buffer_vprint(
f"⚠️ Provider stream ended before a complete response — "
f"rerouting ({truncated_tool_call_retries}/4)..."
)
agent._session_messages = messages
continue
_incomplete_error = (
"Provider stream repeatedly ended before a complete response"
if _is_transport_stub
else "Response truncated due to output length limit"
)
# If we have prior messages, roll back to last complete state.
if len(messages) > 1:
agent._vprint(f"{agent.log_prefix} ⏪ Rolling back to last complete assistant turn")
rolled_back_messages = agent._get_messages_up_to_last_assistant(messages)
agent._cleanup_task_resources(effective_task_id)
agent._persist_session(messages, conversation_history)
return {
"final_response": _incomplete_error,
"messages": rolled_back_messages,
"api_calls": api_call_count,
"completed": False,
"partial": True,
"error": _incomplete_error,
}
agent._flush_status_buffer()
agent._vprint(
f"{agent.log_prefix}❌ First response did not complete",
force=True,
)
agent._persist_session(messages, conversation_history)
return {
"final_response": _incomplete_error,
"messages": messages,
"api_calls": api_call_count,
"completed": False,
"failed": True,
"error": _incomplete_error,
}
'''
FALLBACK_CONTEXT_BEFORE = '''def run_codex_app_server_turn(
'''
FALLBACK_CONTEXT_AFTER = '''def build_cross_provider_codex_prompt(
messages: List[Dict[str, Any]],
user_message: str,
*,
max_chars: int = 120_000,
) -> str:
"""Bridge recent user/assistant context into a new Codex fallback thread."""
def _text(value: Any) -> str:
if isinstance(value, str):
return value
if isinstance(value, list):
parts = []
for item in value:
if isinstance(item, dict):
candidate = item.get("text") or item.get("content")
if isinstance(candidate, str):
parts.append(candidate)
return "\\n".join(parts)
return ""
exchanges = []
for message in messages:
if not isinstance(message, dict):
continue
role = str(message.get("role") or "").strip().lower()
if role not in {"user", "assistant"}:
continue
content = _text(message.get("content")).strip()
if content:
exchanges.append(f"{role.upper()}: {content}")
transcript = "\\n\\n".join(exchanges)
if len(transcript) > max_chars:
transcript = "[Earlier conversation omitted for context size.]\\n\\n" + transcript[-max_chars:]
if not transcript:
transcript = f"USER: {user_message}"
return (
"Continue this Hermes conversation after the previously selected "
"provider exhausted its capacity. Preserve the objective and answer "
"the latest user request; do not restart or discard completed work.\\n\\n"
f"<hermes_conversation>\\n{transcript}\\n</hermes_conversation>"
)
def run_codex_app_server_turn(
'''
AUXILIARY_TOKEN_BEFORE = ''' except Exception as exc:
logger.debug("Could not read Codex auth for auxiliary client: %s", exc)
return None
'''
AUXILIARY_TOKEN_AFTER = ''' except Exception as exc:
logger.debug("Could not read Hermes Codex auth for auxiliary client: %s", exc)
# Agent Hermes deliberately runs Codex through the authenticated CLI
# app-server and does not duplicate those credentials into Hermes'
# provider auth store. Auxiliary tasks still use Hermes' native Codex
# Responses adapter, so read the current CLI access token without
# copying or refreshing it here. The authenticated Codex broker and
# Codex CLI coordinate through this canonical file; this auxiliary
# path remains read-only and never creates a metered API-key lane.
try:
codex_home = os.environ.get("CODEX_HOME", "").strip()
if not codex_home:
codex_home = str(Path.home() / ".codex")
auth_path = Path(codex_home).expanduser() / "auth.json"
payload = json.loads(auth_path.read_text(encoding="utf-8"))
tokens = payload.get("tokens") or {}
access_token = tokens.get("access_token")
if not isinstance(access_token, str) or not access_token.strip():
return None
# Match the native expiry check above. An expired CLI token is not
# refreshed from this side channel because the broker owns the
# serialized, atomic refresh operation for chat boundaries.
try:
import base64
jwt_payload = access_token.split(".")[1]
jwt_payload += "=" * (-len(jwt_payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(jwt_payload))
expires_at = claims.get("exp", 0)
if expires_at and time.time() > expires_at:
logger.debug("Codex CLI access token is expired; skipping auxiliary route")
return None
except Exception:
pass
return access_token.strip()
except Exception as cli_exc:
logger.debug("Could not read Codex CLI auth for auxiliary client: %s", cli_exc)
return None
'''
def _replace_once(content: str, before: str, after: str, label: str) -> str:
"""Apply one exact replacement and fail closed when upstream drifts."""
if content.count(before) != 1:
raise RuntimeError(f"Hermes {label} patch context changed")
return content.replace(before, after, 1)
def patch_provider(source: Path, destination: Path) -> None:
"""Allow the explicit Codex app-server runtime without duplicate OAuth."""
content = source.read_text(encoding="utf-8")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
_replace_once(
content,
PROVIDER_BEFORE,
PROVIDER_AFTER,
"runtime provider",
),
encoding="utf-8",
)
def patch_session(source: Path, destination: Path) -> None:
"""Pass the router's selected model and effort to each Codex turn."""
content = source.read_text(encoding="utf-8")
content = _replace_once(
content,
SESSION_SIGNATURE_BEFORE,
SESSION_SIGNATURE_AFTER,
"app-server session signature",
)
content = _replace_once(
content,
SESSION_REQUEST_BEFORE,
SESSION_REQUEST_AFTER,
"app-server turn request",
)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content, encoding="utf-8")
def patch_turn(source: Path, destination: Path) -> None:
"""Forward route metadata and preserve context on Codex failover."""
content = source.read_text(encoding="utf-8")
content = _replace_once(
content,
FALLBACK_CONTEXT_BEFORE,
FALLBACK_CONTEXT_AFTER,
"Codex fallback context",
)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
_replace_once(content, TURN_BEFORE, TURN_AFTER, "Codex turn"),
encoding="utf-8",
)
def patch_fallback(source: Path, destination: Path) -> None:
"""Activate the CLI-backed Codex runtime in Hermes' fallback chain."""
content = source.read_text(encoding="utf-8")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
_replace_once(
content,
FALLBACK_RESOLUTION_BEFORE,
FALLBACK_RESOLUTION_AFTER,
"Codex fallback activation",
),
encoding="utf-8",
)
def patch_loop(source: Path, destination: Path) -> None:
"""Dispatch a mid-turn Codex fallback through app-server."""
content = source.read_text(encoding="utf-8")
content = _replace_once(
content,
FALLBACK_DISPATCH_BEFORE,
FALLBACK_DISPATCH_AFTER,
"Codex outer-loop fallback dispatch",
)
content = _replace_once(
content,
RETRY_FALLBACK_DISPATCH_BEFORE,
RETRY_FALLBACK_DISPATCH_AFTER,
"Codex retry-loop fallback dispatch",
)
content = patch_stream_recovery_text(content)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(content, encoding="utf-8")
def patch_stream_recovery_text(content: str) -> str:
"""Retry transport stubs and keep output-limit reporting truthful."""
return _replace_once(
content,
STREAM_RECOVERY_BEFORE,
STREAM_RECOVERY_AFTER,
"partial stream recovery",
)
def patch_stream_recovery(source: Path, destination: Path) -> None:
"""Patch only shared stream recovery for non-Codex Hermes lanes."""
content = source.read_text(encoding="utf-8")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
patch_stream_recovery_text(content),
encoding="utf-8",
)
def patch_auxiliary(source: Path, destination: Path) -> None:
"""Let auxiliary tasks reuse the current Codex CLI access token safely."""
content = source.read_text(encoding="utf-8")
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
_replace_once(
content,
AUXILIARY_TOKEN_BEFORE,
AUXILIARY_TOKEN_AFTER,
"Codex CLI auxiliary auth",
),
encoding="utf-8",
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("provider_source", type=Path)
parser.add_argument("provider_destination", type=Path)
parser.add_argument("session_source", type=Path)
parser.add_argument("session_destination", type=Path)
parser.add_argument("turn_source", type=Path)
parser.add_argument("turn_destination", type=Path)
parser.add_argument("fallback_source", type=Path)
parser.add_argument("fallback_destination", type=Path)
parser.add_argument("loop_source", type=Path)
parser.add_argument("loop_destination", type=Path)
parser.add_argument("auxiliary_source", type=Path)
parser.add_argument("auxiliary_destination", type=Path)
args = parser.parse_args()
patch_provider(args.provider_source, args.provider_destination)
patch_session(args.session_source, args.session_destination)
patch_turn(args.turn_source, args.turn_destination)
patch_fallback(args.fallback_source, args.fallback_destination)
patch_loop(args.loop_source, args.loop_destination)
patch_auxiliary(args.auxiliary_source, args.auxiliary_destination)
return 0
if __name__ == "__main__":
raise SystemExit(main())