fix(llm): make gpt-5.x usable through ccproxy Codex OAuth (#324)

* fix(llm): make gpt-5.x usable through ccproxy Codex OAuth

Two independent blockers made current OpenAI models fail when routed
through ccproxy's Codex OAuth endpoint:

1. ccproxy's default Codex model mappings rewrite any gpt-*/o1-*/o3-*/
   claude-* model to gpt-5.3-codex before forwarding, silently overriding
   the configured model and failing outright on accounts where
   gpt-5.3-codex is not served ("The 'gpt-5.3-codex' model is not
   supported when using Codex with a ChatGPT account").
   start_ccproxy() now generates a config with empty codex model
   mappings and passes it via 'ccproxy serve --config'.

2. ccproxy forwards the client's own User-Agent upstream and only
   gap-fills its Codex headers, so the backend gates current models on
   the client identity ("The '<model>' model requires a newer version
   of Codex"). get_chat_model() now sends Codex-CLI-shaped
   originator/version/User-Agent headers when the ccproxy Codex adapter
   is detected, overridable via EVOSCIENTIST_CODEX_CLIENT_VERSION.

Verified live: gpt-5.5 and gpt-5.4 complete successfully through
ccproxy Codex OAuth on a ChatGPT Plus account with both fixes; each
fails without them.

* fix(ccproxy): harden Codex client routing

* fix(llm): keep Codex client identity consistent

* docs: clarify Codex version floor

* style: ruff format models.py after merge

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
Co-authored-by: X-iZhang <zacharyzhang2022@gmail.com>
This commit is contained in:
Mani Saint-Victor
2026-07-13 07:04:47 -04:00
committed by GitHub
parent da6ca38d53
commit 2b28c46caf
4 changed files with 268 additions and 18 deletions
+49
View File
@@ -17,6 +17,7 @@ from EvoScientist.ccproxy_manager import (
setup_codex_env,
start_ccproxy,
stop_ccproxy,
write_ccproxy_config,
)
# =============================================================================
@@ -168,6 +169,54 @@ class TestStartCcproxy:
with pytest.raises(FileNotFoundError):
start_ccproxy(8000)
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running")
@patch("subprocess.Popen")
def test_passes_generated_config(self, mock_popen, mock_running, tmp_path):
proc = MagicMock()
proc.poll.return_value = None
mock_popen.return_value = proc
mock_running.side_effect = [True]
with patch("EvoScientist.config.get_config_dir", return_value=tmp_path):
start_ccproxy(8000)
cmd = mock_popen.call_args[0][0]
assert "--config" in cmd
assert cmd[cmd.index("--config") + 1] == str(tmp_path / "ccproxy.toml")
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running")
@patch("EvoScientist.ccproxy_manager.write_ccproxy_config", side_effect=OSError)
@patch("subprocess.Popen")
def test_config_write_failure_starts_without_config(
self, mock_popen, mock_write, mock_running
):
proc = MagicMock()
proc.poll.return_value = None
mock_popen.return_value = proc
mock_running.side_effect = [True]
start_ccproxy(8000)
cmd = mock_popen.call_args[0][0]
assert "--config" not in cmd
# =============================================================================
# write_ccproxy_config
# =============================================================================
class TestWriteCcproxyConfig:
def test_writes_codex_mapping_override(self, tmp_path):
config_dir = tmp_path / "missing" / "config"
with patch("EvoScientist.config.get_config_dir", return_value=config_dir):
path = write_ccproxy_config()
assert path == str(config_dir / "ccproxy.toml")
content = (config_dir / "ccproxy.toml").read_text(encoding="utf-8")
assert "[plugins.codex]" in content
assert "model_mappings = []" in content
# =============================================================================
# ensure_ccproxy