Sandbox
@mxyhi/token_proxy

Local API gateway for Claude Code and Codex

Token Proxy runs on your machine and forwards requests to OpenAI, Gemini, Anthropic, Kiro, Codex, and xAI providers. It can balance upstreams by priority, convert between API formats, and store request and token stats in SQLite for later review. It also includes one-click setup for Claude Code and Codex, plus a Tauri dashboard for upstreams, logs, and usage. The CLI and app share a workspace so you can run it locally or manage it through the desktop UI.

92 stars19 forksRustUpdated 7d ago
Who it's for

Builders who want Claude Code, Codex, or other agent clients to use a local proxy with routing and token tracking.

What it delivers

You can point your agent at one local endpoint and get routing, logging, format conversion, and setup in one place.

What it does

Provider routing

Routes requests across OpenAI, Gemini, Anthropic, Kiro, Codex, and xAI upstreams by priority and strategy.

Format conversion

Converts between OpenAI Chat, OpenAI Responses, Anthropic Messages, and Gemini formats, including SSE, tools, and images.

SQLite token tracking

Stores per-request usage, cached tokens, latency, model, and upstream in `data.db`.

Dashboard and logs

Shows usage totals, trends, top models, upstream probes, and recent requests in the app UI.

One-click client setup

Writes config for Claude Code in `~/.claude/settings.json` and Codex in `~/.codex/config.toml` and `~/.codex/auth.json`.

Local auth and upstream keys

Supports a local access key plus upstream credential injection through `credential.api_keys` or account-backed credentials.

How to get it

  1. 1Call via curl (example with local auth)
    curl -X POST \
      -H "Authorization: Bearer YOUR_LOCAL_KEY" \
      -H "Content-Type: application/json" \
      http://127.0.0.1:9208/v1/chat/completions \
      -d '{"model":"gpt-4.1-mini","messages":[{"role":"user","content":"hi"}]}'
  2. 2You can also call using the Anthropic Messages format (useful for Claude Code clients)
    curl -X POST \
      -H "x-api-key: YOUR_LOCAL_KEY" \
      -H "Content-Type: application/json" \
      http://127.0.0.1:9208/v1/messages \
      -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":256,"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}'

README

Token Proxy

English | 中文

Local AI API gateway for OpenAI / Gemini / Anthropic. Runs on your machine, keeps tokens counted (SQLite), offers priority-based load balancing, optional API format conversion (OpenAI Chat/Responses ↔ Anthropic Messages, plus Gemini ↔ OpenAI/Anthropic, including SSE/tools/images), and one-click setup for Claude Code / Codex.

Default listen port: 9208 (release) / 19208 (debug builds).


What you get

  • Multiple providers: openai, openai-response, anthropic, gemini, kiro, codex, xai
  • Built-in routing + optional format conversion (OpenAI Chat ⇄ Responses; Anthropic Messages ↔ OpenAI; Gemini ↔ OpenAI/Anthropic; SSE supported)
  • Upstream is the only scheduling unit: priority, proxy, enabled, models, and credential live on each upstream entry
  • No separate Accounts / Providers page — on Upstreams, use Add Account to log in or import Kiro / Codex / xAI; the same editor manages identity, quota, token refresh / auto-refresh (where the provider supports it), and routing
  • Account-backed upstreams: one account maps to one stable upstream; deleting it cascades account credentials; copy is disabled
  • Model alias mapping (exact / prefix* / wildcard*) and response model rewrite
  • Local access key + upstream credential injection (credential.api_keys or request-header fallback when local auth is off)
  • SQLite-powered dashboard (requests, tokens, cached tokens, latency, recent)
  • macOS tray live token rate (optional)

Screenshots

Dashboard
Dashboard
Core
Core settings
Upstreams
Upstreams
Add upstream
Add upstream

Quick start (macOS)

  1. Install: move Token Proxy.app to /Applications. If blocked: xattr -cr /Applications/Token\ Proxy.app.
  2. Launch the app. The proxy starts automatically.
  3. Open Config File tab, edit and save (writes config.jsonc in the Tauri config dir). Defaults are usable; just paste your upstream API keys. Running proxies auto-apply the new config via reload or restart when needed.
  4. Call via curl (example with local auth):
curl -X POST \
  -H "Authorization: Bearer YOUR_LOCAL_KEY" \
  -H "Content-Type: application/json" \
  http://127.0.0.1:9208/v1/chat/completions \
  -d '{"model":"gpt-4.1-mini","messages":[{"role":"user","content":"hi"}]}'

You can also call using the Anthropic Messages format (useful for Claude Code clients):

