mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add in-memory transport for chatd -> aibridge routing (#25576)
### TL;DR Introduces an in-process `TransportFactory` for aibridge so that chatd (coder-agent LLM traffic) can route requests through the aibridged handler without crossing the HTTP route or requiring a license entitlement check. ### What changed? - Added a new `coderd/aibridge` package with a `TransportFactory` interface and a `Source` type for tagging the call site on request contexts. `SourceAgents` is defined as the constant for coder-agent traffic. - Implemented `NewTransportFactory` in `coderd/aibridged/transport.go`, which returns an `http.RoundTripper` that dispatches requests to the aibridged handler in-process. The response body is streamed through an `io.Pipe` so SSE/NDJSON/chunked responses propagate token-by-token. Handler panics are recovered and surfaced as 500 responses, and context cancellation closes the pipe with the appropriate error. - `RegisterInMemoryAIBridgedHTTPHandler` now also constructs a `TransportFactory` from the registered handler and stores it on `API.AIBridgeTransportFactory` (an `atomic.Pointer`), making it available to chatd without going through the license-gated HTTP route. - Added `API.AIBridgeTransportFactory` as a public `atomic.Pointer[aibridge.TransportFactory]` field on `coderd.API`. ### How to test? - `coderd/aibridged/transport_test.go` covers: transport creation, nil-handler errors, source attachment to context, header/status passthrough, streaming (SSE-style chunked writes visible before handler completion), context cancellation closing the body with an error, concurrent requests, handler panics producing 500s, and handlers that return without writing. - `coderd/aibridge_test.go` verifies that `AIBridgeTransportFactory` starts as nil on AGPL coderd, can be stored and loaded atomically, and that the stored factory correctly dispatches requests through the stub handler. ### Why make this change? Chatd needs to send LLM requests through aibridge in-process rather than via the external HTTP route, which is license-gated. The `TransportFactory` abstraction provides a clean seam: the entitlement check remains on the HTTP route for external callers, while in-process coder-agent traffic bypasses it through the factory. The `Source` type allows downstream handlers and logs to attribute traffic without gating behavior on the caller identity.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package aibridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Source identifies the call site that asked aibridge for a transport. It is
|
||||
// attached to the request context so downstream handlers and logs can attribute
|
||||
// traffic without changing behavior based on the value.
|
||||
type Source string
|
||||
|
||||
// SourceAgents is chatd traffic originating from a Coder agent.
|
||||
const SourceAgents Source = "agents"
|
||||
|
||||
type sourceCtxKey struct{}
|
||||
|
||||
// WithSource returns a copy of ctx carrying the given Source. Use this on the
|
||||
// request context before invoking a downstream handler so [SourceFromContext]
|
||||
// can recover it for logging.
|
||||
func WithSource(ctx context.Context, src Source) context.Context {
|
||||
return context.WithValue(ctx, sourceCtxKey{}, src)
|
||||
}
|
||||
|
||||
// SourceFromContext returns the Source attached by [WithSource], or the empty
|
||||
// string when no Source is set.
|
||||
func SourceFromContext(ctx context.Context) Source {
|
||||
src, _ := ctx.Value(sourceCtxKey{}).(Source)
|
||||
return src
|
||||
}
|
||||
|
||||
// TransportFactory returns an [http.RoundTripper] that dispatches an aibridge
|
||||
// request in-process for a given ai_providers row.
|
||||
//
|
||||
// Implementations live in coderd/aibridged. coderd registers an in-process
|
||||
// factory on coderd.API.AIBridgeTransportFactory at startup so callers route
|
||||
// traffic through the daemon without going through the gated HTTP route.
|
||||
//
|
||||
// Source is informational: implementations must not gate on it. It is attached
|
||||
// to the request context so handlers can include it in logs and metrics.
|
||||
type TransportFactory interface {
|
||||
TransportFor(providerID uuid.UUID, source Source) (http.RoundTripper, error)
|
||||
}
|
||||
Reference in New Issue
Block a user