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

@@ -55,7 +55,12 @@ def provider(monkeypatch):
def codex_backend(monkeypatch):
"""Route the plugin's ``httpx.Client`` at a fake Codex images backend; returns the request log
and lets a test swap the response via ``state["respond"]``."""
# Seed the auth.json token below the credential/base resolver so generate() exercises the real
# (token, base_url) binding; no pool present.
from agent import auxiliary_client
monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: "codex-token")
monkeypatch.setattr(auxiliary_client, "_select_pool_entry", lambda provider: (False, None))
monkeypatch.setattr(auxiliary_client, "_read_codex_singleton_token", lambda: "codex-token")
state = {"requests": [], "respond": None}
def _default(request):
@@ -181,7 +186,10 @@ class TestGenerate:
assert request.url.host == "images.example.test"
def test_returns_auth_error_without_codex_token(self, provider, monkeypatch):
from agent import auxiliary_client
monkeypatch.setattr(codex_plugin, "_read_codex_access_token", lambda: None)
monkeypatch.setattr(auxiliary_client, "_select_pool_entry", lambda provider: (False, None))
monkeypatch.setattr(auxiliary_client, "_read_codex_singleton_token", lambda: None)
result = provider.generate("a cat")
assert result["success"] is False
assert result["error_type"] == "auth_required"