mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add ai_providers table, queries, dbauthz, audit, RBAC (#24892)
This commit is contained in:
Generated
+47
-31
@@ -13889,7 +13889,7 @@ const docTemplate = `{
|
||||
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
|
||||
"$ref": "#/definitions/codersdk.AIProviderConfig"
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
@@ -14010,36 +14010,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProxyConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14421,6 +14391,36 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.APIAllowListTarget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14522,6 +14522,11 @@ const docTemplate = `{
|
||||
"ai_model_price:*",
|
||||
"ai_model_price:read",
|
||||
"ai_model_price:update",
|
||||
"ai_provider:*",
|
||||
"ai_provider:create",
|
||||
"ai_provider:delete",
|
||||
"ai_provider:read",
|
||||
"ai_provider:update",
|
||||
"ai_seat:*",
|
||||
"ai_seat:create",
|
||||
"ai_seat:read",
|
||||
@@ -14737,6 +14742,11 @@ const docTemplate = `{
|
||||
"APIKeyScopeAiModelPriceAll",
|
||||
"APIKeyScopeAiModelPriceRead",
|
||||
"APIKeyScopeAiModelPriceUpdate",
|
||||
"APIKeyScopeAiProviderAll",
|
||||
"APIKeyScopeAiProviderCreate",
|
||||
"APIKeyScopeAiProviderDelete",
|
||||
"APIKeyScopeAiProviderRead",
|
||||
"APIKeyScopeAiProviderUpdate",
|
||||
"APIKeyScopeAiSeatAll",
|
||||
"APIKeyScopeAiSeatCreate",
|
||||
"APIKeyScopeAiSeatRead",
|
||||
@@ -21351,6 +21361,7 @@ const docTemplate = `{
|
||||
"enum": [
|
||||
"*",
|
||||
"ai_model_price",
|
||||
"ai_provider",
|
||||
"ai_seat",
|
||||
"aibridge_interception",
|
||||
"api_key",
|
||||
@@ -21399,6 +21410,7 @@ const docTemplate = `{
|
||||
"x-enum-varnames": [
|
||||
"ResourceWildcard",
|
||||
"ResourceAiModelPrice",
|
||||
"ResourceAIProvider",
|
||||
"ResourceAiSeat",
|
||||
"ResourceAibridgeInterception",
|
||||
"ResourceApiKey",
|
||||
@@ -21658,6 +21670,8 @@ const docTemplate = `{
|
||||
"workspace_app",
|
||||
"task",
|
||||
"ai_seat",
|
||||
"ai_provider",
|
||||
"ai_provider_key",
|
||||
"chat",
|
||||
"user_secret"
|
||||
],
|
||||
@@ -21689,6 +21703,8 @@ const docTemplate = `{
|
||||
"ResourceTypeWorkspaceApp",
|
||||
"ResourceTypeTask",
|
||||
"ResourceTypeAISeat",
|
||||
"ResourceTypeAIProvider",
|
||||
"ResourceTypeAIProviderKey",
|
||||
"ResourceTypeChat",
|
||||
"ResourceTypeUserSecret"
|
||||
]
|
||||
|
||||
Generated
+47
-31
@@ -12361,7 +12361,7 @@
|
||||
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
|
||||
"$ref": "#/definitions/codersdk.AIProviderConfig"
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
@@ -12482,36 +12482,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProxyConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12893,6 +12863,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.APIAllowListTarget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12986,6 +12986,11 @@
|
||||
"ai_model_price:*",
|
||||
"ai_model_price:read",
|
||||
"ai_model_price:update",
|
||||
"ai_provider:*",
|
||||
"ai_provider:create",
|
||||
"ai_provider:delete",
|
||||
"ai_provider:read",
|
||||
"ai_provider:update",
|
||||
"ai_seat:*",
|
||||
"ai_seat:create",
|
||||
"ai_seat:read",
|
||||
@@ -13201,6 +13206,11 @@
|
||||
"APIKeyScopeAiModelPriceAll",
|
||||
"APIKeyScopeAiModelPriceRead",
|
||||
"APIKeyScopeAiModelPriceUpdate",
|
||||
"APIKeyScopeAiProviderAll",
|
||||
"APIKeyScopeAiProviderCreate",
|
||||
"APIKeyScopeAiProviderDelete",
|
||||
"APIKeyScopeAiProviderRead",
|
||||
"APIKeyScopeAiProviderUpdate",
|
||||
"APIKeyScopeAiSeatAll",
|
||||
"APIKeyScopeAiSeatCreate",
|
||||
"APIKeyScopeAiSeatRead",
|
||||
@@ -19575,6 +19585,7 @@
|
||||
"enum": [
|
||||
"*",
|
||||
"ai_model_price",
|
||||
"ai_provider",
|
||||
"ai_seat",
|
||||
"aibridge_interception",
|
||||
"api_key",
|
||||
@@ -19623,6 +19634,7 @@
|
||||
"x-enum-varnames": [
|
||||
"ResourceWildcard",
|
||||
"ResourceAiModelPrice",
|
||||
"ResourceAIProvider",
|
||||
"ResourceAiSeat",
|
||||
"ResourceAibridgeInterception",
|
||||
"ResourceApiKey",
|
||||
@@ -19872,6 +19884,8 @@
|
||||
"workspace_app",
|
||||
"task",
|
||||
"ai_seat",
|
||||
"ai_provider",
|
||||
"ai_provider_key",
|
||||
"chat",
|
||||
"user_secret"
|
||||
],
|
||||
@@ -19903,6 +19917,8 @@
|
||||
"ResourceTypeWorkspaceApp",
|
||||
"ResourceTypeTask",
|
||||
"ResourceTypeAISeat",
|
||||
"ResourceTypeAIProvider",
|
||||
"ResourceTypeAIProviderKey",
|
||||
"ResourceTypeChat",
|
||||
"ResourceTypeUserSecret"
|
||||
]
|
||||
|
||||
@@ -34,6 +34,8 @@ type Auditable interface {
|
||||
idpsync.RoleSyncSettings |
|
||||
database.TaskTable |
|
||||
database.AiSeatState |
|
||||
database.AIProvider |
|
||||
database.AIProviderKey |
|
||||
database.Chat |
|
||||
database.UserSecret
|
||||
}
|
||||
|
||||
@@ -134,6 +134,13 @@ func ResourceTarget[T Auditable](tgt T) string {
|
||||
return typed.Name
|
||||
case database.AiSeatState:
|
||||
return "AI Seat"
|
||||
case database.AIProvider:
|
||||
return typed.Name
|
||||
case database.AIProviderKey:
|
||||
// Provider keys have no user-facing name; show the parent
|
||||
// provider's UUID so the row can be correlated back to its
|
||||
// provider in the audit UI.
|
||||
return typed.ProviderID.String()
|
||||
case database.Chat:
|
||||
// Chat titles can contain sensitive content (secrets, internal
|
||||
// project names), so we use a short UUID prefix as a display
|
||||
@@ -210,6 +217,10 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
|
||||
return typed.ID
|
||||
case database.AiSeatState:
|
||||
return typed.UserID
|
||||
case database.AIProvider:
|
||||
return typed.ID
|
||||
case database.AIProviderKey:
|
||||
return typed.ID
|
||||
case database.Chat:
|
||||
return typed.ID
|
||||
case database.UserSecret:
|
||||
@@ -271,6 +282,10 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
|
||||
return database.ResourceTypeTask
|
||||
case database.AiSeatState:
|
||||
return database.ResourceTypeAiSeat
|
||||
case database.AIProvider:
|
||||
return database.ResourceTypeAiProvider
|
||||
case database.AIProviderKey:
|
||||
return database.ResourceTypeAiProviderKey
|
||||
case database.Chat:
|
||||
return database.ResourceTypeChat
|
||||
case database.UserSecret:
|
||||
@@ -335,6 +350,12 @@ func ResourceRequiresOrgID[T Auditable]() bool {
|
||||
return true
|
||||
case database.AiSeatState:
|
||||
return false
|
||||
case database.AIProvider:
|
||||
// AI providers are deployment-scoped, not org-scoped.
|
||||
return false
|
||||
case database.AIProviderKey:
|
||||
// AI provider keys are deployment-scoped, not org-scoped.
|
||||
return false
|
||||
case database.Chat:
|
||||
// Chats always have a non-null organization_id (since
|
||||
// migration 000467).
|
||||
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
CheckAiModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
|
||||
CheckAiModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
|
||||
CheckAiModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
|
||||
CheckAiProvidersNameCheck CheckConstraint = "ai_providers_name_check" // ai_providers
|
||||
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
|
||||
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
|
||||
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
|
||||
|
||||
@@ -1851,6 +1851,20 @@ func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParam
|
||||
return q.db.CustomRoles(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteAIProviderByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteAIProviderKey(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -2488,6 +2502,52 @@ func (q *querier) GetAIModelPriceByProviderModel(ctx context.Context, arg databa
|
||||
return q.db.GetAIModelPriceByProviderModel(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.GetAIProviderByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.GetAIProviderByName(ctx, name)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.GetAIProviderKeyByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
// This query intentionally returns every key row, including those
|
||||
// whose provider has been soft-deleted, so the dbcrypt key rotation
|
||||
// utility can re-encrypt every row that holds a foreign-key
|
||||
// reference to dbcrypt_keys.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviderKeys(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviderKeysByProviderID(ctx, providerID)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviders(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -5193,6 +5253,20 @@ func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.Ins
|
||||
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.InsertAIProvider(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.InsertAIProviderKey(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
// TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we
|
||||
// don't currently have a capability to conditionally deny creating resources by owner ID in a role.
|
||||
@@ -6259,6 +6333,13 @@ func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params da
|
||||
return q.db.UpdateAIBridgeInterceptionEnded(ctx, params)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.UpdateAIProvider(ctx, arg)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -6545,6 +6626,27 @@ func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCusto
|
||||
return q.db.UpdateCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
// Encrypted columns can be rewritten on any row, including those
|
||||
// whose provider has been soft-deleted, so the dbcrypt rotation can
|
||||
// move every FK reference to a new key digest before old keys are
|
||||
// revoked.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.UpdateEncryptedAIProviderKey(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
// Settings can be rewritten on any row, including soft-deleted ones,
|
||||
// so the dbcrypt rotation can move every FK reference to a new key
|
||||
// digest before old keys are revoked.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.UpdateEncryptedAIProviderSettings(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
|
||||
|
||||
@@ -6208,6 +6208,103 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()).Return(database.AiModelPrice{}, nil).AnyTimes()
|
||||
check.Args(database.GetAIModelPriceByProviderModelParams{}).Asserts(rbac.ResourceAiModelPrice, policy.ActionRead)
|
||||
}))
|
||||
|
||||
s.Run("GetAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().GetAIProviderByID(gomock.Any(), provider.ID).Return(provider, nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviderByName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().GetAIProviderByName(gomock.Any(), provider.Name).Return(provider, nil).AnyTimes()
|
||||
check.Args(provider.Name).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviders", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
providerA := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
providerB := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.GetAIProvidersParams{}
|
||||
dbm.EXPECT().GetAIProviders(gomock.Any(), arg).Return([]database.AIProvider{providerA, providerB}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProvider{providerA, providerB})
|
||||
}))
|
||||
s.Run("InsertAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertAIProviderParams{
|
||||
ID: uuid.New(),
|
||||
Type: database.AiProviderTypeOpenai,
|
||||
Name: "test-provider",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{ID: arg.ID, Name: arg.Name})
|
||||
dbm.EXPECT().InsertAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(provider)
|
||||
}))
|
||||
s.Run("UpdateAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.UpdateAIProviderParams{
|
||||
ID: provider.ID,
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
dbm.EXPECT().UpdateAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
|
||||
}))
|
||||
s.Run("DeleteAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().DeleteAIProviderByID(gomock.Any(), provider.ID).Return(nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("UpdateEncryptedAIProviderSettings", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.UpdateEncryptedAIProviderSettingsParams{
|
||||
ID: provider.ID,
|
||||
Settings: sql.NullString{String: "encrypted-settings", Valid: true},
|
||||
}
|
||||
dbm.EXPECT().UpdateEncryptedAIProviderSettings(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviderKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().GetAIProviderKeyByID(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
|
||||
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(key)
|
||||
}))
|
||||
s.Run("GetAIProviderKeysByProviderID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
|
||||
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
|
||||
dbm.EXPECT().GetAIProviderKeysByProviderID(gomock.Any(), provider.ID).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
|
||||
}))
|
||||
s.Run("GetAIProviderKeys", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().GetAIProviderKeys(gomock.Any()).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
|
||||
}))
|
||||
s.Run("InsertAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.InsertAIProviderKeyParams{
|
||||
ID: uuid.New(),
|
||||
ProviderID: provider.ID,
|
||||
APIKey: "test-key",
|
||||
}
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{ID: arg.ID, ProviderID: arg.ProviderID})
|
||||
dbm.EXPECT().InsertAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(key)
|
||||
}))
|
||||
s.Run("DeleteAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().DeleteAIProviderKey(gomock.Any(), key.ID).Return(nil).AnyTimes()
|
||||
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("UpdateEncryptedAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
arg := database.UpdateEncryptedAIProviderKeyParams{
|
||||
ID: key.ID,
|
||||
APIKey: "encrypted-api-key",
|
||||
}
|
||||
dbm.EXPECT().UpdateEncryptedAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(key)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestTelemetry() {
|
||||
|
||||
@@ -169,6 +169,62 @@ func ChatModelConfig(t testing.TB, db database.Store, seed database.ChatModelCon
|
||||
return cfg
|
||||
}
|
||||
|
||||
func AIProvider(t testing.TB, db database.Store, seed database.AIProvider, munge ...func(*database.InsertAIProviderParams)) database.AIProvider {
|
||||
t.Helper()
|
||||
id := seed.ID
|
||||
if id == uuid.Nil {
|
||||
id = uuid.New()
|
||||
}
|
||||
provType := seed.Type
|
||||
if provType == "" {
|
||||
provType = database.AiProviderTypeOpenai
|
||||
}
|
||||
name := takeFirst(seed.Name, testutil.GetRandomNameHyphenated(t))
|
||||
displayName := seed.DisplayName
|
||||
if !displayName.Valid {
|
||||
displayName = sql.NullString{String: name, Valid: true}
|
||||
}
|
||||
params := database.InsertAIProviderParams{
|
||||
ID: id,
|
||||
Type: provType,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Enabled: takeFirst(seed.Enabled, true),
|
||||
BaseUrl: takeFirst(seed.BaseUrl, "https://api.example.com/"),
|
||||
Settings: seed.Settings,
|
||||
SettingsKeyID: seed.SettingsKeyID,
|
||||
}
|
||||
for _, fn := range munge {
|
||||
fn(¶ms)
|
||||
}
|
||||
provider, err := db.InsertAIProvider(genCtx, params)
|
||||
require.NoError(t, err, "insert ai provider")
|
||||
return provider
|
||||
}
|
||||
|
||||
func AIProviderKey(t testing.TB, db database.Store, seed database.AIProviderKey, munge ...func(*database.InsertAIProviderKeyParams)) database.AIProviderKey {
|
||||
t.Helper()
|
||||
id := seed.ID
|
||||
if id == uuid.Nil {
|
||||
id = uuid.New()
|
||||
}
|
||||
now := dbtime.Now()
|
||||
params := database.InsertAIProviderKeyParams{
|
||||
ID: id,
|
||||
ProviderID: seed.ProviderID,
|
||||
APIKey: takeFirst(seed.APIKey, "test-key"),
|
||||
ApiKeyKeyID: seed.ApiKeyKeyID,
|
||||
CreatedAt: takeFirst(seed.CreatedAt, now),
|
||||
UpdatedAt: takeFirst(seed.UpdatedAt, now),
|
||||
}
|
||||
for _, fn := range munge {
|
||||
fn(¶ms)
|
||||
}
|
||||
key, err := db.InsertAIProviderKey(genCtx, params)
|
||||
require.NoError(t, err, "insert ai provider key")
|
||||
return key
|
||||
}
|
||||
|
||||
func ChatProvider(t testing.TB, db database.Store, seed database.ChatProvider, munge ...func(*database.InsertChatProviderParams)) database.ChatProvider {
|
||||
t.Helper()
|
||||
params := database.InsertChatProviderParams{
|
||||
|
||||
@@ -377,6 +377,22 @@ func (m queryMetricsStore) CustomRoles(ctx context.Context, arg database.CustomR
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAIProviderByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAIProviderByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAIProviderKey(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderKey").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAPIKeyByID(ctx, id)
|
||||
@@ -985,6 +1001,54 @@ func (m queryMetricsStore) GetAIModelPriceByProviderModel(ctx context.Context, a
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderByName(ctx, name)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderByName").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByName").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeyByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeyByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeyByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeys(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeys").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeys").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeysByProviderID(ctx, providerID)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeysByProviderID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeysByProviderID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviders(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviders").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviders").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAPIKeyByID(ctx, id)
|
||||
@@ -3561,6 +3625,22 @@ func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg dat
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAIProvider(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAIProvider").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProvider").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAIProviderKey(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProviderKey").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAPIKey(ctx, arg)
|
||||
@@ -4513,6 +4593,14 @@ func (m queryMetricsStore) UpdateAIBridgeInterceptionEnded(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateAIProvider(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAIProvider").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAIProvider").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateAPIKeyByID(ctx, arg)
|
||||
@@ -4697,6 +4785,22 @@ func (m queryMetricsStore) UpdateCustomRole(ctx context.Context, arg database.Up
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateEncryptedAIProviderKey(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderKey").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateEncryptedAIProviderSettings(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderSettings").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderSettings").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateExternalAuthLink(ctx, arg)
|
||||
|
||||
@@ -603,6 +603,34 @@ func (mr *MockStoreMockRecorder) CustomRoles(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomRoles", reflect.TypeOf((*MockStore)(nil).CustomRoles), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteAIProviderByID mocks base method.
|
||||
func (m *MockStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAIProviderByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAIProviderByID indicates an expected call of DeleteAIProviderByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteAIProviderByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderByID", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteAIProviderKey mocks base method.
|
||||
func (m *MockStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAIProviderKey", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAIProviderKey indicates an expected call of DeleteAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) DeleteAIProviderKey(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderKey", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderKey), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteAPIKeyByID mocks base method.
|
||||
func (m *MockStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1698,6 +1726,96 @@ func (mr *MockStoreMockRecorder) GetAIModelPriceByProviderModel(ctx, arg any) *g
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIModelPriceByProviderModel", reflect.TypeOf((*MockStore)(nil).GetAIModelPriceByProviderModel), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAIProviderByID mocks base method.
|
||||
func (m *MockStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderByID indicates an expected call of GetAIProviderByID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAIProviderByName mocks base method.
|
||||
func (m *MockStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderByName", ctx, name)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderByName indicates an expected call of GetAIProviderByName.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderByName(ctx, name any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByName", reflect.TypeOf((*MockStore)(nil).GetAIProviderByName), ctx, name)
|
||||
}
|
||||
|
||||
// GetAIProviderKeyByID mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeyByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeyByID indicates an expected call of GetAIProviderKeyByID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeyByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeyByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeyByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAIProviderKeys mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeys", ctx)
|
||||
ret0, _ := ret[0].([]database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeys indicates an expected call of GetAIProviderKeys.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeys(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeys", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeys), ctx)
|
||||
}
|
||||
|
||||
// GetAIProviderKeysByProviderID mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeysByProviderID", ctx, providerID)
|
||||
ret0, _ := ret[0].([]database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeysByProviderID indicates an expected call of GetAIProviderKeysByProviderID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeysByProviderID(ctx, providerID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeysByProviderID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeysByProviderID), ctx, providerID)
|
||||
}
|
||||
|
||||
// GetAIProviders mocks base method.
|
||||
func (m *MockStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviders", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviders indicates an expected call of GetAIProviders.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviders(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviders", reflect.TypeOf((*MockStore)(nil).GetAIProviders), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAPIKeyByID mocks base method.
|
||||
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -6677,6 +6795,36 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAIProvider mocks base method.
|
||||
func (m *MockStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAIProvider", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAIProvider indicates an expected call of InsertAIProvider.
|
||||
func (mr *MockStoreMockRecorder) InsertAIProvider(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProvider", reflect.TypeOf((*MockStore)(nil).InsertAIProvider), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAIProviderKey mocks base method.
|
||||
func (m *MockStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAIProviderKey", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAIProviderKey indicates an expected call of InsertAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) InsertAIProviderKey(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProviderKey", reflect.TypeOf((*MockStore)(nil).InsertAIProviderKey), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAPIKey mocks base method.
|
||||
func (m *MockStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8539,6 +8687,21 @@ func (mr *MockStoreMockRecorder) UpdateAIBridgeInterceptionEnded(ctx, arg any) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIBridgeInterceptionEnded", reflect.TypeOf((*MockStore)(nil).UpdateAIBridgeInterceptionEnded), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAIProvider mocks base method.
|
||||
func (m *MockStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAIProvider", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateAIProvider indicates an expected call of UpdateAIProvider.
|
||||
func (mr *MockStoreMockRecorder) UpdateAIProvider(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIProvider", reflect.TypeOf((*MockStore)(nil).UpdateAIProvider), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAPIKeyByID mocks base method.
|
||||
func (m *MockStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8881,6 +9044,36 @@ func (mr *MockStoreMockRecorder) UpdateCustomRole(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCustomRole", reflect.TypeOf((*MockStore)(nil).UpdateCustomRole), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderKey mocks base method.
|
||||
func (m *MockStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderKey", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderKey indicates an expected call of UpdateEncryptedAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderKey(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderKey", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderKey), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderSettings mocks base method.
|
||||
func (m *MockStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderSettings", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderSettings indicates an expected call of UpdateEncryptedAIProviderSettings.
|
||||
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderSettings(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderSettings", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderSettings), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateExternalAuthLink mocks base method.
|
||||
func (m *MockStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+75
-2
@@ -10,6 +10,11 @@ CREATE TYPE agent_key_scope_enum AS ENUM (
|
||||
'no_user_data'
|
||||
);
|
||||
|
||||
CREATE TYPE ai_provider_type AS ENUM (
|
||||
'openai',
|
||||
'anthropic'
|
||||
);
|
||||
|
||||
CREATE TYPE ai_seat_usage_reason AS ENUM (
|
||||
'aibridge',
|
||||
'task'
|
||||
@@ -226,7 +231,12 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'ai_seat:read',
|
||||
'ai_model_price:*',
|
||||
'ai_model_price:read',
|
||||
'ai_model_price:update'
|
||||
'ai_model_price:update',
|
||||
'ai_provider:*',
|
||||
'ai_provider:create',
|
||||
'ai_provider:delete',
|
||||
'ai_provider:read',
|
||||
'ai_provider:update'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -533,7 +543,9 @@ CREATE TYPE resource_type AS ENUM (
|
||||
'task',
|
||||
'ai_seat',
|
||||
'chat',
|
||||
'user_secret'
|
||||
'user_secret',
|
||||
'ai_provider',
|
||||
'ai_provider_key'
|
||||
);
|
||||
|
||||
CREATE TYPE shareable_workspace_owners AS ENUM (
|
||||
@@ -1108,6 +1120,46 @@ CREATE TABLE ai_model_prices (
|
||||
|
||||
COMMENT ON TABLE ai_model_prices IS 'Per-model token prices used by AI Bridge to compute interception cost.';
|
||||
|
||||
CREATE TABLE ai_provider_keys (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
provider_id uuid NOT NULL,
|
||||
api_key text NOT NULL,
|
||||
api_key_key_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
|
||||
|
||||
CREATE TABLE ai_providers (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
type ai_provider_type NOT NULL,
|
||||
name text NOT NULL,
|
||||
display_name text,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
base_url text NOT NULL,
|
||||
settings text,
|
||||
settings_key_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT ai_providers_name_check CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
|
||||
|
||||
CREATE TABLE ai_seat_state (
|
||||
user_id uuid NOT NULL,
|
||||
first_used_at timestamp with time zone NOT NULL,
|
||||
@@ -3409,6 +3461,12 @@ ALTER TABLE ONLY workspace_agent_stats
|
||||
ALTER TABLE ONLY ai_model_prices
|
||||
ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ai_providers
|
||||
ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ai_seat_state
|
||||
ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
|
||||
@@ -3775,6 +3833,8 @@ ALTER TABLE ONLY workspace_resources
|
||||
ALTER TABLE ONLY workspaces
|
||||
ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
|
||||
|
||||
CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
|
||||
|
||||
CREATE INDEX api_keys_last_used_idx ON api_keys USING btree (last_used DESC);
|
||||
|
||||
COMMENT ON INDEX api_keys_last_used_idx IS 'Index for optimizing api_keys queries filtering by last_used';
|
||||
@@ -3783,6 +3843,10 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
|
||||
|
||||
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
|
||||
|
||||
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys USING btree (provider_id);
|
||||
|
||||
CREATE INDEX idx_ai_providers_enabled ON ai_providers USING btree (enabled) WHERE (deleted = false);
|
||||
|
||||
CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions USING btree (client);
|
||||
|
||||
CREATE INDEX idx_aibridge_interceptions_client_session_id ON aibridge_interceptions USING btree (client_session_id) WHERE (client_session_id IS NOT NULL);
|
||||
@@ -4149,6 +4213,15 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'U
|
||||
the uniqueness requirement. A trigger allows us to enforce uniqueness going
|
||||
forward without requiring a migration to clean up historical data.';
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ai_providers
|
||||
ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
ALTER TABLE ONLY ai_seat_state
|
||||
ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ type ForeignKeyConstraint string
|
||||
|
||||
// ForeignKeyConstraint enums.
|
||||
const (
|
||||
ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
|
||||
ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
|
||||
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS ai_provider_keys;
|
||||
DROP TABLE IF EXISTS ai_providers;
|
||||
DROP TYPE IF EXISTS ai_provider_type;
|
||||
-- No-op for ALTER TYPE resource_type / api_key_scope ADD VALUE:
|
||||
-- Postgres does not allow removing enum values safely.
|
||||
@@ -0,0 +1,67 @@
|
||||
CREATE TYPE ai_provider_type AS ENUM (
|
||||
'openai',
|
||||
'anthropic'
|
||||
);
|
||||
|
||||
CREATE TABLE ai_providers (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type ai_provider_type NOT NULL,
|
||||
name text NOT NULL
|
||||
CONSTRAINT ai_providers_name_check
|
||||
CHECK (name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
|
||||
display_name text,
|
||||
enabled boolean NOT NULL DEFAULT TRUE,
|
||||
deleted boolean NOT NULL DEFAULT FALSE,
|
||||
base_url text NOT NULL,
|
||||
settings text,
|
||||
settings_key_id text REFERENCES dbcrypt_keys(active_key_digest),
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Provider names are unique among live rows only. Soft-deleted rows
|
||||
-- are retained for audit and FK history but do not reserve names.
|
||||
CREATE UNIQUE INDEX ai_providers_name_unique
|
||||
ON ai_providers (name)
|
||||
WHERE deleted = FALSE;
|
||||
|
||||
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
|
||||
|
||||
CREATE INDEX idx_ai_providers_enabled ON ai_providers (enabled) WHERE deleted = FALSE;
|
||||
|
||||
CREATE TABLE ai_provider_keys (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_id uuid NOT NULL REFERENCES ai_providers(id) ON DELETE CASCADE,
|
||||
api_key text NOT NULL,
|
||||
api_key_key_id text REFERENCES dbcrypt_keys(active_key_digest),
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
|
||||
|
||||
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys (provider_id);
|
||||
|
||||
-- Audit support: allow ai_providers and ai_provider_keys to appear in
|
||||
-- audit_log.resource_type.
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider';
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider_key';
|
||||
|
||||
-- API key scopes for ai_provider resources.
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:*';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:update';
|
||||
@@ -0,0 +1,56 @@
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
deleted,
|
||||
base_url,
|
||||
settings
|
||||
) VALUES
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'openai',
|
||||
'openai',
|
||||
'OpenAI (Fixture)',
|
||||
TRUE,
|
||||
FALSE,
|
||||
'https://api.openai.com/v1/',
|
||||
''
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a02',
|
||||
'anthropic',
|
||||
'anthropic-bedrock',
|
||||
'Anthropic via Bedrock (Fixture)',
|
||||
TRUE,
|
||||
FALSE,
|
||||
'https://bedrock-runtime.us-west-2.amazonaws.com/',
|
||||
'{"bedrock_region":"us-west-2","bedrock_model":"global.anthropic.claude-sonnet-4-5-20250929-v1:0","bedrock_access_key":"fixture-bedrock-access-key","bedrock_access_key_secret":"fixture-bedrock-access-key-secret"}'
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a03',
|
||||
'openai',
|
||||
'openai-deleted',
|
||||
'OpenAI (Deleted Fixture)',
|
||||
FALSE,
|
||||
TRUE,
|
||||
'https://api.openai.com/v1/',
|
||||
''
|
||||
);
|
||||
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key
|
||||
) VALUES
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b01',
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'fixture-openai-key'
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b02',
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'fixture-openai-key-failover'
|
||||
);
|
||||
+112
-2
@@ -16,6 +16,64 @@ import (
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
)
|
||||
|
||||
type AIProviderType string
|
||||
|
||||
const (
|
||||
AiProviderTypeOpenai AIProviderType = "openai"
|
||||
AiProviderTypeAnthropic AIProviderType = "anthropic"
|
||||
)
|
||||
|
||||
func (e *AIProviderType) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = AIProviderType(s)
|
||||
case string:
|
||||
*e = AIProviderType(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for AIProviderType: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullAIProviderType struct {
|
||||
AIProviderType AIProviderType `json:"ai_provider_type"`
|
||||
Valid bool `json:"valid"` // Valid is true if AIProviderType is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullAIProviderType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.AIProviderType, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.AIProviderType.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullAIProviderType) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.AIProviderType), nil
|
||||
}
|
||||
|
||||
func (e AIProviderType) Valid() bool {
|
||||
switch e {
|
||||
case AiProviderTypeOpenai,
|
||||
AiProviderTypeAnthropic:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllAIProviderTypeValues() []AIProviderType {
|
||||
return []AIProviderType{
|
||||
AiProviderTypeOpenai,
|
||||
AiProviderTypeAnthropic,
|
||||
}
|
||||
}
|
||||
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
@@ -230,6 +288,11 @@ const (
|
||||
ApiKeyScopeAiModelPrice APIKeyScope = "ai_model_price:*"
|
||||
ApiKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
|
||||
ApiKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
|
||||
ApiKeyScopeAiProvider APIKeyScope = "ai_provider:*"
|
||||
ApiKeyScopeAiProviderCreate APIKeyScope = "ai_provider:create"
|
||||
ApiKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
|
||||
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
||||
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -479,7 +542,12 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeAiSeatRead,
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate:
|
||||
ApiKeyScopeAiModelPriceUpdate,
|
||||
ApiKeyScopeAiProvider,
|
||||
ApiKeyScopeAiProviderCreate,
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -698,6 +766,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate,
|
||||
ApiKeyScopeAiProvider,
|
||||
ApiKeyScopeAiProviderCreate,
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3225,6 +3298,8 @@ const (
|
||||
ResourceTypeAiSeat ResourceType = "ai_seat"
|
||||
ResourceTypeChat ResourceType = "chat"
|
||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||
ResourceTypeAiProvider ResourceType = "ai_provider"
|
||||
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
|
||||
)
|
||||
|
||||
func (e *ResourceType) Scan(src interface{}) error {
|
||||
@@ -3292,7 +3367,9 @@ func (e ResourceType) Valid() bool {
|
||||
ResourceTypeTask,
|
||||
ResourceTypeAiSeat,
|
||||
ResourceTypeChat,
|
||||
ResourceTypeUserSecret:
|
||||
ResourceTypeUserSecret,
|
||||
ResourceTypeAiProvider,
|
||||
ResourceTypeAiProviderKey:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3329,6 +3406,8 @@ func AllResourceTypeValues() []ResourceType {
|
||||
ResourceTypeAiSeat,
|
||||
ResourceTypeChat,
|
||||
ResourceTypeUserSecret,
|
||||
ResourceTypeAiProvider,
|
||||
ResourceTypeAiProviderKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4299,6 +4378,37 @@ type AIBridgeUserPrompt struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.
|
||||
type AIProvider struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
// Optional human-readable label. When NULL, callers should fall back to name.
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
// Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
// Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
// The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.
|
||||
type AIProviderKey struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
|
||||
// API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
// The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
// hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.
|
||||
|
||||
@@ -99,6 +99,8 @@ type sqlcQuerier interface {
|
||||
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||
CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error)
|
||||
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
|
||||
DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error
|
||||
DeleteAPIKeyByID(ctx context.Context, id string) error
|
||||
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error
|
||||
@@ -246,6 +248,21 @@ type sqlcQuerier interface {
|
||||
GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error)
|
||||
GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error)
|
||||
GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error)
|
||||
GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error)
|
||||
GetAIProviderByName(ctx context.Context, name string) (AIProvider, error)
|
||||
GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error)
|
||||
// Returns every AI provider key row, including those belonging to a
|
||||
// soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
// re-encrypt their api_key and clear references to retired keys.
|
||||
GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error)
|
||||
// Returns all keys for a provider, ordered by created_at ASC so the
|
||||
// oldest key is returned first. AI Bridge currently uses the oldest
|
||||
// key per provider; multiple keys are stored to support future
|
||||
// failover and rotation flows.
|
||||
GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error)
|
||||
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
// unless include_deleted or include_disabled is set.
|
||||
GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error)
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// there is no unique constraint on empty token names
|
||||
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
|
||||
@@ -866,6 +883,8 @@ type sqlcQuerier interface {
|
||||
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error)
|
||||
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error)
|
||||
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error)
|
||||
InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error)
|
||||
InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
// We use the organization_id as the id
|
||||
// for simplicity since all users is
|
||||
@@ -1097,6 +1116,7 @@ type sqlcQuerier interface {
|
||||
UnpinChatByID(ctx context.Context, id uuid.UUID) error
|
||||
UnsetDefaultChatModelConfigs(ctx context.Context) error
|
||||
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
|
||||
UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
|
||||
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
|
||||
@@ -1159,6 +1179,15 @@ type sqlcQuerier interface {
|
||||
UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error)
|
||||
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
|
||||
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
|
||||
// Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
// the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
// rotation utility to re-encrypt or decrypt rows in place.
|
||||
UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error)
|
||||
// Updates only the encrypted columns (settings, settings_key_id) and
|
||||
// the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
// rows in place.
|
||||
UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error)
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
// Optimistic lock: only update the row if the refresh token in the database
|
||||
// still matches the one we read before attempting the refresh. This prevents
|
||||
|
||||
@@ -111,6 +111,499 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec
|
||||
DELETE FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAIProviderKey, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAIProviderKeyByID = `-- name: GetAIProviderKeyByID :one
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderKeyByID, id)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviderKeys = `-- name: GetAIProviderKeys :many
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
ORDER BY
|
||||
provider_id ASC,
|
||||
created_at ASC,
|
||||
id ASC
|
||||
`
|
||||
|
||||
// Returns every AI provider key row, including those belonging to a
|
||||
// soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
// re-encrypt their api_key and clear references to retired keys.
|
||||
func (q *sqlQuerier) GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviderKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProviderKey
|
||||
for rows.Next() {
|
||||
var i AIProviderKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); 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 getAIProviderKeysByProviderID = `-- name: GetAIProviderKeysByProviderID :many
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
provider_id = $1::uuid
|
||||
ORDER BY
|
||||
created_at ASC,
|
||||
id ASC
|
||||
`
|
||||
|
||||
// Returns all keys for a provider, ordered by created_at ASC so the
|
||||
// oldest key is returned first. AI Bridge currently uses the oldest
|
||||
// key per provider; multiple keys are stored to support future
|
||||
// failover and rotation flows.
|
||||
func (q *sqlQuerier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderID, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProviderKey
|
||||
for rows.Next() {
|
||||
var i AIProviderKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); 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 insertAIProviderKey = `-- name: InsertAIProviderKey :one
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key,
|
||||
api_key_key_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::timestamptz,
|
||||
$6::timestamptz
|
||||
)
|
||||
RETURNING
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertAIProviderKeyParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAIProviderKey,
|
||||
arg.ID,
|
||||
arg.ProviderID,
|
||||
arg.APIKey,
|
||||
arg.ApiKeyKeyID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateEncryptedAIProviderKey = `-- name: UpdateEncryptedAIProviderKey :one
|
||||
UPDATE
|
||||
ai_provider_keys
|
||||
SET
|
||||
api_key = $1::text,
|
||||
api_key_key_id = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateEncryptedAIProviderKeyParams struct {
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
// Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
// the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
// rotation utility to re-encrypt or decrypt rows in place.
|
||||
func (q *sqlQuerier) UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteAIProviderByID = `-- name: DeleteAIProviderByID :exec
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
deleted = TRUE,
|
||||
enabled = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $1::uuid AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAIProviderByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAIProviderByID = `-- name: GetAIProviderByID :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
id = $1::uuid AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderByID, id)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviderByName = `-- name: GetAIProviderByName :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
name = $1::text AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderByName, name)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviders = `-- name: GetAIProviders :many
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
($1::boolean OR NOT deleted)
|
||||
AND ($2::boolean OR enabled)
|
||||
ORDER BY
|
||||
name ASC
|
||||
`
|
||||
|
||||
type GetAIProvidersParams struct {
|
||||
IncludeDeleted bool `db:"include_deleted" json:"include_deleted"`
|
||||
IncludeDisabled bool `db:"include_disabled" json:"include_disabled"`
|
||||
}
|
||||
|
||||
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
// unless include_deleted or include_disabled is set.
|
||||
func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviders, arg.IncludeDeleted, arg.IncludeDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProvider
|
||||
for rows.Next() {
|
||||
var i AIProvider
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); 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 insertAIProvider = `-- name: InsertAIProvider :one
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
settings_key_id
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::ai_provider_type,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::boolean,
|
||||
$6::text,
|
||||
$7::text,
|
||||
$8::text
|
||||
)
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertAIProviderParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAIProvider,
|
||||
arg.ID,
|
||||
arg.Type,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
arg.SettingsKeyID,
|
||||
)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateAIProvider = `-- name: UpdateAIProvider :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
display_name = $1::text,
|
||||
enabled = $2::boolean,
|
||||
base_url = $3::text,
|
||||
settings = $4::text,
|
||||
settings_key_id = $5::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $6::uuid AND deleted = FALSE
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateAIProviderParams struct {
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateAIProvider,
|
||||
arg.DisplayName,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
arg.SettingsKeyID,
|
||||
arg.ID,
|
||||
)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateEncryptedAIProviderSettings = `-- name: UpdateEncryptedAIProviderSettings :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
settings = $1::text,
|
||||
settings_key_id = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateEncryptedAIProviderSettingsParams struct {
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
// Updates only the encrypted columns (settings, settings_key_id) and
|
||||
// the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
// rows in place.
|
||||
func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderSettings, arg.Settings, arg.SettingsKeyID, arg.ID)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const calculateAIBridgeInterceptionsTelemetrySummary = `-- name: CalculateAIBridgeInterceptionsTelemetrySummary :one
|
||||
WITH interceptions_in_range AS (
|
||||
-- Get all matching interceptions in the given timeframe.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- name: GetAIProviderKeyByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: GetAIProviderKeysByProviderID :many
|
||||
-- Returns all keys for a provider, ordered by created_at ASC so the
|
||||
-- oldest key is returned first. AI Bridge currently uses the oldest
|
||||
-- key per provider; multiple keys are stored to support future
|
||||
-- failover and rotation flows.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
provider_id = @provider_id::uuid
|
||||
ORDER BY
|
||||
created_at ASC,
|
||||
id ASC;
|
||||
|
||||
-- name: GetAIProviderKeys :many
|
||||
-- Returns every AI provider key row, including those belonging to a
|
||||
-- soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
-- re-encrypt their api_key and clear references to retired keys.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
ORDER BY
|
||||
provider_id ASC,
|
||||
created_at ASC,
|
||||
id ASC;
|
||||
|
||||
-- name: InsertAIProviderKey :one
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key,
|
||||
api_key_key_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@provider_id::uuid,
|
||||
@api_key::text,
|
||||
sqlc.narg('api_key_key_id')::text,
|
||||
@created_at::timestamptz,
|
||||
@updated_at::timestamptz
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: DeleteAIProviderKey :exec
|
||||
DELETE FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: UpdateEncryptedAIProviderKey :one
|
||||
-- Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
-- the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
-- rotation utility to re-encrypt or decrypt rows in place.
|
||||
UPDATE
|
||||
ai_provider_keys
|
||||
SET
|
||||
api_key = @api_key::text,
|
||||
api_key_key_id = sqlc.narg('api_key_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- name: GetAIProviderByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE;
|
||||
|
||||
-- name: GetAIProviderByName :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
name = @name::text AND deleted = FALSE;
|
||||
|
||||
-- name: GetAIProviders :many
|
||||
-- Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
-- unless include_deleted or include_disabled is set.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
(@include_deleted::boolean OR NOT deleted)
|
||||
AND (@include_disabled::boolean OR enabled)
|
||||
ORDER BY
|
||||
name ASC;
|
||||
|
||||
-- name: InsertAIProvider :one
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
settings_key_id
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@type::ai_provider_type,
|
||||
@name::text,
|
||||
sqlc.narg('display_name')::text,
|
||||
@enabled::boolean,
|
||||
@base_url::text,
|
||||
sqlc.narg('settings')::text,
|
||||
sqlc.narg('settings_key_id')::text
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateAIProvider :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
display_name = sqlc.narg('display_name')::text,
|
||||
enabled = @enabled::boolean,
|
||||
base_url = @base_url::text,
|
||||
settings = sqlc.narg('settings')::text,
|
||||
settings_key_id = sqlc.narg('settings_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: DeleteAIProviderByID :exec
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
deleted = TRUE,
|
||||
enabled = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE;
|
||||
|
||||
-- name: UpdateEncryptedAIProviderSettings :one
|
||||
-- Updates only the encrypted columns (settings, settings_key_id) and
|
||||
-- the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
-- Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
-- rows in place.
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
settings = sqlc.narg('settings')::text,
|
||||
settings_key_id = sqlc.narg('settings_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
@@ -240,6 +240,9 @@ sql:
|
||||
aibridge_token_usage: AIBridgeTokenUsage
|
||||
aibridge_user_prompt: AIBridgeUserPrompt
|
||||
aibridge_model_thought: AIBridgeModelThought
|
||||
ai_provider: AIProvider
|
||||
ai_provider_key: AIProviderKey
|
||||
ai_provider_type: AIProviderType
|
||||
mcp_server_config: MCPServerConfig
|
||||
mcp_server_configs: MCPServerConfigs
|
||||
mcp_server_user_token: MCPServerUserToken
|
||||
|
||||
@@ -8,6 +8,8 @@ type UniqueConstraint string
|
||||
const (
|
||||
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
|
||||
UniqueAiModelPricesPkey UniqueConstraint = "ai_model_prices_pkey" // ALTER TABLE ONLY ai_model_prices ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
UniqueAiProviderKeysPkey UniqueConstraint = "ai_provider_keys_pkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
|
||||
UniqueAiProvidersPkey UniqueConstraint = "ai_providers_pkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
|
||||
UniqueAiSeatStatePkey UniqueConstraint = "ai_seat_state_pkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
UniqueAibridgeInterceptionsPkey UniqueConstraint = "aibridge_interceptions_pkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
|
||||
UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
|
||||
@@ -130,6 +132,7 @@ const (
|
||||
UniqueWorkspaceResourceMetadataPkey UniqueConstraint = "workspace_resource_metadata_pkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspaceResourcesPkey UniqueConstraint = "workspace_resources_pkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
|
||||
UniqueAiProvidersNameUnique UniqueConstraint = "ai_providers_name_unique" // CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
|
||||
UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
|
||||
UniqueIndexChatDebugRunsIDChat UniqueConstraint = "idx_chat_debug_runs_id_chat" // CREATE UNIQUE INDEX idx_chat_debug_runs_id_chat ON chat_debug_runs USING btree (id, chat_id);
|
||||
UniqueIndexChatDebugStepsRunStep UniqueConstraint = "idx_chat_debug_steps_run_step" // CREATE UNIQUE INDEX idx_chat_debug_steps_run_step ON chat_debug_steps USING btree (run_id, step_number);
|
||||
|
||||
@@ -23,6 +23,16 @@ var (
|
||||
Type: "ai_model_price",
|
||||
}
|
||||
|
||||
// ResourceAIProvider
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an AI provider
|
||||
// - "ActionDelete" :: delete an AI provider
|
||||
// - "ActionRead" :: read AI provider configuration
|
||||
// - "ActionUpdate" :: update an AI provider
|
||||
ResourceAIProvider = Object{
|
||||
Type: "ai_provider",
|
||||
}
|
||||
|
||||
// ResourceAiSeat
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: record AI seat usage
|
||||
@@ -450,6 +460,7 @@ func AllResources() []Objecter {
|
||||
return []Objecter{
|
||||
ResourceWildcard,
|
||||
ResourceAiModelPrice,
|
||||
ResourceAIProvider,
|
||||
ResourceAiSeat,
|
||||
ResourceAibridgeInterception,
|
||||
ResourceApiKey,
|
||||
|
||||
@@ -398,6 +398,15 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionUpdate: "update AI model prices",
|
||||
},
|
||||
},
|
||||
"ai_provider": {
|
||||
Name: "AIProvider",
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: "read AI provider configuration",
|
||||
ActionCreate: "create an AI provider",
|
||||
ActionUpdate: "update an AI provider",
|
||||
ActionDelete: "delete an AI provider",
|
||||
},
|
||||
},
|
||||
"ai_seat": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: "record AI seat usage",
|
||||
|
||||
@@ -1105,6 +1105,25 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Only owners can manage AI providers. Provider
|
||||
// configuration is deployment-wide and includes secret
|
||||
// material (api_key, settings) so it is not exposed to
|
||||
// org admins or auditors.
|
||||
Name: "AIProviders",
|
||||
Actions: crud,
|
||||
Resource: rbac.ResourceAIProvider,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "BoundaryUsage",
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
|
||||
@@ -9,6 +9,10 @@ package rbac
|
||||
const (
|
||||
ScopeAiModelPriceRead ScopeName = "ai_model_price:read"
|
||||
ScopeAiModelPriceUpdate ScopeName = "ai_model_price:update"
|
||||
ScopeAiProviderCreate ScopeName = "ai_provider:create"
|
||||
ScopeAiProviderDelete ScopeName = "ai_provider:delete"
|
||||
ScopeAiProviderRead ScopeName = "ai_provider:read"
|
||||
ScopeAiProviderUpdate ScopeName = "ai_provider:update"
|
||||
ScopeAiSeatCreate ScopeName = "ai_seat:create"
|
||||
ScopeAiSeatRead ScopeName = "ai_seat:read"
|
||||
ScopeAibridgeInterceptionCreate ScopeName = "aibridge_interception:create"
|
||||
@@ -177,6 +181,10 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAiModelPriceRead,
|
||||
ScopeAiModelPriceUpdate,
|
||||
ScopeAiProviderCreate,
|
||||
ScopeAiProviderDelete,
|
||||
ScopeAiProviderRead,
|
||||
ScopeAiProviderUpdate,
|
||||
ScopeAiSeatCreate,
|
||||
ScopeAiSeatRead,
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
@@ -346,6 +354,10 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAiModelPriceRead,
|
||||
ScopeAiModelPriceUpdate,
|
||||
ScopeAiProviderCreate,
|
||||
ScopeAiProviderDelete,
|
||||
ScopeAiProviderRead,
|
||||
ScopeAiProviderUpdate,
|
||||
ScopeAiSeatCreate,
|
||||
ScopeAiSeatRead,
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
|
||||
Reference in New Issue
Block a user