Skip to content

Commit b1105b6

Browse files
committed
fix(wrap): route OMP OpenAI providers through proxy
Route OMP's OpenAI API-key and Codex OAuth provider IDs alongside Anthropic so current Responses models no longer bypass Headroom. Constraint: Preserve byte-for-byte models.yml restore and the existing helper return shape Rejected: OPENAI_BASE_URL injection | OMP resolves chat endpoints from models.yml Confidence: high Scope-risk: narrow Not-tested: Full pytest suite; narrow OMP tests and live Codex routing were verified
1 parent b88b907 commit b1105b6

4 files changed

Lines changed: 79 additions & 39 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ shows an **Output Tokens Saved** card next to input compression, labelled
241241
| Goose || starts proxy + launches |
242242
| OpenHands || starts proxy + launches |
243243
| Mistral Vibe || starts proxy + launches |
244-
| Oh My Pi || injects config · starts proxy + launches |
244+
| Oh My Pi || Anthropic + OpenAI/Codex · injects config · starts proxy + launches |
245245
| Cortex Code | Library only | 60–65% savings (library mode; no `wrap`) |
246246
| Kimi CLI || OAuth bearer forwarded — log in once |
247247
| ZCode || starts proxy and prints base URLs for ZCode settings |

headroom/cli/wrap.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7751,14 +7751,14 @@ def omp(
77517751
"""Launch Oh My Pi (omp) through Headroom proxy.
77527752
77537753
\b
7754-
Points omp's built-in `anthropic` provider at Headroom by injecting a
7755-
marker-fenced `providers.anthropic.baseUrl` override into
7756-
~/.omp/agent/models.yml (pre-wrap file backed up byte-for-byte; undo with
7757-
`headroom unwrap omp`). omp resolves its Anthropic chat endpoint from
7758-
models.yml — ANTHROPIC_BASE_URL only affects its web-search helper — and a
7759-
same-ID override keeps omp's bundled model catalog and stored credentials.
7760-
omp's other providers (OpenAI-direct, Gemini, ...) keep their normal
7761-
endpoints; route those via your own custom provider in models.yml.
7754+
Points omp's built-in `anthropic`, `openai` (API-key), and `openai-codex`
7755+
(OAuth) providers at Headroom by injecting marker-fenced base-URL overrides
7756+
into ~/.omp/agent/models.yml (pre-wrap file backed up byte-for-byte; undo
7757+
with `headroom unwrap omp`). omp resolves these endpoints from models.yml —
7758+
ANTHROPIC_BASE_URL only affects its web-search helper — and same-ID
7759+
overrides keep omp's bundled model catalog and stored credentials. OMP
7760+
Codex appends `/codex/responses`, which Headroom already aliases. Gemini
7761+
and other providers keep their normal endpoints.
77627762
77637763
\b
77647764
Examples:

headroom/providers/omp/runtime.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"""Runtime helpers for Oh My Pi (omp) integrations.
22
3-
omp resolves its Anthropic chat endpoint from the model registry
4-
(``providers.anthropic.baseUrl`` in ``~/.omp/agent/models.yml``), not from
3+
omp resolves its Anthropic and OpenAI provider endpoints from the model registry
4+
(``providers.<id>.baseUrl`` in ``~/.omp/agent/models.yml``), not from
55
``ANTHROPIC_BASE_URL`` — that env var only feeds omp's web-search helper.
66
Verified empirically: with ``ANTHROPIC_BASE_URL`` pointed at a local probe
77
server, omp's chat traffic still went to the real Anthropic endpoint; with a
88
``models.yml`` same-ID override, every ``/v1/messages`` request arrived at the
9-
probe. A same-ID override keeps omp's bundled Anthropic model catalog and
10-
stored credentials (both keyed by provider id ``anthropic``), so only the
11-
endpoint moves.
12-
13-
The wrap therefore injects a marker-fenced ``providers.anthropic.baseUrl``
14-
override into ``models.yml``, snapshotting the pre-wrap file byte-for-byte
15-
first — the same durable-wrap + backup + ``headroom unwrap`` contract the
16-
Codex wrap uses for ``config.toml``.
9+
probe. Same-ID overrides keep omp's bundled model catalog and stored
10+
credentials, so only the endpoints move.
11+
12+
The wrap injects marker-fenced ``providers.anthropic.baseUrl``,
13+
``providers.openai.baseUrl``, and ``providers.openai-codex.baseUrl`` overrides
14+
into ``models.yml``, snapshotting the pre-wrap file byte-for-byte first. OMP's
15+
Codex provider appends ``/codex/responses``; Headroom already aliases that
16+
path. Gemini and other providers remain untouched.
1717
"""
1818

1919
from __future__ import annotations
@@ -23,6 +23,7 @@
2323
from pathlib import Path
2424

2525
from headroom.providers.claude import proxy_base_url as claude_proxy_base_url
26+
from headroom.providers.codex import proxy_base_url as codex_proxy_base_url
2627
from headroom.proxy.project_context import with_project_prefix
2728

2829
MANAGED_MARKER = "# managed by `headroom wrap omp`"
@@ -62,6 +63,11 @@ def proxy_anthropic_base_url(port: int, project: str | None = None) -> str:
6263
return with_project_prefix(claude_proxy_base_url(port), project)
6364

6465

66+
def proxy_openai_base_url(port: int, project: str | None = None) -> str:
67+
"""Proxy base URL omp's OpenAI and Codex providers are pointed at."""
68+
return with_project_prefix(codex_proxy_base_url(port), project)
69+
70+
6571
def is_managed(models_file: Path) -> bool:
6672
"""Whether ``models_file`` is currently a wrap-managed override."""
6773
if not models_file.exists():
@@ -74,25 +80,26 @@ def is_managed(models_file: Path) -> bool:
7480

7581

7682
def inject_models_override(port: int, project: str | None = None) -> tuple[Path, str]:
77-
"""Point ``providers.anthropic.baseUrl`` at the local proxy.
83+
"""Point omp's Anthropic, OpenAI, and Codex provider endpoints at Headroom.
7884
79-
Returns ``(models_file, base_url)``.
85+
Returns ``(models_file, anthropic_base_url)`` for backward compatibility.
8086
8187
* First injection snapshots the user's pre-wrap file byte-for-byte to
82-
``models.yml.headroom-backup`` so unwrap can restore it exactly. A file
88+
``models.yml.headroom-backup`` so unwrap can restore it exactly. A file
8389
that is already wrap-managed is never re-snapshotted (that would clobber
8490
the pristine backup — same guard as the Codex config snapshot).
8591
* The managed file is regenerated from the backup (or from scratch when
8692
the user had no ``models.yml``) on every call, so re-running with a
87-
different ``--port`` updates the override idempotently.
93+
different ``--port`` updates the overrides idempotently.
8894
* Any user-defined providers/models from the pre-wrap file are preserved:
89-
the override only deep-sets ``providers.anthropic.baseUrl``.
95+
the override only deep-sets each supported provider's ``baseUrl``.
9096
"""
9197
import yaml # type: ignore[import-untyped] # PyYAML ships no stubs; lint env installs no deps
9298

9399
models_file = models_yml_path()
94100
backup = backup_path(models_file)
95101
base_url = proxy_anthropic_base_url(port, project)
102+
openai_base_url = proxy_openai_base_url(port, project)
96103

97104
original_bytes: bytes | None = None
98105
if backup.exists():
@@ -119,11 +126,16 @@ def inject_models_override(port: int, project: str | None = None) -> tuple[Path,
119126
if not isinstance(providers, dict): # malformed user value: keep it in backup only
120127
providers = {}
121128
data["providers"] = providers
122-
anthropic = providers.setdefault("anthropic", {})
123-
if not isinstance(anthropic, dict):
124-
anthropic = {}
125-
providers["anthropic"] = anthropic
126-
anthropic["baseUrl"] = base_url
129+
for provider_name, provider_base_url in (
130+
("anthropic", base_url),
131+
("openai", openai_base_url),
132+
("openai-codex", openai_base_url),
133+
):
134+
provider = providers.setdefault(provider_name, {})
135+
if not isinstance(provider, dict):
136+
provider = {}
137+
providers[provider_name] = provider
138+
provider["baseUrl"] = provider_base_url
127139

128140
models_file.parent.mkdir(parents=True, exist_ok=True)
129141
rendered = yaml.safe_dump(data, sort_keys=False, default_flow_style=False)
@@ -157,10 +169,15 @@ def build_launch_env(
157169
) -> tuple[dict[str, str], list[str]]:
158170
"""Build the launch environment and display lines for the omp wrap.
159171
160-
The endpoint redirect itself lives in ``models.yml`` (see module
172+
The endpoint redirects themselves live in ``models.yml`` (see module
161173
docstring), so the environment passes through unchanged; the display
162-
lines surface where the override went.
174+
lines surface where the overrides went.
163175
"""
164176
env = dict(environ or os.environ)
165177
base_url = proxy_anthropic_base_url(port, project)
166-
return env, [f"models.yml: providers.anthropic.baseUrl={base_url}"]
178+
openai_base_url = proxy_openai_base_url(port, project)
179+
return env, [
180+
f"models.yml: providers.anthropic.baseUrl={base_url}",
181+
f"models.yml: providers.openai.baseUrl={openai_base_url}",
182+
f"models.yml: providers.openai-codex.baseUrl={openai_base_url}",
183+
]

tests/test_cli/test_wrap_omp.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ def test_inject_fresh_create_writes_managed_marker_and_no_backup(omp_home: Path)
7171
assert base_url == "http://127.0.0.1:8787/p/proj"
7272

7373
text = models_file.read_text(encoding="utf-8")
74+
providers = yaml.safe_load(text)["providers"]
75+
openai_base_url = "http://127.0.0.1:8787/p/proj/v1"
7476
assert MANAGED_MARKER in text
75-
assert yaml.safe_load(text)["providers"]["anthropic"]["baseUrl"] == base_url
76-
77+
assert providers["anthropic"]["baseUrl"] == base_url
78+
assert providers["openai"]["baseUrl"] == openai_base_url
79+
assert providers["openai-codex"]["baseUrl"] == openai_base_url
7780
# Nothing pre-existed, so there is nothing to snapshot.
7881
assert not backup_path(models_file).exists()
7982

@@ -85,6 +88,10 @@ def test_inject_over_existing_backs_up_pristine_and_merges(omp_home: Path) -> No
8588
" apiKey: sk-user-secret\n"
8689
" openai:\n"
8790
" baseUrl: https://api.openai.com/v1\n"
91+
" apiKey: sk-user-openai\n"
92+
" openai-codex:\n"
93+
" baseUrl: https://chatgpt.com/backend-api/codex\n"
94+
" oauth: true\n"
8895
"models:\n"
8996
" - id: my-custom-model\n"
9097
)
@@ -97,9 +104,13 @@ def test_inject_over_existing_backs_up_pristine_and_merges(omp_home: Path) -> No
97104

98105
merged = yaml.safe_load(omp_home.read_text(encoding="utf-8"))
99106
assert merged["providers"]["anthropic"]["baseUrl"] == base_url
100-
# Only anthropic.baseUrl is set; every other user key survives the merge.
107+
openai_base_url = "http://127.0.0.1:8787/p/proj/v1"
108+
assert merged["providers"]["openai"]["baseUrl"] == openai_base_url
109+
assert merged["providers"]["openai"]["apiKey"] == "sk-user-openai"
110+
assert merged["providers"]["openai-codex"]["baseUrl"] == openai_base_url
111+
assert merged["providers"]["openai-codex"]["oauth"] is True
112+
# Only supported provider base URLs are set; every other user key survives.
101113
assert merged["providers"]["anthropic"]["apiKey"] == "sk-user-secret"
102-
assert merged["providers"]["openai"]["baseUrl"] == "https://api.openai.com/v1"
103114
assert merged["models"] == [{"id": "my-custom-model"}]
104115
assert MANAGED_MARKER in omp_home.read_text(encoding="utf-8")
105116

@@ -120,6 +131,8 @@ def test_reinject_new_port_regenerates_from_pristine_backup(omp_home: Path) -> N
120131
merged = yaml.safe_load(omp_home.read_text(encoding="utf-8"))
121132
assert base_url_9999 == "http://127.0.0.1:9999/p/proj"
122133
assert merged["providers"]["anthropic"]["baseUrl"] == base_url_9999
134+
assert merged["providers"]["openai"]["baseUrl"] == "http://127.0.0.1:9999/p/proj/v1"
135+
assert merged["providers"]["openai-codex"]["baseUrl"] == "http://127.0.0.1:9999/p/proj/v1"
123136
# Regenerated from the backup, so user creds still survive the new port.
124137
assert merged["providers"]["anthropic"]["apiKey"] == "sk-user-secret"
125138

@@ -171,11 +184,15 @@ def test_build_launch_env_passes_env_through_and_emits_display(omp_home: Path) -
171184
source = {"PATH": "/usr/bin", "ANTHROPIC_BASE_URL": "https://api.anthropic.com"}
172185
env, display = build_launch_env(8787, source, project="proj")
173186

174-
# The redirect lives in models.yml, so env is a verbatim copy — notably
187+
# The redirects live in models.yml, so env is a verbatim copy — notably
175188
# ANTHROPIC_BASE_URL is NOT rewritten to the proxy.
176189
assert env == source
177190
assert env is not source # a copy, so caller's environ can't be mutated
178-
assert display == ["models.yml: providers.anthropic.baseUrl=http://127.0.0.1:8787/p/proj"]
191+
assert display == [
192+
"models.yml: providers.anthropic.baseUrl=http://127.0.0.1:8787/p/proj",
193+
"models.yml: providers.openai.baseUrl=http://127.0.0.1:8787/p/proj/v1",
194+
"models.yml: providers.openai-codex.baseUrl=http://127.0.0.1:8787/p/proj/v1",
195+
]
179196

180197

181198
# ---------------------------------------------------------------------------
@@ -217,12 +234,18 @@ def fake_launch_tool(**kwargs: object) -> None:
217234
text_at_launch = captured["models_text_at_launch"]
218235
assert isinstance(text_at_launch, str)
219236
assert MANAGED_MARKER in text_at_launch
220-
base_url = yaml.safe_load(text_at_launch)["providers"]["anthropic"]["baseUrl"]
237+
providers = yaml.safe_load(text_at_launch)["providers"]
238+
base_url = providers["anthropic"]["baseUrl"]
239+
openai_base_url = providers["openai"]["baseUrl"]
221240
assert base_url.startswith("http://127.0.0.1:8787/p/")
241+
assert openai_base_url == f"{base_url}/v1"
242+
assert providers["openai-codex"]["baseUrl"] == openai_base_url
222243

223244
display = captured["env_vars_display"]
224245
assert isinstance(display, list)
225246
assert f"models.yml: providers.anthropic.baseUrl={base_url}" in display
247+
assert f"models.yml: providers.openai.baseUrl={openai_base_url}" in display
248+
assert f"models.yml: providers.openai-codex.baseUrl={openai_base_url}" in display
226249

227250

228251
def test_wrap_omp_does_not_write_agents_md(

0 commit comments

Comments
 (0)