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.
- No Node.js runtime required. The upstream router needs
node/npmon the host; this one is a single static-ish Go binary,ccr(built fromcmd/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 byclaude_toolkit'scma_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).
| 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 live — routes() 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 (ProxyConfig — url/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.
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.
# 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 stopccr 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.
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/Routerare capitalised in JSON — not idiomatic Go, kept intentionally so existing installs never need to change (internal/config/config.go:16-18).api_base_urlmust 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
namemust be non-empty and unique. - A
Routerroute 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). Routeralso acceptsthinkandlongContextalongsidedefault/background(all validated the same way).longContextdrives routing in production — a request whose estimated prompt exceedsDefaultLongContextThreshold(60000 tokens) is routed there when it is set. As of v0.4.0thinkrouting is live too: a request carrying a non-null Anthropicthinkingfield routes toRouter.thinkwhen 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 fromapi_base_url— anapi.anthropic.com/*.anthropic.comhost, or an/anthropicpath segment, resolves toanthropic; everything else toopenai(internal/config/config.go:87-130). Ananthropicprovider 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 thanopenai/anthropicis a hard validation error (internal/config/config.go:206-215). - An optional top-level
Cacheblock enables a response cache; it is off by default (an absent/nilblock, 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 cachestemperature>0, streaming, tool-call (unlessallow_tool_responses), or error responses. Fields:enabled,backend("memory"LRU or"sqlite";pathrequired 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. Seedocs/USER_GUIDE.md§8 anddocs/ADMIN_MANUAL.md§6.1. - v0.3.0 adds two optional, opt-in
Routerfields for cross-provider fallback (both default off/absent → today's single-provider retry, byte-for-byte):"crossProviderFallback": trueadvances 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. Seedocs/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.
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/--portconfigure the management control-plane server;--gateway-host/--gateway-portconfigure 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_PORTandCCR_GATEWAY_HOST/CCR_GATEWAY_PORTset the same values via environment; an explicit flag always wins over its environment variable (cmd/ccr/flags.go:50-129, tested atcmd/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 not0.0.0.0: it holds live provider API keys inconfig.jsonand 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.1is 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; seedocs/ADMIN_MANUAL.md§1.2 for the full picture, including the stockDockerfile's currentCMD, which does not pass this yet. --gateway-host/--gateway-portare now forwarded byccr start/ui. Those two commands re-exec themselves asccr servein 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 inheritedCCR_API_KEYSenvironment (cmd/ccr/service.go:107-114), never via argv, because argv is visible to any local user viaps. The environment-variable form of every other flag also still survives, since the detached child inherits the parent's environment regardless.
- Why the gateway defaults to loopback (
start/uire-exec the current binary asservein a fully detached child process (new session viasetsidon 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). Runningstarta second time while already running is refused with the running PID reported, not silently started twice (cmd/ccr/service.go:93-96).stopreads that pidfile, sendsSIGTERM, waits up to 5 seconds (polling every 100 ms), thenSIGKILLs 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/webrun in the foreground: start the gateway (unless--no-gateway) withWireDefaultsapplied, start the management server (always), optionally open a browser at the management URL (--open), then block untilSIGINT/SIGTERMand 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 exits1— 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 atcmd/ccr/main_test.go:43-65). -h,--help,help, and no arguments at all all print the same usage text and exit0(cmd/ccr/main.go:71-79, tested atcmd/ccr/main_test.go:26-37).ccr config validate [path]andccr config show [path]are also implemented (cmd/ccr/config_cmd.go, dispatched atcmd/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 grammarclaude_toolkitgreps).validateloads a config and reports every structural problem in a single pass — exit0iff valid,1otherwise (config.LoadForValidation+config.CheckAll).showprints the effective config as indented JSON with every provider'sapi_keyreplaced 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).pathdefaults to the same~/.claude-code-router/config.jsonthatservereads.
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 | 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_use → message.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 |
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.RequireAPIKeyis 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 /readyare deliberately never gated, so liveness/readiness probing keeps working regardless of auth configuration.cmd/ccrnow populatesOptions.APIKeysfrom a repeatable--api-key <key>flag or the comma-separatedCCR_API_KEYSenvironment variable (cmd/ccr/flags.go; a flag value replaces the env list wholesale). But the accepted-key list still defaults to empty, whichRequireAPIKey'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. PreferCCR_API_KEYS(or, forccr start/ui, its environment form) over the--api-keyflag: a flag value is visible in the process list (ps), whilestart/uiforward configured keys to the detachedservechild 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 configuringCCR_API_KEYS, keeping the gateway on loopback (the default), or putting an authenticating reverse proxy in front — seedocs/ADMIN_MANUAL.md§5. Router.thinkrouting applies only to Anthropic-inbound requests. It is live as of v0.4.0: aPOST /v1/messagesrequest carrying a non-nullthinkingfield routes toRouter.thinkwhen 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 nothinking), sothinkis 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 viaroutingRequestFromOpenAI(measuring message text + tools;image_urldata-URIs excluded), so a large/v1/chat/completionsprompt tripsRouter.longContextsymmetrically with/v1/messages, when it is configured and the estimate exceedsDefaultLongContextThreshold(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.
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).
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.
claude_toolkit— the multi-account Claude Code toolkit whoseclaude-providers.sh/cma_run_provideralready writeconfig.jsonin the exact shape this router reads (internal/config/config_test.go:18-20).