Skip to content

fix(runtime): exchange raw Copilot OAuth token in runtime resolution - #58830

Open
SvichkarevAnatoly wants to merge 1 commit into
NousResearch:mainfrom
SvichkarevAnatoly:fix/copilot-token-exchange-runtime
Open

fix(runtime): exchange raw Copilot OAuth token in runtime resolution#58830
SvichkarevAnatoly wants to merge 1 commit into
NousResearch:mainfrom
SvichkarevAnatoly:fix/copilot-token-exchange-runtime

Conversation

@SvichkarevAnatoly

Copy link
Copy Markdown

What does this PR do?

Fixes GitHub Copilot models failing with HTTP 400 "The requested model is not available for integrator" / "...is not supported".

When Hermes sends a raw GitHub OAuth token (ghu_...) as the Bearer to api.githubcopilot.com, GitHub ignores the Copilot-Integration-Id: vscode-chat header and pins the request to integrator copilot-language-server, whose model allow-list is tiny. As a result Claude, Gemini and most GPT models fail; only a handful (gpt-5.5, gpt-5-mini, …) work.

The credential pool can end up holding the raw token: _seed_from_env writes the raw COPILOT_GITHUB_TOKEN under the same source key as the token-exchanging singleton seeder, and _upsert_entry (matching by source) overwrites the exchanged tid= entry. Runtime resolution then puts the raw token on the wire.

This PR exchanges a raw GitHub token for a short-lived Copilot API token (tid=…) at the two copilot choke points in runtime resolution, and adopts the account-specific base URL advertised by the exchange (Business/Enterprise tenants). Already-exchanged tokens and exchange failures pass through unchanged, so behaviour is never worse than before.

Empirical matrix on one Business seat (same token):

Bearer Copilot-Integration-Id Result
raw ghu_… vscode-chat 400 — integrator copilot-language-server, most models blocked
exchanged tid=… vscode-chat 200 — all models

Supersedes #24546, which attempts the same exchange but assigns the (token, base_url) tuple returned by get_copilot_api_token directly to api_key, sending a stringified tuple on the wire — every model then fails with invalid token: invalid whitespace. (Verified by applying #24546 on current main.) Credit to @lucvan for identifying the same root cause.

Related Issue

Fixes #45813

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/runtime_provider.py: add _looks_like_raw_github_token() and _exchange_copilot_api_key(); call the exchange in _resolve_runtime_from_pool_entry and _resolve_explicit_runtime for the copilot provider, adopting the exchange base URL.
  • tests/hermes_cli/test_copilot_runtime_token_exchange.py: 4 network-free regression tests — raw-token detection, exchange + base-URL adoption, already-exchanged pass-through, base-URL preserved on exchange failure.

How to Test

  1. Configure the Copilot provider via OAuth (Business/Enterprise seat) and select claude-opus-4.8.
  2. On main: request fails with HTTP 400 … not available for integrator "copilot-language-server".
  3. With this patch: the same request returns 200 and the model answers correctly. Verified across all 13 Copilot models advertised by the seat — every model that previously failed with the integrator error now works.
  4. pytest tests/hermes_cli/test_copilot_runtime_token_exchange.py -q → 4 passed.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(runtime):)
  • I searched for existing PRs (this supersedes fix(runtime): exchange Copilot raw OAuth token in pool resolution path #24546)
  • My PR contains only changes related to this fix
  • I've run the relevant tests and they pass (test_copilot_runtime_token_exchange.py, test_provider_attribution_headers.py)
  • I've added tests for my changes
  • I've tested on my platform: macOS 15

Documentation & Housekeeping

  • Docstrings added for new helpers — no user-facing docs/config keys changed (N/A for README / cli-config.yaml.example)
  • No architecture/workflow change (N/A for CONTRIBUTING.md / AGENTS.md)
  • Cross-platform safe (pure-Python string/token handling, no platform primitives)

A raw GitHub OAuth token (ghu_...) sent as the Bearer to
api.githubcopilot.com makes GitHub ignore `Copilot-Integration-Id:
vscode-chat` and pin the request to integrator "copilot-language-server",
whose model allow-list is tiny. Claude, Gemini and most GPT models then
fail with HTTP 400 model_not_available_for_integrator; only a handful of
GPT models (gpt-5.5, gpt-5-mini, ...) work.

The credential pool can hold the raw token: `_seed_from_env` writes the
raw COPILOT_GITHUB_TOKEN under the same source key as the
token-exchanging singleton seeder, and `_upsert_entry` (matching by
source) overwrites the exchanged `tid=` entry. Runtime resolution then
sends the raw token on the wire.

Fix: at the two copilot choke points in runtime resolution
(`_resolve_runtime_from_pool_entry` and `_resolve_explicit_runtime`),
detect a raw GitHub token and exchange it for a short-lived Copilot API
token, adopting the account-specific base URL advertised by the exchange
(Business/Enterprise tenants). Already-exchanged tokens and exchange
failures pass through unchanged, so behaviour is never worse than before.

Verified against all 13 Copilot models advertised by a Business seat:
every model that previously failed with the integrator error now returns
200 and self-identifies correctly.

Supersedes NousResearch#24546, which attempts the same exchange but assigns the
`(token, base_url)` tuple returned by `get_copilot_api_token` directly to
`api_key`, putting a stringified tuple on the wire so every model fails
with "invalid token: invalid whitespace".

Fixes NousResearch#45813

Signed-off-by: Anatoly Svichkarev <6915620+SvichkarevAnatoly@users.noreply.github.com>
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard provider/copilot GitHub Copilot (ACP + Chat) area/auth Authentication, OAuth, credential pools labels Jul 5, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: supersedes #24546 (same root cause but #24546 assigns the (token, base_url) tuple to api_key, sending a stringified tuple on the wire -> every model fails with invalid whitespace; this PR sends the exchanged tid= token correctly). Also related: #58743 (distinct 400 model_not_available_for_integrator runtime-recovery mechanism). Competing cluster — maintainer to pick; this is the working exchange.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment (LGTM)

Copilot OAuth fix: exchanges the raw Copilot OAuth token during runtime resolution. This completes the Copilot OAuth flow by properly exchanging the token rather than using it directly. 2 files changed — runtime resolution and Copilot client.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing this to the runtime resolver. The pooled path remains reachable on current main: agent/credential_pool.py:2435-2436 runs the Copilot singleton seeder before the generic environment seeder, and agent/credential_pool.py:2276-2285 then upserts the raw environment token. hermes_cli/runtime_provider.py:416,451-453 forwards that selected pool key without a runtime exchange, so the proposed defense at this boundary is directionally correct.

Problems

  • tests/hermes_cli/test_copilot_runtime_token_exchange.py:28-76 tests the new helper only; it never calls either changed resolver branch. That would not catch a future wiring regression at hermes_cli/runtime_provider.py:477 or :1518.

Suggested changes

  • Add resolver-level tests for a raw pooled Copilot entry and an explicit raw key, asserting both exchanged token and exchanged base URL are returned. Keep the exchange-failure base-URL assertion on those actual resolution paths.
  • Consider reconciling website/docs/integrations/providers.md:180-188, which currently describes direct raw-token authentication.

Automated hermes-sweeper review.

"ghs_abc123",
"github_pat_abc123",
" ghu_leading_ws ",
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a regression test through _resolve_runtime_from_pool_entry() with a raw ghu_... entry. These helper-only tests do not prove the changed pooled resolver returns the exchanged key and enterprise base URL.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/copilot GitHub Copilot (ACP + Chat) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: I always get HTTP 400: Bad Request with Github Copilot provider

4 participants