feat: add endpoint to list aibridge interceptions (#19929)

Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
Paweł Banaszewski
2025-09-27 00:20:33 +10:00
committed by GitHub
co-authored by Dean Sheather
parent d70e26d2e3
commit 0a6ba5d51a
29 changed files with 2646 additions and 144 deletions
+197
View File
@@ -85,6 +85,51 @@ const docTemplate = `{
}
}
},
"/api/experimental/aibridge/interceptions": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"AIBridge"
],
"summary": "List AIBridge interceptions",
"operationId": "list-aibridge-interceptions",
"parameters": [
{
"type": "string",
"description": "Search query in the format ` + "`" + `key:value` + "`" + `. Available keys are: initiator, provider, model, started_after, started_before.",
"name": "q",
"in": "query"
},
{
"type": "integer",
"description": "Page limit",
"name": "limit",
"in": "query"
},
{
"type": "string",
"description": "Cursor pagination after ID",
"name": "after_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.AIBridgeListInterceptionsResponse"
}
}
}
}
},
"/appearance": {
"get": {
"security": [
@@ -11226,6 +11271,62 @@ const docTemplate = `{
}
}
},
"codersdk.AIBridgeInterception": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"initiator_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"token_usages": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeTokenUsage"
}
},
"tool_usages": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeToolUsage"
}
},
"user_prompts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeUserPrompt"
}
}
}
},
"codersdk.AIBridgeListInterceptionsResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeInterception"
}
}
}
},
"codersdk.AIBridgeOpenAIConfig": {
"type": "object",
"properties": {
@@ -11237,6 +11338,102 @@ const docTemplate = `{
}
}
},
"codersdk.AIBridgeTokenUsage": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"input_tokens": {
"type": "integer"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"output_tokens": {
"type": "integer"
},
"provider_response_id": {
"type": "string"
}
}
},
"codersdk.AIBridgeToolUsage": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"injected": {
"type": "boolean"
},
"input": {
"type": "string"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"invocation_error": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"provider_response_id": {
"type": "string"
},
"server_url": {
"type": "string"
},
"tool": {
"type": "string"
}
}
},
"codersdk.AIBridgeUserPrompt": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"prompt": {
"type": "string"
},
"provider_response_id": {
"type": "string"
}
}
},
"codersdk.AIConfig": {
"type": "object",
"properties": {
+193
View File
@@ -65,6 +65,47 @@
}
}
},
"/api/experimental/aibridge/interceptions": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["AIBridge"],
"summary": "List AIBridge interceptions",
"operationId": "list-aibridge-interceptions",
"parameters": [
{
"type": "string",
"description": "Search query in the format `key:value`. Available keys are: initiator, provider, model, started_after, started_before.",
"name": "q",
"in": "query"
},
{
"type": "integer",
"description": "Page limit",
"name": "limit",
"in": "query"
},
{
"type": "string",
"description": "Cursor pagination after ID",
"name": "after_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.AIBridgeListInterceptionsResponse"
}
}
}
}
},
"/appearance": {
"get": {
"security": [
@@ -9954,6 +9995,62 @@
}
}
},
"codersdk.AIBridgeInterception": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"initiator_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"started_at": {
"type": "string",
"format": "date-time"
},
"token_usages": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeTokenUsage"
}
},
"tool_usages": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeToolUsage"
}
},
"user_prompts": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeUserPrompt"
}
}
}
},
"codersdk.AIBridgeListInterceptionsResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.AIBridgeInterception"
}
}
}
},
"codersdk.AIBridgeOpenAIConfig": {
"type": "object",
"properties": {
@@ -9965,6 +10062,102 @@
}
}
},
"codersdk.AIBridgeTokenUsage": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"input_tokens": {
"type": "integer"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"output_tokens": {
"type": "integer"
},
"provider_response_id": {
"type": "string"
}
}
},
"codersdk.AIBridgeToolUsage": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"injected": {
"type": "boolean"
},
"input": {
"type": "string"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"invocation_error": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"provider_response_id": {
"type": "string"
},
"server_url": {
"type": "string"
},
"tool": {
"type": "string"
}
}
},
"codersdk.AIBridgeUserPrompt": {
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"interception_id": {
"type": "string",
"format": "uuid"
},
"metadata": {
"type": "object",
"additionalProperties": {}
},
"prompt": {
"type": "string"
},
"provider_response_id": {
"type": "string"
}
}
},
"codersdk.AIConfig": {
"type": "object",
"properties": {
+31 -26
View File
@@ -1000,38 +1000,41 @@ func New(options *Options) *API {
// Experimental routes are not guaranteed to be stable and may change at any time.
r.Route("/api/experimental", func(r chi.Router) {
api.ExperimentalHandler = r
r.NotFound(func(rw http.ResponseWriter, _ *http.Request) { httpapi.RouteNotFound(rw) })
// Only this group should be subject to apiKeyMiddleware; aibridged will mount its own
// router and handles key validation in a different fashion.
// See enterprise/x/aibridged/http.go.
r.Group(func(r chi.Router) {
r.Use(
// Specific routes can specify different limits, but every rate
// limit must be configurable by the admin.
apiRateLimiter,
httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
)
r.Route("/aitasks", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/prompts", api.aiTasksPrompts)
})
r.Route("/tasks", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/aitasks", func(r chi.Router) {
r.Get("/prompts", api.aiTasksPrompts)
})
r.Route("/tasks", func(r chi.Router) {
r.Use(apiRateLimiter)
r.Get("/", api.tasksList)
r.Get("/", api.tasksList)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize))
r.Get("/{id}", api.taskGet)
r.Delete("/{id}", api.taskDelete)
r.Post("/{id}/send", api.taskSend)
r.Get("/{id}/logs", api.taskLogs)
r.Post("/", api.tasksCreate)
})
})
r.Route("/mcp", func(r chi.Router) {
r.Use(
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2, codersdk.ExperimentMCPServerHTTP),
)
// MCP HTTP transport endpoint with mandatory authentication
r.Mount("/http", api.mcpHTTPHandler())
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database, api.HTTPAuth.Authorize))
r.Get("/{id}", api.taskGet)
r.Delete("/{id}", api.taskDelete)
r.Post("/{id}/send", api.taskSend)
r.Get("/{id}/logs", api.taskLogs)
r.Post("/", api.tasksCreate)
})
})
r.Route("/mcp", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2, codersdk.ExperimentMCPServerHTTP),
)
// MCP HTTP transport endpoint with mandatory authentication
r.Mount("/http", api.mcpHTTPHandler())
})
})
r.Route("/api/v2", func(r chi.Router) {
@@ -1727,6 +1730,8 @@ type API struct {
// APIHandler serves "/api/v2"
APIHandler chi.Router
// ExperimentalHandler serves "/api/experimental"
ExperimentalHandler chi.Router
// RootHandler serves "/"
RootHandler chi.Router
+82
View File
@@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/hcl/v2"
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
"tailscale.com/tailcfg"
@@ -918,3 +919,84 @@ func PreviewParameterValidation(v *previewtypes.ParameterValidation) codersdk.Pr
Monotonic: v.Monotonic,
}
}
func AIBridgeInterception(interception database.AIBridgeInterception, tokenUsages []database.AIBridgeTokenUsage, userPrompts []database.AIBridgeUserPrompt, toolUsages []database.AIBridgeToolUsage) codersdk.AIBridgeInterception {
sdkTokenUsages := List(tokenUsages, AIBridgeTokenUsage)
sort.Slice(sdkTokenUsages, func(i, j int) bool {
// created_at ASC
return sdkTokenUsages[i].CreatedAt.Before(sdkTokenUsages[j].CreatedAt)
})
sdkUserPrompts := List(userPrompts, AIBridgeUserPrompt)
sort.Slice(sdkUserPrompts, func(i, j int) bool {
// created_at ASC
return sdkUserPrompts[i].CreatedAt.Before(sdkUserPrompts[j].CreatedAt)
})
sdkToolUsages := List(toolUsages, AIBridgeToolUsage)
sort.Slice(sdkToolUsages, func(i, j int) bool {
// created_at ASC
return sdkToolUsages[i].CreatedAt.Before(sdkToolUsages[j].CreatedAt)
})
return codersdk.AIBridgeInterception{
ID: interception.ID,
InitiatorID: interception.InitiatorID,
Provider: interception.Provider,
Model: interception.Model,
Metadata: jsonOrEmptyMap(interception.Metadata),
StartedAt: interception.StartedAt,
TokenUsages: sdkTokenUsages,
UserPrompts: sdkUserPrompts,
ToolUsages: sdkToolUsages,
}
}
func AIBridgeTokenUsage(usage database.AIBridgeTokenUsage) codersdk.AIBridgeTokenUsage {
return codersdk.AIBridgeTokenUsage{
ID: usage.ID,
InterceptionID: usage.InterceptionID,
ProviderResponseID: usage.ProviderResponseID,
InputTokens: usage.InputTokens,
OutputTokens: usage.OutputTokens,
Metadata: jsonOrEmptyMap(usage.Metadata),
CreatedAt: usage.CreatedAt,
}
}
func AIBridgeUserPrompt(prompt database.AIBridgeUserPrompt) codersdk.AIBridgeUserPrompt {
return codersdk.AIBridgeUserPrompt{
ID: prompt.ID,
InterceptionID: prompt.InterceptionID,
ProviderResponseID: prompt.ProviderResponseID,
Prompt: prompt.Prompt,
Metadata: jsonOrEmptyMap(prompt.Metadata),
CreatedAt: prompt.CreatedAt,
}
}
func AIBridgeToolUsage(usage database.AIBridgeToolUsage) codersdk.AIBridgeToolUsage {
return codersdk.AIBridgeToolUsage{
ID: usage.ID,
InterceptionID: usage.InterceptionID,
ProviderResponseID: usage.ProviderResponseID,
ServerURL: usage.ServerUrl.String,
Tool: usage.Tool,
Input: usage.Input,
Injected: usage.Injected,
InvocationError: usage.InvocationError.String,
Metadata: jsonOrEmptyMap(usage.Metadata),
CreatedAt: usage.CreatedAt,
}
}
func jsonOrEmptyMap(rawMessage pqtype.NullRawMessage) map[string]any {
var m map[string]any
if !rawMessage.Valid {
return m
}
err := json.Unmarshal(rawMessage.RawMessage, &m)
if err != nil {
// Don't reuse m
return map[string]any{}
}
return m
}
+51 -6
View File
@@ -3842,26 +3842,26 @@ func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.I
return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg)
}
func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) (database.AIBridgeTokenUsage, error) {
// All aibridge_token_usages records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil {
return err
return database.AIBridgeTokenUsage{}, err
}
return q.db.InsertAIBridgeTokenUsage(ctx, arg)
}
func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) (database.AIBridgeToolUsage, error) {
// All aibridge_tool_usages records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil {
return err
return database.AIBridgeToolUsage{}, err
}
return q.db.InsertAIBridgeToolUsage(ctx, arg)
}
func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) (database.AIBridgeUserPrompt, error) {
// All aibridge_user_prompts records belong to the initiator of their associated interception.
if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, arg.InterceptionID); err != nil {
return err
return database.AIBridgeUserPrompt{}, err
}
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
}
@@ -4409,6 +4409,44 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
return q.db.InsertWorkspaceResourceMetadata(ctx, arg)
}
func (q *querier) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) {
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
if err != nil {
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
}
return q.db.ListAuthorizedAIBridgeInterceptions(ctx, arg, prep)
}
func (q *querier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeTokenUsage, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.ListAIBridgeTokenUsagesByInterceptionIDs(ctx, interceptionIDs)
}
func (q *querier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeToolUsage, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.ListAIBridgeToolUsagesByInterceptionIDs(ctx, interceptionIDs)
}
func (q *querier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeUserPrompt, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIDs)
}
func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID)
}
@@ -5761,3 +5799,10 @@ func (q *querier) GetAuthorizedConnectionLogsOffset(ctx context.Context, arg dat
func (q *querier) CountAuthorizedConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams, _ rbac.PreparedAuthorized) (int64, error) {
return q.CountConnectionLogs(ctx, arg)
}
func (q *querier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, _ rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) {
// TODO: Delete this function, all ListAIBridgeInterceptions should be authorized. For now just call ListAIBridgeInterceptions on the authz querier.
// This cannot be deleted for now because it's included in the
// database.Store interface, so dbauthz needs to implement it.
return q.ListAIBridgeInterceptions(ctx, arg)
}
+65 -27
View File
@@ -4334,13 +4334,6 @@ func TestInsertAPIKey_AsPrebuildsUser(t *testing.T) {
}
func (s *MethodTestSuite) TestAIBridge() {
s.Run("GetAIBridgeInterceptionByID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes()
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(intc)
}))
s.Run("InsertAIBridgeInterception", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
initID := uuid.UUID{3}
user := testutil.Fake(s.T(), faker, database.User{ID: initID})
@@ -4360,30 +4353,43 @@ func (s *MethodTestSuite) TestAIBridge() {
s.Run("InsertAIBridgeTokenUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
params := database.InsertAIBridgeTokenUsageParams{InterceptionID: intc.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), params).Return(nil).AnyTimes()
check.Args(params).Asserts(intc, policy.ActionUpdate)
}))
s.Run("InsertAIBridgeToolUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
params := database.InsertAIBridgeToolUsageParams{InterceptionID: intc.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeToolUsage(gomock.Any(), params).Return(nil).AnyTimes()
params := database.InsertAIBridgeTokenUsageParams{InterceptionID: intc.ID}
expected := testutil.Fake(s.T(), faker, database.AIBridgeTokenUsage{InterceptionID: intc.ID})
db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), params).Return(expected, nil).AnyTimes()
check.Args(params).Asserts(intc, policy.ActionUpdate)
}))
s.Run("InsertAIBridgeUserPrompt", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
params := database.InsertAIBridgeUserPromptParams{InterceptionID: intc.ID}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
db.EXPECT().InsertAIBridgeUserPrompt(gomock.Any(), params).Return(nil).AnyTimes()
params := database.InsertAIBridgeUserPromptParams{InterceptionID: intc.ID}
expected := testutil.Fake(s.T(), faker, database.AIBridgeUserPrompt{InterceptionID: intc.ID})
db.EXPECT().InsertAIBridgeUserPrompt(gomock.Any(), params).Return(expected, nil).AnyTimes()
check.Args(params).Asserts(intc, policy.ActionUpdate)
}))
s.Run("InsertAIBridgeToolUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
params := database.InsertAIBridgeToolUsageParams{InterceptionID: intc.ID}
expected := testutil.Fake(s.T(), faker, database.AIBridgeToolUsage{InterceptionID: intc.ID})
db.EXPECT().InsertAIBridgeToolUsage(gomock.Any(), params).Return(expected, nil).AnyTimes()
check.Args(params).Asserts(intc, policy.ActionUpdate)
}))
s.Run("GetAIBridgeInterceptionByID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes()
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(intc)
}))
s.Run("GetAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
a := testutil.Fake(s.T(), faker, database.AIBridgeInterception{})
b := testutil.Fake(s.T(), faker, database.AIBridgeInterception{})
@@ -4401,6 +4407,16 @@ func (s *MethodTestSuite) TestAIBridge() {
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(toks)
}))
s.Run("GetAIBridgeUserPromptsByInterceptionID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
pr := testutil.Fake(s.T(), faker, database.AIBridgeUserPrompt{InterceptionID: intID})
prs := []database.AIBridgeUserPrompt{pr}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
db.EXPECT().GetAIBridgeUserPromptsByInterceptionID(gomock.Any(), intID).Return(prs, nil).AnyTimes()
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(prs)
}))
s.Run("GetAIBridgeToolUsagesByInterceptionID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
@@ -4411,13 +4427,35 @@ func (s *MethodTestSuite) TestAIBridge() {
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(tools)
}))
s.Run("GetAIBridgeUserPromptsByInterceptionID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
intID := uuid.UUID{2}
intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intID})
pr := testutil.Fake(s.T(), faker, database.AIBridgeUserPrompt{InterceptionID: intID})
prs := []database.AIBridgeUserPrompt{pr}
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intID).Return(intc, nil).AnyTimes() // Validation.
db.EXPECT().GetAIBridgeUserPromptsByInterceptionID(gomock.Any(), intID).Return(prs, nil).AnyTimes()
check.Args(intID).Asserts(intc, policy.ActionRead).Returns(prs)
s.Run("ListAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.ListAIBridgeInterceptionsParams{}
db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.AIBridgeInterception{}, nil).AnyTimes()
// No asserts here because SQLFilter.
check.Args(params).Asserts()
}))
s.Run("ListAuthorizedAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.ListAIBridgeInterceptionsParams{}
db.EXPECT().ListAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return([]database.AIBridgeInterception{}, nil).AnyTimes()
// No asserts here because SQLFilter.
check.Args(params, emptyPreparedAuthorized{}).Asserts()
}))
s.Run("ListAIBridgeTokenUsagesByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeTokenUsagesByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeTokenUsage{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeTokenUsage{})
}))
s.Run("ListAIBridgeUserPromptsByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeUserPromptsByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeUserPrompt{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeUserPrompt{})
}))
s.Run("ListAIBridgeToolUsagesByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeToolUsagesByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeToolUsage{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeToolUsage{})
}))
}
+65
View File
@@ -1471,6 +1471,71 @@ func ClaimPrebuild(
return claimedWorkspace
}
func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertAIBridgeInterceptionParams) database.AIBridgeInterception {
interception, err := db.InsertAIBridgeInterception(genCtx, database.InsertAIBridgeInterceptionParams{
ID: takeFirst(seed.ID, uuid.New()),
InitiatorID: takeFirst(seed.InitiatorID, uuid.New()),
Provider: takeFirst(seed.Provider, "provider"),
Model: takeFirst(seed.Model, "model"),
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
StartedAt: takeFirst(seed.StartedAt, dbtime.Now()),
})
require.NoError(t, err, "insert aibridge interception")
return interception
}
func AIBridgeTokenUsage(t testing.TB, db database.Store, seed database.InsertAIBridgeTokenUsageParams) database.AIBridgeTokenUsage {
usage, err := db.InsertAIBridgeTokenUsage(genCtx, database.InsertAIBridgeTokenUsageParams{
ID: takeFirst(seed.ID, uuid.New()),
InterceptionID: takeFirst(seed.InterceptionID, uuid.New()),
ProviderResponseID: takeFirst(seed.ProviderResponseID, "provider_response_id"),
InputTokens: takeFirst(seed.InputTokens, 100),
OutputTokens: takeFirst(seed.OutputTokens, 100),
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
})
require.NoError(t, err, "insert aibridge token usage")
return usage
}
func AIBridgeUserPrompt(t testing.TB, db database.Store, seed database.InsertAIBridgeUserPromptParams) database.AIBridgeUserPrompt {
prompt, err := db.InsertAIBridgeUserPrompt(genCtx, database.InsertAIBridgeUserPromptParams{
ID: takeFirst(seed.ID, uuid.New()),
InterceptionID: takeFirst(seed.InterceptionID, uuid.New()),
ProviderResponseID: takeFirst(seed.ProviderResponseID, "provider_response_id"),
Prompt: takeFirst(seed.Prompt, "prompt"),
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
})
require.NoError(t, err, "insert aibridge user prompt")
return prompt
}
func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBridgeToolUsageParams) database.AIBridgeToolUsage {
serverURL := sql.NullString{}
if seed.ServerUrl.Valid {
serverURL = seed.ServerUrl
}
invocationError := sql.NullString{}
if seed.InvocationError.Valid {
invocationError = seed.InvocationError
}
toolUsage, err := db.InsertAIBridgeToolUsage(genCtx, database.InsertAIBridgeToolUsageParams{
ID: takeFirst(seed.ID, uuid.New()),
InterceptionID: takeFirst(seed.InterceptionID, uuid.New()),
ProviderResponseID: takeFirst(seed.ProviderResponseID, "provider_response_id"),
Tool: takeFirst(seed.Tool, "tool"),
ServerUrl: serverURL,
Input: takeFirst(seed.Input, "input"),
Injected: takeFirst(seed.Injected, false),
InvocationError: invocationError,
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
})
require.NoError(t, err, "insert aibridge tool usage")
return toolUsage
}
func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
JobID: takeFirst(seed.JobID, uuid.New()),
+44 -9
View File
@@ -2210,25 +2210,25 @@ func (m queryMetricsStore) InsertAIBridgeInterception(ctx context.Context, arg d
return r0, r1
}
func (m queryMetricsStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
func (m queryMetricsStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) (database.AIBridgeTokenUsage, error) {
start := time.Now()
r0 := m.s.InsertAIBridgeTokenUsage(ctx, arg)
r0, r1 := m.s.InsertAIBridgeTokenUsage(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeTokenUsage").Observe(time.Since(start).Seconds())
return r0
return r0, r1
}
func (m queryMetricsStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
func (m queryMetricsStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) (database.AIBridgeToolUsage, error) {
start := time.Now()
r0 := m.s.InsertAIBridgeToolUsage(ctx, arg)
r0, r1 := m.s.InsertAIBridgeToolUsage(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeToolUsage").Observe(time.Since(start).Seconds())
return r0
return r0, r1
}
func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) (database.AIBridgeUserPrompt, error) {
start := time.Now()
r0 := m.s.InsertAIBridgeUserPrompt(ctx, arg)
r0, r1 := m.s.InsertAIBridgeUserPrompt(ctx, arg)
m.queryLatencies.WithLabelValues("InsertAIBridgeUserPrompt").Observe(time.Since(start).Seconds())
return r0
return r0, r1
}
func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
@@ -2665,6 +2665,34 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context,
return metadata, err
}
func (m queryMetricsStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) {
start := time.Now()
r0, r1 := m.s.ListAIBridgeInterceptions(ctx, arg)
m.queryLatencies.WithLabelValues("ListAIBridgeInterceptions").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeTokenUsage, error) {
start := time.Now()
r0, r1 := m.s.ListAIBridgeTokenUsagesByInterceptionIDs(ctx, interceptionIds)
m.queryLatencies.WithLabelValues("ListAIBridgeTokenUsagesByInterceptionIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeToolUsage, error) {
start := time.Now()
r0, r1 := m.s.ListAIBridgeToolUsagesByInterceptionIDs(ctx, interceptionIds)
m.queryLatencies.WithLabelValues("ListAIBridgeToolUsagesByInterceptionIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeUserPrompt, error) {
start := time.Now()
r0, r1 := m.s.ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIds)
m.queryLatencies.WithLabelValues("ListAIBridgeUserPromptsByInterceptionIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
start := time.Now()
r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID)
@@ -3630,3 +3658,10 @@ func (m queryMetricsStore) CountAuthorizedConnectionLogs(ctx context.Context, ar
m.queryLatencies.WithLabelValues("CountAuthorizedConnectionLogs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) {
start := time.Now()
r0, r1 := m.s.ListAuthorizedAIBridgeInterceptions(ctx, arg, prepared)
m.queryLatencies.WithLabelValues("ListAuthorizedAIBridgeInterceptions").Observe(time.Since(start).Seconds())
return r0, r1
}
+87 -9
View File
@@ -4724,11 +4724,12 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeInterception(ctx, arg any) *gomoc
}
// InsertAIBridgeTokenUsage mocks base method.
func (m *MockStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
func (m *MockStore) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) (database.AIBridgeTokenUsage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeTokenUsage", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
ret0, _ := ret[0].(database.AIBridgeTokenUsage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIBridgeTokenUsage indicates an expected call of InsertAIBridgeTokenUsage.
@@ -4738,11 +4739,12 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeTokenUsage(ctx, arg any) *gomock.
}
// InsertAIBridgeToolUsage mocks base method.
func (m *MockStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
func (m *MockStore) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) (database.AIBridgeToolUsage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeToolUsage", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
ret0, _ := ret[0].(database.AIBridgeToolUsage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIBridgeToolUsage indicates an expected call of InsertAIBridgeToolUsage.
@@ -4752,11 +4754,12 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeToolUsage(ctx, arg any) *gomock.C
}
// InsertAIBridgeUserPrompt mocks base method.
func (m *MockStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
func (m *MockStore) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) (database.AIBridgeUserPrompt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertAIBridgeUserPrompt", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
ret0, _ := ret[0].(database.AIBridgeUserPrompt)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertAIBridgeUserPrompt indicates an expected call of InsertAIBridgeUserPrompt.
@@ -5680,6 +5683,81 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), ctx, arg)
}
// ListAIBridgeInterceptions mocks base method.
func (m *MockStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.AIBridgeInterception, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAIBridgeInterceptions", ctx, arg)
ret0, _ := ret[0].([]database.AIBridgeInterception)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAIBridgeInterceptions indicates an expected call of ListAIBridgeInterceptions.
func (mr *MockStoreMockRecorder) ListAIBridgeInterceptions(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeInterceptions", reflect.TypeOf((*MockStore)(nil).ListAIBridgeInterceptions), ctx, arg)
}
// ListAIBridgeTokenUsagesByInterceptionIDs mocks base method.
func (m *MockStore) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeTokenUsage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAIBridgeTokenUsagesByInterceptionIDs", ctx, interceptionIds)
ret0, _ := ret[0].([]database.AIBridgeTokenUsage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAIBridgeTokenUsagesByInterceptionIDs indicates an expected call of ListAIBridgeTokenUsagesByInterceptionIDs.
func (mr *MockStoreMockRecorder) ListAIBridgeTokenUsagesByInterceptionIDs(ctx, interceptionIds any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeTokenUsagesByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeTokenUsagesByInterceptionIDs), ctx, interceptionIds)
}
// ListAIBridgeToolUsagesByInterceptionIDs mocks base method.
func (m *MockStore) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeToolUsage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAIBridgeToolUsagesByInterceptionIDs", ctx, interceptionIds)
ret0, _ := ret[0].([]database.AIBridgeToolUsage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAIBridgeToolUsagesByInterceptionIDs indicates an expected call of ListAIBridgeToolUsagesByInterceptionIDs.
func (mr *MockStoreMockRecorder) ListAIBridgeToolUsagesByInterceptionIDs(ctx, interceptionIds any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeToolUsagesByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeToolUsagesByInterceptionIDs), ctx, interceptionIds)
}
// ListAIBridgeUserPromptsByInterceptionIDs mocks base method.
func (m *MockStore) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeUserPrompt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAIBridgeUserPromptsByInterceptionIDs", ctx, interceptionIds)
ret0, _ := ret[0].([]database.AIBridgeUserPrompt)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAIBridgeUserPromptsByInterceptionIDs indicates an expected call of ListAIBridgeUserPromptsByInterceptionIDs.
func (mr *MockStoreMockRecorder) ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIds any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeUserPromptsByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeUserPromptsByInterceptionIDs), ctx, interceptionIds)
}
// ListAuthorizedAIBridgeInterceptions mocks base method.
func (m *MockStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.AIBridgeInterception, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListAuthorizedAIBridgeInterceptions", ctx, arg, prepared)
ret0, _ := ret[0].([]database.AIBridgeInterception)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListAuthorizedAIBridgeInterceptions indicates an expected call of ListAuthorizedAIBridgeInterceptions.
func (mr *MockStoreMockRecorder) ListAuthorizedAIBridgeInterceptions(ctx, arg, prepared any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAuthorizedAIBridgeInterceptions", reflect.TypeOf((*MockStore)(nil).ListAuthorizedAIBridgeInterceptions), ctx, arg, prepared)
}
// ListProvisionerKeysByOrganization mocks base method.
func (m *MockStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
m.ctrl.T.Helper()
+6
View File
@@ -3112,6 +3112,12 @@ CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_
CREATE INDEX idx_aibridge_interceptions_initiator_id ON aibridge_interceptions USING btree (initiator_id);
CREATE INDEX idx_aibridge_interceptions_model ON aibridge_interceptions USING btree (model);
CREATE INDEX idx_aibridge_interceptions_provider ON aibridge_interceptions USING btree (provider);
CREATE INDEX idx_aibridge_interceptions_started_id_desc ON aibridge_interceptions USING btree (started_at DESC, id DESC);
CREATE INDEX idx_aibridge_token_usages_interception_id ON aibridge_token_usages USING btree (interception_id);
CREATE INDEX idx_aibridge_token_usages_provider_response_id ON aibridge_token_usages USING btree (provider_response_id);
@@ -0,0 +1,5 @@
DROP INDEX IF EXISTS idx_aibridge_interceptions_started_id_desc;
DROP INDEX IF EXISTS idx_aibridge_interceptions_provider;
DROP INDEX IF EXISTS idx_aibridge_interceptions_model;
@@ -0,0 +1,9 @@
-- This is used for consistent cursor pagination.
CREATE INDEX IF NOT EXISTS idx_aibridge_interceptions_started_id_desc
ON aibridge_interceptions (started_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_aibridge_interceptions_provider
ON aibridge_interceptions (provider);
CREATE INDEX IF NOT EXISTS idx_aibridge_interceptions_model
ON aibridge_interceptions (model);
+55
View File
@@ -51,6 +51,7 @@ type customQuerier interface {
userQuerier
auditLogQuerier
connectionLogQuerier
aibridgeQuerier
}
type templateQuerier interface {
@@ -761,6 +762,60 @@ func (q *sqlQuerier) CountAuthorizedConnectionLogs(ctx context.Context, arg Coun
return count, nil
}
type aibridgeQuerier interface {
ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]AIBridgeInterception, error)
}
func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]AIBridgeInterception, error) {
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
VariableConverter: regosql.AIBridgeInterceptionConverter(),
})
if err != nil {
return nil, xerrors.Errorf("compile authorized filter: %w", err)
}
filtered, err := insertAuthorizedFilter(listAIBridgeInterceptions, fmt.Sprintf(" AND %s", authorizedFilter))
if err != nil {
return nil, xerrors.Errorf("insert authorized filter: %w", err)
}
query := fmt.Sprintf("-- name: ListAuthorizedAIBridgeInterceptions :many\n%s", filtered)
rows, err := q.db.QueryContext(ctx, query,
arg.StartedAfter,
arg.StartedBefore,
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.AfterID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeInterception
for rows.Next() {
var i AIBridgeInterception
if err := rows.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
func insertAuthorizedFilter(query string, replaceWith string) (string, error) {
if !strings.Contains(query, authorizedQueryPlaceholder) {
return "", xerrors.Errorf("query does not contain authorized replace string, this is not an authorized query")
+7 -3
View File
@@ -508,9 +508,9 @@ type sqlcQuerier interface {
GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error)
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error)
InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error)
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error)
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error)
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
// We use the organization_id as the id
// for simplicity since all users is
@@ -585,6 +585,10 @@ type sqlcQuerier interface {
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]AIBridgeInterception, error)
ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error)
ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error)
ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error)
ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error)
ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error)
ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]UserSecret, error)
+320 -19
View File
@@ -112,7 +112,12 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump
}
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT id, initiator_id, provider, model, started_at, metadata FROM aibridge_interceptions WHERE id = $1::uuid
SELECT
id, initiator_id, provider, model, started_at, metadata
FROM
aibridge_interceptions
WHERE
id = $1::uuid
`
func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (AIBridgeInterception, error) {
@@ -130,7 +135,10 @@ func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UU
}
const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many
SELECT id, initiator_id, provider, model, started_at, metadata FROM aibridge_interceptions
SELECT
id, initiator_id, provider, model, started_at, metadata
FROM
aibridge_interceptions
`
func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeInterception, error) {
@@ -164,7 +172,13 @@ func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeIn
}
const getAIBridgeTokenUsagesByInterceptionID = `-- name: GetAIBridgeTokenUsagesByInterceptionID :many
SELECT id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at FROM aibridge_token_usages WHERE interception_id = $1::uuid
SELECT
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
FROM
aibridge_token_usages WHERE interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeTokenUsage, error) {
@@ -199,7 +213,15 @@ func (q *sqlQuerier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context,
}
const getAIBridgeToolUsagesByInterceptionID = `-- name: GetAIBridgeToolUsagesByInterceptionID :many
SELECT id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at FROM aibridge_tool_usages WHERE interception_id = $1::uuid
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at
FROM
aibridge_tool_usages
WHERE
interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error) {
@@ -237,7 +259,15 @@ func (q *sqlQuerier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context,
}
const getAIBridgeUserPromptsByInterceptionID = `-- name: GetAIBridgeUserPromptsByInterceptionID :many
SELECT id, interception_id, provider_response_id, prompt, metadata, created_at FROM aibridge_user_prompts WHERE interception_id = $1::uuid
SELECT
id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
aibridge_user_prompts
WHERE
interception_id = $1::uuid
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error) {
@@ -271,8 +301,11 @@ func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context,
}
const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (id, initiator_id, provider, model, metadata, started_at)
VALUES ($1::uuid, $2::uuid, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6)
INSERT INTO aibridge_interceptions (
id, initiator_id, provider, model, metadata, started_at
) VALUES (
$1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6
)
RETURNING id, initiator_id, provider, model, started_at, metadata
`
@@ -306,12 +339,13 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA
return i, err
}
const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :exec
const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :one
INSERT INTO aibridge_token_usages (
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, COALESCE($6::jsonb, '{}'::jsonb), $7
)
RETURNING id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
`
type InsertAIBridgeTokenUsageParams struct {
@@ -324,8 +358,8 @@ type InsertAIBridgeTokenUsageParams struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeTokenUsage,
func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeTokenUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
@@ -334,15 +368,26 @@ func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIB
arg.Metadata,
arg.CreatedAt,
)
return err
var i AIBridgeTokenUsage
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.InputTokens,
&i.OutputTokens,
&i.Metadata,
&i.CreatedAt,
)
return i, err
}
const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :exec
const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9::jsonb, '{}'::jsonb), $10
)
RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at
`
type InsertAIBridgeToolUsageParams struct {
@@ -358,8 +403,8 @@ type InsertAIBridgeToolUsageParams struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeToolUsage,
func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeToolUsage,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
@@ -371,15 +416,29 @@ func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBr
arg.Metadata,
arg.CreatedAt,
)
return err
var i AIBridgeToolUsage
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.ServerUrl,
&i.Tool,
&i.Input,
&i.Injected,
&i.InvocationError,
&i.Metadata,
&i.CreatedAt,
)
return i, err
}
const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :exec
const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :one
INSERT INTO aibridge_user_prompts (
id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
$1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6
)
RETURNING id, interception_id, provider_response_id, prompt, metadata, created_at
`
type InsertAIBridgeUserPromptParams struct {
@@ -391,8 +450,8 @@ type InsertAIBridgeUserPromptParams struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) error {
_, err := q.db.ExecContext(ctx, insertAIBridgeUserPrompt,
func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error) {
row := q.db.QueryRowContext(ctx, insertAIBridgeUserPrompt,
arg.ID,
arg.InterceptionID,
arg.ProviderResponseID,
@@ -400,7 +459,249 @@ func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIB
arg.Metadata,
arg.CreatedAt,
)
return err
var i AIBridgeUserPrompt
err := row.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.Prompt,
&i.Metadata,
&i.CreatedAt,
)
return i, err
}
const listAIBridgeInterceptions = `-- name: ListAIBridgeInterceptions :many
SELECT
id, initiator_id, provider, model, started_at, metadata
FROM
aibridge_interceptions
WHERE
-- Filter by time frame
CASE
WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
ELSE true
END
AND CASE
WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= $2::timestamptz
ELSE true
END
-- Filter initiator_id
AND CASE
WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = $3::uuid
ELSE true
END
-- Filter provider
AND CASE
WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text
ELSE true
END
-- Filter model
AND CASE
WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text
ELSE true
END
-- Cursor pagination
AND CASE
WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the started_at field, so select all
-- rows before the cursor and before the after_id UUID.
-- This uses a less than operator because we're sorting DESC. The
-- "after_id" terminology comes from our pagination parser in
-- coderd.
(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
(SELECT started_at FROM aibridge_interceptions WHERE id = $6),
$6::uuid
)
)
ELSE true
END
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
-- @authorize_filter
ORDER BY
aibridge_interceptions.started_at DESC,
aibridge_interceptions.id DESC
LIMIT COALESCE(NULLIF($7::integer, 0), 100)
`
type ListAIBridgeInterceptionsParams struct {
StartedAfter time.Time `db:"started_after" json:"started_after"`
StartedBefore time.Time `db:"started_before" json:"started_before"`
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
Limit int32 `db:"limit_" json:"limit_"`
}
func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]AIBridgeInterception, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptions,
arg.StartedAfter,
arg.StartedBefore,
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.AfterID,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeInterception
for rows.Next() {
var i AIBridgeInterception
if err := rows.Scan(
&i.ID,
&i.InitiatorID,
&i.Provider,
&i.Model,
&i.StartedAt,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeTokenUsagesByInterceptionIDs = `-- name: ListAIBridgeTokenUsagesByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
FROM
aibridge_token_usages
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeTokenUsagesByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeTokenUsage
for rows.Next() {
var i AIBridgeTokenUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.InputTokens,
&i.OutputTokens,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeToolUsagesByInterceptionIDs = `-- name: ListAIBridgeToolUsagesByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at
FROM
aibridge_tool_usages
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeToolUsagesByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeToolUsage
for rows.Next() {
var i AIBridgeToolUsage
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.ServerUrl,
&i.Tool,
&i.Input,
&i.Injected,
&i.InvocationError,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAIBridgeUserPromptsByInterceptionIDs = `-- name: ListAIBridgeUserPromptsByInterceptionIDs :many
SELECT
id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
aibridge_user_prompts
WHERE
interception_id = ANY($1::uuid[])
ORDER BY
created_at ASC,
id ASC
`
func (q *sqlQuerier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error) {
rows, err := q.db.QueryContext(ctx, listAIBridgeUserPromptsByInterceptionIDs, pq.Array(interceptionIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []AIBridgeUserPrompt
for rows.Next() {
var i AIBridgeUserPrompt
if err := rows.Scan(
&i.ID,
&i.InterceptionID,
&i.ProviderResponseID,
&i.Prompt,
&i.Metadata,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec
+136 -13
View File
@@ -1,40 +1,163 @@
-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (id, initiator_id, provider, model, metadata, started_at)
VALUES (@id::uuid, @initiator_id::uuid, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at)
INSERT INTO aibridge_interceptions (
id, initiator_id, provider, model, metadata, started_at
) VALUES (
@id, @initiator_id, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at
)
RETURNING *;
-- name: InsertAIBridgeTokenUsage :exec
-- name: InsertAIBridgeTokenUsage :one
INSERT INTO aibridge_token_usages (
id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @input_tokens, @output_tokens, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
)
RETURNING *;
-- name: InsertAIBridgeUserPrompt :exec
-- name: InsertAIBridgeUserPrompt :one
INSERT INTO aibridge_user_prompts (
id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @prompt, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
)
RETURNING *;
-- name: InsertAIBridgeToolUsage :exec
-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
id, interception_id, provider_response_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
@id, @interception_id, @provider_response_id, @tool, @server_url, @input, @injected, @invocation_error, COALESCE(@metadata::jsonb, '{}'::jsonb), @created_at
);
)
RETURNING *;
-- name: GetAIBridgeInterceptionByID :one
SELECT * FROM aibridge_interceptions WHERE id = @id::uuid;
SELECT
*
FROM
aibridge_interceptions
WHERE
id = @id::uuid;
-- name: GetAIBridgeInterceptions :many
SELECT * FROM aibridge_interceptions;
SELECT
*
FROM
aibridge_interceptions;
-- name: GetAIBridgeTokenUsagesByInterceptionID :many
SELECT * FROM aibridge_token_usages WHERE interception_id = @interception_id::uuid;
SELECT
*
FROM
aibridge_token_usages WHERE interception_id = @interception_id::uuid
ORDER BY
created_at ASC,
id ASC;
-- name: GetAIBridgeUserPromptsByInterceptionID :many
SELECT * FROM aibridge_user_prompts WHERE interception_id = @interception_id::uuid;
SELECT
*
FROM
aibridge_user_prompts
WHERE
interception_id = @interception_id::uuid
ORDER BY
created_at ASC,
id ASC;
-- name: GetAIBridgeToolUsagesByInterceptionID :many
SELECT * FROM aibridge_tool_usages WHERE interception_id = @interception_id::uuid;
SELECT
*
FROM
aibridge_tool_usages
WHERE
interception_id = @interception_id::uuid
ORDER BY
created_at ASC,
id ASC;
-- name: ListAIBridgeInterceptions :many
SELECT
*
FROM
aibridge_interceptions
WHERE
-- Filter by time frame
CASE
WHEN @started_after::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= @started_after::timestamptz
ELSE true
END
AND CASE
WHEN @started_before::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= @started_before::timestamptz
ELSE true
END
-- Filter initiator_id
AND CASE
WHEN @initiator_id::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = @initiator_id::uuid
ELSE true
END
-- Filter provider
AND CASE
WHEN @provider::text != '' THEN aibridge_interceptions.provider = @provider::text
ELSE true
END
-- Filter model
AND CASE
WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text
ELSE true
END
-- Cursor pagination
AND CASE
WHEN @after_id::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the started_at field, so select all
-- rows before the cursor and before the after_id UUID.
-- This uses a less than operator because we're sorting DESC. The
-- "after_id" terminology comes from our pagination parser in
-- coderd.
(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
(SELECT started_at FROM aibridge_interceptions WHERE id = @after_id),
@after_id::uuid
)
)
ELSE true
END
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
-- @authorize_filter
ORDER BY
aibridge_interceptions.started_at DESC,
aibridge_interceptions.id DESC
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
;
-- name: ListAIBridgeTokenUsagesByInterceptionIDs :many
SELECT
*
FROM
aibridge_token_usages
WHERE
interception_id = ANY(@interception_ids::uuid[])
ORDER BY
created_at ASC,
id ASC;
-- name: ListAIBridgeUserPromptsByInterceptionIDs :many
SELECT
*
FROM
aibridge_user_prompts
WHERE
interception_id = ANY(@interception_ids::uuid[])
ORDER BY
created_at ASC,
id ASC;
-- name: ListAIBridgeToolUsagesByInterceptionIDs :many
SELECT
*
FROM
aibridge_tool_usages
WHERE
interception_id = ANY(@interception_ids::uuid[])
ORDER BY
created_at ASC,
id ASC;
+16 -1
View File
@@ -54,7 +54,7 @@ func AuditLogConverter() *sqltypes.VariableConverter {
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
resourceIDMatcher(),
sqltypes.StringVarMatcher("COALESCE(audit_logs.organization_id :: text, '')", []string{"input", "object", "org_owner"}),
// Aduit logs have no user owner, only owner by an organization.
// Audit logs have no user owner, only owner by an organization.
sqltypes.AlwaysFalse(userOwnerMatcher()),
)
matcher.RegisterMatcher(
@@ -78,6 +78,21 @@ func ConnectionLogConverter() *sqltypes.VariableConverter {
return matcher
}
func AIBridgeInterceptionConverter() *sqltypes.VariableConverter {
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
resourceIDMatcher(),
// AIBridge interceptions are not tied to any organization.
sqltypes.StringVarMatcher("''", []string{"input", "object", "org_owner"}),
sqltypes.StringVarMatcher("initiator_id :: text", []string{"input", "object", "owner"}),
)
matcher.RegisterMatcher(
// No ACLs on the aibridge interception type
sqltypes.AlwaysFalse(groupACLMatcher(matcher)),
sqltypes.AlwaysFalse(userACLMatcher(matcher)),
)
return matcher
}
func UserConverter() *sqltypes.VariableConverter {
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
resourceIDMatcher(),
+2
View File
@@ -327,6 +327,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Allow auditors to query deployment stats and insights.
ResourceDeploymentStats.Type: {policy.ActionRead},
ResourceDeploymentConfig.Type: {policy.ActionRead},
// Allow auditors to query aibridge interceptions.
ResourceAibridgeInterception.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
+46 -2
View File
@@ -226,7 +226,8 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
filter.HasExternalAgent = parser.NullableBoolean(values, sql.NullBool{}, "has_external_agent")
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
filter.Shared = parser.NullableBoolean(values, sql.NullBool{}, "shared")
filter.SharedWithUserID = parseUser(ctx, db, parser, values, "shared_with_user")
// TODO: support "me" by passing in the actorID
filter.SharedWithUserID = parseUser(ctx, db, parser, values, "shared_with_user", uuid.Nil)
filter.SharedWithGroupID = parseGroup(ctx, db, parser, values, "shared_with_group")
type paramMatch struct {
@@ -304,6 +305,46 @@ func Templates(ctx context.Context, db database.Store, actorID uuid.UUID, query
return filter, parser.Errors
}
func AIBridgeInterceptions(ctx context.Context, db database.Store, query string, page codersdk.Pagination, actorID uuid.UUID) (database.ListAIBridgeInterceptionsParams, []codersdk.ValidationError) {
// nolint:exhaustruct // Empty values just means "don't filter by that field".
filter := database.ListAIBridgeInterceptionsParams{
AfterID: page.AfterID,
// #nosec G115 - Safe conversion for pagination limit which is expected to be within int32 range
Limit: int32(page.Limit),
}
if query == "" {
return filter, nil
}
values, errors := searchTerms(query, func(term string, values url.Values) error {
// Default to the initiating user
values.Add("user", term)
return nil
})
if len(errors) > 0 {
return filter, errors
}
parser := httpapi.NewQueryParamParser()
filter.InitiatorID = parseUser(ctx, db, parser, values, "initiator", actorID)
filter.Provider = parser.String(values, "", "provider")
filter.Model = parser.String(values, "", "model")
// Time must be between started_after and started_before.
filter.StartedAfter = parser.Time3339Nano(values, time.Time{}, "started_after")
filter.StartedBefore = parser.Time3339Nano(values, time.Time{}, "started_before")
if !filter.StartedBefore.IsZero() && !filter.StartedAfter.IsZero() && !filter.StartedBefore.After(filter.StartedAfter) {
parser.Errors = append(parser.Errors, codersdk.ValidationError{
Field: "started_before",
Detail: `Query param "started_before" has invalid value: "started_before" must be after "started_after" if set`,
})
}
parser.ErrorExcessParams(values)
return filter, parser.Errors
}
func searchTerms(query string, defaultKey func(term string, values url.Values) error) (url.Values, []codersdk.ValidationError) {
searchValues := make(url.Values)
@@ -365,11 +406,14 @@ func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.Q
})
}
func parseUser(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string) uuid.UUID {
func parseUser(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string, actorID uuid.UUID) uuid.UUID {
return httpapi.ParseCustom(parser, vals, uuid.Nil, queryParam, func(v string) (uuid.UUID, error) {
if v == "" {
return uuid.Nil, nil
}
if v == codersdk.Me && actorID != uuid.Nil {
return actorID, nil
}
userID, err := uuid.Parse(v)
if err == nil {
return userID, nil