Skip to content

Repository files navigation

claude-code-router (Go)

Go Status Licence

A clean-room Go reimplementation of @musistudio/claude-code-router (Node.js, MIT). It is a small HTTP gateway that sits between Claude Code and roughly twenty third-party LLM providers: Claude Code speaks the Anthropic Messages API to it, and the gateway translates each request into OpenAI-style chat-completions for whichever upstream provider your config routes it to.

Project status. This repository is under active, parallel development. The tables below mark every component Implemented (code exists, is tested, and is described here from that code) or PLANNED (designed, but not yet in the repository). Do not treat a PLANNED item as available today. As of this pass, the CLI (cmd/ccr) exists and is real — see below.

Why a Go rewrite

  • No Node.js runtime required. The upstream router needs node/npm on the host; this one is a single static-ish Go binary, ccr (built from cmd/ccr).
  • Byte-compatible configuration. ~/.claude-code-router/config.json — including the capitalised "Providers"/"Router" keys — is read exactly as the Node version writes it, so an existing install (including configs generated by claude_toolkit's cma_run_provider) does not need to change (internal/config/config.go:1-19).
  • Same wire behaviour, memory-safer implementation. Anthropic → OpenAI translation, provider routing, and transport negotiation are reimplemented from observed behaviour (never from upstream source — see Upstream attribution), each backed by table-driven Go tests.
  • Modern transport stack out of the box. Gin, optional HTTP/3 over QUIC, and brotli→gzip→identity content-encoding negotiation (internal/gateway/gateway.go:1-18, internal/gateway/compress.go:39-81).

What is implemented today

Component Package Status
Config load/validate, byte-compatible config.json internal/config Implemented
Anthropic → OpenAI request translation internal/translate Implemented
cache_control stripping at any JSON depth (json.Number-safe) internal/translate Implemented
Provider/model routing (Router.default/background + haiku-tier detection) internal/router Implemented, and wired into the live gateway by cmd/ccr — see below
Upstream HTTP client (streaming-safe timeouts, secret-safe errors) internal/proxy Implemented, and wired into the live gateway by cmd/ccr — see below
Gateway listener: HTTP/1.1, HTTP/2, HTTP/3 (QUIC), brotli/gzip negotiation internal/gateway Implemented
GET /health, GET /ready internal/gateway Implemented
POST /v1/messages, incl. non-streaming and SSE-streaming responses, upstream error mapping internal/gateway (messages.go) Implemented
OpenAI → Anthropic response translation (buffered and SSE re-framing) internal/gateway/messages.go (not internal/translate — see docs/ARCHITECTURE.md) Implemented
CLI: ccr start / ui / serve (alias web) / stop, with pidfile-based background service management cmd/ccr Implemented
Separate management HTTP control-plane server (--host/--port, default 127.0.0.1:3458) cmd/ccr Implemented, deliberately minimal (own /health, a placeholder / page) — see docs/ADMIN_MANUAL.md
Structured logging (redacting slog logger + per-request access-log middleware) internal/logging, internal/gateway/logging_middleware.go Implemented and liveroutes() mounts LoggingMiddleware as the outermost middleware (internal/gateway/gateway.go:152); with cmd/ccr's nil Options.Logger it falls back to an env-configured (CCR_LOG_LEVEL/CCR_LOG_FORMAT) redacting logger, so every request is logged once (metadata only — never bodies or header values)
Router.longContext routing behaviour internal/router Implemented and live: router.Select routes to Router.LongContext when a request's estimated prompt token count exceeds DefaultLongContextThreshold (60000) and the route is configured (internal/router/router.go:130-175, internal/router/selector.go:95-138)
Router.think routing behaviour internal/router Implemented and live (v0.4.0): translate.AnthropicRequest now models Anthropic's thinking request field (internal/translate/anthropic.go:51-60), so a request that carries a non-null thinking block routes to Router.Think when it is configured (requestWantsThinking, internal/router/selector.go:150-174; chooseRoute, internal/router/router.go:164-175). No proxy signal is substituted — the signal is read strictly from the client's own thinking field
Explicit per-request provider/model selector ("provider,model"/"provider/model" in the request model field) internal/router Implemented, live in router.Select
Environment-variable outbound proxy (HTTP_PROXY/HTTPS_PROXY/NO_PROXY) internal/proxy Implemented, live for every proxy.Client
Authenticated custom outbound proxy internal/proxy, internal/config Implemented and wired: a top-level proxy block in config.json (ProxyConfigurl/username/password, all three required together, internal/config/config.go:163-185) is read by Server.WireDefaults, which builds the upstream client via proxy.NewWithUpstreamProxy when Config.Proxy is set, routing every provider request through it with HTTP Basic auth to the proxy and overriding ambient HTTP_PROXY/HTTPS_PROXY (internal/gateway/wiring.go:72-90). Absent/nil (the default) leaves the env-only behaviour unchanged. A partial block is rejected at load (Config.Validate) and by ccr config validate (internal/config/config.go:358-368, internal/config/validate_cmd.go). The password is redacted to [REDACTED] by ccr config show (internal/config/validate_cmd.go). See docs/USER_GUIDE.md and docs/ADMIN_MANUAL.md
Retry/fallback on a failed upstream call internal/gateway, internal/router Implemented and live: handleMessages now drives a real retry loop (doUpstreamWithRetry, internal/gateway/messages.go) against internal/router/fallback.go's classification/backoff policy — up to Options.MaxAttempts (default 3) upstream attempts per request, never retrying a Terminal failure. The attempt budget is now CLI/env-configurable: --max-attempts <n> / CCR_MAX_ATTEMPTS (must be >= 1, cmd/ccr/flags.go)
Response cache (optional, off by default): exact-match responses served locally on a HIT with no upstream call internal/cache, internal/gateway, internal/config Implemented and live: an optional top-level Cache block (internal/config/config.go:152-171) builds a memory (LRU) or sqlite store via gateway.BuildCache (internal/gateway/gateway.go:129-149), wired by cmd/ccr serve (cmd/ccr/serve.go:46-65); lookup/store live in internal/gateway/messages.go:287-338. Gated to non-streaming, OpenAI-shaped requests; never caches temperature>0, streaming, tool-call (unless opted in), or error responses (internal/cache/gate.go). See docs/USER_GUIDE.md §8
Semantic cache tier + deterministic LocalEmbedder (a lexical near-duplicate approximation, not a learned model) internal/cache, internal/config, internal/gateway Implemented and live (v0.4.0), off by default: Cache.semantic (+ optional Cache.semantic_threshold, default 0.85) wraps the exact tier in a SemanticCache driven by LocalEmbedder (gateway.BuildCache, internal/gateway/gateway.go:194-235). Exact-first, scope-isolated, bounded, short-turn-guarded. Feature-hashing of character trigrams, L2-normalised for cosine similarity — catches re-asks/near-duplicates, not deep paraphrase. See docs/USER_GUIDE.md §8.4
Prometheus /metrics endpoint (self-contained, no client_golang) on the management server internal/metrics, cmd/ccr, internal/gateway Implemented and live (v0.4.0): a text-exposition /metrics on the loopback management server (127.0.0.1:3458), fed by a shared metrics.Recorder the gateway records into (cmd/ccr/serve.go:45-62, cmd/ccr/management.go:39-41, internal/gateway/gateway.go:375-393, internal/gateway/messages.go). Families: ccr_http_requests_total, ccr_http_request_duration_seconds, ccr_http_inflight_requests, ccr_gen_ai_upstream_requests_total, ccr_gen_ai_input_tokens_total, ccr_gen_ai_output_tokens_total, ccr_gen_ai_cache_lookups_total. Labels are bounded and secret-free. See docs/ADMIN_MANUAL.md
Cross-provider fallback (advance to the next provider that serves the same model on a retryable failure) internal/router v0.3.0 — opt-in schema + policy landing this release: Router.crossProviderFallback (bool) + an explicit Router.fallback ([]string) chain. The planning primitives are implemented and unit-tested (internal/router/plan.go, internal/router/fallback.go); the gateway consumes them in v0.3.0
Inbound gateway authentication internal/gateway gateway.RequireAPIKey is mounted on all four completion routes — /v1/messages, /proxy/v1/messages, /v1/chat/completions, /proxy/v1/chat/completions (route-scoped; /health//ready stay always-open) — and is now operator-configurable via a repeatable --api-key <key> flag or the comma-separated CCR_API_KEYS env var (cmd/ccr/flags.go; a flag value replaces the env list). The accepted-key list still defaults to empty, so a CLI-launched gateway remains unauthenticated by default unless an operator opts in — same backward-compatible default as before, now with a switch
Image (vision) content blocks internal/translate Implemented: base64 or URL image sources convert to OpenAI image_url content parts, including inside tool_result content (computer-use screenshots); an unsupported media type or malformed source is a named error, not a silent drop
OpenAI chat-completions inbound (an OpenAI-SDK client can POST /v1/chat/completions) internal/gateway (openai_inbound.go, protocol.go) Implemented — served alongside /v1/messages via a path→protocol classifier; near-passthrough to OpenAI-shaped providers (501 if routed to an Anthropic-native one)
OpenAI Responses / Gemini inbound, ToolHub/MCP, billing, rules DSL, hosted web search OpenAI Responses & Gemini are recognised by the path classifier but not served (no route → 404); the rest is out of scope by design — see test/PORTING-MATRIX.md and docs/FAQ.md

internal/gateway/messages.go deliberately does not import internal/router or internal/proxy directly — it defines its own narrow Router/Upstream interfaces with minimal in-package default implementations, so the gateway package compiles and serves correctly standalone (internal/gateway/messages.go:30-50). A separate file, internal/gateway/wiring.go, adapts the real internal/router.Select and internal/proxy.Client onto those same interfaces and exposes Server.WireDefaults(timeout) to install them. cmd/ccr calls WireDefaults on every gateway it starts (cmd/ccr/serve.go:44-51) — so a gateway launched via ccr start/ui/serve gets the full haiku-tier-aware routing and streaming-safe upstream client, not the minimal built-ins. The minimal built-ins only matter if you construct gateway.New directly as a library, without also calling WireDefaults — see docs/ARCHITECTURE.md for the full picture.

Install

v0.1.0 is tagged and published as a GitHub release (cross-compiled linux/darwin/windows × amd64/arm64 archives + checksums — see docs/RELEASE.md). Either download an artifact from that release, or build from source:

git clone https://github.com/vasic-digital/claude-code-router.git
cd claude-code-router
make build      # or: go build -o bin/ccr ./cmd/ccr
make test        # or: go test ./...

A Makefile at the repository root wraps the common local workflows — make help lists them all (build, test, test-race, fuzz, bench, lint, cover, cross-compile, install, clean). There is deliberately no hosted CI/CD in this repository; every target is meant to be run locally or from a git hook (Makefile:1-21). .gitignore reserves /bin/ and /dist/ for build output (.gitignore:1-2), matching make build's and make cross-compile's output locations.

A multi-stage Dockerfile also ships at the repository root — see docs/ADMIN_MANUAL.md §1.2 for build/run instructions and its important loopback-binding caveat.

Quick start

# 1. Write ~/.claude-code-router/config.json (see "Configuration" below).

# 2. Start the router as a background service (pidfile + log under
#    ~/.claude-code-router/), gateway on 127.0.0.1:3456 by default.
./bin/ccr start

# 3. Point Claude Code at the gateway and use it as normal — every
#    request it sends is translated and routed by the gateway.
ANTHROPIC_BASE_URL=http://127.0.0.1:3456 claude

# 4. Stop it later.
./bin/ccr stop

ccr serve (alias web) runs the same thing in the foreground instead of detaching — useful under a supervisor like systemd (see docs/ADMIN_MANUAL.md). ccr ui is identical to start but also opens the management interface in a browser.

Configuration

The config file lives at ~/.claude-code-router/config.json on Linux/macOS, or %APPDATA%\claude-code-router\config.json on Windows when APPDATA is set (internal/config/config.go:148-162). A missing file is not an error — the gateway boots with zero providers and reports that via /health//ready rather than refusing to start; malformed JSON, or JSON that fails validation, is an error (internal/config/config.go:170-186).

This is the exact shape claude_toolkit's cma_run_provider writes at launch time (internal/config/config_test.go:21-35 — the toolkit-compatibility fixture the test suite pins against):

{
  "Providers": [
    {
      "name": "chutes",
      "api_base_url": "https://llm.chutes.ai/v1/chat/completions",
      "api_key": "sk-test",
      "models": ["zai-org/GLM-5.2-TEE", "Qwen/Qwen3.6-27B-TEE"],
      "transformer": {"use": ["cleancache", "streamoptions"]}
    }
  ],
  "Router": {
    "default": "chutes,zai-org/GLM-5.2-TEE",
    "background": "chutes,Qwen/Qwen3.6-27B-TEE"
  }
}

Key points, all enforced by Config.Validate() (internal/config/config.go:190-233):

  • Providers/Router are capitalised in JSON — not idiomatic Go, kept intentionally so existing installs never need to change (internal/config/config.go:16-18).
  • api_base_url must be the complete chat-completions URL (https://…/chat/completions), not a bare host — the gateway posts to it verbatim and never appends a path (internal/config/config.go:48-51, internal/proxy/proxy.go:49-53).
  • Every provider name must be non-empty and unique.
  • A Router route is a single string "provider,model"; only the first comma splits provider from model, so a model id may itself contain commas (internal/config/config.go:239-249).
  • Router also accepts think and longContext alongside default/background (all validated the same way). longContext drives routing in production — a request whose estimated prompt exceeds DefaultLongContextThreshold (60000 tokens) is routed there when it is set. As of v0.4.0 think routing is live too: a request carrying a non-null Anthropic thinking field routes to Router.think when it is configured — see the rows above.
  • Each provider may optionally set "protocol": "openai" (the default) or "protocol": "anthropic". When the field is absent the protocol is inferred from api_base_url — an api.anthropic.com/*.anthropic.com host, or an /anthropic path segment, resolves to anthropic; everything else to openai (internal/config/config.go:87-130). An anthropic provider receives the Anthropic-shaped request unchanged (no OpenAI translation) and its response is relayed back verbatim, so a provider can be pointed straight at a real Anthropic Messages endpoint. Any value other than openai/anthropic is a hard validation error (internal/config/config.go:206-215).
  • An optional top-level Cache block enables a response cache; it is off by default (an absent/nil block, or "enabled": false, leaves the request path byte-identical to a build with no cache). When on, an exact-match HIT is served locally with no upstream call. Caching applies only to non-streaming requests routed to an OpenAI-shaped provider, and never caches temperature>0, streaming, tool-call (unless allow_tool_responses), or error responses. Fields: enabled, backend ("memory" LRU or "sqlite"; path required for sqlite), ttl_seconds, max_entries, allow_tool_responses (internal/config/config.go:152-171). Changing the block requires a restart — hot-reload validates and logs it but does not swap the live gateway. See docs/USER_GUIDE.md §8 and docs/ADMIN_MANUAL.md §6.1.
  • v0.3.0 adds two optional, opt-in Router fields for cross-provider fallback (both default off/absent → today's single-provider retry, byte-for-byte): "crossProviderFallback": true advances to the next configured provider that also serves the model when the primary fails with a retryable class (5xx/429/transport) after its same-provider retries — a terminal failure (400/401/404, i.e. a bad request or credentials) never falls back, because it would fail everywhere; and "fallback": ["provider,model", …] is an optional explicit ordered chain tried before the auto-discovered same-model providers. See docs/USER_GUIDE.md §9.

See docs/USER_GUIDE.md for a full provider walkthrough and docs/FAQ.md for the reasoning behind each of these rules.

CLI grammar

cmd/ccr is a clean-room reimplementation of the upstream @musistudio/claude-code-router v3.0.6 CLI grammar (cmd/ccr/main.go:1-19) — claude_toolkit shells out to this binary and greps ccr --help for the exact substrings ccr start and ccr serve to confirm it's talking to a compatible router, so this usage text is load-bearing, not decorative, and is reproduced verbatim from cmd/ccr/main.go:28-79:

ccr - Claude Code Router

Usage:
  ccr start [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
  ccr ui    [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
  ccr serve [--host <host>] [--port <port>] [--open|--no-open] [--gateway|--no-gateway]
  ccr stop
  ccr <profile-name-or-id> [cli|app] [-- <agent args>]

Commands:
  start   Start the router service in the background (writes a pidfile).
  ui      Start the service and open the management UI in a browser.
  serve   Run the router service in the foreground. Alias: web.
  web     Alias for serve.
  stop    Stop the background service started with "start" (or "ui").

Flags (start, ui, serve, web):
  --host <host>            Management interface host (default 127.0.0.1, env CCR_WEB_HOST)
  --port <port>            Management interface port (default 3458, env CCR_WEB_PORT)
  --open, --no-open        Open (or don't open) the management UI in a browser
  --gateway, --no-gateway  Start (or don't start) the Anthropic-compatible gateway
                            (default: on, port 3456)
  --gateway-port <port>    Gateway port (default 3456, env CCR_GATEWAY_PORT).
                            Distinct from --port, which sets the management
                            interface. Use this when 3456 is already taken.
  --gateway-host <host>    Gateway bind address (default 127.0.0.1, env
                            CCR_GATEWAY_HOST). Loopback-only by default
                            because the gateway holds live provider API keys.
                            Set 0.0.0.0 inside a container, where 127.0.0.1
                            is the container's own loopback and a published
                            port can never reach it.
  --tls-cert <path>        PEM certificate for the gateway TLS listener (env
                            CCR_TLS_CERT). Serves HTTPS (HTTP/2 over TLS via
                            ALPN) instead of cleartext HTTP. Must be paired
                            with --tls-key.
  --tls-key <path>         PEM private key for --tls-cert (env CCR_TLS_KEY).
  --http3, --no-http3      Advertise and serve HTTP/3 (QUIC) on the gateway
                            alongside TLS (env CCR_HTTP3). Requires --tls-cert
                            and --tls-key — QUIC has no cleartext mode.
  --api-key <key>          Accept this key for INBOUND gateway auth (repeatable;
                            env CCR_API_KEYS = comma-separated list). Enforced on
                            the completion routes via Authorization: Bearer <key>
                            or x-api-key; /health and /ready are never gated.
                            Default (none) leaves the gateway unauthenticated.
                            Prefer CCR_API_KEYS over the flag — a flag value is
                            visible in the process list. Keys may not contain a
                            comma (the env-list separator).
  --max-attempts <n>       Upstream retry budget (env CCR_MAX_ATTEMPTS). Must be
                            >= 1; default 3.

  -h, --help                Show this help

Notes, all grounded in cmd/ccr/*.go:

  • --host/--port configure the management control-plane server; --gateway-host/--gateway-port configure the Anthropic-compatible gateway — the two servers have always been independent, but until this pass the gateway's address was hardcoded (cmd/ccr/flags.go:9-43). CCR_WEB_HOST/CCR_WEB_PORT and CCR_GATEWAY_HOST/CCR_GATEWAY_PORT set the same values via environment; an explicit flag always wins over its environment variable (cmd/ccr/flags.go:50-129, tested at cmd/ccr/flags_test.go:46-65, cmd/ccr/gatewayhost_test.go, cmd/ccr/gatewayport_test.go).
    • Why the gateway defaults to loopback (127.0.0.1) and not 0.0.0.0: it holds live provider API keys in config.json and sends them upstream on every request — binding it to every interface must be a deliberate, explicit act, never the default (cmd/ccr/flags.go:38-43).
    • Why a container needs --gateway-host 0.0.0.0: inside a container, 127.0.0.1 is the container's own loopback interface — a published port (docker run -p 3456:3456) can never reach a process bound only to that address, no matter how the port is published. Pass --gateway-host 0.0.0.0 (or -e CCR_GATEWAY_HOST=0.0.0.0) explicitly when the gateway needs to be reachable from outside its container; see docs/ADMIN_MANUAL.md §1.2 for the full picture, including the stock Dockerfile's current CMD, which does not pass this yet.
    • --gateway-host/--gateway-port are now forwarded by ccr start/ui. Those two commands re-exec themselves as ccr serve in a detached child process; the code that builds the child's argument list, serveChildArgs (cmd/ccr/service.go:196-234), forwards --host, --port, --gateway/--no-gateway, --open/--no-open, --gateway-host, --gateway-port, the TLS/HTTP3 flags (--tls-cert/--tls-key/--http3/--no-http3), and --max-attempts (when set) — previously only the first four survived, so e.g. ccr start --tls-cert … silently served plaintext. Inbound API keys are the one exception, and deliberately so: they travel to the child via the inherited CCR_API_KEYS environment (cmd/ccr/service.go:107-114), never via argv, because argv is visible to any local user via ps. The environment-variable form of every other flag also still survives, since the detached child inherits the parent's environment regardless.
  • start/ui re-exec the current binary as serve in a fully detached child process (new session via setsid on Unix — cmd/ccr/spawn_unix.go), record its PID/host/port/gateway-flag/start-time in ~/.claude-code-router/service.json, and return immediately; the child's stdout/stderr go to ~/.claude-code-router/service.log (cmd/ccr/service.go:16-27, 77-143). Running start a second time while already running is refused with the running PID reported, not silently started twice (cmd/ccr/service.go:93-96).
  • stop reads that pidfile, sends SIGTERM, waits up to 5 seconds (polling every 100 ms), then SIGKILLs if it hasn't exited, and removes the pidfile either way; a stale pidfile pointing at a dead process is cleaned up and reported rather than claimed as a successful stop (cmd/ccr/service.go:145-184).
  • serve/web run in the foreground: start the gateway (unless --no-gateway) with WireDefaults applied, start the management server (always), optionally open a browser at the management URL (--open), then block until SIGINT/SIGTERM and shut both servers down gracefully within a 10-second grace period (cmd/ccr/serve.go:16-96).
  • Any other first argument is treated as a profile name/id. Since this reimplementation does not (yet) carry a profile store, every such invocation prints Profile "<name>" was not found or is disabled. to stderr and exits 1 — this is a deliberate, tested match for the upstream CLI's observed behaviour on an unknown profile, not an oversight (cmd/ccr/main.go:88-94, tested at cmd/ccr/main_test.go:43-65).
  • -h, --help, help, and no arguments at all all print the same usage text and exit 0 (cmd/ccr/main.go:71-79, tested at cmd/ccr/main_test.go:26-37).
  • ccr config validate [path] and ccr config show [path] are also implemented (cmd/ccr/config_cmd.go, dispatched at cmd/ccr/main.go:88-89), even though they are not part of the usage text above (which is pinned to the upstream v3.0.6 grammar claude_toolkit greps). validate loads a config and reports every structural problem in a single pass — exit 0 iff valid, 1 otherwise (config.LoadForValidation + config.CheckAll). show prints the effective config as indented JSON with every provider's api_key replaced by the fixed marker [REDACTED] — the real key's bytes are never marshalled at all, so it is safe to paste into a bug report (internal/config/validate_cmd.go). path defaults to the same ~/.claude-code-router/config.json that serve reads.

The gateway's transport knobs — host/port, TLS/HTTP3, retry attempts, and inbound API keys — are all exposed as internal/gateway.Options (internal/gateway/gateway.go:35-74) and, as of this pass, all reachable from the CLI. cmdServe constructs gateway.Options{Host: flags.GatewayHost, Port: flags.GatewayPort, CertFile: flags.TLSCert, KeyFile: flags.TLSKey, EnableHTTP3: flags.HTTP3, APIKeys: flags.APIKeys, MaxAttempts: flags.MaxAttempts} (cmd/ccr/serve.go), fed by the flags and env vars below. Only UpstreamTimeout remains reachable solely as a library default:

internal/gateway.Options field Default CLI-exposed?
Host 127.0.0.1 Yes--gateway-host / CCR_GATEWAY_HOST
Port 3456 Yes--gateway-port / CCR_GATEWAY_PORT
CertFile, KeyFile unset Yes--tls-cert / CCR_TLS_CERT and --tls-key / CCR_TLS_KEY (must be provided together, or serve exits 2)
EnableHTTP3 false Yes--http3 / --no-http3 / CCR_HTTP3 (requires --tls-cert/--tls-key; serve exits 2 without them)
UpstreamTimeout 10 minutes No — PLANNED (also see the differing streaming/non-streaming semantics note in docs/FAQ.md Q18)
MaxAttempts 3 (1 initial try + 2 retries) Yes--max-attempts <n> / CCR_MAX_ATTEMPTS (must be >= 1; cmd/ccr/flags.go)
APIKeys empty (auth disabled) Yes — repeatable --api-key <key> / comma-separated CCR_API_KEYS (a flag value replaces the env list; cmd/ccr/flags.go); RequireAPIKey is mounted on all four completion routes, still disabled by default — see "Known limitations"

Feature table

Feature Status Where
Anthropic Messages request → OpenAI chat-completions translation Implemented internal/translate/anthropic.go:338-473
System prompt (top-level → leading system message) Implemented internal/translate/anthropic.go:355-365
Polymorphic content (string or block array) flattening Implemented internal/translate/anthropic.go:167-180
tool_usemessage.tool_calls (string-encoded arguments) Implemented internal/translate/anthropic.go:392-400
tool_result → separate role:"tool" message Implemented internal/translate/anthropic.go:291-335, 401-408
Image content blocks (base64 or URL, incl. inside tool_result) → OpenAI image_url parts Implemented internal/translate/anthropic.go:237-335, 409-416
Provider protocol field ("openai"/"anthropic", optional; inferred from api_base_url when absent) + Anthropic-native passthrough (Anthropic-shaped request/response relayed unchanged to a Messages-API upstream) Implemented and live internal/config/config.go:31-130 (ResolvedProtocol), internal/translate/anthropic.go:495-516 (AnthropicPassthrough), wired at internal/gateway/messages.go:233-295
cache_control stripping at every JSON depth (number-literal-safe via json.Number) Implemented internal/translate/anthropic.go:538-585
stream_options.include_usage injection Implemented internal/translate/anthropic.go:351-353
Empty tool-parameter-schema backfill (EnsureToolParameters) Implemented — always on in the live handler internal/translate/anthropic.go:441-464, internal/gateway/messages.go:248-256
Provider/model routing incl. haiku-tier background routing Implemented, wired into the CLI-launched gateway via WireDefaults internal/router/router.go:51-97, internal/gateway/wiring.go:29-44
Bare-model resolution (a bare model id served by exactly one provider resolves to it when no Router.default is set; two+ providers → loud ambiguity error; Router.default always wins) Implemented internal/router/router.go:72-86, internal/router/selector.go (resolveBareModel)
Upstream HTTP client, streaming-safe timeout, no-secret-leak errors Implemented, wired into the CLI-launched gateway via WireDefaults internal/proxy/proxy.go:65-84, internal/gateway/wiring.go:46-71
Retry loop on a failed upstream call (respects Retryable/Terminal classification and backoff) Implemented and live internal/gateway/messages.go:319-416 (doUpstreamWithRetry), internal/router/fallback.go
Response cache (exact tier): non-streaming OpenAI HIT served locally with no upstream call; memory/sqlite; gated on temperature/streaming/tool/error Implemented and live (off by default) internal/cache, internal/gateway/messages.go:287-338, internal/gateway/gateway.go:129-149, internal/cache/gate.go
Deterministic LocalEmbedder for the semantic cache tier (lexical near-duplicate, not a learned model) Implemented (seam) internal/cache/embedder_local.go, internal/cache/semantic.go
Cross-provider fallback (Router.crossProviderFallback + Router.fallback chain) v0.3.0 — schema + policy this release; planning primitives implemented and unit-tested internal/router/plan.go, internal/router/fallback.go
HTTP/1.1, HTTP/2 Implemented internal/gateway/gateway.go:212-245
HTTP/3 (QUIC), TLS-gated Implemented; CLI-exposed via --http3 (requires --tls-cert/--tls-key) internal/gateway/gateway.go:226-229, cmd/ccr/flags.go:146-161
brotli → gzip → identity content negotiation Implemented internal/gateway/compress.go:39-81
GET /health, GET /ready Implemented internal/gateway/gateway.go:160-182
POST /v1/messages: non-streaming request/response translation Implemented internal/gateway/messages.go:189-296, 538-608
POST /v1/messages: SSE streaming (OpenAI chunks → Anthropic events) Implemented internal/gateway/messages.go:621-763
POST /v1/messages: upstream error → Anthropic error-shape mapping, preserving status code Implemented internal/gateway/messages.go:448-504
Inbound API-key check (RequireAPIKey, route-scoped on all four completion routes) Mounted and CLI-configurable via --api-key/CCR_API_KEYS; accepted-key list defaults to empty (auth disabled) internal/gateway/gateway.go:362-367, internal/gateway/auth.go, cmd/ccr/flags.go
OpenAI chat-completions inbound facade (POST /v1/chat/completions, /proxy/v1/... aliases) + path→protocol classifier dispatch (handleInbound) Implemented — near-passthrough to OpenAI-shaped providers; 501 if routed to an Anthropic-native provider internal/gateway/openai_inbound.go, internal/gateway/protocol.go, internal/gateway/gateway.go:362-367
CLI (start/ui/serve/web/stop, pidfile service management) Implemented cmd/ccr/*.go
Separate management control-plane server (own /health) Implemented, deliberately minimal cmd/ccr/management.go
Structured logging + per-request access logging Implemented and live (mounted in routes(); env-configured via CCR_LOG_LEVEL/CCR_LOG_FORMAT) internal/logging, internal/gateway/logging_middleware.go, internal/gateway/gateway.go:152
ccr config validate / ccr config show (api_key-redacting) Implemented cmd/ccr/config_cmd.go, internal/config/validate_cmd.go

Known limitations

An honest, current accounting of what does not yet work the way you might expect, kept in sync with the code (see docs/DOC-AUDIT.md for how this list was derived and re-verified):

  • Inbound gateway authentication now has an operator-facing switch, but it is still off by default. gateway.RequireAPIKey is mounted as route-scoped middleware on all four completion routes — /v1/messages, /proxy/v1/messages, /v1/chat/completions, /proxy/v1/chat/completions (internal/gateway/gateway.go:362-367) — GET /health/GET /ready are deliberately never gated, so liveness/readiness probing keeps working regardless of auth configuration. cmd/ccr now populates Options.APIKeys from a repeatable --api-key <key> flag or the comma-separated CCR_API_KEYS environment variable (cmd/ccr/flags.go; a flag value replaces the env list wholesale). But the accepted-key list still defaults to empty, which RequireAPIKey's own documented behaviour treats as "authentication disabled" — a CLI-launched gateway is therefore still unauthenticated by default today, unless an operator explicitly configures keys. Prefer CCR_API_KEYS (or, for ccr start/ui, its environment form) over the --api-key flag: a flag value is visible in the process list (ps), while start/ui forward configured keys to the detached serve child only via the inherited environment, never argv (cmd/ccr/service.go:107-114). Anyone who can reach the gateway's port with no keys configured can send it requests billed to your configured provider keys. Mitigate by configuring CCR_API_KEYS, keeping the gateway on loopback (the default), or putting an authenticating reverse proxy in front — see docs/ADMIN_MANUAL.md §5.
  • Router.think routing applies only to Anthropic-inbound requests. It is live as of v0.4.0: a POST /v1/messages request carrying a non-null thinking field routes to Router.think when configured (internal/router/selector.go, internal/router/router.go:130-175). The OpenAI-inbound facade (POST /v1/chat/completions) has no equivalent field (OpenAI chat-completions carries no thinking), so think is not derivable there and such a request routes on model/size alone. Router.longContext, by contrast, now applies to BOTH inbound paths (v0.4.5): the facade estimates its own request size via routingRequestFromOpenAI (measuring message text + tools; image_url data-URIs excluded), so a large /v1/chat/completions prompt trips Router.longContext symmetrically with /v1/messages, when it is configured and the estimate exceeds DefaultLongContextThreshold (60000 tokens).

Several items originally tracked as limitations were closed while this documentation pass was in progress — an authenticated outbound proxy is now configurable via the proxy config block (see the feature table above) and no longer belongs on this list; the fallback/retry classifiers gained a real retry loop, vision/image blocks convert to image_url instead of erroring, the provider protocol field + Anthropic-native passthrough landed, and unambiguous bare-model resolution (in the no-Router.default window) landed. --gateway-host/--gateway-port (plus TLS/HTTP3/--max-attempts) are now forwarded by ccr start/ui, and --api-key/CCR_API_KEYS plus --max-attempts/CCR_MAX_ATTEMPTS gave the retry budget and inbound auth their first operator-facing switches. They are documented as implemented above and in docs/DOC-AUDIT.md.

Documentation

  • docs/USER_GUIDE.md — install, configure, run, provider walkthrough, TLS/HTTP3, troubleshooting.
  • docs/ADMIN_MANUAL.md — deployment, TLS, firewall, logs, backup/restore, upgrade/rollback, hardening.
  • docs/FAQ.md — 20+ code-grounded Q&A.
  • docs/API.md — endpoint reference, schemas, status codes, headers.
  • docs/ARCHITECTURE.md — component/data-flow diagrams (Mermaid).
  • docs/DOC-AUDIT.md — the documentation-accuracy audit behind this pass: every claim checked against the code, with verdicts and evidence.
  • web/index.html — self-contained interactive micro-site (open directly from disk).

Upstream attribution

This is a clean-room reimplementation of musistudio/claude-code-router (Node.js, MIT licence). No upstream source was copied; the wire formats, CLI grammar, configuration file layout, and default ports are reproduced deliberately so existing installations remain compatible (NOTICE:1-11). The upstream MIT licence text is retained verbatim in LICENSE-UPSTREAM-MIT for attribution.

This repository does not currently contain its own top-level LICENSE file for the Go code — only LICENSE-UPSTREAM-MIT (the retained upstream text). Treat the licensing terms of the Go implementation itself as to be confirmed.

Related

  • claude_toolkit — the multi-account Claude Code toolkit whose claude-providers.sh/cma_run_provider already write config.json in the exact shape this router reads (internal/config/config_test.go:18-20).

About

Claude Code Router in Go — HTTP/3+QUIC gateway with brotli, HTTP/2+gzip fallback. Anthropic↔OpenAI translation for multi-provider Claude Code.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages