mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@ func newMockUpstream(t *testing.T, name string) *mockUpstream {
|
||||
|
||||
// startTestAIBridgeDaemon wires an in-process aibridged daemon onto
|
||||
// the supplied API and subscribes it to ai_providers change events.
|
||||
// This mirrors what cli/server.go does in production so /api/v2/aibridge
|
||||
// This mirrors what cli/server.go does in production so /api/v2/ai-gateway
|
||||
// requests dispatch through the real pool and reloader.
|
||||
func startTestAIBridgeDaemon(t *testing.T, api *coderd.API) *aibridged.Metrics {
|
||||
t.Helper()
|
||||
@@ -109,7 +109,7 @@ func (r *testPoolReloader) Reload(ctx context.Context) error {
|
||||
// TestAIBridgeProviderHotReload exercises the end-to-end CRUD ->
|
||||
// reload -> routing path: every provider mutation made through codersdk
|
||||
// must, within a short window, change the routing observed at
|
||||
// /api/v2/aibridge/{name}/v1/models. The OpenAI passthrough route
|
||||
// /api/v2/ai-gateway/{name}/v1/models. The OpenAI passthrough route
|
||||
// /v1/models reverse-proxies to BaseURL, so the upstream that responds
|
||||
// identifies which provider the daemon's mux dispatched to.
|
||||
func TestAIBridgeProviderHotReload(t *testing.T) {
|
||||
@@ -162,11 +162,11 @@ func TestAIBridgeProviderHotReload(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// sendRequest issues GET /api/v2/aibridge/{name}/v1/models and
|
||||
// sendRequest issues GET /api/v2/ai-gateway/{name}/v1/models and
|
||||
// returns the status and the upstream marker decoded from the
|
||||
// JSON body (empty if the body was not the marker JSON).
|
||||
sendRequest := func(providerName string) (int, string) {
|
||||
url := client.URL.String() + "/api/v2/aibridge/" + providerName + "/v1/models"
|
||||
url := client.URL.String() + "/api/v2/ai-gateway/" + providerName + "/v1/models"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+client.SessionToken())
|
||||
|
||||
@@ -1232,7 +1232,7 @@ func TestAIBridgeRouting(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "StablePrefix",
|
||||
path: "/api/v2/aibridge/openai/v1/chat/completions",
|
||||
path: "/api/v2/ai-gateway/openai/v1/chat/completions",
|
||||
expectedPath: "/openai/v1/chat/completions",
|
||||
},
|
||||
}
|
||||
@@ -1290,7 +1290,7 @@ func TestAIBridgeRateLimiting(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
httpClient := &http.Client{}
|
||||
url := client.URL.String() + "/api/v2/aibridge/test"
|
||||
url := client.URL.String() + "/api/v2/ai-gateway/test"
|
||||
|
||||
// Make requests up to the limit - should succeed.
|
||||
for range 2 {
|
||||
@@ -1350,7 +1350,7 @@ func TestAIBridgeConcurrencyLimiting(t *testing.T) {
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
httpClient := &http.Client{}
|
||||
url := client.URL.String() + "/api/v2/aibridge/test"
|
||||
url := client.URL.String() + "/api/v2/ai-gateway/test"
|
||||
|
||||
// Start a request that will block.
|
||||
done := make(chan struct{})
|
||||
@@ -2012,7 +2012,7 @@ func TestAIBridgeAllowBYOK(t *testing.T) {
|
||||
api.AGPL.RegisterInMemoryAIBridgedHTTPHandler(testHandler)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
reqURL := client.URL.String() + "/api/v2/aibridge/test"
|
||||
reqURL := client.URL.String() + "/api/v2/ai-gateway/test"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
|
||||
@@ -5,10 +5,17 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
agplaibridge "github.com/coder/coder/v2/coderd/aibridge"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// API route prefixes for the AI Gateway Proxy and legacy AI Bridge Proxy endpoints.
|
||||
const (
|
||||
AIGatewayProxyPath = agplaibridge.AIGatewayRootPath + "/proxy"
|
||||
AIBridgeProxyPath = agplaibridge.AIBridgeRootPath + "/proxy"
|
||||
)
|
||||
|
||||
// RegisterInMemoryAIBridgeProxydHTTPHandler mounts [aibridgeproxyd.Server]'s HTTP handler
|
||||
// onto [API]'s router, so that requests to aibridgedproxy will be relayed from Coder's API server
|
||||
// to the in-memory aibridgedproxy.
|
||||
@@ -20,8 +27,27 @@ func (api *API) RegisterInMemoryAIBridgeProxydHTTPHandler(srv http.Handler) {
|
||||
api.aibridgeproxydHandler = srv
|
||||
}
|
||||
|
||||
// aibridgeproxyHandler handles AI Bridge Proxy endpoints.
|
||||
func aibridgeproxyHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
|
||||
// aibridgeProxyHTTPHandler returns the legacy /api/v2/aibridge/proxy 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 aibridgeProxyHTTPHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
|
||||
return aiGatewayProxyRoutes(api, AIBridgeProxyPath, middlewares...)
|
||||
}
|
||||
|
||||
// aiGatewayProxyHTTPHandler returns the /api/v2/ai-gateway/proxy route tree.
|
||||
// This shares the same route builder as /aibridge/proxy 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 aiGatewayProxyHTTPHandler(api *API, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
|
||||
return aiGatewayProxyRoutes(api, AIGatewayProxyPath, middlewares...)
|
||||
}
|
||||
|
||||
// aiGatewayProxyRoutes builds the route tree for AI Gateway Proxy endpoints.
|
||||
func aiGatewayProxyRoutes(api *API, stripPrefix string, middlewares ...func(http.Handler) http.Handler) func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge))
|
||||
r.Use(middlewares...)
|
||||
@@ -30,7 +56,7 @@ func aibridgeproxyHandler(api *API, middlewares ...func(http.Handler) http.Handl
|
||||
// Check if the proxy is enabled.
|
||||
if !api.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "AI Bridge Proxy is not enabled.",
|
||||
Message: "AI Gateway Proxy is not enabled.",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -38,13 +64,13 @@ func aibridgeproxyHandler(api *API, middlewares ...func(http.Handler) http.Handl
|
||||
// Check if the handler is registered.
|
||||
if api.aibridgeproxydHandler == nil {
|
||||
httpapi.Write(r.Context(), rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "AI Bridge Proxy handler not mounted.",
|
||||
Message: "AI Gateway Proxy handler not mounted.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Strip the prefix and relay to the aibridgeproxyd handler.
|
||||
http.StripPrefix("/api/v2/aibridge/proxy", api.aibridgeproxydHandler).ServeHTTP(rw, r)
|
||||
http.StripPrefix(stripPrefix, api.aibridgeproxydHandler).ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/ai-gateway/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/ai-gateway/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestAIBridgeProxyCertificateRetrieval(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Make a request to the proxy CA cert endpoint without authentication.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/aibridge/proxy/ca-cert.pem", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, client.URL.String()+"/api/v2/ai-gateway/proxy/ca-cert.pem", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// No session token header set.
|
||||
|
||||
@@ -28,7 +28,7 @@ const nameFormatDetail = "Must be 64 characters or fewer, lowercase letters, num
|
||||
// @Tags Enterprise
|
||||
// @Param request body codersdk.CreateAIGatewayKeyRequest true "Create AI Gateway key request"
|
||||
// @Success 201 {object} codersdk.CreateAIGatewayKeyResponse
|
||||
// @Router /api/v2/aibridge/keys [post]
|
||||
// @Router /api/v2/ai-gateway/keys [post]
|
||||
func (api *API) postAIGatewayKey(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
@@ -116,7 +116,7 @@ func writeKeyInsertError(ctx context.Context, rw http.ResponseWriter, err error)
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Success 200 {array} codersdk.AIGatewayKey
|
||||
// @Router /api/v2/aibridge/keys [get]
|
||||
// @Router /api/v2/ai-gateway/keys [get]
|
||||
func (api *API) aiGatewayKeys(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -146,7 +146,7 @@ func (api *API) aiGatewayKeys(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Tags Enterprise
|
||||
// @Param key path string true "Key ID" format(uuid)
|
||||
// @Success 204
|
||||
// @Router /api/v2/aibridge/keys/{key} [delete]
|
||||
// @Router /api/v2/ai-gateway/keys/{key} [delete]
|
||||
func (api *API) deleteAIGatewayKey(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestAIGatewayKeys(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
fullKey := created.Key
|
||||
|
||||
resp, err := ownerClient.Request(ctx, http.MethodGet, "/api/v2/aibridge/keys", nil)
|
||||
resp, err := ownerClient.Request(ctx, http.MethodGet, "/api/v2/ai-gateway/keys", nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = resp.Body.Close() })
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
@@ -137,7 +137,7 @@ func TestAIGatewayKeys(t *testing.T) {
|
||||
|
||||
// Invalid UUID -> 400 (raw request; SDK method accepts uuid.UUID).
|
||||
//nolint:gocritic // Managing AI Gateway keys is owner-only.
|
||||
resp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/aibridge/keys/not-a-uuid", nil)
|
||||
resp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/ai-gateway/keys/not-a-uuid", nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = resp.Body.Close() })
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
@@ -148,7 +148,7 @@ func TestAIGatewayKeys(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// SDK returns no code on success, using raw request to check for 204.
|
||||
delResp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/aibridge/keys/"+created.ID.String(), nil)
|
||||
delResp, err := ownerClient.Request(ctx, http.MethodDelete, "/api/v2/ai-gateway/keys/"+created.ID.String(), nil)
|
||||
require.NoError(t, err)
|
||||
defer delResp.Body.Close()
|
||||
require.Equal(t, http.StatusNoContent, delResp.StatusCode)
|
||||
@@ -333,7 +333,7 @@ func TestAIGatewayKeysDatabaseErrors(t *testing.T) {
|
||||
name: "CreateDBError",
|
||||
errStore: aiGatewayKeyErrorStore{insertErr: dbErr},
|
||||
method: http.MethodPost,
|
||||
path: "/api/v2/aibridge/keys",
|
||||
path: "/api/v2/ai-gateway/keys",
|
||||
body: codersdk.CreateAIGatewayKeyRequest{Name: "db-err-create"},
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantMsg: "Failed to create key. Please retry.",
|
||||
@@ -342,7 +342,7 @@ func TestAIGatewayKeysDatabaseErrors(t *testing.T) {
|
||||
name: "ListDBError",
|
||||
errStore: aiGatewayKeyErrorStore{listErr: dbErr},
|
||||
method: http.MethodGet,
|
||||
path: "/api/v2/aibridge/keys",
|
||||
path: "/api/v2/ai-gateway/keys",
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantMsg: "Failed to list keys.",
|
||||
},
|
||||
@@ -350,7 +350,7 @@ func TestAIGatewayKeysDatabaseErrors(t *testing.T) {
|
||||
name: "DeleteDBError",
|
||||
errStore: aiGatewayKeyErrorStore{deleteErr: dbErr},
|
||||
method: http.MethodDelete,
|
||||
path: "/api/v2/aibridge/keys/" + uuid.New().String(),
|
||||
path: "/api/v2/ai-gateway/keys/" + uuid.New().String(),
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantMsg: "Failed to delete key.",
|
||||
},
|
||||
|
||||
@@ -291,16 +291,27 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
return api.refreshEntitlements(ctx)
|
||||
}
|
||||
|
||||
// Legacy aibridge routes: kept for backward compatibility.
|
||||
// New endpoints should be added to /ai-gateway only.
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/aibridge", aibridgeHandler(api, apiKeyMiddleware))
|
||||
r.Route("/aibridge", aibridgeHTTPHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/aibridge/proxy", aibridgeproxyHandler(api, apiKeyMiddleware))
|
||||
r.Route("/aibridge/proxy", aibridgeProxyHTTPHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
// AI Gateway routes: canonical aliases for the aibridge endpoints.
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/ai-gateway", aiGatewayHTTPHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/aibridge/keys", func(r chi.Router) {
|
||||
r.Route("/ai-gateway/proxy", aiGatewayProxyHTTPHandler(api, apiKeyMiddleware))
|
||||
})
|
||||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Route("/ai-gateway/keys", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
|
||||
Reference in New Issue
Block a user