mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Merge pull request #141 from JerrettDavis/feat/copilot-wrap
feat: add GitHub Copilot CLI wrap support
This commit is contained in:
commit
fb953ebfe8
10 changed files with 419 additions and 6 deletions
|
|
@ -131,6 +131,8 @@ Works with any language, any tool, any framework. **[Proxy docs](docs/proxy.md)*
|
|||
|
||||
```bash
|
||||
headroom wrap claude # Starts proxy + launches Claude Code
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
# Starts proxy + launches GitHub Copilot CLI
|
||||
headroom wrap codex # Starts proxy + launches OpenAI Codex CLI
|
||||
headroom wrap aider # Starts proxy + launches Aider
|
||||
headroom wrap cursor # Starts proxy + prints Cursor config
|
||||
|
|
@ -178,6 +180,7 @@ Gives your AI tool three MCP tools: `headroom_compress`, `headroom_retrieve`, `h
|
|||
| **LangChain** | Wrap model | `HeadroomChatModel(your_llm)` |
|
||||
| **OpenClaw** | One-command wrap/unwrap | `headroom wrap openclaw` / `headroom unwrap openclaw` |
|
||||
| **Claude Code** | Wrap | `headroom wrap claude` |
|
||||
| **GitHub Copilot CLI** | Wrap | `headroom wrap copilot -- --model claude-sonnet-4-20250514` |
|
||||
| **Codex / Aider** | Wrap | `headroom wrap codex` or `headroom wrap aider` |
|
||||
|
||||
**[Full Integration Guide](docs/integration-guide.md)** | **[TypeScript SDK](docs/typescript-sdk.md)**
|
||||
|
|
@ -316,7 +319,7 @@ Plugin-based: reads conversation history from Claude Code, Codex, or Gemini CLI.
|
|||
| **Compression Hooks** | Customize compression with pre/post hooks |
|
||||
| **Read Lifecycle** | Detects stale/superseded Read outputs, replaces with CCR markers |
|
||||
| **`headroom learn`** | Plugin-based failure learning for Claude Code, Codex, Gemini CLI (extensible via entry points) |
|
||||
| **`headroom wrap`** | One-command setup for Claude Code, Codex, Aider, Cursor |
|
||||
| **`headroom wrap`** | One-command setup for Claude Code, GitHub Copilot CLI, Codex, Aider, Cursor |
|
||||
| **SharedContext** | Compressed inter-agent context sharing for multi-agent workflows |
|
||||
| **MCP Tools** | headroom_compress, headroom_retrieve, headroom_stats for Claude Code/Cursor |
|
||||
|
||||
|
|
@ -374,7 +377,7 @@ Context compression is a new space. Here's how the approaches differ:
|
|||
|
||||
| Integration | Status | Docs |
|
||||
|-------------|--------|------|
|
||||
| `headroom wrap claude/codex/aider/cursor` | **Stable** | [Proxy Docs](docs/proxy.md) |
|
||||
| `headroom wrap claude/copilot/codex/aider/cursor` | **Stable** | [Proxy Docs](docs/proxy.md) |
|
||||
| `compress()` — one function | **Stable** | [Integration Guide](docs/integration-guide.md) |
|
||||
| `SharedContext` — multi-agent | **Stable** | [SharedContext Guide](docs/shared-context.md) |
|
||||
| LiteLLM callback | **Stable** | [Integration Guide](docs/integration-guide.md#litellm) |
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ Then point your LLM client at it:
|
|||
# Claude Code
|
||||
ANTHROPIC_BASE_URL=http://localhost:8787 claude
|
||||
|
||||
# GitHub Copilot CLI (default Anthropic-style proxy route)
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
|
||||
# OpenAI-compatible clients
|
||||
OPENAI_BASE_URL=http://localhost:8787/v1 your-app
|
||||
```
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ Headroom works as a **transparent proxy** (zero code changes), a **Python functi
|
|||
|
||||
```bash
|
||||
headroom wrap claude # Claude Code
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
headroom wrap codex # OpenAI Codex CLI
|
||||
headroom wrap aider # Aider
|
||||
headroom wrap cursor # Cursor
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ You don't need to run the Headroom proxy. Headroom is a compression library that
|
|||
| Any Python app | [`compress()`](#compress-function) | 2 lines |
|
||||
| LiteLLM | [LiteLLM callback](#litellm) | 1 line |
|
||||
| A Python proxy (FastAPI, custom) | [ASGI middleware](#asgi-middleware) | 1 line |
|
||||
| Claude Code / Cursor | [Headroom proxy](#proxy) | 1 env var |
|
||||
| Claude Code / Cursor / Copilot CLI | [Headroom proxy](#proxy) | 1 command or env var |
|
||||
| Agno agents | [Agno integration](#agno) | Wrap model |
|
||||
| LangChain | [LangChain integration](#langchain) | Wrap model |
|
||||
| Non-Python app | [Headroom proxy](#proxy) | HTTP |
|
||||
|
|
@ -182,7 +182,7 @@ Response headers include:
|
|||
|
||||
## Proxy
|
||||
|
||||
The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor).
|
||||
The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor, GitHub Copilot CLI).
|
||||
|
||||
```bash
|
||||
pip install "headroom-ai[all]"
|
||||
|
|
@ -193,10 +193,21 @@ headroom proxy --port 8787
|
|||
# Claude Code
|
||||
ANTHROPIC_BASE_URL=http://localhost:8787 claude
|
||||
|
||||
# GitHub Copilot CLI
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
|
||||
# Cursor / Any OpenAI client
|
||||
OPENAI_BASE_URL=http://localhost:8787/v1 cursor
|
||||
```
|
||||
|
||||
For translated backends, the Copilot wrapper can switch to Headroom's OpenAI-compatible route:
|
||||
|
||||
```bash
|
||||
headroom wrap copilot --backend anyllm --anyllm-provider groq -- --model gpt-4o
|
||||
```
|
||||
|
||||
By default, `headroom wrap copilot` installs `rtk` and appends token-optimized shell guidance to `.github/copilot-instructions.md` so Copilot sessions reuse the same command-saving conventions as other wrapped agent CLIs. Use `--no-rtk` to skip that step.
|
||||
|
||||
### With Cloud Providers
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -21,6 +21,21 @@ headroom proxy \
|
|||
--budget 100.0
|
||||
```
|
||||
|
||||
### Common agent CLI entrypoints
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
ANTHROPIC_BASE_URL=http://localhost:8787 claude
|
||||
|
||||
# GitHub Copilot CLI
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
|
||||
# OpenAI-compatible clients
|
||||
OPENAI_BASE_URL=http://localhost:8787/v1 your-app
|
||||
```
|
||||
|
||||
`headroom wrap copilot` uses Copilot CLI's BYOK provider settings under the hood. In `provider-type=auto`, it chooses Headroom's Anthropic route for the default proxy backend and the OpenAI-compatible `/v1` route for translated backends such as `anyllm` and LiteLLM.
|
||||
|
||||
Anonymous aggregate telemetry is enabled by default. Opt out with `HEADROOM_TELEMETRY=off` or `headroom proxy --no-telemetry`. Downstream apps can set `HEADROOM_SDK=headroom-app` to override the anonymous telemetry `sdk` label; the default remains `proxy`.
|
||||
|
||||
Operational OTEL metrics are configured separately and are **off by default**. Install `headroom-ai[proxy,otel]` and set:
|
||||
|
|
@ -183,6 +198,7 @@ Response:
|
|||
"ready": true,
|
||||
"version": "0.5.21",
|
||||
"config": {
|
||||
"backend": "anthropic",
|
||||
"optimize": true,
|
||||
"cache": true,
|
||||
"rate_limit": true
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ headroom proxy --port 8787
|
|||
|
||||
```bash
|
||||
curl http://localhost:8787/health
|
||||
# Expected: {"status": "healthy", "mode": "optimize", ...}
|
||||
# Expected: {"status":"healthy","ready":true,"config":{"backend":"anthropic",...},...}
|
||||
```
|
||||
|
||||
### Step 3: Point Your Client
|
||||
|
|
@ -50,6 +50,9 @@ curl http://localhost:8787/health
|
|||
# Claude Code
|
||||
ANTHROPIC_BASE_URL=http://localhost:8787 claude
|
||||
|
||||
# GitHub Copilot CLI (default Anthropic-style proxy route)
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
|
||||
# Cursor / Continue / any OpenAI client
|
||||
OPENAI_BASE_URL=http://localhost:8787/v1 your-app
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
Usage:
|
||||
headroom wrap claude # Start proxy + rtk + claude
|
||||
headroom wrap copilot -- --model ... # Start proxy + launch GitHub Copilot CLI
|
||||
headroom wrap codex # Start proxy + OpenAI Codex CLI
|
||||
headroom wrap aider # Start proxy + aider
|
||||
headroom wrap cursor # Start proxy + print Cursor config instructions
|
||||
|
|
@ -22,6 +23,8 @@ import socket
|
|||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -327,6 +330,46 @@ def _inject_rtk_instructions(file_path: Path, verbose: bool = False) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _resolve_copilot_provider_type(backend: str | None, provider_type: str) -> str:
|
||||
"""Resolve Copilot BYOK provider type for the current proxy backend."""
|
||||
if provider_type != "auto":
|
||||
return provider_type
|
||||
|
||||
effective_backend = backend or os.environ.get("HEADROOM_BACKEND") or "anthropic"
|
||||
return "anthropic" if effective_backend == "anthropic" else "openai"
|
||||
|
||||
|
||||
def _detect_running_proxy_backend(port: int) -> str | None:
|
||||
"""Read the backend of an already-running proxy from its health endpoint."""
|
||||
url = f"http://127.0.0.1:{port}/health"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except (OSError, urllib.error.URLError, ValueError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
config = payload.get("config")
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
|
||||
backend = config.get("backend")
|
||||
return backend if isinstance(backend, str) else None
|
||||
|
||||
|
||||
def _copilot_model_configured(copilot_args: tuple[str, ...], env: dict[str, str]) -> bool:
|
||||
"""Return True when Copilot BYOK model selection is configured."""
|
||||
if env.get("COPILOT_MODEL") or env.get("COPILOT_PROVIDER_MODEL_ID"):
|
||||
return True
|
||||
|
||||
for idx, arg in enumerate(copilot_args):
|
||||
if arg == "--model" and idx + 1 < len(copilot_args):
|
||||
return True
|
||||
if arg.startswith("--model="):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_proxy(
|
||||
port: int,
|
||||
no_proxy: bool,
|
||||
|
|
@ -675,6 +718,7 @@ def wrap() -> None:
|
|||
\b
|
||||
Supported tools:
|
||||
headroom wrap claude # Claude Code (Anthropic)
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
headroom wrap codex # OpenAI Codex CLI
|
||||
headroom wrap aider # Aider
|
||||
headroom wrap cursor # Cursor (prints config instructions)
|
||||
|
|
@ -768,6 +812,153 @@ def claude(
|
|||
cleanup()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GitHub Copilot CLI
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@wrap.command(context_settings={"ignore_unknown_options": True})
|
||||
@click.option("--port", "-p", default=8787, type=int, help="Proxy port (default: 8787)")
|
||||
@click.option(
|
||||
"--no-rtk",
|
||||
is_flag=True,
|
||||
help="Skip rtk installation and Copilot instructions injection",
|
||||
)
|
||||
@click.option("--no-proxy", is_flag=True, help="Skip proxy startup (use existing proxy)")
|
||||
@click.option(
|
||||
"--backend",
|
||||
default=None,
|
||||
help="API backend for the proxy: 'anthropic', 'anyllm', 'litellm-vertex', etc. (env: HEADROOM_BACKEND)",
|
||||
)
|
||||
@click.option(
|
||||
"--anyllm-provider",
|
||||
default=None,
|
||||
help="Provider for any-llm backend: openai, mistral, groq, etc. (env: HEADROOM_ANYLLM_PROVIDER)",
|
||||
)
|
||||
@click.option(
|
||||
"--region", default=None, help="Cloud region for Bedrock/Vertex (env: HEADROOM_REGION)"
|
||||
)
|
||||
@click.option(
|
||||
"--provider-type",
|
||||
type=click.Choice(["auto", "anthropic", "openai"]),
|
||||
default="auto",
|
||||
show_default=True,
|
||||
help="Copilot BYOK provider mode. 'auto' uses anthropic for the default proxy backend and openai for translated backends.",
|
||||
)
|
||||
@click.option(
|
||||
"--wire-api",
|
||||
type=click.Choice(["completions", "responses"]),
|
||||
default=None,
|
||||
help="OpenAI-compatible Copilot wire API. Defaults to 'completions' when provider-type resolves to openai.",
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
||||
@click.argument("copilot_args", nargs=-1, type=click.UNPROCESSED)
|
||||
def copilot(
|
||||
port: int,
|
||||
no_rtk: bool,
|
||||
no_proxy: bool,
|
||||
backend: str | None,
|
||||
anyllm_provider: str | None,
|
||||
region: str | None,
|
||||
provider_type: str,
|
||||
wire_api: str | None,
|
||||
verbose: bool,
|
||||
copilot_args: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Launch GitHub Copilot CLI through Headroom proxy.
|
||||
|
||||
\b
|
||||
Configures Copilot CLI BYOK provider variables so Copilot routes through
|
||||
the local Headroom proxy. In auto mode, the wrapper uses Anthropic-style
|
||||
routing for the stock proxy backend and OpenAI-compatible routing for
|
||||
translated backends such as any-llm and LiteLLM.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
headroom wrap copilot -- --model claude-sonnet-4-20250514
|
||||
headroom wrap copilot --backend anyllm --anyllm-provider groq -- --model gpt-4o
|
||||
headroom wrap copilot --provider-type openai --wire-api responses -- --model gpt-5.4
|
||||
headroom wrap copilot --no-rtk -- --prompt "explain this file"
|
||||
"""
|
||||
copilot_bin = shutil.which("copilot")
|
||||
if not copilot_bin:
|
||||
click.echo("Error: 'copilot' not found in PATH.")
|
||||
click.echo(
|
||||
"Install GitHub Copilot CLI: "
|
||||
"https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
effective_backend = backend or os.environ.get("HEADROOM_BACKEND")
|
||||
if _check_proxy(port):
|
||||
running_backend = _detect_running_proxy_backend(port)
|
||||
if effective_backend and running_backend and effective_backend != running_backend:
|
||||
raise click.ClickException(
|
||||
f"Proxy already running on port {port} with backend '{running_backend}'. "
|
||||
f"Stop it or rerun with --backend {running_backend}."
|
||||
)
|
||||
effective_backend = running_backend or effective_backend
|
||||
|
||||
effective_provider_type = _resolve_copilot_provider_type(effective_backend, provider_type)
|
||||
if effective_provider_type == "anthropic" and wire_api is not None:
|
||||
raise click.ClickException(
|
||||
"--wire-api is only valid when Copilot is using the openai provider type."
|
||||
)
|
||||
if wire_api == "responses" and effective_backend not in (None, "anthropic"):
|
||||
raise click.ClickException(
|
||||
"--wire-api responses is not supported with translated backends; use completions."
|
||||
)
|
||||
|
||||
if not no_rtk:
|
||||
click.echo(" Setting up rtk for Copilot...")
|
||||
rtk_path = _ensure_rtk_binary(verbose=verbose)
|
||||
if rtk_path:
|
||||
copilot_instructions = Path.cwd() / ".github" / "copilot-instructions.md"
|
||||
_inject_rtk_instructions(copilot_instructions, verbose=verbose)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["COPILOT_PROVIDER_TYPE"] = effective_provider_type
|
||||
env.pop("COPILOT_PROVIDER_WIRE_API", None)
|
||||
|
||||
env_vars_display: list[str]
|
||||
if effective_provider_type == "anthropic":
|
||||
env["COPILOT_PROVIDER_BASE_URL"] = f"http://127.0.0.1:{port}"
|
||||
env_vars_display = [
|
||||
"COPILOT_PROVIDER_TYPE=anthropic",
|
||||
f"COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:{port}",
|
||||
]
|
||||
else:
|
||||
effective_wire_api = wire_api or "completions"
|
||||
env["COPILOT_PROVIDER_BASE_URL"] = f"http://127.0.0.1:{port}/v1"
|
||||
env["COPILOT_PROVIDER_WIRE_API"] = effective_wire_api
|
||||
env_vars_display = [
|
||||
"COPILOT_PROVIDER_TYPE=openai",
|
||||
f"COPILOT_PROVIDER_BASE_URL=http://127.0.0.1:{port}/v1",
|
||||
f"COPILOT_PROVIDER_WIRE_API={effective_wire_api}",
|
||||
]
|
||||
|
||||
if not _copilot_model_configured(copilot_args, env):
|
||||
click.echo(
|
||||
" Note: Copilot BYOK requires a model. Pass `--model <name>` "
|
||||
"or set `COPILOT_MODEL` / `COPILOT_PROVIDER_MODEL_ID`."
|
||||
)
|
||||
|
||||
_launch_tool(
|
||||
binary=copilot_bin,
|
||||
args=copilot_args,
|
||||
env=env,
|
||||
port=port,
|
||||
no_proxy=no_proxy,
|
||||
tool_label="COPILOT",
|
||||
env_vars_display=env_vars_display,
|
||||
learn=False,
|
||||
agent_type="copilot",
|
||||
backend=backend,
|
||||
anyllm_provider=anyllm_provider,
|
||||
region=region,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OpenAI Codex CLI
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -998,7 +998,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|||
|
||||
def _uptime_seconds() -> float:
|
||||
started_at = getattr(app.state, "started_at", None)
|
||||
if not isinstance(started_at, (int, float)):
|
||||
if not isinstance(started_at, int | float):
|
||||
return 0.0
|
||||
return round(max(0.0, time.time() - float(started_at)), 3)
|
||||
|
||||
|
|
@ -1072,6 +1072,7 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|||
}
|
||||
if include_config:
|
||||
payload["config"] = {
|
||||
"backend": config.backend,
|
||||
"optimize": config.optimize,
|
||||
"cache": config.cache_enabled,
|
||||
"rate_limit": config.rate_limit_enabled,
|
||||
|
|
|
|||
183
tests/test_cli/test_wrap_copilot.py
Normal file
183
tests/test_cli/test_wrap_copilot.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""Tests for `headroom wrap copilot` command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from headroom.cli import wrap as wrap_cli
|
||||
from headroom.cli.main import main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner() -> CliRunner:
|
||||
return CliRunner()
|
||||
|
||||
|
||||
def test_wrap_copilot_auto_anthropic_injects_instructions(
|
||||
runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_launch_tool(**kwargs): # noqa: ANN003
|
||||
captured.update(kwargs)
|
||||
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
with patch("headroom.cli.wrap._ensure_rtk_binary", return_value=Path("/tmp/rtk")):
|
||||
with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["wrap", "copilot", "--", "--model", "claude-sonnet-4-20250514"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
instructions = tmp_path / ".github" / "copilot-instructions.md"
|
||||
assert instructions.exists()
|
||||
content = instructions.read_text()
|
||||
assert wrap_cli._RTK_MARKER in content
|
||||
assert "RTK (Rust Token Killer)" in content
|
||||
|
||||
env = captured["env"]
|
||||
assert isinstance(env, dict)
|
||||
assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
|
||||
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787"
|
||||
assert "COPILOT_PROVIDER_WIRE_API" not in env
|
||||
assert captured["agent_type"] == "copilot"
|
||||
assert captured["tool_label"] == "COPILOT"
|
||||
assert captured["args"] == ("--model", "claude-sonnet-4-20250514")
|
||||
|
||||
|
||||
def test_wrap_copilot_openai_backend_sets_completions_env(runner: CliRunner) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_launch_tool(**kwargs): # noqa: ANN003
|
||||
captured.update(kwargs)
|
||||
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"wrap",
|
||||
"copilot",
|
||||
"--no-rtk",
|
||||
"--backend",
|
||||
"anyllm",
|
||||
"--anyllm-provider",
|
||||
"groq",
|
||||
"--region",
|
||||
"us-central1",
|
||||
"--",
|
||||
"--model",
|
||||
"gpt-4o",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
|
||||
env = captured["env"]
|
||||
assert isinstance(env, dict)
|
||||
assert env["COPILOT_PROVIDER_TYPE"] == "openai"
|
||||
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
|
||||
assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
|
||||
assert captured["backend"] == "anyllm"
|
||||
assert captured["anyllm_provider"] == "groq"
|
||||
assert captured["region"] == "us-central1"
|
||||
assert captured["args"] == ("--model", "gpt-4o")
|
||||
|
||||
|
||||
def test_wrap_copilot_auto_detects_running_proxy_backend(runner: CliRunner) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_launch_tool(**kwargs): # noqa: ANN003
|
||||
captured.update(kwargs)
|
||||
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
with patch("headroom.cli.wrap._check_proxy", return_value=True):
|
||||
with patch("headroom.cli.wrap._detect_running_proxy_backend", return_value="anyllm"):
|
||||
with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["wrap", "copilot", "--no-rtk", "--", "--model", "gpt-4o"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
env = captured["env"]
|
||||
assert isinstance(env, dict)
|
||||
assert env["COPILOT_PROVIDER_TYPE"] == "openai"
|
||||
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
|
||||
assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
|
||||
|
||||
|
||||
def test_wrap_copilot_rejects_wire_api_for_anthropic_provider(runner: CliRunner) -> None:
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"wrap",
|
||||
"copilot",
|
||||
"--wire-api",
|
||||
"responses",
|
||||
"--",
|
||||
"--model",
|
||||
"claude-sonnet-4-20250514",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "--wire-api is only valid" in result.output
|
||||
|
||||
|
||||
def test_wrap_copilot_rejects_responses_for_translated_backends(runner: CliRunner) -> None:
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"wrap",
|
||||
"copilot",
|
||||
"--backend",
|
||||
"anyllm",
|
||||
"--wire-api",
|
||||
"responses",
|
||||
"--",
|
||||
"--model",
|
||||
"gpt-4o",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "not supported with translated backends" in result.output
|
||||
|
||||
|
||||
def test_wrap_copilot_clears_stale_wire_api_in_anthropic_mode(runner: CliRunner) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_launch_tool(**kwargs): # noqa: ANN003
|
||||
captured.update(kwargs)
|
||||
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value="copilot"):
|
||||
with patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool):
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["wrap", "copilot", "--no-rtk", "--", "--model", "claude-sonnet-4-20250514"],
|
||||
env={"COPILOT_PROVIDER_WIRE_API": "responses"},
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
env = captured["env"]
|
||||
assert isinstance(env, dict)
|
||||
assert env["COPILOT_PROVIDER_TYPE"] == "anthropic"
|
||||
assert "COPILOT_PROVIDER_WIRE_API" not in env
|
||||
|
||||
|
||||
def test_wrap_copilot_fails_when_binary_missing(runner: CliRunner) -> None:
|
||||
with patch("headroom.cli.wrap.shutil.which", return_value=None):
|
||||
result = runner.invoke(main, ["wrap", "copilot", "--", "--model", "gpt-4o"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "'copilot' not found in PATH" in result.output
|
||||
assert "Install GitHub Copilot CLI" in result.output
|
||||
|
|
@ -57,6 +57,7 @@ def test_health_preserves_backwards_compatible_config_payload(client):
|
|||
assert data["status"] == "healthy"
|
||||
assert data["ready"] is True
|
||||
assert data["config"] == {
|
||||
"backend": "anthropic",
|
||||
"optimize": False,
|
||||
"cache": False,
|
||||
"rate_limit": False,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue