mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
#3077 stopped Copilot's inline completions being forwarded to
`api.openai.com` (the corporate-blocked host in the original report) —
but sent them to the **CAPI host**, which does not serve that endpoint.
Copilot has two surfaces on two different hosts, and GitHub's own client
library keeps them apart:
```js
_getCAPIUrl(t) -> t?.endpoints.api || "https://api.githubcopilot.com"
_getProxyUrl(t) -> t?.endpoints.proxy || DEFAULT_PROXY_BASE_URL
DEFAULT_PROXY_BASE_URL = "https://copilot-proxy.githubusercontent.com"
```
building completions as
`${proxyBaseURL}/v1/engines/<engine>/completions` (`@vscode/copilot-api`
0.5.2). Probed unauthenticated against the live hosts:
| host | `POST /v1/engines/<e>/completions` |
|---|---|
| `copilot-proxy.githubusercontent.com` | **401** — exists, needs auth |
| `proxy.individual.githubcopilot.com` | **401** — CNAME to the above |
| `api.githubcopilot.com` | **404** — does not serve this path |
So the destination #3077 chose could not have worked. Three separate
defects were in the way, each sufficient on its own to keep completions
broken.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `copilot_auth.py`: added `DEFAULT_COMPLETIONS_PROXY_URL` and made it
the default in `copilot_completions_base_url()`, replacing the CAPI
host.
- `copilot_auth.py`: the "custom deployment keeps its own host" rule now
excludes public Copilot hosts. Without this, `headroom wrap vscode` —
the common setup, and the one that exports
`GITHUB_COPILOT_API_URL=<resolved subscription URL>` — resolved straight
back to the 404 host. **This was a bug in my own first cut of the fix,
found by testing the real `wrap vscode` environment rather than just the
routing table.**
- `copilot_auth.py`: added `is_copilot_completions_host()` and
`is_copilot_upstream_url()` (chat ∪ completions). The completions host
was recognised as Copilot **nowhere**, so `apply_copilot_api_auth`
attached no credentials (401 — routing correctly to a host we then
failed to authenticate against) and `build_copilot_upstream_url` skipped
`mark_request_routed_to_copilot()`, mislabelling the provider in
telemetry.
- The union is applied at exactly those two call sites.
`is_copilot_api_url` is left alone, so validation of a token payload's
`endpoints.api` and the Responses-API preference check keep their strict
chat-only meaning. All six call sites were read before choosing this.
- `proxy_targets.py`: the "already a Copilot host" guard now keys on the
*completions* host. A CAPI host is not a completions host, so it must
still be redirected; a genuine per-SKU completions host or operator
override is still left untouched.
- `providers/copilot/vscode.py`, `cli/wrap.py`,
`docs/…/vscode-copilot.mdx`: stop writing/printing
`github.copilot.advanced.debug.overrideAuthType`. No such setting exists
in the modern Copilot Chat extension — the only one left after
`GitHub.copilot` was deprecated in early 2026. Its full `advanced.*`
surface is `authPermissions`, `authProvider`, `debug.overrideCapiUrl`,
`debug.overrideProxyUrl`, `debug.use*Fetcher`. It is still *recognised*
so a stale hand-written copy is detected, just never emitted.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
tests/test_copilot_vscode_completions_routing.py 59 passed
Copilot-related suites 293 passed, 8 skipped
Full suite:
3 failed, 11250 passed, 581 skipped in 342.50s
```
The 3 failures are pre-existing and environmental, identical to a
plain-`main` baseline on this machine: no `cargo`
(`test_no_native_tls_in_wheel_build_tree`), no `codex` CLI
(`test_learn/test_integration.py`), and
`test_run_server_installs_cancelled_error_filter`, which fails under
full-suite ordering on `main` too.
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0), Python 3.12.13, worktree off
`main` @ `139c7cbd`, `HEADROOM_SKIP_UPSTREAM_CHECK=1`
- Exact command / steps: (1) composed the real request path —
`select_passthrough_base_url(proxy, headers, path)` →
`build_copilot_upstream_url` → `apply_copilot_api_auth` — across 7
deployment shapes (no config, `wrap vscode`, advertised
`endpoints.proxy`, operator override, GHE `.ghe.com`, GHE custom domain,
target already a completions host); (2) probed the three candidate hosts
unauthenticated with `curl -X POST
/v1/engines/gpt-4o-copilot/completions`; (3) round-tripped
`settings.json` through empty / one-setting / comments+array / CRLF
shapes asserting valid JSON, idempotency and clean removal.
- Observed result: before — `api.githubcopilot.com/...` (404 host), and
with `GITHUB_COPILOT_API_URL` set as `wrap vscode` sets it,
`api.business.githubcopilot.com` (also 404); no `Authorization` header
on the completions host. After —
`copilot-proxy.githubusercontent.com/v1/engines/gpt-41-copilot/completions`
with credentials attached in every public-Copilot shape,
`endpoints.proxy` and the operator override still winning, and a GHE
tenant staying on its own host. `settings.json` stays valid JSON in all
four shapes with the dead key gone; the two `restored=False` cases are
pre-existing whitespace/CRLF normalisation, identical on `main`.
Reverting the source fails 14 of the new tests, including the credential
test on the completions host.
- Not tested: no live VS Code session and no authenticated completion —
the 401 proves the endpoint exists, not that GitHub accepts our
forwarded request, which needs a real Copilot token. Confirmation from
@rganesh-msys is still wanted. **Enterprise remains unresolved by
default**: a GHE tenant stays on its own CAPI host, which is likely
still the wrong surface for completions, but staying in-tenant beats
forwarding keystrokes to a public GitHub host —
`GITHUB_COPILOT_PROXY_URL` is the exact fix and now takes precedence
over everything.
## Runtime Rollout Safety
- Rollout-managed feature(s): None — no rollout channel gates this.
- Minimum rollout channel: n/a
- Stable/default behavior changed: Yes, and deliberately — the
completions destination moves from a host that answers 404 to the one
GitHub's own client defaults to. Only `/v1/engines/<engine>/completions`
is affected; every other path keeps its upstream, pinned by tests.
Copilot credentials now also reach the completions host, which is the
point.
- Kill switch / disable path: `GITHUB_COPILOT_PROXY_URL` pins the
destination explicitly and beats all inference.
- Unsafe override required: No.
- Qualification impact: None.
- Rollback path: Revert this commit; completions return to the CAPI host
(404) and the settings block regains the inert `overrideAuthType`.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
Two things found while reading the extension source, **not changed
here**:
1. `advanced.debug.overrideProxyUrl` is **not** deprecated — the report
that Copilot 0.60.0 stopped honouring it does not hold. The current
canonical key is `github.copilot.internal.completionsUrl`, and
`advanced.debug.overrideProxyUrl` is checked as its explicit legacy
fallback (`getEndpointOverrideUrl` in
`completions-core/lib/src/networkConfiguration.ts`), so what we write
still works. Worth migrating to the `internal.*` keys eventually, since
they take precedence.
2. `endpoints.proxy` is still only recorded during a token exchange,
which is opt-in via `GITHUB_COPILOT_USE_TOKEN_EXCHANGE`, and the base
URL is chosen before auth runs. With the default now correct this is a
refinement for per-SKU hosts rather than a correctness requirement, so
it is left as-is.
Closes #3076
---------
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""CLI coverage for transparent VS Code Copilot setup and undo."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from headroom.cli.main import main
|
|
from headroom.copilot_auth import CopilotSubscriptionTokenResolution
|
|
|
|
|
|
def _resolution() -> CopilotSubscriptionTokenResolution:
|
|
return CopilotSubscriptionTokenResolution(
|
|
token="copilot-token",
|
|
source="test",
|
|
confidence="test",
|
|
api_url="https://api.githubcopilot.com",
|
|
token_fingerprint="sha256:test",
|
|
)
|
|
|
|
|
|
def test_wrap_vscode_configures_actual_port_and_seeds_subscription(tmp_path: Path) -> None:
|
|
path = tmp_path / "settings.json"
|
|
captured = {}
|
|
|
|
def fake_watcher(**kwargs): # noqa: ANN003, ANN202
|
|
captured.update(kwargs)
|
|
kwargs["print_setup_lines"](9999)
|
|
|
|
with (
|
|
patch(
|
|
"headroom.cli.wrap._require_copilot_subscription_resolution", return_value=_resolution()
|
|
),
|
|
patch("headroom.cli.wrap._run_proxy_only_watcher", side_effect=fake_watcher),
|
|
):
|
|
result = CliRunner().invoke(main, ["wrap", "vscode", "--settings-file", str(path)])
|
|
|
|
assert result.exit_code == 0, result.output
|
|
settings = path.read_text(encoding="utf-8")
|
|
assert "http://127.0.0.1:9999/" in settings
|
|
assert "model" not in settings.lower()
|
|
assert "normal model picker" in result.output
|
|
assert captured["openai_api_url"] == "https://api.githubcopilot.com"
|
|
assert captured["copilot_api_token"] == "copilot-token"
|
|
|
|
|
|
def test_wrap_vscode_no_configure_prints_transparent_settings(tmp_path: Path) -> None:
|
|
path = tmp_path / "settings.json"
|
|
|
|
def fake_watcher(**kwargs): # noqa: ANN003, ANN202
|
|
kwargs["print_setup_lines"](8787)
|
|
|
|
with (
|
|
patch(
|
|
"headroom.cli.wrap._require_copilot_subscription_resolution", return_value=_resolution()
|
|
),
|
|
patch("headroom.cli.wrap._run_proxy_only_watcher", side_effect=fake_watcher),
|
|
):
|
|
result = CliRunner().invoke(
|
|
main,
|
|
["wrap", "vscode", "--no-configure", "--settings-file", str(path)],
|
|
)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
assert not path.exists()
|
|
assert "overrideProxyUrl" in result.output
|
|
assert "overrideCapiUrl" in result.output
|
|
# No `overrideAuthType`: the setting does not exist in the modern Copilot
|
|
# Chat extension, so printing it told users to add a key VS Code flags as
|
|
# unknown and which does nothing (#3076).
|
|
assert "overrideAuthType" not in result.output
|
|
|
|
|
|
def test_unwrap_vscode_removes_only_managed_settings(tmp_path: Path) -> None:
|
|
path = tmp_path / "settings.json"
|
|
original = '{\n "editor.fontSize": 14\n}\n'
|
|
path.write_text(original, encoding="utf-8")
|
|
from headroom.providers.copilot.vscode import configure_vscode_proxy_settings
|
|
|
|
configure_vscode_proxy_settings(path, "http://127.0.0.1:8787")
|
|
result = CliRunner().invoke(main, ["unwrap", "vscode", "--settings-file", str(path)])
|
|
assert result.exit_code == 0, result.output
|
|
assert path.read_text(encoding="utf-8") == original
|