fix(copilot): preserve native enterprise model routing (#2998)

## Description

GitHub Copilot Enterprise/Business users without a BYOK provider key
were routed through Copilot CLI's single-model provider override. Native
model aliases and runtime `/model` switches were therefore forwarded
literally to the override and rejected with `400 model not supported`.
This change routes implicit GitHub OAuth through Copilot's native API
surface while retaining explicit subscription and provider-key behavior.

Closes #1910

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Added explicit `--native` routing and made it automatic for implicit
GitHub OAuth without BYOK.
- Clears every Copilot BYOK variable before native launch.
- Routes both OpenAI and Anthropic protocol targets through the resolved
tenant Copilot host.
- Preserves Enterprise/Business native aliases and runtime model
switching.
- Rejects BYOK-only options when native routing is selected.
- Refuses known Copilot bundles that do not reference `COPILOT_API_URL`,
avoiding silent proxy bypass.
- Preserves explicit `--subscription` and provider-key BYOK semantics.
- Added coverage for unreadable and unverifiable Copilot CLI bundles.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
884 passed, 4 skipped in 103.11s
ruff check .: All checks passed
ruff format --check .: 1412 files already formatted
mypy headroom/providers/copilot/wrap.py headroom/cli/wrap.py:
Success: no issues found in 2 source files
```

Exact-head CI is entirely green on
`0aca48c096`.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted macOS and
Ubuntu native-wrap jobs.
- Exact command / steps: invoke `headroom wrap copilot` with implicit
OAuth and an Enterprise model alias; inspect the captured child/proxy
environment and resolved target URLs; exercise explicit native conflicts
and bundle-support probes.
- Observed result: native launch uses `COPILOT_API_URL`, clears all BYOK
state, and points both protocol targets at the tenant host. Native-wrap
jobs are green on macOS and Ubuntu for the refreshed head.
- Not tested: live request against a real Enterprise tenant; the
repository has no organization Enterprise credential available to CI.

## Runtime Rollout Safety

- Rollout-managed feature(s): implicit native Copilot routing for GitHub
OAuth sessions without BYOK.
- Minimum rollout channel: normal patch release.
- Stable/default behavior changed: implicit OAuth now uses native
routing; explicit subscription and BYOK paths are unchanged.
- Kill switch / disable path: use an explicit supported provider-key
BYOK configuration; native mode also fails closed when CLI support is
known absent.
- Unsafe override required: none.
- Qualification impact: native-wrap macOS/Ubuntu, Docker wrapper, full
Python matrix, and Copilot focused suites must pass.
- Rollback path: human revert of this PR restores the fixed-wire OAuth
behavior; no configuration migration is persisted.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation — CLI help
and inline routing documentation; no separate guide required
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; CLI routing change.

## Additional Notes

Human review only. No merge or auto-merge is configured. Refreshed from
main after #2996; the MCP cap `mcp>=1.28.1,<2.0.0` is preserved.
This commit is contained in:
JD Davis 2026-08-25 21:46:31 -05:00 committed by GitHub
parent 632cb81dbe
commit 997a47992c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 362 additions and 69 deletions

View file

@ -32,7 +32,7 @@ import subprocess
import sys
import time
import urllib.parse
from collections.abc import Callable
from collections.abc import Callable, Mapping
from contextlib import contextmanager
from functools import wraps
from pathlib import Path
@ -3943,6 +3943,20 @@ def _copilot_default_wire_api_for_model(model: str | None) -> str:
return _copilot_default_wire_api_for_model_impl(model)
def _build_copilot_native_launch_env(
*, port: int, environ: dict[str, str], project: str | None
) -> tuple[dict[str, str], list[str]]:
from headroom.providers.copilot.wrap import build_native_launch_env
return build_native_launch_env(port=port, environ=environ, project=project)
def _native_api_url_supported(*, environ: Mapping[str, str] | None = None) -> bool | None:
from headroom.providers.copilot.wrap import native_api_url_supported
return native_api_url_supported(environ=environ)
def _should_use_copilot_oauth(
*,
backend: str | None,
@ -4646,6 +4660,7 @@ def _launch_tool(
anyllm_provider: str | None = None,
region: str | None = None,
openai_api_url: str | None = None,
anthropic_api_url: str | None = None,
copilot_api_token: str | None = None,
copilot_refresh_oauth_token: str | None = None,
copilot_api_token_expires_at: float | None = None,
@ -4682,6 +4697,7 @@ def _launch_tool(
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=openai_api_url,
anthropic_api_url=anthropic_api_url,
copilot_api_token=copilot_api_token,
copilot_refresh_oauth_token=copilot_refresh_oauth_token,
copilot_api_token_expires_at=copilot_api_token_expires_at,
@ -5607,6 +5623,14 @@ def _require_copilot_subscription_resolution() -> CopilotSubscriptionTokenResolu
),
)
@click.option("--memory", is_flag=True, help="Enable persistent cross-session memory")
@click.option(
"--native",
is_flag=True,
help=(
"Route Copilot's own GitHub-authenticated API through Headroom instead of "
"the single-model BYOK override. Keeps native model aliases and /model switching."
),
)
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
@click.argument("copilot_args", nargs=-1, type=click.UNPROCESSED)
def copilot(
@ -5619,6 +5643,7 @@ def copilot(
wire_api: str | None,
subscription: bool,
memory: bool,
native: bool,
verbose: bool,
copilot_args: tuple[str, ...],
) -> None:
@ -5653,6 +5678,7 @@ def copilot(
)
raise SystemExit(1)
explicit_subscription = subscription
effective_backend = backend or os.environ.get("HEADROOM_BACKEND")
if _check_proxy(port):
running_backend = _detect_running_proxy_backend(port)
@ -5663,6 +5689,17 @@ def copilot(
)
effective_backend = running_backend or effective_backend
if native:
subscription = True
if provider_type == "anthropic":
raise click.ClickException(
"--native does not use the BYOK provider override; drop --provider-type anthropic."
)
if wire_api is not None:
raise click.ClickException(
"--native selects the wire per request; drop the BYOK-only --wire-api option."
)
effective_provider_type = _resolve_copilot_provider_type(effective_backend, provider_type)
if subscription:
if effective_backend not in (None, "", "anthropic"):
@ -5690,12 +5727,22 @@ def copilot(
copilot_api_token_expires_at: float | None = None
client_bearer: str | None = None
subscription_resolution: CopilotSubscriptionTokenResolution | None = None
if _should_use_copilot_oauth(
anthropic_api_url: str | None = None
use_copilot_oauth = _should_use_copilot_oauth(
backend=effective_backend,
provider_type=provider_type,
env=env,
force_subscription=subscription,
):
)
# Without a provider key, the old implicit OAuth lane still configured
# Copilot as a one-model BYOK client. Native aliases (and runtime /model
# switches) were then forwarded literally and rejected by GitHub (#1910).
# Explicit --subscription remains on its existing fixed-wire behavior;
# implicit GitHub OAuth uses Copilot's own routing automatically.
if use_copilot_oauth and not explicit_subscription:
native = True
if use_copilot_oauth:
if subscription:
subscription_resolution = _require_copilot_subscription_resolution()
client_bearer = subscription_resolution.token
@ -5708,7 +5755,35 @@ def copilot(
"GITHUB_COPILOT_TOKEN / GITHUB_COPILOT_GITHUB_TOKEN."
)
selected_model = _copilot_model_from_args(copilot_args, env)
if native:
openai_api_url = (
subscription_resolution.api_url
if subscription_resolution is not None
else resolve_copilot_api_url(client_bearer)
)
env, env_vars_display = _build_copilot_native_launch_env(
port=port,
environ=env,
project=_project_name_from_cwd(),
)
env["GITHUB_COPILOT_API_URL"] = openai_api_url
env["OPENAI_TARGET_API_URL"] = openai_api_url
env["ANTHROPIC_TARGET_API_URL"] = openai_api_url
anthropic_api_url = openai_api_url
copilot_proxy_token = client_bearer
if subscription_resolution is not None:
copilot_refresh_oauth_token = subscription_resolution.refresh_oauth_token
copilot_api_token_expires_at = subscription_resolution.api_token_expires_at
support = _native_api_url_supported(environ=os.environ)
if support is False:
raise click.ClickException(
"This Copilot CLI build does not reference COPILOT_API_URL; refusing "
"a native launch that could silently bypass Headroom."
)
if support is None and verbose:
click.echo(" Note: could not verify this Copilot CLI's COPILOT_API_URL support.")
else:
selected_model = _copilot_model_from_args(copilot_args, env)
# ``--model auto`` is a Copilot-internal routing token that the BYOK
# API rejects with ``400 The requested model is not supported``. In
@ -5716,7 +5791,7 @@ def copilot(
# Copilot's own native auto-selection works fine — we just need to
# strip the ``--model auto`` flag before launch so Copilot doesn't
# forward it to the provider endpoint.
if _is_auto_model(selected_model):
if not native and _is_auto_model(selected_model):
copilot_args = _strip_auto_model_args(copilot_args)
selected_model = None
click.echo(
@ -5725,57 +5800,58 @@ def copilot(
"automatic model selection."
)
env_wire_api = env.get("COPILOT_PROVIDER_WIRE_API")
effective_wire_api = wire_api or (
env_wire_api
if env_wire_api in {"completions", "responses"}
else _copilot_default_wire_api_for_model(selected_model)
)
env["COPILOT_PROVIDER_TYPE"] = "openai"
# Per-project savings: the Copilot CLI cannot send custom headers, so
# the project rides as a /p/<name> base-URL prefix the proxy strips.
env["COPILOT_PROVIDER_BASE_URL"] = _with_project_prefix(
f"http://127.0.0.1:{port}/v1", _project_name_from_cwd()
)
env["COPILOT_PROVIDER_WIRE_API"] = effective_wire_api
env["COPILOT_PROVIDER_BEARER_TOKEN"] = client_bearer
env["GITHUB_COPILOT_USE_TOKEN_EXCHANGE"] = "false"
env.pop("COPILOT_PROVIDER_API_KEY", None)
# Hand the exact token we resolved (and, for --subscription, validated
# against GitHub) to the proxy explicitly via copilot_proxy_token below.
# The proxy pins it as GITHUB_COPILOT_API_TOKEN, so upstream auth is
# deterministic instead of the proxy re-running unvalidated discovery
# (read_cached_oauth_token returns the *first* candidate, which may not
# be the one the wrapper approved → environment-dependent 401s). Passing
# it as a launch argument — rather than mutating this process's global
# os.environ — keeps the token off shared state and out of unrelated
# code paths.
copilot_proxy_token = client_bearer
if subscription_resolution is not None:
copilot_refresh_oauth_token = subscription_resolution.refresh_oauth_token
copilot_api_token_expires_at = subscription_resolution.api_token_expires_at
env_vars_display = [
"COPILOT_PROVIDER_TYPE=openai",
f"COPILOT_PROVIDER_BASE_URL={env['COPILOT_PROVIDER_BASE_URL']}",
f"COPILOT_PROVIDER_WIRE_API={effective_wire_api}",
(
"COPILOT_AUTH_MODE=github-subscription-experimental"
if subscription
else "COPILOT_AUTH_MODE=github-oauth"
),
]
# Non-subscription OAuth keeps upstream's generic-host policy from
# #610. Subscription mode can use the endpoint returned by the Copilot
# token exchange, which is how Business accounts advertise their API
# host without requiring users to configure it manually.
openai_api_url = (
subscription_resolution.api_url
if subscription_resolution is not None
else resolve_copilot_api_url(client_bearer)
)
env["GITHUB_COPILOT_API_URL"] = openai_api_url
env["OPENAI_TARGET_API_URL"] = openai_api_url
env_vars_display.append(f"COPILOT_PROVIDER_API_URL={openai_api_url}")
if not native:
env_wire_api = env.get("COPILOT_PROVIDER_WIRE_API")
effective_wire_api = wire_api or (
env_wire_api
if env_wire_api in {"completions", "responses"}
else _copilot_default_wire_api_for_model(selected_model)
)
env["COPILOT_PROVIDER_TYPE"] = "openai"
# Per-project savings: the Copilot CLI cannot send custom headers, so
# the project rides as a /p/<name> base-URL prefix the proxy strips.
env["COPILOT_PROVIDER_BASE_URL"] = _with_project_prefix(
f"http://127.0.0.1:{port}/v1", _project_name_from_cwd()
)
env["COPILOT_PROVIDER_WIRE_API"] = effective_wire_api
env["COPILOT_PROVIDER_BEARER_TOKEN"] = client_bearer
env["GITHUB_COPILOT_USE_TOKEN_EXCHANGE"] = "false"
env.pop("COPILOT_PROVIDER_API_KEY", None)
# Hand the exact token we resolved (and, for --subscription, validated
# against GitHub) to the proxy explicitly via copilot_proxy_token below.
# The proxy pins it as GITHUB_COPILOT_API_TOKEN, so upstream auth is
# deterministic instead of the proxy re-running unvalidated discovery
# (read_cached_oauth_token returns the *first* candidate, which may not
# be the one the wrapper approved → environment-dependent 401s). Passing
# it as a launch argument — rather than mutating this process's global
# os.environ — keeps the token off shared state and out of unrelated
# code paths.
copilot_proxy_token = client_bearer
if subscription_resolution is not None:
copilot_refresh_oauth_token = subscription_resolution.refresh_oauth_token
copilot_api_token_expires_at = subscription_resolution.api_token_expires_at
env_vars_display = [
"COPILOT_PROVIDER_TYPE=openai",
f"COPILOT_PROVIDER_BASE_URL={env['COPILOT_PROVIDER_BASE_URL']}",
f"COPILOT_PROVIDER_WIRE_API={effective_wire_api}",
(
"COPILOT_AUTH_MODE=github-subscription-experimental"
if subscription
else "COPILOT_AUTH_MODE=github-oauth"
),
]
# Non-subscription OAuth keeps upstream's generic-host policy from
# #610. Subscription mode can use the endpoint returned by the Copilot
# token exchange, which is how Business accounts advertise their API
# host without requiring users to configure it manually.
openai_api_url = (
subscription_resolution.api_url
if subscription_resolution is not None
else resolve_copilot_api_url(client_bearer)
)
env["GITHUB_COPILOT_API_URL"] = openai_api_url
env["OPENAI_TARGET_API_URL"] = openai_api_url
env_vars_display.append(f"COPILOT_PROVIDER_API_URL={openai_api_url}")
else:
env, env_vars_display = _build_copilot_launch_env(
port=port,
@ -5798,7 +5874,7 @@ def copilot(
)
raise SystemExit(1)
if not subscription and not _copilot_model_configured(copilot_args, env):
if not subscription and not native and not _copilot_model_configured(copilot_args, env):
# Distinguish between "--model auto" (wrong model for BYOK) and
# genuinely missing model (no --model flag at all).
raw_model = _copilot_model_from_args(copilot_args, env)
@ -5835,6 +5911,7 @@ def copilot(
anyllm_provider=anyllm_provider,
region=region,
openai_api_url=openai_api_url,
anthropic_api_url=anthropic_api_url,
copilot_api_token=copilot_proxy_token,
copilot_refresh_oauth_token=copilot_refresh_oauth_token,
copilot_api_token_expires_at=copilot_api_token_expires_at,

View file

@ -156,6 +156,73 @@ def provider_key_source(provider_type: str) -> str:
return "ANTHROPIC_API_KEY" if provider_type == "anthropic" else "OPENAI_API_KEY"
COPILOT_NATIVE_API_URL_ENV = "COPILOT_API_URL"
# Any survivor keeps Copilot in its single-model BYOK lane, defeating native
# model routing while making the launch look superficially successful.
COPILOT_BYOK_ENV_VARS: tuple[str, ...] = (
"COPILOT_PROVIDER_BASE_URL",
"COPILOT_PROVIDER_TYPE",
"COPILOT_PROVIDER_API_KEY",
"COPILOT_PROVIDER_BEARER_TOKEN",
"COPILOT_PROVIDER_WIRE_API",
"COPILOT_PROVIDER_TRANSPORT",
"COPILOT_PROVIDER_AZURE_API_VERSION",
"COPILOT_PROVIDER_MODEL_ID",
"COPILOT_PROVIDER_WIRE_MODEL",
"COPILOT_PROVIDER_MODEL_LIMITS_ID",
"COPILOT_PROVIDER_MAX_PROMPT_TOKENS",
"COPILOT_PROVIDER_MAX_OUTPUT_TOKENS",
"COPILOT_PROVIDER_HEADERS",
)
def build_native_launch_env(
*,
port: int,
environ: Mapping[str, str] | None = None,
project: str | None = None,
) -> tuple[dict[str, str], list[str]]:
"""Redirect Copilot's native API surface through Headroom, not BYOK."""
env = dict(environ if environ is not None else os.environ)
base_url = with_project_prefix(f"http://127.0.0.1:{port}", project)
env[COPILOT_NATIVE_API_URL_ENV] = base_url
for variable in COPILOT_BYOK_ENV_VARS:
env.pop(variable, None)
return env, [
f"{COPILOT_NATIVE_API_URL_ENV}={base_url}",
"COPILOT_AUTH_MODE=github-native",
]
def native_api_url_supported(*, environ: Mapping[str, str] | None = None) -> bool | None:
"""Best-effort tri-state probe for the CLI's native API URL override."""
env = environ if environ is not None else os.environ
local = env.get("LOCALAPPDATA") or env.get("HOME") or os.path.expanduser("~")
roots = (
os.path.join(local, "copilot", "pkg"),
os.path.join(os.path.expanduser("~"), ".local", "share", "copilot", "pkg"),
)
found_bundle = False
for root in roots:
if not os.path.isdir(root):
continue
for dirpath, _dirnames, filenames in os.walk(root):
if "app.js" not in filenames:
continue
found_bundle = True
try:
with open(
os.path.join(dirpath, "app.js"), encoding="utf-8", errors="replace"
) as bundle:
while chunk := bundle.read(1 << 20):
if COPILOT_NATIVE_API_URL_ENV in chunk:
return True
except OSError:
continue
return False if found_bundle else None
def build_launch_env(
*,
port: int,

View file

@ -266,17 +266,16 @@ def test_wrap_copilot_prefers_existing_oauth_session(
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"] == (
f"http://127.0.0.1:8787{_expected_project_prefix()}/v1"
)
assert env["COPILOT_PROVIDER_WIRE_API"] == "completions"
assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-existing"
assert env["COPILOT_API_URL"] == f"http://127.0.0.1:8787{_expected_project_prefix()}"
assert "COPILOT_PROVIDER_TYPE" not in env
assert "COPILOT_PROVIDER_BASE_URL" not in env
assert "COPILOT_PROVIDER_WIRE_API" not in env
assert "COPILOT_PROVIDER_BEARER_TOKEN" not in env
assert env["GITHUB_COPILOT_API_URL"] == DEFAULT_API_URL
assert env["OPENAI_TARGET_API_URL"] == DEFAULT_API_URL
assert "COPILOT_PROVIDER_API_KEY" not in env
assert captured["openai_api_url"] == DEFAULT_API_URL
assert f"COPILOT_PROVIDER_API_URL={DEFAULT_API_URL}" in captured["env_vars_display"]
assert "COPILOT_AUTH_MODE=github-native" in captured["env_vars_display"]
@pytest.mark.parametrize(
@ -293,7 +292,7 @@ def test_wrap_copilot_oauth_defaults_wire_api_for_selected_model(
model: str,
expected_wire_api: str,
) -> None:
"""OAuth sessions use the same model-aware wire API default as subscriptions."""
"""Implicit OAuth leaves wire selection to Copilot's native router."""
_wrap_cli, main = wrap_modules
_clear_copilot_env(monkeypatch)
captured: dict[str, object] = {}
@ -315,8 +314,8 @@ def test_wrap_copilot_oauth_defaults_wire_api_for_selected_model(
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
assert env["COPILOT_PROVIDER_WIRE_API"] == expected_wire_api
assert f"COPILOT_PROVIDER_WIRE_API={expected_wire_api}" in captured["env_vars_display"]
assert "COPILOT_PROVIDER_WIRE_API" not in env
assert env["COPILOT_API_URL"].startswith("http://127.0.0.1:8787")
@pytest.mark.parametrize("wire_api", ["completions", "responses"])
@ -348,7 +347,8 @@ def test_wrap_copilot_oauth_honors_existing_wire_api(
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
assert env["COPILOT_PROVIDER_WIRE_API"] == wire_api
assert "COPILOT_PROVIDER_WIRE_API" not in env
assert env["COPILOT_API_URL"].startswith("http://127.0.0.1:8787")
def test_wrap_copilot_subscription_uses_github_auth_without_provider_key(
@ -869,7 +869,8 @@ def test_wrap_copilot_oauth_keeps_generic_endpoint_when_account_advertised(
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "gho-oauth"
assert "COPILOT_PROVIDER_BEARER_TOKEN" not in env
assert env["COPILOT_API_URL"].startswith("http://127.0.0.1:8787")
assert captured["openai_api_url"] == DEFAULT_API_URL
assert env["OPENAI_TARGET_API_URL"] == DEFAULT_API_URL
assert env["GITHUB_COPILOT_API_URL"] == DEFAULT_API_URL

View file

@ -0,0 +1,148 @@
from __future__ import annotations
import pytest
from click.testing import CliRunner
from headroom.providers.copilot.wrap import (
COPILOT_BYOK_ENV_VARS,
COPILOT_NATIVE_API_URL_ENV,
build_launch_env,
build_native_launch_env,
native_api_url_supported,
)
def test_native_env_redirects_api_and_clears_all_byok_state() -> None:
seeded = dict.fromkeys(COPILOT_BYOK_ENV_VARS, "stale")
seeded["UNRELATED"] = "preserved"
env, _ = build_native_launch_env(port=8890, environ=seeded, project="repo name")
assert env[COPILOT_NATIVE_API_URL_ENV] == "http://127.0.0.1:8890/p/repo%20name"
assert env["UNRELATED"] == "preserved"
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
def test_byok_builder_remains_disjoint_from_native_mode() -> None:
env, _ = build_launch_env(
port=8787,
provider_type="openai",
wire_api="responses",
environ={},
)
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
assert env["COPILOT_PROVIDER_WIRE_API"] == "responses"
assert COPILOT_NATIVE_API_URL_ENV not in env
def test_native_support_probe_distinguishes_unknown_and_unsupported(tmp_path) -> None:
local = tmp_path / "local"
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is None
bundle = local / "copilot" / "pkg" / "platform" / "1.0" / "app.js"
bundle.parent.mkdir(parents=True)
bundle.write_text("no override here", encoding="utf-8")
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is False
bundle.write_text("process.env.COPILOT_API_URL", encoding="utf-8")
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is True
def test_native_support_probe_skips_unreadable_bundle(monkeypatch, tmp_path) -> None:
local = tmp_path / "local"
bundle = local / "copilot" / "pkg" / "platform" / "1.0" / "app.js"
bundle.parent.mkdir(parents=True)
bundle.write_text("process.env.COPILOT_API_URL", encoding="utf-8")
def _unreadable(*_args, **_kwargs):
raise OSError("synthetic unreadable bundle")
monkeypatch.setattr("builtins.open", _unreadable)
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is False
def _invoke_native(
monkeypatch: pytest.MonkeyPatch,
extra: list[str] | None = None,
*,
support: bool | None = True,
):
from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main
captured: dict[str, object] = {}
class Resolution:
token = "copilot-token"
api_url = "https://api.business.githubcopilot.com"
refresh_oauth_token = "refresh-token"
api_token_expires_at = 123.0
monkeypatch.setattr(wrap_mod.shutil, "which", lambda _name: "/usr/bin/copilot")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda _port: False)
monkeypatch.setattr(wrap_mod, "_require_copilot_subscription_resolution", lambda: Resolution())
monkeypatch.setattr(wrap_mod, "_native_api_url_supported", lambda **_kwargs: support)
monkeypatch.setattr(wrap_mod, "_launch_tool", lambda **kwargs: captured.update(kwargs))
result = CliRunner().invoke(
main,
["wrap", "copilot", "--native", "--port", "8890", *(extra or [])],
)
return result, captured
def test_implicit_oauth_uses_native_routing_without_flag(monkeypatch) -> None:
from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main
captured: dict[str, object] = {}
monkeypatch.setattr(wrap_mod.shutil, "which", lambda _name: "/usr/bin/copilot")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda _port: False)
monkeypatch.setattr(wrap_mod, "has_oauth_auth", lambda: True)
monkeypatch.setattr(wrap_mod, "resolve_client_bearer_token", lambda: "oauth-token")
monkeypatch.setattr(
wrap_mod, "resolve_copilot_api_url", lambda _token: "https://api.githubcopilot.com"
)
monkeypatch.setattr(wrap_mod, "_native_api_url_supported", lambda **_kwargs: True)
monkeypatch.setattr(wrap_mod, "_launch_tool", lambda **kwargs: captured.update(kwargs))
result = CliRunner().invoke(
main,
["wrap", "copilot", "--port", "8890", "--", "--model", "claude-sonnet-5"],
)
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
assert COPILOT_NATIVE_API_URL_ENV in env
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
def test_native_cli_routes_both_protocols_to_tenant_host(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch)
assert result.exit_code == 0, result.output
assert captured["openai_api_url"] == "https://api.business.githubcopilot.com"
assert captured["anthropic_api_url"] == "https://api.business.githubcopilot.com"
env = captured["env"]
assert isinstance(env, dict)
assert COPILOT_NATIVE_API_URL_ENV in env
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
@pytest.mark.parametrize("extra", [["--wire-api", "responses"], ["--provider-type", "anthropic"]])
def test_native_cli_rejects_byok_only_options(monkeypatch, extra) -> None:
result, captured = _invoke_native(monkeypatch, extra)
assert result.exit_code != 0
assert not captured
def test_native_cli_refuses_known_unsupported_bundle(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch, support=False)
assert result.exit_code != 0
assert "COPILOT_API_URL" in result.output
assert not captured
def test_native_cli_reports_unknown_support_in_verbose_mode(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch, ["--verbose"], support=None)
assert result.exit_code == 0, result.output
assert "could not verify" in result.output
assert captured