2
0
Files
logikonline 26793bf898 feat(ai): add ai operation logging and org settings models
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.
2026-02-11 23:46:57 -05:00

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 ""
}
}