How translation flows
Translation happens twice: once before the network request and once after it.
sequenceDiagram
participant Caller
participant Boundary as Rust API / proxy adapter
participant Router
participant Adapter as Provider adapter
participant API as Native provider API
Caller->>Boundary: model + messages + controls
Boundary->>Router: resolve(model)
Router-->>Boundary: provider + raw model name
Boundary->>Adapter: transform_request(model, value)
Adapter->>API: provider-native HTTP request
API-->>Adapter: provider-native response
Adapter-->>Boundary: transform_response(model, response)
Boundary-->>Caller: normalized result
1. Resolve the model address
Every request contains a model string. Router::resolve turns that string
into two things:
- a registered provider adapter;
- the raw model name to send to that provider.
For example, anthropic/claude-sonnet-5 selects the adapter registered as
anthropic and passes claude-sonnet-5 to it. A bare model name can use
prefix inference instead. Models and the Router covers the exact
rules.
Resolution chooses an adapter. It does not check the static model-discovery list or choose a model based on prompt content.
2. Translate the outbound request
The Rust API receives the request as serde_json::Value. There is no canonical
Rust request struct between the caller and the providers. The selected
adapter's transform_request method reads the fields it understands and builds
the provider-native URL, headers, and JSON body.
This is where an OpenAI-style messages array becomes an Anthropic Messages
request, Gemini contents, or an OpenAI/xAI Responses request. Common controls
are translated here too.
3. Send the native HTTP request
The shared HTTP client sends the translated request to the selected provider. Provider authentication belongs to this outbound request; callers do not send one provider's key to another provider.
4. Translate the inbound response
The same adapter's transform_response method converts the provider-native
response into llmshim's normalized OpenAI Chat Completions-style value. Text,
tool calls, usage, and provider-returned reasoning are placed into that common
shape when present.
The proxy then performs one additional conversion into its compact
ChatResponse. This is why the Rust and proxy APIs share semantics without
sharing the same response envelope.
Streaming follows the same boundary
Streaming changes the inbound step, not the model. Each provider emits its own
SSE event format. The adapter's third method, transform_stream_chunk, converts
individual provider events into normalized chunks. The Rust API yields those
chunks directly; the proxy converts them again into typed content,
reasoning, tool_call, usage, done, and error events.