Files
coder/enterprise/aibridged/http.go
T
Paweł Banaszewski e00e85765b chore: move aibridge library code into coder repo (#24190)
This PR merges code from `coder/aibridge` repository into `coder/coder`.
It was split into 4 PRs for easier review but stacked PRs will need to
be merged into this PR so all checks pass.

* https://github.com/coder/coder/pull/24190 -> raw code copy (this PR,
before merging PRs on top of it, it was just 1 commit:
https://github.com/coder/coder/commit/70d33f33200c7e77df910957595715f81f9bec24)
* https://github.com/coder/coder/pull/24570 -> update imports in
`coder/coder` to use copied code
* https://github.com/coder/coder/pull/24586 -> linter fixes and CI
integration (also added README.md)
* https://github.com/coder/coder/pull/24571 -> added exclude to
scripts/check_emdash.sh check

Original PR message (before PR squash):
Moves coder/aibridge code into coder/coder repository.

Omitted files:

- `go.mod`, `go.sum`, `.gitignore`, `.github/workflows/ci.yml,`
`Makefile`, `LICENSE`, `README.md` (modified README.md is added later)
- `.github`, `example`, `buildinfo,` `scripts` directories

Simple verification script (will list omitted files)

```
tmp=$(mktemp -d)
echo "$tmp"
git clone --depth=1 https://github.com/coder/aibridge "$tmp/aibridge"
git clone --depth=1 --branch pb/aibridge-code-move https://github.com/coder/coder "$tmp/coder"
diff -rq --exclude=.git "$tmp/aibridge" "$tmp/coder/aibridge"
# rm -rf "$tmp"
```
2026-04-22 17:01:01 +02:00

133 lines
4.3 KiB
Go

package aibridged
import (
"net/http"
"strings"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge"
"github.com/coder/coder/v2/aibridge/recorder"
agplaibridge "github.com/coder/coder/v2/coderd/aibridge"
"github.com/coder/coder/v2/enterprise/aibridged/proto"
)
var _ http.Handler = &Server{}
var (
ErrNoAuthKey = xerrors.New("no authentication key provided")
ErrConnect = xerrors.New("could not connect to coderd")
ErrUnauthorized = xerrors.New("unauthorized")
ErrAcquireRequestHandler = xerrors.New("failed to acquire request handler")
)
// ServeHTTP is the entrypoint for requests which will be intercepted by AI Bridge.
// This function will validate that the given API key may be used to perform the request.
//
// An [aibridge.RequestBridge] instance is acquired from a pool based on the API key's
// owner (referred to as the "initiator"); this instance is responsible for the
// AI Bridge-specific handling of the request.
//
// A [DRPCClient] is provided to the [aibridge.RequestBridge] instance so that data can
// be passed up to a [DRPCServer] for persistence.
func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
logger := s.logger.With(
slog.F("method", r.Method),
slog.F("path", r.URL.Path),
)
// Extract and strip proxy request ID for cross-service log
// correlation. Absent for direct requests not routed through
// aibridgeproxyd.
if proxyReqID := r.Header.Get(agplaibridge.HeaderCoderRequestID); proxyReqID != "" {
// Inject into context so downstream loggers include it.
ctx = slog.With(ctx, slog.F("aibridgeproxy_id", proxyReqID))
logger = logger.With(slog.F("aibridgeproxy_id", proxyReqID))
}
r.Header.Del(agplaibridge.HeaderCoderRequestID)
byok := agplaibridge.IsBYOK(r.Header)
authMode := "centralized"
if byok {
authMode = "byok"
}
key := strings.TrimSpace(agplaibridge.ExtractAuthToken(r.Header))
if key == "" {
// Some clients (e.g. Claude) send a HEAD request
// without credentials to check connectivity.
if r.Method == http.MethodHead {
logger.Info(ctx, "unauthenticated HEAD request")
} else {
logger.Warn(ctx, "no auth key provided")
}
http.Error(rw, ErrNoAuthKey.Error(), http.StatusBadRequest)
return
}
// Strip every header that may carry the Coder token so it is
// never forwarded to upstream providers. After stripping, the
// aibridge library can treat the request as a normal LLM API call
// with no Coder-specific information.
if byok {
// In BYOK mode the token is in X-Coder-AI-Governance-Token;
// Authorization and X-Api-Key carry the user's own LLM credentials
// and must be preserved.
r.Header.Del(agplaibridge.HeaderCoderToken)
} else {
// In centralized mode the token may be in Authorization (the
// documented path) or X-Api-Key (legacy clients that set
// ANTHROPIC_API_KEY to their Coder token). Both are
// stripped.
r.Header.Del("Authorization")
r.Header.Del("X-Api-Key")
}
client, err := s.Client()
if err != nil {
logger.Warn(ctx, "failed to connect to coderd", slog.Error(err))
http.Error(rw, ErrConnect.Error(), http.StatusServiceUnavailable)
return
}
resp, err := client.IsAuthorized(ctx, &proto.IsAuthorizedRequest{Key: key})
if err != nil {
logger.Warn(ctx, "key authorization check failed", slog.Error(err), slog.F("auth_mode", authMode))
http.Error(rw, ErrUnauthorized.Error(), http.StatusForbidden)
return
}
// Rewire request context to include actor.
//
// [NOTE]
// The metadata provided here must NOT be sensitive as it could be included
// in requests to upstream services.
r = r.WithContext(aibridge.AsActor(ctx, resp.GetOwnerId(), recorder.Metadata{
"Username": resp.GetUsername(),
}))
id, err := uuid.Parse(resp.GetOwnerId())
if err != nil {
logger.Warn(ctx, "failed to parse user ID", slog.Error(err), slog.F("id", resp.GetOwnerId()))
http.Error(rw, ErrUnauthorized.Error(), http.StatusForbidden)
return
}
handler, err := s.GetRequestHandler(ctx, Request{
SessionKey: key,
APIKeyID: resp.ApiKeyId,
InitiatorID: id,
})
if err != nil {
logger.Warn(ctx, "failed to acquire request handler", slog.Error(err))
http.Error(rw, ErrAcquireRequestHandler.Error(), http.StatusInternalServerError)
return
}
handler.ServeHTTP(rw, r)
}