Skip to content

fix(copilot): route by what the API says, and refuse un-renewable logins - #95

Merged
Bhasvanth-Spinabot merged 1 commit into
mainfrom
fix/copilot-enterprise-421
Aug 8, 2026
Merged

fix(copilot): route by what the API says, and refuse un-renewable logins#95
Bhasvanth-Spinabot merged 1 commit into
mainfrom
fix/copilot-enterprise-421

Conversation

@Bhasvanth-Dev9380

Copy link
Copy Markdown
Contributor

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:

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. 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 a free_limited_copilot seat where gpt-5-mini and claude-haiku-4.5 both fail this way even though /models advertises them for 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 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-sol is /responses-only. The advertised layer was only warmed when synthesizing an unknown id, leaving catalogued models on the id heuristic — which gets mai-code-1-flash wrong (/responses-only, matching no gpt-5/codex rule). 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 plain api_key has no refresh path: the only accepted bearer is the ~30-minute token exchanged from a GitHub OAuth grant, and Pi refreshes type: "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.

  • the loop detects it, warns once while 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 accepts a key for any provider id, and is how the credential got there — now refuses a Copilot bearer outright

Verification

  • 33 routing tests + 41 classifier tests, covering every wording captured from the live API
  • tsc --noEmit clean on both configs; npm run build clean
  • broader agents + integrations + core sweep: 0 failures

Known, deliberately not changed

The model picker still lists models a seat can't run. /models reports model_picker_enabled: false for all 53 entries on a free seat and misreports restricted_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.

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
Bhasvanth-Spinabot merged commit 04d2f85 into main Aug 8, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants