Skip to content

Repository files navigation

Copilot CLI Custom Proxy

CI License: MIT Platform: Windows Rust Tauri v2

A local HTTP proxy that lives in the system tray (Windows) for GitHub Copilot CLI, Codex CLI, and Claude Code. It intercepts requests, swaps the LLM model on the fly, and forwards them to a configured OpenAI-compatible (or Anthropic Messages) endpoint — without restarting your terminal session.

In BYOK mode, these CLIs fix the model at startup. This proxy lets you switch the model from the tray menu (or map five Claude Code slots) while you work.

How it works

Copilot CLI ──▶ http://127.0.0.1:8080  (this proxy)
                     │  • replaces the "model" field with the model picked in the tray
                     │  • injects Authorization: Bearer <key held in memory>
                     │  • forwards the remaining headers (except Host)
                     ▼
              endpoint base  (OpenAI-compatible endpoint)
                     │  • the response stream is piped straight back
                     ▼
                Copilot CLI
  • Everything is configured in the settings window — the endpoint URL and the local listen address are set there and persist to config.json (next to the executable). No config.toml editing required.
  • You enter the API key in the settings window — it is kept only in memory (wrapped in secrecy::SecretString), never written to disk or to logs.

Project layout

proxy-core/   # core: Axum + Reqwest, model swap, streaming (testable without a GUI)
src-tauri/    # Tauri v2 app: tray, settings window, background server startup
config.example.toml

Configuration

Everything is configured in the settings window — there is nothing to edit by hand. On first run (no config yet) the app starts with defaults, writes a config.json next to the executable straight away, and opens the settings window automatically. Your choices persist to that same config.json (gitignored, as it holds your private endpoint URL). If the file is ever found corrupt at startup it is copied to config.json.bak before being reset to defaults, and the settings window shows a one-time notice so the reset isn't silent.

In the Endpoint section you set:

  • Endpoint URL — the full upstream URL, including a host and the API suffix, e.g. https://openrouter.ai/api/v1/chat/completions, https://openrouter.ai/api/v1/responses, or https://your-provider.example.com/v1/messages. Do not stop at /v1 — the URL suffix (matched against the URL's path, so it must be the trailing path segment, not part of the host) is what tells the proxy which wire API is active.
  • Chat completions ⟷ Responses ⟷ Messages switch — one API is active at a time. Flipping the switch rewrites the URL suffix; the active API decides which CLI agent you can launch (Copilot for chat, Codex for responses, Claude Code for messages).
  • Listen address — the local host:port the proxy binds (the host is restricted to a strict character set). Changing it restarts only the background proxy task (no app/terminal restart): the new address is bound first, so if the port is already in use the error is shown in the window and the running proxy is left intact. Re-confirming the same address does nothing.

The model list is fetched automatically from {endpoint base}/models (the endpoint URL minus the API suffix) once you enter your API key, and via the tray's "Refresh models".

Which agents you can launch

Different CLI agents speak different OpenAI-compatible APIs. The active endpoint serves exactly one API (derived from its URL suffix), so the app enables only the matching agent:

Endpoint suffix API Agent
/chat/completions chat GitHub Copilot CLI
/responses responses Codex CLI
/messages messages (Anthropic) Claude Code

The other agents are shown disabled (settings window) or hidden (tray), so you never point a CLI at an endpoint that can't answer it. To use another agent, flip the switch (and ensure your upstream serves that API).

Optional config.toml seed

config.toml is no longer required. If present on first run (and no config.json exists yet), it is migrated into config.json once and then ignored — useful for upgrading an older install or pre-baking a deployment. Copy config.example.toml to config.toml for the seed format. The app looks for config next to the .exe, then in the working directory.

Build and run (Windows)

Requirements: Rust and the Tauri v2 system prerequisites (WebView2 ships with Windows 10/11).

# development mode (with a console and logs)
cargo install tauri-cli --version "^2.0"
cargo tauri dev

# production build (.exe / installer)
cargo tauri build

On launch the app minimizes to the tray. From the tray menu you can:

  • pick the active model (applied instantly, and remembered per-endpoint across restarts),
  • "Refresh models" — re-fetch the model list from the endpoint,
  • "Run Copilot" / "Run Codex" / "Run Claude Code" — open a new terminal with the proxy environment already set and start the chosen agent (see the Codex and Claude Code notes below),
  • open "Open Settings…" for the full window (API key, model list, launcher),
  • choose "Quit" to exit.

The tray icon has two states: an accent-filled glyph when the proxy is ready (a key is set and a model is selected) and a muted outline when it is idle. Models live in a "Models ▸" submenu (not the first level) so "Open Settings…" and "Quit" stay reachable even with hundreds of models. On a Messages endpoint that submenu nests one per-slot submenu (Opus … Subagent) instead of a flat model list — see Configuring Claude Code. You choose which chat models appear in those submenus in the settings window (see below) — the full catalog is always available there.

Settings window

The settings window is a small, single-purpose webview (vanilla HTML/CSS/JS — no bundler, served from src-tauri/dist/ under a restrictive 'self' CSP). It is a frameless window (decorations: false) with its own title bar, and ships a dark/light theme toggle (defaults to dark; the choice is remembered in localStorage). Fonts (IBM Plex Sans/Mono) are bundled locally, so the UI needs no network access. It has six sections:

  • Endpoint — the full upstream URL with a Chat completions ⟷ Responses ⟷ Messages switch (one active at a time; the switch rewrites the URL suffix), plus the local listen address. Both are validated and persisted to config.json; changing the listen address restarts the proxy task. An "expose to network" toggle lets you bind beyond loopback on purpose — see Exposing the proxy on your network.
  • API key — paste your key (held in memory only; a forget link clears it).
  • Model — searchable list of the upstream catalog with a "hide non-chat" toggle. Models are classified in proxy-core (chat vs the embed / image / audio / rerank / moderation families) and tagged accordingly; clicking a model applies it instantly — and the choice is remembered per-endpoint in ui_state.json, so each upstream restores its own active model after a restart (and switching endpoints back and forth). Each chat model has a "show in tray" checkbox that controls whether it appears in the tray's Models submenu — with all / none shortcuts and shift-click range selection. That tray-visibility choice is likewise saved per-endpoint to ui_state.json (next to config.toml).
  • Claude Code slots (Messages endpoint only) — five model slots (Opus, Sonnet, Haiku, Fable, Subagent). Each maps a stable proxy-cc/<slot> label (what Claude Code sends) to a catalog model id at request time. Choices persist per endpoint in ui_state.json. The subagent slot accepts a model or an Inherit toggle (mutually exclusive). Run Claude Code stays disabled until all five slots are configured.
  • Start agent — one button per known agent, gated against the active endpoint's API (the incompatible agent is disabled with a tooltip explaining which API it needs). A copy-able PowerShell command block is shown too. The "live" indicator reflects the agent terminal the app launched (from this window or from the tray) and clears within ~1.5 s of that terminal being closed.
  • Status — live, real values polled from the proxy (~1.5 s): the configured endpoint, the active API, the forwarded request counter, and the last request (model → endpoint → status code).

Configuring GitHub Copilot CLI

The easiest way is the "Run Copilot" button (tray or settings window) — it launches a terminal with the environment already pointed at the proxy.

Alternatively, the settings window has a "Copy commands" button (and shows the commands as selectable text) so you can paste them into your own PowerShell — handy if copilot is not on the PATH of the launched shell:

$env:COPILOT_PROVIDER_BASE_URL="http://127.0.0.1:8080"
$env:COPILOT_MODEL="copilot-proxy-model"   # value is arbitrary — the proxy overrides it
copilot

COPILOT_PROVIDER_API_KEY is not needed — the proxy injects the key from memory. Use http://127.0.0.1:8080 without /v1: Copilot appends /chat/completions, and the proxy forwards that path to your endpoint base (the endpoint URL minus its API suffix).

Token limits (silencing the catalog warning)

By default Copilot warns that copilot-proxy-model is not in its built-in catalog and falls back to default token limits. The proxy fills COPILOT_PROVIDER_MAX_PROMPT_TOKENS / COPILOT_PROVIDER_MAX_OUTPUT_TOKENS automatically from the selected model's /models metadata when the upstream advertises it (OpenRouter-style context_length / max_completion_tokens, LiteLLM max_input_tokens / max_output_tokens, vLLM max_model_len, …). When it doesn't — plain OpenAI / Ollama report no context length — set the max prompt / output tokens fields in the settings window to override (a value you enter wins over auto-detection). Either source silences the warning; leaving both empty keeps Copilot's own defaults.

Copilot reads these once at launch, so they reflect the model selected when you start it — re-launch after switching models if you want the new model's budget.

Configuring Codex CLI

The "Run Codex" button (tray or settings window) launches codex with an ephemeral provider pointed at the proxy — no edits to your ~/.codex/config.toml. The equivalent manual commands are shown under "Copy commands":

