Your own provider models, inside the GitHub Copilot CLI /model picker — in the same session as the GitHub ones.
npm install -g copilot-byok-switcherCreated by Michael Gasperini.
Start Copilot the way you always do, open /model, and find your own models listed next to GitHub's:
Recommended models
❯ Auto
New models
Claude Sonnet 4.6 (Anthropic) ← your Anthropic key
GPT-5.5 (OpenAI) ← your OpenAI key
DeepSeek V4 Pro (DeepSeek) ← your DeepSeek key
GLM-5.2 (Z.AI GLM) ← your Z.AI key
Qwen3 Coder Plus (Alibaba Qwen) ← your DashScope key
Unavailable models
claude-sonnet-5
...
Pick one and the session talks straight to that provider. GitHub authentication stays intact, every other Copilot feature keeps working, and those turns cost zero AI credits — you pay your provider directly.
The list is not hard-coded: each provider is asked what it serves at startup, so a model released this morning is in the picker this morning.
● Model changed to: byok-anthropic-claude-sonnet-4-6 for this session
Copilot CLI does support BYOK out of the box, through COPILOT_PROVIDER_BASE_URL. That path has three limits this project removes:
| Built-in BYOK | Copilot BYOK Switcher | |
|---|---|---|
| Providers per session | one | as many as you configure |
/model picker |
shows nothing to choose from | your models listed with GitHub's |
| GitHub models | unavailable while BYOK is on | still there, same session |
| Switching model | restart with new env vars | /model inside the session |
Bringing your own key for OpenAI, Anthropic or Gemini is not possible natively on personal accounts at all — GitHub reserves that for enterprise owners, who get custom models in the picker. On a personal plan those vendors appear in the picker but are served by GitHub's own subscription: they consume premium requests and follow your plan's restrictions. On the free plan most of them are visible yet unusable.
Copilot CLI reads an environment variable, COPILOT_API_URL, that repoints its API endpoint anywhere — including plain HTTP on localhost. The switcher starts a small local router there:
copilot ──COPILOT_API_URL──> router ──┬──> api.<tier>.githubcopilot.com (everything else, untouched)
└──> your providers (OpenAI-compatible)
The router does exactly three things:
GET /models— takes GitHub's list and appends your models, so the picker shows them.POSTwith one of your models — recognises thebyok-*id, maps it back to the provider's real model name, forwards it, and streams the response through untouched.- Everything else — passes to GitHub byte for byte.
There is no TLS interception: Copilot talks in the clear to 127.0.0.1, and the router opens its own ordinary HTTPS connection to GitHub. The router listens on an ephemeral port bound to loopback and shuts down when Copilot exits.
Important
COPILOT_API_URL is an internal variable — it is not listed in copilot help environment. It works today (verified on CLI 1.0.79) but GitHub may rename or change it. If your models ever vanish from the picker, that is the first thing to check, and copilot-byok --native always gets you back to a stock session.
# 1. Store a key for each provider you own (prompted, never echoed)
copilot-byok keys set openai
copilot-byok keys set anthropic
# 2. See what is active
copilot-byok --list-providers
# 3. Start Copilot
copilot-byokThen /model inside the session. Arguments after -- go to Copilot unchanged:
copilot-byok -- -p "fix the failing test" --allow-all-toolsEvery provider below is reached through its OpenAI-compatible endpoint, verified reachable at release time. A provider activates only when its key is present, so the picker stays as short as your setup.
| Provider | Env var | Provider | Env var |
|---|---|---|---|
| OpenAI | OPENAI_API_KEY |
DeepSeek | DEEPSEEK_API_KEY |
| Anthropic | ANTHROPIC_API_KEY |
Alibaba Qwen | DASHSCOPE_API_KEY |
| Google Gemini | GEMINI_API_KEY |
Z.AI GLM | ZAI_API_KEY |
| xAI Grok | XAI_API_KEY |
Zhipu AI (CN) | ZHIPU_CN_API_KEY |
| Mistral | MISTRAL_API_KEY |
Moonshot Kimi | MOONSHOT_API_KEY |
| Groq | GROQ_API_KEY |
MiniMax | MINIMAX_API_KEY |
| Cerebras | CEREBRAS_API_KEY |
StepFun | STEPFUN_API_KEY |
| Together AI | TOGETHER_API_KEY |
ByteDance Doubao | ARK_API_KEY |
| DeepInfra | DEEPINFRA_API_KEY |
Tencent Hunyuan | HUNYUAN_API_KEY |
| Fireworks AI | FIREWORKS_API_KEY |
Baidu Qianfan | QIANFAN_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
SiliconFlow | SILICONFLOW_API_KEY |
| Chutes | CHUTES_API_KEY |
ModelScope | MODELSCOPE_API_KEY |
| OpenCode Zen | OPENCODE_ZEN_API_KEY |
Alibaba Token Plan | ALIBABA_TOKEN_PLAN_API_KEY |
| OpenCode Go | OPENCODE_GO_API_KEY |
Ollama / LM Studio | no key needed |
Each provider also accepts a COPILOT_BYOK_<PROVIDER>_API_KEY variant, useful when you already use the plain name for something else.
Two sources, in this order:
- Environment variables — the default and the recommendation. Nothing is written to disk, and a variable exported for one shell overrides everything else.
- The local key store — for when a dozen
setxcalls are not appealing. Fill it withcopilot-byok keys setor the/byokcommand.
The store lives at ~/.config/copilot-byok/keys.json (%APPDATA%\copilot-byok\keys.json on Windows), written with 0600 and an owner-only ACL on Windows. The trade-off is worth stating: keys stored there are plain text on disk, protected by file permissions alone. If that does not fit your threat model, stay with environment variables — the store is opt-in and never created unless you use it.
Either way the keys stay out of the Copilot process: the router uses them in its own, and strips them from the environment it hands to the agent.
Some entries ship without a preset model list, either because their model names are account-specific (Volcengine endpoint ids) or because the catalog is too large to curate (OpenRouter). Add the ones you want in your own config — see below.
Providers are asked directly. At startup each configured provider is queried
for the models it serves right now, so the picker shows today's line-up rather
than whatever was curated at release time — a model released this morning is
there this morning. All 30 providers were verified to expose an OpenAI-style
GET /models; the ones that also publish capabilities (Chutes, OpenRouter and
others) supply the real context window, output limit and tool-calling support
along with it.
While this happens the startup shows what it is waiting on, since the pause comes before Copilot has drawn anything:
⠹ copilot-byok [██░] 2/3 providers alibaba-token-plan
It appears only on a terminal — piped output and CI logs stay clean —
and COPILOT_BYOK_PROGRESS=off turns it off, COPILOT_BYOK_ASCII=1 swaps the
spinner for ASCII.
Only the providers you have a key for are queried, in parallel, with a hard timeout. Measured on three configured providers: about a second. Results are cached for ten minutes, failures for one.
Anything the provider does not report is filled in from the shipped catalog, which is generated from models.dev and refreshed with:
npm run catalog:updateIf a request fails — a rate limit, a blip, an expired key — the list that key
returned last time is used instead, kept in ~/.cache/copilot-byok/models.json
(%LOCALAPPDATA% on Windows). That matters: the shipped list is generic, while a
provider's catalog belongs to the plan behind your key, so falling back to it can
offer models your plan does not include and hide ones it does. Only when nothing
was ever remembered does the shipped list step in, so a hiccup never empties the
picker. To skip discovery entirely and always use the shipped lists:
copilot-byok --no-discoveryModels that cannot call tools are excluded, whether that is known from the provider's own metadata or from the catalog: Copilot's harness trusts what a model entry declares, and an agent that cannot call tools breaks halfway through a session instead of failing cleanly. Embedding, image, audio and moderation models are filtered out for the same reason. Each provider contributes at most twelve models, so a catalog like OpenRouter's 400+ cannot swamp the picker.
Copilot CLI extensions can register their own slash commands, so key management works without leaving the session:
copilot-byok extension install
copilot-byok -- --experimental # extensions are gated behind this flagThen /byok opens a provider list and asks for the key:
Choose a provider to configure:
❯ Anthropic — not configured (4 models)
Google Gemini — not configured (3 models)
DeepSeek — not configured (4 models)
OpenAI — set via environment
Chutes — set via environment
Paste the API key for Anthropic: ****
/byok list shows the current state and /byok remove deletes a stored key.
One caveat: Copilot caches its model list for the lifetime of a session, so
models unlocked by a key added this way appear the next time you start
copilot-byok — neither reopening the picker nor /restart refreshes them.
By default the two commands stay separate: copilot is GitHub's CLI, copilot-byok
is this one. If you would rather type copilot and get your models:
copilot-byok shim install
copilot-byok shim status # says which mechanism is actually in effectIt does two things, because on many machines one is not enough:
- writes a small
copilotinto~/.local/share/copilot-byok/bin(%APPDATA%\copilot-byok\binon Windows, plus a.cmd), and - adds a
copilotfunction to your shell profiles.
The function is what makes it work when another copilot sits earlier on PATH. On
Windows that is the normal case: the system PATH is searched before the user
one, so a CLI installed machine-wide beats anything you can add for your own user
— a shim alone would never be reached. A shell function takes precedence over PATH
entirely, in PowerShell and in POSIX shells alike.
Both mechanisms hand the real CLI's path to the launcher through COPILOT_BIN,
which is what stops it resolving the shim and calling itself. copilot-vanilla
runs the original CLI, and copilot-byok shim uninstall removes everything it
wrote — the profile block is delimited by markers, so the rest of your profile is
never touched.
Open a new terminal afterwards. PowerShell 5.1 and PowerShell 7 read different profile files; both are handled.
Copilot's own --effort flag and its effort selector do not reach BYOK providers.
Verified against a provider that records what it receives: the payloads produced
with low and with high are byte-identical, in both wire formats. This is a
known CLI bug rather than a limitation of BYOK itself — see
copilot-cli#4012 (still open,
same symptom on GLM-5.2),
#3119 and
#3135, which records that the
level was being sent in 1.0.41.
Until it is fixed, set the level per model and the router applies it:
{ "model": "deepseek-reasoner", "reasoningEffort": "high" }reasoningEffort also works at provider level as a default. Values are the ones
Copilot uses: none, minimal, low, medium, high, xhigh, max.
Levels differ per model and providers do not fall back. GLM-5.2 accepts max
but answers 400 to xhigh; an unsupported level would otherwise break every
request to that model. The router steps down one level at a time until one is
accepted, records it, and remembers it for the session:
notice: chutes: zai-org/GLM-5.2-TEE rejected reasoning effort "xhigh", retrying with "high"
Nothing here needs rewriting when the CLI starts forwarding the level again: the
router already prefers an incoming reasoning_effort over the configured one, so
your pick in the picker will simply take effect. The only step will be to turn the
selector on:
{ "model": "deepseek-reasoner", "reasoningEffortPicker": true }It ships off, because a selector that accepts a choice and changes nothing is
worse than no selector. reasoningEffortLevels narrows what it offers, for models
that do not take every level.
To use different models, an internal gateway, or a local runtime, write your own config:
- Linux/macOS:
~/.config/copilot-byok/providers.json - Windows:
%APPDATA%\copilot-byok\providers.json - or point
--config/COPILOT_BYOK_CONFIGat any path
{
"$schema": "https://raw.githubusercontent.com/TheStreamCode/copilot-byok-switcher/main/schemas/providers.schema.json",
"providers": [
{
"id": "ollama",
"name": "Ollama",
"baseUrl": "http://127.0.0.1:11434/v1",
"authRequired": false,
"models": [
{ "model": "qwen3-coder:30b", "label": "Qwen3 Coder 30B (local)", "contextWindow": 262144, "maxOutputTokens": 32768 }
]
}
]
}A custom file replaces the built-in catalog. See examples/providers.example.json for a fuller sample, and use --list-models to discover what a provider offers.
API keys are never written to the config — only the names of the environment variables that hold them. Inline apiKey fields are rejected.
copilot-byok # Copilot with your models in the picker
copilot-byok --list-providers # which providers are active, and what unlocks the rest
copilot-byok --native # stock Copilot, no router
copilot-byok --dry-run # show what would start, then exit
copilot-byok -- <copilot args> # pass anything through to Copilot
copilot-byok keys list # where each provider's key comes from
copilot-byok keys set <provider> # store a key (prompted, never echoed)
copilot-byok keys remove <id> # delete a stored key
copilot-byok keys path # print the key store location
copilot-byok extension install # add the /byok command to Copilot
copilot-byok extension status
copilot-byok extension uninstall
copilot-byok shim install # make plain `copilot` use the router
copilot-byok shim status # which mechanism is actually in effect
copilot-byok shim uninstallEverything has a working default; these are for when you need to change one.
| Variable | Effect |
|---|---|
COPILOT_BIN |
Path to the real Copilot CLI, when it is not on PATH under that name |
COPILOT_BYOK_CONFIG |
Provider configuration file (same as --config) |
COPILOT_BYOK_UPSTREAM |
Force the Copilot API tier (same as --upstream) |
COPILOT_BYOK_KEYSTORE |
Where stored keys live |
COPILOT_BYOK_MODEL_CACHE |
Where the last model list each key returned is kept |
COPILOT_BYOK_LOG |
Router log location |
COPILOT_BYOK_PROGRESS=off |
Turn off the startup progress line |
COPILOT_BYOK_ASCII=1 |
ASCII spinner, for consoles that cannot render Braille |
COPILOT_BYOK_DEBUG=1 |
Stream router events live instead of only to the log |
COPILOT_BYOK_PROVIDER_TIMEOUT_MS |
Idle timeout on a provider request (default 10 minutes) |
npm run catalog:update # refresh models and limits from models.dev
npm run catalog:audit # check every curated model still exists on its providerThe audit exits non-zero when a curated entry is not served by its provider. Curated lists are only a fallback, but a wrong entry is worse than a missing one: it is offered in the picker and fails only when selected.
The original behaviour is still available with --legacy: it sets COPILOT_PROVIDER_* for one provider and runs Copilot against it, without the router. GitHub models are unavailable in that mode, which is exactly what you want for --offline work against a local model:
copilot-byok --legacy --provider ollama --model qwen3-coder --offlineThe short version: no TLS is intercepted, no certificate authority is installed, the router listens on loopback only and exits with the session, provider keys never reach Copilot or GitHub, and your GitHub token passes through the router unlogged because it has to.
SECURITY.md covers the full model, including what the undocumented COPILOT_API_URL dependency means for you.
- Node.js 22.13 or newer
- GitHub Copilot CLI 1.0.79 or newer, available as
copilotonPATH(setCOPILOT_BINto override) - A GitHub Copilot account — any plan, including free
Diagnostics go to ~/.config/copilot-byok/router.log
(%APPDATA%\copilot-byok outer.log on Windows) instead of the screen, which
Copilot's interface owns while it runs. If anything failed, a single line after
the session tells you how many errors there were and where to read them.
| Symptom | Cause |
|---|---|
No BYOK models in /model |
No provider active — run copilot-byok --list-providers |
Unknown BYOK model |
Catalog changed since the session started; restart copilot-byok |
| Provider returns 401 | Wrong or expired key for that provider, not for GitHub |
| Everything is a GitHub model again | Check whether COPILOT_API_URL still works on your CLI version |
More in docs/troubleshooting.md.
Issues and pull requests are welcome — see CONTRIBUTING.md. Run npm run check (lint + tests) before opening one.
MIT — see LICENSE.