curl -X POST \
  -H "x-api-key: YOUR_LOCAL_KEY" \
  -H "Content-Type: application/json" \
  http://127.0.0.1:9208/v1/messages \
  -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":256,"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]}]}'

Workspace & CLI (Rust)

  • This repo is now a Cargo workspace; the Tauri app still lives in src-tauri/.
  • CLI crate: crates/token_proxy_cli (binary token-proxy).
  • Default config path: ./config.jsonc (override with --config).
  • GitHub Releases also publish packaged CLI archives per target:
    • Unix: token-proxy_cli_<version>_<target>.tar.gz
    • Windows: token-proxy_cli_<version>_<target>.zip
# start proxy
cargo run -p token_proxy_cli -- serve

# start with custom config path
cargo run -p token_proxy_cli -- --config ./config.jsonc serve

# config helpers
cargo run -p token_proxy_cli -- config init
cargo run -p token_proxy_cli -- --config ./config.jsonc config path

Frontend tests

# watch mode
pnpm test

# run once (CI-friendly)
pnpm test:run

# coverage (optional)
pnpm test:coverage

# TypeScript typecheck
pnpm exec tsc --noEmit

Notes:

  • Test files live in src/**/*.test.{ts,tsx}.
  • Global test setup (Tauri mocks + jsdom polyfills) is in src/test/setup.ts.
  • Vitest config is in vitest.config.ts.

Configuration reference

  • File: config.jsonc (comments + trailing commas allowed)
  • Location:
    • CLI: --config (default: ./config.jsonc)
    • Tauri: AppConfig directory (resolved automatically by the app)

Core fields

FieldDefaultNotes
host127.0.0.1Listen address (IPv6 allowed; will be bracketed in URLs)
port9208 release / 19208 debugChange if the port is taken
local_api_keynullWhen set: local auth uses format-specific headers (see Auth rules); local auth inputs are not forwarded upstream.
app_proxy_urlnullProxy for app updater & as placeholder for upstreams ("$app_proxy_url"). Supports http/https/socks5/socks5h.
log_levelsilent`silent
max_request_body_bytes104857600 (100 MiB)0 = fallback to default. Shared inbound, JSON filter, and format-conversion ceiling.
retryable_failure_cooldown_secs15Cooldown window after retryable failures that should temporarily sideline an upstream. 0 disables cooldown. Reloading or restarting the running proxy resets current cooldown state.
same_upstream_retry_count1Extra same-upstream retries after a retryable failure (excluding the first attempt). 0 disables same-upstream retry; max 5.
codex_session_scoped_cooldown_enabledfalseOnly applies to Codex account-backed OpenAI Responses requests. When enabled, cooldown is isolated by session_id; final success clears that session, and requests without session_id do not share cooldown.
tray_token_rate.enabledtruemacOS tray live rate; harmless elsewhere.
tray_token_rate.formatsplitcombined (total), split (↑in ↓out), both (`total
upstream_strategy{ "order": "fill_first", "dispatch": { "type": "serial" } }Structured strategy object. order controls candidate ordering inside one priority group; dispatch controls serial / hedged / race execution.

Upstream entries (upstreams[])

FieldDefaultNotes
idrequiredUnique per upstream.
providersrequiredOne upstream can serve multiple providers. Account-based providers kiro / codex / xai must be alone (cannot mix with each other or with API-key providers).
base_urlrequiredFull base; overlapping path parts are de-duplicated. (providers=["kiro"] / ["codex"] can be empty; xai account-backed uses the fixed CLI gateway base.)
credential{ "type": "passthrough" }Discriminated union — the only credential shape. See below.
preferred_endpointnullkiro only: ide or cli.
proxy_urlnullPer-upstream proxy (not an account field); supports http/https/socks5/socks5h; default is no system proxy. $app_proxy_url placeholder allowed.
priority0Per-upstream scheduling weight (not an account field). Higher = tried earlier.
enabledtruePer-upstream toggle (not an account field). Disabled upstreams are skipped.
available_models[]Inbound model allowlist; empty = no restriction.
model_mappings{}Exact / prefix* / *. Priority: exact > longest prefix > wildcard. Response echoes original alias.
convert_from_map{}Explicitly allow inbound format conversion per provider. Example: { "openai-response": ["openai_chat", "anthropic_messages"] }.
overrides.header{}Set/remove headers (null removes). Hop-by-hop/Host/Content-Length are always ignored.

credential (required shape)

