fix(runtime): exchange raw Copilot OAuth token in runtime resolution - #58830
fix(runtime): exchange raw Copilot OAuth token in runtime resolution#58830SvichkarevAnatoly wants to merge 1 commit into
Conversation
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>
Related: supersedes #24546 (same root cause but #24546 assigns the |
tonydwb
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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-76tests the new helper only; it never calls either changed resolver branch. That would not catch a future wiring regression athermes_cli/runtime_provider.py:477or: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 ", | ||
| ): |
There was a problem hiding this comment.
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.
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 toapi.githubcopilot.com, GitHub ignores theCopilot-Integration-Id: vscode-chatheader and pins the request to integratorcopilot-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_envwrites the rawCOPILOT_GITHUB_TOKENunder the same source key as the token-exchanging singleton seeder, and_upsert_entry(matching by source) overwrites the exchangedtid=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 twocopilotchoke 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):
Copilot-Integration-Idghu_…vscode-chatcopilot-language-server, most models blockedtid=…vscode-chatRelated Issue
Fixes #45813
Type of Change
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_entryand_resolve_explicit_runtimefor thecopilotprovider, 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
claude-opus-4.8.main: request fails withHTTP 400 … not available for integrator "copilot-language-server".pytest tests/hermes_cli/test_copilot_runtime_token_exchange.py -q→ 4 passed.Checklist
Code
fix(runtime):)test_copilot_runtime_token_exchange.py,test_provider_attribution_headers.py)Documentation & Housekeeping
cli-config.yaml.example)CONTRIBUTING.md/AGENTS.md)