{"repo":"NousResearch/hermes-agent","public_id":"6455","display_label":"GitHub Copilot Enterprise host and auth support","summary":"These issues and pull requests concern adding and fixing GitHub Copilot support for GitHub Enterprise Server, including authentication, base URL derivation, client ID handling, and model catalog integration.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":55562,"merged_at":"2026-06-30T10:27:42+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-30T10:27:42+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":6455,"pr_number":55562,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-30T10:27:42+00:00"},{"issue_number":50252,"pr_number":55562,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-30T10:27:42+00:00"}],"members":[{"kind":"issue","number":6455,"title":"[Feature]: Add support for Github Copilot though Github Enterprise subscription","url":null,"state":"closed"},{"kind":"pr","number":7272,"title":"fix(copilot): use live model catalog for context windows and token exchange","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7731","number":7731},{"kind":"pr","value":"#12840","number":12840},{"kind":"pr","value":"#12876","number":12876}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":15139,"title":"fix(copilot): switch to VS Code client ID and derive enterprise base URL","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"fbd15e285","number":0},{"kind":"commit","value":"fb0721584","number":0},{"kind":"pr","value":"#55562","number":55562},{"kind":"pr","value":"#50590","number":50590},{"kind":"pr","value":"#50252","number":50252}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":55562,"title":"fix(copilot): VS Code client ID + enterprise base URL (salvage #15139, fold #50590)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_init.py","agent/auxiliary_client.py","agent/chat_completion_helpers.py","agent/credential_pool.py","agent/model_metadata.py","hermes_cli/auth.py","hermes_cli/copilot_auth.py","run_agent.py","scripts/release.py","tests/hermes_cli/test_auth_commands.py","tests/hermes_cli/test_copilot_token_exchange.py","tests/run_agent/test_provider_attribution_headers.py","tests/run_agent/test_run_agent.py"],"fix_hunks":[{"file":"agent/agent_init.py","hunks":["@@ -828,7 +828,7 @@ def _moa_reference_relay(event: str, **kwargs: Any) -> None:\n                 client_kwargs[\"default_headers\"] = build_nvidia_nim_headers(effective_base)\n             elif base_url_host_matches(effective_base, \"api.routermint.com\"):\n                 client_kwargs[\"default_headers\"] = _ra()._routermint_headers()\n-            elif base_url_host_matches(effective_base, \"api.githubcopilot.com\"):\n+            elif base_url_host_matches(effective_base, \"githubcopilot.com\"):\n                 from hermes_cli.models import copilot_default_headers\n \n                 client_kwargs[\"default_headers\"] = copilot_default_headers()"]},{"file":"agent/auxiliary_client.py","hunks":["@@ -1624,7 +1624,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n             extra = {}\n             if base_url_host_matches(base_url, \"api.kimi.com\"):\n                 extra[\"default_headers\"] = {\"User-Agent\": \"claude-code/0.1.0\"}\n-            elif base_url_host_matches(base_url, \"api.githubcopilot.com\"):\n+            elif base_url_host_matches(base_url, \"githubcopilot.com\"):\n                 from hermes_cli.models import copilot_default_headers\n \n                 extra[\"default_headers\"] = copilot_default_headers()","@@ -1664,7 +1664,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n         extra = {}\n         if base_url_host_matches(base_url, \"api.kimi.com\"):\n             extra[\"default_headers\"] = {\"User-Agent\": \"claude-code/0.1.0\"}\n-        elif base_url_host_matches(base_url, \"api.githubcopilot.com\"):\n+        elif base_url_host_matches(base_url, \"githubcopilot.com\"):\n             from hermes_cli.models import copilot_default_headers\n \n             extra[\"default_headers\"] = copilot_default_headers()","@@ -2954,7 +2954,7 @@ def _recoverable_pool_provider(\n         return \"nous\"\n     if base_url_host_matches(base, \"api.anthropic.com\"):\n         return \"anthropic\"\n-    if base_url_host_matches(base, \"api.githubcopilot.com\"):\n+    if base_url_host_matches(base, \"githubcopilot.com\"):\n         return \"copilot\"\n     if base_url_host_matches(base, \"api.kimi.com\"):\n         return \"kimi-coding\"","@@ -3823,7 +3823,7 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False):\n     sync_base_url = str(sync_client.base_url)\n     if base_url_host_matches(sync_base_url, \"openrouter.ai\"):\n         async_kwargs[\"default_headers\"] = build_or_headers()\n-    elif base_url_host_matches(sync_base_url, \"api.githubcopilot.com\"):\n+    elif base_url_host_matches(sync_base_url, \"githubcopilot.com\"):\n         from hermes_cli.copilot_auth import copilot_request_headers\n \n         async_kwargs[\"default_headers\"] = copilot_request_headers(","@@ -4128,7 +4128,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n                 extra[\"default_query\"] = _dq\n             if base_url_host_matches(custom_base, \"api.kimi.com\"):\n                 extra[\"default_headers\"] = {\"User-Agent\": \"claude-code/0.1.0\"}\n-            elif base_url_host_matches(custom_base, \"api.githubcopilot.com\"):\n+            elif base_url_host_matches(custom_base, \"githubcopilot.com\"):\n                 from hermes_cli.copilot_auth import copilot_request_headers\n                 extra[\"default_headers\"] = copilot_request_headers(\n                     is_agent_turn=True, is_vision=is_vision","@@ -4381,7 +4381,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n         headers = {}\n         if base_url_host_matches(base_url, \"api.kimi.com\"):\n             headers[\"User-Agent\"] = \"claude-code/0.1.0\"\n-        elif base_url_host_matches(base_url, \"api.githubcopilot.com\"):\n+        elif base_url_host_matches(base_url, \"githubcopilot.com\"):\n             from hermes_cli.copilot_auth import copilot_request_headers\n \n             headers.update(copilot_request_headers(","@@ -4854,9 +4854,14 @@ def auxiliary_max_tokens_param(value: int, *, model: Optional[str] = None) -> di\n     or_key = os.getenv(\"OPENROUTER_API_KEY\")\n     # Use max_completion_tokens for direct OpenAI-compatible providers that reject\n     # max_tokens on newer GPT-4o/o-series/GPT-5-style models.\n+    _custom_host = base_url_hostname(custom_base) or \"\"\n     if (not or_key\n             and _read_nous_auth() is None\n-            and base_url_hostname(custom_base) in {\"api.openai.com\", \"api.githubcopilot.com\"}):\n+            and (\n+                _custom_host == \"api.openai.com\"\n+                or _custom_host == \"api.githubcopilot.com\"\n+                or _custom_host.endswith(\".githubcopilot.com\")\n+            )):\n         return {\"max_completion_tokens\": [redacted:secret-assignment]\n     # ...and for any caller serving a newer OpenAI-family model by name.\n     if model_forces_max_completion_tokens(model):"]},{"file":"agent/chat_completion_helpers.py","hunks":["@@ -632,7 +632,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:\n         _ct = agent._get_transport()\n         is_github_responses = (\n             base_url_host_matches(agent.base_url, \"models.github.ai\")\n-            or base_url_host_matches(agent.base_url, \"api.githubcopilot.com\")\n+            or base_url_host_matches(agent.base_url, \"githubcopilot.com\")\n         )\n         is_codex_backend = (\n             agent.provider == \"openai-codex\"","@@ -702,7 +702,7 @@ def build_api_kwargs(agent, api_messages: list) -> dict:\n     _is_or = agent._is_openrouter_url()\n     _is_gh = (\n         base_url_host_matches(agent._base_url_lower, \"models.github.ai\")\n-        or base_url_host_matches(agent._base_url_lower, \"api.githubcopilot.com\")\n+        or base_url_host_matches(agent._base_url_lower, \"githubcopilot.com\")\n     )\n     _is_nous = \"nousresearch\" in agent._base_url_lower\n     _is_nvidia = \"integrate.api.nvidia.com\" in agent._base_url_lower"]},{"file":"agent/credential_pool.py","hunks":["@@ -1884,11 +1884,16 @@ def _env_val(key: str) -> str:\n             from hermes_cli.copilot_auth import resolve_copilot_token, get_copilot_api_token\n             token, source = resolve_copilot_token()\n             if token:\n-                api_token = [redacted:secret-assignment]\n+                api_token, enterprise_base_url = get_copilot_api_token(token)\n                 source_name = \"gh_cli\" if \"gh\" in source.lower() else f\"env:{source}\"\n                 if not _is_suppressed(provider, source_name):\n                     active_sources.add(source_name)\n                     pconfig = PROVIDER_REGISTRY.get(provider)\n+                    # Use enterprise base URL from token exchange if available,\n+                    # otherwise fall back to the provider's default.\n+                    effective_base_url = enterprise_base_url or (\n+                        pconfig.inference_base_url if pconfig else \"\"\n+                    )\n                     changed |= _upsert_entry(\n                         entries,\n                         provider,","@@ -1897,7 +1902,7 @@ def _env_val(key: str) -> str:\n                             \"source\": source_name,\n                             \"auth_type\": AUTH_TYPE_API_KEY,\n                             \"access_token\": [redacted:secret-assignment]\n-                            \"base_url\": pconfig.inference_base_url if pconfig else \"\",\n+                            \"base_url\": effective_base_url,\n                             \"label\": source,\n                         },\n                     )"]},{"file":"agent/model_metadata.py","hunks":["@@ -429,6 +429,10 @@ def _is_custom_endpoint(base_url: str) -> bool:\n     \"inference-api.nousresearch.com\": \"nous\",\n     \"api.deepseek.com\": \"deepseek\",\n     \"api.githubcopilot.com\": \"copilot\",\n+    # Enterprise Copilot endpoints look like api.enterprise.githubcopilot.com,\n+    # api.business.githubcopilot.com, etc.  Match the suffix so context-window\n+    # resolution works for enterprise accounts too.\n+    \".githubcopilot.com\": \"copilot\",\n     \"models.github.ai\": \"copilot\",\n     # GitHub Models free tier (Azure-hosted prototyping endpoint) — same\n     # canonical provider as the Copilot API.  Hard per-request token cap"]},{"file":"hermes_cli/auth.py","hunks":["@@ -568,7 +568,8 @@ def _resolve_api_key_provider_secret(\n             from hermes_cli.copilot_auth import resolve_copilot_token, get_copilot_api_token\n             token, source = resolve_copilot_token()\n             if token:\n-                return get_copilot_api_token(token), source\n+                api_token, _base_url = get_copilot_api_token(token)\n+                return api_token, source\n         except ValueError as exc:\n             logger.warning(\"Copilot token validation failed: %s\", exc)\n         except Exception:","@@ -6356,6 +6357,26 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:\n         base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)\n     elif provider_id == \"zai\":\n         base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url)\n+    elif provider_id == \"copilot\":\n+        # Resolve the Copilot API base URL from the token-exchange response\n+        # (endpoints.api, with a proxy-ep fallback), which is authoritative\n+        # for Enterprise / proxied accounts. Falls back to the registry\n+        # default and is guarded non-empty below so chat inference never\n+        # resolves an empty base URL (#50252).\n+        base_url = env_url.rstrip(\"/\") if env_url else pconfig.inference_base_url\n+        try:\n+            from hermes_cli.copilot_auth import (\n+                resolve_copilot_token,\n+                get_copilot_api_token,\n+            )\n+            raw_token, _ = resolve_copilot_token()\n+            if raw_token:\n+                _, resolved = get_copilot_api_token(raw_token)\n+                resolved = (resolved or \"\").strip()\n+                if resolved:\n+                    base_url = resolved\n+        except Exception as exc:\n+            logger.debug(\"Copilot base URL resolution fell back to default: %s\", exc)\n     elif env_url:\n         base_url = env_url.rstrip(\"/\")\n     else:","@@ -6364,6 +6385,12 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:\n     if provider_id == \"lmstudio\":\n         base_url = _normalize_lmstudio_runtime_base_url(base_url)\n \n+    # Last-resort guard: an API-key provider must never hand back an empty\n+    # base URL (a set-but-empty COPILOT_API_BASE_URL or similar env override\n+    # otherwise wedges chat inference — #50252).\n+    if not (isinstance(base_url, str) and base_url.strip()):\n+        base_url = pconfig.inference_base_url\n+\n     return {\n         \"provider\": provider_id,\n         \"api_key\": [redacted:secret-assignment]"]},{"file":"hermes_cli/copilot_auth.py","hunks":["@@ -31,8 +31,14 @@\n \n logger = logging.getLogger(__name__)\n \n-# OAuth device code flow constants (same client ID as opencode/Copilot CLI)\n-COPILOT_OAUTH_CLIENT_ID = \"Ov23li8tweQw6odWQebz\"\n+# OAuth device code flow constants — VS Code's GitHub App client ID.\n+# The previous opencode OAuth App ID (Ov23li8tweQw6odWQebz) produces gho_*\n+# tokens that cannot be exchanged for Copilot API JWTs (404 on\n+# /copilot_internal/v2/token). VS Code's App ID produces ghu_* tokens\n+# that support exchange, which is required to access internal-only models\n+# (e.g. claude-opus-4.6-1m) and enterprise endpoints.\n+# Tested on Individual and Enterprise accounts.\n+COPILOT_OAUTH_CLIENT_ID = \"Iv1.b507a08c87ecfe98\"\n # Token type prefixes\n _CLASSIC_PAT_PREFIX = \"ghp_\"\n _SUPPORTED_PREFIXES = (\"gho_\", \"github_pat_\", \"ghu_\")","@@ -282,8 +288,8 @@ def copilot_device_code_login(\n # ─── Copilot Token Exchange ────────────────────────────────────────────────\n \n # Module-level cache for exchanged Copilot API tokens.\n-# Maps raw_token_fingerprint -> (api_token, expires_at_epoch).\n-_jwt_cache: dict[str, tuple[str, float]] = {}\n+# Maps raw_token_fingerprint -> (api_token, expires_at_epoch, base_url).\n+_jwt_cache: dict[str, tuple[str, float, Optional[str]]] = {}\n _JWT_REFRESH_MARGIN_SECONDS = 120  # refresh 2 min before expiry\n \n # Token exchange endpoint and headers (matching VS Code / Copilot CLI)","@@ -298,14 +304,18 @@ def _token_fingerprint(raw_token: str) -> str:\n     return hashlib.sha256(raw_token.encode()).hexdigest()[:16]\n \n \n-def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[str, float]:\n+def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[str, float, Optional[str]]:\n     \"\"\"Exchange a raw GitHub token for a short-lived Copilot API token.\n \n     Calls ``GET https://api.github.com/copilot_internal/v2/token`` with\n-    the raw GitHub token and returns ``(api_token, expires_at)``.\n+    the raw GitHub token and returns ``(api_token, expires_at, base_url)``.\n \n     The returned token is a semicolon-separated string (not a standard JWT)\n     used as ``Authorization: Bearer <token>`` for Copilot API requests.\n+    ``base_url`` is the account-specific API host: the authoritative\n+    ``endpoints.api`` advertised by the exchange (enterprise/proxied\n+    accounts), falling back to a host derived from the token's ``proxy-ep``\n+    field. Individual accounts have neither, so ``base_url`` is None.\n \n     Results are cached in-process and reused until close to expiry.\n     Raises ``ValueError`` on failure.","@@ -317,9 +327,9 @@ def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[st\n     # Check cache first\n     cached = _jwt_cache.get(fp)\n     if cached:\n-        api_token, expires_at = cached\n+        api_token, expires_at, base_url = cached\n         if time.time() < expires_at - _JWT_REFRESH_MARGIN_SECONDS:\n-            return api_token, expires_at\n+            return api_token, expires_at, base_url\n \n     req = urllib.request.Request(\n         _TOKEN_EXCHANGE_URL,","@@ -346,30 +356,82 @@ def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[st\n     # Convert expires_at to float if needed\n     expires_at = float(expires_at) if expires_at else time.time() + 1800\n \n-    _jwt_cache[fp] = (api_token, expires_at)\n+    # Resolve the account-specific API base URL. GitHub advertises the\n+    # authoritative endpoint under ``endpoints.api`` in the exchange response\n+    # (it differs for Copilot Enterprise / proxied accounts). When the\n+    # response omits it, fall back to deriving the host from the ``proxy-ep``\n+    # field embedded in the exchanged token. Individual accounts have neither,\n+    # so ``base_url`` stays None and callers use the registry default.\n+    base_url: Optional[str] = None\n+    endpoints = data.get(\"endpoints\")\n+    if isinstance(endpoints, dict):\n+        api_endpoint = str(endpoints.get(\"api\") or \"\").strip().rstrip(\"/\")\n+        if api_endpoint:\n+            base_url = api_endpoint\n+    if not base_url:\n+        base_url = _derive_base_url_from_proxy_ep(api_token)\n+\n+    _jwt_cache[fp] = (api_token, expires_at, base_url)\n     logger.debug(\n-        \"Copilot token exchanged, expires_at=%s\",\n+        \"Copilot token exchanged, expires_at=%s, base_url=%s\",\n         expires_at,\n+        base_url,\n     )\n-    return api_token, expires_at\n+    return api_token, expires_at, base_url\n \n \n-def get_copilot_api_token(raw_token: str) -> str:\n+def _derive_base_url_from_proxy_ep(token: str) -> Optional[str]:\n+    \"\"\"Derive the Copilot API base URL from a proxy-ep field in the token.\n+\n+    The exchanged Copilot token is a semicolon-separated string like\n+    ``tid=xxx;exp=xxx;proxy-ep=proxy.enterprise.githubcopilot.com;...``.\n+    This extracts ``proxy-ep`` and converts it to an API base URL by\n+    replacing the leading ``proxy.`` with ``api.``.\n+\n+    Returns ``https://{api_hostname}`` or None if proxy-ep is absent.\n+    \"\"\"\n+    import re\n+    m = re.search(r'(?:^|;)\\s*proxy-ep=([^;\\s]+)', token)\n+    if not m:\n+        return None\n+\n+    proxy_ep = m.group(1)\n+    # Strip scheme if present\n+    for prefix in (\"https://\", \"http://\"):\n+        if proxy_ep.startswith(prefix):\n+            proxy_ep = proxy_ep[len(prefix):]\n+            break\n+    proxy_ep = proxy_ep.rstrip(\"/\")\n+\n+    # Replace leading \"proxy.\" with \"api.\"\n+    if proxy_ep.startswith(\"proxy.\"):\n+        api_host = \"api.\" + proxy_ep[len(\"proxy.\"):]\n+    else:\n+        api_host = proxy_ep\n+\n+    return f\"https://{api_host}\"\n+\n+\n+def get_copilot_api_token(raw_token: str) -> tuple[str, Optional[str]]:\n     \"\"\"Exchange a raw GitHub token for a Copilot API token, with fallback.\n \n-    Convenience wrapper: returns the exchanged token on success, or the\n-    raw token unchanged if the exchange fails (e.g. network error, unsupported\n+    Convenience wrapper: returns ``(api_token, base_url)`` on success, or\n+    ``(raw_token, None)`` if the exchange fails (e.g. network error, unsupported\n     account type). This preserves existing behaviour for accounts that don't\n     need exchange while enabling access to internal-only models for those that do.\n+\n+    ``base_url`` is the account-specific API endpoint advertised by the\n+    exchange (``endpoints.api``, with a ``proxy-ep`` fallback), or None for\n+    individual accounts.\n     \"\"\"\n     if not raw_token:\n-        return raw_token\n+        return raw_token, None\n     try:\n-        api_token, _ = exchange_copilot_token(raw_token)\n-        return api_token\n+        api_token, _, base_url = exchange_copilot_token(raw_token)\n+        return api_token, base_url\n     except Exception as exc:\n         logger.debug(\"Copilot token exchange failed, using raw token: %s\", exc)\n-        return raw_token\n+        return raw_token, None\n \n \n # ─── Copilot API Headers ───────────────────────────────────────────────────"]},{"file":"run_agent.py","hunks":["@@ -1133,7 +1133,9 @@ def _is_github_copilot_url(self, base_url: str = None) -> bool:\n             hostname = getattr(self, \"_base_url_hostname\", \"\") or base_url_hostname(\n                 getattr(self, \"_base_url_lower\", \"\")\n             )\n-        return hostname == \"api.githubcopilot.com\"\n+        if not hostname:\n+            return False\n+        return hostname == \"api.githubcopilot.com\" or hostname.endswith(\".githubcopilot.com\")\n \n     def _resolved_api_call_timeout(self) -> float:\n         \"\"\"Resolve the effective per-call request timeout in seconds.","@@ -3837,7 +3839,7 @@ def _create_request_openai_client(self, *, reason: str, api_kwargs: Optional[dic\n         # unaffected (they don't go through here).\n         request_kwargs[\"max_retries\"] = 0\n         if (\n-            base_url_host_matches(str(request_kwargs.get(\"base_url\", \"\")), \"api.githubcopilot.com\")\n+            base_url_host_matches(str(request_kwargs.get(\"base_url\", \"\")), \"githubcopilot.com\")\n             and self._api_kwargs_have_image_parts(api_kwargs or {})\n         ):\n             request_kwargs[\"default_headers\"] = self._copilot_headers_for_request(is_vision=True)","@@ -4099,7 +4101,7 @@ def _apply_client_headers_for_base_url(self, base_url: str) -> None:\n             self._client_kwargs[\"default_headers\"] = build_nvidia_nim_headers(base_url)\n         elif base_url_host_matches(base_url, \"api.routermint.com\"):\n             self._client_kwargs[\"default_headers\"] = _routermint_headers()\n-        elif base_url_host_matches(base_url, \"api.githubcopilot.com\"):\n+        elif base_url_host_matches(base_url, \"githubcopilot.com\"):\n             from hermes_cli.models import copilot_default_headers\n \n             self._client_kwargs[\"default_headers\"] = copilot_default_headers()","@@ -4995,7 +4997,7 @@ def _supports_reasoning_extra_body(self) -> bool:\n             return True\n         if (\n             base_url_host_matches(self._base_url_lower, \"models.github.ai\")\n-            or base_url_host_matches(self._base_url_lower, \"api.githubcopilot.com\")\n+            or base_url_host_matches(self._base_url_lower, \"githubcopilot.com\")\n         ):\n             try:\n                 from hermes_cli.models import github_model_reasoning_efforts"]},{"file":"scripts/release.py","hunks":["@@ -898,6 +898,7 @@\n     \"A-FdL-Prog@users.noreply.github.com\": \"A-FdL-Prog\",\n     \"l0hde@users.noreply.github.com\": \"l0hde\",\n     \"difujia@users.noreply.github.com\": \"difujia\",\n+    \"[redacted:email]\": \"trevorgordon981\",  # PR #50590 co-author (prefer endpoints.api for Copilot base URL + empty-base-URL guard; #50252)\n     \"[redacted:email]\": \"vominh1919\",\n     \"[redacted:email]\": \"YueLich\",\n     \"51783311+andyylin@users.noreply.github.com\": \"andyylin\","]},{"file":"tests/hermes_cli/test_auth_commands.py","hunks":["@@ -1859,7 +1859,7 @@ def test_auth_remove_copilot_suppresses_all_variants(tmp_path, monkeypatch):\n         return_value=(\"ghp_fake\", \"gh\"),\n     ), patch(\n         \"hermes_cli.copilot_auth.get_copilot_api_token\",\n-        return_value=\"ghu_fake_api\",\n+        return_value=(\"ghu_fake_api\", None),\n     ):\n         auth_remove_command(SimpleNamespace(provider=\"copilot\", target=\"1\"))\n "]},{"file":"tests/hermes_cli/test_copilot_token_exchange.py","hunks":["@@ -37,10 +37,11 @@ def test_exchanges_token_successfully(self, mock_urlopen):\n         from hermes_cli.copilot_auth import exchange_copilot_token\n \n         mock_urlopen.return_value = self._mock_urlopen(token=\"[redacted:secret-assignment]\")\n-        api_token, expires_at = exchange_copilot_token(\"gho_test123\")\n+        api_token, expires_at, base_url = exchange_copilot_token(\"gho_test123\")\n \n         assert api_token == \"tid=abc;exp=999\"\n         assert isinstance(expires_at, float)\n+        assert base_url is None  # no proxy-ep in this token\n \n         # Verify request was made with correct headers\n         call_args = mock_urlopen.call_args","@@ -66,12 +67,12 @@ def test_refreshes_expired_cache(self, mock_urlopen):\n \n         # Seed cache with expired entry\n         fp = _token_fingerprint(\"gho_test123\")\n-        _jwt_cache[fp] = (\"old_token\", time.time() - 10)\n+        _jwt_cache[fp] = (\"old_token\", time.time() - 10, None)\n \n         mock_urlopen.return_value = self._mock_urlopen(\n             token=\"[redacted:secret-assignment]\", expires_at=time.time() + 1800\n         )\n-        api_token, _ = exchange_copilot_token(\"gho_test123\")\n+        api_token, _, _ = exchange_copilot_token(\"gho_test123\")\n \n         assert api_token == \"new_token\"\n         assert mock_urlopen.call_count == 1","@@ -105,19 +106,25 @@ class TestGetCopilotApiToken:\n     def test_returns_exchanged_token(self, mock_exchange):\n         from hermes_cli.copilot_auth import get_copilot_api_token\n \n-        mock_exchange.return_value = (\"exchanged_jwt\", time.time() + 1800)\n-        assert get_copilot_api_token(\"gho_raw\") == \"exchanged_jwt\"\n+        mock_exchange.return_value = (\"exchanged_jwt\", time.time() + 1800, None)\n+        api_token, base_url = get_copilot_api_token(\"gho_raw\")\n+        assert api_token == \"exchanged_jwt\"\n+        assert base_url is None\n \n     @patch(\"hermes_cli.copilot_auth.exchange_copilot_token\", side_effect=ValueError(\"fail\"))\n     def test_falls_back_to_raw_token(self, mock_exchange):\n         from hermes_cli.copilot_auth import get_copilot_api_token\n \n-        assert get_copilot_api_token(\"gho_raw\") == \"gho_raw\"\n+        api_token, base_url = get_copilot_api_token(\"gho_raw\")\n+        assert api_token == \"gho_raw\"\n+        assert base_url is None\n \n     def test_empty_token_passthrough(self):\n         from hermes_cli.copilot_auth import get_copilot_api_token\n \n-        assert get_copilot_api_token(\"\") == \"\"\n+        api_token, base_url = get_copilot_api_token(\"\")\n+        assert api_token == \"\"\n+        assert base_url is None\n \n \n class TestTokenFingerprint:","@@ -147,7 +154,7 @@ class TestCallerIntegration:\n     \"\"\"Test that callers correctly use token exchange.\"\"\"\n \n     @patch(\"hermes_cli.copilot_auth.resolve_copilot_token\", return_value=(\"gho_raw\", \"GH_TOKEN\"))\n-    @patch(\"hermes_cli.copilot_auth.get_copilot_api_token\", return_value=\"exchanged_jwt\")\n+    @patch(\"hermes_cli.copilot_auth.get_copilot_api_token\", return_value=(\"exchanged_jwt\", None))\n     def test_auth_resolve_uses_exchange(self, mock_exchange, mock_resolve):\n         from hermes_cli.auth import _resolve_api_key_provider_secret\n ","@@ -157,3 +164,65 @@ def test_auth_resolve_uses_exchange(self, mock_exchange, mock_resolve):\n         assert token == \"exchanged_jwt\"\n         assert source == \"GH_TOKEN\"\n         mock_exchange.assert_called_once_with(\"gho_raw\")\n+\n+\n+class TestDeriveBaseUrlFromProxyEp:\n+    \"\"\"Tests for _derive_base_url_from_proxy_ep().\"\"\"\n+\n+    def test_extracts_enterprise_url(self):\n+        from hermes_cli.copilot_auth import _derive_base_url_from_proxy_ep\n+\n+        token = \"[redacted:secret-assignment]\"\n+        assert _derive_base_url_from_proxy_ep(token) == \"https://api.enterprise.githubcopilot.com\"\n+\n+    def test_returns_none_without_proxy_ep(self):\n+        from hermes_cli.copilot_auth import _derive_base_url_from_proxy_ep\n+\n+        token = \"[redacted:secret-assignment]\"\n+        assert _derive_base_url_from_proxy_ep(token) is None\n+\n+    def test_handles_https_prefix(self):\n+        from hermes_cli.copilot_auth import _derive_base_url_from_proxy_ep\n+\n+        token = \"[redacted:secret-assignment]\"\n+        assert _derive_base_url_from_proxy_ep(token) == \"https://api.enterprise.githubcopilot.com\"\n+\n+    def test_no_proxy_prefix(self):\n+        from hermes_cli.copilot_auth import _derive_base_url_from_proxy_ep\n+\n+        token = \"[redacted:secret-assignment]\"\n+        assert _derive_base_url_from_proxy_ep(token) == \"https://custom.copilot.example.com\"\n+\n+    @patch(\"urllib.request.urlopen\")\n+    def test_exchange_returns_enterprise_base_url(self, mock_urlopen, _clear_jwt_cache):\n+        \"\"\"exchange_copilot_token returns base_url from proxy-ep.\"\"\"\n+        from hermes_cli.copilot_auth import exchange_copilot_token\n+\n+        token_with_ep = \"[redacted:secret-assignment]\"\n+        expires_at = time.time() + 1800\n+        resp_data = json.dumps({\"token\": [redacted:secret-assignment] \"expires_at\": expires_at}).encode()\n+        mock_resp = MagicMock()\n+        mock_resp.read.return_value = resp_data\n+        mock_resp.__enter__ = MagicMock(return_value=mock_resp)\n+        mock_resp.__exit__ = MagicMock(return_value=False)\n+        mock_urlopen.return_value = mock_resp\n+\n+        api_token, _, base_url = exchange_copilot_token(\"gho_test\")\n+        assert base_url == \"https://api.enterprise.githubcopilot.com\"\n+\n+    @patch(\"urllib.request.urlopen\")\n+    def test_exchange_returns_none_base_url_for_individual(self, mock_urlopen, _clear_jwt_cache):\n+        \"\"\"exchange_copilot_token returns None base_url for individual accounts.\"\"\"\n+        from hermes_cli.copilot_auth import exchange_copilot_token\n+\n+        token_no_ep = \"[redacted:secret-assignment]\"\n+        expires_at = time.time() + 1800\n+        resp_data = json.dumps({\"token\": [redacted:secret-assignment] \"expires_at\": expires_at}).encode()\n+        mock_resp = MagicMock()\n+        mock_resp.read.return_value = resp_data\n+        mock_resp.__enter__ = MagicMock(return_value=mock_resp)\n+        mock_resp.__exit__ = MagicMock(return_value=False)\n+        mock_urlopen.return_value = mock_resp\n+\n+        api_token, _, base_url = exchange_copilot_token(\"gho_test\")\n+        assert base_url is None"]},{"file":"tests/run_agent/test_provider_attribution_headers.py","hunks":["@@ -320,3 +320,31 @@ def test_openrouter_headers_no_cache_when_disabled(mock_openai):\n     assert headers[\"HTTP-Referer\"] == \"https://hermes-agent.nousresearch.com\"\n     assert \"X-OpenRouter-Cache\" not in headers\n     assert \"X-OpenRouter-Cache-TTL\" not in headers\n+\n+\n+@patch(\"run_agent.OpenAI\")\n+def test_copilot_enterprise_base_url_applies_copilot_default_headers(mock_openai):\n+    \"\"\"Enterprise Copilot endpoints (api.<tenant>.githubcopilot.com) must apply\n+    the same default_headers — including Copilot-Integration-Id: vscode-chat —\n+    as the default api.githubcopilot.com endpoint. Without this, the upstream\n+    sees the request as integrator 'zed' or 'copilot-language-server' and\n+    rejects it with a 400 error for many models (regression seen May 2026).\"\"\"\n+    mock_openai.return_value = MagicMock()\n+    agent = AIAgent(\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://api.enterprise.githubcopilot.com\",\n+        model=\"claude-opus-4.6-1m\",\n+        provider=\"copilot\",\n+        quiet_mode=True,\n+        skip_context_files=True,\n+        skip_memory=True,\n+    )\n+\n+    agent._apply_client_headers_for_base_url(\"https://api.enterprise.githubcopilot.com\")\n+\n+    headers = agent._client_kwargs.get(\"default_headers\", {})\n+    # Lookup is case-insensitive — normalize for the assertion.\n+    lc = {k.lower(): v for k, v in headers.items()}\n+    assert lc.get(\"copilot-integration-id\") == \"vscode-chat\", (\n+        f\"enterprise Copilot endpoint must carry Copilot-Integration-Id=vscode-chat; got {headers}\"\n+    )"]},{"file":"tests/run_agent/test_run_agent.py","hunks":["@@ -5699,6 +5699,13 @@ def test_returns_max_tokens_for_llama_on_local(self, agent):\n         result = agent._max_tokens_param(4096)\n         assert result == {\"max_tokens\": 4096}\n \n+    def test_returns_max_completion_tokens_for_enterprise_copilot(self, agent):\n+        \"\"\"Enterprise Copilot endpoints (api.<tenant>.githubcopilot.com) must\n+        share the same max_tokens behavior as the default endpoint.\"\"\"\n+        agent.base_url = \"https://api.enterprise.githubcopilot.com\"\n+        result = agent._max_tokens_param(4096)\n+        assert result == {\"max_completion_tokens\": 4096}\n+\n \n class TestGpt5ApiModeRouting:\n     \"\"\"Verify provider-specific GPT-5 API-mode routing.\"\"\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}