TUI + CLI dual-mode profile switcher for pi agent
Manage provider profiles and run a local model-name routing gateway with failover β via an interactive TUI or CLI.
# npm (recommended)
npm install -g @cokefenta/pi-switch
# or via pi
pi install npm:@cokefenta/pi-switchBuild from source (requires Node.js >= 20, Rust 1.80+):
git clone https://github.com/user/pi-switch.git
cd pi-switch
npm install
npm run build:native
node bin/pi-switch.js tuiSupported platforms:
- β Windows (x64)
- β macOS (Intel & Apple Silicon)
- β Linux (x64) - glibc & musl
Linux users: This package includes prebuilt binaries for both glibc and musl systems. If you encounter a GLIBC version error, the package will automatically fallback to the musl binary which has broader compatibility.
Troubleshooting GLIBC errors:
# If you see "GLIBC_X.XX not found", build from source:
npm install -g @cokefenta/pi-switch --build-from-sourcepi-switch tui # Interactive TUI (recommended)
pi-switch webui start --daemon # Browser UI at http://127.0.0.1:43110 (background daemon)
pi-switch doctor # Run environment diagnosticsThree ways, one core. CLI, TUI, and WebUI are thin adapters over the same Rust core. See WEBUI_GUIDE.md for the WebUI and how to keep the three interfaces in sync.
# Provider management
pi-switch provider add <name> [--preset <id>] [--api-key <key>]
pi-switch provider list
pi-switch provider show <name>
pi-switch provider delete <name>
pi-switch provider expose <name> <model-ids...> # Expose models to pi agent
pi-switch provider fetch-models <name> # Fetch models from API
# Proxy (gateway)
pi-switch proxy failover <p1,p2,...> # Same-model fallback chain
pi-switch proxy start --daemon # Start proxy daemon
pi-switch proxy status
# Package management
pi-switch package list # List installed packages
pi-switch package add <id> <name> <version> # Add a new package
pi-switch package toggle <id> # Enable/disable package
pi-switch package remove <id> # Remove package
pi-switch package show <id> # Show package details
# WebUI (browser config) β always use --daemon so it runs in the
# background and can be stopped with `pi-switch webui stop`
pi-switch webui start --daemon [--host <ip>] [--port <port>]
pi-switch webui status
pi-switch webui stop
# Other
pi-switch presets list # List built-in presets
pi-switch config show # Display current config
pi-switch config backups # List backup files
pi-switch config export <passphrase> # Encrypted export
pi-switch config import <path> <passphrase> # Encrypted import
pi-switch import ccswitch [--path <db>] [--all] [--force] # Import providers from cc-switch
pi-switch stats # View request statistics| Category | Highlights |
|---|---|
| π Provider Management | CRUD, duplicate, search/filter, model management, expose to pi agent, configure Responses API passthrough/conversion mode |
| β₯ cc-switch Import | One-click import of providers from cc-switch (Claude Code / Codex / Gemini), dedup by base URL, skip official presets β CLI, TUI, WebUI |
| π‘ Built-in Presets | OpenRouter, Anthropic, DeepSeek, SiliconFlow, OpenAI β add profiles instantly |
| π Model-Name Gateway | Stateless routing by profile/model in the request body, SSE streaming, User-Agent disguise, request-body filtering, OpenAI β Anthropic conversion, Responses β Chat Completions conversion with function tools, native OpenAI Responses passthrough, failover, circuit breaker |
| π¦ Package Management | Install, enable/disable, and manage packages across CLI, TUI, and WebUI |
| π₯οΈ Interactive TUI | ratatui-powered, Dracula theme, mouse support, vim keys (hjkl) |
| π Bilingual | English / δΈζ, persisted to config, toggle in Settings |
| π Usage Stats | Per-provider, per-model request metrics & latency; four-dimension token totals (input/output/cached/reasoning), cache hit rate, time-window queries (today/24h/7d/custom), per-conversation breakdown |
| πΎ Backup & Sync | Auto-backup on mutation, AES-256-CBC encrypted export/import |
| π©Ί Diagnostics | doctor command checks config, models.json, structure |
Already using cc-switch? You can import its providers into pi-switch with one command instead of re-adding them by hand:
pi-switch import ccswitch # interactive selection
pi-switch import ccswitch --all # import everything new
pi-switch import ccswitch --path /path/to/cc-switch.db # custom db location- Reads
~/.cc-switch/cc-switch.db(SQLite, read-only β cc-switch is never modified) - Maps the three common client types: Claude β
anthropic-messages, Codex βopenai-responses, Gemini βgoogle-generative-ai - Official presets (e.g.
claude-official) are skipped - Dedup by base URL: providers already in pi-switch are flagged as existing and skipped (use
--forceto overwrite) - Name collisions resolve to
name (cc)instead of silently overwriting - If the default db path is missing, you are prompted for the path to
cc-switch.db(or you can cancel)
Available in all three UIs: CLI (pi-switch import ccswitch), TUI (Profiles β i), WebUI (Profiles β Import from cc-switch).
Every proxied request is appended to ~/.pi-switch/requests.log as a JSON line. For streaming responses the upstream SSE stream is teed: each request's input/output/cached/reasoning token counts (when the upstream reports them) and conversation id are parsed on the side and the log line is written when the stream ends β the stream itself is never buffered. Reasoning tokens are a subset of output tokens (parsed from completion_tokens_details.reasoning_tokens / output_tokens_details.reasoning_tokens where the upstream reports them); they never inflate the total.
- TUI Stats page shows the cumulative input/output tokens and the cache hit rate.
- Stats API (
GET /api/stats) returnstotalTokenswith four dimensions β input / output / cached / reasoning (total = input + output, reasoning is a subset of output) β pluscacheHitRate, per-provider token columns, andbyConversationβ conversations sorted by most recent activity (top 20), with requests without an id merged into a singleunlabeledgroup. - Time window β the WebUI stats page has a time-range picker: Today (local calendar day from 00:00), Last 24h and Last 7d (rolling windows), and Custom (start day 00:00 β end day 24:00, both dates required). The default is Today. The picker converts the window to
from/toepoch-millis and callsGET /api/stats?range=<today|last24h|last7d|custom>&from=<ms>&to=<ms>; a bare request with no window parameters returns the full history. - WebUI dashboard β token totals render as five tiles (Input / Output / Cached / Reasoning / Total) with subset badges (
Cached β Input,Reasoning β Output), and each conversation row adds Input / Output / Cached / Reasoning / cache hit rate / Total in a two-line layout when over-wide; missing or zero token values show-. - Request details β below the conversation card, the stats page lists every request in the current window, newest first, capped at the most recent 100: time, provider, model, status (with error for failures), and input / output / cached / reasoning / total tokens plus per-request cache hit rate. Rows without reported usage show
-, never a misleading zero. - Cache hit rate = cached input tokens Γ· total input tokens (output tokens excluded). When no cache data exists it shows
-, never a misleading0%. - Conversation id comes from the client:
x-conversation-idheader first,x-opencode-sessionsecond (sent by pi/open-code clients),conversation_idbody field as fallback (ADR-0002). - Only successful requests with reported usage count towards token totals; failover/retry intermediate rows and old log lines without token fields are excluded gracefully, so upgrading never breaks or blanks existing history.
graph LR
subgraph Setup["βοΈ Setup"]
A[Add Provider] --> B[Configure Models]
B --> C[Expose to Pi]
C --> D[Set Failover Chain]
end
subgraph Runtime["π Runtime"]
E["Request<br/>model: provider-a/gpt-5.4"] --> F{Resolve Route}
F --> G[Try provider-a]
G --> H{Success?}
H -->|β| I[Response]
H -->|β 429/5xx| J[Try provider-b]
J --> K{Success?}
K -->|β| I
K -->|β| L[Circuit Breaker]
L --> M[60s Cooldown]
M --> N[Half-Open Probe]
N -->|β| G
N -->|β| M
end
Setup --> Runtime
style A fill:#50fa7b,stroke:#50fa7b,color:#282a36
style E fill:#8be9fd,stroke:#8be9fd,color:#282a36
style I fill:#50fa7b,stroke:#50fa7b,color:#282a36
style L fill:#ff5555,stroke:#ff5555,color:#f8f8f2
1. Add a provider (CLI or TUI)
pi-switch provider add provider-a --api openai-completions --base-url https://api.example.com/v1 \
--api-key '$API_KEY' --models gpt-5.4,claude-sonnet-4-5In TUI: Profiles β a β fill form β Ctrl+S
2. Expose models to pi agent β choose which models appear in ~/.pi/agent/models.json
pi-switch provider expose provider-a gpt-5.4In TUI: Profiles β select provider β x
3. Start the proxy β it writes a single pi-switch gateway provider to pi
pi-switch proxy failover provider-b,provider-c # optional same-model fallback
pi-switch proxy start --daemon4. Use in pi β select the pi-switch provider, then pick a profile/model like provider-a/gpt-5.4
Requests are routed by the model name in the request body β no out-of-band state, no "current target":
- Model-name routing β
"model": "provider-a/gpt-5.4"resolves to profileprovider-a, real modelgpt-5.4; the proxy rewrites the body before forwarding upstream - Single gateway provider β pi sees one
pi-switchprovider advertising every exposed model asprofile/realModelId; switching model in pi = sending a different model string = instant routing change - Automatic failover β same-model fallback across the configured chain on 429/5xx errors or network failures
- Circuit breaker β after 3 consecutive failures, provider enters 60s cooldown; auto-recovery on half-open probe success
- Streaming (SSE) β same-format requests (openaiβopenai, anthropicβanthropic) stream token-by-token; upstream response headers (Content-Type, etc.) are preserved
- OpenAI β Anthropic β transparently converts between chat completions and messages APIs
- User-Agent disguise β built-in presets (Claude Code / Codex / Gemini) send the matching client's real User-Agent (and headers like
anthropic-beta) to pass upstream client checks; settable globally or per-profile
Known limitation β the OpenAI β Anthropic conversion path can't stream: it parses the full JSON to convert formats. If pi sends
stream: truebut the model routes to a cross-format upstream (OpenAI request β Anthropic upstream, or vice-versa), the reply comes back as a single non-streamed response. Same-format routes stream normally.
pi-switch/
βββ bin/pi-switch.js # CLI entry point
βββ index.js # ESM wrapper for native addon
βββ pi-switch-native.cjs # NAPI loader (auto platform detection)
βββ src-rust/ # Rust native core (napi-rs)
β βββ lib.rs # NAPI function exports
β βββ config.rs # Config load/save, types
β βββ ops.rs # Core operations
β βββ presets.rs # Built-in provider presets
β βββ proxy.rs # Proxy server (gateway routing, failover, circuit breaker)
β βββ daemon.rs # Daemon lifecycle
β βββ stats.rs # Request log aggregation + token usage stats
β βββ usage.rs # Token usage extraction & SSE stream parsing
β βββ sync.rs # Encrypted export/import
β βββ tui/ # Interactive terminal UI (ratatui)
β βββ app.rs # State machine + key handler
β βββ form.rs # Provider form state
β βββ i18n.rs # Bilingual (EN/ZH)
β βββ ui/ # Rendering (chrome, pages, overlays)
βββ src/ # JavaScript layer (pi extension support)
βββ extensions/index.ts # Pi agent extension (/piswitch)
βββ Cargo.toml
Config files:
~/.pi-switch/config.jsonβ profiles, proxy settings, failover chain~/.pi-switch/requests.logβ per-request JSON log (status, latency, token usage, conversation id)~/.pi-switch/backups/β timestamped auto-backups on every mutation~/.pi/agent/models.jsonβ pi's provider registry (pi-switch writes a single gateway provider)
How do I switch models in pi?
In pi, open /model and pick any advertised profile/model (e.g. provider-a/gpt-5.4). The proxy routes by the model name in each request β no extra step needed.
To add more models, expose them in TUI (Profiles β select provider β x) or via CLI:
pi-switch provider expose <name> <model-id>...How do I set up failover?
In TUI: Settings β Failover β Enter β enter comma-separated profile names β Enter.
Or via CLI:
pi-switch proxy failover provider-b,provider-cProfiles in the failover chain that expose the same model are tried in order when the primary fails.
What does the [proxy] badge mean?
The [proxy] badge indicates this profile is a meta-profile (with "proxy": true). Proxy profiles are used to register a pi provider that points to the local gateway. They are excluded from upstream routing.
In the current gateway mode, proxy profiles are typically not needed β the proxy automatically writes a single pi-switch gateway provider to pi's models.json on startup.
How does gateway routing work?
The proxy advertises every exposed model as profile/realModelId under a single pi-switch provider. When pi sends a request with "model": "provider-a/gpt-5.4", the proxy:
- Splits on the first
/β profileprovider-a, real modelgpt-5.4 - Routes to the
provider-aprofile's upstream, rewritingbody.modeltogpt-5.4 - On failure (429/5xx), tries the failover chain for any other profile exposing
gpt-5.4
# 1. Expose models (per profile)
pi-switch provider expose provider-a gpt-5.4
pi-switch provider expose provider-b gpt-5.4
# 2. Set failover chain (optional)
pi-switch proxy failover provider-b
# 3. Start proxy daemon
pi-switch proxy start --daemonIn pi, select the pi-switch provider, then provider-a/gpt-5.4. The model name in each request determines the route β no "target" to manage.
How does User-Agent disguise work?
Some upstream channels only accept requests from whitelisted clients (checking the User-Agent name prefix). pi-switch has three built-in presets that send the matching client's real identity:
| Preset | User-Agent | Extra headers |
|---|---|---|
| Claude Code | claude-cli/2.1.161 (external, cli) |
anthropic-version, anthropic-beta |
| Codex | codex_cli_rs/0.1.0 |
β |
| Gemini | gemini-cli/0.1.5 |
x-goog-api-client |
- Global:
Settings β User-Agent, cycle withβ/β. - Per-profile: in a profile's detail view press
uto cycle; a per-profile value overrides the global one. Useful when only some upstreams enforce a UA whitelist.
Note: this only passes checks that look at the client name. It does not fabricate deeper per-request tokens (turn state, session ids), which strict first-party endpoints validate.
Where is my data stored?
Everything under ~/.pi-switch/. Pi's own registry is ~/.pi/agent/models.json. No data leaves your machine.
npm run build:native:debug # Build Rust addon (debug)
npm run build:native # Build Rust addon (release)
cargo build # Rust-only build
cargo clippy # Lint
cargo fmt # Format
cargo test --release --lib # Run unit testsNote: Stop the TUI/daemon before npm run build:native to avoid file-lock errors on Windows.
- cc-switch β the original TUI-based profile switcher for Claude Code, which pioneered the interactive terminal UI pattern and proxy failover design
- cc-switch-cli β the CLI counterpart, providing a clean command-line interface for provider management
Thanks also to the LINUX DO community for the discussions that sparked this project.
MIT
