skip to content
$worker

provider-github-copilot

v0.1.1

GitHub Copilot subscription provider worker; sign in with GitHub once and the models the plan grants appear in the picker. Implements provider::github-copilot::stream, refresh_models, and a device-flow login surface behind llm-router.

iiiverified
3 installs0 in 7d0 today
install
$iii trigger compose::add worker=provider-github-copilot@0.1.1
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

README.md

provider-github-copilot

GitHub Copilot subscription provider worker behind llm-router: sign in with GitHub once and the models the subscription grants appear in the model picker. The wire is OpenAI Chat Completions against the Copilot API endpoint; what makes this provider different from the api_key providers is the credential lifecycle, which it owns end to end.

Implements the provider protocol from tech-specs/2026-06-agentic/llm-router.md: provider::github-copilot::stream (SSE chunks → AssistantMessageEvent frames into a router-owned channel), provider::github-copilot::refresh_models (live GET /models, admitted rows mapped to catalog records → router::models::reconcile), plus a device-flow sign-in surface (login::start / login::poll).

Signing in

iii trigger provider::github-copilot::login::start
# → { user_code, verification_uri, device_code, interval }
# enter the code at the verification URL, then:
iii trigger provider::github-copilot::login::poll device_code=<device_code>
# → { status: "ok" } — the catalog fills within seconds

Machines already signed in through an editor need no login at all: the worker imports (read-only) ~/.config/github-copilot/apps.json or pi's auth store. GITHUB_COPILOT_NO_LOCAL_IMPORT=1 opts out of that import; GITHUB_COPILOT_OAUTH_TOKEN supplies the GitHub OAuth token directly; and GITHUB_COPILOT_TOKEN supplies a ready Copilot bearer (tests, short-lived dev sessions).

Behavior

  • Credential lifecycle: the long-lived GitHub OAuth token (login, env, or editor import) is exchanged at copilot_internal/v2/token for a short-lived Copilot bearer (~25 minutes) that also names the API endpoint. The bearer is cached in-memory, refreshed proactively inside a 2-minute margin, and invalidated when a stream dies with an auth error — the next call re-exchanges instead of failing again. The GitHub token persists in iii-state (scope provider-github-copilot, key oauth_token).
  • Catalog ids are prefixed: Copilot serves several vendors' models under bare ids (gpt-5.2, claude-sonnet-4.6) that would collide with the sibling single-vendor providers. Catalog ids are copilot/; the prefix is stripped on every upstream call.
  • Admission: a listing row must be type: chat, support tool_calls, carry a non-disabled policy.state, and declare /chat/completions among its endpoints. The editor's internal feature models (preview rows with no picker category — search, compaction, exec agents) are dropped too. Windows, ceilings, and capability flags come from the listing's capabilities tree; there is no pricing — a subscription meters in premium requests, so records carry no per-token cost and usage.cost_usd stays unset.
  • Verified, not guessed: which models a plan may actually call is not in the listing. Two models can be identical across every field the API exposes — same vendor, same policy: enabled, same picker category — and one answers while the other returns model_not_supported (a free or educational plan carries the base families but no premium requests). Discovery therefore probes each admitted model with a one-token request, four at a time, and reconciles only what answered. Verdicts persist for 24 hours, so a model is probed once and answered from cache after that — a refresh runs at boot, on every router::ready rebind, and after each sign-in, and a probe that succeeds on a premium model spends a premium request, not just a token. When the TTL lapses everything is re-checked, so enabling more models upstream (or changing plan) needs no code change.
  • Self-healing: if a model refuses between refreshes, the stream returns an actionable permanent error, the row is removed from the catalog, and the verdict is recorded — so the picker never offers the same dead model twice and no further probe is spent on it.
  • model_picker_enabled is deliberately ignored. It reflects an editor-side picker preference and reads false for every row on accounts that have never toggled models in an editor — gating on it admits nothing.
  • Registration: self-declares via router::provider::register with backoff until acked, and re-declares on the router::ready trigger type. The declaration carries no static models slice; a refresh fires right after registration and after every successful login.
  • Client identity: the token exchange, discovery, and every chat call carry the integration headers the Copilot gateway requires (Copilot-Integration-Id, Editor-Version, plugin version, user agent), plus X-Initiator: agent so agent-initiated turns are billed per Copilot's convention and never misattributed as user keystrokes.
  • Reasoning: the wire has no reasoning-effort parameter — thinking models decide for themselves and stream reasoning back as reasoning deltas (surfaced as thinking blocks; the older reasoning_content field from compatible gateways is honored too). A requested thinking_level is reported as ignored via a report-and-continue warning.
  • Structured output: strict json_schema mode on models whose listing declares structured_outputs; a schema requested for any other model degrades to json_object with a warning.
  • Token counting: none. The Copilot API exposes no tokenizer endpoint and its models span several vendors' vocabularies — router::count_tokens reports no_token_counter for this provider and the harness falls back to its own estimate.
  • Liveness: ping at least every 30s of upstream silence; a failed channel write (caller gone / router::abort) drops the SSE receiver and aborts the in-flight HTTP request.
  • Errors: statuses carry subscription semantics on the shared taxonomy: 401 (bearer or login died) → auth_expired and the cached bearer is dropped; 403 (no Copilot access / model not authorized) → permanent; 429 → rate_limited; 5xx and network failures → transient; context-length errors → context_overflow. The numeric error.code envelope wins over the transport status. No transport retries here — the router owns retry policy.
  • api_url precedence: operator override in the llm-router entry → the endpoint the exchange reply names (GitHub Enterprise tenants land here automatically) → the public default.

Tests

cargo test                                            # unit (pure modules + TCP stubs)
III_ENGINE_BIN=$(which iii) cargo test --test integration -- --test-threads=1

The integration suite spawns a real engine, the real router (path dep), this provider, and a local stub upstream; the ready-bearer env path short-circuits the token exchange so no external API is called. A suite-wide lock serializes the tests (they manage credential env vars), so plain cargo test is safe too.

Running

The binary takes the standard worker CLI flags: --url (engine WebSocket, default ws://127.0.0.1:49134, falls back to the III_URL environment variable), --manifest (print the registry manifest and exit), and --config (accepted but ignored with a warning — provider config comes from the llm-router configuration entry).