typeShapeNotes
passthrough{ "type": "passthrough" }No static upstream key; may rely on request-header fallback when local_api_key is unset. Not allowed for kiro / codex / xai.
api_keys{ "type": "api_keys", "api_keys": ["key-a", "key-b"] }Static keys; empty list behaves like passthrough after normalize. Not allowed for account-based providers.
account{ "type": "account", "provider": "kiro"|"codex"|"xai", "account_id": "..." }Binds one Provider Account. provider must match the sole account-based entry in providers[]. One (provider, account_id) may bind only one upstream.

Kiro / Codex / xAI must use credential.type = "account". Account login/import creates a stable binding via app lifecycle; do not invent frontend-only ids such as kiro-default / codex-default / xai-default. Deleting an account-backed upstream cascades deletion of that account credential; copy is forbidden.

Legacy flat fields (api_key, api_keys, kiro_account_id, codex_account_id, xai_account_id) are not part of the documented schema. On load they are migrated once into credential and written back (no dual-format docs).

Routing & format conversion

  • Gemini native API: /v1beta/models/* (including :generateContent, :streamGenerateContent, :countTokens, :embedContent, :batchEmbedContents), model catalog/detail, /v1beta/files*, /upload/v1beta/files*, /v1beta/cachedContents*, /v1beta/tunedModels*gemini.
  • Anthropic: /v1/messages (and subpaths) and /v1/completeanthropic (Kiro shares the same format).
  • OpenAI create routes: /v1/chat/completionsopenai; /v1/responsesopenai-response.
  • OpenAI native pass-through routes are explicitly pinned to OpenAI-compatible providers and won't fall through to anthropic: chat/completions/*, responses/*, assistants*, threads*, conversations*, chatkit*, containers*, evals*, files*, uploads*, batches*, vector_stores*, images/*, audio/*, embeddings, moderations, completions, fine_tuning/*, realtime/*, skills*, videos*.
  • For responses/* resources, provider preference is openai-responseopenai; for other OpenAI native resources, provider preference is openaiopenai-response.
  • Other paths: choose the provider with the highest configured priority; tie-break is openai > openai-response > anthropic.
  • Cross-format fallback/conversion is controlled by upstreams[].convert_from_map (no global switch). If a provider has no eligible upstream for the inbound format, it won't be selected.
  • If openai is missing for /v1/chat/completions: fallback can be openai-response, anthropic, or gemini (priority-based; tie-break prefers openai-response).
  • For /v1/messages: choose between anthropic and kiro by priority; tie-break uses upstream id. If the chosen provider returns a retryable error, the proxy will fall back to the other native provider (Anthropic ↔ Kiro) when configured.
  • If neither anthropic nor kiro exists for /v1/messages: other providers can be selected only when allowed for anthropic_messages via convert_from_map (e.g. openai-response, openai, gemini).
  • If openai-response is missing for /v1/responses: fallback can be openai, anthropic, or gemini (priority-based; tie-break prefers openai).
  • If gemini is missing for /v1beta/models/*:generateContent or *:streamGenerateContent: fallback can be openai-response, openai, or anthropic (priority-based; tie-break prefers openai-response).
  • Other Gemini native endpoints are pass-through only and require a configured gemini upstream.

Auth rules (important)

  • Local access: local_api_key enabled → require format-specific key. Local auth inputs are reserved for gateway access and not used as upstream credentials.
    • Public whitelist: GET / HEAD /v1/models and /v1beta/openai/models do not require local key.
    • OpenAI / Responses: Authorization: Bearer <key>
    • Anthropic /v1/messages: x-api-key or x-anthropic-api-key
    • Gemini native API: x-goog-api-key or ?key=...
  • When local_api_key is enabled, inbound request auth headers are not collected for upstream; configure credential.api_keys (or an account credential) on the upstream instead.
  • Upstream auth resolution (per request; runtime expands credential.api_keys into the attempt key):
    • OpenAI-compatible (and most non-Anthropic providers): credential.api_keys → request x-openai-api-key / Authorization only when local_api_key is unset → no key.
    • Anthropic: credential.api_keys → request x-api-key / x-anthropic-api-key / bearer fallback only when local_api_key is unset → no key. Missing anthropic-version is auto-filled with 2023-06-01.
    • Gemini: credential.api_keys → request x-goog-api-key → query ?key=... (query/header fallback only when local_api_key is unset) → skip attempt.
    • Account-backed (kiro / codex / xai): uses the bound Provider Account identity (OAuth / Agent Assertion); not api_keys.

Load balancing & retries

  • Priorities: higher priority groups first.
  • upstream_strategy.order controls selection inside the same priority group:
    • fill_first: keep the configured list order.
    • round_robin: rotate the starting point across requests.
  • upstream_strategy.dispatch controls how requests are launched inside one priority group:
    • {"type":"serial"}: try one candidate at a time.
    • {"type":"hedged","delay_ms":2000,"max_parallel":2}: launch the first candidate immediately, then add one more attempt after delay_ms if the prior attempt is still unresolved, up to max_parallel.
    • {"type":"race","max_parallel":3}: launch up to max_parallel candidates immediately and take the first successful result.
  • Retryable conditions: network timeout/connect errors, or status 400/401/403/404/408/413/422/429/307/5xx (including 504/524). Non-context-window 413 responses skip same-upstream retry and fail over directly; context-window errors remain terminal.
  • Explicit Responses field rejections and xAI invalid encrypted reasoning use bounded same-identity request repair before ordinary retry/failover. Repair retries do not consume same_upstream_retry_count, and the repaired body is retained for later attempts.
  • Same-upstream retry: on a retryable failure, retry the same upstream up to same_upstream_retry_count extra times (default 1, excluding the first attempt) before failing over. After the first client-visible stream output, the proxy does not replay the same attempt.
  • Cooldown conditions: 401/403/408/429/5xx will temporarily move the failed upstream behind ready peers for retryable_failure_cooldown_secs (default 15); 400/404/422/307 stay retryable but do not trigger cross-request cooldown. With codex_session_scoped_cooldown_enabled=true, Codex account-backed OpenAI Responses cooldown is isolated by session_id; final successful requests do not keep same-session cooldown, and requests without session_id do not share cooldown.
  • /v1/messages only: after the chosen native provider is exhausted (retryable errors), the proxy can fall back to the other native provider (anthropickiro) if it is configured.

Observability

  • SQLite log: data.db in config dir. Stores per-request stats (tokens, cached tokens, latency, model, upstream).
  • Token rate: macOS tray shows live total or split rates (configurable via tray_token_rate).
  • Debug/trace log bodies capped at 64KiB.

Dashboard

  • In-app Dashboard page visualizes totals, token usage trend, model usage ranking (Top 20), and upstream model probes
  • Time range, upstream, and account filters apply to summary / series / models
  • Recent requests live on the Logs page (page size 50, offset supported)
  • The Logs panel supports a 30-second request-detail capture window: when enabled it stores request headers/bodies during that window, always keeps error responses for failed requests, and turns off automatically afterward.

One-click CLI setup

  • Claude Code: writes ~/.claude/settings.json env (ANTHROPIC_BASE_URL, ANTHROPIC_MODEL=claude-sonnet-4-6, ANTHROPIC_AUTH_TOKEN when local key is set).
  • Codex: writes ~/.codex/config.toml model="gpt-5.5", model_provider="token_proxy", and [model_providers.token_proxy].base_urlhttp://127.0.0.1:<port>/v1; writes ~/.codex/auth.json OPENAI_API_KEY.
  • A .token_proxy.bak file is created before overwriting; restart the CLI to apply.

FAQ

  • Port already in use? Change port in config.jsonc; remember to update your client base URL.
  • Got 401? If local_api_key is set, you must send the format-specific local key (OpenAI/Responses: Authorization, Anthropic: x-api-key, Gemini: x-goog-api-key or ?key=). With local auth enabled, configure upstream keys in upstreams[].credential (api_keys or account).
  • Got 504? Upstream did not send response headers or the first body chunk within 120s. For streaming responses, a 120s idle timeout between chunks may also close the connection.
  • 413 Payload Too Large? Body exceeded max_request_body_bytes (default 100 MiB) or the transform limit for format-conversion requests.
  • Why no system proxy? By design, reqwest is built with .no_proxy(); set per-upstream proxy_url if needed.

Files in the repo

Repository payload33 top-level entries
  • .agents
  • .github
  • .vscode
  • crates
  • docs
  • images
  • messages
  • project.inlang
  • public
  • scripts
  • src
  • src-tauri
  • .gitignore
  • AGENTS.md
  • Cargo.lock
  • Cargo.toml
  • CLAUDE.md
  • components.json
  • CONTEXT.md
  • design-qa.md
  • eslint.config.js
  • index.html
  • LICENSE
  • package.json
  • pnpm-lock.yaml
  • pnpm-workspace.yaml
  • README.md
  • README.zh-CN.md
  • tsconfig.json
  • tsconfig.node.json
  • tsr.config.json
  • vite.config.ts
  • vitest.config.ts

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More tools

JuliusBrussee/
caveman

🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman

105k
1 add
MemPalace/
mempalace

The best-benchmarked open-source AI memory system. And it's free.

59k
stablyai/
orca

Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.

66k

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

132k

Never stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors

64k
headroomlabs-ai/
headroom

Compress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.

71k