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:
@@ -35,6 +35,15 @@ const (
|
||||
BaseURLChatGPT = "https://" + HostChatGPT + "/backend-api/codex"
|
||||
)
|
||||
|
||||
// API route prefixes for the AI Gateway and legacy AI Bridge endpoints.
|
||||
const (
|
||||
// AIGatewayRootPath is the URL prefix the AI Gateway handler
|
||||
// registers all of its routes under.
|
||||
AIGatewayRootPath = "/api/v2/ai-gateway"
|
||||
// AIBridgeRootPath is the legacy prefix kept for backward compatibility.
|
||||
AIBridgeRootPath = "/api/v2/aibridge"
|
||||
)
|
||||
|
||||
// IsBYOK reports whether the request is using BYOK mode, determined
|
||||
// by the presence of the X-Coder-AI-Governance-Token header.
|
||||
func IsBYOK(header http.Header) bool {
|
||||
|
||||
@@ -59,7 +59,7 @@ func DelegatedAPIKeyIDFromContext(ctx context.Context) (string, bool) {
|
||||
//
|
||||
// The returned RoundTripper is responsible for adapting the caller's request
|
||||
// to the aibridge daemon's mount path: callers hand it an upstream-shaped
|
||||
// request and the transport rewrites URL.Path to "/api/v2/aibridge/<name>/..."
|
||||
// request and the transport rewrites URL.Path to "/api/v2/ai-gateway/<name>/..."
|
||||
// before dispatching. Routing keys on the provider's instance name so callers
|
||||
// can use the same string the proxy daemon and the bridge mount use.
|
||||
//
|
||||
|
||||
+8
-9
@@ -19,13 +19,12 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk/drpcsdk"
|
||||
)
|
||||
|
||||
// GetAIBridgedHandler returns the in-memory aibridge HTTP handler set by
|
||||
// [API.RegisterInMemoryAIBridgedHTTPHandler], or nil if the daemon has not
|
||||
// been wired in. Used by the enterprise /api/v2/aibridge route (license-gated)
|
||||
// to forward requests into the same in-memory handler that chatd dispatches
|
||||
// to in-process.
|
||||
func (api *API) GetAIBridgedHandler() http.Handler {
|
||||
return api.aibridgedHandler
|
||||
// AIGatewayHandler returns the in-memory AI Gateway HTTP handler
|
||||
// set by [API.RegisterInMemoryAIBridgedHTTPHandler], or nil if the daemon
|
||||
// has not been wired in. Callers must apply their own [http.StripPrefix]
|
||||
// for the route prefix they are mounting under.
|
||||
func (api *API) AIGatewayHandler() http.Handler {
|
||||
return api.aiGatewayHandler
|
||||
}
|
||||
|
||||
// RegisterInMemoryAIBridgedHTTPHandler mounts [aibridged.Server]'s HTTP router onto
|
||||
@@ -42,9 +41,9 @@ func (api *API) RegisterInMemoryAIBridgedHTTPHandler(srv http.Handler) {
|
||||
panic("aibridged cannot be nil")
|
||||
}
|
||||
|
||||
api.aibridgedHandler = http.StripPrefix("/api/v2/aibridge", srv)
|
||||
api.aiGatewayHandler = srv
|
||||
|
||||
factory := aibridged.NewTransportFactory(api.aibridgedHandler)
|
||||
factory := aibridged.NewTransportFactory(http.StripPrefix(agplaibridge.AIGatewayRootPath, srv))
|
||||
var asInterface agplaibridge.TransportFactory = factory
|
||||
api.AIBridgeTransportFactory.Store(&asInterface)
|
||||
}
|
||||
|
||||
@@ -12,13 +12,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/aibridge"
|
||||
)
|
||||
|
||||
// aibridgeRootPath is the URL prefix the in-memory aibridged handler
|
||||
// registers all of its routes under. The in-process round-tripper
|
||||
// prepends this plus the provider name to every request before
|
||||
// dispatch so callers can hand it upstream-shaped requests without
|
||||
// knowing the daemon's mount layout.
|
||||
const aibridgeRootPath = "/api/v2/aibridge"
|
||||
|
||||
// NewTransportFactory returns an [aibridge.TransportFactory] whose RoundTripper
|
||||
// dispatches requests to handler in-process, streaming the response body
|
||||
// through an [io.Pipe] so SSE/NDJSON/chunked responses propagate token-by-token
|
||||
@@ -37,7 +30,7 @@ type transportFactory struct {
|
||||
// TransportFor returns an in-process [http.RoundTripper] that dispatches
|
||||
// requests through the aibridged handler. The provider name is the routing
|
||||
// key the daemon mounts on; the round-tripper rewrites each request's URL
|
||||
// path to "/api/v2/aibridge/<providerName>/..." before dispatching so
|
||||
// path to "/api/v2/ai-gateway/<providerName>/..." before dispatching so
|
||||
// callers can build upstream-shaped requests and stay agnostic of the
|
||||
// daemon's mount layout. The source is attached to the request context for
|
||||
// downstream logging; routing does not depend on it.
|
||||
@@ -69,10 +62,10 @@ func (t *inMemoryRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
|
||||
}
|
||||
|
||||
// Adapt the caller's upstream-shaped URL to the daemon's mount layout:
|
||||
// "/api/v2/aibridge/<providerName>/<original-path>". Done here so
|
||||
// "/api/v2/ai-gateway/<providerName>/<original-path>". Done here so
|
||||
// callers do not need to encode the mount prefix or the provider
|
||||
// routing key into the requests they hand to the transport.
|
||||
newPath, err := url.JoinPath(aibridgeRootPath, t.providerName, req.URL.Path)
|
||||
newPath, err := url.JoinPath(aibridge.AIGatewayRootPath, t.providerName, req.URL.Path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("rewrite request URL for provider %q: %w", t.providerName, err)
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestTransportFactory_TransportFor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The round-tripper must adapt an upstream-shaped URL.Path
|
||||
// ("/v1/messages") to the aibridge mount layout
|
||||
// ("/api/v2/aibridge/<provider>/v1/messages") so callers don't
|
||||
// ("/v1/messages") to the ai-gateway mount layout
|
||||
// ("/api/v2/ai-gateway/<provider>/v1/messages") so callers don't
|
||||
// have to encode the daemon's routing key into their requests.
|
||||
got := make(chan string, 1)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -73,7 +73,7 @@ func TestTransportFactory_TransportFor(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, "/api/v2/aibridge/my-anthropic/v1/messages", <-got)
|
||||
require.Equal(t, "/api/v2/ai-gateway/my-anthropic/v1/messages", <-got)
|
||||
require.Equal(t, origPath, req.URL.Path,
|
||||
"caller's request URL must not be mutated by RoundTrip")
|
||||
})
|
||||
|
||||
Generated
+257
-253
@@ -1380,6 +1380,263 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/clients": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Gateway"
|
||||
],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/keys": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/models": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Gateway"
|
||||
],
|
||||
"summary": "List AI Bridge models",
|
||||
"operationId": "list-ai-bridge-models",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/sessions": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Gateway"
|
||||
],
|
||||
"summary": "List AI Bridge sessions",
|
||||
"operationId": "list-ai-bridge-sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Cursor pagination after session ID (cannot be used with offset)",
|
||||
"name": "after_session_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset pagination (cannot be used with after_session_id)",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/sessions/{session_id}": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Gateway"
|
||||
],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai/providers": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -1549,259 +1806,6 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/clients": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/models": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "List AI Bridge models",
|
||||
"operationId": "list-ai-bridge-models",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/sessions": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "List AI Bridge sessions",
|
||||
"operationId": "list-ai-bridge-sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Cursor pagination after session ID (cannot be used with offset)",
|
||||
"name": "after_session_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset pagination (cannot be used with after_session_id)",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/sessions/{session_id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/appearance": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
||||
Generated
+229
-225
@@ -1223,6 +1223,235 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/clients": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/clients for backward compatibility.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Gateway"],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/keys": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/models": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/models for backward compatibility.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Gateway"],
|
||||
"summary": "List AI Bridge models",
|
||||
"operationId": "list-ai-bridge-models",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/sessions": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/sessions for backward compatibility.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Gateway"],
|
||||
"summary": "List AI Bridge sessions",
|
||||
"operationId": "list-ai-bridge-sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Cursor pagination after session ID (cannot be used with offset)",
|
||||
"name": "after_session_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset pagination (cannot be used with after_session_id)",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai-gateway/sessions/{session_id}": {
|
||||
"get": {
|
||||
"description": "Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward compatibility.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Gateway"],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ai/providers": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -1370,231 +1599,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/clients": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/models": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "List AI Bridge models",
|
||||
"operationId": "list-ai-bridge-models",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/sessions": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "List AI Bridge sessions",
|
||||
"operationId": "list-ai-bridge-sessions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, client, session_id, started_after, started_before.",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Page limit",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Cursor pagination after session ID (cannot be used with offset)",
|
||||
"name": "after_session_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset pagination (cannot be used with after_session_id)",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeListSessionsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/sessions/{session_id}": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/appearance": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
|
||||
+6
-5
@@ -2235,11 +2235,12 @@ type API struct {
|
||||
// providers directly. Registered by coderd at startup once aibridged is
|
||||
// wired in-memory.
|
||||
AIBridgeTransportFactory atomic.Pointer[aibridge.TransportFactory]
|
||||
// aibridgedHandler is the in-memory aibridge HTTP handler. Set by
|
||||
// RegisterInMemoryAIBridgedHTTPHandler; read both by the enterprise
|
||||
// /api/v2/aibridge route (license-gated) and by the in-memory transport
|
||||
// (used by chatd, license-exempt).
|
||||
aibridgedHandler http.Handler
|
||||
// aiGatewayHandler is the in-memory AI Gateway HTTP handler
|
||||
// (no prefix stripping). Set by RegisterInMemoryAIBridgedHTTPHandler,
|
||||
// used by the enterprise /api/v2/aibridge and /api/v2/ai-gateway
|
||||
// routes (license-gated) which apply their own StripPrefix, and by
|
||||
// the in-memory transport (used by chatd, license-exempt).
|
||||
aiGatewayHandler http.Handler
|
||||
|
||||
UpdatesProvider tailnet.WorkspaceUpdatesProvider
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/aibridge"
|
||||
)
|
||||
|
||||
type SwaggerComment struct {
|
||||
@@ -168,6 +170,14 @@ func isExperimentalEndpoint(route string) bool {
|
||||
return strings.HasPrefix(route, "/api/v2/workspaceagents/me/experimental/")
|
||||
}
|
||||
|
||||
// isLegacyAIBridgeAlias returns true for /api/v2/aibridge routes that are
|
||||
// backward-compatibility aliases of /api/v2/ai-gateway. The swagger
|
||||
// annotations live on the canonical /ai-gateway paths, so the legacy
|
||||
// routes have no matching annotation and must be skipped.
|
||||
func isLegacyAIBridgeAlias(route string) bool {
|
||||
return strings.HasPrefix(route, aibridge.AIBridgeRootPath+"/")
|
||||
}
|
||||
|
||||
func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments []SwaggerComment, opts ...SwaggerOption) {
|
||||
cfg := swaggerOptions{}
|
||||
for _, opt := range opts {
|
||||
@@ -206,6 +216,9 @@ func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments [
|
||||
if isExperimentalEndpoint(route) {
|
||||
return
|
||||
}
|
||||
if isLegacyAIBridgeAlias(route) {
|
||||
return
|
||||
}
|
||||
|
||||
c := findSwaggerCommentByMethodAndRoute(swaggerComments, method, route)
|
||||
assert.NotNil(t, c, "Missing @Router annotation")
|
||||
|
||||
Reference in New Issue
Block a user