#!/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"\\n{transcript}\\n" ) 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 Codex CLI remains the sole owner # of refresh-token rotation. 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 that would race the # app-server's canonical refresh-token owner. 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())