aibridge

package
v2.34.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: AGPL-3.0 Imports: 36 Imported by: 0

README

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/aibridge/<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.Handler that 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

  1. Client sends request to /anthropic/v1/messages or /openai/v1/chat/completions
  2. Actor extraction: Request must have an actor in context (via AsActor()). The host is responsible for authenticating the caller before invoking the handler.
  3. Upstream call: Request forwarded to the AI provider
  4. Response relay: Response streamed/sent to client
  5. 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 response
  • aibridge_user_prompts - user prompts
  • aibridge_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/aibridge/<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/aibridge/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

Documentation

Index

Constants

View Source
const (
	ProviderAnthropic = config.ProviderAnthropic
	ProviderOpenAI    = config.ProviderOpenAI
	ProviderCopilot   = config.ProviderCopilot
)

Const + Type + function aliases for backwards compatibility.

View Source
const (
	SSEEventTypeMessage = "message"
	SSEEventTypeError   = "error"
	SSEEventTypePing    = "ping"
)
View Source
const (

	// ErrorCodeProviderDisabled is the code written in the response
	// body when a request targets a configured-but-disabled provider.
	// Paired with HTTP 503.
	ErrorCodeProviderDisabled = "provider_disabled"
)

Variables

This section is empty.

Functions

func AsActor

func AsActor(ctx context.Context, actorID string, metadata recorder.Metadata) context.Context

func GuessSessionID

func GuessSessionID(client Client, r *http.Request) *string

GuessSessionID attempts to retrieve a session ID which may have been sent by the client. We only attempt to retrieve sessions using methods recognized for the given client.

func NewAnthropicProvider

func NewAnthropicProvider(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) provider.Provider

func NewCopilotProvider

func NewCopilotProvider(cfg config.Copilot) provider.Provider

func NewDisabledProviderStub added in v2.34.0

func NewDisabledProviderStub(name, providerType string) provider.Provider

NewDisabledProviderStub returns a Provider that reports Enabled() == false and has no-op implementations for all other methods. Use this instead of constructing a concrete provider for disabled rows so that adding a new provider type does not require updating a switch here.

func NewMetrics

func NewMetrics(reg prometheus.Registerer) *metrics.Metrics

func NewOpenAIProvider

func NewOpenAIProvider(cfg config.OpenAI) provider.Provider

Types

type AWSBedrockConfig

type AWSBedrockConfig = config.AWSBedrock

type AnthropicConfig

type AnthropicConfig = config.Anthropic

type Client

type Client string
const (
	// Possible values for the "client" field in interception records.
	// Must be kept in sync with documentation: https://github.com/coder/coder/blob/90c11f3386578da053ec5cd9f1475835b980e7c7/docs/ai-coder/ai-bridge/monitoring.md?plain=1#L36-L44
	ClientClaudeCode  Client = "Claude Code"
	ClientCodex       Client = "Codex"
	ClientZed         Client = "Zed"
	ClientCopilotVSC  Client = "GitHub Copilot (VS Code)"
	ClientCopilotCLI  Client = "GitHub Copilot (CLI)"
	ClientKilo        Client = "Kilo Code"
	ClientCoderAgents Client = "Coder Agents"
	ClientCrush       Client = "Charm Crush"
	ClientMux         Client = "Mux"
	ClientRoo         Client = "Roo Code"
	ClientCursor      Client = "Cursor"
	ClientUnknown     Client = "Unknown"
)

func GuessClient

func GuessClient(r *http.Request) Client

GuessClient attempts to guess the client application from the request headers. Not all clients set proper user agent headers, so this is a best-effort approach. Based on https://github.com/coder/aibridge/issues/20#issuecomment-3769444101.

type CopilotConfig

type CopilotConfig = config.Copilot

type InterceptionRecord

type InterceptionRecord = recorder.InterceptionRecord

type InterceptionRecordEnded

type InterceptionRecordEnded = recorder.InterceptionRecordEnded

type Metadata

type Metadata = recorder.Metadata

type Metrics

type Metrics = metrics.Metrics

type ModelThoughtRecord

type ModelThoughtRecord = recorder.ModelThoughtRecord

type OpenAIConfig

type OpenAIConfig = config.OpenAI

type PromptUsageRecord

type PromptUsageRecord = recorder.PromptUsageRecord

type Provider

type Provider = provider.Provider

type Recorder

type Recorder = recorder.Recorder

func NewRecorder

func NewRecorder(logger slog.Logger, tracer trace.Tracer, clientFn func() (Recorder, error)) Recorder

type RequestBridge

type RequestBridge struct {
	// contains filtered or unexported fields
}

RequestBridge is an http.Handler which is capable of masquerading as AI providers' APIs; specifically, OpenAI's & Anthropic's at present. RequestBridge intercepts requests to - and responses from - these upstream services to provide a centralized governance layer.

RequestBridge has no concept of authentication or authorization. It does have a concept of identity, in the narrow sense that it expects an [actor] to be defined in the context, to record the initiator of each interception.

RequestBridge is safe for concurrent use.

func NewRequestBridge

func NewRequestBridge(ctx context.Context, providers []provider.Provider, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) (*RequestBridge, error)

NewRequestBridge creates a new *RequestBridge and registers the HTTP routes defined by the given providers. Any routes which are requested but not registered will be reverse-proxied to the upstream service.

A intercept.Recorder is also required to record prompt, tool, and token use.

mcpProxy will be closed when the RequestBridge is closed.

Circuit breaker configuration is obtained from each provider's CircuitBreakerConfig() method. Providers returning nil will not have circuit breaker protection.

func (*RequestBridge) InflightRequests

func (b *RequestBridge) InflightRequests() int32

func (*RequestBridge) ServeHTTP

func (b *RequestBridge) ServeHTTP(rw http.ResponseWriter, r *http.Request)

ServeHTTP exposes the internal http.Handler, which has all [Provider]s' routes registered. It also tracks inflight requests.

func (*RequestBridge) Shutdown

func (b *RequestBridge) Shutdown(ctx context.Context) error

Shutdown will attempt to gracefully shutdown. This entails waiting for all requests to complete, and shutting down the MCP server proxier. TODO: add tests.

type SSEEvent

type SSEEvent struct {
	Type  string
	Data  string
	ID    string
	Retry int
}

type SSEParser

type SSEParser struct {
	// contains filtered or unexported fields
}

func NewSSEParser

func NewSSEParser() *SSEParser

func (*SSEParser) AllEvents

func (p *SSEParser) AllEvents() map[string][]SSEEvent

func (*SSEParser) EventsByType

func (p *SSEParser) EventsByType(eventType string) []SSEEvent

func (*SSEParser) MessageEvents

func (p *SSEParser) MessageEvents() []SSEEvent

func (*SSEParser) Parse

func (p *SSEParser) Parse(reader io.Reader) error

type TokenUsageRecord

type TokenUsageRecord = recorder.TokenUsageRecord

type ToolUsageRecord

type ToolUsageRecord = recorder.ToolUsageRecord

Directories

Path Synopsis
internal
Package mcpmock is a generated GoMock package.
Package mcpmock is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL