Add database models and infrastructure for AI operation tracking and organization-level AI configuration. OperationLog model tracks all AI operations for auditing, including: - Operation type, tier, and trigger event - Token usage (input/output) - Status tracking (pending, success, failed, escalated) - Performance metrics (duration) - Rate limiting support via CountRecentOperations OrgAISettings model stores per-organization AI configuration: - Provider and model selection - Encrypted API key storage - Rate limits (max operations per hour) - Allowed operations whitelist - Agent mode permissions Also adds AI unit type to repository units for enabling/disabling AI features per repo.
136 lines
4.0 KiB
Go
136 lines
4.0 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ai
|
|
|
|
import (
|
|
"context"
|
|
|
|
"code.gitcaddy.com/server/v3/models/db"
|
|
secret_module "code.gitcaddy.com/server/v3/modules/secret"
|
|
"code.gitcaddy.com/server/v3/modules/setting"
|
|
"code.gitcaddy.com/server/v3/modules/timeutil"
|
|
)
|
|
|
|
func init() {
|
|
db.RegisterModel(new(OrgAISettings))
|
|
}
|
|
|
|
// OrgAISettings stores AI configuration per organization
|
|
type OrgAISettings struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
OrgID int64 `xorm:"UNIQUE NOT NULL INDEX"`
|
|
Provider string `xorm:"NOT NULL DEFAULT ''"`
|
|
Model string `xorm:"NOT NULL DEFAULT ''"`
|
|
APIKeyEncrypted string `xorm:"TEXT"`
|
|
MaxOpsPerHour int `xorm:"NOT NULL DEFAULT 0"`
|
|
AllowedOps string `xorm:"TEXT"` // JSON array of allowed operation names
|
|
AgentModeAllowed bool `xorm:"NOT NULL DEFAULT false"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
|
|
}
|
|
|
|
// TableName returns the table name for OrgAISettings
|
|
func (OrgAISettings) TableName() string {
|
|
return "org_ai_settings"
|
|
}
|
|
|
|
// SetAPIKey encrypts and stores the API key
|
|
func (s *OrgAISettings) SetAPIKey(key string) error {
|
|
if key == "" {
|
|
s.APIKeyEncrypted = ""
|
|
return nil
|
|
}
|
|
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.APIKeyEncrypted = encrypted
|
|
return nil
|
|
}
|
|
|
|
// GetAPIKey decrypts and returns the API key
|
|
func (s *OrgAISettings) GetAPIKey() (string, error) {
|
|
if s.APIKeyEncrypted == "" {
|
|
return "", nil
|
|
}
|
|
return secret_module.DecryptSecret(setting.SecretKey, s.APIKeyEncrypted)
|
|
}
|
|
|
|
// GetOrgAISettings returns the AI settings for an organization
|
|
func GetOrgAISettings(ctx context.Context, orgID int64) (*OrgAISettings, error) {
|
|
settings := &OrgAISettings{OrgID: orgID}
|
|
has, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Get(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, nil
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
// CreateOrUpdateOrgAISettings creates or updates AI settings for an organization
|
|
func CreateOrUpdateOrgAISettings(ctx context.Context, settings *OrgAISettings) error {
|
|
existing := &OrgAISettings{}
|
|
has, err := db.GetEngine(ctx).Where("org_id = ?", settings.OrgID).Get(existing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
settings.ID = existing.ID
|
|
_, err = db.GetEngine(ctx).ID(existing.ID).AllCols().Update(settings)
|
|
return err
|
|
}
|
|
return db.Insert(ctx, settings)
|
|
}
|
|
|
|
// ResolveProvider resolves the AI provider using the cascade: repo → org → system
|
|
func ResolveProvider(ctx context.Context, orgID int64, repoProvider string) string {
|
|
if repoProvider != "" {
|
|
return repoProvider
|
|
}
|
|
if orgID > 0 {
|
|
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Provider != "" {
|
|
return orgSettings.Provider
|
|
}
|
|
}
|
|
return setting.AI.DefaultProvider
|
|
}
|
|
|
|
// ResolveModel resolves the AI model using the cascade: repo → org → system
|
|
func ResolveModel(ctx context.Context, orgID int64, repoModel string) string {
|
|
if repoModel != "" {
|
|
return repoModel
|
|
}
|
|
if orgID > 0 {
|
|
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Model != "" {
|
|
return orgSettings.Model
|
|
}
|
|
}
|
|
return setting.AI.DefaultModel
|
|
}
|
|
|
|
// ResolveAPIKey resolves the API key using the cascade: org → system
|
|
func ResolveAPIKey(ctx context.Context, orgID int64, provider string) string {
|
|
// Try org-level key first
|
|
if orgID > 0 {
|
|
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil {
|
|
if key, err := orgSettings.GetAPIKey(); err == nil && key != "" {
|
|
return key
|
|
}
|
|
}
|
|
}
|
|
// Fall back to system-level key
|
|
switch provider {
|
|
case "claude":
|
|
return setting.AI.ClaudeAPIKey
|
|
case "openai":
|
|
return setting.AI.OpenAIAPIKey
|
|
case "gemini":
|
|
return setting.AI.GeminiAPIKey
|
|
default:
|
|
return ""
|
|
}
|
|
}
|