interceptions](./aibridge_interceptions.md) | Manage AI Bridge interceptions. |
diff --git a/docs/reference/cli/aibridge_interceptions.md b/docs/reference/cli/aibridge_interceptions.md
deleted file mode 100644
index 80c2135b07..0000000000
--- a/docs/reference/cli/aibridge_interceptions.md
+++ /dev/null
@@ -1,16 +0,0 @@
-
-# aibridge interceptions
-
-Manage AI Bridge interceptions.
-
-## Usage
-
-```console
-coder aibridge interceptions
-```
-
-## Subcommands
-
-| Name | Purpose |
-|-------------------------------------------------------|---------------------------------------|
-| [list](./aibridge_interceptions_list.md) | List AI Bridge interceptions as JSON. |
diff --git a/docs/reference/cli/aibridge_interceptions_list.md b/docs/reference/cli/aibridge_interceptions_list.md
deleted file mode 100644
index 796032edbe..0000000000
--- a/docs/reference/cli/aibridge_interceptions_list.md
+++ /dev/null
@@ -1,85 +0,0 @@
-
-# aibridge interceptions list
-
-List AI Bridge interceptions as JSON.
-
-## Usage
-
-```console
-coder aibridge interceptions list [flags]
-```
-
-## Options
-
-### --initiator
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions initiated by this user. Accepts a user ID, username, or "me".
-
-### --started-before
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions started before this time. Must be after 'started-after' if set. Accepts a time in the RFC 3339 format, e.g. "2006-01-02T15:04:05Z07:00".
-
-### --started-after
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions started after this time. Must be before 'started-before' if set. Accepts a time in the RFC 3339 format, e.g. "2006-01-02T15:04:05Z07:00".
-
-### --provider
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions from this provider.
-
-### --provider-name
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions from the named provider.
-
-### --model
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions from this model.
-
-### --client
-
-| | |
-|------|---------------------|
-| Type | string |
-
-Only return interceptions from this client.
-
-### --after-id
-
-| | |
-|------|---------------------|
-| Type | string |
-
-The ID of the last result on the previous page to use as a pagination cursor.
-
-### --limit
-
-| | |
-|---------|------------------|
-| Type | int |
-| Default | 100 |
-
-The limit of results to return. Must be between 1 and 1000.
diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md
index bbb7e85a31..a25269fd06 100644
--- a/docs/reference/cli/index.md
+++ b/docs/reference/cli/index.md
@@ -72,7 +72,6 @@ Coder — A tool for provisioning self-hosted development environments with Terr
| [groups](./groups.md) | Manage groups |
| [prebuilds](./prebuilds.md) | Manage Coder prebuilds |
| [external-workspaces](./external-workspaces.md) | Create or manage external workspaces |
-| [aibridge](./aibridge.md) | Manage AI Bridge. |
## Options
diff --git a/enterprise/cli/aibridge.go b/enterprise/cli/aibridge.go
deleted file mode 100644
index d809580bd3..0000000000
--- a/enterprise/cli/aibridge.go
+++ /dev/null
@@ -1,181 +0,0 @@
-package cli
-
-import (
- "encoding/json"
- "fmt"
- "time"
-
- "github.com/google/uuid"
- "golang.org/x/xerrors"
-
- "github.com/coder/coder/v2/codersdk"
- "github.com/coder/serpent"
-)
-
-const maxInterceptionsLimit = 1000
-
-func (r *RootCmd) aibridge() *serpent.Command {
- cmd := &serpent.Command{
- Use: "aibridge",
- Short: "Manage AI Bridge.",
- Handler: func(inv *serpent.Invocation) error {
- return inv.Command.HelpHandler(inv)
- },
- Children: []*serpent.Command{
- r.aibridgeInterceptions(),
- },
- }
- return cmd
-}
-
-func (r *RootCmd) aibridgeInterceptions() *serpent.Command {
- cmd := &serpent.Command{
- Use: "interceptions",
- Short: "Manage AI Bridge interceptions.",
- Handler: func(inv *serpent.Invocation) error {
- return inv.Command.HelpHandler(inv)
- },
- Children: []*serpent.Command{
- r.aibridgeInterceptionsList(),
- },
- }
- return cmd
-}
-
-func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command {
- var (
- initiator string
- startedBeforeRaw string
- startedAfterRaw string
- provider string
- providerName string
- model string
- client string
- afterIDRaw string
- limit int64
- )
-
- return &serpent.Command{
- Use: "list",
- Short: "List AI Bridge interceptions as JSON.",
- Options: serpent.OptionSet{
- {
- Flag: "initiator",
- Description: `Only return interceptions initiated by this user. Accepts a user ID, username, or "me".`,
- Default: "",
- Value: serpent.StringOf(&initiator),
- },
- {
- Flag: "started-before",
- Description: fmt.Sprintf("Only return interceptions started before this time. Must be after 'started-after' if set. Accepts a time in the RFC 3339 format, e.g. %q.", time.RFC3339),
- Default: "",
- Value: serpent.StringOf(&startedBeforeRaw),
- },
- {
- Flag: "started-after",
- Description: fmt.Sprintf("Only return interceptions started after this time. Must be before 'started-before' if set. Accepts a time in the RFC 3339 format, e.g. %q.", time.RFC3339),
- Default: "",
- Value: serpent.StringOf(&startedAfterRaw),
- },
- {
- Flag: "provider",
- Description: `Only return interceptions from this provider.`,
- Default: "",
- Value: serpent.StringOf(&provider),
- },
- {
- Flag: "provider-name",
- Description: `Only return interceptions from the named provider.`,
- Default: "",
- Value: serpent.StringOf(&providerName),
- },
- {
- Flag: "model",
- Description: `Only return interceptions from this model.`,
- Default: "",
- Value: serpent.StringOf(&model),
- },
- {
- Flag: "client",
- Description: `Only return interceptions from this client.`,
- Default: "",
- Value: serpent.StringOf(&client),
- },
- {
- Flag: "after-id",
- Description: "The ID of the last result on the previous page to use as a pagination cursor.",
- Default: "",
- Value: serpent.StringOf(&afterIDRaw),
- },
- {
- Flag: "limit",
- Description: fmt.Sprintf(`The limit of results to return. Must be between 1 and %d.`, maxInterceptionsLimit),
- Default: "100",
- Value: serpent.Int64Of(&limit),
- },
- },
- Handler: func(inv *serpent.Invocation) error {
- serpetClient, err := r.InitClient(inv)
- if err != nil {
- return err
- }
-
- startedBefore := time.Time{}
- if startedBeforeRaw != "" {
- startedBefore, err = time.Parse(time.RFC3339, startedBeforeRaw)
- if err != nil {
- return xerrors.Errorf("parse started before filter value %q: %w", startedBeforeRaw, err)
- }
- }
-
- startedAfter := time.Time{}
- if startedAfterRaw != "" {
- startedAfter, err = time.Parse(time.RFC3339, startedAfterRaw)
- if err != nil {
- return xerrors.Errorf("parse started after filter value %q: %w", startedAfterRaw, err)
- }
- }
-
- afterID := uuid.Nil
- if afterIDRaw != "" {
- afterID, err = uuid.Parse(afterIDRaw)
- if err != nil {
- return xerrors.Errorf("parse after_id filter value %q: %w", afterIDRaw, err)
- }
- }
-
- if limit < 1 || limit > maxInterceptionsLimit {
- return xerrors.Errorf("limit value must be between 1 and %d", maxInterceptionsLimit)
- }
-
- resp, err := serpetClient.AIBridgeListInterceptions(inv.Context(), codersdk.AIBridgeListInterceptionsFilter{
- Pagination: codersdk.Pagination{
- AfterID: afterID,
- // #nosec G115 - Checked above.
- Limit: int(limit),
- },
- Client: client,
- Initiator: initiator,
- StartedBefore: startedBefore,
- StartedAfter: startedAfter,
- Provider: provider,
- ProviderName: providerName,
- Model: model,
- })
- if err != nil {
- return xerrors.Errorf("list interceptions: %w", err)
- }
-
- // We currently only support JSON output, so we don't use a
- // formatter.
- enc := json.NewEncoder(inv.Stdout)
- enc.SetIndent("", " ")
- err = enc.Encode(resp.Results)
- if err != nil {
- return err
- }
-
- return err
- },
- }
-}
diff --git a/enterprise/cli/aibridge_test.go b/enterprise/cli/aibridge_test.go
deleted file mode 100644
index 018d7bb0c9..0000000000
--- a/enterprise/cli/aibridge_test.go
+++ /dev/null
@@ -1,274 +0,0 @@
-package cli_test
-
-import (
- "bytes"
- "encoding/json"
- "testing"
- "time"
-
- "github.com/google/uuid"
- "github.com/stretchr/testify/require"
-
- "github.com/coder/coder/v2/cli/clitest"
- "github.com/coder/coder/v2/coderd/coderdtest"
- "github.com/coder/coder/v2/coderd/database"
- "github.com/coder/coder/v2/coderd/database/dbgen"
- "github.com/coder/coder/v2/coderd/database/dbtime"
- "github.com/coder/coder/v2/codersdk"
- "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/v2/enterprise/coderd/license"
- "github.com/coder/coder/v2/testutil"
-)
-
-func TestAIBridgeListInterceptions(t *testing.T) {
- t.Parallel()
-
- t.Run("OK", func(t *testing.T) {
- t.Parallel()
-
- dv := coderdtest.DeploymentValues(t)
- dv.AI.BridgeConfig.Enabled = true
- ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureAIBridge: 1,
- },
- },
- })
- _, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
- now := dbtime.Now()
- interception1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: member.ID,
- StartedAt: now.Add(-time.Hour),
- }, &now)
- interception2EndedAt := now.Add(time.Minute)
- interception2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: member.ID,
- StartedAt: now,
- }, &interception2EndedAt)
- interception3EndedAt := now.Add(-time.Hour)
- interception3 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: owner.UserID,
- StartedAt: now.Add(-2 * time.Hour),
- }, &interception3EndedAt)
-
- args := []string{
- "aibridge",
- "interceptions",
- "list",
- }
- inv, root := newCLI(t, args...)
- //nolint:gocritic // Owner can read all interceptions.
- clitest.SetupConfig(t, ownerClient, root)
-
- ctx := testutil.Context(t, testutil.WaitLong)
-
- out := bytes.NewBuffer(nil)
- inv.Stdout = out
- err := inv.WithContext(ctx).Run()
- require.NoError(t, err)
-
- // Owner sees all interceptions. Ordered by started_at DESC.
- requireHasInterceptions(t, out.Bytes(), []uuid.UUID{interception2.ID, interception1.ID, interception3.ID})
- })
-
- t.Run("Filter", func(t *testing.T) {
- t.Parallel()
-
- dv := coderdtest.DeploymentValues(t)
- dv.AI.BridgeConfig.Enabled = true
- ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureAIBridge: 1,
- },
- },
- })
- _, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
-
- now := dbtime.Now()
-
- // This interception should be returned since it matches all filters.
- goodInterceptionEndedAt := now.Add(time.Minute)
- goodInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: member.ID,
- Provider: "real-provider",
- Model: "real-model",
- StartedAt: now,
- }, &goodInterceptionEndedAt)
-
- // These interceptions should not be returned since they don't match the
- // filters.
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: owner.UserID,
- Provider: goodInterception.Provider,
- Model: goodInterception.Model,
- StartedAt: goodInterception.StartedAt,
- }, nil)
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: goodInterception.InitiatorID,
- Provider: "bad-provider",
- Model: goodInterception.Model,
- StartedAt: goodInterception.StartedAt,
- }, nil)
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: goodInterception.InitiatorID,
- Provider: goodInterception.Provider,
- Model: "bad-model",
- StartedAt: goodInterception.StartedAt,
- }, nil)
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: goodInterception.InitiatorID,
- Provider: goodInterception.Provider,
- Model: goodInterception.Model,
- // Violates the started after filter.
- StartedAt: now.Add(-2 * time.Hour),
- }, nil)
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: goodInterception.InitiatorID,
- Provider: goodInterception.Provider,
- Model: goodInterception.Model,
- // Violates the started before filter.
- StartedAt: now.Add(2 * time.Hour),
- }, nil)
-
- args := []string{
- "aibridge",
- "interceptions",
- "list",
- "--started-after", now.Add(-time.Hour).Format(time.RFC3339),
- "--started-before", now.Add(time.Hour).Format(time.RFC3339),
- "--initiator", member.Username,
- "--provider", goodInterception.Provider,
- "--model", goodInterception.Model,
- }
- inv, root := newCLI(t, args...)
- //nolint:gocritic // Owner can read all interceptions.
- clitest.SetupConfig(t, ownerClient, root)
-
- ctx := testutil.Context(t, testutil.WaitLong)
-
- out := bytes.NewBuffer(nil)
- inv.Stdout = out
- err := inv.WithContext(ctx).Run()
- require.NoError(t, err)
-
- requireHasInterceptions(t, out.Bytes(), []uuid.UUID{goodInterception.ID})
- })
-
- t.Run("FilterByMe", func(t *testing.T) {
- t.Parallel()
-
- dv := coderdtest.DeploymentValues(t)
- dv.AI.BridgeConfig.Enabled = true
- ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureAIBridge: 1,
- },
- },
- })
- memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
-
- now := dbtime.Now()
-
- // Create an interception initiated by the member.
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: member.ID,
- StartedAt: now,
- }, nil)
-
- args := []string{
- "aibridge",
- "interceptions",
- "list",
- "--initiator", codersdk.Me,
- }
- inv, root := newCLI(t, args...)
- clitest.SetupConfig(t, memberClient, root)
-
- ctx := testutil.Context(t, testutil.WaitLong)
-
- out := bytes.NewBuffer(nil)
- inv.Stdout = out
- err := inv.WithContext(ctx).Run()
- require.NoError(t, err)
-
- // Member cannot read their own interceptions.
- requireHasInterceptions(t, out.Bytes(), []uuid.UUID{})
- })
-
- t.Run("Pagination", func(t *testing.T) {
- t.Parallel()
-
- dv := coderdtest.DeploymentValues(t)
- dv.AI.BridgeConfig.Enabled = true
- ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureAIBridge: 1,
- },
- },
- })
-
- now := dbtime.Now()
- firstInterceptionEndedAt := now.Add(time.Minute)
- firstInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: owner.UserID,
- StartedAt: now,
- }, &firstInterceptionEndedAt)
- returnedInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: owner.UserID,
- StartedAt: now.Add(-time.Hour),
- }, &now)
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: owner.UserID,
- StartedAt: now.Add(-2 * time.Hour),
- }, nil)
-
- args := []string{
- "aibridge",
- "interceptions",
- "list",
- "--limit", "1",
- "--after-id", firstInterception.ID.String(),
- }
- inv, root := newCLI(t, args...)
- //nolint:gocritic // Owner can read all interceptions.
- clitest.SetupConfig(t, ownerClient, root)
-
- ctx := testutil.Context(t, testutil.WaitLong)
-
- out := bytes.NewBuffer(nil)
- inv.Stdout = out
- err := inv.WithContext(ctx).Run()
- require.NoError(t, err)
-
- // Only contains the second interception because after_id is the first
- // interception, and we set a limit of 1.
- requireHasInterceptions(t, out.Bytes(), []uuid.UUID{returnedInterception.ID})
- })
-}
-
-func requireHasInterceptions(t *testing.T, out []byte, ids []uuid.UUID) {
- t.Helper()
-
- var results []codersdk.AIBridgeInterception
- require.NoError(t, json.Unmarshal(out, &results))
- require.Len(t, results, len(ids))
- for i, id := range ids {
- require.Equal(t, id, results[i].ID)
- }
-}
diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go
index b211c0d598..720624031a 100644
--- a/enterprise/cli/root.go
+++ b/enterprise/cli/root.go
@@ -27,7 +27,6 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command {
r.prebuilds(),
r.provisionerd(),
r.externalWorkspaces(),
- r.aibridge(),
}
}
diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden
index 373a3609e4..4e392a8dd6 100644
--- a/enterprise/cli/testdata/coder_--help.golden
+++ b/enterprise/cli/testdata/coder_--help.golden
@@ -16,7 +16,6 @@ USAGE:
SUBCOMMANDS:
agent-firewall Network isolation tool for monitoring and restricting
HTTP/HTTPS requests
- aibridge Manage AI Bridge.
external-workspaces Create or manage external workspaces
features List Enterprise features
groups Manage groups
diff --git a/enterprise/cli/testdata/coder_aibridge_--help.golden b/enterprise/cli/testdata/coder_aibridge_--help.golden
deleted file mode 100644
index 5fdb98d21a..0000000000
--- a/enterprise/cli/testdata/coder_aibridge_--help.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-coder v0.0.0-devel
-
-USAGE:
- coder aibridge
-
- Manage AI Bridge.
-
-SUBCOMMANDS:
- interceptions Manage AI Bridge interceptions.
-
-———
-Run `coder --help` for a list of global options.
diff --git a/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden b/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden
deleted file mode 100644
index 49e36fb712..0000000000
--- a/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-coder v0.0.0-devel
-
-USAGE:
- coder aibridge interceptions
-
- Manage AI Bridge interceptions.
-
-SUBCOMMANDS:
- list List AI Bridge interceptions as JSON.
-
-———
-Run `coder --help` for a list of global options.
diff --git a/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden b/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden
deleted file mode 100644
index eaf45dc169..0000000000
--- a/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden
+++ /dev/null
@@ -1,43 +0,0 @@
-coder v0.0.0-devel
-
-USAGE:
- coder aibridge interceptions list [flags]
-
- List AI Bridge interceptions as JSON.
-
-OPTIONS:
- --after-id string
- The ID of the last result on the previous page to use as a pagination
- cursor.
-
- --client string
- Only return interceptions from this client.
-
- --initiator string
- Only return interceptions initiated by this user. Accepts a user ID,
- username, or "me".
-
- --limit int (default: 100)
- The limit of results to return. Must be between 1 and 1000.
-
- --model string
- Only return interceptions from this model.
-
- --provider string
- Only return interceptions from this provider.
-
- --provider-name string
- Only return interceptions from the named provider.
-
- --started-after string
- Only return interceptions started after this time. Must be before
- 'started-before' if set. Accepts a time in the RFC 3339 format, e.g.
- "====[timestamp]=====07:00".
-
- --started-before string
- Only return interceptions started before this time. Must be after
- 'started-after' if set. Accepts a time in the RFC 3339 format, e.g.
- "====[timestamp]=====07:00".
-
-———
-Run `coder --help` for a list of global options.
diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go
index cb4171ee2b..02a52c1495 100644
--- a/enterprise/coderd/aibridge.go
+++ b/enterprise/coderd/aibridge.go
@@ -27,14 +27,12 @@ import (
)
const (
- maxListInterceptionsLimit = 1000
- maxListSessionsLimit = 1000
- maxListModelsLimit = 1000
- maxListClientsLimit = 1000
- defaultListInterceptionsLimit = 100
- defaultListSessionsLimit = 100
- defaultListModelsLimit = 100
- defaultListClientsLimit = 100
+ maxListSessionsLimit = 1000
+ maxListModelsLimit = 1000
+ maxListClientsLimit = 1000
+ defaultListSessionsLimit = 100
+ defaultListModelsLimit = 100
+ defaultListClientsLimit = 100
// aiBridgeRateLimitWindow is the fixed duration for rate limiting AI Bridge
// requests. This is hardcoded to keep configuration simple.
aiBridgeRateLimitWindow = time.Second
@@ -61,7 +59,6 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f
r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge))
r.Group(func(r chi.Router) {
r.Use(middlewares...)
- r.Get("/interceptions", api.aiBridgeListInterceptions)
r.Get("/sessions", api.aiBridgeListSessions)
r.Get("/sessions/{session_id}", api.aiBridgeGetSessionThreads)
r.Get("/models", api.aiBridgeListModels)
@@ -98,125 +95,6 @@ func aibridgeHandler(api *API, middlewares ...func(http.Handler) http.Handler) f
}
}
-// aiBridgeListInterceptions returns all AI Bridge interceptions a user can read.
-// Optional filters with query params.
-//
-// Deprecated: Use /aibridge/sessions instead, which provides richer
-// session-level aggregation including threads and agentic actions.
-//
-// @Summary List AI Bridge interceptions
-// @ID list-ai-bridge-interceptions
-// @Security CoderSessionToken
-// @Produce json
-// @Tags AI Bridge
-// @Param q query string false "Search query in the format `key:value`. Available keys are: initiator, provider, provider_name, model, started_after, started_before."
-// @Param limit query int false "Page limit"
-// @Param after_id query string false "Cursor pagination after ID (cannot be used with offset)"
-// @Param offset query int false "Offset pagination (cannot be used with after_id)"
-// @Success 200 {object} codersdk.AIBridgeListInterceptionsResponse
-// @Router /api/v2/aibridge/interceptions [get]
-// @Deprecated Use /aibridge/sessions instead.
-func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- apiKey := httpmw.APIKey(r)
-
- page, ok := coderd.ParsePagination(rw, r)
- if !ok {
- return
- }
- if page.AfterID != uuid.Nil && page.Offset != 0 {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Query parameters have invalid values.",
- Detail: "Cannot use both after_id and offset pagination in the same request.",
- })
- return
- }
- if page.Limit == 0 {
- page.Limit = defaultListInterceptionsLimit
- }
- if page.Limit > maxListInterceptionsLimit || page.Limit < 1 {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Invalid pagination limit value.",
- Detail: fmt.Sprintf("Pagination limit must be in range (0, %d]", maxListInterceptionsLimit),
- })
- return
- }
-
- queryStr := r.URL.Query().Get("q")
- filter, errs := searchquery.AIBridgeInterceptions(ctx, api.Database, queryStr, page, apiKey.UserID)
- if len(errs) > 0 {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Invalid workspace search query.",
- Validations: errs,
- })
- return
- }
-
- var (
- count int64
- rows []database.ListAIBridgeInterceptionsRow
- )
- err := api.Database.InTx(func(db database.Store) error {
- // Validate the cursor interception exists and is visible.
- if err := validateInterceptionCursor(ctx, db, page.AfterID, "after_id", ""); err != nil {
- return err
- }
-
- var err error
- // Get the full count of authorized interceptions matching the filter
- // for pagination purposes.
- count, err = db.CountAIBridgeInterceptions(ctx, database.CountAIBridgeInterceptionsParams{
- StartedAfter: filter.StartedAfter,
- StartedBefore: filter.StartedBefore,
- InitiatorID: filter.InitiatorID,
- Provider: filter.Provider,
- ProviderName: filter.ProviderName,
- Model: filter.Model,
- Client: filter.Client,
- })
- if err != nil {
- return xerrors.Errorf("count authorized aibridge interceptions: %w", err)
- }
-
- // This only returns authorized interceptions (when using dbauthz).
- rows, err = db.ListAIBridgeInterceptions(ctx, filter)
- if err != nil {
- return xerrors.Errorf("list aibridge interceptions: %w", err)
- }
-
- return nil
- }, nil)
- if err != nil {
- if errors.Is(err, errInvalidCursor) {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Invalid pagination cursor.",
- Detail: err.Error(),
- })
- return
- }
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Internal error getting AI Bridge interceptions.",
- Detail: err.Error(),
- })
- return
- }
-
- // This fetches the other rows associated with the interceptions.
- items, err := populatedAndConvertAIBridgeInterceptions(ctx, api.Database, rows)
- if err != nil {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Internal error converting database rows to API response.",
- Detail: err.Error(),
- })
- return
- }
-
- httpapi.Write(ctx, rw, http.StatusOK, codersdk.AIBridgeListInterceptionsResponse{
- Count: count,
- Results: items,
- })
-}
-
// aiBridgeListSessions returns AI Bridge sessions (aggregated interceptions).
//
// @Summary List AI Bridge sessions
@@ -656,57 +534,6 @@ func validateInterceptionCursor(ctx context.Context, db database.Store, cursorID
return nil
}
-func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.Store, dbInterceptions []database.ListAIBridgeInterceptionsRow) ([]codersdk.AIBridgeInterception, error) {
- if len(dbInterceptions) == 0 {
- return []codersdk.AIBridgeInterception{}, nil
- }
-
- ids := make([]uuid.UUID, len(dbInterceptions))
- for i, row := range dbInterceptions {
- ids[i] = row.AIBridgeInterception.ID
- }
-
- tokenUsagesRows, err := db.ListAIBridgeTokenUsagesByInterceptionIDs(ctx, ids)
- if err != nil {
- return nil, xerrors.Errorf("get linked aibridge token usages from database: %w", err)
- }
- tokenUsagesMap := make(map[uuid.UUID][]database.AIBridgeTokenUsage, len(dbInterceptions))
- for _, row := range tokenUsagesRows {
- tokenUsagesMap[row.InterceptionID] = append(tokenUsagesMap[row.InterceptionID], row)
- }
-
- userPromptRows, err := db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, ids)
- if err != nil {
- return nil, xerrors.Errorf("get linked aibridge user prompts from database: %w", err)
- }
- userPromptsMap := make(map[uuid.UUID][]database.AIBridgeUserPrompt, len(dbInterceptions))
- for _, row := range userPromptRows {
- userPromptsMap[row.InterceptionID] = append(userPromptsMap[row.InterceptionID], row)
- }
-
- toolUsagesRows, err := db.ListAIBridgeToolUsagesByInterceptionIDs(ctx, ids)
- if err != nil {
- return nil, xerrors.Errorf("get linked aibridge tool usages from database: %w", err)
- }
- toolUsagesMap := make(map[uuid.UUID][]database.AIBridgeToolUsage, len(dbInterceptions))
- for _, row := range toolUsagesRows {
- toolUsagesMap[row.InterceptionID] = append(toolUsagesMap[row.InterceptionID], row)
- }
-
- items := make([]codersdk.AIBridgeInterception, len(dbInterceptions))
- for i, row := range dbInterceptions {
- items[i] = db2sdk.AIBridgeInterception(
- row.AIBridgeInterception,
- row.VisibleUser,
- tokenUsagesMap[row.AIBridgeInterception.ID],
- userPromptsMap[row.AIBridgeInterception.ID],
- toolUsagesMap[row.AIBridgeInterception.ID],
- )
- }
-
- return items, nil
-}
-
// @Summary Get group AI budget
// @ID get-group-ai-budget
// @Security CoderSessionToken
diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go
index b239cce860..64682cd20f 100644
--- a/enterprise/coderd/aibridge_test.go
+++ b/enterprise/coderd/aibridge_test.go
@@ -16,13 +16,11 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
- "github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
- "github.com/coder/coder/v2/cryptorand"
entaudit "github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/audit/backends"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
@@ -31,625 +29,6 @@ import (
"github.com/coder/serpent"
)
-func TestAIBridgeListInterceptions(t *testing.T) {
- t.Parallel()
-
- t.Run("RequiresLicenseFeature", func(t *testing.T) {
- t.Parallel()
-
- dv := coderdtest.DeploymentValues(t)
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- // No aibridge feature
- Features: license.Features{},
- },
- })
-
- ctx := testutil.Context(t, testutil.WaitLong)
- //nolint:gocritic // Owner role is irrelevant here.
- _, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- var sdkErr *codersdk.Error
- require.ErrorAs(t, err, &sdkErr)
- require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
- require.Equal(t, "AI Gateway is a Premium feature. Contact sales!", sdkErr.Message)
- })
-
- t.Run("EmptyDB", func(t *testing.T) {
- t.Parallel()
- client, _ := coderdenttest.New(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
- //nolint:gocritic // Owner role is irrelevant here.
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- require.NoError(t, err)
- require.Empty(t, res.Results)
- })
-
- t.Run("OK", func(t *testing.T) {
- t.Parallel()
- client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- user1, err := client.User(ctx, codersdk.Me)
- require.NoError(t, err)
- user1Visible := database.VisibleUser{
- ID: user1.ID,
- Username: user1.Username,
- Name: user1.Name,
- AvatarURL: user1.AvatarURL,
- }
-
- _, user2 := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
- user2Visible := database.VisibleUser{
- ID: user2.ID,
- Username: user2.Username,
- Name: user2.Name,
- AvatarURL: user2.AvatarURL,
- }
-
- // Insert a bunch of test data.
- now := dbtime.Now()
- i1ApiKey := sql.NullString{String: "some-api-key", Valid: true}
- i1EndedAt := now.Add(-time.Hour + time.Minute)
- i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- APIKeyID: i1ApiKey,
- InitiatorID: user1.ID,
- StartedAt: now.Add(-time.Hour),
- }, &i1EndedAt)
- i1tok1 := dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{
- InterceptionID: i1.ID,
- CreatedAt: now,
- })
- i1tok2 := dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{
- InterceptionID: i1.ID,
- CreatedAt: now.Add(-time.Minute),
- })
- i1up1 := dbgen.AIBridgeUserPrompt(t, db, database.InsertAIBridgeUserPromptParams{
- InterceptionID: i1.ID,
- CreatedAt: now,
- })
- i1up2 := dbgen.AIBridgeUserPrompt(t, db, database.InsertAIBridgeUserPromptParams{
- InterceptionID: i1.ID,
- CreatedAt: now.Add(-time.Minute),
- })
- i1tool1 := dbgen.AIBridgeToolUsage(t, db, database.InsertAIBridgeToolUsageParams{
- InterceptionID: i1.ID,
- CreatedAt: now,
- })
- i1tool2 := dbgen.AIBridgeToolUsage(t, db, database.InsertAIBridgeToolUsageParams{
- InterceptionID: i1.ID,
- CreatedAt: now.Add(-time.Minute),
- })
- i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: user2.ID,
- StartedAt: now,
- }, &now)
-
- // Convert to SDK types for response comparison.
- // You may notice that the ordering of the inner arrays are ASC, this is
- // intentional.
- i1SDK := db2sdk.AIBridgeInterception(i1, user1Visible, []database.AIBridgeTokenUsage{i1tok2, i1tok1}, []database.AIBridgeUserPrompt{i1up2, i1up1}, []database.AIBridgeToolUsage{i1tool2, i1tool1})
- i2SDK := db2sdk.AIBridgeInterception(i2, user2Visible, nil, nil, nil)
-
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- require.NoError(t, err)
- require.Len(t, res.Results, 2)
- require.Equal(t, i2SDK.ID, res.Results[0].ID)
- require.Equal(t, i1SDK.ID, res.Results[1].ID)
-
- require.Equal(t, &i1ApiKey.String, i1SDK.APIKeyID)
- require.Nil(t, i2SDK.APIKeyID)
-
- // Normalize timestamps in the response so we can compare the whole
- // thing easily.
- res.Results[0].StartedAt = i2SDK.StartedAt
- res.Results[1].StartedAt = i1SDK.StartedAt
- require.Len(t, res.Results[1].TokenUsages, 2)
- require.Equal(t, i1SDK.TokenUsages[0].ID, res.Results[1].TokenUsages[0].ID)
- require.Equal(t, i1SDK.TokenUsages[1].ID, res.Results[1].TokenUsages[1].ID)
- res.Results[1].TokenUsages[0].CreatedAt = i1SDK.TokenUsages[0].CreatedAt
- res.Results[1].TokenUsages[1].CreatedAt = i1SDK.TokenUsages[1].CreatedAt
- require.Len(t, res.Results[1].UserPrompts, 2)
- require.Equal(t, i1SDK.UserPrompts[0].ID, res.Results[1].UserPrompts[0].ID)
- require.Equal(t, i1SDK.UserPrompts[1].ID, res.Results[1].UserPrompts[1].ID)
- res.Results[1].UserPrompts[0].CreatedAt = i1SDK.UserPrompts[0].CreatedAt
- res.Results[1].UserPrompts[1].CreatedAt = i1SDK.UserPrompts[1].CreatedAt
- require.Len(t, res.Results[1].ToolUsages, 2)
- require.Equal(t, i1SDK.ToolUsages[0].ID, res.Results[1].ToolUsages[0].ID)
- require.Equal(t, i1SDK.ToolUsages[1].ID, res.Results[1].ToolUsages[1].ID)
- res.Results[1].ToolUsages[0].CreatedAt = i1SDK.ToolUsages[0].CreatedAt
- res.Results[1].ToolUsages[1].CreatedAt = i1SDK.ToolUsages[1].CreatedAt
-
- // Time comparison
- require.Len(t, res.Results, 2)
- require.Equal(t, res.Results[0].ID, i2SDK.ID)
- require.NotNil(t, res.Results[0].EndedAt)
- require.WithinDuration(t, now, *res.Results[0].EndedAt, 5*time.Second)
- res.Results[0].EndedAt = i2SDK.EndedAt
- require.NotNil(t, res.Results[1].EndedAt)
- res.Results[1].EndedAt = i1SDK.EndedAt
-
- require.Equal(t, []codersdk.AIBridgeInterception{i2SDK, i1SDK}, res.Results)
- })
-
- t.Run("Pagination", func(t *testing.T) {
- t.Parallel()
-
- client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- allInterceptionIDs := make([]uuid.UUID, 0, 20)
-
- // Create 10 interceptions with the same started_at time. The returned
- // order for these should still be deterministic.
- now := dbtime.Now()
- for i := range 10 {
- interception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- ID: uuid.UUID{byte(i)},
- InitiatorID: firstUser.UserID,
- StartedAt: now,
- }, &now)
- allInterceptionIDs = append(allInterceptionIDs, interception.ID)
- }
-
- // Create 10 interceptions with a random started_at time.
- for i := range 10 {
- randomOffset, err := cryptorand.Intn(10000)
- require.NoError(t, err)
- randomOffsetDur := time.Duration(randomOffset) * time.Second
- endedAt := now.Add(randomOffsetDur + time.Minute)
- interception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- ID: uuid.UUID{byte(i + 10)},
- InitiatorID: firstUser.UserID,
- StartedAt: now.Add(randomOffsetDur),
- }, &endedAt)
- allInterceptionIDs = append(allInterceptionIDs, interception.ID)
- }
-
- // Try to fetch with an invalid limit.
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- Pagination: codersdk.Pagination{
- Limit: 1001,
- },
- })
- var sdkErr *codersdk.Error
- require.ErrorAs(t, err, &sdkErr)
- require.Contains(t, sdkErr.Message, "Invalid pagination limit value.")
- require.Empty(t, res.Results)
-
- // Try to fetch with both after_id and offset pagination.
- res, err = client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- Pagination: codersdk.Pagination{
- AfterID: allInterceptionIDs[0],
- Offset: 1,
- },
- })
- require.ErrorAs(t, err, &sdkErr)
- require.Contains(t, sdkErr.Message, "Query parameters have invalid values")
- require.Contains(t, sdkErr.Detail, "Cannot use both after_id and offset pagination in the same request.")
-
- // Iterate over all interceptions using both cursor and offset
- // pagination modes.
- for _, paginationMode := range []string{"after_id", "offset"} {
- t.Run(paginationMode, func(t *testing.T) {
- t.Parallel()
-
- ctx := testutil.Context(t, testutil.WaitLong)
-
- // Get all interceptions one by one using the given pagination
- // mode.
- getAllInterceptionsOneByOne := func() []uuid.UUID {
- interceptionIDs := []uuid.UUID{}
- for {
- pagination := codersdk.Pagination{
- Limit: 1,
- }
- if paginationMode == "after_id" {
- if len(interceptionIDs) > 0 {
- pagination.AfterID = interceptionIDs[len(interceptionIDs)-1]
- }
- } else {
- pagination.Offset = len(interceptionIDs)
- }
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- Pagination: pagination,
- })
- require.NoError(t, err)
- if len(res.Results) == 0 {
- break
- }
- require.EqualValues(t, len(allInterceptionIDs), res.Count)
- require.Len(t, res.Results, 1)
- interceptionIDs = append(interceptionIDs, res.Results[0].ID)
- }
- return interceptionIDs
- }
-
- // First attempt: get all interceptions one by one.
- gotInterceptionIDs1 := getAllInterceptionsOneByOne()
- // We should have all of the interceptions returned:
- require.ElementsMatch(t, allInterceptionIDs, gotInterceptionIDs1)
-
- // Second attempt: get all interceptions one by one again.
- gotInterceptionIDs2 := getAllInterceptionsOneByOne()
- // They should be returned in the exact same order.
- require.Equal(t, gotInterceptionIDs1, gotInterceptionIDs2)
- })
- }
- })
-
- t.Run("InflightInterceptions", func(t *testing.T) {
- t.Parallel()
- client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- now := dbtime.Now()
- i1EndedAt := now.Add(time.Minute)
- i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: firstUser.UserID,
- StartedAt: now,
- }, &i1EndedAt)
- dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: firstUser.UserID,
- StartedAt: now.Add(-time.Hour),
- }, nil)
-
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- require.NoError(t, err)
- require.EqualValues(t, 1, res.Count)
- require.Len(t, res.Results, 1)
- require.Equal(t, i1.ID, res.Results[0].ID)
- })
-
- t.Run("Authorized", func(t *testing.T) {
- t.Parallel()
- adminClient, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- secondUserClient, secondUser := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
-
- now := dbtime.Now()
- i1EndedAt := now.Add(time.Minute)
- i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: firstUser.UserID,
- StartedAt: now,
- }, &i1EndedAt)
- i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: secondUser.ID,
- StartedAt: now.Add(-time.Hour),
- }, &now)
-
- // Members cannot read AIBridge interceptions, not even their
- // own (i2 is owned by secondUser).
- res, err := secondUserClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- require.NoError(t, err)
- require.EqualValues(t, 0, res.Count)
- require.Empty(t, res.Results)
-
- // Owner can see all interceptions, including secondUser's,
- // proving the data exists and the member was filtered out.
- res, err = adminClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
- require.NoError(t, err)
- require.EqualValues(t, 2, res.Count)
- require.Len(t, res.Results, 2)
- require.Equal(t, i1.ID, res.Results[0].ID)
- require.Equal(t, i2.ID, res.Results[1].ID)
- })
-
- t.Run("Filter", func(t *testing.T) {
- t.Parallel()
- client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- user1, err := client.User(ctx, codersdk.Me)
- require.NoError(t, err)
- user1Visible := database.VisibleUser{
- ID: user1.ID,
- Username: user1.Username,
- Name: user1.Name,
- AvatarURL: user1.AvatarURL,
- }
-
- _, user2 := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
- user2Visible := database.VisibleUser{
- ID: user2.ID,
- Username: user2.Username,
- Name: user2.Name,
- AvatarURL: user2.AvatarURL,
- }
-
- // Insert a bunch of test data with varying filterable fields.
- now := dbtime.Now()
- i1EndedAt := now.Add(time.Minute)
- i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
- InitiatorID: user1.ID,
- Provider: "one",
- Model: "one",
- StartedAt: now,
- }, &i1EndedAt)
- i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"),
- InitiatorID: user1.ID,
- Provider: "two",
- Model: "two",
- StartedAt: now.Add(-time.Hour),
- Client: sql.NullString{String: string(aiblib.ClientCursor), Valid: true},
- }, &now)
- i3 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- ID: uuid.MustParse("00000000-0000-0000-0000-000000000003"),
- InitiatorID: user2.ID,
- Provider: "three",
- Model: "three",
- StartedAt: now.Add(-2 * time.Hour),
- Client: sql.NullString{String: string(aiblib.ClientClaudeCode), Valid: true},
- }, &now)
-
- // Convert to SDK types for response comparison. We don't care about the
- // inner arrays for this test.
- i1SDK := db2sdk.AIBridgeInterception(i1, user1Visible, nil, nil, nil)
- i2SDK := db2sdk.AIBridgeInterception(i2, user1Visible, nil, nil, nil)
- i3SDK := db2sdk.AIBridgeInterception(i3, user2Visible, nil, nil, nil)
-
- cases := []struct {
- name string
- filter codersdk.AIBridgeListInterceptionsFilter
- want []codersdk.AIBridgeInterception
- }{
- {
- name: "NoFilter",
- filter: codersdk.AIBridgeListInterceptionsFilter{},
- want: []codersdk.AIBridgeInterception{i1SDK, i2SDK, i3SDK},
- },
- {
- name: "Initiator/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: uuid.New().String()},
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "Initiator/Me",
- filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: codersdk.Me},
- want: []codersdk.AIBridgeInterception{i1SDK, i2SDK},
- },
- {
- name: "Initiator/UserID",
- filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: user2.ID.String()},
- want: []codersdk.AIBridgeInterception{i3SDK},
- },
- {
- name: "Initiator/Username",
- filter: codersdk.AIBridgeListInterceptionsFilter{Initiator: user2.Username},
- want: []codersdk.AIBridgeInterception{i3SDK},
- },
- {
- name: "Provider/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{Provider: "nonsense"},
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "Provider/OK",
- filter: codersdk.AIBridgeListInterceptionsFilter{Provider: "two"},
- want: []codersdk.AIBridgeInterception{i2SDK},
- },
- {
- name: "Model/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{Model: "nonsense"},
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "Model/OK",
- filter: codersdk.AIBridgeListInterceptionsFilter{Model: "three"},
- want: []codersdk.AIBridgeInterception{i3SDK},
- },
- {
- name: "Client/Unknown",
- filter: codersdk.AIBridgeListInterceptionsFilter{Client: string(aiblib.ClientUnknown)},
- want: []codersdk.AIBridgeInterception{i1SDK},
- },
- {
- name: "Client/Match",
- filter: codersdk.AIBridgeListInterceptionsFilter{Client: string(aiblib.ClientCursor)},
- want: []codersdk.AIBridgeInterception{i2SDK},
- },
- {
- name: "Client/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{Client: "nonsense"},
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "StartedAfter/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedAfter: i1.StartedAt.Add(10 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "StartedAfter/OK",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedAfter: i2.StartedAt.Add(-10 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{i1SDK, i2SDK},
- },
- {
- name: "StartedBefore/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedBefore: i3.StartedAt.Add(-10 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "StartedBefore/OK",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedBefore: i3.StartedAt.Add(10 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{i3SDK},
- },
- {
- name: "BothBeforeAndAfter/NoMatch",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedAfter: i1.StartedAt.Add(10 * time.Minute),
- StartedBefore: i1.StartedAt.Add(20 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{},
- },
- {
- name: "BothBeforeAndAfter/OK",
- filter: codersdk.AIBridgeListInterceptionsFilter{
- StartedAfter: i2.StartedAt.Add(-10 * time.Minute),
- StartedBefore: i2.StartedAt.Add(10 * time.Minute),
- },
- want: []codersdk.AIBridgeInterception{i2SDK},
- },
- }
-
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- ctx := testutil.Context(t, testutil.WaitLong)
- res, err := client.AIBridgeListInterceptions(ctx, tc.filter)
- require.NoError(t, err)
- require.EqualValues(t, len(tc.want), res.Count)
- // We just compare UUID strings for the sake of this test.
- wantIDs := make([]string, len(tc.want))
- for i, r := range tc.want {
- wantIDs[i] = r.ID.String()
- }
- gotIDs := make([]string, len(res.Results))
- for i, r := range res.Results {
- gotIDs[i] = r.ID.String()
- }
- require.Equal(t, wantIDs, gotIDs)
- })
- }
- })
-
- t.Run("FilterByMe/MemberCannotReadOwn", func(t *testing.T) {
- t.Parallel()
- dv := coderdtest.DeploymentValues(t)
- dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
- ownerClient, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: dv,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureAIBridge: 1,
- },
- },
- })
- ctx := testutil.Context(t, testutil.WaitLong)
-
- memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, firstUser.OrganizationID)
-
- now := dbtime.Now()
- // Create an interception initiated by the member.
- _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
- InitiatorID: member.ID,
- StartedAt: now,
- }, nil)
-
- // Member cannot read their own interceptions, even when
- // filtering by "me".
- res, err := memberClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- Initiator: codersdk.Me,
- })
- require.NoError(t, err)
- require.EqualValues(t, 0, res.Count)
- require.Empty(t, res.Results)
- })
-
- t.Run("FilterErrors", func(t *testing.T) {
- t.Parallel()
- client, _ := coderdenttest.New(t, aibridgeOpts(t))
-
- // No need to insert any test data, we're just testing the filter
- // errors.
-
- cases := []struct {
- name string
- q string
- want []codersdk.ValidationError
- }{
- {
- name: "UnknownUsername",
- q: "initiator:unknown",
- want: []codersdk.ValidationError{
- {
- Field: "initiator",
- Detail: `Query param "initiator" has invalid value: user "unknown" either does not exist, or you are unauthorized to view them`,
- },
- },
- },
- {
- name: "InvalidStartedAfter",
- q: "started_after:invalid",
- want: []codersdk.ValidationError{
- {
- Field: "started_after",
- Detail: `Query param "started_after" must be a valid date format (2006-01-02T15:04:05.999999999Z07:00): parsing time "INVALID" as "2006-01-02T15:04:05.999999999Z07:00": cannot parse "INVALID" as "2006"`,
- },
- },
- },
- {
- name: "InvalidStartedBefore",
- q: "started_before:invalid",
- want: []codersdk.ValidationError{
- {
- Field: "started_before",
- Detail: `Query param "started_before" must be a valid date format (2006-01-02T15:04:05.999999999Z07:00): parsing time "INVALID" as "2006-01-02T15:04:05.999999999Z07:00": cannot parse "INVALID" as "2006"`,
- },
- },
- },
- {
- name: "InvalidBeforeAfterRange",
- // Before MUST be after After if both are set
- q: `started_after:"2025-01-01T00:00:00Z" started_before:"2024-01-01T00:00:00Z"`,
- want: []codersdk.ValidationError{
- {
- Field: "started_before",
- Detail: `Query param "started_before" has invalid value: "started_before" must be after "started_after" if set`,
- },
- },
- },
- }
-
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- ctx := testutil.Context(t, testutil.WaitLong)
- res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- FilterQuery: tc.q,
- })
- var sdkErr *codersdk.Error
- require.ErrorAs(t, err, &sdkErr)
- require.Equal(t, tc.want, sdkErr.Validations)
- require.Empty(t, res.Results)
- })
- }
- })
-
- t.Run("InvalidCursor", func(t *testing.T) {
- t.Parallel()
- client, _ := coderdenttest.New(t, aibridgeOpts(t))
- ctx := testutil.Context(t, testutil.WaitLong)
-
- // Using a nonexistent UUID as after_id should return 400,
- // not silently return an empty page.
- //nolint:gocritic // Owner role is irrelevant here.
- _, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
- Pagination: codersdk.Pagination{
- AfterID: uuid.New(),
- },
- })
- var sdkErr *codersdk.Error
- require.ErrorAs(t, err, &sdkErr)
- require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
- require.Contains(t, sdkErr.Message, "Invalid pagination cursor")
- })
-}
-
func aibridgeOpts(t *testing.T) *coderdenttest.Options {
t.Helper()
dv := coderdtest.DeploymentValues(t)
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 8976bf901c..d2b09d9b40 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -3047,13 +3047,17 @@ class ApiMethods {
});
};
- getAIBridgeInterceptions = async (options: SearchParamOptions) => {
- const url = getURLWithSearchParams(
- "/api/v2/aibridge/interceptions",
- options,
- );
- const response =
- await this.axios.get{JSON.stringify(tokenUsagesMetadata, null, 2)}
-