$env:CODEX_PROXY_KEY="proxy-managed"   # dummy — the proxy injects the real key
codex -c model_provider=proxy `
  -c model_providers.proxy.name=copilot-proxy `
  -c 'model_providers.proxy.base_url=http://127.0.0.1:8080' `
  -c model_providers.proxy.wire_api=responses `
  -c model_providers.proxy.env_key=CODEX_PROXY_KEY `
  -c model=copilot-proxy-model

The settings window's Copy commands renders this as a single line (the backtick continuations above are only for readability).

Important: since February 2026 Codex speaks only the Responses API (wire_api = "responses"); the chat wire API was removed. Your endpoint must therefore be a /responses URL — set it in the settings window (or flip the switch to Responses). When the active endpoint is a chat or messages one, "Run Codex" is disabled, so you never point Codex at an endpoint that can't answer it. Chat-only upstreams (e.g. a plain Ollama server) would need a Responses→Chat translation proxy, which is out of scope for now.

Configuring Claude Code

Claude Code speaks the Anthropic Messages API (/messages). Set your endpoint to a Messages URL (or flip the switch to Messages), configure all five model slots in the settings window, then use "Run Claude Code" (tray or settings window).

At launch the proxy sets ANTHROPIC_BASE_URL to the local proxy and maps each slot to a stable proxy-cc/<slot> label via env vars (ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, …). At request time the proxy rewrites those labels to the catalog model ids you picked. The subagent slot can inherit Claude Code's default (env var omitted) or point at proxy-cc/subagent.

The equivalent manual commands are shown under "Copy commands":

$env:ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
$env:ANTHROPIC_API_KEY="proxy-managed"   # dummy — the proxy injects the real key
$env:ANTHROPIC_DEFAULT_OPUS_MODEL="proxy-cc/opus"
$env:ANTHROPIC_DEFAULT_SONNET_MODEL="proxy-cc/sonnet"
$env:ANTHROPIC_DEFAULT_HAIKU_MODEL="proxy-cc/haiku"
$env:ANTHROPIC_DEFAULT_FABLE_MODEL="proxy-cc/fable"
# $env:CLAUDE_CODE_SUBAGENT_MODEL="proxy-cc/subagent"   # omit to inherit
claude

Important: your upstream must serve /messages (Anthropic-compatible). OpenAI-style chat endpoints won't work for Claude Code. When the active endpoint is chat or responses, "Run Claude Code" is disabled. All five slots must be configured before launch — the tray shows Claude Code — N/5 slots set until then.

Verifying what Copilot really talks to

The proxy logs every forwarded request (model + target URL + status). In dev mode (cargo tauri dev) you see them in the console, e.g.:

INFO forwarding request method=POST path=/chat/completions model=model-b target=https://your-endpoint.example.com/v1/chat/completions
INFO upstream responded status=200 OK model=model-b

The settings window also shows a live "Requests forwarded" counter and the last request (model → endpoint → status), which works in release builds too.

Tests and demo (run on any platform)

The core is GUI-independent:

cargo test -p proxy-core                 # tests: model swap, auth, missing key (502), streaming
cargo run -p proxy-core --example demo   # end-to-end demo against a stub endpoint

CI / prebuilt executable

Two GitHub Actions workflows run on push:

  • CI (.github/workflows/ci.yml) — on every push to main and on pull requests: Linux runs proxy-core tests only (~1 min); Windows builds the release .exe (no installers) and uploads it as a workflow artifact.
  • Release (.github/workflows/release.yml) — on v* tags only: validates that the tag matches Cargo.toml / tauri.conf.json, builds the .exe plus MSI/NSIS installers, and attaches them to a GitHub Release.

Push a v* tag (e.g. v0.4.1) to publish installers to Releases.

Security

  • The API key is kept in memory only (wrapped in secrecy::SecretString): never written to disk, never logged, never returned to the UI.
  • The proxy is loopback-only by default (127.0.0.1). It injects your API key into every forwarded request, so a non-loopback bind would let anything on the network use your key. Binding beyond loopback is therefore an explicit opt-in protected by a gateway token — see Exposing the proxy on your network.
  • Use an https:// endpoint — a non-HTTPS endpoint URL sends the key unencrypted (the app warns about this too).
  • Don't embed credentials in the endpoint URL (https://user:pass@host/…) — such URLs are rejected so a key can't leak into config.json or the logs.
  • The listen address and endpoint URL are validated both when entered and when config.json is loaded at startup; an invalid hand-edited value falls back to a safe default instead of being trusted.
  • The settings window loads only local, static assets under a restrictive CSP.

Exposing the proxy on your network

By default the proxy binds to 127.0.0.1 and only the local machine can reach it. To let another device (e.g. a second machine on your LAN) use the proxy:

  1. In the settings window, turn on "expose to network" under the listen address. This generates a gateway token and reveals it (with copy and regenerate actions).
  2. Set the listen address to a reachable interface — e.g. 0.0.0.0:8080 to bind all interfaces. (Without the opt-in, a non-loopback address is rejected, and a hand-edited config.json with one is reset to loopback on startup.)
  3. On the remote client, point it at http://<this-machine-ip>:8080 and send the gateway token in the Authorization header: Authorization: Bearer <token>.

Loopback clients (including the locally launched Copilot, Codex, and Claude Code agents, which always connect via 127.0.0.1) never need the token — it gates non-loopback peers only. The token is a self-generated credential for this proxy, separate from your upstream API key; it is stored in config.json so a remote device need not re-pair after a restart. Regenerate it to revoke access. Even token-gated, remember the proxy spends your upstream key on behalf of any authorized client — only expose it on networks you trust.

Notes

  • OpenAI-compatible endpoints are supported for Copilot (/chat/completions) and Codex (/responses). Claude Code needs an Anthropic Messages (/messages) endpoint. The proxy forwards whichever path the client sends, so the upstream must support it.
  • All models share a single endpoint base; the proxy only changes the model field.
  • The API key lives in memory only — re-enter it after restarting the app.

About

Local Windows tray app that live-swaps the LLM model for GitHub Copilot CLI and Codex CLI — an OpenAI-compatible local proxy; API key kept in memory only.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages