fix: add config option for ccproxy port number (#52)

* fix: add config option for ccproxy port number

* feat: add user prompt for ccproxy port configuration and validation
fix: update is_ccproxy_running to use health check endpoint
test: enhance tests for ccproxy port handling and validation

* fix: streamline ccproxy installation process using _install_pip_package

* fix: auto-patch ccproxy adapter for correct OAuth beta header

---------

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:
Wiktor Cupiał
2026-03-17 23:27:20 +01:00
committed by GitHub
parent 97684eab7b
commit d5e50a690e
4 changed files with 255 additions and 81 deletions
+100 -18
View File
@@ -15,15 +15,11 @@ import os
import shutil
import subprocess
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
from EvoScientist.config import EvoScientistConfig
logger = logging.getLogger(__name__)
_DEFAULT_PORT = 8000
# =============================================================================
# Availability & auth checks
@@ -138,18 +134,18 @@ def check_ccproxy_auth(provider: str = "claude_api") -> tuple[bool, str]:
# =============================================================================
def is_ccproxy_running(port: int = _DEFAULT_PORT) -> bool:
def is_ccproxy_running(port: int) -> bool:
"""Check if ccproxy is already serving on the given port."""
import httpx
try:
resp = httpx.get(f"http://127.0.0.1:{port}/", timeout=2.0)
return resp.status_code < 500
resp = httpx.get(f"http://127.0.0.1:{port}/health/live", timeout=2.0)
return resp.status_code == 200
except (httpx.ConnectError, httpx.TimeoutException, OSError):
return False
def start_ccproxy(port: int = _DEFAULT_PORT) -> subprocess.Popen:
def start_ccproxy(port: int) -> subprocess.Popen:
"""Start ccproxy serve as a background process.
Args:
@@ -206,7 +202,7 @@ def stop_ccproxy(proc: subprocess.Popen | None) -> None:
pass
def ensure_ccproxy(port: int = _DEFAULT_PORT) -> subprocess.Popen | None:
def ensure_ccproxy(port: int) -> subprocess.Popen | None:
"""Ensure ccproxy is running — reuse existing or start new.
Returns:
@@ -223,7 +219,7 @@ def ensure_ccproxy(port: int = _DEFAULT_PORT) -> subprocess.Popen | None:
# =============================================================================
def setup_ccproxy_env(port: int = _DEFAULT_PORT) -> None:
def setup_ccproxy_env(port: int) -> None:
"""Set environment variables for Anthropic ccproxy routing.
Force-sets ``ANTHROPIC_BASE_URL`` and ``ANTHROPIC_API_KEY`` so that
@@ -236,7 +232,7 @@ def setup_ccproxy_env(port: int = _DEFAULT_PORT) -> None:
os.environ["ANTHROPIC_API_KEY"] = "ccproxy-oauth"
def setup_codex_env(port: int = _DEFAULT_PORT) -> None:
def setup_codex_env(port: int) -> None:
"""Set environment variables for OpenAI/Codex ccproxy routing.
Force-sets ``OPENAI_BASE_URL`` and ``OPENAI_API_KEY`` so that
@@ -255,7 +251,86 @@ def setup_codex_env(port: int = _DEFAULT_PORT) -> None:
# =============================================================================
def maybe_start_ccproxy(config: object) -> subprocess.Popen | None:
def _patch_ccproxy_oauth_header() -> None:
"""Auto-patch ccproxy's adapter to send the correct OAuth beta header.
ccproxy 0.2.4 hardcodes ``computer-use-2025-01-24`` as the
``anthropic-beta`` header, which causes two problems:
- Missing ``oauth-2025-04-20`` → 401 from Anthropic
- ``computer-use-2025-01-24`` incompatible with OAuth auth → 400
The ccproxy binary may use a different Python environment than the one
running EvoScientist, so we resolve the adapter path via the ccproxy
binary's shebang line rather than the current Python's import system.
This patch is idempotent and places the header AFTER cli_headers so it
cannot be overridden.
"""
import pathlib
import re
try:
ccproxy_bin = _ccproxy_exe()
if not ccproxy_bin:
return
# Find the Python interpreter used by the ccproxy binary via shebang
shebang = pathlib.Path(ccproxy_bin).read_text().splitlines()[0]
python_exe = shebang.lstrip("#!").strip()
# Ask that Python where ccproxy's adapter lives
result = subprocess.run(
[
python_exe, "-c",
"import inspect, ccproxy.plugins.claude_api.adapter as m; print(inspect.getfile(m))",
],
capture_output=True, text=True, timeout=10,
)
if result.returncode != 0:
return
src_file = pathlib.Path(result.stdout.strip())
if not src_file.exists():
return
text = src_file.read_text()
# Check if already correctly patched (oauth header set after cli_headers)
correct = 'filtered_headers["anthropic-beta"] = "oauth-2025-04-20"'
cli_marker = "cli_headers = self._collect_cli_headers()"
if correct in text:
# Verify it's placed after cli_headers
if text.index(correct) > text.index(cli_marker):
return # Already correctly patched
# Move/replace anthropic-beta assignment to after cli_headers loop,
# and set only oauth-2025-04-20 (computer-use-* is incompatible).
# Step 1: remove any existing filtered_headers["anthropic-beta"] line
patched = re.sub(
r'\s*filtered_headers\["anthropic-beta"\]\s*=\s*"[^"]*"\n',
"\n",
text,
)
# Step 2: insert correct assignment after the cli_headers block
insert_after = 'filtered_headers[lk] = value\n'
replacement = (
'filtered_headers[lk] = value\n\n'
' # oauth-2025-04-20: required for OAuth Bearer token auth (Anthropic 2026-03)\n'
' filtered_headers["anthropic-beta"] = "oauth-2025-04-20"\n'
)
patched = patched.replace(insert_after, replacement, 1)
if patched == text:
return
src_file.write_text(patched)
for pyc in src_file.parent.glob("__pycache__/adapter*.pyc"):
pyc.unlink(missing_ok=True)
logger.info("Auto-patched ccproxy adapter: set anthropic-beta=oauth-2025-04-20")
except Exception as exc:
logger.warning("Could not auto-patch ccproxy adapter: %s", exc)
def maybe_start_ccproxy(config: EvoScientistConfig) -> subprocess.Popen | None:
"""High-level: conditionally start ccproxy based on config.
Checks ``config.anthropic_auth_mode`` and ``config.openai_auth_mode``:
@@ -301,17 +376,24 @@ def maybe_start_ccproxy(config: object) -> subprocess.Popen | None:
"Run: ccproxy auth login codex"
)
port = config.ccproxy_port
if not (1 <= port <= 65535):
raise ValueError(f"Invalid ccproxy port: {port}. Must be between 1 and 65535.")
# Auto-patch ccproxy adapter to fix OAuth header compatibility
_patch_ccproxy_oauth_header()
# Start ccproxy (single process serves both providers)
proc = ensure_ccproxy()
proc = ensure_ccproxy(port)
# Set environment for each OAuth provider
if anthropic_oauth:
setup_ccproxy_env()
setup_ccproxy_env(port)
if openai_oauth:
setup_codex_env()
setup_codex_env(port)
if proc:
logger.info("Started ccproxy on port %d", _DEFAULT_PORT)
logger.info("Started ccproxy on port %d", port)
else:
logger.info("Reusing existing ccproxy on port %d", _DEFAULT_PORT)
logger.info("Reusing existing ccproxy on port %d", port)
return proc