fix(copilot): route by what the API says, and refuse un-renewable logins - #95
Merged
Merged
Conversation
Follow-up to #93, driven by probing a real Copilot seat rather than issue reports. Three defects, all confirmed against the live API. 1. The endpoint-mismatch detector missed the wording GitHub actually uses. #93's patterns came from github/copilot-cli#4337 ("not accessible via the /chat/completions endpoint"); the common rejection is "model gpt-4.1 is not supported via Responses API." The self-heal never fired for it, so those turns burned three retries and surfaced "last reason=unknown". The detector now reads the surface out of the message and re-issues on the OTHER one — targeted rather than a blind flip, so a future third surface can't send the retry to the wrong place. A bare 421 (which names nothing) still flips. 2. A second, unrelated HTTP 400 was being treated as routing. "The requested model is not supported." is plan entitlement: the seat can't run the model on ANY surface. Verified on a free_limited_copilot seat, where gpt-5-mini and claude-haiku-4.5 both fail this way even though /models advertises them as available on every plan — so `billing.restricted_to` cannot be trusted for entitlement. It now fails fast with "your subscription doesn't include this model", is remembered so the next turn is instant, and never spends a request discovering the other endpoint refuses it too. 3. `supported_endpoints` IS shipped by /models (35 of 53 entries), so the advertised layer is live, not dormant as #93 assumed. It was only warmed when synthesizing an unknown id, leaving catalogued models on the id heuristic — which gets `mai-code-1-flash` (/responses-only, matching no gpt-5/codex rule) wrong. The account's catalog is now loaded on the turn path too, awaited only on a cold cache. Also: a Copilot bearer stored as a plain api_key cannot renew itself. The only accepted bearer is the ~30-minute token exchanged from a GitHub OAuth grant, and Pi refreshes `type: "oauth"` credentials only — so such a login looks healthy and then 401s within the half hour with nothing to refresh from. Found on a live profile. The loop now detects it, warns once while the token is valid, and fails with a "run `brigade login copilot`" message once it lapses instead of a bare 401. The gateway's `add-provider` RPC — which accepted a key for any provider id and is how the credential got there — now refuses a Copilot bearer outright.
Bhasvanth-Spinabot
approved these changes
Aug 8, 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.
Follow-up to #93, driven by probing a real Copilot seat rather than issue reports. Three defects, all confirmed against the live API, plus a credential-durability hole.
1. The endpoint detector missed the wording GitHub actually uses
#93's patterns came from github/copilot-cli#4337 (
"not accessible via the /chat/completions endpoint"). The common rejection is:The self-heal never fired for it, so those turns burned three retries and surfaced
last reason=unknown. Worse, the heal flipped blindly. It now reads the rejected surface out of the message and re-issues on the other one — a future third surface can't send the retry somewhere wrong. A bare 421 (which names nothing) still flips.2. A second, unrelated 400 was being treated as routing
The requested model is not supported.is plan entitlement — the seat can't run the model on any surface. Verified on afree_limited_copilotseat wheregpt-5-miniandclaude-haiku-4.5both fail this way even though/modelsadvertises them for every plan, sobilling.restricted_tocannot be trusted for entitlement.It now fails fast with "your subscription doesn't include this model", is remembered so the next turn is instant, and never spends a request discovering the other endpoint refuses it too.
3.
supported_endpointsis real, and wasn't being consulted#93 assumed GitHub ships no endpoint metadata. It does — 35 of 53 entries carry it, and it confirms
gpt-5.6-solis/responses-only. The advertised layer was only warmed when synthesizing an unknown id, leaving catalogued models on the id heuristic — which getsmai-code-1-flashwrong (/responses-only, matching nogpt-5/codexrule). The account's catalog now loads on the turn path too, awaited only on a cold cache.4. Copilot logins that cannot renew themselves
A
tid=…;exp=…bearer stored as a plainapi_keyhas no refresh path: the only accepted bearer is the ~30-minute token exchanged from a GitHub OAuth grant, and Pi refreshestype: "oauth"credentials only. Such a login looks healthy and then 401s within the half hour with nothing to refresh from. Found on a live profile.brigade login copilot" message once it lapses instead of a bare 401add-providerRPC — which accepts a key for any provider id, and is how the credential got there — now refuses a Copilot bearer outrightVerification
tsc --noEmitclean on both configs;npm run buildcleanagents+integrations+coresweep: 0 failuresKnown, deliberately not changed
The model picker still lists models a seat can't run.
/modelsreportsmodel_picker_enabled: falsefor all 53 entries on a free seat and misreportsrestricted_to, so filtering on it would hide working models and show broken ones. Failing fast with an accurate message (#2) is the honest behaviour until GitHub's metadata is trustworthy.