mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(wrap/codex): export the detected custom upstream base URL (#2125)
## Description `headroom wrap codex` detects a user's custom upstream gateway but never tells Codex to use it, so the user's gateway key is sent to `api.openai.com`. `_inject_codex_provider_config` handles a Codex user who has an OpenAI-compatible gateway declared in `~/.codex/config.toml`, e.g. ```toml model_provider = "freemodel" [model_providers.freemodel] base_url = "https://api.freemodel.dev" ``` It injects the Headroom provider with `env_http_headers = { ... "X-Headroom-Base-Url" = "HEADROOM_CODEX_UPSTREAM_BASE_URL" }` and **returns the preserved upstream URL** so the caller can export it. Its docstring even says: *"Callers that go on to launch Codex should export this value into `HEADROOM_CODEX_UPSTREAM_BASE_URL`."* But `_prepare_codex_wrap_state` called it as a bare statement and discarded the return, and `_run_codex_wrap` / `_build_codex_launch_env` only ever set `OPENAI_BASE_URL`. A repo-wide grep confirms `HEADROOM_CODEX_UPSTREAM_BASE_URL` (`_UPSTREAM_BASE_URL_ENV_VAR`) is never assigned into any process env — it appears only at its definition and in that docstring. Since Codex only emits the `X-Headroom-Base-Url` header when the env var exists, the header is omitted, the proxy's OpenAI handler falls back to its hardcoded `https://api.openai.com`, and the user's `freemodel.dev` key is sent to OpenAI, which rejects it. This is a regression: the wiring existed in the original `#1614` fix (`_codex_custom_upstream = _inject_codex_provider_config(...)` then `env[_UPSTREAM_BASE_URL_ENV_VAR] = ...`) and was dropped by a later refactor that extracted `_prepare_codex_wrap_state`. ## Fix Restore the wiring: `_prepare_codex_wrap_state` now captures and returns `_inject_codex_provider_config`'s value, and `_run_codex_wrap` exports it into the launch env (`env[_UPSTREAM_BASE_URL_ENV_VAR] = custom_upstream`) when it is non-None and not already set, so a user-provided value still wins. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/cli/wrap.py`: `_prepare_codex_wrap_state` returns the detected custom upstream URL; `_run_codex_wrap` exports it into the launch env (and its display list) when set. - `tests/test_cli/test_wrap_codex.py`: add `TestCodexLaunchExportsCustomUpstream` — drives `_run_codex_wrap` with mocked prepare/launch and asserts the launch env carries `HEADROOM_CODEX_UPSTREAM_BASE_URL` when a custom upstream is detected, and does not when there isn't one. - `CHANGELOG.md`: Bug Fixes entry. ## Testing - [x] Unit tests pass (`uv run --extra dev pytest tests/test_cli/test_wrap_codex.py::TestCodexLaunchExportsCustomUpstream -q`) - [x] Linting passes (`uvx ruff@0.15.17 check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py headroom/memory/factory.py`) - [x] Type checking passes (`uvx mypy==1.20.2 headroom/memory/factory.py`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py All checks passed! $ python -m py_compile headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py OK ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing `headroom` pulls in the torch/transformers stack and running the CLI test locally OOM-kills this box, so I verified the wiring with a dependency-free script that models prepare -> run -> the proxy's upstream fallback, and left the full pytest (including the new CLI test) to CI. - Exact command / steps: modelled the old flow (inject return discarded) and the new flow (return exported into the launch env), then applied the proxy's rule that a missing `HEADROOM_CODEX_UPSTREAM_BASE_URL` falls back to `api.openai.com`. - Observed result: old effective upstream is `https://api.openai.com` (the gateway key is misrouted); new effective upstream is `https://api.freemodel.dev` (the user's gateway). The new CLI test asserts the launch env carries the var when a custom upstream is present and omits it otherwise. - Not tested: a live Codex process reading the env and emitting the header; full local `pytest` deferred to CI (OOM, per above). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes Merged current `main` to pick up the repository-wide mypy cache-key annotation fix, then verified the focused regression locally. the change threads one return value through two functions and exports it, verified by the wiring proof and a new CLI test that drives `_run_codex_wrap` with the heavy prepare/launch steps mocked so only the env-export logic is exercised. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
This commit is contained in:
parent
f8eaaeb26a
commit
d236b27c60
3 changed files with 82 additions and 4 deletions
|
|
@ -1953,3 +1953,63 @@ class TestCodexPortResolution:
|
|||
assert call_kw.get("port") == 8787
|
||||
assert call_kw.get("no_proxy") is False
|
||||
assert call_kw.get("prepare_only") is False
|
||||
|
||||
|
||||
class TestCodexLaunchExportsCustomUpstream:
|
||||
"""`_run_codex_wrap` must export the detected custom upstream base URL into
|
||||
the launch env so Codex emits the ``X-Headroom-Base-Url`` header. Otherwise
|
||||
the proxy falls back to api.openai.com and the user's gateway key is sent to
|
||||
the wrong host (regression of #1614)."""
|
||||
|
||||
def _launch_env(self, monkeypatch, tmp_path, *, custom_upstream):
|
||||
from contextlib import contextmanager
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
monkeypatch.setattr(wrap_mod.shutil, "which", lambda name: "/usr/bin/codex")
|
||||
monkeypatch.setattr(wrap_mod, "_codex_home_dir", lambda: tmp_path)
|
||||
|
||||
@contextmanager
|
||||
def _fake_overlay():
|
||||
yield tmp_path / "session"
|
||||
|
||||
monkeypatch.setattr(wrap_mod, "_codex_session_home_overlay", _fake_overlay)
|
||||
# Stand in for the heavy prepare step; only its return value matters here.
|
||||
monkeypatch.setattr(wrap_mod, "_prepare_codex_wrap_state", lambda **kwargs: custom_upstream)
|
||||
|
||||
def _fake_launch(*, env, **kwargs):
|
||||
captured["env"] = env
|
||||
|
||||
monkeypatch.setattr(wrap_mod, "_launch_tool", _fake_launch)
|
||||
|
||||
wrap_mod._run_codex_wrap(
|
||||
port=8787,
|
||||
no_rtk=True,
|
||||
no_mcp=True,
|
||||
no_tokensave=True,
|
||||
serena=False,
|
||||
no_serena=True,
|
||||
code_graph=False,
|
||||
no_proxy=True,
|
||||
learn=False,
|
||||
memory=False,
|
||||
backend=None,
|
||||
anyllm_provider=None,
|
||||
region=None,
|
||||
verbose=False,
|
||||
prepare_only=False,
|
||||
codex_args=(),
|
||||
)
|
||||
return captured["env"]
|
||||
|
||||
def test_custom_upstream_exported_into_launch_env(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
env = self._launch_env(monkeypatch, tmp_path, custom_upstream="https://api.freemodel.dev")
|
||||
assert env[wrap_mod._UPSTREAM_BASE_URL_ENV_VAR] == "https://api.freemodel.dev"
|
||||
|
||||
def test_no_custom_upstream_leaves_env_var_unset(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
env = self._launch_env(monkeypatch, tmp_path, custom_upstream=None)
|
||||
assert wrap_mod._UPSTREAM_BASE_URL_ENV_VAR not in env
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue