Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9dd3abe
feat: return all the models from the db
jakehwll Dec 13, 2025
020daf6
feat: implement filtering by `model`
jakehwll Dec 13, 2025
7caae7a
feat: implement `q`, `limit` and `offset`
jakehwll Dec 13, 2025
d98267e
feat: add missing docs
jakehwll Dec 13, 2025
0d13fe9
fix: resolve `dbmock.go`
jakehwll Dec 13, 2025
d6ead77
chore: regenerate `dbmock.go`
jakehwll Dec 13, 2025
4fd7a36
chore: resolve `swagger.json` diff
jakehwll Dec 13, 2025
64e5b41
fix: remove `dbmock.go` from root
jakehwll Dec 13, 2025
0188989
chore: commit bump (ci)
jakehwll Dec 13, 2025
f0cb35b
Merge branch 'jakehwll/ai-bridge-request-logs-improvements' into jake…
jakehwll Dec 15, 2025
3392877
Merge branch 'jakehwll/ai-bridge-request-logs-improvements' into jake…
jakehwll Dec 15, 2025
9979902
fix: remove unused `db`
jakehwll Dec 15, 2025
9278746
feat: add `dbauthz_test.go`
jakehwll Dec 15, 2025
205a235
fix: change `ListAuthorizedAIBridgeModels` to `ListAIBridgeModels` in…
jakehwll Dec 15, 2025
cddf851
chore: remove `.db` to remove panic error
jakehwll Dec 15, 2025
c5f5d89
chore: rename `prepared` to `_` as its unused
jakehwll Dec 15, 2025
b7ff2a5
Merge branch 'jakehwll/ai-bridge-request-logs-improvements' into jake…
jakehwll Dec 15, 2025
5e15a0e
fix: resolve missing key in story
jakehwll Dec 15, 2025
e2c0a29
chore: commit bump (ci)
jakehwll Dec 15, 2025
325f70b
fix: use `ILIKE` instead of `=` on search
jakehwll Dec 15, 2025
1e1e92f
chore: commit bump (ci)
jakehwll Dec 15, 2025
837a49c
Merge branch 'jakehwll/ai-bridge-request-logs-improvements' into jake…
jakehwll Dec 15, 2025
8d0fd1d
fix: add comment describing `@authorize_filter` usecase
jakehwll Dec 15, 2025
f933ea4
fix: describe the default in `AIBridgeModels()`
jakehwll Dec 15, 2025
6078455
fix: add missing comment in `queries.sql.go`
jakehwll Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4647,6 +4647,14 @@ func (q *querier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Contex
return q.db.ListAIBridgeInterceptionsTelemetrySummaries(ctx, arg)
}

func (q *querier) ListAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams) ([]string, 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.ListAuthorizedAIBridgeModels(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.
Expand Down Expand Up @@ -6156,3 +6164,10 @@ func (q *querier) CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg
// database.Store interface, so dbauthz needs to implement it.
return q.CountAIBridgeInterceptions(ctx, arg)
}

func (q *querier) ListAuthorizedAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams, _ rbac.PreparedAuthorized) ([]string, error) {
// TODO: Delete this function, all ListAIBridgeModels should be authorized. For now just call ListAIBridgeModels 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.ListAIBridgeModels(ctx, arg)
}
14 changes: 14 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4662,6 +4662,20 @@ func (s *MethodTestSuite) TestAIBridge() {
check.Args(params, emptyPreparedAuthorized{}).Asserts()
}))

s.Run("ListAIBridgeModels", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.ListAIBridgeModelsParams{}
db.EXPECT().ListAuthorizedAIBridgeModels(gomock.Any(), params, gomock.Any()).Return([]string{}, nil).AnyTimes()
// No asserts here because SQLFilter.
check.Args(params).Asserts()
}))

s.Run("ListAuthorizedAIBridgeModels", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.ListAIBridgeModelsParams{}
db.EXPECT().ListAuthorizedAIBridgeModels(gomock.Any(), params, gomock.Any()).Return([]string{}, nil).AnyTimes()
// No asserts here because SQLFilter.
check.Args(params, emptyPreparedAuthorized{}).Asserts()
}))

s.Run("CountAIBridgeInterceptions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
params := database.CountAIBridgeInterceptionsParams{}
db.EXPECT().CountAuthorizedAIBridgeInterceptions(gomock.Any(), params, gomock.Any()).Return(int64(0), nil).AnyTimes()
Expand Down
14 changes: 14 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions coderd/database/modelqueries.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ func (q *sqlQuerier) CountAuthorizedConnectionLogs(ctx context.Context, arg Coun
type aibridgeQuerier interface {
ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error)
CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) (int64, error)
ListAuthorizedAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams, prepared rbac.PreparedAuthorized) ([]string, error)
}

func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error) {
Expand Down Expand Up @@ -864,6 +865,35 @@ func (q *sqlQuerier) CountAuthorizedAIBridgeInterceptions(ctx context.Context, a
return count, nil
}

func (q *sqlQuerier) ListAuthorizedAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams, prepared rbac.PreparedAuthorized) ([]string, 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(listAIBridgeModels, fmt.Sprintf(" AND %s", authorizedFilter))
if err != nil {
return nil, xerrors.Errorf("insert authorized filter: %w", err)
}

query := fmt.Sprintf("-- name: ListAIBridgeModels :many\n%s", filtered)
rows, err := q.db.QueryContext(ctx, query, arg.Model, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var model string
if err := rows.Scan(&model); err != nil {
return nil, err
}
items = append(items, model)
}
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")
Expand Down
1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions coderd/database/queries/aibridge.sql
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,25 @@ SELECT (
(SELECT COUNT(*) FROM user_prompts) +
(SELECT COUNT(*) FROM interceptions)
)::bigint as total_deleted;

-- name: ListAIBridgeModels :many
SELECT
model
FROM
aibridge_interceptions
WHERE
ended_at IS NOT NULL
-- Filter model
AND CASE
WHEN @model::text != '' THEN aibridge_interceptions.model ILIKE '%' || @model::text || '%'
ELSE true
END
-- We use an `@authorize_filter` as we are attempting to list models that are relevant
-- to the user and what they are allowed to see.
-- Authorize Filter clause will be injected below in ListAIBridgeModelsAuthorized
-- @authorize_filter
GROUP BY
model
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
OFFSET @offset_
;
29 changes: 29 additions & 0 deletions coderd/searchquery/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,35 @@ func AIBridgeInterceptions(ctx context.Context, db database.Store, query string,
return filter, parser.Errors
}

func AIBridgeModels(query string, page codersdk.Pagination) (database.ListAIBridgeModelsParams, []codersdk.ValidationError) {
// nolint:exhaustruct // Empty values just means "don't filter by that field".
filter := database.ListAIBridgeModelsParams{
// #nosec G115 - Safe conversion for pagination offset which is expected to be within int32 range
Offset: int32(page.Offset),
// #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 {
// Defaults to the `model` if no `key:value` pair is provided.
values.Add("model", term)
return nil
})
if len(errors) > 0 {
return filter, errors
}

parser := httpapi.NewQueryParamParser()
filter.Model = parser.String(values, "", "model")

parser.ErrorExcessParams(values)
return filter, parser.Errors
}

// Tasks parses a search query for tasks.
//
// Supported query parameters:
Expand Down
33 changes: 33 additions & 0 deletions docs/reference/api/aibridge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading