Skip to content
⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⣶⣾⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣀⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠗⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣰⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠐⠛⠁⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠈⠛⢉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣦⣤⣤⣀⣀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠐⠋⠁⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⠛⠉⠉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷

Lupin

Claude Code, on any model.
The gentleman router: it borrows the Claude Code harness and lends it to other models.



CI License: Apache-2.0 Node >= 20 TypeScript strict tests


Your whole setup lives in Claude Code, not in the model: MCP servers, skills, CLAUDE.md, hooks, memories, plugins. But Claude Code speaks one protocol, the Anthropic Messages API, so trying another model means changing tool and starting over.

Lupin changes the model. Nothing else moves.

Sixty seconds

npx lupin-code@latest init            # wizard: provider, key, a real connectivity test
npx lupin-code@latest run -- claude   # Claude Code, already routed

If you keep it, install it once and drop the npx. Every example below uses the short command:

npm i -g lupin-code
lupin --version

Switch model with the session open, no restart:

lupin use glm                # you are on GLM-5.2 from the next request
lupin use gpt --bg kimi      # main on GPT, subagents on Kimi, because subagents are most of the bill
lupin go kimi-sub -- claude  # switch and launch, one gesture

Use a subscription instead of an API key:

lupin login kimi      # device flow
lupin login openai    # Sign in with ChatGPT
lupin login gemini --i-accept-the-risk
lupin login copilot --i-accept-the-risk

Or without leaving Claude Code at all: open /model and pick a row that reads switch Lupin profile: <name>. Lupin publishes one per profile, and picking one moves the active profile from inside the session, with the conversation intact. That is the answer to running out of quota mid-task, and it is exactly how this was verified: Kimi answered usage limit for this billing cycle, the picker moved the session to ChatGPT, and the next request was served.

One honest limit of the picker

The client re-sends the picked id on every later turn, so Lupin acts on it only when it changes. Consequence: pick switch: B from the picker, then switch to A from the CLI or the TUI, and the picker cannot bring you back to B, because re-picking the row it still shows as selected sends the same id and no gesture is seen. Any other surface works (lupin use B, the TUI, or picking a different row first). The alternative was letting every turn of an old session drag the active profile back, which would mean no other surface could ever hold a switch.

Not one more router: the honest one

Everybody translates formats. Nobody tells you whether the model survives the harness.

That is the hard part. A model can speak the protocol perfectly and still fail every task, because Claude Code demands exact-match edits, a tool loop that closes, and roughly 46,000 tokens of prompt before the first word. So Lupin ships the question as a command:

lupin doctor kimi-sub
lupin doctor scoring the ChatGPT subscription 10/10
A real run against the ChatGPT subscription. Only the waiting is edited: the session took 116 seconds and is held for two.

lupin doctor runs a real headless Claude Code session against a dedicated server on an ephemeral port, and scores it from artefacts on disk (files really edited, scripts that really run), never from what the model claims it did. Six checks, threshold 7, no hidden retries.

Three things it refuses to do, each of them learned the hard way:

  • It will not grade a session that never reached the model. Claude Code reports success when its own loop ends cleanly, even when every request died on a protocol error. Where another tool would print 1/10 and blame the model, the doctor prints notRun and the cause (ADR-23).
  • It will not hide its own help. A 10/10 earned because the proxy repaired a broken tool call is different information from a clean 10/10, and the verdict says which one you got.
  • It will not invent a number. No score is reported for a provider nobody has run.
What the doctor has actually measured (dates included, because a score without one is a rumour)
Profile Score When Notes
kimi-sub (Kimi Code subscription) 10/10 2026-07-28 reproducibility measured separately on 2026-07-19: 10, 10, 10
kimi (Moonshot API key) 10/10 2026-07-19 95s, cache_control accepted
openai-sub (ChatGPT subscription) 10/10 2026-08-05 116s over 15 requests, 49% of input served from cache (the run in the GIF above)
gemini-sub (Google Code Assist) 8/10 2026-07-29 free tier: the run ended on 429 No capacity, which is a tier limit, not a translation defect
lmstudio + gemma-4-12b 0/10 2026-07-19 honest: the harness floor and this GPU cannot be satisfied together
copilot-sub not run works live, but a full doctor run would spend most of a free plan's monthly allowance

Providers

Four lanes, picked by Lupin, never by you. Passthrough first: when a provider already speaks Anthropic, nothing is translated at all.

