Compare commits

...

5 Commits

Author SHA1 Message Date
Danny Kopping 0c25da5399 chore: minor fixes
Signed-off-by: Danny Kopping <danny@coder.com>
2025-10-22 20:23:15 +02:00
Danny Kopping 3155d11c3a chore: no replace
Signed-off-by: Danny Kopping <danny@coder.com>
2025-10-22 20:23:14 +02:00
Danny Kopping 1df18d433d chore: add race protection
Signed-off-by: Danny Kopping <danny@coder.com>
2025-10-22 20:22:48 +02:00
Danny Kopping 34fedfc4eb chore: add route for enabling/disabling logging, CLI command
Signed-off-by: Danny Kopping <danny@coder.com>
2025-10-22 20:22:48 +02:00
Danny Kopping aca10a2e97 chore: request logging
Signed-off-by: Danny Kopping <danny@coder.com>
2025-10-22 20:22:46 +02:00
14 changed files with 377 additions and 12 deletions
+47
View File
@@ -136,6 +136,45 @@ const docTemplate = `{
}
}
},
"/api/experimental/aibridge/log-requests": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AIBridge"
],
"summary": "Set AI Bridge request logging",
"operationId": "set-aibridge-request-logging",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.AIBridgeSetRequestLoggingRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/api/experimental/tasks": {
"get": {
"security": [
@@ -11749,6 +11788,14 @@ const docTemplate = `{
}
}
},
"codersdk.AIBridgeSetRequestLoggingRequest": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"codersdk.AIBridgeTokenUsage": {
"type": "object",
"properties": {
+41
View File
@@ -112,6 +112,39 @@
}
}
},
"/api/experimental/aibridge/log-requests": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["AIBridge"],
"summary": "Set AI Bridge request logging",
"operationId": "set-aibridge-request-logging",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.AIBridgeSetRequestLoggingRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/api/experimental/tasks": {
"get": {
"security": [
@@ -10449,6 +10482,14 @@
}
}
},
"codersdk.AIBridgeSetRequestLoggingRequest": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"codersdk.AIBridgeTokenUsage": {
"type": "object",
"properties": {
+18
View File
@@ -124,3 +124,21 @@ func (c *ExperimentalClient) AIBridgeListInterceptions(ctx context.Context, filt
var resp AIBridgeListInterceptionsResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
type AIBridgeSetRequestLoggingRequest struct {
Enabled bool `json:"enabled"`
}
// AIBridgeSetRequestLogging toggles upstream request/response logging for AI Bridge providers.
// Only users with the owner role can toggle request logging.
func (c *ExperimentalClient) AIBridgeSetRequestLogging(ctx context.Context, req AIBridgeSetRequestLoggingRequest) error {
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/aibridge/log-requests", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ReadBodyAsError(res)
}
return nil
}
+53
View File
@@ -101,3 +101,56 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/aibridge/intercepti
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.AIBridgeListInterceptionsResponse](schemas.md#codersdkaibridgelistinterceptionsresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Set AI Bridge request logging
### Code samples
```shell
# Example request using curl
curl -X POST http://coder-server:8080/api/v2/api/experimental/aibridge/log-requests \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`POST /api/experimental/aibridge/log-requests`
> Body parameter
```json
{
"enabled": true
}
```
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------------------------------------------------------------------------------------------|----------|--------------|
| `body` | body | [codersdk.AIBridgeSetRequestLoggingRequest](schemas.md#codersdkaibridgesetrequestloggingrequest) | true | Request body |
### Example responses
> 200 Response
```json
{
"detail": "string",
"message": "string",
"validations": [
{
"detail": "string",
"field": "string"
}
]
}
```
### Responses
| Status | Meaning | Description | Schema |
|--------|---------------------------------------------------------|-------------|--------------------------------------------------|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+14
View File
@@ -604,6 +604,20 @@
| `base_url` | string | false | | |
| `key` | string | false | | |
## codersdk.AIBridgeSetRequestLoggingRequest
```json
{
"enabled": true
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------|---------|----------|--------------|-------------|
| `enabled` | boolean | false | | |
## codersdk.AIBridgeTokenUsage
```json
+79
View File
@@ -0,0 +1,79 @@
package cli
import (
"fmt"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) aibridgeLogRequests() *serpent.Command {
var (
enable bool
disable bool
)
cmd := &serpent.Command{
Use: "log-requests",
Short: "Toggle upstream request/response logging for AI Bridge providers",
Long: cli.FormatExamples(
cli.Example{
Description: "Enable request logging (owner only)",
Command: "coder exp aibridge log-requests --enable",
},
cli.Example{
Description: "Disable request logging (owner only)",
Command: "coder exp aibridge log-requests --disable",
},
),
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
),
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
ctx := inv.Context()
if enable == disable {
return xerrors.New("must specify either --enable or --disable")
}
experimental := codersdk.NewExperimentalClient(client)
err = experimental.AIBridgeSetRequestLogging(ctx, codersdk.AIBridgeSetRequestLoggingRequest{
Enabled: enable,
})
if err != nil {
return err
}
state := "disabled"
if enable {
state = "enabled"
}
_, err = fmt.Fprintf(inv.Stdout, "Request logging %s successfully.\n", cliui.Bold(state))
return err
},
Options: serpent.OptionSet{
{
Flag: "enable",
Description: "Enable request logging.",
Value: serpent.BoolOf(&enable),
},
{
Flag: "disable",
Description: "Disable request logging.",
Value: serpent.BoolOf(&disable),
},
},
}
return cmd
}
+28 -9
View File
@@ -4,11 +4,13 @@ package cli
import (
"context"
"os"
"golang.org/x/xerrors"
"github.com/coder/aibridge"
"github.com/coder/coder/v2/enterprise/coderd"
aibridgepkg "github.com/coder/coder/v2/enterprise/coderd/aibridge"
"github.com/coder/coder/v2/enterprise/x/aibridged"
)
@@ -19,16 +21,33 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) {
logger := coderAPI.Logger.Named("aibridged")
// Setup supported providers.
providers := []aibridge.Provider{
aibridge.NewOpenAIProvider(aibridge.ProviderConfig{
BaseURL: coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(),
Key: coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String(),
}),
aibridge.NewAnthropicProvider(aibridge.ProviderConfig{
BaseURL: coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(),
Key: coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String(),
}),
openAIConfig := aibridge.NewProviderConfig(
coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.BaseURL.String(),
coderAPI.DeploymentValues.AI.BridgeConfig.OpenAI.Key.String(),
os.TempDir(), // TODO: configurable?
)
anthropicConfig := aibridge.NewProviderConfig(
coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.BaseURL.String(),
coderAPI.DeploymentValues.AI.BridgeConfig.Anthropic.Key.String(),
os.TempDir(), // TODO: configurable?
)
openAIProvider, err := aibridge.NewOpenAIProvider(openAIConfig)
if err != nil {
return nil, xerrors.Errorf("create openai provider: %w", err)
}
anthropicProvider, err := aibridge.NewAnthropicProvider(anthropicConfig)
if err != nil {
return nil, xerrors.Errorf("create anthropic provider: %w", err)
}
providers := []aibridge.Provider{
openAIProvider,
anthropicProvider,
}
// Store provider configs so we can update them when logging is toggled.
aibridgepkg.SetProviderConfigs([]*aibridge.ProviderConfig{openAIConfig, anthropicConfig})
// Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user).
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool")) // TODO: configurable.
+1
View File
@@ -23,6 +23,7 @@ func (r *RootCmd) aibridge() *serpent.Command {
},
Children: []*serpent.Command{
r.aibridgeInterceptions(),
r.aibridgeLogRequests(),
},
}
return cmd
+37
View File
@@ -0,0 +1,37 @@
package aibridge
import (
"sync"
"sync/atomic"
"github.com/coder/aibridge"
)
var (
upstreamLoggingEnabled atomic.Bool
providerConfigsMu sync.Mutex
providerConfigs []*aibridge.ProviderConfig
)
// SetProviderConfigs stores the provider configs so they can be updated at runtime.
func SetProviderConfigs(configs []*aibridge.ProviderConfig) {
providerConfigsMu.Lock()
defer providerConfigsMu.Unlock()
providerConfigs = configs
}
// SetUpstreamLoggingEnabled sets whether upstream request/response logging is enabled
// and updates all registered provider configs.
func SetUpstreamLoggingEnabled(enabled bool) {
providerConfigsMu.Lock()
defer providerConfigsMu.Unlock()
upstreamLoggingEnabled.Store(enabled)
// Update all provider configs.
for _, cfg := range providerConfigs {
if cfg != nil {
cfg.SetEnableUpstreamLogging(enabled)
}
}
}
+42
View File
@@ -12,8 +12,14 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/drpcsdk"
aibridgepkg "github.com/coder/coder/v2/enterprise/coderd/aibridge"
"github.com/coder/coder/v2/enterprise/x/aibridged"
aibridgedproto "github.com/coder/coder/v2/enterprise/x/aibridged/proto"
"github.com/coder/coder/v2/enterprise/x/aibridgedserver"
@@ -101,3 +107,39 @@ func (api *API) CreateInMemoryAIBridgeServer(dialCtx context.Context) (client ai
DRPCAuthorizerClient: aibridgedproto.NewDRPCAuthorizerClient(clientSession),
}, nil
}
// aiBridgeSetRequestLogging toggles upstream request/response logging for AI Bridge providers.
//
// @Summary Set AI Bridge request logging
// @ID set-aibridge-request-logging
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags AIBridge
// @Param request body codersdk.AIBridgeSetRequestLoggingRequest true "Request body"
// @Success 200 {object} codersdk.Response
// @Router /api/experimental/aibridge/log-requests [post]
func (api *API) aiBridgeSetRequestLogging(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
httpapi.Forbidden(rw)
return
}
var req codersdk.AIBridgeSetRequestLoggingRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
aibridgepkg.SetUpstreamLoggingEnabled(req.Enabled)
api.Logger.Info(ctx, "upstream request logging state changed",
slog.F("enabled", req.Enabled),
slog.F("user_id", httpmw.APIKey(r).UserID),
)
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
Message: "Request logging state updated successfully.",
})
}
+1
View File
@@ -235,6 +235,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/interceptions", api.aiBridgeListInterceptions)
r.Post("/log-requests", api.aiBridgeSetRequestLogging)
})
// This is a bit funky but since aibridge only exposes a HTTP
@@ -164,7 +164,9 @@ func TestIntegration(t *testing.T) {
require.NoError(t, err)
logger := testutil.Logger(t)
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.ProviderConfig{BaseURL: mockOpenAI.URL})}
openAIProvider, err := aibridge.NewOpenAIProvider(aibridge.NewProviderConfig(mockOpenAI.URL, "", ""))
require.NoError(t, err)
providers := []aibridge.Provider{openAIProvider}
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
require.NoError(t, err)
+8 -2
View File
@@ -287,10 +287,16 @@ func TestRouting(t *testing.T) {
ctrl := gomock.NewController(t)
client := mock.NewMockDRPCClient(ctrl)
openAIProvider, err := aibridge.NewOpenAIProvider(aibridge.NewProviderConfig(openaiSrv.URL, "", ""))
require.NoError(t, err)
anthropicProvider, err := aibridge.NewAnthropicProvider(aibridge.NewProviderConfig(antSrv.URL, "", ""))
require.NoError(t, err)
providers := []aibridge.Provider{
aibridge.NewOpenAIProvider(aibridge.ProviderConfig{BaseURL: openaiSrv.URL}),
aibridge.NewAnthropicProvider(aibridge.ProviderConfig{BaseURL: antSrv.URL}),
openAIProvider,
anthropicProvider,
}
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger)
require.NoError(t, err)
conn := &mockDRPCConn{}
+5
View File
@@ -49,6 +49,11 @@ export interface AIBridgeOpenAIConfig {
readonly key: string;
}
// From codersdk/aibridge.go
export interface AIBridgeSetRequestLoggingRequest {
readonly enabled: boolean;
}
// From codersdk/aibridge.go
export interface AIBridgeTokenUsage {
readonly id: string;