This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
make build # Build binary to bin/routatic-proxy (CGO disabled by default)
make run # Run without building
make test # Run tests with race detector
make lint # go vet + test
make clean # Remove build artifacts
make install # Build and install to $GOPATH/bin
make dist # Cross-compile for all platforms
# Start proxy with dashboard (recommended)
./bin/routatic-proxy start
# Start proxy only (headless)
./bin/routatic-proxy serveroutatic-proxy start runs both the proxy server and GUI dashboard:
- Proxy listens on
127.0.0.1:3456(configurable) - Dashboard at
http://127.0.0.1:3445 - Usage data persists to SQLite (
~/.local/share/routatic-proxy/data.db) regardless of dashboard state - Press Ctrl+C to stop both servers
routatic-proxy serve runs headless (no dashboard).
Purpose: routatic-proxy is a proxy server that sits between Claude Code and OpenCode Go. It intercepts Anthropic API requests, transforms them to OpenAI Chat Completions format, forwards them to OpenCode Go, and transforms responses back to Anthropic SSE.
Model routing is config-driven, not code-driven. All models are defined in ~/.config/routatic-proxy/config.json — adding a new model requires no code changes. Go provider models are transformed to OpenAI Chat Completions format automatically. Zen models use endpoint classification via ClassifyEndpoint(). The router in internal/router/ selects models by matching request content against scenario patterns defined in scenarios.go.
If a model's upstream doesn't support Anthropic tool format (type: "custom" server-tool shorthands), set "anthropic_tools_disabled": true in the model config to force it through the Chat Completions transform path instead of the raw Anthropic endpoint.
Two API endpoints:
- OpenAI endpoint (
/v1/chat/completions) — used by most models (GLM, Kimi, MiMo, Qwen) - Anthropic endpoint (
/v1/messages) — used only by MiniMax models
Available models:
| Model | Provider | Type | Best For |
|---|---|---|---|
| GLM-5.2 | Go | Premium | Complex reasoning, architecture decisions (new) |
| GLM-5.1 | Go | Standard | Complex patterns, tool operations |
| GLM-5 | Go | Standard | Reasoning tasks (deprecated May 14, 2026) |
| Kimi K3 | Go | Flagship | Latest Kimi, 1M context, 131K output, multimodal (new) |
| Kimi K2.7 Code | Go | Code specialist | Code generation, 32K output context |
| Kimi K2.6 | Go | Standard | General purpose, default fallback |
| Qwen3.7 Plus | Go | Fast | Streaming, low-latency (new) |
| Qwen3.7 Max | Go | Fast | Background tasks (new) |
| Qwen3.6 Plus | Go | Fast | Streaming fallback |
| Qwen3.5 Plus | Go | Fast | Simple read-only ops |
| MiniMax | Zen | Long context | 1M context window |
| MiMo | Go | Reasoning | Step-by-step reasoning |
internal/client/opencode.go routes Go provider models to Chat Completions; Zen models are classified by models.ClassifyEndpoint() in internal/models/classifier.go. If a model's upstream doesn't support Anthropic tool format, set anthropic_tools_disabled: true in config.
Scenario detection priority (internal/router/scenarios.go):
- Long Context (>80K tokens, configurable) → MiniMax (1M context)
- Complex (architectural patterns, tool operations) → GLM-5.2
- Think (reasoning keywords in system prompt) → GLM-5.1
- Background (simple read-only ops, no tools) → Qwen3.7 Max
- Default → Kimi K2.6
Model overrides: two config blocks bypass scenario routing based on the requested model. model_overrides matches the model string exactly (best with CC-Switch, which sends a custom model string). model_family_overrides maps a Claude family keyword (opus, sonnet, haiku) via case-insensitive substring match, so the versioned IDs Claude Code sends natively (claude-opus-4-20250514) route without CC-Switch. Precedence: exact model_overrides → model_family_overrides (longest key first) → respect_requested_model → scenario routing. Both are wired through ModelRouter.RouteWithOverride / RouteWithFamilyOverride (internal/router/model_router.go) and merged with a deduplicated scenario safety-net chain in buildModelChain (internal/handlers/messages.go).
Cost-based routing: when cost_routing.enabled is set, Selector in internal/router/selector.go replaces the static primary model with automatic cheapest-model selection from the catalog. It applies max_context_window (hard cap on context window), prefer_providers (global provider filter, intersected with per-scenario preferences), and penalty_per_provider (per-provider cost penalty added during sort). Enabled via cost_routing.enabled or the legacy enable_cost_based_routing flag.
Catalog schema: Models are keyed as provider/model-name (e.g., opencode-go/glm-5.2). The catalog (~/.config/routatic-proxy/catalog/catalog.json) contains:
providers— Provider definitions withname,base_url,enabledmodels— Model definitions keyed by full key with fields:id— Full key (matches the map key)name— Display namelimit.context— Context window sizerates.input/rates.output— Cost per million tokenstool_call— Whether tools are supportedmodalities.input/output— Input/output types (["text"]or["text", "image"]for vision)reasoning— Whether reasoning mode is supported
Resolution functions in internal/catalog/resolve.go extract the provider from the key prefix. ResolvedModel.ModelID is the model name only (without provider prefix); ResolvedModel.CanonicalName is the full key.
For streaming, the router downgrades to fast models (Qwen3.7 Plus) for better TTFT.
Deprecated models:
- GLM-5 — deprecated May 14, 2026; use GLM-5.1 or GLM-5.2
Polymorphic field handling: Anthropic's system and content fields accept both strings and arrays. pkg/types/ uses json.RawMessage with accessor methods (SystemText(), ContentBlocks()) to handle both formats.
Long-running stream policy: The proxy never kills a stream that is actively producing bytes. The server-level WriteTimeout is set to 0; instead each upstream read uses a per-Read deadline via http.ResponseController.SetReadDeadline that is renewed on every successful byte. If the gap between bytes exceeds OpenCodeGo.stream_timeout_ms (or OpenCodeZen.stream_timeout_ms), the connection is treated as stuck and the request is routed to the next fallback model. Defaults to timeout_ms when unset. Client disconnects during a stream are logged at Debug level — this is normal during Claude Code tool execution and is not a failure signal.
Provider-specific API keys: Each provider (OpenCode Go, OpenCode Zen, AWS Bedrock) can have its own api_key or api_keys array. Provider-specific keys take precedence over global keys. This enables per-provider fallback strategies and key rotation.
Environment variable overrides (single key):
ROUTATIC_PROXY_OPENCODE_GO_API_KEYROUTATIC_PROXY_OPENCODE_ZEN_API_KEYROUTATIC_PROXY_AWS_BEDROCK_API_KEY
Environment variable overrides (comma-separated keys for round-robin):
ROUTATIC_PROXY_OPENCODE_GO_API_KEYS=key-1,key-2,key-3ROUTATIC_PROXY_OPENCODE_ZEN_API_KEYS=key-1,key-2ROUTATIC_PROXY_AWS_BEDROCK_API_KEYS=key-1,key-2
Precedence: *_API_KEYS → *_API_KEY → global API_KEYS → global API_KEY.
cmd/routatic-proxy/main.go— CLI entry point (cobra). Default config template is generated here.internal/config/— Config types and JSON loader with${VAR}env interpolation.internal/transformer/— Request/response format conversion (Anthropic ↔ OpenAI).internal/router/fallback.go— Circuit breaker per model (3 failures = 30s skip).internal/handlers/models.go—GET /v1/models(OpenAI-style listing). Used by provider-switching tools like CC-Switch's "Fetch Models" button; sources IDs fromModelRouter.ListModels(config aliases +model_overrideskeys + catalog canonical names).configs/config.example.json— Reference config with all options documented.internal/gui/— Embedded HTTP server for the dashboard (serves static assets + API endpoints).internal/gui/assets/— HTML/CSS/JS for the dashboard (Overview, History, Analytics, Settings tabs).internal/history/— In-memory ring buffer (1000 entries, O(1) insert, thread-safe).internal/metrics/— In-process request counters (received, streamed, success, failed, model distribution).internal/storage/— SQLite persistence layer for request history, latency samples, and analytics.
The Settings tab exposes all config fields as editable form inputs. On save, only changed fields are sent to the backend as a JSON patch. The backend reads the current config from disk, merges the patch, writes back, and reloads atomically — the running proxy picks up changes immediately without restart.
Partial update flow:
- Frontend builds a patch object with only fields the user changed (compared to the last loaded config)
- Backend reads current config from disk via
config.LoadFromPath() - Backend merges patch fields onto current config via JSON marshal/unmarshal
- Backend validates essential fields (host, port)
- Backend writes merged config to disk and calls
atomicCfg.Reload()
Nil safety: The /api/metrics and /api/history handlers handle nil dependencies gracefully — they return zero values instead of panicking if the history or metrics instance is unavailable.
This project uses a dual release channel system for separating beta and production releases:
- Trigger: Every push to
mainbranch (see.github/workflows/beta-release.yml) - Version format:
v{UPCOMING}-beta.{N}(e.g.,v0.5.3-beta.1), where{N}is a sequential counter - GitHub release: Marked as
prerelease: true - Docker tags:
v{UPCOMING}-beta.{N},beta-{UPCOMING}, andbeta(rolling pointer to newest beta)
Beta releases are fully automated and include:
- Test suite validation
- Cross-platform binary builds (darwin-amd64/arm64, linux-amd64/arm64, windows-amd64/arm64)
- macOS DMG with CGO-enabled binary
- AI-generated changelog from commits
- Docker images for linux/amd64 and linux/arm64
- Trigger: Manual
workflow_dispatchonreleasesbranch (see.github/workflows/release.yml) - Version format:
vX.Y.Z(semantic versioning) - GitHub release: Marked as
prerelease: false(stable) - Docker tags:
vX.Y.Z,vX.Y,vX,latest
Production releases include all beta features plus:
- Homebrew tap update (requires
HOMEBREW_PATsecret) - Scoop bucket update (requires
SCOOP_PATsecret)
.github/scripts/get-versions.sh is used by the beta workflow to:
- Fetch tags from the
origin/releasesbranch to get current production version (e.g.,v0.5.2) - Increment the patch to the next version (e.g.,
v0.5.3) - beta is based on the upcoming patch release - Generate beta version by appending
-beta.{N}, where{N}ismax(existing beta counters for this upcoming version) + 1- the counter resets to 1 once the upcoming version ships as stable - Output both versions as JSON for CI consumption
Version Format Explanation:
v0.5.3= The upcoming production version (patch incremented from latest production)beta.1= Sequential prerelease counter for that upcoming version- Full example: stable
v0.5.2→v0.5.3-beta.1, thenv0.5.3-beta.2, ... untilv0.5.3ships →v0.5.4-beta.1
- Merge all changes to
mainand verify via beta - Ensure
releasesbranch exists and is up-to-date - Go to GitHub Actions → Release workflow
- Click "Run workflow"
- Enter version (must follow
vX.Y.Zformat) - Workflow validates, builds, and releases
Both workflows share the same stages:
- validate — Run
go vet,go test -race, and build sanity check on ubuntu-latest - release — Build cross-platform binaries and macOS DMG on macos-latest
- docker — Publish multi-arch Docker images on ubuntu-latest
Production adds: 4. homebrew — Update the homebrew-tap formula 5. scoop — Update the scoop-bucket manifest
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
Key routing rules:
- Product ideas/brainstorming → invoke /office-hours
- Strategy/scope → invoke /plan-ceo-review
- Architecture → invoke /plan-eng-review
- Design system/plan review → invoke /design-consultation or /plan-design-review
- Full review pipeline → invoke /autoplan
- Bugs/errors → invoke /investigate
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec