{"repo":"NousResearch/hermes-agent","public_id":"10223","display_label":"GitHub Copilot provider base URL bugs and fixes","summary":"Multiple bug reports and fix attempts addressing the GitHub Copilot provider returning an empty base URL, particularly during credential pool resolution. Also includes the original Copilot provider integration feature and related auth/routing corrections.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":55562},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-30T10:27:42+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":6455,"pr_number":55562,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":50252,"pr_number":55562,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":10223,"title":"[Bug]: GitHub Copilot provider returns empty base URL when resolved via credential pool","url":null,"state":"closed"},{"kind":"issue","number":50252,"title":"[Bug]: GitHub Copilot provider returns \"empty base URL\" on every inference (v0.17.0, token valid, 12 models detected)","url":null,"state":"closed"},{"kind":"pr","number":10266,"title":"fix(auth): add default base URL fallback for copilot provider in credential pool","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":50590,"title":"fix(copilot): resolve empty base URL from the token-exchange endpoint","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"15e44527a","number":0},{"kind":"pr","value":"#55562","number":55562},{"kind":"pr","value":"#15139","number":15139},{"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","implication":"superseded"},"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\": value}\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\": api_token,\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\": api_key,"]},{"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     \"[redacted:email]\": \"A-FdL-Prog\",\n     \"[redacted:email]\": \"l0hde\",\n     \"[redacted:email]\": \"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     \"[redacted:email]\": \"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\": token_with_ep, \"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\": token_no_ep, \"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}}