From fb07215844cdc6d08e1f266288c284b95014df2f Mon Sep 17 00:00:00 2001 From: NiuNiu Xia Date: Fri, 22 May 2026 15:30:22 +0000 Subject: [PATCH] fix(copilot): recognize enterprise subdomains in host checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier enterprise base URL change (proxy-ep parsing) gave us URLs like `api.enterprise.githubcopilot.com`, but ~15 host-matching call sites still hard-coded `api.githubcopilot.com`. Enterprise users would therefore drop the `Copilot-Integration-Id: vscode-chat` header at client-build time, and upstream rejected requests with: The requested model is not available for integrator "zed" (or "copilot-language-server") — verify the correct Copilot-Integration-Id header is being sent. The header was correct in copilot_default_headers(); it just never made it into default_headers for non-default hostnames because every detector compared against the exact string "api.githubcopilot.com". This commit broadens all those checks to "githubcopilot.com" via base_url_host_matches (which already does proper subdomain matching), so api.enterprise.githubcopilot.com, api.business.githubcopilot.com, etc. all share the same headers, vision routing, max_completion_tokens selection, and reasoning-effort detection as the default endpoint. Also adds ".githubcopilot.com" to _URL_TO_PROVIDER so context-window resolution via models.dev works for enterprise base URLs, and tightens _is_github_copilot_url to use suffix matching instead of strict equality. Tests: - New: enterprise Copilot endpoint preserves Copilot-Integration-Id - New: enterprise endpoint returns max_completion_tokens (not max_tokens) - Existing 333 base_url / copilot / aux-client / credential-pool tests pass Parts 5 of #7731. --- agent/agent_init.py | 2 +- agent/auxiliary_client.py | 19 ++++++++----- agent/chat_completion_helpers.py | 4 +-- agent/model_metadata.py | 4 +++ run_agent.py | 10 ++++--- .../test_provider_attribution_headers.py | 28 +++++++++++++++++++ tests/run_agent/test_run_agent.py | 7 +++++ 7 files changed, 60 insertions(+), 14 deletions(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index 12597e5050f..dcfb1082d4c 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -828,7 +828,7 @@ def init_agent( client_kwargs["default_headers"] = build_nvidia_nim_headers(effective_base) elif base_url_host_matches(effective_base, "api.routermint.com"): client_kwargs["default_headers"] = _ra()._routermint_headers() - elif base_url_host_matches(effective_base, "api.githubcopilot.com"): + elif base_url_host_matches(effective_base, "githubcopilot.com"): from hermes_cli.models import copilot_default_headers client_kwargs["default_headers"] = copilot_default_headers() diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 459ec8da2ac..f24e2bd7d6a 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -1624,7 +1624,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: extra = {} if base_url_host_matches(base_url, "api.kimi.com"): extra["default_headers"] = {"User-Agent": "claude-code/0.1.0"} - elif base_url_host_matches(base_url, "api.githubcopilot.com"): + elif base_url_host_matches(base_url, "githubcopilot.com"): from hermes_cli.models import copilot_default_headers extra["default_headers"] = copilot_default_headers() @@ -1664,7 +1664,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: extra = {} if base_url_host_matches(base_url, "api.kimi.com"): extra["default_headers"] = {"User-Agent": "claude-code/0.1.0"} - elif base_url_host_matches(base_url, "api.githubcopilot.com"): + elif base_url_host_matches(base_url, "githubcopilot.com"): from hermes_cli.models import copilot_default_headers extra["default_headers"] = copilot_default_headers() @@ -2954,7 +2954,7 @@ def _recoverable_pool_provider( return "nous" if base_url_host_matches(base, "api.anthropic.com"): return "anthropic" - if base_url_host_matches(base, "api.githubcopilot.com"): + if base_url_host_matches(base, "githubcopilot.com"): return "copilot" if base_url_host_matches(base, "api.kimi.com"): return "kimi-coding" @@ -3823,7 +3823,7 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False): sync_base_url = str(sync_client.base_url) if base_url_host_matches(sync_base_url, "openrouter.ai"): async_kwargs["default_headers"] = build_or_headers() - elif base_url_host_matches(sync_base_url, "api.githubcopilot.com"): + elif base_url_host_matches(sync_base_url, "githubcopilot.com"): from hermes_cli.copilot_auth import copilot_request_headers async_kwargs["default_headers"] = copilot_request_headers( @@ -4128,7 +4128,7 @@ def resolve_provider_client( extra["default_query"] = _dq if base_url_host_matches(custom_base, "api.kimi.com"): extra["default_headers"] = {"User-Agent": "claude-code/0.1.0"} - elif base_url_host_matches(custom_base, "api.githubcopilot.com"): + elif base_url_host_matches(custom_base, "githubcopilot.com"): from hermes_cli.copilot_auth import copilot_request_headers extra["default_headers"] = copilot_request_headers( is_agent_turn=True, is_vision=is_vision @@ -4381,7 +4381,7 @@ def resolve_provider_client( headers = {} if base_url_host_matches(base_url, "api.kimi.com"): headers["User-Agent"] = "claude-code/0.1.0" - elif base_url_host_matches(base_url, "api.githubcopilot.com"): + elif base_url_host_matches(base_url, "githubcopilot.com"): from hermes_cli.copilot_auth import copilot_request_headers headers.update(copilot_request_headers( @@ -4854,9 +4854,14 @@ def auxiliary_max_tokens_param(value: int, *, model: Optional[str] = None) -> di or_key = os.getenv("OPENROUTER_API_KEY") # Use max_completion_tokens for direct OpenAI-compatible providers that reject # max_tokens on newer GPT-4o/o-series/GPT-5-style models. + _custom_host = base_url_hostname(custom_base) or "" if (not or_key and _read_nous_auth() is None - and base_url_hostname(custom_base) in {"api.openai.com", "api.githubcopilot.com"}): + and ( + _custom_host == "api.openai.com" + or _custom_host == "api.githubcopilot.com" + or _custom_host.endswith(".githubcopilot.com") + )): return {"max_completion_tokens": value} # ...and for any caller serving a newer OpenAI-family model by name. if model_forces_max_completion_tokens(model): diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index ace3b6439de..7a529052e78 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -632,7 +632,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict: _ct = agent._get_transport() is_github_responses = ( base_url_host_matches(agent.base_url, "models.github.ai") - or base_url_host_matches(agent.base_url, "api.githubcopilot.com") + or base_url_host_matches(agent.base_url, "githubcopilot.com") ) is_codex_backend = ( agent.provider == "openai-codex" @@ -702,7 +702,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict: _is_or = agent._is_openrouter_url() _is_gh = ( base_url_host_matches(agent._base_url_lower, "models.github.ai") - or base_url_host_matches(agent._base_url_lower, "api.githubcopilot.com") + or base_url_host_matches(agent._base_url_lower, "githubcopilot.com") ) _is_nous = "nousresearch" in agent._base_url_lower _is_nvidia = "integrate.api.nvidia.com" in agent._base_url_lower diff --git a/agent/model_metadata.py b/agent/model_metadata.py index e2ca04399ab..734febd3bf4 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -429,6 +429,10 @@ _URL_TO_PROVIDER: Dict[str, str] = { "inference-api.nousresearch.com": "nous", "api.deepseek.com": "deepseek", "api.githubcopilot.com": "copilot", + # Enterprise Copilot endpoints look like api.enterprise.githubcopilot.com, + # api.business.githubcopilot.com, etc. Match the suffix so context-window + # resolution works for enterprise accounts too. + ".githubcopilot.com": "copilot", "models.github.ai": "copilot", # GitHub Models free tier (Azure-hosted prototyping endpoint) — same # canonical provider as the Copilot API. Hard per-request token cap diff --git a/run_agent.py b/run_agent.py index 467744c5a1c..0116edf876d 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1133,7 +1133,9 @@ class AIAgent: hostname = getattr(self, "_base_url_hostname", "") or base_url_hostname( getattr(self, "_base_url_lower", "") ) - return hostname == "api.githubcopilot.com" + if not hostname: + return False + return hostname == "api.githubcopilot.com" or hostname.endswith(".githubcopilot.com") def _resolved_api_call_timeout(self) -> float: """Resolve the effective per-call request timeout in seconds. @@ -3837,7 +3839,7 @@ class AIAgent: # unaffected (they don't go through here). request_kwargs["max_retries"] = 0 if ( - base_url_host_matches(str(request_kwargs.get("base_url", "")), "api.githubcopilot.com") + base_url_host_matches(str(request_kwargs.get("base_url", "")), "githubcopilot.com") and self._api_kwargs_have_image_parts(api_kwargs or {}) ): request_kwargs["default_headers"] = self._copilot_headers_for_request(is_vision=True) @@ -4099,7 +4101,7 @@ class AIAgent: self._client_kwargs["default_headers"] = build_nvidia_nim_headers(base_url) elif base_url_host_matches(base_url, "api.routermint.com"): self._client_kwargs["default_headers"] = _routermint_headers() - elif base_url_host_matches(base_url, "api.githubcopilot.com"): + elif base_url_host_matches(base_url, "githubcopilot.com"): from hermes_cli.models import copilot_default_headers self._client_kwargs["default_headers"] = copilot_default_headers() @@ -4995,7 +4997,7 @@ class AIAgent: return True if ( base_url_host_matches(self._base_url_lower, "models.github.ai") - or base_url_host_matches(self._base_url_lower, "api.githubcopilot.com") + or base_url_host_matches(self._base_url_lower, "githubcopilot.com") ): try: from hermes_cli.models import github_model_reasoning_efforts diff --git a/tests/run_agent/test_provider_attribution_headers.py b/tests/run_agent/test_provider_attribution_headers.py index dab69d57b3d..dec932e34e0 100644 --- a/tests/run_agent/test_provider_attribution_headers.py +++ b/tests/run_agent/test_provider_attribution_headers.py @@ -320,3 +320,31 @@ def test_openrouter_headers_no_cache_when_disabled(mock_openai): assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com" assert "X-OpenRouter-Cache" not in headers assert "X-OpenRouter-Cache-TTL" not in headers + + +@patch("run_agent.OpenAI") +def test_copilot_enterprise_base_url_applies_copilot_default_headers(mock_openai): + """Enterprise Copilot endpoints (api..githubcopilot.com) must apply + the same default_headers — including Copilot-Integration-Id: vscode-chat — + as the default api.githubcopilot.com endpoint. Without this, the upstream + sees the request as integrator 'zed' or 'copilot-language-server' and + rejects it with a 400 error for many models (regression seen May 2026).""" + mock_openai.return_value = MagicMock() + agent = AIAgent( + api_key="test-key", + base_url="https://api.enterprise.githubcopilot.com", + model="claude-opus-4.6-1m", + provider="copilot", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + agent._apply_client_headers_for_base_url("https://api.enterprise.githubcopilot.com") + + headers = agent._client_kwargs.get("default_headers", {}) + # Lookup is case-insensitive — normalize for the assertion. + lc = {k.lower(): v for k, v in headers.items()} + assert lc.get("copilot-integration-id") == "vscode-chat", ( + f"enterprise Copilot endpoint must carry Copilot-Integration-Id=vscode-chat; got {headers}" + ) diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index ad9204223c5..7995d8feb04 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -5699,6 +5699,13 @@ class TestMaxTokensParam: result = agent._max_tokens_param(4096) assert result == {"max_tokens": 4096} + def test_returns_max_completion_tokens_for_enterprise_copilot(self, agent): + """Enterprise Copilot endpoints (api..githubcopilot.com) must + share the same max_tokens behavior as the default endpoint.""" + agent.base_url = "https://api.enterprise.githubcopilot.com" + result = agent._max_tokens_param(4096) + assert result == {"max_completion_tokens": 4096} + class TestGpt5ApiModeRouting: """Verify provider-specific GPT-5 API-mode routing."""