fix(copilot): prefer endpoints.api for base URL, guard empty chat base URL

Folds @trevorgordon981's #50590 into difujia's #15139:
- exchange_copilot_token now prefers the authoritative endpoints.api from
  the token-exchange response, falling back to the proxy-ep-derived host
- resolve_api_key_provider_credentials gains a copilot branch that resolves
  the account-specific base URL and a non-empty last-resort guard, so chat
  inference never wedges on an empty base URL (#50252)

Co-authored-by: Trevor Gordon <trevorbgordon@gmail.com>
This commit is contained in:
teknium1 2026-06-30 03:04:51 -07:00 committed by Teknium
parent fb07215844
commit 15e44527ab
2 changed files with 47 additions and 8 deletions

View file

@ -6357,6 +6357,26 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
elif provider_id == "zai":
base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url)
elif provider_id == "copilot":
# Resolve the Copilot API base URL from the token-exchange response
# (endpoints.api, with a proxy-ep fallback), which is authoritative
# for Enterprise / proxied accounts. Falls back to the registry
# default and is guarded non-empty below so chat inference never
# resolves an empty base URL (#50252).
base_url = env_url.rstrip("/") if env_url else pconfig.inference_base_url
try:
from hermes_cli.copilot_auth import (
resolve_copilot_token,
get_copilot_api_token,
)
raw_token, _ = resolve_copilot_token()
if raw_token:
_, resolved = get_copilot_api_token(raw_token)
resolved = (resolved or "").strip()
if resolved:
base_url = resolved
except Exception as exc:
logger.debug("Copilot base URL resolution fell back to default: %s", exc)
elif env_url:
base_url = env_url.rstrip("/")
else:
@ -6365,6 +6385,12 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
if provider_id == "lmstudio":
base_url = _normalize_lmstudio_runtime_base_url(base_url)
# Last-resort guard: an API-key provider must never hand back an empty
# base URL (a set-but-empty COPILOT_API_BASE_URL or similar env override
# otherwise wedges chat inference — #50252).
if not (isinstance(base_url, str) and base_url.strip()):
base_url = pconfig.inference_base_url
return {
"provider": provider_id,
"api_key": api_key,

View file

@ -312,8 +312,10 @@ def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[st
The returned token is a semicolon-separated string (not a standard JWT)
used as ``Authorization: Bearer <token>`` for Copilot API requests.
If the token contains a ``proxy-ep`` field (enterprise accounts), the
derived API base URL is returned; otherwise ``base_url`` is None.
``base_url`` is the account-specific API host: the authoritative
``endpoints.api`` advertised by the exchange (enterprise/proxied
accounts), falling back to a host derived from the token's ``proxy-ep``
field. Individual accounts have neither, so ``base_url`` is None.
Results are cached in-process and reused until close to expiry.
Raises ``ValueError`` on failure.
@ -354,10 +356,20 @@ def exchange_copilot_token(raw_token: str, *, timeout: float = 10.0) -> tuple[st
# Convert expires_at to float if needed
expires_at = float(expires_at) if expires_at else time.time() + 1800
# Derive enterprise base URL from proxy-ep in the token.
# The token is semicolon-separated: "tid=xxx;exp=xxx;proxy-ep=proxy.enterprise.githubcopilot.com;..."
# Replace leading "proxy." with "api." to get the API base URL.
base_url = _derive_base_url_from_proxy_ep(api_token)
# Resolve the account-specific API base URL. GitHub advertises the
# authoritative endpoint under ``endpoints.api`` in the exchange response
# (it differs for Copilot Enterprise / proxied accounts). When the
# response omits it, fall back to deriving the host from the ``proxy-ep``
# field embedded in the exchanged token. Individual accounts have neither,
# so ``base_url`` stays None and callers use the registry default.
base_url: Optional[str] = None
endpoints = data.get("endpoints")
if isinstance(endpoints, dict):
api_endpoint = str(endpoints.get("api") or "").strip().rstrip("/")
if api_endpoint:
base_url = api_endpoint
if not base_url:
base_url = _derive_base_url_from_proxy_ep(api_token)
_jwt_cache[fp] = (api_token, expires_at, base_url)
logger.debug(
@ -408,8 +420,9 @@ def get_copilot_api_token(raw_token: str) -> tuple[str, Optional[str]]:
account type). This preserves existing behaviour for accounts that don't
need exchange while enabling access to internal-only models for those that do.
``base_url`` is the enterprise API endpoint derived from the token's
``proxy-ep`` field, or None for individual accounts.
``base_url`` is the account-specific API endpoint advertised by the
exchange (``endpoints.api``, with a ``proxy-ep`` fallback), or None for
individual accounts.
"""
if not raw_token:
return raw_token, None