Skip to content

Overview

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
Page Relationship
Configuration Explains how model IDs map to providers
Operations Turns the overview into runnable commands
Architecture Deep runtime component map
Adapters and Translation Details the adapter implementation