feat: add /api/v2/ai-gateway API route aliases (#26475)

## Description

Registers `/api/v2/ai-gateway/*` as the new API path for AI Gateway, replacing `/api/v2/aibridge/*`. Both prefixes share the same route builder (`aiBridgeRoutes`) backed by a single in-memory handler, so existing `/aibridge` endpoints continue to work. New endpoints must be registered on the enterprise API handler under `/api/v2/ai-gateway` only.

Swagger annotations now point to `/api/v2/ai-gateway` paths with a backward-compatibility note referencing `/aibridge`. The legacy `/aibridge` routes are skipped in the swagger documentation test.

## Changes

- Store one raw handler (`aiGatewayHandler`) instead of two prefix-stripped handlers
- Register `/ai-gateway` and `/ai-gateway/proxy` route aliases alongside legacy `/aibridge` routes
- Move `/aibridge/keys` to `/ai-gateway/keys`
- Update in-process transport to use `/api/v2/ai-gateway` prefix
- Update SDK client URLs and proxy forwarding URL
- Swap `@Router` and `@Tags` annotations from `aibridge`/`AI Bridge` to `ai-gateway`/`AI Gateway`
- Rename user-facing error messages from "AI Bridge" to "AI Gateway"
- Define consts for route prefixes (`AIGatewayRootPath`, `AIBridgeRootPath`)
- Update tests and comments to use new paths

Note: the following will be addressed in follow-up PRs:
- Frontend API URLs
- Frontend routes and redirects
- Dogfood main.tf updates
- Hand-written documentation URL updates
- aibridge internal comments and nits
- Scale tests path updates

Refs https://linear.app/coder/issue/AIGOV-230

> Generated with the assistance of Coder Agents (@ssncferreira)
This commit is contained in:
Susana Ferreira
2026-06-23 12:15:10 +01:00
committed by GitHub
parent 4691ef39cd
commit 970bd73691
28 changed files with 727 additions and 630 deletions
+47 -18
View File
@@ -47,12 +47,35 @@ var errInvalidCursor = xerrors.New("invalid pagination cursor")
// check_constraint.go.
const userAIBudgetOverridesMustBeGroupMemberConstraint database.CheckConstraint = "user_ai_budget_overrides_must_be_group_member"
// aibridgeHandler handles all aibridged-related endpoints.
func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
// aibridgeHTTPHandler returns the legacy /api/v2/aibridge route tree.
// Kept for backward compatibility only.
//
// NOTE: new endpoints must be registered on the enterprise API
// handler under /api/v2/ai-gateway, not in this shared route builder.
func aibridgeHTTPHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
return aiBridgeRoutes(api, agplaibridge.AIBridgeRootPath, middlewares...)
}
// aiGatewayHTTPHandler returns the /api/v2/ai-gateway route tree.
// This shares the same route builder as /aibridge for endpoints that
// existed before the rename.
//
// NOTE: new endpoints must be registered on the enterprise API
// handler under /api/v2/ai-gateway, not in this shared route builder.
func aiGatewayHTTPHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
return aiBridgeRoutes(api, agplaibridge.AIGatewayRootPath, middlewares...)
}
// aiBridgeRoutes builds the shared route tree for the legacy /aibridge
// and /ai-gateway prefixes. It contains the upstream AI provider
// catch-all handler and the management endpoints that were released
// under /aibridge. The stripPrefix parameter selects which URL prefix
// to strip before forwarding to the in-memory aibridged handler.
func aiBridgeRoutes(api *API, stripPrefix string, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
// Build the overload protection middleware chain for the aibridged handler.
// These limits are applied per-replica.
bridgeCfg := api.DeploymentValues.AI.BridgeConfig
concurrencyLimiter := httpmw.ConcurrencyLimit(bridgeCfg.MaxConcurrency.Value(), "AI Bridge")
concurrencyLimiter := httpmw.ConcurrencyLimit(bridgeCfg.MaxConcurrency.Value(), "AI Gateway")
rateLimiter := httpmw.RateLimitByAuthToken(int(bridgeCfg.RateLimit.Value()), aiBridgeRateLimitWindow)
return func(r chi.Router) {
@@ -72,7 +95,8 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f
// This is a bit funky but since aibridge only exposes a HTTP
// handler, this is how it has to be.
r.HandleFunc("/*", func(rw http.ResponseWriter, r *http.Request) {
if api.AGPL.GetAIBridgedHandler() == nil {
handler := api.AGPL.AIGatewayHandler()
if handler == nil {
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
Message: "aibridged handler not mounted",
})
@@ -89,7 +113,8 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f
return
}
api.AGPL.GetAIBridgedHandler().ServeHTTP(rw, r)
// Strip the prefix and relay to the aibridged handler.
http.StripPrefix(stripPrefix, handler).ServeHTTP(rw, r)
})
})
}
@@ -98,16 +123,17 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f
// aiBridgeListSessions returns AI Bridge sessions (aggregated interceptions).
//
// @Summary List AI Bridge sessions
// @Description Alias: also available at /api/v2/aibridge/sessions for backward compatibility.
// @ID list-ai-bridge-sessions
// @Security CoderSessionToken
// @Produce json
// @Tags AI Bridge
// @Tags AI Gateway
// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before."
// @Param limit query int false "Page limit"
// @Param after_session_id query string false "Cursor pagination after session ID (cannot be used with offset)"
// @Param offset query int false "Offset pagination (cannot be used with after_session_id)"
// @Success 200 {object} codersdk.AIBridgeListSessionsResponse
// @Router /api/v2/aibridge/sessions [get]
// @Router /api/v2/ai-gateway/sessions [get]
func (api *API) aiBridgeListSessions(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
@@ -203,7 +229,7 @@ func (api *API) aiBridgeListSessions(rw http.ResponseWriter, r *http.Request) {
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error getting AI Bridge sessions.",
Message: "Internal error getting AI Gateway sessions.",
Detail: err.Error(),
})
return
@@ -224,16 +250,17 @@ func (api *API) aiBridgeListSessions(rw http.ResponseWriter, r *http.Request) {
// threads including agentic actions and thinking blocks.
//
// @Summary Get AI Bridge session threads
// @Description Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.
// @ID get-ai-bridge-session-threads
// @Security CoderSessionToken
// @Produce json
// @Tags AI Bridge
// @Tags AI Gateway
// @Param session_id path string true "Session ID (client_session_id or interception UUID)"
// @Param after_id query string false "Thread pagination cursor (forward/older)"
// @Param before_id query string false "Thread pagination cursor (backward/newer)"
// @Param limit query int false "Number of threads per page (default 50)"
// @Success 200 {object} codersdk.AIBridgeSessionThreadsResponse
// @Router /api/v2/aibridge/sessions/{session_id} [get]
// @Router /api/v2/ai-gateway/sessions/{session_id} [get]
func (api *API) aiBridgeGetSessionThreads(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -415,12 +442,13 @@ func (api *API) aiBridgeGetSessionThreads(rw http.ResponseWriter, r *http.Reques
// aiBridgeListModels returns all AI Bridge models a user can see.
//
// @Summary List AI Bridge models
// @Description Alias: also available at /api/v2/aibridge/models for backward compatibility.
// @ID list-ai-bridge-models
// @Security CoderSessionToken
// @Produce json
// @Tags AI Bridge
// @Tags AI Gateway
// @Success 200 {array} string
// @Router /api/v2/aibridge/models [get]
// @Router /api/v2/ai-gateway/models [get]
func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -446,7 +474,7 @@ func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) {
if len(errs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid AI Bridge models search query.",
Message: "Invalid AI Gateway models search query.",
Validations: errs,
})
return
@@ -455,7 +483,7 @@ func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) {
models, err := api.Database.ListAIBridgeModels(ctx, filter)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error getting AI Bridge models.",
Message: "Internal error getting AI Gateway models.",
Detail: err.Error(),
})
return
@@ -467,12 +495,13 @@ func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) {
// aiBridgeListClients returns all AI Bridge clients a user can see.
//
// @Summary List AI Bridge clients
// @Description Alias: also available at /api/v2/aibridge/clients for backward compatibility.
// @ID list-ai-bridge-clients
// @Security CoderSessionToken
// @Produce json
// @Tags AI Bridge
// @Tags AI Gateway
// @Success 200 {array} string
// @Router /api/v2/aibridge/clients [get]
// @Router /api/v2/ai-gateway/clients [get]
func (api *API) aiBridgeListClients(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -498,7 +527,7 @@ func (api *API) aiBridgeListClients(rw http.ResponseWriter, r *http.Request) {
if len(errs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid AI Bridge clients search query.",
Message: "Invalid AI Gateway clients search query.",
Validations: errs,
})
return
@@ -507,7 +536,7 @@ func (api *API) aiBridgeListClients(rw http.ResponseWriter, r *http.Request) {
clients, err := api.Database.ListAIBridgeClients(ctx, filter)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error getting AI Bridge clients.",
Message: "Internal error getting AI Gateway clients.",
Detail: err.Error(),
})
return