fix(copilot): fall back to credential_pool OAuth access_token for /model picker (#16708)#16901
Merged
Conversation
…del picker (#16708) Users whose only Copilot credential is the OAuth `access_token` saved by `hermes auth add copilot` (device-code flow) saw the `/model` picker drop back to a stale hardcoded list. Reason: `_resolve_copilot_catalog_api_key` only consulted env vars (`COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN`) and the `gh auth token` CLI fallback, never the credential pool that Hermes's own login flow writes into `auth.json`. With no token, the live catalog fetch silently 401s and the picker hides current models (claude-opus-4.7, claude-sonnet-4.6, gpt-5.5, grok-code-fast-1) — even though `/model <id>` works fine because runtime inference reads the pool through a different code path. Mirror the Codex catalog resolver pattern: env-var first (unchanged), then walk `read_credential_pool("copilot")` for the first entry with a supported `access_token` (`gho_*` / `github_pat_*` / `ghu_*`). Run it through `get_copilot_api_token()` so the catalog request uses the same exchanged token the runtime path uses. Classic PATs (`ghp_*`) are still rejected up-front via `validate_copilot_token` since the Copilot API doesn't accept them. Strictly additive: env still wins, and a missing/locked auth.json (or any exception during pool read) still returns "" so the caller falls through to the curated catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l catalog tokens Address Copilot review on #16868: 1. Tighten pool iteration. ``validate_copilot_token`` only rejects empty strings and classic PATs (``ghp_*``); a malformed/unsupported ``gho_*`` token at ``credential_pool.copilot[0]`` would pass the gate and short- circuit the loop, hiding a later valid entry. Switch to calling ``exchange_copilot_token`` directly: only entries that actually exchange into a live Copilot API token are returned. Bad/expired entries fall through to the next, and an exhausted pool returns ``""`` so the picker falls back to the curated list (existing behaviour). 2. Reword the docstring + test module docstring to describe the pool seed path accurately — ``hermes auth add copilot`` adds an api-key-typed credential whose ``access_token`` field stores the pasted token, and ``_seed_from_env`` mirrors ``COPILOT_GITHUB_TOKEN`` from ``~/.hermes/.env`` into the pool. The previous wording implied ``auth add copilot`` itself ran the device-code flow, which it does not (the device-code flow lives in ``hermes model``). Two new tests cover the iteration change: - ``test_skips_pool_entry_that_fails_to_exchange`` — pool[0] raises, pool[1] succeeds, picker uses pool[1]. - ``test_all_pool_entries_fail_exchange_returns_empty`` — every entry raises, return ``""``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a08ec39 to
0da6416
Compare
Closed
3 tasks
This was referenced May 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Salvage of #16868 (@briandevans) onto current main. Original branch was ~35 commits behind; cherry-picked cleanly with authorship preserved.
Summary
Copilot
/modelpicker now picks up the OAuthgho_*token thathermes auth add copilotwrites toauth.json's credential pool, instead of only looking at env vars /gh auth token. Device-code-only users were silently seeing a stale hardcoded Copilot model list (missingclaude-opus-4.7,gpt-5.5, etc.) because_resolve_copilot_catalog_api_key()never consulted the pool./model <id>worked because runtime inference reads the pool through a different path — only the catalog fetch was wedged.Changes
hermes_cli/models.py::_resolve_copilot_catalog_api_key— env lookup first (unchanged). On miss, walkread_credential_pool("copilot"), reject classicghp_*up-front viavalidate_copilot_token, run each candidate throughexchange_copilot_token— only entries that actually exchange return a value, so an expired pool[0] doesn't wedge a later valid entry.hermes_cli/models.py:1791.tests/hermes_cli/test_copilot_catalog_oauth_fallback.py— 7 focused tests + skip-and-try-next regression (8 total after the follow-up commit).Why exchange, not raw access_token
COPILOT_MODELS_URLisapi.githubcopilot.com/models, which requires the exchangedtid_*API token — not the rawgho_*OAuth token. The issue's proposed fix (returnaccess_tokendirectly) would still 401.Validation
test_copilot_catalog_oauth_fallback,test_copilot_in_model_list,test_copilot_auth,test_copilot_token_exchange.HERMES_HOME:gho_*→_resolve_copilot_catalog_api_key()returns exchangedtid_*;provider_model_ids("copilot")returns full list.Closes #16708. Supersedes #16868.