mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
fix(codex): avoid duplicate headroom provider config (#1431)
## Description Fixes #1425. `headroom wrap codex` could leave `~/.codex/config.toml` invalid when the user already had a `[model_providers.headroom]` table. The previous duplicate-key handling covered top-level `model_provider` and `openai_base_url`, but the provider table was still appended as a static block. That could produce duplicate `env_http_headers` or duplicate provider-table TOML errors before Codex started. ## Type of Change - [x] Bug fix (non-breaking change fixes an issue) - [ ] New feature (non-breaking change adds functionality) - [ ] Breaking change (fix or feature would cause existing functionality change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a Codex config cleanup helper that removes any pre-existing `[model_providers.headroom]` table from the working copy before `wrap codex` appends the managed Headroom provider block. - Kept unwrap behavior backed by the existing pre-wrap snapshot, so a custom prior `headroom` provider table is restored byte-for-byte on `headroom unwrap codex`. - Added regression tests for TOML validity, a single `env_http_headers` mapping, one managed `[model_providers.headroom]` table, and unwrap restoration. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added to cover the fix ### Test Output ```text Docker: python:3.12-slim Command: uv run --frozen --with pytest pytest tests/test_cli/test_wrap_codex.py Result: 68 passed, 1 warning ``` ## Real Behavior Proof - Environment: disposable Docker container, `python:3.12-slim`, Linux, Python 3.12.13. - Exact command / steps: mounted the worktree into `/workspace`, installed build tools inside the container, then ran `uv run --frozen --with pytest pytest tests/test_cli/test_wrap_codex.py`. - Observed result: all Codex wrap tests passed, including the new regression where an existing `[model_providers.headroom]` table contains `env_http_headers` before wrapping. - Not tested: live interactive `headroom wrap codex` launch against a real user Codex session. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review
This commit is contained in:
parent
15ac650d40
commit
ddd4adf911
2 changed files with 70 additions and 0 deletions
|
|
@ -1350,6 +1350,20 @@ def _strip_codex_headroom_blocks(
|
|||
_REDIRECTABLE_KEYS: tuple[str, ...] = ("model_provider", "openai_base_url")
|
||||
|
||||
|
||||
def _strip_existing_codex_headroom_provider_table(content: str) -> str:
|
||||
"""Remove a pre-existing ``[model_providers.headroom]`` table before wrap."""
|
||||
if "[model_providers.headroom]" not in content:
|
||||
return content
|
||||
|
||||
import re # local import to match surrounding helper convention
|
||||
|
||||
provider_table = re.compile(
|
||||
r"(?ms)^[ \t]*\[model_providers\.headroom\][^\n]*\n.*?(?=^[ \t]*\[|\Z)"
|
||||
)
|
||||
content = provider_table.sub("", content)
|
||||
return content.lstrip("\n").rstrip() + "\n" if content.strip() else ""
|
||||
|
||||
|
||||
def _redirect_existing_top_level_keys(content: str, port: int) -> str:
|
||||
"""Rewrite user-defined top-level keys so wrap does not create duplicates.
|
||||
|
||||
|
|
@ -1614,6 +1628,7 @@ def _inject_codex_provider_config(port: int) -> None:
|
|||
# Remove any prior Headroom-managed blocks before re-injecting so
|
||||
# the operation is idempotent and supports port changes.
|
||||
content = _strip_codex_headroom_blocks(content)
|
||||
content = _strip_existing_codex_headroom_provider_table(content)
|
||||
|
||||
# Bare top-level keys must precede any [section] in TOML, and
|
||||
# TOML rejects duplicate top-level keys. Rewrite any existing
|
||||
|
|
|
|||
|
|
@ -735,6 +735,61 @@ class TestInjectAvoidsDuplicateTopLevelKeys:
|
|||
assert 'openai_base_url = "http://127.0.0.1:8787/v1"' in content
|
||||
assert "[model_providers.headroom]" in content
|
||||
|
||||
def test_inject_replaces_existing_headroom_provider_table(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""Existing headroom provider table must not create duplicate TOML keys."""
|
||||
import tomllib # Python 3.11+ stdlib
|
||||
|
||||
_set_test_home(monkeypatch, tmp_path)
|
||||
config_dir = tmp_path / ".codex"
|
||||
config_dir.mkdir()
|
||||
config_file = config_dir / "config.toml"
|
||||
config_file.write_text(
|
||||
"[model_providers.headroom]\n"
|
||||
'name = "Existing custom headroom"\n'
|
||||
'base_url = "http://example.invalid/v1"\n'
|
||||
"supports_websockets = true\n"
|
||||
'env_http_headers = { "X-Headroom-Project" = "HEADROOM_PROJECT" }\n'
|
||||
"\n"
|
||||
"[profiles.default]\n"
|
||||
'model = "gpt-5"\n'
|
||||
)
|
||||
|
||||
wrap_mod._inject_codex_provider_config(8787)
|
||||
content = config_file.read_text()
|
||||
|
||||
tomllib.loads(content)
|
||||
assert content.count("[model_providers.headroom]") == 1
|
||||
assert content.count("env_http_headers") == 1
|
||||
assert 'base_url = "http://127.0.0.1:8787/v1"' in content
|
||||
assert 'env_http_headers = { "X-Headroom-Project" = "HEADROOM_PROJECT" }' in content
|
||||
assert "[profiles.default]" in content
|
||||
assert 'model = "gpt-5"' in content
|
||||
|
||||
def test_unwrap_restores_prior_headroom_provider_table(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""Pre-wrap headroom provider table is restored from snapshot."""
|
||||
_set_test_home(monkeypatch, tmp_path)
|
||||
config_dir = tmp_path / ".codex"
|
||||
config_dir.mkdir()
|
||||
config_file = config_dir / "config.toml"
|
||||
original = (
|
||||
"[model_providers.headroom]\n"
|
||||
'name = "Existing custom headroom"\n'
|
||||
'base_url = "http://example.invalid/v1"\n'
|
||||
"supports_websockets = true\n"
|
||||
'env_http_headers = { "X-Headroom-Project" = "HEADROOM_PROJECT" }\n'
|
||||
)
|
||||
config_file.write_text(original)
|
||||
|
||||
wrap_mod._inject_codex_provider_config(8787)
|
||||
status, _ = wrap_mod._restore_codex_provider_config()
|
||||
|
||||
assert status == "restored"
|
||||
assert config_file.read_text() == original
|
||||
|
||||
def test_unwrap_restores_prior_model_provider_after_rewrite(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue