fix(copilot): recognize enterprise subdomains in host checks

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.
This commit is contained in:
NiuNiu Xia 2026-05-22 15:30:22 +00:00 committed by Teknium
parent fbd15e285c
commit fb07215844
7 changed files with 60 additions and 14 deletions

View file

@ -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.<tenant>.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}"
)

View file

@ -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.<tenant>.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."""