Skip to content

fix(copilot): fall back to credential_pool OAuth access_token for /model picker (#16708)#16901

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-2f662469
Apr 28, 2026
Merged

fix(copilot): fall back to credential_pool OAuth access_token for /model picker (#16708)#16901
teknium1 merged 2 commits into
mainfrom
hermes/hermes-2f662469

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Salvage of #16868 (@briandevans) onto current main. Original branch was ~35 commits behind; cherry-picked cleanly with authorship preserved.

Summary

Copilot /model picker now picks up the OAuth gho_* token that hermes auth add copilot writes to auth.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 (missing claude-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, walk read_credential_pool("copilot"), reject classic ghp_* up-front via validate_copilot_token, run each candidate through exchange_copilot_token — only entries that actually exchange return a value, so an expired pool[0] doesn't wedge a later valid entry.
  • Mirrors the Codex catalog resolver at 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_URL is api.githubcopilot.com/models, which requires the exchanged tid_* API token — not the raw gho_* OAuth token. The issue's proposed fix (return access_token directly) would still 401.

Validation

  • Targeted: 48/48 pass across test_copilot_catalog_oauth_fallback, test_copilot_in_model_list, test_copilot_auth, test_copilot_token_exchange.
  • E2E with real imports + isolated HERMES_HOME:
    • env empty + pool gho_*_resolve_copilot_catalog_api_key() returns exchanged tid_*; provider_model_ids("copilot") returns full list.
    • env set + pool populated → pool is never read (exchange called exactly once, for the env token).

Closes #16708. Supersedes #16868.

briandevans and others added 2 commits April 28, 2026 01:18
…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>
@teknium1 teknium1 force-pushed the hermes/hermes-2f662469 branch from a08ec39 to 0da6416 Compare April 28, 2026 08:18
@teknium1 teknium1 merged commit 66a05e4 into main Apr 28, 2026
4 checks passed
@teknium1 teknium1 deleted the hermes/hermes-2f662469 branch April 28, 2026 08:18
@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) labels Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/copilot GitHub Copilot (ACP + Chat) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copilot /model picker should fall back to OAuth access_token from auth.json

3 participants