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:
+100
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user