diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b8f4b7301c..c0cf84224c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11685,6 +11685,10 @@ const docTemplate = `{ "codersdk.AIBridgeInterception": { "type": "object", "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 909a0d5394..0c7a865112 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10381,6 +10381,10 @@ "codersdk.AIBridgeInterception": { "type": "object", "properties": { + "ended_at": { + "type": "string", + "format": "date-time" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 4c1e3da1a4..c7594d0025 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -962,7 +962,7 @@ func AIBridgeInterception(interception database.AIBridgeInterception, initiator // created_at ASC return sdkToolUsages[i].CreatedAt.Before(sdkToolUsages[j].CreatedAt) }) - return codersdk.AIBridgeInterception{ + intc := codersdk.AIBridgeInterception{ ID: interception.ID, Initiator: MinimalUserFromVisibleUser(initiator), Provider: interception.Provider, @@ -973,6 +973,10 @@ func AIBridgeInterception(interception database.AIBridgeInterception, initiator UserPrompts: sdkUserPrompts, ToolUsages: sdkToolUsages, } + if interception.EndedAt.Valid { + intc.EndedAt = &interception.EndedAt.Time + } + return intc } func AIBridgeTokenUsage(usage database.AIBridgeTokenUsage) codersdk.AIBridgeTokenUsage { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 4397ddfee3..4dc0fb93e3 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4711,6 +4711,13 @@ func (q *querier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error { return update(q.log, q.auth, fetch, q.db.UnfavoriteWorkspace)(ctx, id) } +func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params database.UpdateAIBridgeInterceptionEndedParams) (database.AIBridgeInterception, error) { + if err := q.authorizeAIBridgeInterceptionAction(ctx, policy.ActionUpdate, params.ID); err != nil { + return database.AIBridgeInterception{}, err + } + return q.db.UpdateAIBridgeInterceptionEnded(ctx, params) +} + func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) { return q.db.GetAPIKeyByID(ctx, arg.ID) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 465bd67905..c7a0b2bb70 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4617,4 +4617,13 @@ func (s *MethodTestSuite) TestAIBridge() { db.EXPECT().ListAIBridgeToolUsagesByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeToolUsage{}, nil).AnyTimes() check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeToolUsage{}) })) + + s.Run("UpdateAIBridgeInterceptionEnded", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + intcID := uuid.UUID{1} + params := database.UpdateAIBridgeInterceptionEndedParams{ID: intcID} + intc := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: intcID}) + db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intcID).Return(intc, nil).AnyTimes() // Validation. + db.EXPECT().UpdateAIBridgeInterceptionEnded(gomock.Any(), params).Return(intc, nil).AnyTimes() + check.Args(params).Asserts(intc, policy.ActionUpdate).Returns(intc) + })) } diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 0f38dd1f93..0a62911223 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1495,7 +1495,7 @@ func ClaimPrebuild( return claimedWorkspace } -func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertAIBridgeInterceptionParams) database.AIBridgeInterception { +func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertAIBridgeInterceptionParams, endedAt *time.Time) database.AIBridgeInterception { interception, err := db.InsertAIBridgeInterception(genCtx, database.InsertAIBridgeInterceptionParams{ ID: takeFirst(seed.ID, uuid.New()), InitiatorID: takeFirst(seed.InitiatorID, uuid.New()), @@ -1504,6 +1504,13 @@ func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertA Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")), StartedAt: takeFirst(seed.StartedAt, dbtime.Now()), }) + if endedAt != nil { + interception, err = db.UpdateAIBridgeInterceptionEnded(genCtx, database.UpdateAIBridgeInterceptionEndedParams{ + ID: interception.ID, + EndedAt: *endedAt, + }) + require.NoError(t, err, "insert aibridge interception") + } require.NoError(t, err, "insert aibridge interception") return interception } diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 9f0589178f..57396cd31b 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2888,6 +2888,13 @@ func (m queryMetricsStore) UnfavoriteWorkspace(ctx context.Context, arg uuid.UUI return r0 } +func (m queryMetricsStore) UpdateAIBridgeInterceptionEnded(ctx context.Context, id database.UpdateAIBridgeInterceptionEndedParams) (database.AIBridgeInterception, error) { + start := time.Now() + r0, r1 := m.s.UpdateAIBridgeInterceptionEnded(ctx, id) + m.queryLatencies.WithLabelValues("UpdateAIBridgeInterceptionEnded").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error { start := time.Now() err := m.s.UpdateAPIKeyByID(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 237fc3d0eb..a558c41dac 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -6216,6 +6216,21 @@ func (mr *MockStoreMockRecorder) UnfavoriteWorkspace(ctx, id any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnfavoriteWorkspace", reflect.TypeOf((*MockStore)(nil).UnfavoriteWorkspace), ctx, id) } +// UpdateAIBridgeInterceptionEnded mocks base method. +func (m *MockStore) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg database.UpdateAIBridgeInterceptionEndedParams) (database.AIBridgeInterception, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAIBridgeInterceptionEnded", ctx, arg) + ret0, _ := ret[0].(database.AIBridgeInterception) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAIBridgeInterceptionEnded indicates an expected call of UpdateAIBridgeInterceptionEnded. +func (mr *MockStoreMockRecorder) UpdateAIBridgeInterceptionEnded(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIBridgeInterceptionEnded", reflect.TypeOf((*MockStore)(nil).UpdateAIBridgeInterceptionEnded), ctx, arg) +} + // UpdateAPIKeyByID mocks base method. func (m *MockStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 5744796abc..617959cb5b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -632,6 +632,7 @@ type sqlcQuerier interface { // This will always work regardless of the current state of the template version. UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error + UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error) UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index b9878478a9..e1b6cbd7ad 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -7724,3 +7724,68 @@ func TestUpdateTaskWorkspaceID(t *testing.T) { }) } } + +func TestUpdateAIBridgeInterceptionEnded(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + + t.Run("NonExistingInterception", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + got, err := db.UpdateAIBridgeInterceptionEnded(ctx, database.UpdateAIBridgeInterceptionEndedParams{ + ID: uuid.New(), + EndedAt: time.Now(), + }) + require.ErrorContains(t, err, "no rows in result set") + require.EqualValues(t, database.AIBridgeInterception{}, got) + }) + + t.Run("OK", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitLong) + + user := dbgen.User(t, db, database.User{}) + interceptions := []database.AIBridgeInterception{} + + for _, uid := range []uuid.UUID{{1}, {2}, {3}} { + insertParams := database.InsertAIBridgeInterceptionParams{ + ID: uid, + InitiatorID: user.ID, + Metadata: json.RawMessage("{}"), + } + + intc, err := db.InsertAIBridgeInterception(ctx, insertParams) + require.NoError(t, err) + require.Equal(t, uid, intc.ID) + require.False(t, intc.EndedAt.Valid) + interceptions = append(interceptions, intc) + } + + intc0 := interceptions[0] + endedAt := time.Now() + // Mark first interception as done + updated, err := db.UpdateAIBridgeInterceptionEnded(ctx, database.UpdateAIBridgeInterceptionEndedParams{ + ID: intc0.ID, + EndedAt: endedAt, + }) + require.NoError(t, err) + require.EqualValues(t, updated.ID, intc0.ID) + require.True(t, updated.EndedAt.Valid) + require.WithinDuration(t, endedAt, updated.EndedAt.Time, 5*time.Second) + + // Updating first interception again should fail + updated, err = db.UpdateAIBridgeInterceptionEnded(ctx, database.UpdateAIBridgeInterceptionEndedParams{ + ID: intc0.ID, + EndedAt: endedAt.Add(time.Hour), + }) + require.ErrorIs(t, err, sql.ErrNoRows) + + // Other interceptions should not have ended_at set + for _, intc := range interceptions[1:] { + got, err := db.GetAIBridgeInterceptionByID(ctx, intc.ID) + require.NoError(t, err) + require.False(t, got.EndedAt.Valid) + } + }) +} diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 94fdc03d32..d7bb027628 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -778,6 +778,35 @@ func (q *sqlQuerier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Contex return items, nil } +const updateAIBridgeInterceptionEnded = `-- name: UpdateAIBridgeInterceptionEnded :one +UPDATE aibridge_interceptions + SET ended_at = $1::timestamptz +WHERE + id = $2::uuid + AND ended_at IS NULL +RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at +` + +type UpdateAIBridgeInterceptionEndedParams struct { + EndedAt time.Time `db:"ended_at" json:"ended_at"` + ID uuid.UUID `db:"id" json:"id"` +} + +func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) { + row := q.db.QueryRowContext(ctx, updateAIBridgeInterceptionEnded, arg.EndedAt, arg.ID) + var i AIBridgeInterception + err := row.Scan( + &i.ID, + &i.InitiatorID, + &i.Provider, + &i.Model, + &i.StartedAt, + &i.Metadata, + &i.EndedAt, + ) + return i, err +} + const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec DELETE FROM api_keys diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 3a3b63b848..e32eb0ec29 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -6,6 +6,14 @@ INSERT INTO aibridge_interceptions ( ) RETURNING *; +-- name: UpdateAIBridgeInterceptionEnded :one +UPDATE aibridge_interceptions + SET ended_at = @ended_at::timestamptz +WHERE + id = @id::uuid + AND ended_at IS NULL +RETURNING *; + -- name: InsertAIBridgeTokenUsage :one INSERT INTO aibridge_token_usages ( id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index afe0e19f78..b627f5e9d5 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -18,6 +18,7 @@ type AIBridgeInterception struct { Model string `json:"model"` Metadata map[string]any `json:"metadata"` StartedAt time.Time `json:"started_at" format:"date-time"` + EndedAt *time.Time `json:"ended_at" format:"date-time"` TokenUsages []AIBridgeTokenUsage `json:"token_usages"` UserPrompts []AIBridgeUserPrompt `json:"user_prompts"` ToolUsages []AIBridgeToolUsage `json:"tool_usages"` diff --git a/docs/reference/api/aibridge.md b/docs/reference/api/aibridge.md index 2a2dd16ea9..d2be736eb3 100644 --- a/docs/reference/api/aibridge.md +++ b/docs/reference/api/aibridge.md @@ -31,6 +31,7 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/aibridge/intercepti "count": 0, "results": [ { + "ended_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "initiator": { "avatar_url": "http://example.com", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8945bdf1a6..cb3345e67c 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -379,6 +379,7 @@ ```json { + "ended_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "initiator": { "avatar_url": "http://example.com", @@ -444,6 +445,7 @@ | Name | Type | Required | Restrictions | Description | |--------------------|---------------------------------------------------------------------|----------|--------------|-------------| +| `ended_at` | string | false | | | | `id` | string | false | | | | `initiator` | [codersdk.MinimalUser](#codersdkminimaluser) | false | | | | `metadata` | object | false | | | @@ -462,6 +464,7 @@ "count": 0, "results": [ { + "ended_at": "2019-08-24T14:15:22Z", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "initiator": { "avatar_url": "http://example.com", diff --git a/enterprise/cli/exp_aibridge_test.go b/enterprise/cli/exp_aibridge_test.go index 64b7eef698..466d6b3df8 100644 --- a/enterprise/cli/exp_aibridge_test.go +++ b/enterprise/cli/exp_aibridge_test.go @@ -43,16 +43,16 @@ func TestAIBridgeListInterceptions(t *testing.T) { interception1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: member.ID, StartedAt: now.Add(-time.Hour), - }) + }, &now) interception2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: member.ID, StartedAt: now, - }) + }, nil) // Should not be returned because the user can't see it. _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: owner.UserID, StartedAt: now.Add(-2 * time.Hour), - }) + }, nil) args := []string{ "exp", @@ -99,7 +99,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { Provider: "real-provider", Model: "real-model", StartedAt: now, - }) + }, nil) // These interceptions should not be returned since they don't match the // filters. @@ -108,33 +108,33 @@ func TestAIBridgeListInterceptions(t *testing.T) { 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{ "exp", @@ -181,15 +181,15 @@ func TestAIBridgeListInterceptions(t *testing.T) { firstInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: member.ID, StartedAt: now, - }) + }, nil) returnedInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: member.ID, StartedAt: now.Add(-time.Hour), - }) + }, &now) _ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: member.ID, StartedAt: now.Add(-2 * time.Hour), - }) + }, nil) args := []string{ "exp", diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 5de1df5b60..abaf82dbe8 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -107,7 +107,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: user1.ID, StartedAt: now.Add(-time.Hour), - }) + }, nil) i1tok1 := dbgen.AIBridgeTokenUsage(t, db, database.InsertAIBridgeTokenUsageParams{ InterceptionID: i1.ID, CreatedAt: now, @@ -135,7 +135,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { 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 @@ -169,6 +169,13 @@ func TestAIBridgeListInterceptions(t *testing.T) { 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, now, res.Results[0].EndedAt) + require.WithinDuration(t, now, *res.Results[0].EndedAt, 5*time.Second) + res.Results[0].EndedAt = i2SDK.EndedAt + require.Equal(t, []codersdk.AIBridgeInterception{i2SDK, i1SDK}, res.Results) }) @@ -200,7 +207,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { ID: uuid.UUID{byte(i)}, InitiatorID: firstUser.UserID, StartedAt: now, - }) + }, &now) allInterceptionIDs = append(allInterceptionIDs, interception.ID) } @@ -213,7 +220,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { ID: uuid.UUID{byte(i + 10)}, InitiatorID: firstUser.UserID, StartedAt: now.Add(randomOffsetDur), - }) + }, nil) allInterceptionIDs = append(allInterceptionIDs, interception.ID) } @@ -313,11 +320,11 @@ func TestAIBridgeListInterceptions(t *testing.T) { i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: firstUser.UserID, StartedAt: now, - }) + }, nil) i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ InitiatorID: secondUser.ID, StartedAt: now.Add(-time.Hour), - }) + }, &now) // Admin can see all interceptions. res, err := adminExperimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) @@ -377,21 +384,21 @@ func TestAIBridgeListInterceptions(t *testing.T) { Provider: "one", Model: "one", StartedAt: now, - }) + }, nil) 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), - }) + }, &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), - }) + }, &now) // Convert to SDK types for response comparison. We don't care about the // inner arrays for this test. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0bcd2ba6cc..5e9bc73975 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -32,6 +32,7 @@ export interface AIBridgeInterception { // empty interface{} type, falling back to unknown readonly metadata: Record; readonly started_at: string; + readonly ended_at: string | null; readonly token_usages: readonly AIBridgeTokenUsage[]; readonly user_prompts: readonly AIBridgeUserPrompt[]; readonly tool_usages: readonly AIBridgeToolUsage[];