hermes - 💡(How to fix) Fix Copilot provider fails for Individual Pro subscribers — exchange endpoint returns 404

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The built-in copilot provider plugin fails for GitHub Copilot Individual Pro subscribers because it unconditionally tries to exchange the OAuth token via GET https://api.github.com/copilot_internal/v2/token, which is enterprise/business-only. Individual subscribers' API endpoint is https://api.individual.githubcopilot.com and accepts the raw OAuth token (gho_*) directly as Authorization: Bearer … — no exchange step needed.

Result: every Copilot call from a Hermes instance authenticated as an individual Pro subscriber 404s on the exchange step before the actual chat completion is ever attempted. Verified on Hermes Agent v0.14.0 (v2026.5.16).

Error Message

  • exchange_copilot_token() raises ValueError: Copilot token exchange failed: HTTP Error 404: Not Found

Root Cause

The built-in copilot provider plugin fails for GitHub Copilot Individual Pro subscribers because it unconditionally tries to exchange the OAuth token via GET https://api.github.com/copilot_internal/v2/token, which is enterprise/business-only. Individual subscribers' API endpoint is https://api.individual.githubcopilot.com and accepts the raw OAuth token (gho_*) directly as Authorization: Bearer … — no exchange step needed.

Fix Action

Fix / Workaround

Current workaround (not a fix, just unblocks individual users)

Happy to test a patch if useful, and willing to send a PR once direction is confirmed.

Code Example

from hermes_cli.copilot_auth import copilot_device_code_login
   token = copilot_device_code_login(timeout_seconds=300)
   # Save token to ~/.hermes/.env as COPILOT_GITHUB_TOKEN=<gho_...>

---

fallback_providers:
     - provider: copilot
       model: claude-sonnet-4.5

---

$ curl -s -H "Authorization: token $TOKEN" \
       -H "User-Agent: GitHubCopilotChat/0.20.0" \
       https://api.github.com/copilot_internal/user | jq '{copilot_plan, endpoints, chat_enabled, quota_snapshots: .quota_snapshots.premium_interactions}'
{
  "copilot_plan": "individual",
  "endpoints": {
    "api": "https://api.individual.githubcopilot.com",
    "proxy": "https://proxy.individual.githubcopilot.com",
    ...
  },
  "chat_enabled": true,
  "quota_snapshots": {
    "remaining": 211, "entitlement": 300, "unlimited": false
  }
}

---

$ curl -s -w "%{http_code}" -H "Authorization: token $TOKEN" \
       -H "User-Agent: GitHubCopilotChat/0.26.7" \
       -H "Editor-Version: vscode/1.104.1" \
       https://api.github.com/copilot_internal/v2/token
{"message":"Not Found","documentation_url":"...","status":"404"}404

---

$ curl -s -X POST \
    -H "Authorization: Bearer $TOKEN" \
    -H "Editor-Version: vscode/1.99.0" \
    -H "Copilot-Integration-Id: vscode-chat" \
    -H "Content-Type: application/json" \
    -d '{"model":"claude-sonnet-4.5","messages":[{"role":"user","content":"OK"}],"max_tokens":10}' \
    https://api.individual.githubcopilot.com/chat/completions
{"choices":[{"finish_reason":"stop","message":{"content":"OK","role":"assistant"}}],
 "model":"Claude Sonnet 4.5", "usage":{...}, ...}

---

def resolve_copilot_endpoint(raw_token: str) -> tuple[str, bool]:
    """Returns (api_base_url, needs_exchange) for the account behind this token."""
    req = urllib.request.Request(
        "https://api.github.com/copilot_internal/user",
        headers={"Authorization": f"token {raw_token}", ...},
    )
    data = json.loads(urllib.request.urlopen(req, timeout=10).read())
    api = data.get("endpoints", {}).get("api", "https://api.githubcopilot.com")
    # Individual: raw token works as Bearer; Business/Enterprise: needs exchange
    needs_exchange = data.get("copilot_plan") != "individual"
    return api, needs_exchange

---

fallback_providers:
  - provider: custom
    model: claude-sonnet-4.5      # use exact id from /models catalog
    base_url: https://api.individual.githubcopilot.com
    api_key: ${COPILOT_GITHUB_TOKEN}
    api_mode: openai
RAW_BUFFERClick to expand / collapse

Summary

The built-in copilot provider plugin fails for GitHub Copilot Individual Pro subscribers because it unconditionally tries to exchange the OAuth token via GET https://api.github.com/copilot_internal/v2/token, which is enterprise/business-only. Individual subscribers' API endpoint is https://api.individual.githubcopilot.com and accepts the raw OAuth token (gho_*) directly as Authorization: Bearer … — no exchange step needed.

Result: every Copilot call from a Hermes instance authenticated as an individual Pro subscriber 404s on the exchange step before the actual chat completion is ever attempted. Verified on Hermes Agent v0.14.0 (v2026.5.16).

Repro

  1. On a fresh Hermes install, authenticate with an Individual-plan Copilot account:
    from hermes_cli.copilot_auth import copilot_device_code_login
    token = copilot_device_code_login(timeout_seconds=300)
    # Save token to ~/.hermes/.env as COPILOT_GITHUB_TOKEN=<gho_...>
  2. Add Copilot to the model chain (anywhere — primary, fallback, or via hermes model):
    fallback_providers:
      - provider: copilot
        model: claude-sonnet-4.5
  3. Restart Hermes and trigger any Copilot call (e.g., via async_call_llm, or by letting the fallback cascade fire).

