shunt exists because Claude Code can talk to a first-class LLM gateway through ANTHROPIC_BASE_URL, but teams sometimes want only selected model IDs to run on another provider while the rest of the Claude Code session keeps its normal tools, skills, settings, and Anthropic pass-through behavior. The project implements that gateway as a Rust/Axum process that routes by the request model instead of trying to infer caller identity from prompts README.md:1-60 docs/implementation-plan.md:46-71.
| Component |
Responsibility |
Key file |
Source |
| CLI process |
Starts server, validates config, prints token helper output |
src/main.rs |
src/main.rs:38-76 |
| Router |
Exposes HEAD /, GET /v1/models, POST /v1/messages, and POST /v1/messages/count_tokens |
src/server.rs |
src/server.rs:13-25 |
| Proxy handler |
Buffers request, resolves route, dispatches adapter, logs latency |
src/proxy.rs |
src/proxy.rs:19-126 |
| Route resolver |
Applies exact, prefix, then default provider precedence |
src/routing.rs |
src/routing.rs:37-89 |
| Config |
Defines built-in providers and validates routes/providers |
src/config.rs |
src/config.rs:142-183 |
| Translation |
Converts Anthropic request/stream shapes to and from Responses |
src/model/responses_request.rs, src/model/responses.rs |
src/model/responses_request.rs:4-280 src/model/responses.rs:45-378 |
graph TB
subgraph Client[Claude Code process]
ModelPicker[/model picker]
ToolLoop[Tool loop and skills]
end
subgraph Gateway[shunt]
Server[Axum router]
Resolver[Model route resolver]
Adapter[Selected adapter]
end
subgraph Upstreams[Provider APIs]
Anthropic[Anthropic-compatible Messages API]
Responses[OpenAI Responses API]
end
ModelPicker --> ToolLoop --> Server --> Resolver --> Adapter
Adapter --> Anthropic
Adapter --> Responses
classDef dark fill:#2d333b,stroke:#6d5dfc,color:#e6edf3;
class ModelPicker,ToolLoop,Server,Resolver,Adapter,Anthropic,Responses dark;
style Client fill:#161b22,stroke:#30363d,color:#e6edf3;
style Gateway fill:#161b22,stroke:#30363d,color:#e6edf3;
style Upstreams fill:#161b22,stroke:#30363d,color:#e6edf3;
linkStyle default stroke:#8b949e;
sequenceDiagram
autonumber
participant CC as Claude Code
participant Axum as shunt Router
participant Proxy as proxy::post
participant Routing as routing::resolve
participant Adapter as Adapter
participant Upstream as Provider API
CC->>Axum: POST /v1/messages
Axum->>Proxy: body + headers + OriginalUri
Proxy->>Routing: parse JSON model field
Routing-->>Proxy: Route(provider, adapter, upstream_model)
Proxy->>Adapter: forward(state, route, uri, headers, body)
Adapter->>Upstream: provider-specific request
Upstream-->>Adapter: stream or JSON response
Adapter-->>CC: Anthropic-shaped response
stateDiagram-v2
[*] --> ConfigLoad
ConfigLoad --> Listening: valid config
ConfigLoad --> Failed: validation error
Listening --> RequestBuffered: POST received
RequestBuffered --> Routed: model parsed
Routed --> AnthropicPath: AdapterKind::Anthropic
Routed --> ResponsesPath: AdapterKind::Responses
AnthropicPath --> StreamBack
ResponsesPath --> TranslateBack
StreamBack --> Listening
TranslateBack --> Listening
Failed --> [*]
| Approach |
shunt decision |
Reason |
Source |
| Prompt fingerprinting |
Not used |
The request already contains the selected model ID, so prompt-shape coupling is unnecessary |
docs/implementation-plan.md:24-32 |
| Global provider swap |
Not the focus |
Unmapped models must continue to pass through to Anthropic |
README.md:43-56 |
| Per-model mapping |
Primary mechanism |
Claude Code lets users pick model IDs per context; shunt honors those IDs |
README.md:18-20 |