{"id":51174126,"url":"https://github.com/numikel/copilot-cli-custom-proxy","last_synced_at":"2026-06-27T02:31:12.178Z","repository":{"id":362270746,"uuid":"1258112491","full_name":"numikel/copilot-cli-custom-proxy","owner":"numikel","description":"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.","archived":false,"fork":false,"pushed_at":"2026-06-25T19:10:37.000Z","size":510,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-25T21:08:15.601Z","etag":null,"topics":["byok","codex-cli","copilot-cli","developer-tools","github-copilot","llm","openai-compatible","proxy","reverse-proxy","rust","system-tray","tauri","windows"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/numikel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T09:33:09.000Z","updated_at":"2026-06-25T19:10:45.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/numikel/copilot-cli-custom-proxy","commit_stats":null,"previous_names":["numikel/copilot-cli-custom-proxy"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/numikel/copilot-cli-custom-proxy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numikel%2Fcopilot-cli-custom-proxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numikel%2Fcopilot-cli-custom-proxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numikel%2Fcopilot-cli-custom-proxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numikel%2Fcopilot-cli-custom-proxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/numikel","download_url":"https://codeload.github.com/numikel/copilot-cli-custom-proxy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numikel%2Fcopilot-cli-custom-proxy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34839888,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["byok","codex-cli","copilot-cli","developer-tools","github-copilot","llm","openai-compatible","proxy","reverse-proxy","rust","system-tray","tauri","windows"],"created_at":"2026-06-27T02:31:09.159Z","updated_at":"2026-06-27T02:31:12.172Z","avatar_url":"https://github.com/numikel.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Copilot CLI Custom Proxy\n\n[![CI](https://github.com/numikel/copilot-cli-custom-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/numikel/copilot-cli-custom-proxy/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n![Platform: Windows](https://img.shields.io/badge/platform-Windows-0078D6?logo=windows)\n![Rust](https://img.shields.io/badge/Rust-2021-orange?logo=rust)\n![Tauri v2](https://img.shields.io/badge/Tauri-v2-24C8DB?logo=tauri)\n\nA local HTTP proxy that lives in the system tray (Windows) for **GitHub Copilot\nCLI**. It intercepts requests, **swaps the LLM model on the fly**, and forwards\nthem to a configured OpenAI-compatible endpoint — without restarting your\nterminal session.\n\nIn BYOK mode, Copilot CLI fixes the model at startup (`COPILOT_MODEL`). This\nproxy lets you switch the model from the tray menu while you work.\n\n## How it works\n\n```\nCopilot CLI ──▶ http://127.0.0.1:8080  (this proxy)\n                     │  • replaces the \"model\" field with the model picked in the tray\n                     │  • injects Authorization: Bearer \u003ckey held in memory\u003e\n                     │  • forwards the remaining headers (except Host)\n                     ▼\n              endpoint base  (OpenAI-compatible endpoint)\n                     │  • the response stream is piped straight back\n                     ▼\n                Copilot CLI\n```\n\n- **Everything is configured in the settings window** — the endpoint URL and the\n  local listen address are set there and persist to `config.json` (next to the\n  executable). No `config.toml` editing required.\n- **You enter the API key in the settings window** — it is kept only in memory\n  (wrapped in `secrecy::SecretString`), never written to disk or to logs.\n\n## Project layout\n\n```\nproxy-core/   # core: Axum + Reqwest, model swap, streaming (testable without a GUI)\nsrc-tauri/    # Tauri v2 app: tray, settings window, background server startup\nconfig.example.toml\n```\n\n## Configuration\n\nEverything is configured **in the settings window** — there is nothing to edit by\nhand. On first run (no config yet) the app starts with defaults, writes a\n`config.json` next to the executable straight away, and opens the settings window\nautomatically. Your choices persist to that same `config.json` (gitignored, as it\nholds your private endpoint URL). If the file is ever found corrupt at startup it\nis copied to `config.json.bak` before being reset to defaults, and the settings\nwindow shows a one-time notice so the reset isn't silent.\n\nIn the **Endpoint** section you set:\n\n- **Endpoint URL** — the *full* upstream URL, including a host and the API\n  suffix, e.g. `https://openrouter.ai/api/v1/chat/completions` or\n  `https://openrouter.ai/api/v1/responses`. **Do not stop at `/v1`** — the URL\n  suffix (matched against the URL's path, so it must be the trailing path\n  segment, not part of the host) is what tells the proxy whether this is a Chat\n  Completions or a Responses endpoint.\n- **Chat completions ⟷ Responses switch** — one API is active at a time. Flipping\n  the switch rewrites the URL suffix; the active API decides which CLI agent you\n  can launch (Copilot for chat, Codex for responses).\n- **Listen address** — the local `host:port` the proxy binds (the host is\n  restricted to a strict character set). Changing it restarts only the background\n  proxy task (no app/terminal restart): the new address is bound first, so if the\n  port is already in use the error is shown in the window and the running proxy is\n  left intact. Re-confirming the same address does nothing.\n\nThe **model list is fetched automatically** from `{endpoint base}/models` (the\nendpoint URL minus the API suffix) once you enter your API key, and via the\ntray's **\"Refresh models\"**.\n\n### Which agents you can launch\n\nDifferent CLI agents speak different OpenAI-compatible APIs. The active endpoint\nserves exactly one API (derived from its URL suffix), so the app enables only the\nmatching agent:\n\n| Endpoint suffix | API | Agent |\n|-----------------|-----|-------|\n| `/chat/completions` | chat | GitHub Copilot CLI |\n| `/responses` | responses | Codex CLI |\n\nThe other agent is shown disabled (settings window) or hidden (tray), so you\nnever point a CLI at an endpoint that can't answer it. To use the other agent,\nflip the switch (and ensure your upstream serves that API).\n\n### Optional `config.toml` seed\n\n`config.toml` is **no longer required**. If present on first run (and no\n`config.json` exists yet), it is migrated into `config.json` once and then\nignored — useful for upgrading an older install or pre-baking a deployment. Copy\n`config.example.toml` to `config.toml` for the seed format. The app looks for\nconfig next to the `.exe`, then in the working directory.\n\n## Build and run (Windows)\n\nRequirements: [Rust](https://rustup.rs) and the\n[Tauri v2 system prerequisites](https://tauri.app/start/prerequisites/)\n(WebView2 ships with Windows 10/11).\n\n```powershell\n# development mode (with a console and logs)\ncargo install tauri-cli --version \"^2.0\"\ncargo tauri dev\n\n# production build (.exe / installer)\ncargo tauri build\n```\n\nOn launch the app minimizes to the tray. From the tray menu you can:\n- pick the active model (applied instantly, and remembered per-endpoint across\n  restarts),\n- **\"Refresh models\"** — re-fetch the model list from the endpoint,\n- **\"Run Copilot\" / \"Run Codex\"** — open a new terminal with the proxy\n  environment already set and start the chosen agent (see the Codex note below),\n- open **\"Open Settings…\"** for the full window (API key, model list, launcher),\n- choose **\"Quit\"** to exit.\n\nThe tray icon has two states: an accent-filled glyph when the proxy is ready (a\nkey is set and a model is selected) and a muted outline when it is idle. Models\nlive in a **\"Models ▸\" submenu** (not the first level) so \"Open Settings…\" and\n\"Quit\" stay reachable even with hundreds of models. You choose **which** models\nappear in that submenu in the settings window (see below) — the full catalog is\nalways available there.\n\n## Settings window\n\nThe settings window is a small, single-purpose webview (vanilla HTML/CSS/JS — no\nbundler, served from `src-tauri/dist/` under a restrictive `'self'` CSP). It is a\n**frameless** window (`decorations: false`) with its own title bar, and ships a\n**dark/light theme toggle** (defaults to dark; the choice is remembered in\n`localStorage`). Fonts (IBM Plex Sans/Mono) are bundled locally, so the UI needs\nno network access. It has five sections:\n\n- **Endpoint** — the full upstream URL with a **Chat completions ⟷ Responses**\n  switch (one active at a time; the switch rewrites the URL suffix), plus the\n  local **listen address**. Both are validated and persisted to `config.json`;\n  changing the listen address restarts the proxy task. An **\"expose to network\"**\n  toggle lets you bind beyond loopback on purpose — see\n  [Exposing the proxy on your network](#exposing-the-proxy-on-your-network).\n- **API key** — paste your key (held in memory only; a **forget** link clears it).\n- **Model** — searchable list of the upstream catalog with a **\"hide non-chat\"**\n  toggle. Models are classified in `proxy-core` (chat vs the `embed` / `image` /\n  `audio` / `rerank` / `moderation` families) and tagged accordingly; clicking a\n  model applies it instantly — and the choice is **remembered per-endpoint** in\n  `ui_state.json`, so each upstream restores its own active model after a restart\n  (and switching endpoints back and forth). Each chat model has a **\"show in tray\"\n  checkbox** that controls whether it appears in the tray's Models submenu — with\n  **all / none** shortcuts and **shift-click** range selection. That tray-visibility\n  choice is likewise saved per-endpoint to `ui_state.json` (next to `config.toml`).\n- **Start agent** — one button per known agent, gated against the active\n  endpoint's API (the incompatible agent is disabled with a tooltip explaining\n  which API it needs). A copy-able PowerShell command block is shown too. The\n  **\"live\"** indicator reflects the agent terminal the app launched (from this\n  window or from the tray) and clears within ~1.5 s of that terminal being\n  closed.\n- **Status** — live, real values polled from the proxy (~1.5 s): the configured\n  endpoint, the active API, the **forwarded** request counter, and the\n  **last** request (model → endpoint → status code).\n\n## Configuring GitHub Copilot CLI\n\nThe easiest way is the **\"Run Copilot\"** button (tray or settings window) — it\nlaunches a terminal with the environment already pointed at the proxy.\n\nAlternatively, the settings window has a **\"Copy commands\"** button (and shows\nthe commands as selectable text) so you can paste them into your own PowerShell —\nhandy if `copilot` is not on the PATH of the launched shell:\n\n```powershell\n$env:COPILOT_PROVIDER_BASE_URL=\"http://127.0.0.1:8080\"\n$env:COPILOT_MODEL=\"copilot-proxy-model\"   # value is arbitrary — the proxy overrides it\ncopilot\n```\n\n`COPILOT_PROVIDER_API_KEY` is not needed — the proxy injects the key from memory.\nUse `http://127.0.0.1:8080` without `/v1`: Copilot appends `/chat/completions`,\nand the proxy forwards that path to your endpoint base (the endpoint URL minus its\nAPI suffix).\n\n#### Token limits (silencing the catalog warning)\n\nBy default Copilot warns that `copilot-proxy-model` is not in its built-in\ncatalog and falls back to default token limits. The proxy fills\n`COPILOT_PROVIDER_MAX_PROMPT_TOKENS` / `COPILOT_PROVIDER_MAX_OUTPUT_TOKENS`\nautomatically from the selected model's `/models` metadata when the upstream\nadvertises it (OpenRouter-style `context_length` / `max_completion_tokens`,\nLiteLLM `max_input_tokens` / `max_output_tokens`, vLLM `max_model_len`, …). When\nit doesn't — plain OpenAI / Ollama report no context length — set the **max\nprompt / output tokens** fields in the settings window to override (a value you\nenter wins over auto-detection). Either source silences the warning; leaving both\nempty keeps Copilot's own defaults.\n\n\u003e Copilot reads these once at launch, so they reflect the model selected when you\n\u003e start it — re-launch after switching models if you want the new model's budget.\n\n### Configuring Codex CLI\n\nThe **\"Run Codex\"** button (tray or settings window) launches `codex` with an\nephemeral provider pointed at the proxy — no edits to your `~/.codex/config.toml`.\nThe equivalent manual commands are shown under **\"Copy commands\"**:\n\n```powershell\n$env:CODEX_PROXY_KEY=\"proxy-managed\"   # dummy — the proxy injects the real key\ncodex -c model_provider=proxy `\n  -c model_providers.proxy.name=copilot-proxy `\n  -c 'model_providers.proxy.base_url=http://127.0.0.1:8080' `\n  -c model_providers.proxy.wire_api=responses `\n  -c model_providers.proxy.env_key=CODEX_PROXY_KEY `\n  -c model=copilot-proxy-model\n```\n\nThe settings window's **Copy commands** renders this as a single line (the\nbacktick continuations above are only for readability).\n\n\u003e **Important:** since February 2026 Codex speaks **only the Responses API**\n\u003e (`wire_api = \"responses\"`); the `chat` wire API was removed. Your endpoint must\n\u003e therefore be a **`/responses`** URL — set it in the settings window (or flip the\n\u003e switch to **Responses**). When the active endpoint is a chat one,\n\u003e **\"Run Codex\" is disabled**, so you never point Codex at an endpoint that can't\n\u003e answer it. Chat-only upstreams (e.g. a plain Ollama server) would need a\n\u003e Responses→Chat translation proxy, which is out of scope for now.\n\n### Verifying what Copilot really talks to\n\nThe proxy logs every forwarded request (model + target URL + status). In dev\nmode (`cargo tauri dev`) you see them in the console, e.g.:\n\n```\nINFO forwarding request method=POST path=/chat/completions model=model-b target=https://your-endpoint.example.com/v1/chat/completions\nINFO upstream responded status=200 OK model=model-b\n```\n\nThe settings window also shows a live **\"Requests forwarded\"** counter and the\n**last request** (model → endpoint → status), which works in release builds too.\n\n## Tests and demo (run on any platform)\n\nThe core is GUI-independent:\n\n```bash\ncargo test -p proxy-core                 # tests: model swap, auth, missing key (502), streaming\ncargo run -p proxy-core --example demo   # end-to-end demo against a stub endpoint\n```\n\n## CI / prebuilt executable\n\nA GitHub Actions workflow (`.github/workflows/ci.yml`) runs on every push:\n\n- **test** (Linux) — runs the core tests and type-checks the Tauri app.\n- **build-windows** — builds the release `.exe` and the MSI/NSIS installers, and\n  uploads them as workflow **artifacts** (downloadable from the run's summary page).\n\nPush a `v*` tag (e.g. `v0.1.0`) to also attach the binaries to a GitHub Release.\n\n## Security\n\n- The API key is kept in memory only (wrapped in `secrecy::SecretString`):\n  never written to disk, never logged, never returned to the UI.\n- **The proxy is loopback-only by default** (`127.0.0.1`). It injects your API\n  key into every forwarded request, so a non-loopback bind would let anything on\n  the network use your key. Binding beyond loopback is therefore an explicit\n  opt-in protected by a gateway token — see\n  [Exposing the proxy on your network](#exposing-the-proxy-on-your-network).\n- Use an `https://` endpoint — a non-HTTPS endpoint URL sends the key\n  unencrypted (the app warns about this too).\n- Don't embed credentials in the endpoint URL (`https://user:pass@host/…`) — such\n  URLs are rejected so a key can't leak into `config.json` or the logs.\n- The listen address and endpoint URL are validated both when entered and when\n  `config.json` is loaded at startup; an invalid hand-edited value falls back to a\n  safe default instead of being trusted.\n- The settings window loads only local, static assets under a restrictive CSP.\n\n### Exposing the proxy on your network\n\nBy default the proxy binds to `127.0.0.1` and only the local machine can reach\nit. To let another device (e.g. a second machine on your LAN) use the proxy:\n\n1. In the settings window, turn on **\"expose to network\"** under the listen\n   address. This generates a **gateway token** and reveals it (with **copy** and\n   **regenerate** actions).\n2. Set the listen address to a reachable interface — e.g. `0.0.0.0:8080` to bind\n   all interfaces. (Without the opt-in, a non-loopback address is rejected, and a\n   hand-edited `config.json` with one is reset to loopback on startup.)\n3. On the remote client, point it at `http://\u003cthis-machine-ip\u003e:8080` and send the\n   gateway token in the `Authorization` header: `Authorization: Bearer \u003ctoken\u003e`.\n\nLoopback clients (including the locally launched Copilot/Codex agents, which\nalways connect via `127.0.0.1`) never need the token — it gates non-loopback\npeers only. The token is a self-generated credential for *this proxy*, separate\nfrom your upstream API key; it is stored in `config.json` so a remote device need\nnot re-pair after a restart. Regenerate it to revoke access. Even token-gated,\nremember the proxy spends your upstream key on behalf of any authorized client —\nonly expose it on networks you trust.\n\n## Notes\n\n- **OpenAI-compatible** endpoints are supported. Copilot uses Chat Completions\n  (`/chat/completions`); Codex uses the Responses API (`/responses`). The proxy\n  forwards whichever path the client sends, so the upstream must support it.\n- All models share a single endpoint base; the proxy only changes the `model` field.\n- The API key lives in memory only — re-enter it after restarting the app.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumikel%2Fcopilot-cli-custom-proxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnumikel%2Fcopilot-cli-custom-proxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumikel%2Fcopilot-cli-custom-proxy/lists"}