Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c25da5399 | |||
| 3155d11c3a | |||
| 1df18d433d | |||
| 34fedfc4eb | |||
| aca10a2e97 |
Generated
+47
@@ -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": {
|
||||
|
||||
Generated
+41
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+53
@@ -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).
|
||||
|
||||
Generated
+14
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -23,6 +23,7 @@ func (r *RootCmd) aibridge() *serpent.Command {
|
||||
},
|
||||
Children: []*serpent.Command{
|
||||
r.aibridgeInterceptions(),
|
||||
r.aibridgeLogRequests(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
Generated
+5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user