Provider Lane Credential State
Kimi / Moonshot passthrough API key or subscription verified live, doctor 10/10
ChatGPT subscription responses Sign in with ChatGPT (PKCE) verified live, doctor 10/10
Gemini Code Assist subscription codeassist Sign in with Google (PKCE) verified live, doctor 8/10
GitHub Copilot subscription translate GitHub device flow verified live 2026-08-05. On the free plan expect about 50 chat requests a month, which one real session can spend
DeepSeek, Z.AI / GLM passthrough API key implemented, not scored
OpenRouter passthrough or translate API key 344 models, 255 with tool calling
OpenAI, Gemini (pay per token) translate API key implemented, not scored
Ollama, LM Studio, ds4-server passthrough none local, native Anthropic endpoint
llama.cpp server translate none local

344 models on OpenRouter, and Claude Code can natively use zero of them, because OpenRouter's Anthropic-compatible endpoint only accepts Anthropic models. Through Lupin the 255 with tool calling become usable. The other 74 connect and stay a chat, which the wizard says out loud instead of letting you find out mid-task.

How the routing works
Claude Code ──ANTHROPIC_BASE_URL──▶ Lupin (127.0.0.1)
                                      │
            ┌───────────────┬─────────┴─────────┬────────────────┐
            ▼               ▼                   ▼                ▼
      [passthrough]    [translate]        [responses]      [codeassist]
      Kimi, DeepSeek   OpenAI, Gemini,    ChatGPT          Gemini Code
      Z.AI, Ollama     llama.cpp,         subscription     Assist
      LM Studio, ds4   OpenRouter,        (WHAM)           subscription
                       Copilot
  • Passthrough rewrites the URL, the auth header and the model name. Nothing else is touched, so the provider's prompt cache keeps hitting: on a local runtime that is the difference between a turn measured in seconds and one measured in minutes.
  • Translate maps requests, responses and SSE streaming, tool calling included, with MCP names longer than 64 characters rewritten through a deterministic hash.
  • responses and codeassist exist because a ChatGPT or Google OAuth token does not spend on those providers' public APIs at all. Each subscription has its own private protocol, so each got its own translator, built from real captured traffic.

Three model slots (opus, sonnet, haiku) map to whatever you point them at, per profile, and --bg sends subagents somewhere cheaper than the main model.

Where routers really break

A provider can honour the protocol to the letter and still hand you garbage inside the content: reasoning wrapped in <think>, a tool call the server never turned into tool_calls[], special tokens leaking into the text. The model "calls" the tool, nobody runs it, and the agentic loop dies quietly.

In a sample of 382 claude-code-router issues, over 29% land here, re-fixed provider by provider instead of once. Lupin treats it as one problem, with one engine.

The four rules that engine follows
  • One engine for both paths. The same normalizer serves the non-streaming mapper and the SSE translator, and a test feeds it the same input character by character and as a single block, demanding identical results. A normalization that behaves differently while streaming is exactly the bug this makes impossible.
  • Verified markers, never guessed. Eight model families (Qwen3, Qwen3-Coder, GLM, DeepSeek, Kimi K2, Mistral, Llama, GPT-OSS Harmony) checked against the official chat templates and the parsers of vLLM, llama.cpp and SGLang, then put through an adversarial pass that threw out the invented ones. GLM reuses Qwen's <tool_call> with a different payload; DeepSeek uses fullwidth vertical bars where Kimi uses ASCII. A wrong character does not fail loudly, it simply never matches.
  • Reasoning is not lost. reasoning_content becomes a thinking block. On gemma-4-12b the content field arrives empty and the whole answer is in there: drop it and Claude Code receives nothing.
  • Never silently. Every normalization that fires lands in the log and in the doctor verdict.

The terminal, done properly

A bare lupin opens the hub. With the optional Rust sidecar on your PATH it is a live dashboard; without it, a status summary and the next step.

The sidecar is not on npm and never will be: it stays out of the JavaScript runtime by design. Build it from a clone, once, then launch it with the bare command:

cargo build --release --manifest-path tui/Cargo.toml
cp tui/target/release/lupin-tui ~/.cargo/bin/      # anywhere on PATH will do

lupin        # the hub finds the sidecar and opens the dashboard
lupin-tui    # or run it directly

Keys: 1-9 switch profile, arrows and Enter do the same on the highlighted row, d runs the doctor on the highlighted profile, : opens the command palette, o edits the failover order, r refreshes now, q quits.

The doctor takes minutes, so it runs as a child process and its output streams into a panel while the dashboard keeps refreshing underneath. The palette lists init, login and run too, and says plainly that they need a shell: the first reads hidden input, the second waits on a browser, and the third hands the terminal to Claude Code.

Give it 32 rows or more and it draws the portrait full size; below that it keeps every fact and shrinks the art. It needs a real terminal: it takes over the screen, so it will not do anything useful inside another tool's output pane.

⣀⣤⣶⣾⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀  L U P I N  v0.1.2   the gentleman router
⠈⢿⣿⣿⣿⣿⣿⣷⣖⠀⠀⠀⠀⠀⠀⠀  daemon up   127.0.0.1:3456
⠀⠴⢿⣿⣿⣿⣿⣿⣿⣀⠀⠀⠀⠀⠀⠀  active: kimi-sub  ->  k3
⠀⠀⠈⠛⢿⠿⣿⣿⣿⣿⣿⣶⣶⣶⣤⣄

Profiles with 1-9 hotkeys, the routing truth per slot, the request tail with every marker (routed, failedOver, tierDowngrade, dialect), and a status line that says in words what just happened. lupin top gives the same truths with no sidecar at all. Details in docs/TUI.md.

There is also a statusline for Claude Code itself, because through a proxy the model introduces itself as the Claude of the system prompt (the UI knows nothing about the mapping, ADR-3), so asking it who it is proves nothing. The truth lives in GET /health, and the statusline shows it as ⇄ profile→model.

Statusline install and every segment it draws

Opt-in, always: Lupin never writes your settings.json (ADR-11). Copy examples/statusline.ps1 (Windows) or examples/statusline.sh (macOS and Linux, needs jq) into ~/.claude/ and register it:

"statusLine": { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File \"C:\\Users\\<you>\\.claude\\statusline.ps1\"" }
Segment Example What it says
Skill flag [CAVEMAN] active mode or skill (flag file), specific to a personal setup
Model Fable 5⚡ the model requested by Claude Code. Through a proxy this is the slot name, not the real model
Lupin routing ⇄ kimi-sub→k3 active profile and the real model of the opus slot, from /health with a 10s cache. Daemon down shows a red OFFLINE
Repo Lupin@main +3/-1 folder, git branch, uncommitted lines
Context ctx: 67k/1M (7%) uses total_input_tokens, cache reads included, which is what makes the percentage honest
Effort effort: xhigh reasoning effort, coloured by rising cost
Thinking ✦think extended thinking active
Cost $4.20 suppressed through Lupin: Claude Code prices Anthropic models, so on another provider it would be fiction
PR PR#12✓ PR state of the branch
Quota 5h: 30% reset 14:00 Claude subscription limits, which through Lupin disappear on their own
Update ↑v2.1.216 a newer Claude Code on npm

Three lines maximum: when space runs out segments drop in order (update, PR, extras, 7d, 5h). The mandatory ones always stay.

Questions people actually ask

Can I use my ChatGPT subscription with Claude Code?

Yes. lupin login openai uses the sanctioned Sign in with ChatGPT flow. The token does not spend on the public OpenAI API, so Lupin talks to the same protocol the official client uses, and lupin doctor openai-sub scores 10/10 on a real session.

Can I run Claude Code on Gemini for free?

Yes, with a caveat worth reading. lupin login gemini reaches Google Code Assist, whose free tier answers on the flash models and returns 429 on the pro ones. Lupin serves you rather than refusing, and logs every substitution as tierDowngrade so you always know which model answered. Two honest warnings: Google collects prompts and code on the free tier with human reviewers able to read them, and Google has suspended accounts for third-party OAuth, which is why the login is gated behind --i-accept-the-risk.

Does my claude-mem / MCP / skills setup keep working?

Everything local does: native tools, local and project MCP servers, plugins, skills, hooks, CLAUDE.md, memory, subagents. Verified with real sessions. What breaks is tied to the claude.ai account, and it breaks with any proxy, not just this one: see the section below.

What does it cost me to run?

Nothing beyond the provider. Lupin is a local process on 127.0.0.1, it has no backend, it uploads nothing, and it never persists prompts or responses. lupin usage aggregates your own log offline, and it sees subagent traffic that the Claude Code transcript does not: 332 requests against 113 visible turns, measured.

Why is Anthropic not in the provider list?

Because Claude Code already runs Claude natively and does it better than a proxy would (ADR-18). Lupin exists to reach the models Claude Code cannot.

What you lose with any proxy

Claude Code ties some features to the claude.ai login and to api.anthropic.com. Point ANTHROPIC_BASE_URL anywhere else (Lupin, claude-code-router, LiteLLM, any gateway) and Claude Code itself disables them. There is no hybrid mode.

  • Remote Control (driving the session from claude.ai or your phone), since v2.1.196.
  • MCP connectors hosted on claude.ai, voice dictation, and the cloud surfaces (web, mobile, Slack, routines, ultrareview).

One cosmetic quirk to expect: resuming a session may print Session model k3 could not be restored. Claude Code persisted the provider's own model name and fails to find it in its catalogue. The routing stays correct, you only lose the persistence of the /model selection across restarts. Lupin does not rewrite the response to hide this, because byte-faithful passthrough is the entire point of passthrough (ADR-7).

Local models, zero keys

Pick ollama, lmstudio, llamacpp or ds4 in the wizard: no key to paste, the models are read from your own server with their real context windows, and you get a warning about the ones that do not declare tool support, since Claude Code cannot take a single step without them.

That distinction is not pedantry. gemma-4-12b declares a 262,144 token window and runs with 8,192: a factor of 32. Lupin always prefers the loaded window over the declared maximum, and marks which one it got.

Everything else

Command reference
Command Does
lupin the hub: TUI when the sidecar is installed, else status and next steps
init wizard: provider, key (never echoed), a real connectivity test
login <provider> / logout OAuth, with --account <label> for a second account on the same provider
use <profile> [--bg <p>] hot switch, no restart: the open session moves on its next request
go [profile] -- <cmd> switch and run in one step
run -- <cmd> start the daemon if needed and run with the env pointed at Lupin
resume [profile] continue this directory's last session on another provider
doctor [profile] the real headless session, scored on disk artefacts
use <profile> --opus <model> aim a slot by hand, for profiles whose models come from the account
list / status / stop / logs -f the plain truths
top live console, no sidecar needed
usage [--days N] tokens really served, aggregated from your local log

Every command behaves identically on Windows PowerShell, cmd, and any POSIX shell. lupin run spawns Claude Code with no shell in between, so your arguments arrive byte for byte (ADR-29).

Documentation index
File Content
docs/NEXT-STEPS.md Start here: current state, how to verify it, what to do next
DESIGN.md Vision, prior art, positioning, risks
docs/DECISIONS.md ADR log: every decision, the why, the rejected alternatives
docs/SPEC-TRANSLATION.md Translation core: mapping, SSE, errors, acceptance fixtures
docs/SPEC-PROVIDERS.md Provider registry, profiles, slot mapping, quirks
docs/SPEC-CLI.md CLI, doctor, security, UX
docs/ROADMAP.md Milestones, verification criteria, next steps
docs/ARCHITECTURE.md Repo layout, dependency rules (a pure core)
docs/TESTING.md Fixtures from real output, test levels
docs/TUI.md The terminal hub: install, keys, panels, troubleshooting
docs/COMPETITIVE.md Competitive analysis: white space, steal candidates
docs/DESIGN-OAUTH.md Pluggable credential source, device flow
docs/DESIGN-OAUTH-PKCE-TUI.md OAuth PKCE, the control API, the Rust sidecar
docs/DESIGN-TRANSLATORS-DEDICATED.md The two subscription translators

Principles

  1. Fixture first. The fixture, recorded from real provider output, comes before the code. Real dialects are stranger than you would guess: keep-alive SSE comments, repeated finish_reason, usage arriving after the stream ended, errors delivered as a data frame.
  2. Centralized quirks. Never if (provider === x) scattered around. Flags in one registry, one implementation each.
  3. Privacy. Prompts and responses are never persisted. Keys live in the OS keychain or a 600 file, never in the config and never in the logs. It binds to 127.0.0.1 only.
  4. Zero side effects. Lupin never touches ~/.claude/settings.json. Uninstalling means stopping using it.
  5. No invented numbers. A context window enters the defaults only when the vendor publishes the exact figure. DeepSeek and Gemini write "1M" without saying whether that is 1000 or 1024, so they stay without one: a route that never fires beats a route that fires on the wrong number.
  6. Disciplined scope. Lupin stays a proxy. No Electron app, no web dashboard, no relay bot. The biggest competitor accumulated 853 open issues while adding surface; the answer here is not to add it.

The portrait is Arsene Lupin as Leo Fontan drew him in 1908 for Arsene Lupin contre Herlock Sholmes. Public domain, like the books.

Apache 2.0

About

Run Claude Code on any LLM provider without losing your setup, local-first proxy with verified profiles, OAuth device-flow login, failover, content-aware routing and an honest per-model compatibility doctor

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages