diff --git a/headroom/copilot_auth.py b/headroom/copilot_auth.py index 6484ed533..67654f5cd 100644 --- a/headroom/copilot_auth.py +++ b/headroom/copilot_auth.py @@ -780,7 +780,7 @@ def _api_url_from_exchange_payload(payload: dict[str, Any], *, oauth_token: str) api_url = _api_url_from_payload(payload) if api_url: if is_copilot_api_url(api_url): - return api_url + return _subscription_api_url_from_user_info_payload({"endpoints": {"api": api_url}}) logger.warning( "Ignoring non-Copilot API URL from token exchange payload: %s", api_url, diff --git a/tests/test_cli/test_wrap_copilot.py b/tests/test_cli/test_wrap_copilot.py index 0323012e7..a99794d13 100644 --- a/tests/test_cli/test_wrap_copilot.py +++ b/tests/test_cli/test_wrap_copilot.py @@ -956,6 +956,57 @@ def test_wrap_copilot_subscription_uses_resolved_subscription_endpoint( assert env["COPILOT_PROVIDER_BEARER_TOKEN"] == "copilot-api" +def test_wrap_copilot_subscription_normalizes_individual_public_endpoint( + runner: CliRunner, + wrap_modules: tuple[types.ModuleType, click.Group], + monkeypatch: pytest.MonkeyPatch, +) -> None: + _wrap_cli, main = wrap_modules + _clear_copilot_env(monkeypatch) + captured: dict[str, object] = {} + + def fake_launch_tool(**kwargs): # noqa: ANN003 + captured.update(kwargs) + + with ( + patch("headroom.cli.wrap.shutil.which", return_value="copilot"), + patch("headroom.cli.wrap.has_oauth_auth", return_value=True), + patch( + "headroom.copilot_auth.iter_oauth_token_candidates", + return_value=[ + types.SimpleNamespace( + token="gho-oauth", + source="headroom-copilot-auth:/tmp/copilot_auth.json", + confidence="copilot-oauth", + validate_for_subscription=True, + ) + ], + ), + patch( + "headroom.copilot_auth.CopilotTokenProvider._exchange_token_sync", + staticmethod( + lambda _headers: { + "token": "copilot-api", + "expires_at": 9999999999, + "endpoints": {"api": "https://api.individual.githubcopilot.com"}, + } + ), + ), + patch("headroom.cli.wrap._launch_tool", side_effect=fake_launch_tool), + ): + result = runner.invoke( + main, + ["wrap", "copilot", "--subscription", "--no-rtk", "--", "--model", "gpt-5.4"], + ) + + assert result.exit_code == 0, result.output + env = captured["env"] + assert isinstance(env, dict) + 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 + + def test_wrap_copilot_subscription_honors_api_url_override( runner: CliRunner, wrap_modules: tuple[types.ModuleType, click.Group], diff --git a/tests/test_copilot_auth.py b/tests/test_copilot_auth.py index 389c53776..4b22c9997 100644 --- a/tests/test_copilot_auth.py +++ b/tests/test_copilot_auth.py @@ -270,6 +270,21 @@ def test_api_url_from_exchange_payload_rejects_non_copilot_host( assert resolved == "https://api.business.githubcopilot.com" +def test_api_url_from_exchange_payload_normalizes_individual_public_host( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("GITHUB_COPILOT_API_URL", raising=False) + monkeypatch.delenv("GITHUB_COPILOT_ENTERPRISE_URL", raising=False) + monkeypatch.delenv("GITHUB_COPILOT_ENTERPRISE_DOMAIN", raising=False) + + resolved = copilot_auth._api_url_from_exchange_payload( + {"endpoints": {"api": "https://api.individual.githubcopilot.com"}}, + oauth_token="gho-oauth", + ) + + assert resolved == copilot_auth.DEFAULT_API_URL + + def test_api_url_from_exchange_payload_rejects_non_copilot_host_without_user_info( monkeypatch: pytest.MonkeyPatch, ) -> None: