Skip to content
This repository was archived by the owner on Jul 4, 2026. It is now read-only.
Merged
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
65 changes: 52 additions & 13 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,25 +203,45 @@ def _parse_api_mode(raw: Any) -> Optional[str]:
return None


def _parse_transport_api_mode(raw: Any) -> Optional[str]:
"""Validate a provider transport value.

``codex_app_server`` is a runtime host, not an endpoint transport. Custom
providers must resolve to ``codex_responses`` first, then opt into the
app-server via ``model.openai_runtime``.
"""
mode = _parse_api_mode(raw)
if mode == "codex_app_server":
return None
return mode


def _maybe_apply_codex_app_server_runtime(
*,
provider: str,
api_mode: str,
model_cfg: Optional[Dict[str, Any]],
) -> str:
"""Optional opt-in: rewrite api_mode → "codex_app_server" for OpenAI/Codex
providers when the user has explicitly enabled that runtime via
`model.openai_runtime: codex_app_server` in config.yaml.
providers, or a custom Codex Responses proxy, when the user has explicitly
enabled that runtime via `model.openai_runtime: codex_app_server` in
config.yaml.

Default behavior is preserved: when the key is unset, "auto", or empty,
this function is a no-op. Only providers in {"openai", "openai-codex"}
are eligible — other providers (anthropic, openrouter, etc.) cannot be
rerouted through codex.
this function is a no-op. Only providers in {"openai", "openai-codex"} and
custom providers that already resolved to ``codex_responses`` are eligible
— other providers (anthropic, openrouter, custom chat proxies, etc.) cannot
be rerouted through codex.

Returns the (possibly-rewritten) api_mode."""
if not model_cfg:
return api_mode
if provider not in {"openai", "openai-codex"}:
provider_norm = (provider or "").strip().lower()
api_mode_norm = (api_mode or "").strip().lower()
eligible_provider = provider_norm in {"openai", "openai-codex"} or (
provider_norm == "custom" and api_mode_norm == "codex_responses"
)
if not eligible_provider:
return api_mode
runtime = str(model_cfg.get("openai_runtime") or "").strip().lower()
if runtime == "codex_app_server":
Expand Down Expand Up @@ -478,7 +498,7 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
# already does, so without this lift every migrated config
# silently downgrades codex_responses / anthropic_messages
# providers to chat_completions in the resolved runtime.
api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
api_mode = _parse_transport_api_mode(entry.get("api_mode") or entry.get("transport"))
if api_mode:
result["api_mode"] = api_mode
return result
Expand All @@ -496,7 +516,7 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
"api_key": resolved_api_key,
"model": entry.get("default_model", ""),
}
api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
api_mode = _parse_transport_api_mode(entry.get("api_mode") or entry.get("transport"))
if api_mode:
result["api_mode"] = api_mode
return result
Expand Down Expand Up @@ -539,7 +559,7 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
result["key_env"] = key_env
if provider_key:
result["provider_key"] = provider_key
api_mode = _parse_api_mode(entry.get("api_mode"))
api_mode = _parse_transport_api_mode(entry.get("api_mode"))
if api_mode:
result["api_mode"] = api_mode
model_name = str(entry.get("model", "") or "").strip()
Expand Down Expand Up @@ -575,10 +595,12 @@ def _resolve_named_custom_runtime(
pass
if requested_norm == "custom" and explicit_base_url:
base_url = explicit_base_url.strip().rstrip("/")
model_cfg = _get_model_config()
configured_mode = _parse_transport_api_mode(model_cfg.get("api_mode"))
# Check credential pool first — mirrors the named-custom-provider path
# so bare `provider: custom` with a configured custom_providers entry
# also gets its api_key from the pool instead of env var fallbacks.
pool_result = _try_resolve_from_custom_pool(base_url, "custom", None)
pool_result = _try_resolve_from_custom_pool(base_url, "custom", configured_mode)
if pool_result:
pool_result["source"] = "direct-alias"
return pool_result
Expand All @@ -593,7 +615,9 @@ def _resolve_named_custom_runtime(
) or "no-key-required"
return {
"provider": "custom",
"api_mode": _detect_api_mode_for_url(base_url) or "chat_completions",
"api_mode": configured_mode
or _detect_api_mode_for_url(base_url)
or "chat_completions",
"base_url": base_url,
"api_key": api_key,
"source": "direct-alias",
Expand Down Expand Up @@ -746,8 +770,9 @@ def _resolve_openrouter_runtime(
if effective_provider == "custom" and base_url:
# Pass requested_provider so pool lookup prefers name match over base_url,
# fixing credential mix-ups when multiple custom providers share a base_url.
configured_mode = _parse_transport_api_mode(model_cfg.get("api_mode"))
pool_result = _try_resolve_from_custom_pool(
base_url, effective_provider, _parse_api_mode(model_cfg.get("api_mode")),
base_url, effective_provider, configured_mode,
provider_name=requested_provider if requested_norm != "custom" else None,
)
if pool_result:
Expand All @@ -758,7 +783,11 @@ def _resolve_openrouter_runtime(

return {
"provider": effective_provider,
"api_mode": _parse_api_mode(model_cfg.get("api_mode"))
"api_mode": (
_parse_transport_api_mode(model_cfg.get("api_mode"))
if effective_provider == "custom"
else _parse_api_mode(model_cfg.get("api_mode"))
)
or _detect_api_mode_for_url(base_url)
or "chat_completions",
"base_url": base_url,
Expand Down Expand Up @@ -1142,6 +1171,11 @@ def resolve_runtime_provider(
explicit_base_url=explicit_base_url,
)
if custom_runtime:
custom_runtime["api_mode"] = _maybe_apply_codex_app_server_runtime(
provider=str(custom_runtime.get("provider") or ""),
api_mode=str(custom_runtime.get("api_mode") or "chat_completions"),
model_cfg=_get_model_config(),
)
custom_runtime["requested_provider"] = requested_provider
return custom_runtime

Expand Down Expand Up @@ -1543,6 +1577,11 @@ def resolve_runtime_provider(
explicit_api_key=explicit_api_key,
explicit_base_url=explicit_base_url,
)
runtime["api_mode"] = _maybe_apply_codex_app_server_runtime(
provider=str(runtime.get("provider") or ""),
api_mode=str(runtime.get("api_mode") or "chat_completions"),
model_cfg=model_cfg,
)
runtime["requested_provider"] = requested_provider
return runtime

Expand Down
8 changes: 6 additions & 2 deletions skills/autonomous-ai-agents/codex/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ Requires the codex CLI and a git repository.
- Use `pty=true` in terminal calls — Codex is an interactive terminal app

For Hermes itself, `model.provider: openai-codex` uses Hermes-managed Codex
OAuth from `~/.hermes/auth.json` after `hermes auth add openai-codex`. For the
standalone Codex CLI, a valid CLI OAuth session may live under
OAuth from `~/.hermes/auth.json` after `hermes auth add openai-codex`. Hermes'
Codex app-server runtime also supports named custom providers when they declare
`transport: codex_responses` or legacy `api_mode: codex_responses`; do not use
`transport: codex_app_server` in the provider entry. Plain custom
chat-completions providers are intentionally left on the default runtime.
For the standalone Codex CLI, a valid CLI OAuth session may live under
`~/.codex/auth.json`; do not treat a missing `OPENAI_API_KEY` alone as proof
that Codex auth is missing.

Expand Down
16 changes: 16 additions & 0 deletions tests/agent/transports/test_codex_app_server_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ def test_opt_in_rewrites_openai_codex(self) -> None:
)
assert got == "codex_app_server"

def test_opt_in_rewrites_custom_codex_responses_proxy(self) -> None:
got = _maybe_apply_codex_app_server_runtime(
provider="custom",
api_mode="codex_responses",
model_cfg={"openai_runtime": "codex_app_server"},
)
assert got == "codex_app_server"

def test_custom_chat_proxy_is_not_rerouted(self) -> None:
got = _maybe_apply_codex_app_server_runtime(
provider="custom",
api_mode="chat_completions",
model_cfg={"openai_runtime": "codex_app_server"},
)
assert got == "chat_completions"

def test_case_insensitive(self) -> None:
got = _maybe_apply_codex_app_server_runtime(
provider="openai",
Expand Down
Loading
Loading