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
+9
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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)
}
+3 -10
View File
@@ -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)
}
+3 -3
View File
@@ -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")
})
+257 -253
View File
@@ -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": [
+229 -225
View File
@@ -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
View File
@@ -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
View File
@@ -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")