Actual behavior

  • exchange_copilot_token() raises ValueError: Copilot token exchange failed: HTTP Error 404: Not Found
  • Falls through to get_copilot_api_token() which returns the raw token unchanged on exchange failure
  • Subsequent call to api.githubcopilot.com/chat/completions fails with bad request: Authorization header is badly formatted (HTTP 400) because the raw OAuth token isn't accepted as Bearer on the business endpoint
  • Net: 100% failure rate for Individual subscribers

Diagnostic evidence

Account introspection (with valid gho_* OAuth token):

$ curl -s -H "Authorization: token $TOKEN" \
       -H "User-Agent: GitHubCopilotChat/0.20.0" \
       https://api.github.com/copilot_internal/user | jq '{copilot_plan, endpoints, chat_enabled, quota_snapshots: .quota_snapshots.premium_interactions}'
{
  "copilot_plan": "individual",
  "endpoints": {
    "api": "https://api.individual.githubcopilot.com",
    "proxy": "https://proxy.individual.githubcopilot.com",
    ...
  },
  "chat_enabled": true,
  "quota_snapshots": {
    "remaining": 211, "entitlement": 300, "unlimited": false
  }
}

The endpoints.api field is api.individual.githubcopilot.com, not the hardcoded api.githubcopilot.com.

Exchange endpoint behaviour:

$ curl -s -w "%{http_code}" -H "Authorization: token $TOKEN" \
       -H "User-Agent: GitHubCopilotChat/0.26.7" \
       -H "Editor-Version: vscode/1.104.1" \
       https://api.github.com/copilot_internal/v2/token
{"message":"Not Found","documentation_url":"...","status":"404"}404

Direct chat completion against the individual endpoint with raw OAuth token (works):

$ curl -s -X POST \
    -H "Authorization: Bearer $TOKEN" \
    -H "Editor-Version: vscode/1.99.0" \
    -H "Copilot-Integration-Id: vscode-chat" \
    -H "Content-Type: application/json" \
    -d '{"model":"claude-sonnet-4.5","messages":[{"role":"user","content":"OK"}],"max_tokens":10}' \
    https://api.individual.githubcopilot.com/chat/completions
{"choices":[{"finish_reason":"stop","message":{"content":"OK","role":"assistant"}}],
 "model":"Claude Sonnet 4.5", "usage":{...}, ...}

200 OK on the first try.

Proposed fix

In hermes_cli/copilot_auth.py:

  1. Detect plan type before attempting exchange. Before calling exchange_copilot_token, hit GET https://api.github.com/copilot_internal/user and read copilot_plan. If "individual", skip the exchange and use the raw token + the endpoints.api URL from that same response.

  2. Or: always read endpoints.api from the entitlement response and route requests there. The /copilot_internal/user endpoint works for both Individual and Business subscribers; the embedded endpoints.api value will be the right host for each. The exchange step only needs to fire when that endpoint actually requires a session token (Business/Enterprise).

  3. Update CopilotProfile.base_url in /opt/hermes/plugins/model-providers/copilot/__init__.py so it isn't hardcoded — make it a runtime lookup from copilot_internal/user.endpoints.api.

Suggested signature change:

def resolve_copilot_endpoint(raw_token: str) -> tuple[str, bool]:
    """Returns (api_base_url, needs_exchange) for the account behind this token."""
    req = urllib.request.Request(
        "https://api.github.com/copilot_internal/user",
        headers={"Authorization": f"token {raw_token}", ...},
    )
    data = json.loads(urllib.request.urlopen(req, timeout=10).read())
    api = data.get("endpoints", {}).get("api", "https://api.githubcopilot.com")
    # Individual: raw token works as Bearer; Business/Enterprise: needs exchange
    needs_exchange = data.get("copilot_plan") != "individual"
    return api, needs_exchange

Current workaround (not a fix, just unblocks individual users)

Add Copilot as a provider: custom entry in fallback_providers: pointing at the individual endpoint:

fallback_providers:
  - provider: custom
    model: claude-sonnet-4.5      # use exact id from /models catalog
    base_url: https://api.individual.githubcopilot.com
    api_key: ${COPILOT_GITHUB_TOKEN}
    api_mode: openai

This bypasses Hermes' copilot provider plumbing entirely and just sends a standard OpenAI-style chat completion request with the raw OAuth token as Bearer. Verified working with claude-sonnet-4.5 and gpt-5.4 model IDs.

Environment

  • Hermes Agent v0.14.0 (v2026.5.16)
  • Docker image: nousresearch/hermes-agent:v2026.5.16 (sha256:...)
  • Host: macOS (Apple Silicon)
  • Copilot plan: Individual Pro ($19/mo tier)
  • Auth: OAuth device code via copilot_device_code_login() (your client ID Ov23li8tweQw6odWQebz, scope read:user)

Notes

  • The OAuth flow itself succeeds — the gho_* token is correctly issued and stored.
  • Token validation in validate_copilot_token() correctly accepts the gho_* prefix.
  • The bug is purely in the post-auth exchange + endpoint-routing layer.
  • Affects any individual Pro / Pro+ subscriber, which is the majority of small-team / solo-developer Copilot installs.

Happy to test a patch if useful, and willing to send a PR once direction is confirmed.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING