Implements https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider # AWS Bedrock mantle support in AI Gateway ## Summary Add support for the AWS Bedrock **mantle** endpoint (`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway. Mantle serves Claude through the native Anthropic Messages API. We model it as a `protocol` field on the existing Bedrock provider settings (`invoke-model` default, or `mantle`) rather than as a new provider type, and we treat mantle as a pure passthrough: SigV4-sign and forward, no body translation. ## Background Claude on AWS Bedrock is reachable through two endpoints, each speaking exactly one wire protocol: 1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`. Model id in the URL path, request translated into Bedrock's InvokeModel format, responses returned as a binary AWS eventstream. This is what AI Gateway already supported for Bedrock. 2. **Mantle** (this doc): `bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native Anthropic Messages API: model in the body, plain SSE streaming. ## Why a `protocol` field, not a new provider type The alternative is to model mantle as its own `ai_provider_type` (`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field instead for two reasons: 1. Mantle reads more like a protocol of Bedrock than a separate provider. It is the same AWS account, credentials, region, and IAM, reached over a different wire protocol and host. One Bedrock provider with two protocols (`invoke-model` default and `mantle`) models that more organically than two provider types. 2. It avoids a database migration. The `protocol` field lives in the settings JSON blob (empty resolves to `invoke-model`, so existing providers are unaffected), whereas a new type means an enum value and the `ALTER TYPE ... ADD VALUE` migration that goes with it. ## Why passthrough, not translation The client already emits Bedrock-legal requests in mantle mode: ```sh export CLAUDE_CODE_USE_MANTLE=1 export CLAUDE_CODE_SKIP_MANTLE_AUTH=1 export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name> ``` So the gateway just forwards the body and SigV4-signs it (service `bedrock-mantle`), and skips all the InvokeModel body-translation (model remap, thinking conversion, beta-flag allowlist, field stripping). This keeps the mantle path thin and avoids a second copy of translation logic to maintain. ## Consequences - Protocol-dependent fields: `model` / `small_fast_model` are used by InvokeModel but ignored by mantle (the client sends the model), and `base_url` is required for mantle but optional for InvokeModel. Validation is protocol-aware. - No central model control on mantle: because it is a passthrough, the operator cannot pin the model. - `region` and the `base_url` host must name the same region (the SigV4 scope must match the endpoint); a mismatch surfaces as `Credential should be scoped to a valid region`. ## Draft UI <img width="1100" height="579" alt="image" src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6" /> ## Follow-up PRs: - https://github.com/coder/coder/pull/27156
aibridge
aibridge provides an HTTP handler that intercepts AI client requests bound for upstream AI providers (Anthropic, OpenAI, Copilot). It records token usage, prompts, and tool invocations per user. Optionally supports centralized MCP tool injection with allowlist/denylist filtering.
The handler is mounted by a host process. Today that host is coderd, which mounts the handler at /api/v2/ai-gateway/<provider>/*. Running aibridge as a separate process is planned for the future.
Architecture
┌─────────────────┐ ┌───────────────────────────────────────────┐
│ AI Client │ │ aibridge │
│ (Claude Code, │────▶│ ┌─────────────────┐ ┌─────────────┐ │
│ Cursor, etc.) │ │ │ RequestBridge │───▶│ Providers │ │
└─────────────────┘ │ │ (http.Handler) │ │ (Anthropic │ │
│ └─────────────────┘ │ OpenAI) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │ ┌─────────────┐
│ ┌─────────────────┐ ┌─────────────┐ │ │ Upstream │
│ │ Recorder │◀───│ Interceptor │─── ───▶│ API │
│ │ (tokens, tools, │ │ (streaming/ │ │ │ (Anthropic │
│ │ prompts) │ │ blocking) │ │ │ OpenAI) │
│ └────────┬────────┘ └──────┬──────┘ │ └─────────────┘
│ │ │ │
│ ▼ ┌──────▼──────┐ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ MCP Proxy │ │
│ │ Database │ │ (tools) │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ┘ └─────────────┘ │
└───────────────────────────────────────────┘
Components
- RequestBridge: The main
http.Handlerthat routes requests to providers - Provider: Defines bridged routes (intercepted) and passthrough routes (proxied)
- Interceptor: Handles request/response processing and streaming
- Recorder: Interface for capturing usage data (tokens, prompts, tools)
- MCP Proxy (optional): Connects to MCP servers to list tool, inject them into requests, and invoke them in an inner agentic loop
Request Flow
- Client sends request to
/anthropic/v1/messagesor/openai/v1/chat/completions - Actor extraction: Request must have an actor in context (via
AsActor()). The host is responsible for authenticating the caller before invoking the handler. - Upstream call: Request forwarded to the AI provider
- Response relay: Response streamed/sent to client
- Recording: Token usage, prompts, and tool invocations recorded
With MCP enabled: Tools from configured MCP servers are centrally defined and injected into requests (prefixed bmcp_). Allowlist/denylist regex patterns control which tools are available. When the model selects an injected tool, the gateway invokes it in an inner agentic loop, and continues the conversation loop until complete.
Passthrough routes (/v1/models, /v1/messages/count_tokens) are reverse-proxied directly.
Observability
Prometheus Metrics
Create metrics with NewMetrics(prometheus.Registerer):
| Metric | Type | Description |
|---|---|---|
interceptions_total |
Counter | Intercepted request count |
interceptions_inflight |
Gauge | Currently processing requests |
interceptions_duration_seconds |
Histogram | Request duration |
passthrough_total |
Counter | Non-intercepted requests forwarded to the upstream |
prompts_total |
Counter | User prompt count |
tokens_total |
Counter | Token usage (input, output, cache read/write, provider extras) |
injected_tool_invocations_total |
Counter | Injected MCP tool invocations performed by the handler |
non_injected_tool_selections_total |
Counter | Client-defined tool selections returned by the model |
circuit_breaker_state |
Gauge | Circuit breaker state per provider/endpoint (0=closed, 0.5=half, 1=open) |
circuit_breaker_trips_total |
Counter | Times the circuit breaker transitioned to open |
circuit_breaker_rejects_total |
Counter | Requests rejected due to an open circuit breaker |
Recorder Interface
Implement Recorder to persist usage data to your database:
aibridge_interceptions- request metadata (provider, model, initiator, timestamps)aibridge_token_usages- input/output and cache read/write token counts per responseaibridge_user_prompts- user promptsaibridge_tool_usages- tool invocations (injected and client-defined)aibridge_model_thoughts- model reasoning content (thinking, reasoning summaries, commentary)
type Recorder interface {
RecordInterception(ctx context.Context, req *InterceptionRecord) error
RecordInterceptionEnded(ctx context.Context, req *InterceptionRecordEnded) error
RecordTokenUsage(ctx context.Context, req *TokenUsageRecord) error
RecordPromptUsage(ctx context.Context, req *PromptUsageRecord) error
RecordToolUsage(ctx context.Context, req *ToolUsageRecord) error
RecordModelThought(ctx context.Context, req *ModelThoughtRecord) error
}
Supported Routes
Each provider instance is mounted under /api/v2/ai-gateway/<name>, where <name> is the provider's configured name. For example, with an Anthropic provider named my-anthropic, its /messages endpoint would be reachable at /api/v2/ai-gateway/my-anthropic/v1/messages.
If a name is not set, the route path defaults to the provider's type: anthropic, openai, or copilot. The table below uses the default names.
(/*) denotes a route that handles both the exact path and any subpaths. A trailing /* denotes subpaths only.
| Provider | Route | Type |
|---|---|---|
| Anthropic | /anthropic/v1/messages |
Bridged (intercepted) |
| Anthropic | /anthropic/v1/messages/count_tokens |
Passthrough |
| Anthropic | /anthropic/v1/models(/*) |
Passthrough |
| Anthropic | /anthropic/api/event_logging/* |
Passthrough |
| OpenAI | /openai/v1/chat/completions |
Bridged (intercepted) |
| OpenAI | /openai/v1/responses |
Bridged (intercepted) |
| OpenAI | /openai/v1/responses/* |
Passthrough |
| OpenAI | /openai/v1/conversations(/*) |
Passthrough |
| OpenAI | /openai/v1/models(/*) |
Passthrough |
| Copilot | /copilot/chat/completions |
Bridged (intercepted) |
| Copilot | /copilot/responses |
Bridged (intercepted) |
| Copilot | /copilot/models(/*) |
Passthrough |
| Copilot | /copilot/agents/* |
Passthrough |
| Copilot | /copilot/mcp/* |
Passthrough |
| Copilot | /copilot/.well-known/* |
Passthrough |