fix(codex): send catalog/image credentials only to their own route

Follow-up to the two contributor commits for #121486. The picker, the
image plugin and the auxiliary Codex client still composed a pooled
gateway key with a base re-read from ambient state (HERMES_CODEX_BASE_URL
or the chatgpt.com default), so a model.base_url-only gateway (env unset)
still sent its key to chatgpt.com.

- auth_codex: resolve_codex_runtime_credentials reports the host a pooled
  credential actually routes to (runtime_provider._pool_entry_mode_and_url:
  env > model.base_url while the row is canonical > row URL) instead of the
  ambient default; get_codex_auth_status carries the same bound base_url.
- picker: get_codex_model_ids(access_token, base_url=) now receives the base
  resolved with the token from hermes_cli/models.py, the CLI default-model
  swap (self.base_url) and the `hermes model` Codex flow.
- aux/image: _resolve_codex_credential_and_base() returns (token, base) from
  one pool selection; the image plugin, _build_codex_client and the raw
  Codex client use it (profile-scoped override from #121497 still wins).
- model_metadata: the non-JWT refusal now applies only when the target is
  chatgpt.com; a gateway key may probe its own gateway's /models.

Adversarial regressions: model.base_url with env unset, env/route mismatch,
opaque + JWT gateway keys, pool-selected credential, pool row with its own
gateway URL, direct-ChatGPT positive control.

Addresses @andrexibiza's review on #121508.
This commit is contained in:
kshitijk4poor
2026-09-24 19:32:54 +05:30
committed by kshitij
parent 602aa9a55b
commit e87f673faa
14 changed files with 484 additions and 57 deletions

View File

@@ -77,6 +77,21 @@ def _read_codex_access_token() -> Optional[str]:
return None
def _read_codex_credential() -> Tuple[Optional[str], Optional[str]]:
"""``(token, base_url)`` from one resolution: the image request goes to the host the token's
credential routes to (pool row / ``model.base_url`` / profile override), never a default the
credential does not belong to (#121486)."""
try:
from agent.auxiliary_client import _resolve_codex_credential_and_base
token, base_url = _resolve_codex_credential_and_base()
token = token.strip() if isinstance(token, str) and token.strip() else None
return token, (base_url or None) if token else None
except Exception as exc:
logger.debug("Could not resolve Codex credential: %s", exc)
return None, None
def _httpx_available() -> bool:
try:
import httpx # noqa: F401
@@ -186,17 +201,20 @@ def _build_image_request(
def _post_image_request(
token: str, *, prompt: str, size: str, quality: str, input_images: Optional[List[Dict[str, str]]] = None
token: str, *, prompt: str, size: str, quality: str, input_images: Optional[List[Dict[str, str]]] = None,
base_url: Optional[str] = None,
) -> Dict[str, Any]:
"""POST to the native Codex images endpoint; return the decoded JSON body plus
``imagegen_request_id`` (backend correlation id, for support tickets)."""
``imagegen_request_id`` (backend correlation id, for support tickets).
``base_url`` should come from the same resolution as ``token`` (``_read_codex_credential``)."""
import httpx
from agent.auxiliary_client import _codex_base_url_override
from agent.codex_headers import codex_cloudflare_headers
# Match the text auxiliary route, including profile-scoped overrides. Resolve
# per request: a multiplexed process can serve different Codex gateways.
base_url = _codex_base_url_override() or _CODEX_BASE_URL
base_url = (base_url or "").strip().rstrip("/") or _codex_base_url_override() or _CODEX_BASE_URL
headers = codex_cloudflare_headers(token, base_url=base_url)
headers.update({
"Authorization": f"Bearer {token}",
@@ -266,7 +284,7 @@ class OpenAICodexImageGenProvider(StaticImageGenProvider):
aspect = resolve_aspect_ratio(aspect_ratio)
if not prompt:
return prompt_required_error("openai-codex", aspect)
token = _read_codex_access_token()
token, base_url = _read_codex_credential()
if not token:
return error_factory("openai-codex", aspect)(_NO_AUTH, "auth_required")
if not _httpx_available():
@@ -283,7 +301,8 @@ class OpenAICodexImageGenProvider(StaticImageGenProvider):
try:
payload = _post_image_request(
token, prompt=prompt, size=size, quality=meta["quality"], input_images=input_images or None)
token, prompt=prompt, size=size, quality=meta["quality"], input_images=input_images or None,
base_url=base_url)
except Exception as exc:
logger.debug("Codex image generation failed", exc_info=True)
return fail(f"OpenAI image generation via Codex auth failed: {exc}", "api_error")