7005 lines
251 KiB
Go
7005 lines
251 KiB
Go
package configstore
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/google/uuid"
|
|
bifrost "github.com/maximhq/bifrost/core"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/maximhq/bifrost/framework/encrypt"
|
|
"github.com/maximhq/bifrost/framework/migrator"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const (
|
|
// migrationAdvisoryLockKey is used for PostgreSQL advisory locks
|
|
// to serialize migrations across cluster nodes
|
|
migrationAdvisoryLockKey = 1000001
|
|
)
|
|
|
|
// migrationLock holds a dedicated connection for the advisory lock.
|
|
// This ensures the lock is held on the same connection throughout migrations,
|
|
// preventing race conditions caused by GORM's connection pooling.
|
|
type migrationLock struct {
|
|
conn *sql.Conn
|
|
}
|
|
|
|
// acquireMigrationLock gets a dedicated connection and acquires an advisory lock.
|
|
// For non-PostgreSQL databases, returns a no-op lock.
|
|
func acquireMigrationLock(ctx context.Context, db *gorm.DB) (*migrationLock, error) {
|
|
if db.Dialector.Name() != "postgres" {
|
|
return &migrationLock{}, nil
|
|
}
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sql.DB: %w", err)
|
|
}
|
|
|
|
// Get a dedicated connection (not returned to pool until Close())
|
|
conn, err := sqlDB.Conn(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get dedicated connection: %w", err)
|
|
}
|
|
|
|
// Acquire advisory lock on this dedicated connection.
|
|
// This will BLOCK if another node holds the lock.
|
|
_, err = conn.ExecContext(ctx, "SELECT pg_advisory_lock($1)", migrationAdvisoryLockKey)
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("failed to acquire migration advisory lock: %w", err)
|
|
}
|
|
|
|
return &migrationLock{conn: conn}, nil
|
|
}
|
|
|
|
// release unlocks and closes the dedicated connection
|
|
func (l *migrationLock) release(ctx context.Context) {
|
|
if l.conn == nil {
|
|
return
|
|
}
|
|
// Release lock on the SAME connection that acquired it
|
|
_, _ = l.conn.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", migrationAdvisoryLockKey)
|
|
l.conn.Close()
|
|
}
|
|
|
|
// RunSingleMigration applies a single gormigrate migration on the given
|
|
// *gorm.DB. Mirrors (*RDBConfigStore).RunMigration but takes the *gorm.DB
|
|
// directly, so downstream consumers (bifrost-enterprise, plugins) can run
|
|
// their migrations inside a MigrateOnFreshConnection callback without having
|
|
// to reach the throwaway pool through the ConfigStore abstraction.
|
|
func RunSingleMigration(ctx context.Context, db *gorm.DB, migration *migrator.Migration) error {
|
|
if db == nil {
|
|
return fmt.Errorf("db cannot be nil")
|
|
}
|
|
if migration == nil {
|
|
return fmt.Errorf("migration cannot be nil")
|
|
}
|
|
m := migrator.New(db.WithContext(ctx), migrator.DefaultOptions, []*migrator.Migration{migration})
|
|
return m.Migrate()
|
|
}
|
|
|
|
type legacyBudgetVirtualKey struct {
|
|
tables.TableVirtualKey
|
|
BudgetID *string `gorm:"column:budget_id;type:varchar(255);index"`
|
|
}
|
|
|
|
func (legacyBudgetVirtualKey) TableName() string { return "governance_virtual_keys" }
|
|
|
|
type legacyBudgetVirtualKeyProviderConfig struct {
|
|
tables.TableVirtualKeyProviderConfig
|
|
BudgetID *string `gorm:"column:budget_id;type:varchar(255);index"`
|
|
}
|
|
|
|
func (legacyBudgetVirtualKeyProviderConfig) TableName() string {
|
|
return "governance_virtual_key_provider_configs"
|
|
}
|
|
|
|
type legacyBudgetTeam struct {
|
|
tables.TableTeam
|
|
BudgetID *string `gorm:"column:budget_id;type:varchar(255);index"`
|
|
}
|
|
|
|
func (legacyBudgetTeam) TableName() string { return "governance_teams" }
|
|
|
|
type sqliteColumnInfo struct {
|
|
Name string `gorm:"column:name"`
|
|
}
|
|
|
|
func legacyBudgetColumnModel(tableName string) (any, error) {
|
|
switch tableName {
|
|
case "governance_virtual_keys":
|
|
return &legacyBudgetVirtualKey{}, nil
|
|
case "governance_virtual_key_provider_configs":
|
|
return &legacyBudgetVirtualKeyProviderConfig{}, nil
|
|
case "governance_teams":
|
|
return &legacyBudgetTeam{}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported legacy budget column drop table: %s", tableName)
|
|
}
|
|
}
|
|
|
|
func currentBudgetOwnerModel(tableName string) (any, error) {
|
|
switch tableName {
|
|
case "governance_virtual_keys":
|
|
return &tables.TableVirtualKey{}, nil
|
|
case "governance_virtual_key_provider_configs":
|
|
return &tables.TableVirtualKeyProviderConfig{}, nil
|
|
case "governance_teams":
|
|
return &tables.TableTeam{}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported legacy budget column drop table: %s", tableName)
|
|
}
|
|
}
|
|
|
|
func quoteSQLiteIdentifier(name string) string {
|
|
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
|
}
|
|
|
|
func sqliteTableColumns(tx *gorm.DB, tableName string) ([]string, error) {
|
|
var columns []sqliteColumnInfo
|
|
query := fmt.Sprintf("PRAGMA table_info(%s)", quoteSQLiteIdentifier(tableName))
|
|
if err := tx.Raw(query).Scan(&columns).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]string, 0, len(columns))
|
|
for _, column := range columns {
|
|
result = append(result, column.Name)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func sqliteTableHasColumn(tx *gorm.DB, tableName, columnName string) (bool, error) {
|
|
columns, err := sqliteTableColumns(tx, tableName)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if slices.Contains(columns, columnName) {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// sqliteDropLegacyBudgetColumn removes the legacy budget_id column from a
|
|
// SQLite table by dumping data, recreating the table from the current GORM
|
|
// model, and copying data back.
|
|
//
|
|
// Strategy: dump-data → drop-original → create-clean → restore-data.
|
|
// We never RENAME the original table because SQLite propagates ALTER TABLE
|
|
// RENAME into FK references in OTHER tables, corrupting them.
|
|
func sqliteDropLegacyBudgetColumn(tx *gorm.DB, tableName string) error {
|
|
model, err := currentBudgetOwnerModel(tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
columns, err := sqliteTableColumns(tx, tableName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to inspect SQLite columns for %s: %w", tableName, err)
|
|
}
|
|
|
|
preservedColumns := make([]string, 0, len(columns))
|
|
for _, column := range columns {
|
|
if column != "budget_id" {
|
|
preservedColumns = append(preservedColumns, column)
|
|
}
|
|
}
|
|
if len(preservedColumns) == len(columns) {
|
|
return nil // budget_id column not present, nothing to do
|
|
}
|
|
|
|
// Build the column list for data transfer.
|
|
quotedColumns := make([]string, 0, len(preservedColumns))
|
|
for _, column := range preservedColumns {
|
|
quotedColumns = append(quotedColumns, quoteSQLiteIdentifier(column))
|
|
}
|
|
columnList := strings.Join(quotedColumns, ", ")
|
|
|
|
// Dump existing data into a temporary table (data-only, no constraints).
|
|
dumpTable := tableName + "__dump"
|
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", quoteSQLiteIdentifier(dumpTable))).Error; err != nil {
|
|
return fmt.Errorf("failed to drop stale dump table %s: %w", dumpTable, err)
|
|
}
|
|
dumpSQL := fmt.Sprintf("CREATE TABLE %s AS SELECT %s FROM %s",
|
|
quoteSQLiteIdentifier(dumpTable), columnList, quoteSQLiteIdentifier(tableName))
|
|
if err := tx.Exec(dumpSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to dump %s data: %w", tableName, err)
|
|
}
|
|
|
|
// Drop the original table. Safe because PRAGMA foreign_keys is OFF.
|
|
// This also removes all indexes and FK definitions cleanly.
|
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE %s", quoteSQLiteIdentifier(tableName))).Error; err != nil {
|
|
return fmt.Errorf("failed to drop original SQLite table %s: %w", tableName, err)
|
|
}
|
|
|
|
// Recreate the table from the current GORM model (no budget_id column,
|
|
// proper indexes and constraints). The original table name is now free.
|
|
if err := tx.Migrator().CreateTable(model); err != nil {
|
|
return fmt.Errorf("failed to recreate SQLite table %s: %w", tableName, err)
|
|
}
|
|
|
|
// Restore data from the dump.
|
|
restoreSQL := fmt.Sprintf("INSERT INTO %s (%s) SELECT %s FROM %s",
|
|
quoteSQLiteIdentifier(tableName), columnList, columnList, quoteSQLiteIdentifier(dumpTable))
|
|
if err := tx.Exec(restoreSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to restore data into %s: %w", tableName, err)
|
|
}
|
|
|
|
// Clean up the dump table.
|
|
if err := tx.Exec(fmt.Sprintf("DROP TABLE %s", quoteSQLiteIdentifier(dumpTable))).Error; err != nil {
|
|
return fmt.Errorf("failed to drop dump table %s: %w", dumpTable, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dropLegacyBudgetColumn(tx *gorm.DB, tableName string) error {
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(tableName, "budget_id") {
|
|
return nil
|
|
}
|
|
|
|
if tx.Dialector.Name() == "sqlite" {
|
|
if err := sqliteDropLegacyBudgetColumn(tx, tableName); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
model, err := legacyBudgetColumnModel(tableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := mg.DropColumn(model, "budget_id"); err != nil {
|
|
return fmt.Errorf("failed to drop legacy %s.budget_id column: %w", tableName, err)
|
|
}
|
|
}
|
|
|
|
var stillExists bool
|
|
var err error
|
|
if tx.Dialector.Name() == "sqlite" {
|
|
stillExists, err = sqliteTableHasColumn(tx, tableName, "budget_id")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify legacy %s.budget_id column drop: %w", tableName, err)
|
|
}
|
|
} else {
|
|
stillExists = mg.HasColumn(tableName, "budget_id")
|
|
}
|
|
if stillExists {
|
|
return fmt.Errorf("legacy %s.budget_id column still exists after migration", tableName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Migrate performs the necessary database migrations.
|
|
func triggerMigrations(ctx context.Context, db *gorm.DB) error {
|
|
// Acquire advisory lock to serialize migrations across cluster nodes.
|
|
// This prevents race conditions when multiple nodes start simultaneously
|
|
// and try to create the same tables in parallel.
|
|
lock, err := acquireMigrationLock(ctx, db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lock.release(ctx)
|
|
|
|
if err := migrationInit(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationMany2ManyJoinTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddCustomProviderConfigJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVirtualKeyProviderConfigTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAllowedOriginsJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAllowDirectKeysColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddEnableLiteLLMFallbacksColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationTeamsTableUpdates(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddKeyNameColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddFrameworkConfigsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationCleanupMCPClientToolsConfig(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVirtualKeyMCPConfigsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPluginPathColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddProviderConfigBudgetRateLimit(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddSessionsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddHeadersJSONColumnIntoMCPClient(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddDisableContentLoggingColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPClientIDColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVertexProjectNumberColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVertexDeploymentsJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationMissingProviderColumnInKeyTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddToolsToAutoExecuteJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddIsCodeModeClientColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddLogRetentionDaysColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddEnabledColumnToKeyTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddBatchAndCachePricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPAgentDepthAndMCPToolExecutionTimeoutColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPCodeModeBindingLevelColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationNormalizeMCPClientNames(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationMoveKeysToProviderConfig(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPluginVersionColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddSendBackRawRequestColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddConfigHashColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVirtualKeyConfigHashColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAdditionalConfigHashColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAdd200kTokenPricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddImagePricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddUseForBatchAPIColumnAndS3BucketsConfig(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddHeaderFilterConfigJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAzureClientIDAndClientSecretAndTenantIDColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddDistributedLocksTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddModelConfigTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddProviderGovernanceColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAllowedHeadersJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddDisableDBPingsInHealthColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddIsPingAvailableColumnToMCPClientTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddToolPricingJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationRemoveServerPrefixFromMCPTools(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddOAuthTables(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddToolSyncIntervalColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPClientConfigToOAuthConfig(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddRoutingRulesTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddBaseModelPricingColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAzureScopesColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddReplicateDeploymentsJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddKeyStatusColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddProviderStatusColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddRateLimitToTeamsAndCustomers(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAsyncJobResultTTLColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddRequiredHeadersJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddLoggingHeadersJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddHideDeletedVirtualKeysInFiltersColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddEnforceSCIMAuthColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddEnforceAuthOnInferenceColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationReconcilePricingOverridesTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddEncryptionColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddOutputCostPerVideoPerSecond(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationDropEnableGovernanceColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddVLLMKeyConfigColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationWidenEncryptedVarcharColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddBedrockAssumeRoleColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddStoreRawRequestResponseColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPricingRefactorColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationRenameTruncatedPricingColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddImageQualityPricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddRoutingTargetsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPromptRepoTables(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPluginOrderColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAllowAllKeysToProviderConfig(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationBackfillEmptyVirtualKeyConfigs(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPDisableAutoToolInjectColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationBackfillAllowedModelsWildcard(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPClientAllowedExtraHeadersJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationMakeBasePricingColumnsNullable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddAllowOnAllVirtualKeysColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddOpenAIConfigJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddKeyBlacklistedModelsJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddChainRuleColumnToRoutingRules(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationDropDeploymentColumnsAndAddAliases(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddReplicateKeyConfigColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddBudgetCalendarAlignedColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddRoutingChainMaxDepthColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPromptVariablesColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddModelCapabilityColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddOllamaSGLConfigColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMultiBudgetTables(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPerUserOAuthTables(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddMCPClientDiscoveredToolsColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddWhitelistedRoutesJSONColumn(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationReplaceEnableLiteLLMWithCompatColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddModelPricingUniqueIndex(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationDefaultCompatShouldConvertParamsFalse(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddPriorityTierPricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddFlexTierPricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationNormalizeOtelTraceType(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrateCalendarAlignedToBudgetsAndRateLimitsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddTeamBudgetsToBudgetsTable(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if err := migrationAddOCRPricingColumns(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationAddStoreRawRequestResponseColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_store_raw_request_response_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "store_raw_request_response") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "store_raw_request_response"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Backfill config_hash for existing providers so they don't appear
|
|
// dirty after upgrade. StoreRawRequestResponse is now part of the
|
|
// hash input; rows written before this migration have stale hashes.
|
|
var providers []tables.TableProvider
|
|
if err := tx.
|
|
Select(
|
|
"id",
|
|
"name",
|
|
"network_config_json",
|
|
"concurrency_buffer_json",
|
|
"proxy_config_json",
|
|
"custom_provider_config_json",
|
|
"send_back_raw_request",
|
|
"send_back_raw_response",
|
|
"store_raw_request_response",
|
|
"encryption_status",
|
|
).
|
|
Find(&providers).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch providers for hash backfill: %w", err)
|
|
}
|
|
for _, provider := range providers {
|
|
providerConfig := ProviderConfig{
|
|
NetworkConfig: provider.NetworkConfig,
|
|
ConcurrencyAndBufferSize: provider.ConcurrencyAndBufferSize,
|
|
ProxyConfig: provider.ProxyConfig,
|
|
SendBackRawRequest: provider.SendBackRawRequest,
|
|
SendBackRawResponse: provider.SendBackRawResponse,
|
|
StoreRawRequestResponse: provider.StoreRawRequestResponse,
|
|
CustomProviderConfig: provider.CustomProviderConfig,
|
|
}
|
|
// Here the default value of store_raw_request_response should be based on the default value of SendBackRawRequest and SendBackRawResponse
|
|
if provider.SendBackRawRequest || provider.SendBackRawResponse {
|
|
providerConfig.StoreRawRequestResponse = true
|
|
}
|
|
hash, err := providerConfig.GenerateConfigHash(provider.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for provider %s: %w", provider.Name, err)
|
|
}
|
|
if err := tx.Model(&provider).Updates(map[string]interface{}{
|
|
"config_hash": hash,
|
|
"store_raw_request_response": providerConfig.StoreRawRequestResponse,
|
|
}).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for provider %s: %w", provider.Name, err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "store_raw_request_response"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add store raw request response column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationInit is the first migration
|
|
func migrationInit(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "init",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.TableConfigHash{}) {
|
|
if err := migrator.CreateTable(&tables.TableConfigHash{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// TableBudget and TableRateLimit must be created before TableProvider
|
|
// because TableProvider has FK references to them
|
|
if !migrator.HasTable(&tables.TableBudget{}) {
|
|
if err := migrator.CreateTable(&tables.TableBudget{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableRateLimit{}) {
|
|
if err := migrator.CreateTable(&tables.TableRateLimit{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableProvider{}) {
|
|
if err := migrator.CreateTable(&tables.TableProvider{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableKey{}) {
|
|
if err := migrator.CreateTable(&tables.TableKey{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableModel{}) {
|
|
if err := migrator.CreateTable(&tables.TableModel{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableOauthConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableOauthConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableOauthToken{}) {
|
|
if err := migrator.CreateTable(&tables.TableOauthToken{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableMCPClient{}) {
|
|
if err := migrator.CreateTable(&tables.TableMCPClient{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableClientConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableClientConfig{}); err != nil {
|
|
return err
|
|
}
|
|
} else if !migrator.HasColumn(&tables.TableClientConfig{}, "max_request_body_size_mb") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "max_request_body_size_mb"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableEnvKey{}) {
|
|
if err := migrator.CreateTable(&tables.TableEnvKey{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableVectorStoreConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableVectorStoreConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableLogStoreConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableLogStoreConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableCustomer{}) {
|
|
if err := migrator.CreateTable(&tables.TableCustomer{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableTeam{}) {
|
|
if err := migrator.CreateTable(&tables.TableTeam{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableVirtualKey{}) {
|
|
if err := migrator.CreateTable(&tables.TableVirtualKey{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableGovernanceConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableGovernanceConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TableModelPricing{}) {
|
|
if err := migrator.CreateTable(&tables.TableModelPricing{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TablePricingOverride{}) {
|
|
if err := migrator.CreateTable(&tables.TablePricingOverride{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasTable(&tables.TablePlugin{}) {
|
|
if err := migrator.CreateTable(&tables.TablePlugin{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Drop children first, then parents (adjust if your actual FKs differ)
|
|
if err := migrator.DropTable(&tables.TableVirtualKey{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableKey{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableTeam{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableProvider{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableCustomer{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableBudget{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableRateLimit{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableModel{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableMCPClient{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableClientConfig{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableEnvKey{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableVectorStoreConfig{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableLogStoreConfig{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableGovernanceConfig{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableModelPricing{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePricingOverride{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePlugin{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableConfigHash{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createMany2ManyJoinTable creates a many-to-many join table for the given tables.
|
|
func migrationMany2ManyJoinTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "many2manyjoin",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// create the many-to-many join table for virtual keys and keys
|
|
if !migrator.HasTable("governance_virtual_key_keys") {
|
|
createJoinTableSQL := `
|
|
CREATE TABLE IF NOT EXISTS governance_virtual_key_keys (
|
|
table_virtual_key_id VARCHAR(255) NOT NULL,
|
|
table_key_id INTEGER NOT NULL,
|
|
PRIMARY KEY (table_virtual_key_id, table_key_id),
|
|
FOREIGN KEY (table_virtual_key_id) REFERENCES governance_virtual_keys(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (table_key_id) REFERENCES config_keys(id) ON DELETE CASCADE
|
|
)
|
|
`
|
|
if err := tx.Exec(createJoinTableSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to create governance_virtual_key_keys table: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
if err := tx.Exec("DROP TABLE IF EXISTS governance_virtual_key_keys").Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddCustomProviderConfigJSONColumn adds the custom_provider_config_json column to the provider table
|
|
func migrationAddCustomProviderConfigJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "addcustomproviderconfigjsoncolumn",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "custom_provider_config_json") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "custom_provider_config_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVirtualKeyProviderConfigTable adds the virtual_key_provider_config table
|
|
func migrationAddVirtualKeyProviderConfigTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "addvirtualkeyproviderconfig",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasTable(&tables.TableVirtualKeyProviderConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableVirtualKeyProviderConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if err := migrator.DropTable(&tables.TableVirtualKeyProviderConfig{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAllowedOriginsJSONColumn adds the allowed_origins_json column to the client config table
|
|
func migrationAddAllowedOriginsJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_allowed_origins_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "allowed_origins_json") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "allowed_origins_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAllowDirectKeysColumn adds the allow_direct_keys column to the client config table
|
|
func migrationAddAllowDirectKeysColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_allow_direct_keys_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "allow_direct_keys") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "allow_direct_keys"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddEnableLiteLLMFallbacksColumn adds the enable_litellm_fallbacks column to the client config table
|
|
func migrationAddEnableLiteLLMFallbacksColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_enable_litellm_fallbacks_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
// Use raw SQL since the struct field was removed in a later migration.
|
|
// This column is subsequently dropped by migrationReplaceEnableLiteLLMWithCompatColumns.
|
|
if !tx.Migrator().HasColumn(&tables.TableClientConfig{}, "enable_litellm_fallbacks") {
|
|
if err := tx.Exec("ALTER TABLE config_client ADD COLUMN enable_litellm_fallbacks BOOLEAN DEFAULT FALSE").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if err := tx.Exec("ALTER TABLE config_client DROP COLUMN IF EXISTS enable_litellm_fallbacks").Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationTeamsTableUpdates adds profile, config, and claims columns to the team table
|
|
func migrationTeamsTableUpdates(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_profile_config_claims_columns_to_team_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableTeam{}, "profile") {
|
|
if err := migrator.AddColumn(&tables.TableTeam{}, "profile"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableTeam{}, "config") {
|
|
if err := migrator.AddColumn(&tables.TableTeam{}, "config"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableTeam{}, "claims") {
|
|
if err := migrator.AddColumn(&tables.TableTeam{}, "claims"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddFrameworkConfigsTable adds the framework_configs table
|
|
func migrationAddFrameworkConfigsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_framework_configs_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.TableFrameworkConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableFrameworkConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddKeyNameColumn adds the name column to the key table and populates unique names
|
|
func migrationAddKeyNameColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_key_name_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "name") {
|
|
// Step 1: Add the column as nullable first
|
|
if err := tx.Exec("ALTER TABLE config_keys ADD COLUMN name VARCHAR(255)").Error; err != nil {
|
|
return fmt.Errorf("failed to add name column: %w", err)
|
|
}
|
|
|
|
// Step 2: Populate unique names for all existing keys
|
|
var keys []tables.TableKey
|
|
if err := tx.Find(&keys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch keys: %w", err)
|
|
}
|
|
|
|
for _, key := range keys {
|
|
// Create unique name: provider_name-key-{first8chars_of_key_id}-{key_index}
|
|
keyIDShort := key.KeyID
|
|
if len(keyIDShort) > 8 {
|
|
keyIDShort = keyIDShort[:8]
|
|
}
|
|
keyName := keyIDShort + "-" + strconv.Itoa(int(key.ID))
|
|
uniqueName := fmt.Sprintf("%s-key-%s", key.Provider, keyName)
|
|
|
|
// Update the key with the unique name
|
|
if err := tx.Model(&key).Update("name", uniqueName).Error; err != nil {
|
|
return fmt.Errorf("failed to update key %s with name %s: %w", key.KeyID, uniqueName, err)
|
|
}
|
|
}
|
|
|
|
// Step 3: Add unique index (SQLite compatible)
|
|
if err := tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_key_name ON config_keys (name)").Error; err != nil {
|
|
return fmt.Errorf("failed to create unique index on name: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Drop the unique index first to avoid orphaned index artifacts
|
|
if err := tx.Exec("DROP INDEX IF EXISTS idx_key_name").Error; err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "name"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationCleanupMCPClientToolsConfig removes ToolsToSkipJSON column and converts empty ToolsToExecuteJSON to wildcard
|
|
func migrationCleanupMCPClientToolsConfig(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "cleanup_mcp_client_tools_config",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Step 1: Remove ToolsToSkipJSON column if it exists (cleanup from old versions)
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "tools_to_skip_json") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "tools_to_skip_json"); err != nil {
|
|
return fmt.Errorf("failed to drop tools_to_skip_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Alternative column name variations that might exist
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "ToolsToSkipJSON") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "ToolsToSkipJSON"); err != nil {
|
|
return fmt.Errorf("failed to drop ToolsToSkipJSON column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Step 2: Update empty ToolsToExecuteJSON arrays to wildcard ["*"]
|
|
// Convert "[]" (empty array) to "[\"*\"]" (wildcard array) for backward compatibility
|
|
updateSQL := `
|
|
UPDATE config_mcp_clients
|
|
SET tools_to_execute_json = '["*"]'
|
|
WHERE tools_to_execute_json = '[]' OR tools_to_execute_json = '' OR tools_to_execute_json IS NULL
|
|
`
|
|
if err := tx.Exec(updateSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to update empty ToolsToExecuteJSON to wildcard: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// For rollback, we could add the column back, but since we're moving away from this
|
|
// functionality, we'll just revert the wildcard changes back to empty arrays
|
|
tx = tx.WithContext(ctx)
|
|
|
|
revertSQL := `
|
|
UPDATE config_mcp_clients
|
|
SET tools_to_execute_json = '[]'
|
|
WHERE tools_to_execute_json = '["*"]'
|
|
`
|
|
if err := tx.Exec(revertSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to revert wildcard ToolsToExecuteJSON to empty arrays: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running MCP client tools cleanup migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVirtualKeyMCPConfigsTable adds the virtual_key_mcp_configs table
|
|
func migrationAddVirtualKeyMCPConfigsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_vk_mcp_configs_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.TableVirtualKeyMCPConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableVirtualKeyMCPConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropTable(&tables.TableVirtualKeyMCPConfig{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddProviderConfigBudgetRateLimit adds budget_id and rate_limit_id columns with proper foreign key constraints
|
|
func migrationAddProviderConfigBudgetRateLimit(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_provider_config_budget_rate_limit",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Add budget_id and rate_limit_id columns if they don't exist
|
|
// Note: budget_id is added via raw SQL because the field was later removed from the struct
|
|
// (migrated to governance_budgets.provider_config_id in add_multi_budget_tables)
|
|
if migrator.HasTable(&tables.TableVirtualKeyProviderConfig{}) {
|
|
if err := tx.Exec("ALTER TABLE governance_virtual_key_provider_configs ADD COLUMN IF NOT EXISTS budget_id VARCHAR(255)").Error; err != nil {
|
|
// Ignore error for databases that don't support IF NOT EXISTS (e.g., SQLite)
|
|
// The column may already exist from a previous run
|
|
}
|
|
|
|
// Add RateLimitID column if it doesn't exist
|
|
if !migrator.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "rate_limit_id") {
|
|
if err := migrator.AddColumn(&tables.TableVirtualKeyProviderConfig{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to add rate_limit_id column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create foreign key indexes for better performance
|
|
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_provider_config_budget ON governance_virtual_key_provider_configs (budget_id)").Error; err != nil {
|
|
// Ignore - index may already exist or column may not exist yet
|
|
}
|
|
|
|
if !migrator.HasIndex(&tables.TableVirtualKeyProviderConfig{}, "idx_provider_config_rate_limit") {
|
|
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_provider_config_rate_limit ON governance_virtual_key_provider_configs (rate_limit_id)").Error; err != nil {
|
|
return fmt.Errorf("failed to create rate_limit_id index: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create FK constraint for RateLimit (Budget FK is no longer needed - budgets use direct FK on budget table)
|
|
if !migrator.HasConstraint(&tables.TableVirtualKeyProviderConfig{}, "RateLimit") {
|
|
if err := migrator.CreateConstraint(&tables.TableVirtualKeyProviderConfig{}, "RateLimit"); err != nil {
|
|
return fmt.Errorf("failed to create RateLimit FK constraint: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop indexes
|
|
_ = tx.Exec("DROP INDEX IF EXISTS idx_provider_config_budget")
|
|
_ = tx.Exec("DROP INDEX IF EXISTS idx_provider_config_rate_limit")
|
|
|
|
// Drop FK constraints
|
|
if migrator.HasConstraint(&tables.TableVirtualKeyProviderConfig{}, "RateLimit") {
|
|
if err := migrator.DropConstraint(&tables.TableVirtualKeyProviderConfig{}, "RateLimit"); err != nil {
|
|
return fmt.Errorf("failed to drop RateLimit FK constraint: %w", err)
|
|
}
|
|
}
|
|
|
|
// Drop columns via raw SQL (budget_id no longer on struct)
|
|
_ = tx.Exec("ALTER TABLE governance_virtual_key_provider_configs DROP COLUMN IF EXISTS budget_id")
|
|
if migrator.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "rate_limit_id") {
|
|
if err := migrator.DropColumn(&tables.TableVirtualKeyProviderConfig{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to drop rate_limit_id column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running provider config budget/rate limit migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPluginPathColumn adds the path column to the plugin table
|
|
func migrationAddPluginPathColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "update_plugins_table_for_custom_plugins",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "path") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "path"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "is_custom") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "is_custom"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "path"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "is_custom"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running plugin path migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddSessionsTable adds the sessions table
|
|
func migrationAddSessionsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_sessions_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.SessionsTable{}) {
|
|
if err := migrator.CreateTable(&tables.SessionsTable{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropTable(&tables.SessionsTable{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddHeadersJSONColumnIntoMCPClient adds the headers_json column to the mcp_client table
|
|
func migrationAddHeadersJSONColumnIntoMCPClient(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_headers_json_column_into_mcp_client",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "headers_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "headers_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "headers_json"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddDisableContentLoggingColumn adds the disable_content_logging column to the client config table
|
|
func migrationAddDisableContentLoggingColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_disable_content_logging_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "disable_content_logging") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "disable_content_logging"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "disable_content_logging"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPClientIDColumn adds the client_id column to the mcp_clients table and populates unique client IDs
|
|
func migrationAddMCPClientIDColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_client_id_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "client_id") {
|
|
// Add the column as nullable first
|
|
if err := tx.Exec("ALTER TABLE config_mcp_clients ADD COLUMN client_id VARCHAR(255)").Error; err != nil {
|
|
return fmt.Errorf("failed to add client_id column: %w", err)
|
|
}
|
|
|
|
// Populate unique client_ids (UUIDs) for all existing MCP clients
|
|
var mcpClients []tables.TableMCPClient
|
|
if err := tx.Find(&mcpClients).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch MCP clients: %w", err)
|
|
}
|
|
|
|
for _, client := range mcpClients {
|
|
// Generate a UUID for the client_id
|
|
clientID := uuid.New().String()
|
|
|
|
// Update the client with the generated client_id
|
|
if err := tx.Model(&client).Update("client_id", clientID).Error; err != nil {
|
|
return fmt.Errorf("failed to update MCP client %d with client_id %s: %w", client.ID, clientID, err)
|
|
}
|
|
}
|
|
|
|
// Create unique index on client_id
|
|
if err := tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_client_id ON config_mcp_clients (client_id)").Error; err != nil {
|
|
return fmt.Errorf("failed to create unique index on client_id: %w", err)
|
|
}
|
|
// Enforce NOT NULL in Postgres to guarantee ID presence on new rows
|
|
if tx.Dialector.Name() == "postgres" {
|
|
if err := tx.Exec("ALTER TABLE config_mcp_clients ALTER COLUMN client_id SET NOT NULL").Error; err != nil {
|
|
return fmt.Errorf("failed to set client_id NOT NULL: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop the unique index first to avoid orphaned index artifacts
|
|
if err := tx.Exec("DROP INDEX IF EXISTS idx_mcp_client_id").Error; err != nil {
|
|
return fmt.Errorf("failed to drop client_id index: %w", err)
|
|
}
|
|
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "client_id"); err != nil {
|
|
return fmt.Errorf("failed to drop client_id column: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running MCP client_id migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVertexProjectNumberColumn adds the vertex_project_number column to the key table
|
|
func migrationAddVertexProjectNumberColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_vertex_project_number_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "vertex_project_number") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "vertex_project_number"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "vertex_project_number"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running vertex project number migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVertexDeploymentsJSONColumn adds the vertex_deployments_json column to the key table.
|
|
// This column is later dropped by migrationDropDeploymentColumnsAndAddAliases after data is migrated.
|
|
func migrationAddVertexDeploymentsJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_vertex_deployments_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if !tx.Migrator().HasColumn(&tables.TableKey{}, "vertex_deployments_json") {
|
|
if err := tx.Exec("ALTER TABLE config_keys ADD COLUMN vertex_deployments_json TEXT").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if tx.Migrator().HasColumn(&tables.TableKey{}, "vertex_deployments_json") {
|
|
if err := tx.Exec("ALTER TABLE config_keys DROP COLUMN vertex_deployments_json").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running vertex deployments JSON migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationMissingProviderColumnInKeyTable(ctx context.Context, db *gorm.DB) error {
|
|
options := &migrator.Options{
|
|
TableName: migrator.DefaultOptions.TableName,
|
|
IDColumnName: migrator.DefaultOptions.IDColumnName,
|
|
IDColumnSize: migrator.DefaultOptions.IDColumnSize,
|
|
UseTransaction: true,
|
|
ValidateUnknownMigrations: migrator.DefaultOptions.ValidateUnknownMigrations,
|
|
}
|
|
m := migrator.New(db, options, []*migrator.Migration{{
|
|
ID: "add_and_fill_provider_column_in_key_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Step 1: Add the provider column if it doesn't exist
|
|
if migrator.HasColumn(&tables.TableKey{}, "provider") {
|
|
return nil
|
|
}
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "provider"); err != nil {
|
|
return fmt.Errorf("failed to add provider column: %w", err)
|
|
}
|
|
|
|
// Step 2: Find all keys where provider is empty/null but provider_id is set
|
|
var keys []tables.TableKey
|
|
if err := tx.Where("provider IS NULL OR provider = ''").Find(&keys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch keys with missing provider: %w", err)
|
|
}
|
|
|
|
// Step 3: Update each key with the provider name from the provider table
|
|
for _, key := range keys {
|
|
var provider tables.TableProvider
|
|
if err := tx.First(&provider, key.ProviderID).Error; err != nil {
|
|
// Skip keys with invalid provider_id
|
|
if err == gorm.ErrRecordNotFound {
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to fetch provider %d for key %s: %w", key.ProviderID, key.KeyID, err)
|
|
}
|
|
|
|
// Update the key with the provider name
|
|
if err := tx.Model(&key).Update("provider", provider.Name).Error; err != nil {
|
|
return fmt.Errorf("failed to update key %s with provider %s: %w", key.KeyID, provider.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "provider"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add and fill provider column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddToolsToAutoExecuteJSONColumn adds the tools_to_auto_execute_json column to the mcp_client table
|
|
func migrationAddToolsToAutoExecuteJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_tools_to_auto_execute_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "tools_to_auto_execute_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "tools_to_auto_execute_json"); err != nil {
|
|
return err
|
|
}
|
|
// Initialize existing rows with empty array
|
|
if err := tx.Exec("UPDATE config_mcp_clients SET tools_to_auto_execute_json = '[]' WHERE tools_to_auto_execute_json IS NULL OR tools_to_auto_execute_json = ''").Error; err != nil {
|
|
return fmt.Errorf("failed to initialize tools_to_auto_execute_json: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "tools_to_auto_execute_json"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddIsCodeModeClientColumn adds the is_code_mode_client column to the config_mcp_clients table
|
|
func migrationAddIsCodeModeClientColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_is_code_mode_client_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "is_code_mode_client") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "is_code_mode_client"); err != nil {
|
|
return err
|
|
}
|
|
// Initialize existing rows with false (default value)
|
|
if err := tx.Exec("UPDATE config_mcp_clients SET is_code_mode_client = false WHERE is_code_mode_client IS NULL").Error; err != nil {
|
|
return fmt.Errorf("failed to initialize is_code_mode_client: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "is_code_mode_client"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddLogRetentionDaysColumn adds the log_retention_days column to the client config table
|
|
func migrationAddLogRetentionDaysColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_log_retention_days_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "log_retention_days") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "log_retention_days"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "log_retention_days"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddEnabledColumnToKeyTable adds the enabled column to the config_keys table
|
|
func migrationAddEnabledColumnToKeyTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_enabled_column_to_key_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
// Check if column already exists
|
|
if !mg.HasColumn(&tables.TableKey{}, "enabled") {
|
|
// Add the column
|
|
if err := mg.AddColumn(&tables.TableKey{}, "enabled"); err != nil {
|
|
return fmt.Errorf("failed to add enabled column: %w", err)
|
|
}
|
|
}
|
|
// Set default = true for existing rows
|
|
if err := tx.Exec("UPDATE config_keys SET enabled = TRUE WHERE enabled IS NULL").Error; err != nil {
|
|
return fmt.Errorf("failed to backfill enabled column: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
if mg.HasColumn(&tables.TableKey{}, "enabled") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "enabled"); err != nil {
|
|
return fmt.Errorf("failed to drop enabled column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running enabled column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddBatchAndCachePricingColumns adds the cache_read_input_token_cost, cache_creation_input_token_cost, input_cost_per_token_batches, and output_cost_per_token_batches columns to the model_pricing table
|
|
func migrationAddBatchAndCachePricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "update_model_pricing_table_to_add_cache_and_batch_pricing",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "cache_read_input_token_cost") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "cache_read_input_token_cost"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "cache_creation_input_token_cost") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "cache_creation_input_token_cost"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "input_cost_per_token_batches") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "input_cost_per_token_batches"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "output_cost_per_token_batches") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "output_cost_per_token_batches"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "cache_read_input_token_cost"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "cache_creation_input_token_cost"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "input_cost_per_token_batches"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "output_cost_per_token_batches"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
return m.Migrate()
|
|
}
|
|
|
|
func migrationAddMCPAgentDepthAndMCPToolExecutionTimeoutColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_agent_depth_and_mcp_tool_execution_timeout_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "mcp_agent_depth") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "mcp_agent_depth"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "mcp_tool_execution_timeout") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "mcp_tool_execution_timeout"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "mcp_agent_depth"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "mcp_tool_execution_timeout"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPCodeModeBindingLevelColumn adds the mcp_code_mode_binding_level column to the client config table.
|
|
// This column stores the code mode binding level preference (server or tool).
|
|
func migrationAddMCPCodeModeBindingLevelColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_code_mode_binding_level_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
if !migratorInstance.HasColumn(&tables.TableClientConfig{}, "mcp_code_mode_binding_level") {
|
|
if err := migratorInstance.AddColumn(&tables.TableClientConfig{}, "mcp_code_mode_binding_level"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
if err := migratorInstance.DropColumn(&tables.TableClientConfig{}, "mcp_code_mode_binding_level"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// normalizeMCPClientName normalizes an MCP client name by:
|
|
// 1. Replacing hyphens and spaces with underscores
|
|
// 2. Removing leading digits
|
|
// 3. Using a default name if the result is empty
|
|
func normalizeMCPClientName(name string) string {
|
|
// Replace hyphens and spaces with underscores
|
|
normalized := strings.ReplaceAll(name, "-", "_")
|
|
normalized = strings.ReplaceAll(normalized, " ", "_")
|
|
|
|
// Remove leading digits
|
|
normalized = strings.TrimLeftFunc(normalized, func(r rune) bool {
|
|
return unicode.IsDigit(r)
|
|
})
|
|
|
|
// If name becomes empty after normalization, use a default name
|
|
if normalized == "" {
|
|
normalized = "mcp_client"
|
|
}
|
|
|
|
return normalized
|
|
}
|
|
|
|
// migrationNormalizeMCPClientNames normalizes MCP client names by:
|
|
// 1. Replacing hyphens and spaces with underscores
|
|
// 2. Removing leading digits
|
|
// 3. Adding number suffix if name already exists
|
|
func migrationNormalizeMCPClientNames(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "normalize_mcp_client_names",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
|
|
// Fetch all MCP clients
|
|
var mcpClients []tables.TableMCPClient
|
|
if err := tx.Find(&mcpClients).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch MCP clients: %w", err)
|
|
}
|
|
|
|
// Track assigned names in memory to avoid transaction visibility issues
|
|
// and ensure we see all updates made during this migration
|
|
assignedNames := make(map[string]bool)
|
|
|
|
// Helper function to find a unique name
|
|
findUniqueName := func(baseName string, originalName string, excludeID uint, tx *gorm.DB, assignedNames map[string]bool) (string, error) {
|
|
// First check if base name is already assigned in this migration
|
|
if !assignedNames[baseName] {
|
|
// Also check database for existing names (excluding current client)
|
|
var existing tables.TableMCPClient
|
|
err := tx.Where("name = ? AND id != ?", baseName, excludeID).First(&existing).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Name is available
|
|
assignedNames[baseName] = true
|
|
// Log normalization even when no collision
|
|
if originalName != baseName {
|
|
log.Printf("MCP Client Name Normalized: '%s' -> '%s'", originalName, baseName)
|
|
}
|
|
return baseName, nil
|
|
} else if err != nil {
|
|
return "", fmt.Errorf("failed to check name availability: %w", err)
|
|
}
|
|
}
|
|
|
|
// Name exists (either assigned in this migration or in database), try with number suffix starting from 2
|
|
// (base name is conceptually "1", so collisions start from "2")
|
|
suffix := 2
|
|
const maxSuffix = 1000 // Safety limit to prevent infinite loops
|
|
for {
|
|
if suffix > maxSuffix {
|
|
return "", fmt.Errorf("could not find unique name after %d attempts for base name: %s", maxSuffix, baseName)
|
|
}
|
|
candidateName := baseName + strconv.Itoa(suffix)
|
|
|
|
// Check both in-memory map and database
|
|
if !assignedNames[candidateName] {
|
|
var existing tables.TableMCPClient
|
|
err := tx.Where("name = ? AND id != ?", candidateName, excludeID).First(&existing).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Found available name - log the transformation
|
|
assignedNames[candidateName] = true
|
|
log.Printf("MCP Client Name Normalized: '%s' -> '%s'", originalName, candidateName)
|
|
return candidateName, nil
|
|
} else if err != nil {
|
|
return "", fmt.Errorf("failed to check name availability: %w", err)
|
|
}
|
|
}
|
|
suffix++
|
|
}
|
|
}
|
|
|
|
// Process each client
|
|
for _, client := range mcpClients {
|
|
originalName := client.Name
|
|
needsUpdate := false
|
|
|
|
// Check if name needs normalization
|
|
if strings.Contains(originalName, "-") || strings.Contains(originalName, " ") {
|
|
needsUpdate = true
|
|
} else if len(originalName) > 0 && unicode.IsDigit(rune(originalName[0])) {
|
|
needsUpdate = true
|
|
}
|
|
|
|
if needsUpdate {
|
|
// Normalize the name
|
|
normalizedName := normalizeMCPClientName(originalName)
|
|
|
|
// Find a unique name (pass assignedNames map to track names in this migration)
|
|
uniqueName, err := findUniqueName(normalizedName, originalName, client.ID, tx, assignedNames)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find unique name for client %d (original: %s): %w", client.ID, originalName, err)
|
|
}
|
|
|
|
// Update the client name
|
|
if err := tx.Model(&client).Update("name", uniqueName).Error; err != nil {
|
|
return fmt.Errorf("failed to update MCP client %d name from %s to %s: %w", client.ID, originalName, uniqueName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// Rollback is not possible as we don't store the original names
|
|
// This migration is one-way
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running MCP client name normalization migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationMoveKeysToProviderConfig migrates keys from virtual key level to provider config level
|
|
func migrationMoveKeysToProviderConfig(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "move_keys_to_provider_config",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
gormMigrator := tx.Migrator()
|
|
|
|
// Step 1: Create the new join table for provider config -> keys relationship
|
|
// Setup the join table so GORM knows about the custom structure
|
|
if err := tx.SetupJoinTable(&tables.TableVirtualKeyProviderConfig{}, "Keys", &tables.TableVirtualKeyProviderConfigKey{}); err != nil {
|
|
return fmt.Errorf("failed to setup join table for provider config keys: %w", err)
|
|
}
|
|
|
|
// Create the join table if it doesn't exist
|
|
if !gormMigrator.HasTable(&tables.TableVirtualKeyProviderConfigKey{}) {
|
|
if err := gormMigrator.CreateTable(&tables.TableVirtualKeyProviderConfigKey{}); err != nil {
|
|
return fmt.Errorf("failed to create join table for provider config keys: %w", err)
|
|
}
|
|
}
|
|
|
|
// Step 2: Migrate existing key associations from virtual key to provider config level
|
|
// Check if old join table exists
|
|
hasOldTable := gormMigrator.HasTable("governance_virtual_key_keys")
|
|
|
|
if hasOldTable {
|
|
// Get all existing associations from old table using GORM's Table method
|
|
type OldAssociation struct {
|
|
VirtualKeyID string `gorm:"column:table_virtual_key_id"`
|
|
KeyID uint `gorm:"column:table_key_id"`
|
|
}
|
|
var oldAssociations []OldAssociation
|
|
if err := tx.Table("governance_virtual_key_keys").Find(&oldAssociations).Error; err == nil {
|
|
// Process each association
|
|
for _, assoc := range oldAssociations {
|
|
// Get only the key ID and provider - using a minimal struct to avoid
|
|
// querying columns that may not exist yet (added by later migrations)
|
|
type KeyMinimal struct {
|
|
ID uint
|
|
Provider string
|
|
}
|
|
var keyData KeyMinimal
|
|
if err := tx.Table("config_keys").Select("id, provider").Where("id = ?", assoc.KeyID).First(&keyData).Error; err != nil {
|
|
// Key might have been deleted, skip
|
|
continue
|
|
}
|
|
|
|
// Find existing provider config for this virtual key and provider
|
|
var providerConfig tables.TableVirtualKeyProviderConfig
|
|
result := tx.Where("virtual_key_id = ? AND provider = ?", assoc.VirtualKeyID, keyData.Provider).First(&providerConfig)
|
|
|
|
if result.Error != nil {
|
|
if result.Error == gorm.ErrRecordNotFound {
|
|
// Create a new provider config for this provider
|
|
providerConfig = tables.TableVirtualKeyProviderConfig{
|
|
VirtualKeyID: assoc.VirtualKeyID,
|
|
Provider: keyData.Provider,
|
|
Weight: bifrost.Ptr(1.0),
|
|
AllowedModels: []string{},
|
|
}
|
|
if err := tx.Create(&providerConfig).Error; err != nil {
|
|
return fmt.Errorf("failed to create provider config for migration: %w", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to query provider config: %w", result.Error)
|
|
}
|
|
}
|
|
|
|
// Insert directly into the join table using clause.OnConflict for
|
|
// database-agnostic duplicate handling (works for SQLite and PostgreSQL)
|
|
joinEntry := tables.TableVirtualKeyProviderConfigKey{
|
|
TableVirtualKeyProviderConfigID: providerConfig.ID,
|
|
TableKeyID: keyData.ID,
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&joinEntry).Error; err != nil {
|
|
return fmt.Errorf("failed to associate key %d with provider config %d: %w", keyData.ID, providerConfig.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: Drop the old join table
|
|
if err := gormMigrator.DropTable("governance_virtual_key_keys"); err != nil {
|
|
return fmt.Errorf("failed to drop old governance_virtual_key_keys table: %w", err)
|
|
}
|
|
}
|
|
|
|
// Note: Empty keys in provider config means all keys are allowed at runtime
|
|
// We don't pre-populate keys here - this is handled at runtime
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
gormMigrator := tx.Migrator()
|
|
|
|
// Recreate the old join table structure
|
|
type OldJoinTable struct {
|
|
VirtualKeyID string `gorm:"column:table_virtual_key_id;primaryKey"`
|
|
KeyID uint `gorm:"column:table_key_id;primaryKey"`
|
|
}
|
|
if err := gormMigrator.CreateTable(&OldJoinTable{}); err != nil {
|
|
// Table might already exist, ignore error
|
|
_ = err
|
|
}
|
|
// Rename to correct table name if needed
|
|
if gormMigrator.HasTable(&OldJoinTable{}) && !gormMigrator.HasTable("governance_virtual_key_keys") {
|
|
if err := gormMigrator.RenameTable(&OldJoinTable{}, "governance_virtual_key_keys"); err != nil {
|
|
return fmt.Errorf("failed to rename old join table: %w", err)
|
|
}
|
|
}
|
|
|
|
// Note: We cannot fully rollback the data migration as it would require
|
|
// reconstructing which keys belonged to which virtual keys
|
|
|
|
// Drop the new join table
|
|
if err := gormMigrator.DropTable("governance_virtual_key_provider_config_keys"); err != nil {
|
|
return fmt.Errorf("failed to drop governance_virtual_key_provider_config_keys table: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running move keys to provider config migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPluginVersionColumn adds the version column to the plugin table
|
|
func migrationAddPluginVersionColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_plugin_version_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "version") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "version"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "version"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add plugin version column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationAddSendBackRawRequestColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_send_back_raw_request_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "send_back_raw_request") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "send_back_raw_request"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "send_back_raw_request"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add send back raw request columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddConfigHashColumn adds the config_hash column to the provider and key tables
|
|
func migrationAddConfigHashColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_config_hash_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Add config_hash to providers table
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing providers
|
|
var providers []tables.TableProvider
|
|
if err := tx.Find(&providers).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch providers for hash migration: %w", err)
|
|
}
|
|
for _, provider := range providers {
|
|
if provider.ConfigHash == "" {
|
|
// Convert to ProviderConfig and generate hash
|
|
providerConfig := ProviderConfig{
|
|
NetworkConfig: provider.NetworkConfig,
|
|
ConcurrencyAndBufferSize: provider.ConcurrencyAndBufferSize,
|
|
ProxyConfig: provider.ProxyConfig,
|
|
SendBackRawRequest: provider.SendBackRawRequest,
|
|
SendBackRawResponse: provider.SendBackRawResponse,
|
|
CustomProviderConfig: provider.CustomProviderConfig,
|
|
}
|
|
hash, err := providerConfig.GenerateConfigHash(provider.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for provider %s: %w", provider.Name, err)
|
|
}
|
|
if err := tx.Model(&provider).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for provider %s: %w", provider.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Add config_hash to keys table
|
|
if !migrator.HasColumn(&tables.TableKey{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing keys
|
|
var keys []tables.TableKey
|
|
if err := tx.Find(&keys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch keys for hash migration: %w", err)
|
|
}
|
|
for _, key := range keys {
|
|
if key.ConfigHash == "" {
|
|
// Convert to schemas.Key and generate hash
|
|
schemaKey := schemas.Key{
|
|
Name: key.Name,
|
|
Value: key.Value,
|
|
Models: key.Models,
|
|
Weight: getWeight(key.Weight),
|
|
AzureKeyConfig: key.AzureKeyConfig,
|
|
VertexKeyConfig: key.VertexKeyConfig,
|
|
BedrockKeyConfig: key.BedrockKeyConfig,
|
|
Aliases: key.Aliases,
|
|
}
|
|
hash, err := GenerateKeyHash(schemaKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for key %s: %w", key.Name, err)
|
|
}
|
|
if err := tx.Model(&key).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for key %s: %w", key.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add config hash column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVirtualKeyConfigHashColumn adds the config_hash column to the virtual keys table
|
|
func migrationAddVirtualKeyConfigHashColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_virtual_key_config_hash_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Add config_hash to virtual keys table
|
|
if !migrator.HasColumn(&tables.TableVirtualKey{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableVirtualKey{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing virtual keys
|
|
var virtualKeys []tables.TableVirtualKey
|
|
if err := tx.Preload("ProviderConfigs").Preload("ProviderConfigs.Keys").Preload("MCPConfigs").Find(&virtualKeys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch virtual keys for hash migration: %w", err)
|
|
}
|
|
for _, vk := range virtualKeys {
|
|
if vk.ConfigHash == "" {
|
|
hash, err := GenerateVirtualKeyHash(vk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for virtual key %s: %w", vk.ID, err)
|
|
}
|
|
if err := tx.Model(&vk).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for virtual key %s: %w", vk.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableVirtualKey{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add virtual key config hash column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAdditionalConfigHashColumns adds config_hash columns to client config, budget, rate limit,
|
|
// customer, team, MCP client, and plugin tables for reconciliation support
|
|
func migrationAddAdditionalConfigHashColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_additional_config_hash_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Add config_hash to client config table
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing client configs
|
|
var clientConfigs []tables.TableClientConfig
|
|
if err := tx.Find(&clientConfigs).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch client configs for hash migration: %w", err)
|
|
}
|
|
for _, cc := range clientConfigs {
|
|
if cc.ConfigHash == "" {
|
|
clientConfig := ClientConfig{
|
|
DropExcessRequests: cc.DropExcessRequests,
|
|
InitialPoolSize: cc.InitialPoolSize,
|
|
PrometheusLabels: cc.PrometheusLabels,
|
|
EnableLogging: cc.EnableLogging,
|
|
DisableContentLogging: cc.DisableContentLogging,
|
|
LogRetentionDays: cc.LogRetentionDays,
|
|
EnforceGovernanceHeader: cc.EnforceGovernanceHeader,
|
|
AllowDirectKeys: cc.AllowDirectKeys,
|
|
AllowedOrigins: cc.AllowedOrigins,
|
|
MaxRequestBodySizeMB: cc.MaxRequestBodySizeMB,
|
|
}
|
|
hash, err := clientConfig.GenerateClientConfigHash()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for client config %d: %w", cc.ID, err)
|
|
}
|
|
if err := tx.Model(&cc).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for client config %d: %w", cc.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to budgets table
|
|
if !migrator.HasColumn(&tables.TableBudget{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableBudget{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing budgets
|
|
var budgets []tables.TableBudget
|
|
if err := tx.Find(&budgets).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch budgets for hash migration: %w", err)
|
|
}
|
|
for _, budget := range budgets {
|
|
if budget.ConfigHash == "" {
|
|
hash, err := GenerateBudgetHash(budget)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for budget %s: %w", budget.ID, err)
|
|
}
|
|
if err := tx.Model(&budget).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for budget %s: %w", budget.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to rate limits table
|
|
if !migrator.HasColumn(&tables.TableRateLimit{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableRateLimit{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing rate limits
|
|
var rateLimits []tables.TableRateLimit
|
|
if err := tx.Find(&rateLimits).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch rate limits for hash migration: %w", err)
|
|
}
|
|
for _, rl := range rateLimits {
|
|
if rl.ConfigHash == "" {
|
|
hash, err := GenerateRateLimitHash(rl)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for rate limit %s: %w", rl.ID, err)
|
|
}
|
|
if err := tx.Model(&rl).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for rate limit %s: %w", rl.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to customers table
|
|
if !migrator.HasColumn(&tables.TableCustomer{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableCustomer{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing customers
|
|
var customers []tables.TableCustomer
|
|
if err := tx.Find(&customers).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch customers for hash migration: %w", err)
|
|
}
|
|
for _, customer := range customers {
|
|
if customer.ConfigHash == "" {
|
|
hash, err := GenerateCustomerHash(customer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for customer %s: %w", customer.ID, err)
|
|
}
|
|
if err := tx.Model(&customer).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for customer %s: %w", customer.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to teams table
|
|
if !migrator.HasColumn(&tables.TableTeam{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableTeam{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing teams
|
|
var teams []tables.TableTeam
|
|
if err := tx.Find(&teams).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch teams for hash migration: %w", err)
|
|
}
|
|
for _, team := range teams {
|
|
if team.ConfigHash == "" {
|
|
hash, err := GenerateTeamHash(team)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for team %s: %w", team.ID, err)
|
|
}
|
|
if err := tx.Model(&team).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for team %s: %w", team.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to MCP clients table
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing MCP clients
|
|
var mcpClients []tables.TableMCPClient
|
|
if err := tx.Find(&mcpClients).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch MCP clients for hash migration: %w", err)
|
|
}
|
|
for _, mcp := range mcpClients {
|
|
if mcp.ConfigHash == "" {
|
|
hash, err := GenerateMCPClientHash(mcp)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for MCP client %s: %w", mcp.Name, err)
|
|
}
|
|
if err := tx.Model(&mcp).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for MCP client %s: %w", mcp.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add config_hash to plugins table
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "config_hash") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
// Pre-populate hashes for existing plugins
|
|
var plugins []tables.TablePlugin
|
|
if err := tx.Find(&plugins).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch plugins for hash migration: %w", err)
|
|
}
|
|
for _, plugin := range plugins {
|
|
if plugin.ConfigHash == "" {
|
|
hash, err := GeneratePluginHash(plugin)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for plugin %s: %w", plugin.Name, err)
|
|
}
|
|
if err := tx.Model(&plugin).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for plugin %s: %w", plugin.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableBudget{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableRateLimit{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableCustomer{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableTeam{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "config_hash"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add additional config hash columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAdd200kTokenPricingColumns adds pricing columns for 200k token tier models
|
|
func migrationAdd200kTokenPricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_200k_token_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_above_200k_tokens",
|
|
"output_cost_per_token_above_200k_tokens",
|
|
"cache_creation_input_token_cost_above_200k_tokens",
|
|
"cache_read_input_token_cost_above_200k_tokens",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_above_200k_tokens",
|
|
"output_cost_per_token_above_200k_tokens",
|
|
"cache_creation_input_token_cost_above_200k_tokens",
|
|
"cache_read_input_token_cost_above_200k_tokens",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if migrator.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
return m.Migrate()
|
|
}
|
|
|
|
// migrationAddImagePricingColumns adds the image generation pricing columns to the model_pricing table
|
|
func migrationAddImagePricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_image_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_image_token",
|
|
"output_cost_per_image_token",
|
|
"input_cost_per_image",
|
|
"output_cost_per_image",
|
|
"cache_read_input_image_token_cost",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_image_token",
|
|
"output_cost_per_image_token",
|
|
"input_cost_per_image",
|
|
"output_cost_per_image",
|
|
"cache_read_input_image_token_cost",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if migrator.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
return m.Migrate()
|
|
}
|
|
|
|
// migrationAddUseForBatchAPIColumnAndS3BucketsConfig adds the use_for_batch_api and bedrock_batch_s3_config_json columns to the config_keys table
|
|
// Existing keys are backfilled with use_for_batch_api = TRUE to preserve current behavior
|
|
func migrationAddUseForBatchAPIColumnAndS3BucketsConfig(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_use_for_batch_api_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
// Add use_for_batch_api column
|
|
if !mg.HasColumn(&tables.TableKey{}, "use_for_batch_api") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "use_for_batch_api"); err != nil {
|
|
return fmt.Errorf("failed to add use_for_batch_api column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add bedrock_batch_s3_config_json column
|
|
if !mg.HasColumn(&tables.TableKey{}, "bedrock_batch_s3_config_json") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "bedrock_batch_s3_config_json"); err != nil {
|
|
return fmt.Errorf("failed to add bedrock_batch_s3_config_json column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
if mg.HasColumn(&tables.TableKey{}, "use_for_batch_api") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "use_for_batch_api"); err != nil {
|
|
return fmt.Errorf("failed to drop use_for_batch_api column: %w", err)
|
|
}
|
|
}
|
|
|
|
if mg.HasColumn(&tables.TableKey{}, "bedrock_batch_s3_config_json") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "bedrock_batch_s3_config_json"); err != nil {
|
|
return fmt.Errorf("failed to drop bedrock_batch_s3_config_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running use_for_batch_api migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddHeaderFilterConfigJSONColumn adds the header_filter_config_json column to the config_client table
|
|
func migrationAddHeaderFilterConfigJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_header_filter_config_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
if !mg.HasColumn(&tables.TableClientConfig{}, "header_filter_config_json") {
|
|
if err := mg.AddColumn(&tables.TableClientConfig{}, "header_filter_config_json"); err != nil {
|
|
return fmt.Errorf("failed to add header_filter_config_json column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
if mg.HasColumn(&tables.TableClientConfig{}, "header_filter_config_json") {
|
|
if err := mg.DropColumn(&tables.TableClientConfig{}, "header_filter_config_json"); err != nil {
|
|
return fmt.Errorf("failed to drop header_filter_config_json column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running header_filter_config_json migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAzureClientIDAndClientSecretAndTenantIDColumns adds the azure_client_id, azure_client_secret, and azure_tenant_id columns to the key table
|
|
func migrationAddAzureClientIDAndClientSecretAndTenantIDColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_azure_client_id_and_client_secret_and_tenant_id_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "azure_client_id") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "azure_client_id"); err != nil {
|
|
return fmt.Errorf("failed to add azure_client_id column: %w", err)
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableKey{}, "azure_client_secret") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "azure_client_secret"); err != nil {
|
|
return fmt.Errorf("failed to add azure_client_secret column: %w", err)
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableKey{}, "azure_tenant_id") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "azure_tenant_id"); err != nil {
|
|
return fmt.Errorf("failed to add azure_tenant_id column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "azure_client_id"); err != nil {
|
|
return fmt.Errorf("failed to drop azure_client_id column: %w", err)
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "azure_client_secret"); err != nil {
|
|
return fmt.Errorf("failed to drop azure_client_secret column: %w", err)
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "azure_tenant_id"); err != nil {
|
|
return fmt.Errorf("failed to drop azure_tenant_id column: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running azure_client_id_and_client_secret_and_tenant_id migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationAddToolPricingJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_tool_pricing_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "tool_pricing_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "tool_pricing_json"); err != nil {
|
|
return fmt.Errorf("failed to add tool_pricing_json column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "tool_pricing_json"); err != nil {
|
|
return fmt.Errorf("failed to drop tool_pricing_json column: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
return m.Migrate()
|
|
}
|
|
|
|
// migrationRemoveServerPrefixFromMCPTools removes the server name prefix from tool names
|
|
// in tools_to_execute_json, tools_to_auto_execute_json, and tool_pricing_json columns
|
|
// in both config_mcp_clients and governance_virtual_key_mcp_configs tables.
|
|
//
|
|
// This migration converts:
|
|
// - tools_to_execute_json: ["calculator_add", "calculator_subtract"] → ["add", "subtract"]
|
|
// - tools_to_auto_execute_json: ["calculator_multiply"] → ["multiply"]
|
|
// - tool_pricing_json: {"calculator_add": 0.001, "calculator_subtract": 0.001} → {"add": 0.001, "subtract": 0.001}
|
|
func migrationRemoveServerPrefixFromMCPTools(ctx context.Context, db *gorm.DB) error {
|
|
// Helper function to check if a tool name has a prefix matching the client name
|
|
// Handles both exact matches and legacy normalized forms
|
|
hasClientPrefix := func(toolName, clientName string) (bool, string) {
|
|
prefix := clientName + "_"
|
|
if strings.HasPrefix(toolName, prefix) {
|
|
return true, strings.TrimPrefix(toolName, prefix)
|
|
}
|
|
// Legacy prefix: normalize the substring before first underscore
|
|
if idx := strings.IndexByte(toolName, '_'); idx > 0 {
|
|
toolPrefix := toolName[:idx]
|
|
unprefixed := toolName[idx+1:]
|
|
if normalizeMCPClientName(toolPrefix) == clientName {
|
|
return true, unprefixed
|
|
}
|
|
}
|
|
return false, ""
|
|
}
|
|
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "remove_server_prefix_from_mcp_tools",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
|
|
// ============================================================
|
|
// Step 1: Migrate config_mcp_clients table
|
|
// ============================================================
|
|
|
|
// Fetch all MCP clients
|
|
var mcpClients []tables.TableMCPClient
|
|
if err := tx.Find(&mcpClients).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch MCP clients: %w", err)
|
|
}
|
|
|
|
// Process each MCP client
|
|
for i := range mcpClients {
|
|
client := &mcpClients[i]
|
|
clientName := client.Name
|
|
needsUpdate := false
|
|
|
|
// Process tools_to_execute_json
|
|
var toolsToExecute []string
|
|
if client.ToolsToExecuteJSON != "" && client.ToolsToExecuteJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolsToExecuteJSON), &toolsToExecute); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tools_to_execute_json for client %s: %w", clientName, err)
|
|
}
|
|
|
|
// Strip prefix from each tool
|
|
updatedTools := make([]string, 0, len(toolsToExecute))
|
|
seenTools := make(map[string]bool)
|
|
for _, tool := range toolsToExecute {
|
|
// Check if tool has client prefix (handles both current and legacy normalized forms)
|
|
if hasPrefix, unprefixedTool := hasClientPrefix(tool, clientName); hasPrefix {
|
|
// Check for collision: if unprefixed tool already exists in the list
|
|
if seenTools[unprefixedTool] {
|
|
log.Printf("Collision detected when stripping prefix from tool '%s' for client '%s': unprefixed name '%s' already exists. Keeping unprefixed value.", tool, clientName, unprefixedTool)
|
|
needsUpdate = true
|
|
continue
|
|
}
|
|
seenTools[unprefixedTool] = true
|
|
updatedTools = append(updatedTools, unprefixedTool)
|
|
needsUpdate = true
|
|
} else {
|
|
// Tool already unprefixed or is wildcard "*"
|
|
if seenTools[tool] {
|
|
log.Printf("Duplicate tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
|
|
continue
|
|
}
|
|
seenTools[tool] = true
|
|
updatedTools = append(updatedTools, tool)
|
|
}
|
|
}
|
|
|
|
// Update the JSON
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(updatedTools)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal updated tools_to_execute for client %s: %w", clientName, err)
|
|
}
|
|
client.ToolsToExecuteJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
// Process tools_to_auto_execute_json
|
|
var toolsToAutoExecute []string
|
|
if client.ToolsToAutoExecuteJSON != "" && client.ToolsToAutoExecuteJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolsToAutoExecuteJSON), &toolsToAutoExecute); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tools_to_auto_execute_json for client %s: %w", clientName, err)
|
|
}
|
|
|
|
// Strip prefix from each tool
|
|
updatedAutoTools := make([]string, 0, len(toolsToAutoExecute))
|
|
seenAutoTools := make(map[string]bool)
|
|
for _, tool := range toolsToAutoExecute {
|
|
// Check if tool has client prefix (handles both current and legacy normalized forms)
|
|
if hasPrefix, unprefixedTool := hasClientPrefix(tool, clientName); hasPrefix {
|
|
// Check for collision: if unprefixed tool already exists in the list
|
|
if seenAutoTools[unprefixedTool] {
|
|
log.Printf("Collision detected when stripping prefix from auto-execute tool '%s' for client '%s': unprefixed name '%s' already exists. Keeping unprefixed value.", tool, clientName, unprefixedTool)
|
|
needsUpdate = true
|
|
continue
|
|
}
|
|
seenAutoTools[unprefixedTool] = true
|
|
updatedAutoTools = append(updatedAutoTools, unprefixedTool)
|
|
needsUpdate = true
|
|
} else {
|
|
// Tool already unprefixed or is wildcard "*"
|
|
if seenAutoTools[tool] {
|
|
log.Printf("Duplicate auto-execute tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
|
|
continue
|
|
}
|
|
seenAutoTools[tool] = true
|
|
updatedAutoTools = append(updatedAutoTools, tool)
|
|
}
|
|
}
|
|
|
|
// Update the JSON
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(updatedAutoTools)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal updated tools_to_auto_execute for client %s: %w", clientName, err)
|
|
}
|
|
client.ToolsToAutoExecuteJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
// Process tool_pricing_json
|
|
var toolPricing map[string]float64
|
|
if client.ToolPricingJSON != "" && client.ToolPricingJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolPricingJSON), &toolPricing); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tool_pricing_json for client %s: %w", clientName, err)
|
|
}
|
|
|
|
// Strip prefix from each tool name key
|
|
updatedPricing := make(map[string]float64)
|
|
for toolName, price := range toolPricing {
|
|
// Check if tool has client prefix (handles both current and legacy normalized forms)
|
|
if hasPrefix, unprefixedTool := hasClientPrefix(toolName, clientName); hasPrefix {
|
|
// Check for collision: if unprefixed key already exists
|
|
if existingPrice, exists := updatedPricing[unprefixedTool]; exists {
|
|
log.Printf("Collision detected when stripping prefix from pricing key '%s' for client '%s': unprefixed key '%s' already exists with price %.6f. Keeping existing unprefixed value (%.6f), discarding prefixed value (%.6f).", toolName, clientName, unprefixedTool, existingPrice, existingPrice, price)
|
|
needsUpdate = true
|
|
continue
|
|
}
|
|
updatedPricing[unprefixedTool] = price
|
|
needsUpdate = true
|
|
} else {
|
|
// Check for collision: if unprefixed key already exists (from a previously processed prefixed entry)
|
|
if existingPrice, exists := updatedPricing[toolName]; exists {
|
|
log.Printf("Collision detected for pricing key '%s' for client '%s': key already exists with price %.6f. Keeping first value (%.6f), discarding duplicate (%.6f).", toolName, clientName, existingPrice, existingPrice, price)
|
|
continue
|
|
}
|
|
updatedPricing[toolName] = price
|
|
}
|
|
}
|
|
|
|
// Update the JSON
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(updatedPricing)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal updated tool_pricing for client %s: %w", clientName, err)
|
|
}
|
|
client.ToolPricingJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
// Save the updated client if any changes were made
|
|
if needsUpdate {
|
|
// Use Model + Updates to ensure changes are persisted
|
|
result := tx.Model(&tables.TableMCPClient{}).Where("id = ?", client.ID).Updates(map[string]interface{}{
|
|
"tools_to_execute_json": client.ToolsToExecuteJSON,
|
|
"tools_to_auto_execute_json": client.ToolsToAutoExecuteJSON,
|
|
"tool_pricing_json": client.ToolPricingJSON,
|
|
})
|
|
|
|
if result.Error != nil {
|
|
return fmt.Errorf("failed to save updated MCP client %s: %w", clientName, result.Error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Step 2: Migrate governance_virtual_key_mcp_configs table
|
|
// ============================================================
|
|
|
|
// Fetch all virtual key MCP configs with their associated MCP client
|
|
var vkMCPConfigs []tables.TableVirtualKeyMCPConfig
|
|
if err := tx.Preload("MCPClient").Find(&vkMCPConfigs).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch virtual key MCP configs: %w", err)
|
|
}
|
|
|
|
// Process each VK MCP config
|
|
for i := range vkMCPConfigs {
|
|
vkConfig := &vkMCPConfigs[i]
|
|
if vkConfig.MCPClient.Name == "" {
|
|
// Skip if MCP client is not loaded
|
|
continue
|
|
}
|
|
|
|
clientName := vkConfig.MCPClient.Name
|
|
needsUpdate := false
|
|
|
|
// Process tools_to_execute (this is a JSON array stored in GORM's serializer format)
|
|
if len(vkConfig.ToolsToExecute) > 0 {
|
|
updatedTools := make([]string, 0, len(vkConfig.ToolsToExecute))
|
|
seen := make(map[string]bool, len(vkConfig.ToolsToExecute))
|
|
|
|
for _, tool := range vkConfig.ToolsToExecute {
|
|
var finalTool string
|
|
// Check if tool has client prefix (handles both current and legacy normalized forms)
|
|
if hasPrefix, unprefixedTool := hasClientPrefix(tool, clientName); hasPrefix {
|
|
finalTool = unprefixedTool
|
|
} else {
|
|
finalTool = tool
|
|
}
|
|
|
|
// Skip if we've already added this tool (collision detection)
|
|
if !seen[finalTool] {
|
|
seen[finalTool] = true
|
|
updatedTools = append(updatedTools, finalTool)
|
|
}
|
|
}
|
|
|
|
// Only update if the final list differs from the original
|
|
needsUpdate = len(updatedTools) != len(vkConfig.ToolsToExecute)
|
|
if !needsUpdate {
|
|
// Check if any tools actually changed
|
|
for j, tool := range vkConfig.ToolsToExecute {
|
|
if tool != updatedTools[j] {
|
|
needsUpdate = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
vkConfig.ToolsToExecute = updatedTools
|
|
}
|
|
}
|
|
|
|
// Save the updated VK config if any changes were made
|
|
if needsUpdate {
|
|
if err := tx.Save(vkConfig).Error; err != nil {
|
|
return fmt.Errorf("failed to save updated VK MCP config ID %d: %w", vkConfig.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// Rollback is complex because we need to re-add the prefix
|
|
// This requires knowing the client name for each tool
|
|
tx = tx.WithContext(ctx)
|
|
|
|
// ============================================================
|
|
// Step 1: Rollback config_mcp_clients table
|
|
// ============================================================
|
|
|
|
var mcpClients []tables.TableMCPClient
|
|
if err := tx.Find(&mcpClients).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch MCP clients for rollback: %w", err)
|
|
}
|
|
|
|
for _, client := range mcpClients {
|
|
clientName := client.Name
|
|
needsUpdate := false
|
|
|
|
// Rollback tools_to_execute_json
|
|
var toolsToExecute []string
|
|
if client.ToolsToExecuteJSON != "" && client.ToolsToExecuteJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolsToExecuteJSON), &toolsToExecute); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tools_to_execute_json for rollback: %w", err)
|
|
}
|
|
|
|
prefixedTools := make([]string, 0, len(toolsToExecute))
|
|
for _, tool := range toolsToExecute {
|
|
// Skip wildcard
|
|
if tool == "*" {
|
|
prefixedTools = append(prefixedTools, tool)
|
|
continue
|
|
}
|
|
// Add prefix if not already present
|
|
prefix := clientName + "_"
|
|
if !strings.HasPrefix(tool, prefix) {
|
|
prefixedTools = append(prefixedTools, prefix+tool)
|
|
needsUpdate = true
|
|
} else {
|
|
prefixedTools = append(prefixedTools, tool)
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(prefixedTools)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal rollback tools_to_execute: %w", err)
|
|
}
|
|
client.ToolsToExecuteJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
// Rollback tools_to_auto_execute_json
|
|
var toolsToAutoExecute []string
|
|
if client.ToolsToAutoExecuteJSON != "" && client.ToolsToAutoExecuteJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolsToAutoExecuteJSON), &toolsToAutoExecute); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tools_to_auto_execute_json for rollback: %w", err)
|
|
}
|
|
|
|
prefixedAutoTools := make([]string, 0, len(toolsToAutoExecute))
|
|
for _, tool := range toolsToAutoExecute {
|
|
if tool == "*" {
|
|
prefixedAutoTools = append(prefixedAutoTools, tool)
|
|
continue
|
|
}
|
|
prefix := clientName + "_"
|
|
if !strings.HasPrefix(tool, prefix) {
|
|
prefixedAutoTools = append(prefixedAutoTools, prefix+tool)
|
|
needsUpdate = true
|
|
} else {
|
|
prefixedAutoTools = append(prefixedAutoTools, tool)
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(prefixedAutoTools)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal rollback tools_to_auto_execute: %w", err)
|
|
}
|
|
client.ToolsToAutoExecuteJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
// Rollback tool_pricing_json
|
|
var toolPricing map[string]float64
|
|
if client.ToolPricingJSON != "" && client.ToolPricingJSON != "null" {
|
|
if err := json.Unmarshal([]byte(client.ToolPricingJSON), &toolPricing); err != nil {
|
|
return fmt.Errorf("failed to unmarshal tool_pricing_json for rollback: %w", err)
|
|
}
|
|
|
|
prefixedPricing := make(map[string]float64)
|
|
for toolName, price := range toolPricing {
|
|
prefix := clientName + "_"
|
|
if !strings.HasPrefix(toolName, prefix) {
|
|
prefixedPricing[prefix+toolName] = price
|
|
needsUpdate = true
|
|
} else {
|
|
prefixedPricing[toolName] = price
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
updatedJSON, err := json.Marshal(prefixedPricing)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal rollback tool_pricing: %w", err)
|
|
}
|
|
client.ToolPricingJSON = string(updatedJSON)
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
if err := tx.Save(&client).Error; err != nil {
|
|
return fmt.Errorf("failed to save rollback MCP client: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Step 2: Rollback governance_virtual_key_mcp_configs table
|
|
// ============================================================
|
|
|
|
var vkMCPConfigs []tables.TableVirtualKeyMCPConfig
|
|
if err := tx.Preload("MCPClient").Find(&vkMCPConfigs).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch virtual key MCP configs for rollback: %w", err)
|
|
}
|
|
|
|
for _, vkConfig := range vkMCPConfigs {
|
|
if vkConfig.MCPClient.Name == "" {
|
|
continue
|
|
}
|
|
|
|
clientName := vkConfig.MCPClient.Name
|
|
needsUpdate := false
|
|
|
|
if len(vkConfig.ToolsToExecute) > 0 {
|
|
prefixedTools := make([]string, 0, len(vkConfig.ToolsToExecute))
|
|
for _, tool := range vkConfig.ToolsToExecute {
|
|
if tool == "*" {
|
|
prefixedTools = append(prefixedTools, tool)
|
|
continue
|
|
}
|
|
prefix := clientName + "_"
|
|
if !strings.HasPrefix(tool, prefix) {
|
|
prefixedTools = append(prefixedTools, prefix+tool)
|
|
needsUpdate = true
|
|
} else {
|
|
prefixedTools = append(prefixedTools, tool)
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
vkConfig.ToolsToExecute = prefixedTools
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
if err := tx.Save(&vkConfig).Error; err != nil {
|
|
return fmt.Errorf("failed to save rollback VK MCP config: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running migration to remove server prefix from MCP tools: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddDistributedLocksTable adds the distributed_locks table for distributed locking
|
|
func migrationAddDistributedLocksTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_distributed_locks_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
// Use raw SQL with IF NOT EXISTS for atomic, race-condition-safe table creation
|
|
createTableSQL := `
|
|
CREATE TABLE IF NOT EXISTS distributed_locks (
|
|
lock_key VARCHAR(255) PRIMARY KEY,
|
|
holder_id VARCHAR(255) NOT NULL,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`
|
|
if err := tx.Exec(createTableSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to create distributed_locks table: %w", err)
|
|
}
|
|
// Create index on expires_at for efficient cleanup queries
|
|
createIndexSQL := `CREATE INDEX IF NOT EXISTS idx_distributed_locks_expires_at ON distributed_locks (expires_at)`
|
|
if err := tx.Exec(createIndexSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to create expires_at index: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if err := tx.Exec("DROP TABLE IF EXISTS distributed_locks").Error; err != nil {
|
|
return fmt.Errorf("failed to drop distributed_locks table: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running distributed_locks table migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddModelConfigTable adds the governance_model_configs table
|
|
func migrationAddModelConfigTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_model_config_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.TableModelConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableModelConfig{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if err := migrator.DropTable(&tables.TableModelConfig{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add model config table migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddProviderGovernanceColumns adds budget_id and rate_limit_id columns to config_providers table
|
|
func migrationAddProviderGovernanceColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_provider_governance_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
provider := &tables.TableProvider{}
|
|
|
|
// Add budget_id column if it doesn't exist
|
|
if !migrator.HasColumn(provider, "budget_id") {
|
|
if err := migrator.AddColumn(provider, "budget_id"); err != nil {
|
|
return fmt.Errorf("failed to add budget_id column: %w", err)
|
|
}
|
|
}
|
|
// Create index for budget_id (outside HasColumn to handle reruns where column exists but index doesn't)
|
|
if !migrator.HasIndex(provider, "idx_provider_budget") {
|
|
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_provider_budget ON config_providers (budget_id)").Error; err != nil {
|
|
return fmt.Errorf("failed to create budget_id index: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add rate_limit_id column if it doesn't exist
|
|
if !migrator.HasColumn(provider, "rate_limit_id") {
|
|
if err := migrator.AddColumn(provider, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to add rate_limit_id column: %w", err)
|
|
}
|
|
}
|
|
// Create index for rate_limit_id (outside HasColumn to handle reruns where column exists but index doesn't)
|
|
if !migrator.HasIndex(provider, "idx_provider_rate_limit") {
|
|
if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_provider_rate_limit ON config_providers (rate_limit_id)").Error; err != nil {
|
|
return fmt.Errorf("failed to create rate_limit_id index: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
provider := &tables.TableProvider{}
|
|
|
|
// Drop indexes first
|
|
if migrator.HasIndex(provider, "idx_provider_rate_limit") {
|
|
if err := tx.Exec("DROP INDEX IF EXISTS idx_provider_rate_limit").Error; err != nil {
|
|
return fmt.Errorf("failed to drop rate_limit_id index: %w", err)
|
|
}
|
|
}
|
|
|
|
if migrator.HasIndex(provider, "idx_provider_budget") {
|
|
if err := tx.Exec("DROP INDEX IF EXISTS idx_provider_budget").Error; err != nil {
|
|
return fmt.Errorf("failed to drop budget_id index: %w", err)
|
|
}
|
|
}
|
|
|
|
// Drop rate_limit_id column if it exists
|
|
if migrator.HasColumn(provider, "rate_limit_id") {
|
|
if err := migrator.DropColumn(provider, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to drop rate_limit_id column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Drop budget_id column if it exists
|
|
if migrator.HasColumn(provider, "budget_id") {
|
|
if err := migrator.DropColumn(provider, "budget_id"); err != nil {
|
|
return fmt.Errorf("failed to drop budget_id column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running add provider governance columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAllowedHeadersJSONColumn adds the allowed_headers_json column to the client config table
|
|
func migrationAddAllowedHeadersJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_allowed_headers_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "allowed_headers_json") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "allowed_headers_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "allowed_headers_json") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "allowed_headers_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddDisableDBPingsInHealthColumn adds the disable_db_pings_in_health column to the client config table
|
|
func migrationAddDisableDBPingsInHealthColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_disable_db_pings_in_health_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "disable_db_pings_in_health") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "disable_db_pings_in_health"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "disable_db_pings_in_health") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "disable_db_pings_in_health"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running db migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddIsPingAvailableColumnToMCPClientTable adds the is_ping_available column to the config_mcp_clients table
|
|
func migrationAddIsPingAvailableColumnToMCPClientTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_is_ping_available_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "is_ping_available") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "is_ping_available"); err != nil {
|
|
return err
|
|
}
|
|
// Set default value for existing rows
|
|
if err := tx.Model(&tables.TableMCPClient{}).Where("is_ping_available IS NULL").Update("is_ping_available", true).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "is_ping_available") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "is_ping_available"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running is_ping_available migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddRoutingRulesTable adds the routing rules table for intelligent request routing
|
|
func migrationAddRoutingRulesTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_routing_rules_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasTable(&tables.TableRoutingRule{}) {
|
|
if err := migrator.CreateTable(&tables.TableRoutingRule{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if err := migrator.DropTable(&tables.TableRoutingRule{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running routing_rules_table migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddOAuthTables creates the oauth_configs and oauth_tokens tables
|
|
func migrationAddOAuthTables(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_oauth_tables",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Create oauth_configs table FIRST (before adding FK columns that reference it)
|
|
if !migrator.HasTable(&tables.TableOauthConfig{}) {
|
|
if err := migrator.CreateTable(&tables.TableOauthConfig{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_configs table: %w", err)
|
|
}
|
|
}
|
|
// Create oauth_tokens table
|
|
if !migrator.HasTable(&tables.TableOauthToken{}) {
|
|
if err := migrator.CreateTable(&tables.TableOauthToken{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_tokens table: %w", err)
|
|
}
|
|
}
|
|
// IF MCPClient table is not present, create it first
|
|
if !migrator.HasTable(&tables.TableMCPClient{}) {
|
|
if err := migrator.CreateTable(&tables.TableMCPClient{}); err != nil {
|
|
return fmt.Errorf("failed to create mcp_clients table: %w", err)
|
|
}
|
|
}
|
|
// Now update MCPClient table to add auth_type, oauth_config_id columns
|
|
// (oauth_config_id has FK constraint to oauth_configs table created above)
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "auth_type") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "auth_type"); err != nil {
|
|
return fmt.Errorf("failed to add auth_type column: %w", err)
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "oauth_config_id") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "oauth_config_id"); err != nil {
|
|
return fmt.Errorf("failed to add oauth_config_id column: %w", err)
|
|
}
|
|
}
|
|
// Set default value for auth_type column
|
|
if err := tx.Model(&tables.TableMCPClient{}).Where("auth_type IS NULL").Update("auth_type", "headers").Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop tables in reverse order
|
|
if migrator.HasTable(&tables.TableOauthToken{}) {
|
|
if err := migrator.DropTable(&tables.TableOauthToken{}); err != nil {
|
|
return fmt.Errorf("failed to drop oauth_tokens table: %w", err)
|
|
}
|
|
}
|
|
|
|
if migrator.HasTable(&tables.TableOauthConfig{}) {
|
|
if err := migrator.DropTable(&tables.TableOauthConfig{}); err != nil {
|
|
return fmt.Errorf("failed to drop oauth_configs table: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running oauth tables migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddToolSyncIntervalColumns adds the tool_sync_interval columns to config_client and config_mcp_clients tables
|
|
func migrationAddToolSyncIntervalColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_tool_sync_interval_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
// Add mcp_tool_sync_interval column to config_client table (global setting)
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "mcp_tool_sync_interval") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "mcp_tool_sync_interval"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Add tool_sync_interval column to config_mcp_clients table (per-client setting)
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "tool_sync_interval") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "tool_sync_interval"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "mcp_tool_sync_interval"); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "tool_sync_interval"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running tool sync interval migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPClientConfigToOAuthConfig adds the mcp_client_config_json column to oauth_configs table
|
|
// This enables multi-instance support by storing pending MCP client config in the database
|
|
// instead of in-memory, so OAuth callbacks can be handled by any server instance
|
|
func migrationAddMCPClientConfigToOAuthConfig(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_client_config_to_oauth_config",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableOauthConfig{}, "mcp_client_config_json") {
|
|
if err := migrator.AddColumn(&tables.TableOauthConfig{}, "mcp_client_config_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableOauthConfig{}, "mcp_client_config_json") {
|
|
if err := migrator.DropColumn(&tables.TableOauthConfig{}, "mcp_client_config_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running mcp client config oauth migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddBaseModelPricingColumn adds the base_model column to the model_pricing table
|
|
func migrationAddBaseModelPricingColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_base_model_pricing_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "base_model") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "base_model"); err != nil {
|
|
return fmt.Errorf("failed to add column base_model: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableModelPricing{}, "base_model") {
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "base_model"); err != nil {
|
|
return fmt.Errorf("failed to drop column base_model: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
return m.Migrate()
|
|
}
|
|
|
|
// migrationAddAzureScopesColumn adds the azure_scopes column to the key table for Entra ID OAuth scopes
|
|
func migrationAddAzureScopesColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_azure_scopes_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "azure_scopes") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "azure_scopes"); err != nil {
|
|
return fmt.Errorf("failed to add azure_scopes column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableKey{}, "azure_scopes") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "azure_scopes"); err != nil {
|
|
return fmt.Errorf("failed to drop azure_scopes column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running azure_scopes migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddReplicateDeploymentsJSONColumn adds the replicate_deployments_json column to the key table.
|
|
// This column is later dropped by migrationDropDeploymentColumnsAndAddAliases after data is migrated.
|
|
func migrationAddReplicateDeploymentsJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_replicate_deployments_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if !tx.Migrator().HasColumn(&tables.TableKey{}, "replicate_deployments_json") {
|
|
if err := tx.Exec("ALTER TABLE config_keys ADD COLUMN replicate_deployments_json TEXT").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if tx.Migrator().HasColumn(&tables.TableKey{}, "replicate_deployments_json") {
|
|
if err := tx.Exec("ALTER TABLE config_keys DROP COLUMN replicate_deployments_json").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running replicate deployments JSON migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationDropDeploymentColumnsAndAddAliases adds the unified aliases_json column, migrates
|
|
// existing per-provider deployment data into it, then drops the legacy columns.
|
|
// Only one deployment column will be populated per row (they were mutually exclusive).
|
|
func migrationDropDeploymentColumnsAndAddAliases(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "drop_deployment_columns_and_add_aliases",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
m := tx.Migrator()
|
|
|
|
// Add aliases_json column first
|
|
if !m.HasColumn(&tables.TableKey{}, "aliases_json") {
|
|
if err := m.AddColumn(&tables.TableKey{}, "aliases_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Copy data from whichever legacy deployment column is populated into aliases_json.
|
|
// Only rows where aliases_json is not already set are touched.
|
|
// Exactly one deployment column will be non-null per row (they were mutually exclusive).
|
|
for _, col := range []string{
|
|
"azure_deployments_json",
|
|
"vertex_deployments_json",
|
|
"bedrock_deployments_json",
|
|
"replicate_deployments_json",
|
|
} {
|
|
if !m.HasColumn(&tables.TableKey{}, col) {
|
|
continue
|
|
}
|
|
if err := tx.Exec(
|
|
"UPDATE config_keys SET aliases_json = " + col +
|
|
" WHERE aliases_json IS NULL AND " + col + " IS NOT NULL AND " + col + " != ''",
|
|
).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Drop legacy deployment columns
|
|
for _, col := range []string{
|
|
"azure_deployments_json",
|
|
"vertex_deployments_json",
|
|
"bedrock_deployments_json",
|
|
"replicate_deployments_json",
|
|
} {
|
|
if m.HasColumn(&tables.TableKey{}, col) {
|
|
if err := tx.Exec("ALTER TABLE config_keys DROP COLUMN " + col).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fail fast if there are encrypted rows that need fixup but encryption isn't initialized.
|
|
// The migration drops legacy deployment columns below, so skipping this fixup
|
|
// would silently lose the ability to recover those values.
|
|
// This case will ideally never happen
|
|
var encryptedAliasCount int64
|
|
if err := tx.Table("config_keys").
|
|
Where(
|
|
"encryption_status = ? AND aliases_json IS NOT NULL AND aliases_json != '' AND aliases_json != '{}'",
|
|
tables.EncryptionStatusEncrypted,
|
|
).
|
|
Count(&encryptedAliasCount).Error; err != nil {
|
|
return fmt.Errorf("failed to count encrypted aliases for fixup: %w", err)
|
|
}
|
|
if encryptedAliasCount > 0 && !encrypt.IsEnabled() {
|
|
return fmt.Errorf("encryption must be enabled before migrating encrypted aliases")
|
|
}
|
|
|
|
// Encrypt aliases_json for rows where encryption_status is already 'encrypted'.
|
|
// The raw SQL copy above preserved the original column's encryption state:
|
|
// - bedrock_deployments_json was encrypted -> aliases_json is already encrypted
|
|
// - azure/vertex/replicate were never encrypted -> aliases_json is plaintext
|
|
// AfterFind will try to decrypt aliases_json for encrypted rows, so we must
|
|
// encrypt any plaintext values first.
|
|
if encrypt.IsEnabled() {
|
|
type aliasRow struct {
|
|
ID uint
|
|
AliasesJSON *string
|
|
}
|
|
var plainRows []aliasRow
|
|
if err := tx.Raw(
|
|
"SELECT id, aliases_json FROM config_keys WHERE encryption_status = ? AND aliases_json IS NOT NULL AND aliases_json != '' AND aliases_json != '{}'",
|
|
tables.EncryptionStatusEncrypted,
|
|
).Scan(&plainRows).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch aliases for encryption fixup: %w", err)
|
|
}
|
|
for _, row := range plainRows {
|
|
if row.AliasesJSON == nil || *row.AliasesJSON == "" {
|
|
continue
|
|
}
|
|
// If Decrypt succeeds, the value is already encrypted — skip it (bedrock case).
|
|
// If Decrypt fails, the value is plaintext — encrypt it.
|
|
if _, err := encrypt.Decrypt(*row.AliasesJSON); err != nil {
|
|
if !json.Valid([]byte(*row.AliasesJSON)) {
|
|
return fmt.Errorf("failed to decrypt aliases for key %d: %w", row.ID, err)
|
|
}
|
|
encrypted, encErr := encrypt.Encrypt(*row.AliasesJSON)
|
|
if encErr != nil {
|
|
return fmt.Errorf("failed to encrypt aliases for key %d: %w", row.ID, encErr)
|
|
}
|
|
if err := tx.Exec(
|
|
"UPDATE config_keys SET aliases_json = ? WHERE id = ?",
|
|
encrypted, row.ID,
|
|
).Error; err != nil {
|
|
return fmt.Errorf("failed to update encrypted aliases for key %d: %w", row.ID, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recompute config_hash for keys that had aliases_json populated above,
|
|
// since aliases_json is part of the hash input and these rows now have stale hashes.
|
|
var affectedKeys []tables.TableKey
|
|
if err := tx.Where(
|
|
"aliases_json IS NOT NULL AND aliases_json != ? AND aliases_json != ?", "", "{}",
|
|
).Find(&affectedKeys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch keys for hash recomputation: %w", err)
|
|
}
|
|
for _, key := range affectedKeys {
|
|
schemaKey := schemas.Key{
|
|
Name: key.Name,
|
|
Value: key.Value,
|
|
Models: key.Models,
|
|
BlacklistedModels: key.BlacklistedModels,
|
|
Weight: getWeight(key.Weight),
|
|
AzureKeyConfig: key.AzureKeyConfig,
|
|
VertexKeyConfig: key.VertexKeyConfig,
|
|
BedrockKeyConfig: key.BedrockKeyConfig,
|
|
Aliases: key.Aliases,
|
|
VLLMKeyConfig: key.VLLMKeyConfig,
|
|
ReplicateKeyConfig: key.ReplicateKeyConfig,
|
|
Enabled: key.Enabled,
|
|
UseForBatchAPI: key.UseForBatchAPI,
|
|
}
|
|
hash, err := GenerateKeyHash(schemaKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for key %s: %w", key.Name, err)
|
|
}
|
|
if err := tx.Model(&key).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for key %s: %w", key.Name, err)
|
|
}
|
|
log.Printf("[Migration] Recomputed config_hash for key '%s' after aliases migration", key.Name)
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
m := tx.Migrator()
|
|
if m.HasColumn(&tables.TableKey{}, "aliases_json") {
|
|
if err := m.DropColumn(&tables.TableKey{}, "aliases_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running drop deployment columns and add aliases migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddKeyStatusColumns adds status and description columns to config_keys table
|
|
// These columns track the status and description of each individual key
|
|
func migrationAddKeyStatusColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_key_status_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Add status column
|
|
if !migrator.HasColumn(&tables.TableKey{}, "status") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "status"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add description column
|
|
if !migrator.HasColumn(&tables.TableKey{}, "description") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "description"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop description column
|
|
if migrator.HasColumn(&tables.TableKey{}, "description") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "description"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Drop status column
|
|
if migrator.HasColumn(&tables.TableKey{}, "status") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "status"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running key model discovery status migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddProviderStatusColumns adds status and description columns to config_providers table
|
|
// These columns track the status of model discovery attempts for keyless providers
|
|
func migrationAddProviderStatusColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_provider_status_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Add status column
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "status") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "status"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add description column
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "description") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "description"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop description column
|
|
if migrator.HasColumn(&tables.TableProvider{}, "description") {
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "description"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Drop status column
|
|
if migrator.HasColumn(&tables.TableProvider{}, "status") {
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "status"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
err := m.Migrate()
|
|
if err != nil {
|
|
return fmt.Errorf("error while running provider model discovery status migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAsyncJobResultTTLColumn adds async_job_result_ttl column to config_client table
|
|
func migrationAddAsyncJobResultTTLColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_async_job_result_ttl_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "async_job_result_ttl") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "AsyncJobResultTTL"); err != nil {
|
|
return fmt.Errorf("failed to add async_job_result_ttl column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "async_job_result_ttl") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "async_job_result_ttl"); err != nil {
|
|
return fmt.Errorf("failed to drop async_job_result_ttl column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running async_job_result_ttl migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddRateLimitToTeamsAndCustomers adds rate_limit_id column to governance_teams and governance_customers tables
|
|
func migrationAddRateLimitToTeamsAndCustomers(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_rate_limit_to_teams_and_customers",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Add rate_limit_id to governance_teams table
|
|
if !migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
|
|
if err := migrator.AddColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to add rate_limit_id column to teams: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add rate_limit_id to governance_customers table
|
|
if !migrator.HasColumn(&tables.TableCustomer{}, "rate_limit_id") {
|
|
if err := migrator.AddColumn(&tables.TableCustomer{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to add rate_limit_id column to customers: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableTeam{}, "rate_limit_id") {
|
|
if err := migrator.DropColumn(&tables.TableTeam{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to drop rate_limit_id column from teams: %w", err)
|
|
}
|
|
}
|
|
|
|
if migrator.HasColumn(&tables.TableCustomer{}, "rate_limit_id") {
|
|
if err := migrator.DropColumn(&tables.TableCustomer{}, "rate_limit_id"); err != nil {
|
|
return fmt.Errorf("failed to drop rate_limit_id column from customers: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running rate limit migration for teams and customers: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationBackfillEmptyVirtualKeyConfigs backfills existing virtual keys that have
|
|
// empty ProviderConfigs or MCPConfigs with all available providers/MCP clients.
|
|
// This preserves the previous "empty means all" behavior for existing VKs after
|
|
// the semantic change to "empty means none" (deny-by-default).
|
|
func migrationBackfillEmptyVirtualKeyConfigs(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "backfill_empty_virtual_key_configs",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
|
|
// Step 1: Backfill ProviderConfigs for VKs that have none
|
|
// Find all virtual keys
|
|
var allVKs []tables.TableVirtualKey
|
|
if err := tx.Find(&allVKs).Error; err != nil {
|
|
return fmt.Errorf("failed to query virtual keys: %w", err)
|
|
}
|
|
|
|
// Get all available providers
|
|
var allProviders []tables.TableProvider
|
|
if err := tx.Find(&allProviders).Error; err != nil {
|
|
return fmt.Errorf("failed to query providers: %w", err)
|
|
}
|
|
|
|
// Track which VK IDs were modified so we can recompute their config_hash
|
|
modifiedVKIDs := make(map[string]struct{})
|
|
|
|
for _, vk := range allVKs {
|
|
// Check if this VK has any provider configs
|
|
var providerConfigCount int64
|
|
if err := tx.Model(&tables.TableVirtualKeyProviderConfig{}).Where("virtual_key_id = ?", vk.ID).Count(&providerConfigCount).Error; err != nil {
|
|
return fmt.Errorf("failed to count provider configs for VK %s: %w", vk.ID, err)
|
|
}
|
|
|
|
if providerConfigCount == 0 && len(allProviders) > 0 {
|
|
// VK has no provider configs - backfill with all available providers
|
|
for _, provider := range allProviders {
|
|
providerConfig := tables.TableVirtualKeyProviderConfig{
|
|
VirtualKeyID: vk.ID,
|
|
Provider: provider.Name,
|
|
Weight: bifrost.Ptr(1.0),
|
|
AllowedModels: []string{},
|
|
AllowAllKeys: true,
|
|
}
|
|
if err := tx.Create(&providerConfig).Error; err != nil {
|
|
return fmt.Errorf("failed to create provider config for VK %s, provider %s: %w", vk.ID, provider.Name, err)
|
|
}
|
|
}
|
|
modifiedVKIDs[vk.ID] = struct{}{}
|
|
log.Printf("[Migration] Backfilled VK '%s' with %d provider configs", vk.Name, len(allProviders))
|
|
}
|
|
}
|
|
|
|
// Step 2: Backfill MCPConfigs for VKs that have none
|
|
// Get all available MCP clients
|
|
var allMCPClients []tables.TableMCPClient
|
|
if err := tx.Find(&allMCPClients).Error; err != nil {
|
|
return fmt.Errorf("failed to query MCP clients: %w", err)
|
|
}
|
|
|
|
for _, vk := range allVKs {
|
|
// Check if this VK has any MCP configs
|
|
var mcpConfigCount int64
|
|
if err := tx.Model(&tables.TableVirtualKeyMCPConfig{}).Where("virtual_key_id = ?", vk.ID).Count(&mcpConfigCount).Error; err != nil {
|
|
return fmt.Errorf("failed to count MCP configs for VK %s: %w", vk.ID, err)
|
|
}
|
|
|
|
if mcpConfigCount == 0 && len(allMCPClients) > 0 {
|
|
// VK has no MCP configs - backfill with all available MCP clients with wildcard
|
|
for _, mcpClient := range allMCPClients {
|
|
mcpConfig := tables.TableVirtualKeyMCPConfig{
|
|
VirtualKeyID: vk.ID,
|
|
MCPClientID: mcpClient.ID,
|
|
ToolsToExecute: []string{"*"},
|
|
}
|
|
if err := tx.Create(&mcpConfig).Error; err != nil {
|
|
return fmt.Errorf("failed to create MCP config for VK %s, client %d: %w", vk.ID, mcpClient.ID, err)
|
|
}
|
|
}
|
|
modifiedVKIDs[vk.ID] = struct{}{}
|
|
log.Printf("[Migration] Backfilled VK '%s' with %d MCP client configs", vk.Name, len(allMCPClients))
|
|
}
|
|
}
|
|
|
|
// Step 3: Recompute and persist config_hash for every VK that was modified.
|
|
// Without this, subsequent config-sync diff logic would see a stale hash and
|
|
// attempt to re-reconcile the VK (potentially undoing the backfill).
|
|
for vkID := range modifiedVKIDs {
|
|
var vk tables.TableVirtualKey
|
|
if err := tx.
|
|
Preload("ProviderConfigs").
|
|
Preload("ProviderConfigs.Keys").
|
|
Preload("MCPConfigs").
|
|
First(&vk, "id = ?", vkID).Error; err != nil {
|
|
return fmt.Errorf("failed to reload VK %s for hash recomputation: %w", vkID, err)
|
|
}
|
|
newHash, err := GenerateVirtualKeyHash(vk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for VK %s: %w", vkID, err)
|
|
}
|
|
if err := tx.Model(&tables.TableVirtualKey{}).
|
|
Where("id = ?", vkID).
|
|
Update("config_hash", newHash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for VK %s: %w", vkID, err)
|
|
}
|
|
log.Printf("[Migration] Recomputed config_hash for VK '%s'", vk.Name)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// No rollback needed - the backfilled configs are valid data
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running backfill empty virtual key configs migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddRequiredHeadersJSONColumn adds the required_headers_json column to the config_client table
|
|
func migrationAddRequiredHeadersJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_required_headers_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "required_headers_json") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "RequiredHeadersJSON"); err != nil {
|
|
return fmt.Errorf("failed to add required_headers_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "required_headers_json") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "required_headers_json"); err != nil {
|
|
return fmt.Errorf("failed to drop required_headers_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running required_headers_json migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddOutputCostPerVideoPerSecond adds output_cost_per_video_per_second column to governance_model_pricing table
|
|
func migrationAddOutputCostPerVideoPerSecond(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_output_cost_per_video_per_second_and_output_cost_per_second_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "output_cost_per_video_per_second") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "output_cost_per_video_per_second"); err != nil {
|
|
return fmt.Errorf("failed to add output_cost_per_video_per_second column: %w", err)
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableModelPricing{}, "output_cost_per_second") {
|
|
if err := migrator.AddColumn(&tables.TableModelPricing{}, "output_cost_per_second"); err != nil {
|
|
return fmt.Errorf("failed to add output_cost_per_second column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableModelPricing{}, "output_cost_per_video_per_second") {
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "output_cost_per_video_per_second"); err != nil {
|
|
return fmt.Errorf("failed to drop output_cost_per_video_per_second column: %w", err)
|
|
}
|
|
}
|
|
|
|
if migrator.HasColumn(&tables.TableModelPricing{}, "output_cost_per_second") {
|
|
if err := migrator.DropColumn(&tables.TableModelPricing{}, "output_cost_per_second"); err != nil {
|
|
return fmt.Errorf("failed to drop output_cost_per_second column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running output_cost_per_video_per_second migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddLoggingHeadersJSONColumn adds the logging_headers_json column to the config_client table
|
|
func migrationAddLoggingHeadersJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_logging_headers_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "logging_headers_json") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "LoggingHeadersJSON"); err != nil {
|
|
return fmt.Errorf("failed to add logging_headers_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "logging_headers_json") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "logging_headers_json"); err != nil {
|
|
return fmt.Errorf("failed to drop logging_headers_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running logging_headers_json migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddHideDeletedVirtualKeysInFiltersColumn adds the hide_deleted_virtual_keys_in_filters column to config_client.
|
|
func migrationAddHideDeletedVirtualKeysInFiltersColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_hide_deleted_virtual_keys_in_filters_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "hide_deleted_virtual_keys_in_filters") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "HideDeletedVirtualKeysInFilters"); err != nil {
|
|
return fmt.Errorf("failed to add hide_deleted_virtual_keys_in_filters column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "hide_deleted_virtual_keys_in_filters") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "hide_deleted_virtual_keys_in_filters"); err != nil {
|
|
return fmt.Errorf("failed to drop hide_deleted_virtual_keys_in_filters column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running hide_deleted_virtual_keys_in_filters migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddEnforceSCIMAuthColumn adds the enforce_scim_auth column to the client config table
|
|
func migrationAddEnforceSCIMAuthColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_enforce_scim_auth_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "enforce_scim_auth") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "enforce_scim_auth"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "enforce_scim_auth") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "enforce_scim_auth"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running enforce SCIM auth column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddEnforceAuthOnInferenceColumn adds the enforce_auth_on_inference column to the config_client table
|
|
func migrationAddEnforceAuthOnInferenceColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_enforce_auth_on_inference_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "enforce_auth_on_inference") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "enforce_auth_on_inference"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Populate from old fields: set to true if either old flag was true
|
|
if err := tx.Exec("UPDATE config_client SET enforce_auth_on_inference = true WHERE enforce_governance_header = true OR enforce_scim_auth = true").Error; err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "enforce_auth_on_inference") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "enforce_auth_on_inference"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running enforce auth on inference column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationReconcilePricingOverridesTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "reconcile_pricing_overrides_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mgr := tx.Migrator()
|
|
|
|
if !mgr.HasTable(&tables.TablePricingOverride{}) {
|
|
if err := mgr.CreateTable(&tables.TablePricingOverride{}); err != nil {
|
|
return fmt.Errorf("failed to create governance_pricing_overrides table: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if err := tx.AutoMigrate(&tables.TablePricingOverride{}); err != nil {
|
|
return fmt.Errorf("failed to automigrate governance_pricing_overrides table: %w", err)
|
|
}
|
|
for _, indexName := range []string{"idx_pricing_override_scope", "idx_pricing_override_match"} {
|
|
if mgr.HasIndex(&tables.TablePricingOverride{}, indexName) {
|
|
continue
|
|
}
|
|
if err := mgr.CreateIndex(&tables.TablePricingOverride{}, indexName); err != nil {
|
|
return fmt.Errorf("failed to create pricing override index %s: %w", indexName, err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mgr := tx.Migrator()
|
|
if mgr.HasTable(&tables.TablePricingOverride{}) {
|
|
if err := mgr.DropTable(&tables.TablePricingOverride{}); err != nil {
|
|
return fmt.Errorf("failed to drop governance_pricing_overrides table: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running pricing overrides table reconcile migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddEncryptionColumns adds the encryption_status column to the config_keys, governance_virtual_keys, sessions, oauth_configs, oauth_tokens, config_mcp_clients, config_providers, config_vector_store, and config_plugins tables
|
|
func migrationAddEncryptionColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_encryption_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mgr := tx.Migrator()
|
|
|
|
type encryptionTable struct {
|
|
table interface{}
|
|
columns []string
|
|
}
|
|
|
|
targets := []encryptionTable{
|
|
{&tables.TableKey{}, []string{"encryption_status"}},
|
|
{&tables.TableVirtualKey{}, []string{"encryption_status", "value_hash"}},
|
|
{&tables.SessionsTable{}, []string{"encryption_status", "token_hash"}},
|
|
{&tables.TableOauthConfig{}, []string{"encryption_status"}},
|
|
{&tables.TableOauthToken{}, []string{"encryption_status"}},
|
|
{&tables.TableMCPClient{}, []string{"encryption_status"}},
|
|
{&tables.TableProvider{}, []string{"encryption_status"}},
|
|
{&tables.TableVectorStoreConfig{}, []string{"encryption_status"}},
|
|
{&tables.TablePlugin{}, []string{"encryption_status"}},
|
|
}
|
|
|
|
for _, t := range targets {
|
|
for _, col := range t.columns {
|
|
if !mgr.HasColumn(t.table, col) {
|
|
if err := mgr.AddColumn(t.table, col); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", col, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Backfill encryption_status for all tables that have the column
|
|
backfillTables := []string{
|
|
"config_keys",
|
|
"governance_virtual_keys",
|
|
"sessions",
|
|
"oauth_configs",
|
|
"oauth_tokens",
|
|
"config_mcp_clients",
|
|
"config_providers",
|
|
"config_vector_store",
|
|
"config_plugins",
|
|
}
|
|
for _, table := range backfillTables {
|
|
if err := tx.Exec(fmt.Sprintf(
|
|
"UPDATE %s SET encryption_status = 'plain_text' WHERE encryption_status IS NULL OR encryption_status = ''",
|
|
table,
|
|
)).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill encryption_status in %s: %w", table, err)
|
|
}
|
|
}
|
|
|
|
// Backfill value_hash for existing virtual keys
|
|
// Use NULL instead of '' to avoid unique constraint violations
|
|
// (multiple rows with '' would violate the unique index, but NULLs are excluded)
|
|
if err := tx.Exec(`
|
|
UPDATE governance_virtual_keys
|
|
SET value_hash = NULL
|
|
WHERE value_hash IS NULL OR value_hash = ''
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to initialize value_hash: %w", err)
|
|
}
|
|
|
|
// Backfill token_hash for existing sessions
|
|
// Use NULL instead of '' to avoid unique constraint violations
|
|
if err := tx.Exec(`
|
|
UPDATE sessions
|
|
SET token_hash = NULL
|
|
WHERE token_hash IS NULL OR token_hash = ''
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to initialize token_hash: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mgr := tx.Migrator()
|
|
|
|
type dropInfo struct {
|
|
table interface{}
|
|
columns []string
|
|
}
|
|
|
|
drops := []dropInfo{
|
|
{&tables.TableKey{}, []string{"encryption_status"}},
|
|
{&tables.TableVirtualKey{}, []string{"encryption_status", "value_hash"}},
|
|
{&tables.SessionsTable{}, []string{"encryption_status", "token_hash"}},
|
|
{&tables.TableOauthConfig{}, []string{"encryption_status"}},
|
|
{&tables.TableOauthToken{}, []string{"encryption_status"}},
|
|
{&tables.TableMCPClient{}, []string{"encryption_status"}},
|
|
{&tables.TableProvider{}, []string{"encryption_status"}},
|
|
{&tables.TableVectorStoreConfig{}, []string{"encryption_status"}},
|
|
{&tables.TablePlugin{}, []string{"encryption_status"}},
|
|
}
|
|
|
|
for _, d := range drops {
|
|
for _, col := range d.columns {
|
|
if mgr.HasColumn(d.table, col) {
|
|
if err := mgr.DropColumn(d.table, col); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running encryption columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationDropEnableGovernanceColumn drops the enable_governance column from the config_client table
|
|
func migrationDropEnableGovernanceColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "drop_enable_governance_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "enable_governance") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "enable_governance"); err != nil {
|
|
return fmt.Errorf("failed to drop enable_governance column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running drop enable governance column rollback: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddVLLMKeyConfigColumns adds vllm_url and vllm_model_name columns to the key table
|
|
func migrationAddVLLMKeyConfigColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_vllm_key_config_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "vllm_url") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "vllm_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableKey{}, "vllm_model_name") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "vllm_model_name"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableKey{}, "vllm_url") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "vllm_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TableKey{}, "vllm_model_name") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "vllm_model_name"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running vllm key config columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationWidenEncryptedVarcharColumns widens varchar columns that store AES-256-GCM
|
|
// encrypted values to TEXT. Encryption adds ~28 bytes of overhead plus base64 expansion (4/3x),
|
|
// so a varchar(255) can only hold ~153-char plaintext. Using TEXT removes any size constraints.
|
|
// SQLite does not enforce varchar(n) size constraints, so no migration is needed there.
|
|
func migrationWidenEncryptedVarcharColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "widen_encrypted_varchar_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
if tx.Dialector.Name() != "postgres" {
|
|
return nil
|
|
}
|
|
stmts := []string{
|
|
// config_keys table - all encrypted EnvVar fields
|
|
"ALTER TABLE config_keys ALTER COLUMN azure_api_version TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN azure_client_id TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN azure_tenant_id TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN vertex_project_id TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN vertex_project_number TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN vertex_region TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN bedrock_access_key TYPE TEXT",
|
|
"ALTER TABLE config_keys ALTER COLUMN bedrock_region TYPE TEXT",
|
|
// sessions table
|
|
"ALTER TABLE sessions ALTER COLUMN token TYPE TEXT",
|
|
// governance_virtual_keys table
|
|
"ALTER TABLE governance_virtual_keys ALTER COLUMN value TYPE TEXT",
|
|
// oauth_configs table
|
|
"ALTER TABLE oauth_configs ALTER COLUMN code_verifier TYPE TEXT",
|
|
}
|
|
for _, stmt := range stmts {
|
|
if err := tx.Exec(stmt).Error; err != nil {
|
|
return fmt.Errorf("failed to widen column (%s): %w", stmt, err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running widen encrypted varchar columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddBedrockAssumeRoleColumns adds bedrock_role_arn, bedrock_external_id, and bedrock_role_session_name
|
|
// columns to the config_keys table for STS AssumeRole support in Bedrock keys.
|
|
func migrationAddBedrockAssumeRoleColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_bedrock_assume_role_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(&tables.TableKey{}, "bedrock_role_arn") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "bedrock_role_arn"); err != nil {
|
|
return fmt.Errorf("failed to add bedrock_role_arn column: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasColumn(&tables.TableKey{}, "bedrock_external_id") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "bedrock_external_id"); err != nil {
|
|
return fmt.Errorf("failed to add bedrock_external_id column: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasColumn(&tables.TableKey{}, "bedrock_role_session_name") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "bedrock_role_session_name"); err != nil {
|
|
return fmt.Errorf("failed to add bedrock_role_session_name column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableKey{}, "bedrock_role_arn") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "bedrock_role_arn"); err != nil {
|
|
return fmt.Errorf("failed to drop bedrock_role_arn column: %w", err)
|
|
}
|
|
}
|
|
if mg.HasColumn(&tables.TableKey{}, "bedrock_external_id") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "bedrock_external_id"); err != nil {
|
|
return fmt.Errorf("failed to drop bedrock_external_id column: %w", err)
|
|
}
|
|
}
|
|
if mg.HasColumn(&tables.TableKey{}, "bedrock_role_session_name") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "bedrock_role_session_name"); err != nil {
|
|
return fmt.Errorf("failed to drop bedrock_role_session_name column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running bedrock assume role columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAllowAllKeysToProviderConfig adds the allow_all_keys column to the provider config table
|
|
// and backfills existing rows: any provider config with no keys in the join table previously meant
|
|
// "allow all keys" (old semantic), so they get allow_all_keys = true to preserve behaviour.
|
|
func migrationAddAllowAllKeysToProviderConfig(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_allow_all_keys_to_provider_config",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
|
|
// Add the column if it doesn't exist
|
|
if !migratorInstance.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "allow_all_keys") {
|
|
if err := migratorInstance.AddColumn(&tables.TableVirtualKeyProviderConfig{}, "allow_all_keys"); err != nil {
|
|
return fmt.Errorf("failed to add allow_all_keys column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill: find all provider configs that have no keys in the join table.
|
|
// These previously meant "allow all keys", so set allow_all_keys = true.
|
|
var allConfigs []tables.TableVirtualKeyProviderConfig
|
|
if err := tx.Find(&allConfigs).Error; err != nil {
|
|
return fmt.Errorf("failed to query provider configs: %w", err)
|
|
}
|
|
|
|
// Track which VK IDs were modified so we can recompute their config_hash.
|
|
// Without this, subsequent config-sync diff logic would see a stale hash
|
|
// and attempt to re-reconcile the VK (potentially undoing the backfill).
|
|
modifiedVKIDs := make(map[string]struct{})
|
|
|
|
for _, pc := range allConfigs {
|
|
var keyCount int64
|
|
if err := tx.Table("governance_virtual_key_provider_config_keys").
|
|
Where("table_virtual_key_provider_config_id = ?", pc.ID).
|
|
Count(&keyCount).Error; err != nil {
|
|
return fmt.Errorf("failed to count keys for provider config %d: %w", pc.ID, err)
|
|
}
|
|
|
|
if keyCount == 0 {
|
|
if err := tx.Model(&tables.TableVirtualKeyProviderConfig{}).
|
|
Where("id = ?", pc.ID).
|
|
Update("allow_all_keys", true).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill allow_all_keys for provider config %d: %w", pc.ID, err)
|
|
}
|
|
modifiedVKIDs[pc.VirtualKeyID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Recompute and persist config_hash for every VK that was modified.
|
|
for vkID := range modifiedVKIDs {
|
|
var vk tables.TableVirtualKey
|
|
if err := tx.
|
|
Preload("ProviderConfigs").
|
|
Preload("ProviderConfigs.Keys").
|
|
Preload("MCPConfigs").
|
|
First(&vk, "id = ?", vkID).Error; err != nil {
|
|
return fmt.Errorf("failed to reload VK %s for hash recomputation: %w", vkID, err)
|
|
}
|
|
newHash, err := GenerateVirtualKeyHash(vk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for VK %s: %w", vkID, err)
|
|
}
|
|
if err := tx.Model(&tables.TableVirtualKey{}).
|
|
Where("id = ?", vkID).
|
|
Update("config_hash", newHash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for VK %s: %w", vkID, err)
|
|
}
|
|
log.Printf("[Migration] Recomputed config_hash for VK '%s'", vk.Name)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
if migratorInstance.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "allow_all_keys") {
|
|
if err := migratorInstance.DropColumn(&tables.TableVirtualKeyProviderConfig{}, "allow_all_keys"); err != nil {
|
|
return fmt.Errorf("failed to drop allow_all_keys column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running allow_all_keys migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPDisableAutoToolInjectColumn adds the mcp_disable_auto_tool_inject column to the client config table.
|
|
// When true, MCP tools are not automatically injected into requests; only explicit context filters apply.
|
|
func migrationAddMCPDisableAutoToolInjectColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_disable_auto_tool_inject_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
if !migratorInstance.HasColumn(&tables.TableClientConfig{}, "mcp_disable_auto_tool_inject") {
|
|
if err := migratorInstance.AddColumn(&tables.TableClientConfig{}, "mcp_disable_auto_tool_inject"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migratorInstance := tx.Migrator()
|
|
if err := migratorInstance.DropColumn(&tables.TableClientConfig{}, "mcp_disable_auto_tool_inject"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running mcp disable auto tool inject migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPricingRefactorColumns adds all new pricing columns introduced in the pricing module refactor
|
|
func migrationAddPricingRefactorColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_pricing_refactor_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_priority",
|
|
"output_cost_per_token_priority",
|
|
"cache_creation_input_token_cost_above_1hr",
|
|
"cache_creation_input_token_cost_above_1hr_above_200k_tokens",
|
|
"cache_creation_input_audio_token_cost",
|
|
"cache_read_input_token_cost_priority",
|
|
"input_cost_per_pixel",
|
|
"output_cost_per_pixel",
|
|
"output_cost_per_image_premium_image",
|
|
"output_cost_per_image_above_512_and_512_pixels",
|
|
"output_cost_per_image_above_512x512_pixels_premium",
|
|
"output_cost_per_image_above_1024_and_1024_pixels",
|
|
"output_cost_per_image_above_1024x1024_pixels_premium",
|
|
"input_cost_per_audio_token",
|
|
"input_cost_per_second",
|
|
"input_cost_per_video_per_second",
|
|
"input_cost_per_audio_per_second",
|
|
"output_cost_per_audio_token",
|
|
"search_context_cost_per_query",
|
|
"code_interpreter_cost_per_session",
|
|
"input_cost_per_character",
|
|
"input_cost_per_token_above_128k_tokens",
|
|
"input_cost_per_image_above_128k_tokens",
|
|
"input_cost_per_video_per_second_above_128k_tokens",
|
|
"input_cost_per_audio_per_second_above_128k_tokens",
|
|
"output_cost_per_token_above_128k_tokens",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_priority",
|
|
"output_cost_per_token_priority",
|
|
"cache_creation_input_token_cost_above_1hr",
|
|
"cache_creation_input_token_cost_above_1hr_above_200k_tokens",
|
|
"cache_creation_input_audio_token_cost",
|
|
"cache_read_input_token_cost_priority",
|
|
"input_cost_per_pixel",
|
|
"output_cost_per_pixel",
|
|
"output_cost_per_image_premium_image",
|
|
"output_cost_per_image_above_512_and_512_pixels",
|
|
"output_cost_per_image_above_512x512_pixels_premium",
|
|
"output_cost_per_image_above_1024_and_1024_pixels",
|
|
"output_cost_per_image_above_1024x1024_pixels_premium",
|
|
"input_cost_per_audio_token",
|
|
"input_cost_per_second",
|
|
"input_cost_per_video_per_second",
|
|
"input_cost_per_audio_per_second",
|
|
"output_cost_per_audio_token",
|
|
"search_context_cost_per_query",
|
|
"code_interpreter_cost_per_session",
|
|
"input_cost_per_character",
|
|
"input_cost_per_token_above_128k_tokens",
|
|
"input_cost_per_image_above_128k_tokens",
|
|
"input_cost_per_video_per_second_above_128k_tokens",
|
|
"input_cost_per_audio_per_second_above_128k_tokens",
|
|
"output_cost_per_token_above_128k_tokens",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running pricing refactor columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationRenameTruncatedPricingColumn renames the output_cost_per_image_above_512_and_512_pixels_and_premium_image
|
|
// column which at 64 chars exceeds PostgreSQL's 63-character identifier limit. PostgreSQL silently truncated
|
|
// it to output_cost_per_image_above_512_and_512_pixels_and_premium_imag (63 chars), while SQLite kept the
|
|
// full 64-char name. This migration renames whichever variant exists to the shorter canonical name.
|
|
func migrationRenameTruncatedPricingColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "rename_truncated_pricing_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
const newName = "output_cost_per_image_above_512x512_pixels_premium"
|
|
if mg.HasColumn(&tables.TableModelPricing{}, newName) {
|
|
return nil
|
|
}
|
|
|
|
// PostgreSQL truncated the 64-char name to 63 chars
|
|
const oldNamePG = "output_cost_per_image_above_512_and_512_pixels_and_premium_imag"
|
|
// SQLite kept the full 64-char name
|
|
const oldNameSQLite = "output_cost_per_image_above_512_and_512_pixels_and_premium_image"
|
|
|
|
if mg.HasColumn(&tables.TableModelPricing{}, oldNamePG) {
|
|
if err := tx.Exec("ALTER TABLE governance_model_pricing RENAME COLUMN " + oldNamePG + " TO " + newName).Error; err != nil {
|
|
return fmt.Errorf("failed to rename column %s to %s: %w", oldNamePG, newName, err)
|
|
}
|
|
} else if mg.HasColumn(&tables.TableModelPricing{}, oldNameSQLite) {
|
|
if err := tx.Exec("ALTER TABLE governance_model_pricing RENAME COLUMN " + oldNameSQLite + " TO " + newName).Error; err != nil {
|
|
return fmt.Errorf("failed to rename column %s to %s: %w", oldNameSQLite, newName, err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running rename_truncated_pricing_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddImageQualityPricingColumns adds quality-based per-image cost columns (low, medium, high, auto).
|
|
func migrationAddImageQualityPricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_image_quality_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"output_cost_per_image_above_2048_and_2048_pixels",
|
|
"output_cost_per_image_above_4096_and_4096_pixels",
|
|
"output_cost_per_image_low_quality",
|
|
"output_cost_per_image_medium_quality",
|
|
"output_cost_per_image_high_quality",
|
|
"output_cost_per_image_auto_quality",
|
|
}
|
|
for _, field := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"output_cost_per_image_above_2048_and_2048_pixels",
|
|
"output_cost_per_image_above_4096_and_4096_pixels",
|
|
"output_cost_per_image_low_quality",
|
|
"output_cost_per_image_medium_quality",
|
|
"output_cost_per_image_high_quality",
|
|
"output_cost_per_image_auto_quality",
|
|
}
|
|
for _, field := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running image quality pricing columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// legacyRoutingRuleColumns is a migration-only struct that represents the old routing_rules
|
|
// schema before provider/model/key_id were moved to the routing_targets table.
|
|
// GORM's SQLite DropColumn/AddColumn need a real struct (not a string table name) to
|
|
// reconstruct the table correctly, so we keep this stub around for migration use only.
|
|
type legacyRoutingRuleColumns struct {
|
|
Provider string `gorm:"column:provider;type:varchar(255)"`
|
|
Model string `gorm:"column:model;type:varchar(255)"`
|
|
}
|
|
|
|
func (legacyRoutingRuleColumns) TableName() string { return "routing_rules" }
|
|
|
|
// migrationAddRoutingTargetsTable creates the routing_targets table and seeds one target row per
|
|
// existing routing rule, migrating the legacy provider/model columns.
|
|
// After seeding, the legacy columns are dropped from routing_rules.
|
|
func migrationAddRoutingTargetsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_routing_targets_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
// 1. Create routing_targets table
|
|
if !mg.HasTable(&tables.TableRoutingTarget{}) {
|
|
if err := mg.CreateTable(&tables.TableRoutingTarget{}); err != nil {
|
|
return fmt.Errorf("failed to create routing_targets table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasConstraint(&tables.TableRoutingRule{}, "Targets") {
|
|
if err := mg.CreateConstraint(&tables.TableRoutingRule{}, "Targets"); err != nil {
|
|
return fmt.Errorf("failed to create routing_targets foreign key: %w", err)
|
|
}
|
|
}
|
|
|
|
// 2. Read legacy data BEFORE dropping columns, then drop columns, then seed.
|
|
// Order matters: DropColumn on SQLite recreates the routing_rules table, which
|
|
// triggers the OnDelete:CASCADE on routing_targets and deletes any rows inserted
|
|
// before the drop. So we read first, drop, then insert.
|
|
type legacyRule struct {
|
|
ID string
|
|
Provider string
|
|
Model string
|
|
}
|
|
var legacyRows []legacyRule
|
|
if mg.HasColumn("routing_rules", "provider") {
|
|
if err := tx.Table("routing_rules").Select("id, provider, model").Scan(&legacyRows).Error; err != nil {
|
|
return fmt.Errorf("failed to scan routing_rules for seeding: %w", err)
|
|
}
|
|
}
|
|
|
|
// 3. Drop legacy single-target columns from routing_rules.
|
|
// Must use the struct form (not string) so SQLite can reconstruct the table correctly.
|
|
// Do this BEFORE seeding so the CASCADE triggered by table recreation hits an empty
|
|
// routing_targets table (nothing to delete yet).
|
|
legacyModel := &legacyRoutingRuleColumns{}
|
|
for _, col := range []string{"provider", "model"} {
|
|
if mg.HasColumn("routing_rules", col) {
|
|
if err := mg.DropColumn(legacyModel, col); err != nil {
|
|
return fmt.Errorf("failed to drop column %s from routing_rules: %w", col, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Seed routing_targets from the legacy data read above (idempotent).
|
|
for _, row := range legacyRows {
|
|
var count int64
|
|
if err := tx.Table("routing_targets").Where("rule_id = ?", row.ID).Count(&count).Error; err != nil {
|
|
return fmt.Errorf("failed to count targets for rule %s: %w", row.ID, err)
|
|
}
|
|
if count > 0 {
|
|
continue // already seeded
|
|
}
|
|
target := tables.TableRoutingTarget{
|
|
RuleID: row.ID,
|
|
Weight: 1.0,
|
|
}
|
|
if row.Provider != "" {
|
|
p := row.Provider
|
|
target.Provider = &p
|
|
}
|
|
if row.Model != "" {
|
|
m := row.Model
|
|
target.Model = &m
|
|
}
|
|
if err := tx.Create(&target).Error; err != nil {
|
|
return fmt.Errorf("failed to seed target for rule %s: %w", row.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
if !mg.HasTable(&tables.TableRoutingTarget{}) {
|
|
return nil
|
|
}
|
|
|
|
// 1. Add provider and model columns back to routing_rules (before dropping targets)
|
|
legacyModel := &legacyRoutingRuleColumns{}
|
|
for _, col := range []string{"provider", "model"} {
|
|
if !mg.HasColumn("routing_rules", col) {
|
|
if err := mg.AddColumn(legacyModel, col); err != nil {
|
|
return fmt.Errorf("failed to add column %s to routing_rules: %w", col, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Backfill provider/model from routing_targets into routing_rules (join by rule_id)
|
|
type targetRow struct {
|
|
RuleID string
|
|
Provider *string
|
|
Model *string
|
|
}
|
|
var targets []targetRow
|
|
if err := tx.Table("routing_targets").Select("rule_id, provider, model").Order("rule_id").Scan(&targets).Error; err != nil {
|
|
return fmt.Errorf("failed to scan routing_targets for backfill: %w", err)
|
|
}
|
|
ruleData := make(map[string]targetRow)
|
|
for _, t := range targets {
|
|
if _, ok := ruleData[t.RuleID]; !ok {
|
|
ruleData[t.RuleID] = t
|
|
}
|
|
}
|
|
for ruleID, t := range ruleData {
|
|
provider, model := "", ""
|
|
if t.Provider != nil {
|
|
provider = *t.Provider
|
|
}
|
|
if t.Model != nil {
|
|
model = *t.Model
|
|
}
|
|
if err := tx.Table("routing_rules").Where("id = ?", ruleID).Updates(map[string]interface{}{
|
|
"provider": provider,
|
|
"model": model,
|
|
}).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill routing_rule %s: %w", ruleID, err)
|
|
}
|
|
}
|
|
|
|
// 3. Drop routing_targets table
|
|
if mg.HasConstraint(&tables.TableRoutingRule{}, "Targets") {
|
|
if err := mg.DropConstraint(&tables.TableRoutingRule{}, "Targets"); err != nil {
|
|
return fmt.Errorf("failed to drop routing_targets foreign key: %w", err)
|
|
}
|
|
}
|
|
if err := mg.DropTable(&tables.TableRoutingTarget{}); err != nil {
|
|
return fmt.Errorf("failed to drop routing_targets table: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running routing_targets_table migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPromptRepoTables adds the prompt repository tables (folders, prompts, versions, sessions)
|
|
func migrationAddPromptRepoTables(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_prompt_repo_tables",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Create folders table
|
|
if !migrator.HasTable(&tables.TableFolder{}) {
|
|
if err := migrator.CreateTable(&tables.TableFolder{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create prompts table
|
|
if !migrator.HasTable(&tables.TablePrompt{}) {
|
|
if err := migrator.CreateTable(&tables.TablePrompt{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create prompt_versions table
|
|
if !migrator.HasTable(&tables.TablePromptVersion{}) {
|
|
if err := migrator.CreateTable(&tables.TablePromptVersion{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create prompt_version_messages table
|
|
if !migrator.HasTable(&tables.TablePromptVersionMessage{}) {
|
|
if err := migrator.CreateTable(&tables.TablePromptVersionMessage{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create prompt_sessions table
|
|
if !migrator.HasTable(&tables.TablePromptSession{}) {
|
|
if err := migrator.CreateTable(&tables.TablePromptSession{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create prompt_session_messages table
|
|
if !migrator.HasTable(&tables.TablePromptSessionMessage{}) {
|
|
if err := migrator.CreateTable(&tables.TablePromptSessionMessage{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Apply schema updates (indexes, constraints) to existing tables
|
|
if err := tx.AutoMigrate(
|
|
&tables.TablePromptVersion{},
|
|
&tables.TablePromptSession{},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
// Drop tables in reverse order (respecting foreign key constraints)
|
|
if err := migrator.DropTable(&tables.TablePromptSessionMessage{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePromptSession{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePromptVersionMessage{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePromptVersion{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TablePrompt{}); err != nil {
|
|
return err
|
|
}
|
|
if err := migrator.DropTable(&tables.TableFolder{}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running prompt repo tables migration: %s", err.Error())
|
|
}
|
|
|
|
// Add prompt_id column to prompt message tables
|
|
m = migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_prompt_id_to_prompt_message_tables",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TablePromptVersionMessage{}, "prompt_id") {
|
|
if err := migrator.AddColumn(&tables.TablePromptVersionMessage{}, "PromptID"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !migrator.HasColumn(&tables.TablePromptSessionMessage{}, "prompt_id") {
|
|
if err := migrator.AddColumn(&tables.TablePromptSessionMessage{}, "PromptID"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TablePromptVersionMessage{}, "prompt_id") {
|
|
if err := migrator.DropColumn(&tables.TablePromptVersionMessage{}, "prompt_id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TablePromptSessionMessage{}, "prompt_id") {
|
|
if err := migrator.DropColumn(&tables.TablePromptSessionMessage{}, "prompt_id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_prompt_id_to_prompt_message_tables migration: %s", err.Error())
|
|
}
|
|
|
|
m = migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_model_parameters_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasTable(&tables.TableModelParameters{}) {
|
|
if err := migrator.CreateTable(&tables.TableModelParameters{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasTable(&tables.TableModelParameters{}) {
|
|
if err := migrator.DropTable(&tables.TableModelParameters{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_model_parameters_table migration: %s", err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrationBackfillAllowedModelsWildcard converts empty allowed_models on
|
|
// governance_virtual_key_provider_configs and empty models_json on keys to ["*"],
|
|
// preserving the previous "empty = allow all" semantics for existing records.
|
|
// After this migration the new convention applies: ["*"] = allow all, [] = deny all.
|
|
func migrationBackfillAllowedModelsWildcard(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "backfill_allowed_models_wildcard",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
|
|
// --- Field 1: vk.provider_config.allowed_models ---
|
|
// Rows with '[]' previously meant "allow all models"; migrate to '["*"]'.
|
|
if err := tx.Model(&tables.TableVirtualKeyProviderConfig{}).
|
|
Where("allowed_models = ? OR allowed_models IS NULL", `[]`).
|
|
Update("allowed_models", `["*"]`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill provider_config allowed_models: %w", err)
|
|
}
|
|
|
|
// Recompute config_hash for all VKs that have provider configs
|
|
// (any of them may have had their allowed_models updated above).
|
|
var modifiedVKIDs []string
|
|
if err := tx.Model(&tables.TableVirtualKeyProviderConfig{}).
|
|
Distinct("virtual_key_id").
|
|
Pluck("virtual_key_id", &modifiedVKIDs).Error; err != nil {
|
|
return fmt.Errorf("failed to query VK IDs for hash recomputation: %w", err)
|
|
}
|
|
|
|
for _, vkID := range modifiedVKIDs {
|
|
var vk tables.TableVirtualKey
|
|
if err := tx.
|
|
Preload("ProviderConfigs").
|
|
Preload("ProviderConfigs.Keys").
|
|
Preload("MCPConfigs").
|
|
First(&vk, "id = ?", vkID).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// Orphaned provider config row — VK was deleted; skip.
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to reload VK %s for hash recomputation: %w", vkID, err)
|
|
}
|
|
newHash, err := GenerateVirtualKeyHash(vk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for VK %s: %w", vkID, err)
|
|
}
|
|
if err := tx.Model(&tables.TableVirtualKey{}).
|
|
Where("id = ?", vkID).
|
|
Update("config_hash", newHash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for VK %s: %w", vkID, err)
|
|
}
|
|
log.Printf("[Migration] Recomputed config_hash for VK '%s' after allowed_models backfill", vk.Name)
|
|
}
|
|
|
|
// --- Field 2: provider.key.models (models_json column) ---
|
|
// Rows with '[]' or empty string previously meant "allow all models"; migrate to '["*"]'.
|
|
if err := tx.Model(&tables.TableKey{}).
|
|
Where("models_json = ? OR models_json = ? OR models_json IS NULL", `[]`, ``).
|
|
Update("models_json", `["*"]`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill key models_json: %w", err)
|
|
}
|
|
|
|
// Recompute config_hash for all keys since models_json is part of the hash input.
|
|
var keys []tables.TableKey
|
|
if err := tx.Find(&keys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch keys for hash recomputation: %w", err)
|
|
}
|
|
for _, key := range keys {
|
|
schemaKey := schemas.Key{
|
|
Name: key.Name,
|
|
Value: key.Value,
|
|
Models: key.Models,
|
|
Weight: getWeight(key.Weight),
|
|
AzureKeyConfig: key.AzureKeyConfig,
|
|
VertexKeyConfig: key.VertexKeyConfig,
|
|
BedrockKeyConfig: key.BedrockKeyConfig,
|
|
Aliases: key.Aliases,
|
|
VLLMKeyConfig: key.VLLMKeyConfig,
|
|
ReplicateKeyConfig: key.ReplicateKeyConfig,
|
|
OllamaKeyConfig: key.OllamaKeyConfig,
|
|
SGLKeyConfig: key.SGLKeyConfig,
|
|
Enabled: key.Enabled,
|
|
UseForBatchAPI: key.UseForBatchAPI,
|
|
}
|
|
hash, err := GenerateKeyHash(schemaKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for key %s: %w", key.Name, err)
|
|
}
|
|
if err := tx.Model(&key).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for key %s: %w", key.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
// Rollback is intentionally a no-op: reverting ["*"] back to [] would
|
|
// re-introduce the ambiguous "empty = allow all" semantics on downgrade.
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running backfill_allowed_models_wildcard migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPClientAllowedExtraHeadersJSONColumn adds the allowed_extra_headers_json column to the mcp_client table
|
|
func migrationAddMCPClientAllowedExtraHeadersJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_client_allowed_extra_headers_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "allowed_extra_headers_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "allowed_extra_headers_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "allowed_extra_headers_json") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "allowed_extra_headers_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_mcp_client_allowed_extra_headers_json_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPluginOrderColumns adds placement and exec_order columns to config_plugins table
|
|
func migrationAddPluginOrderColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_plugin_order_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "placement") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "Placement"); err != nil {
|
|
return fmt.Errorf("failed to add placement column: %w", err)
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TablePlugin{}, "exec_order") {
|
|
if err := migrator.AddColumn(&tables.TablePlugin{}, "Order"); err != nil {
|
|
return fmt.Errorf("failed to add exec_order column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TablePlugin{}, "placement") {
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "placement"); err != nil {
|
|
return fmt.Errorf("failed to drop placement column: %w", err)
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TablePlugin{}, "exec_order") {
|
|
if err := migrator.DropColumn(&tables.TablePlugin{}, "exec_order"); err != nil {
|
|
return fmt.Errorf("failed to drop exec_order column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_plugin_order_columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationMakeBasePricingColumnsNullable drops the NOT NULL constraint on
|
|
// input_cost_per_token and output_cost_per_token in governance_model_pricing,
|
|
// allowing models that only have non-token pricing (image, audio, video) to be
|
|
// stored without a placeholder zero value.
|
|
func migrationMakeBasePricingColumnsNullable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "make_base_pricing_columns_nullable",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
m := tx.Migrator()
|
|
if err := m.AlterColumn(&tables.TableModelPricing{}, "InputCostPerToken"); err != nil {
|
|
return fmt.Errorf("failed to alter input_cost_per_token: %w", err)
|
|
}
|
|
if err := m.AlterColumn(&tables.TableModelPricing{}, "OutputCostPerToken"); err != nil {
|
|
return fmt.Errorf("failed to alter output_cost_per_token: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running make_base_pricing_columns_nullable migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddAllowOnAllVirtualKeysColumn adds the allow_on_all_virtual_keys column to the mcp_client table
|
|
func migrationAddAllowOnAllVirtualKeysColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_allow_on_all_virtual_keys_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "allow_on_all_virtual_keys") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "allow_on_all_virtual_keys"); err != nil {
|
|
return fmt.Errorf("failed to add allow_on_all_virtual_keys column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "allow_on_all_virtual_keys") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "allow_on_all_virtual_keys"); err != nil {
|
|
return fmt.Errorf("failed to drop allow_on_all_virtual_keys column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_allow_on_all_virtual_keys_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddOpenAIConfigJSONColumn adds the open_ai_config_json column to the provider table
|
|
func migrationAddOpenAIConfigJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_open_ai_config_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableProvider{}, "open_ai_config_json") {
|
|
if err := migrator.AddColumn(&tables.TableProvider{}, "OpenAIConfigJSON"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableProvider{}, "open_ai_config_json") {
|
|
if err := migrator.DropColumn(&tables.TableProvider{}, "open_ai_config_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_open_ai_config_json_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPromptVariablesColumns adds variables_json column to prompt_sessions and prompt_versions
|
|
func migrationAddPromptVariablesColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_prompt_variables_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") {
|
|
if err := migrator.AddColumn(&tables.TablePromptSession{}, "VariablesJSON"); err != nil {
|
|
return fmt.Errorf("failed to add variables_json column to prompt_sessions: %w", err)
|
|
}
|
|
}
|
|
|
|
if !migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") {
|
|
if err := migrator.AddColumn(&tables.TablePromptVersion{}, "VariablesJSON"); err != nil {
|
|
return fmt.Errorf("failed to add variables_json column to prompt_versions: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") {
|
|
if err := migrator.DropColumn(&tables.TablePromptSession{}, "variables_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") {
|
|
if err := migrator.DropColumn(&tables.TablePromptVersion{}, "variables_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running add_prompt_variables_columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddKeyBlacklistedModelsJSONColumn adds blacklisted_models_json to config_keys
|
|
// for per-key model deny lists (JSON array of model ids, default []).
|
|
func migrationAddKeyBlacklistedModelsJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_key_blacklisted_models_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(&tables.TableKey{}, "blacklisted_models_json") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "blacklisted_models_json"); err != nil {
|
|
return fmt.Errorf("failed to add blacklisted_models_json column: %w", err)
|
|
}
|
|
}
|
|
if err := tx.Exec("UPDATE config_keys SET blacklisted_models_json = '[]' WHERE blacklisted_models_json IS NULL OR blacklisted_models_json = ''").Error; err != nil {
|
|
return fmt.Errorf("failed to backfill blacklisted_models_json: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableKey{}, "blacklisted_models_json") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "blacklisted_models_json"); err != nil {
|
|
return fmt.Errorf("failed to drop blacklisted_models_json column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_key_blacklisted_models_json_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddChainRuleColumnToRoutingRules adds chain_rule to routing_rules.
|
|
// When true, the routing engine re-evaluates the full rule set after this rule matches,
|
|
// using the resolved provider/model as the new context input.
|
|
func migrationAddChainRuleColumnToRoutingRules(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_chain_rule_column_to_routing_rules",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(&tables.TableRoutingRule{}, "chain_rule") {
|
|
if err := mg.AddColumn(&tables.TableRoutingRule{}, "chain_rule"); err != nil {
|
|
return fmt.Errorf("failed to add chain_rule column: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill config_hash for all existing routing rules.
|
|
// GenerateRoutingRuleHash now includes chain_rule, so existing hashes
|
|
// (computed without it) are stale and must be recomputed to avoid
|
|
// every rule appearing as changed after this upgrade.
|
|
var rules []tables.TableRoutingRule
|
|
if err := tx.Preload("Targets").Find(&rules).Error; err != nil {
|
|
return fmt.Errorf("failed to load routing rules for config_hash backfill: %w", err)
|
|
}
|
|
for _, rule := range rules {
|
|
hash, err := GenerateRoutingRuleHash(rule)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate config_hash for routing rule %s: %w", rule.ID, err)
|
|
}
|
|
if err := tx.Model(&tables.TableRoutingRule{}).Where("id = ?", rule.ID).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for routing rule %s: %w", rule.ID, err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableRoutingRule{}, "chain_rule") {
|
|
if err := mg.DropColumn(&tables.TableRoutingRule{}, "chain_rule"); err != nil {
|
|
return fmt.Errorf("failed to drop chain_rule column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_chain_rule_column_to_routing_rules migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddReplicateKeyConfigColumn adds the replicate_use_deployments_endpoint column to the key table
|
|
func migrationAddReplicateKeyConfigColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_replicate_key_config_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(&tables.TableKey{}, "replicate_use_deployments_endpoint") {
|
|
if err := mg.AddColumn(&tables.TableKey{}, "replicate_use_deployments_endpoint"); err != nil {
|
|
return err
|
|
}
|
|
// Backfill: Replicate keys that had deployments configured (now in aliases_json after
|
|
// migrationDropDeploymentColumnsAndAddAliases) were using the deployments endpoint.
|
|
trueVal := true
|
|
if err := tx.Model(&tables.TableKey{}).
|
|
Where("provider = ? AND aliases_json IS NOT NULL AND aliases_json != ? AND aliases_json != ?",
|
|
string(schemas.Replicate), "", "{}",
|
|
).
|
|
Update("ReplicateUseDeploymentsEndpoint", &trueVal).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Recompute config_hash for Replicate keys that were updated above,
|
|
// since replicate_use_deployments_endpoint is part of the hash input.
|
|
var affectedKeys []tables.TableKey
|
|
if err := tx.Where(
|
|
"provider = ? AND replicate_use_deployments_endpoint IS NOT NULL",
|
|
string(schemas.Replicate),
|
|
).Find(&affectedKeys).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch replicate keys for hash recomputation: %w", err)
|
|
}
|
|
for _, key := range affectedKeys {
|
|
schemaKey := schemas.Key{
|
|
Name: key.Name,
|
|
Value: key.Value,
|
|
Models: key.Models,
|
|
BlacklistedModels: key.BlacklistedModels,
|
|
Weight: getWeight(key.Weight),
|
|
AzureKeyConfig: key.AzureKeyConfig,
|
|
VertexKeyConfig: key.VertexKeyConfig,
|
|
BedrockKeyConfig: key.BedrockKeyConfig,
|
|
Aliases: key.Aliases,
|
|
VLLMKeyConfig: key.VLLMKeyConfig,
|
|
ReplicateKeyConfig: key.ReplicateKeyConfig,
|
|
Enabled: key.Enabled,
|
|
UseForBatchAPI: key.UseForBatchAPI,
|
|
}
|
|
hash, err := GenerateKeyHash(schemaKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for key %s: %w", key.Name, err)
|
|
}
|
|
if err := tx.Model(&key).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update config_hash for key %s: %w", key.Name, err)
|
|
}
|
|
log.Printf("[Migration] Recomputed config_hash for replicate key '%s' after replicate config backfill", key.Name)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableKey{}, "replicate_use_deployments_endpoint") {
|
|
if err := mg.DropColumn(&tables.TableKey{}, "replicate_use_deployments_endpoint"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_replicate_key_config_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddBudgetCalendarAlignedColumn was originally for adding calendar_aligned to governance_budgets.
|
|
// Calendar alignment is now a VK-level field (governance_virtual_keys.calendar_aligned) added in migrationAddMultiBudgetTables.
|
|
// This migration is kept as a no-op so the migrator doesn't try to re-run it.
|
|
func migrationAddBudgetCalendarAlignedColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_budget_calendar_aligned_column",
|
|
Migrate: func(tx *gorm.DB) error { return nil },
|
|
Rollback: func(tx *gorm.DB) error { return nil },
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_budget_calendar_aligned_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddRoutingChainMaxDepthColumn adds routing_chain_max_depth to the client config table.
|
|
// Defaults to 10, which is the built-in default for routing rule chain evaluation depth.
|
|
func migrationAddRoutingChainMaxDepthColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_routing_chain_max_depth_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasColumn(&tables.TableClientConfig{}, "routing_chain_max_depth") {
|
|
if err := mg.AddColumn(&tables.TableClientConfig{}, "routing_chain_max_depth"); err != nil {
|
|
return fmt.Errorf("failed to add routing_chain_max_depth column: %w", err)
|
|
}
|
|
// Recompute config_hash for all existing client configs that have one.
|
|
// RoutingChainMaxDepth is now included in the hash (when > 0), so without
|
|
// this recompute the stored hash would mismatch on every startup after upgrade.
|
|
var clientConfigs []tables.TableClientConfig
|
|
if err := tx.Find(&clientConfigs).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch client configs for hash recompute: %w", err)
|
|
}
|
|
for _, cc := range clientConfigs {
|
|
if cc.ConfigHash == "" {
|
|
continue // no stored hash to invalidate
|
|
}
|
|
depth := cc.RoutingChainMaxDepth
|
|
if depth == 0 {
|
|
// Should never happen, but just in case.
|
|
depth = 10 // DefaultRoutingChainMaxDepth
|
|
}
|
|
clientConfig := ClientConfig{
|
|
DropExcessRequests: cc.DropExcessRequests,
|
|
InitialPoolSize: cc.InitialPoolSize,
|
|
PrometheusLabels: cc.PrometheusLabels,
|
|
EnableLogging: cc.EnableLogging,
|
|
DisableContentLogging: cc.DisableContentLogging,
|
|
DisableDBPingsInHealth: cc.DisableDBPingsInHealth,
|
|
LogRetentionDays: cc.LogRetentionDays,
|
|
EnforceAuthOnInference: cc.EnforceAuthOnInference,
|
|
AllowDirectKeys: cc.AllowDirectKeys,
|
|
AllowedOrigins: cc.AllowedOrigins,
|
|
AllowedHeaders: cc.AllowedHeaders,
|
|
MaxRequestBodySizeMB: cc.MaxRequestBodySizeMB,
|
|
HideDeletedVirtualKeysInFilters: cc.HideDeletedVirtualKeysInFilters,
|
|
MCPAgentDepth: cc.MCPAgentDepth,
|
|
MCPToolExecutionTimeout: cc.MCPToolExecutionTimeout,
|
|
MCPCodeModeBindingLevel: cc.MCPCodeModeBindingLevel,
|
|
MCPToolSyncInterval: cc.MCPToolSyncInterval,
|
|
MCPDisableAutoToolInject: cc.MCPDisableAutoToolInject,
|
|
AsyncJobResultTTL: cc.AsyncJobResultTTL,
|
|
LoggingHeaders: cc.LoggingHeaders,
|
|
RequiredHeaders: cc.RequiredHeaders,
|
|
HeaderFilterConfig: cc.HeaderFilterConfig,
|
|
RoutingChainMaxDepth: depth,
|
|
}
|
|
newHash, err := clientConfig.GenerateClientConfigHash()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for client config %d: %w", cc.ID, err)
|
|
}
|
|
if err := tx.Model(&cc).Update("config_hash", newHash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for client config %d: %w", cc.ID, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableClientConfig{}, "routing_chain_max_depth") {
|
|
if err := mg.DropColumn(&tables.TableClientConfig{}, "routing_chain_max_depth"); err != nil {
|
|
return fmt.Errorf("failed to drop routing_chain_max_depth column: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_routing_chain_max_depth_column migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddModelCapabilityColumns adds model capability metadata columns to governance_model_pricing.
|
|
func migrationAddModelCapabilityColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_model_capability_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"context_length",
|
|
"max_input_tokens",
|
|
"max_output_tokens",
|
|
"architecture",
|
|
}
|
|
for _, column := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, column) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, column); err != nil {
|
|
return fmt.Errorf("failed to add %s column: %w", column, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"context_length",
|
|
"max_input_tokens",
|
|
"max_output_tokens",
|
|
"architecture",
|
|
}
|
|
for _, column := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, column) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, column); err != nil {
|
|
return fmt.Errorf("failed to drop %s column: %w", column, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_model_capability_columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddOllamaSGLConfigColumns adds ollama_url and sgl_url columns to the key table
|
|
func migrationAddOllamaSGLConfigColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_ollama_sgl_config_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableKey{}, "ollama_url") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "ollama_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableKey{}, "sgl_url") {
|
|
if err := migrator.AddColumn(&tables.TableKey{}, "sgl_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Backfill: for each ollama/sgl provider with a base_url, create a key
|
|
// with that URL and clear base_url from network_config.
|
|
var providers []tables.TableProvider
|
|
if err := tx.Where("name IN ?", []string{"ollama", "sgl"}).Find(&providers).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch ollama/sgl providers for URL backfill: %w", err)
|
|
}
|
|
for _, p := range providers {
|
|
if p.NetworkConfigJSON == "" {
|
|
continue
|
|
}
|
|
var nc schemas.NetworkConfig
|
|
if err := json.Unmarshal([]byte(p.NetworkConfigJSON), &nc); err != nil {
|
|
log.Printf("[Migration] Failed to parse network_config for provider %s (id=%d), skipping: %v", p.Name, p.ID, err)
|
|
continue
|
|
}
|
|
if nc.BaseURL == "" {
|
|
continue
|
|
}
|
|
|
|
// Create a new key with the provider's base_url
|
|
urlEnvVar := schemas.EnvVar{Val: nc.BaseURL}
|
|
enabled := true
|
|
weight := 1.0
|
|
newKey := tables.TableKey{
|
|
Provider: p.Name,
|
|
ProviderID: p.ID,
|
|
KeyID: uuid.NewString(),
|
|
Weight: &weight,
|
|
Enabled: &enabled,
|
|
Models: schemas.WhiteList{"*"},
|
|
}
|
|
if strings.ToLower(p.Name) == "ollama" {
|
|
newKey.Name = "Default Ollama Key"
|
|
newKey.OllamaKeyConfig = &schemas.OllamaKeyConfig{URL: urlEnvVar}
|
|
}
|
|
if strings.ToLower(p.Name) == "sgl" {
|
|
newKey.Name = "Default SGL Key"
|
|
newKey.SGLKeyConfig = &schemas.SGLKeyConfig{URL: urlEnvVar}
|
|
}
|
|
|
|
schemaKey := schemaKeyFromTableKey(newKey)
|
|
hash, err := GenerateKeyHash(schemaKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for new key on provider %s: %w", p.Name, err)
|
|
}
|
|
newKey.ConfigHash = hash
|
|
if err := tx.Create(&newKey).Error; err != nil {
|
|
return fmt.Errorf("failed to create key for provider %s: %w", p.Name, err)
|
|
}
|
|
log.Printf("[Migration] Created key '%s' for provider '%s' from network_config.base_url", newKey.Name, p.Name)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableKey{}, "ollama_url") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "ollama_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TableKey{}, "sgl_url") {
|
|
if err := migrator.DropColumn(&tables.TableKey{}, "sgl_url"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running ollama sgl key config columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMultiBudgetTables creates junction tables for multi-budget support and backfills existing data.
|
|
func migrationAddMultiBudgetTables(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_multi_budget_tables",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
// Add calendar_aligned to governance_virtual_keys (VK-level setting)
|
|
if !mg.HasColumn(&tables.TableVirtualKey{}, "calendar_aligned") {
|
|
if err := mg.AddColumn(&tables.TableVirtualKey{}, "CalendarAligned"); err != nil {
|
|
return fmt.Errorf("failed to add calendar_aligned column to governance_virtual_keys: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add FK columns on governance_budgets for multi-budget ownership
|
|
if !mg.HasColumn(&tables.TableBudget{}, "virtual_key_id") {
|
|
if err := mg.AddColumn(&tables.TableBudget{}, "VirtualKeyID"); err != nil {
|
|
return fmt.Errorf("failed to add virtual_key_id column to governance_budgets: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasColumn(&tables.TableBudget{}, "provider_config_id") {
|
|
if err := mg.AddColumn(&tables.TableBudget{}, "ProviderConfigID"); err != nil {
|
|
return fmt.Errorf("failed to add provider_config_id column to governance_budgets: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create indexes on the new FK columns (AddColumn doesn't create indexes from struct tags)
|
|
if !mg.HasIndex(&tables.TableBudget{}, "idx_governance_budgets_virtual_key_id") {
|
|
if err := mg.CreateIndex(&tables.TableBudget{}, "VirtualKeyID"); err != nil {
|
|
return fmt.Errorf("failed to create index on governance_budgets.virtual_key_id: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasIndex(&tables.TableBudget{}, "idx_governance_budgets_provider_config_id") {
|
|
if err := mg.CreateIndex(&tables.TableBudget{}, "ProviderConfigID"); err != nil {
|
|
return fmt.Errorf("failed to create index on governance_budgets.provider_config_id: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill: set virtual_key_id from legacy VK budget_id (if column still exists)
|
|
if mg.HasColumn(&tables.TableVirtualKey{}, "budget_id") {
|
|
if err := tx.Exec(`
|
|
UPDATE governance_budgets SET virtual_key_id = (
|
|
SELECT id FROM governance_virtual_keys
|
|
WHERE governance_virtual_keys.budget_id = governance_budgets.id
|
|
) WHERE virtual_key_id IS NULL AND EXISTS (
|
|
SELECT 1 FROM governance_virtual_keys
|
|
WHERE governance_virtual_keys.budget_id = governance_budgets.id
|
|
)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill VK budget virtual_key_id: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill: set provider_config_id from legacy PC budget_id (if column still exists)
|
|
if mg.HasColumn(&tables.TableVirtualKeyProviderConfig{}, "budget_id") {
|
|
if err := tx.Exec(`
|
|
UPDATE governance_budgets SET provider_config_id = (
|
|
SELECT id FROM governance_virtual_key_provider_configs
|
|
WHERE governance_virtual_key_provider_configs.budget_id = governance_budgets.id
|
|
) WHERE provider_config_id IS NULL AND EXISTS (
|
|
SELECT 1 FROM governance_virtual_key_provider_configs
|
|
WHERE governance_virtual_key_provider_configs.budget_id = governance_budgets.id
|
|
)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill PC budget provider_config_id: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill: copy calendar_aligned from legacy budget column to VK-level field
|
|
// (governance_budgets.calendar_aligned was added by add_budget_calendar_aligned_column on main)
|
|
if mg.HasColumn(&tables.TableBudget{}, "calendar_aligned") {
|
|
if err := tx.Exec(`
|
|
UPDATE governance_virtual_keys SET calendar_aligned = true
|
|
WHERE id IN (
|
|
SELECT DISTINCT virtual_key_id FROM governance_budgets
|
|
WHERE calendar_aligned = true AND virtual_key_id IS NOT NULL
|
|
) AND calendar_aligned = false
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill calendar_aligned from budgets to virtual keys: %w", err)
|
|
}
|
|
// Drop the legacy calendar_aligned column from governance_budgets.
|
|
// Plain column with no FK references — not a correctness risk if left behind,
|
|
// but log a warning so it's not invisible.
|
|
if err := tx.Exec("ALTER TABLE governance_budgets DROP COLUMN IF EXISTS calendar_aligned").Error; err != nil {
|
|
log.Printf("[Migration] warning: could not drop legacy calendar_aligned column from governance_budgets: %v", err)
|
|
}
|
|
}
|
|
|
|
// Drop legacy budget_id columns BEFORE creating FK constraints.
|
|
// On SQLite, ALTER TABLE RENAME propagates into FK references in other tables.
|
|
// If we create FK constraints on governance_budgets first, then rename the
|
|
// parent table during the legacy column drop (table rebuild), SQLite updates
|
|
// those FK references to point at the temporary backup table name.
|
|
if err := dropLegacyBudgetColumn(tx, "governance_virtual_keys"); err != nil {
|
|
return err
|
|
}
|
|
if err := dropLegacyBudgetColumn(tx, "governance_virtual_key_provider_configs"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create FK constraints with CASCADE delete (defined on parent structs).
|
|
// Must happen after legacy column drops so SQLite rename propagation
|
|
// cannot corrupt these FK references.
|
|
if !mg.HasConstraint(&tables.TableVirtualKey{}, "Budgets") {
|
|
if err := mg.CreateConstraint(&tables.TableVirtualKey{}, "Budgets"); err != nil {
|
|
return fmt.Errorf("failed to create FK constraint for VirtualKey -> Budgets: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasConstraint(&tables.TableVirtualKeyProviderConfig{}, "Budgets") {
|
|
if err := mg.CreateConstraint(&tables.TableVirtualKeyProviderConfig{}, "Budgets"); err != nil {
|
|
return fmt.Errorf("failed to create FK constraint for ProviderConfig -> Budgets: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableBudget{}, "virtual_key_id") {
|
|
if err := mg.DropColumn(&tables.TableBudget{}, "virtual_key_id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if mg.HasColumn(&tables.TableBudget{}, "provider_config_id") {
|
|
if err := mg.DropColumn(&tables.TableBudget{}, "provider_config_id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
// SQLite workaround: GORM's CreateConstraint rebuilds the table via DROP+RENAME
|
|
// inside a transaction. The DROP fails when other tables have FKs pointing at the
|
|
// target table and foreign_keys is ON. PRAGMA foreign_keys cannot be changed inside
|
|
// a transaction, so we disable it before the migrator opens its transaction.
|
|
// This only affects SQLite — Postgres supports ALTER TABLE ADD CONSTRAINT natively.
|
|
if db.Dialector.Name() == "sqlite" {
|
|
// PRAGMA foreign_keys is per-connection in SQLite. Pin the pool to a single
|
|
// connection so the PRAGMA and the migration transaction share the same one.
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
defer sqlDB.SetMaxOpenConns(0) // restore default
|
|
|
|
if err := db.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
|
|
return fmt.Errorf("failed to disable SQLite foreign keys: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
|
|
log.Fatalf("[Migration] FATAL: failed to re-enable SQLite foreign keys: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_multi_budget_tables migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddTeamBudgetsToBudgetsTable pivots team budgets from a single-FK on
|
|
// governance_teams.budget_id to multi-budget ownership via governance_budgets.team_id,
|
|
// mirroring how VK/ProviderConfig budgets were restructured in migrationAddMultiBudgetTables.
|
|
func migrationAddTeamBudgetsToBudgetsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_team_budgets_to_budgets_table",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
// Add team_id FK column on governance_budgets
|
|
if !mg.HasColumn(&tables.TableBudget{}, "team_id") {
|
|
if err := mg.AddColumn(&tables.TableBudget{}, "TeamID"); err != nil {
|
|
return fmt.Errorf("failed to add team_id column to governance_budgets: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create index on the new FK column (AddColumn doesn't create indexes from struct tags)
|
|
if !mg.HasIndex(&tables.TableBudget{}, "idx_governance_budgets_team_id") {
|
|
if err := mg.CreateIndex(&tables.TableBudget{}, "TeamID"); err != nil {
|
|
return fmt.Errorf("failed to create index on governance_budgets.team_id: %w", err)
|
|
}
|
|
}
|
|
|
|
// Backfill: set team_id from legacy governance_teams.budget_id (if column still exists)
|
|
if mg.HasColumn(&tables.TableTeam{}, "budget_id") {
|
|
// Preflight: raw SQL below bypasses TableBudget.BeforeSave (which now
|
|
// enforces exactly-one-of {TeamID, VirtualKeyID, ProviderConfigID}).
|
|
// Fail fast if any team-referenced budget is already owned by a VK or
|
|
// ProviderConfig, rather than silently producing a multi-owner row
|
|
// that would later be rejected by the hook on its next update.
|
|
var conflictCount int64
|
|
if err := tx.Raw(`
|
|
SELECT COUNT(*) FROM governance_budgets b
|
|
WHERE (b.virtual_key_id IS NOT NULL OR b.provider_config_id IS NOT NULL)
|
|
AND EXISTS (SELECT 1 FROM governance_teams t WHERE t.budget_id = b.id)
|
|
`).Scan(&conflictCount).Error; err != nil {
|
|
return fmt.Errorf("failed to check for multi-owner team budget conflicts: %w", err)
|
|
}
|
|
if conflictCount > 0 {
|
|
return fmt.Errorf(
|
|
"cannot migrate team budgets: %d budget row(s) referenced by a team are already owned by a virtual key or provider config; resolve manually before re-running",
|
|
conflictCount,
|
|
)
|
|
}
|
|
|
|
if err := tx.Exec(`
|
|
UPDATE governance_budgets SET team_id = (
|
|
SELECT id FROM governance_teams
|
|
WHERE governance_teams.budget_id = governance_budgets.id
|
|
) WHERE team_id IS NULL AND EXISTS (
|
|
SELECT 1 FROM governance_teams
|
|
WHERE governance_teams.budget_id = governance_budgets.id
|
|
)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to backfill team budget team_id: %w", err)
|
|
}
|
|
|
|
// Drop legacy budget_id column BEFORE creating FK constraint.
|
|
// On SQLite, ALTER TABLE RENAME propagates into FK references in other
|
|
// tables. Dropping first prevents the FK on governance_budgets.team_id
|
|
// from being corrupted by the table rebuild's rename step.
|
|
if err := dropLegacyBudgetColumn(tx, "governance_teams"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create FK constraint with CASCADE delete (defined on TableTeam.Budgets).
|
|
// Must happen after legacy column drop so SQLite rename propagation
|
|
// cannot corrupt this FK reference.
|
|
if !mg.HasConstraint(&tables.TableTeam{}, "Budgets") {
|
|
if err := mg.CreateConstraint(&tables.TableTeam{}, "Budgets"); err != nil {
|
|
return fmt.Errorf("failed to create FK constraint for Team -> Budgets: %w", err)
|
|
}
|
|
}
|
|
|
|
// Refresh config_hash for teams whose budgets just got linked. GenerateTeamHash
|
|
// now includes sorted budget IDs, so hashes written by the earlier
|
|
// migrationAddConfigHashColumn (which ran before budgets were associated)
|
|
// are stale and would cause phantom drift on the next config.json sync.
|
|
var teamsToRehash []tables.TableTeam
|
|
if err := tx.Preload("Budgets").Find(&teamsToRehash).Error; err != nil {
|
|
return fmt.Errorf("failed to fetch teams for hash refresh: %w", err)
|
|
}
|
|
for _, team := range teamsToRehash {
|
|
if len(team.Budgets) == 0 {
|
|
continue // hash did not change; skip
|
|
}
|
|
hash, err := GenerateTeamHash(team)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate hash for team %s: %w", team.ID, err)
|
|
}
|
|
if err := tx.Model(&tables.TableTeam{}).Where("id = ?", team.ID).Update("config_hash", hash).Error; err != nil {
|
|
return fmt.Errorf("failed to update hash for team %s: %w", team.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasColumn(&tables.TableBudget{}, "team_id") {
|
|
if err := mg.DropColumn(&tables.TableBudget{}, "team_id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
// SQLite workaround — same reasoning as migrationAddMultiBudgetTables.
|
|
if db.Dialector.Name() == "sqlite" {
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
defer sqlDB.SetMaxOpenConns(0)
|
|
|
|
if err := db.Exec("PRAGMA foreign_keys = OFF").Error; err != nil {
|
|
return fmt.Errorf("failed to disable SQLite foreign keys: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil {
|
|
log.Fatalf("[Migration] FATAL: failed to re-enable SQLite foreign keys: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_team_budgets_to_budgets_table migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPerUserOAuthTables adds the oauth_user_sessions and oauth_user_tokens tables
|
|
func migrationAddPerUserOAuthTables(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_per_user_oauth_tables",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if !mg.HasTable(&tables.TablePerUserOAuthClient{}) {
|
|
if err := mg.CreateTable(&tables.TablePerUserOAuthClient{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_per_user_clients table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasTable(&tables.TablePerUserOAuthSession{}) {
|
|
if err := mg.CreateTable(&tables.TablePerUserOAuthSession{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_per_user_sessions table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasTable(&tables.TablePerUserOAuthCode{}) {
|
|
if err := mg.CreateTable(&tables.TablePerUserOAuthCode{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_per_user_codes table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasTable(&tables.TableOauthUserToken{}) {
|
|
if err := mg.CreateTable(&tables.TableOauthUserToken{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_user_tokens table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasTable(&tables.TableOauthUserSession{}) {
|
|
if err := mg.CreateTable(&tables.TableOauthUserSession{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_user_sessions table: %w", err)
|
|
}
|
|
}
|
|
if !mg.HasTable(&tables.TablePerUserOAuthPendingFlow{}) {
|
|
if err := mg.CreateTable(&tables.TablePerUserOAuthPendingFlow{}); err != nil {
|
|
return fmt.Errorf("failed to create oauth_per_user_pending_flows table: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
for _, table := range []any{
|
|
&tables.TablePerUserOAuthPendingFlow{},
|
|
&tables.TablePerUserOAuthCode{},
|
|
&tables.TablePerUserOAuthSession{},
|
|
&tables.TablePerUserOAuthClient{},
|
|
&tables.TableOauthUserToken{},
|
|
&tables.TableOauthUserSession{},
|
|
} {
|
|
if mg.HasTable(table) {
|
|
if err := mg.DropTable(table); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_per_user_oauth_tables migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddMCPClientDiscoveredToolsColumns adds discovered_tools_json and tool_name_mapping_json columns to the mcp_client table
|
|
func migrationAddMCPClientDiscoveredToolsColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_mcp_client_discovered_tools_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "discovered_tools_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "discovered_tools_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !migrator.HasColumn(&tables.TableMCPClient{}, "tool_name_mapping_json") {
|
|
if err := migrator.AddColumn(&tables.TableMCPClient{}, "tool_name_mapping_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "discovered_tools_json") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "discovered_tools_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if migrator.HasColumn(&tables.TableMCPClient{}, "tool_name_mapping_json") {
|
|
if err := migrator.DropColumn(&tables.TableMCPClient{}, "tool_name_mapping_json"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_mcp_client_discovered_tools_columns migration: %s", err.Error())
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddPriorityTierPricingColumns adds pricing columns for the 272k token tier
|
|
// and the 200k priority variants.
|
|
func migrationAddPriorityTierPricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_priority_tier_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"input_cost_per_token_above_272k_tokens",
|
|
"input_cost_per_token_above_272k_tokens_priority",
|
|
"output_cost_per_token_above_272k_tokens",
|
|
"output_cost_per_token_above_272k_tokens_priority",
|
|
"cache_read_input_token_cost_above_272k_tokens",
|
|
"cache_read_input_token_cost_above_272k_tokens_priority",
|
|
"input_cost_per_token_above_200k_tokens_priority",
|
|
"output_cost_per_token_above_200k_tokens_priority",
|
|
"cache_read_input_token_cost_above_200k_tokens_priority",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"input_cost_per_token_above_272k_tokens",
|
|
"input_cost_per_token_above_272k_tokens_priority",
|
|
"output_cost_per_token_above_272k_tokens",
|
|
"output_cost_per_token_above_272k_tokens_priority",
|
|
"cache_read_input_token_cost_above_272k_tokens",
|
|
"cache_read_input_token_cost_above_272k_tokens_priority",
|
|
"input_cost_per_token_above_200k_tokens_priority",
|
|
"output_cost_per_token_above_200k_tokens_priority",
|
|
"cache_read_input_token_cost_above_200k_tokens_priority",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running priority tier pricing columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddFlexTierPricingColumns adds pricing columns for the flex service tier
|
|
func migrationAddFlexTierPricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_flex_tier_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_flex",
|
|
"output_cost_per_token_flex",
|
|
"cache_read_input_token_cost_flex",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
columns := []string{
|
|
"input_cost_per_token_flex",
|
|
"output_cost_per_token_flex",
|
|
"cache_read_input_token_cost_flex",
|
|
}
|
|
|
|
for _, field := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running flex tier pricing columns migration: %s", err.Error())
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddWhitelistedRoutesJSONColumn adds the whitelisted_routes_json column to the config_client table
|
|
func migrationAddWhitelistedRoutesJSONColumn(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_whitelisted_routes_json_column",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if !migrator.HasColumn(&tables.TableClientConfig{}, "whitelisted_routes_json") {
|
|
if err := migrator.AddColumn(&tables.TableClientConfig{}, "WhitelistedRoutesJSON"); err != nil {
|
|
return fmt.Errorf("failed to add whitelisted_routes_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
migrator := tx.Migrator()
|
|
|
|
if migrator.HasColumn(&tables.TableClientConfig{}, "whitelisted_routes_json") {
|
|
if err := migrator.DropColumn(&tables.TableClientConfig{}, "whitelisted_routes_json"); err != nil {
|
|
return fmt.Errorf("failed to drop whitelisted_routes_json column: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running whitelisted_routes_json migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationReplaceEnableLiteLLMWithCompatColumns replaces the single enable_litellm_fallbacks
|
|
// boolean with compat feature columns. If enable_litellm_fallbacks was true,
|
|
// only convert_text_to_chat is set to true (preserving the original behavior).
|
|
func migrationReplaceEnableLiteLLMWithCompatColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "replace_enable_litellm_with_compat_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mig := tx.Migrator()
|
|
|
|
// Add new columns
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_convert_text_to_chat") {
|
|
if err := mig.AddColumn(&tables.TableClientConfig{}, "compat_convert_text_to_chat"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_convert_chat_to_responses") {
|
|
if err := mig.AddColumn(&tables.TableClientConfig{}, "compat_convert_chat_to_responses"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_should_drop_params") {
|
|
if err := mig.AddColumn(&tables.TableClientConfig{}, "compat_should_drop_params"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_should_convert_params") {
|
|
if err := mig.AddColumn(&tables.TableClientConfig{}, "compat_should_convert_params"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := tx.Exec("UPDATE config_client SET compat_should_convert_params = FALSE").Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Migrate data: if enable_litellm_fallbacks was true, set convert_text_to_chat = true
|
|
if mig.HasColumn(&tables.TableClientConfig{}, "enable_litellm_fallbacks") {
|
|
if err := tx.Exec("UPDATE config_client SET compat_convert_text_to_chat = enable_litellm_fallbacks").Error; err != nil {
|
|
return err
|
|
}
|
|
if err := mig.DropColumn(&tables.TableClientConfig{}, "enable_litellm_fallbacks"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mig := tx.Migrator()
|
|
if tx.Migrator().HasColumn(&tables.TableClientConfig{}, "enable_litellm_fallbacks") {
|
|
if err := tx.Exec("ALTER TABLE config_client ADD COLUMN enable_litellm_fallbacks BOOLEAN DEFAULT FALSE").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if mig.HasColumn(&tables.TableClientConfig{}, "compat_convert_text_to_chat") {
|
|
if err := tx.Exec("UPDATE config_client SET enable_litellm_fallbacks = COALESCE(compat_convert_text_to_chat, FALSE)").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, col := range []string{
|
|
"compat_convert_text_to_chat",
|
|
"compat_convert_chat_to_responses",
|
|
"compat_should_drop_params",
|
|
"compat_should_convert_params",
|
|
} {
|
|
if mig.HasColumn(&tables.TableClientConfig{}, col) {
|
|
if err := mig.DropColumn(&tables.TableClientConfig{}, col); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error while running replace_enable_litellm_with_compat_columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationDefaultCompatShouldConvertParamsFalse ensures existing deployments
|
|
// converge to the new default for compat_should_convert_params. The earlier
|
|
// compat migration may already be marked as applied, so changing its body is not
|
|
// sufficient for installed databases.
|
|
func migrationDefaultCompatShouldConvertParamsFalse(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "default_compat_should_convert_params_false",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mig := tx.Migrator()
|
|
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_should_convert_params") {
|
|
return nil
|
|
}
|
|
|
|
if err := tx.Exec("UPDATE config_client SET compat_should_convert_params = FALSE").Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := mig.AlterColumn(&tables.TableClientConfig{}, "CompatShouldConvertParams"); err != nil {
|
|
return fmt.Errorf("failed to alter compat_should_convert_params default: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mig := tx.Migrator()
|
|
|
|
if !mig.HasColumn(&tables.TableClientConfig{}, "compat_should_convert_params") {
|
|
return nil
|
|
}
|
|
|
|
switch tx.Dialector.Name() {
|
|
case "postgres":
|
|
if err := tx.Exec("ALTER TABLE config_client ALTER COLUMN compat_should_convert_params SET DEFAULT FALSE").Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running default_compat_should_convert_params_false migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationAddModelPricingUniqueIndex ensures the composite unique index (model, provider, mode)
|
|
// exists on governance_model_pricing so that atomic ON CONFLICT upserts work correctly.
|
|
func migrationAddModelPricingUniqueIndex(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_model_pricing_unique_index",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
|
|
// Remove duplicate rows before creating the unique index.
|
|
// The old find-then-insert path could have produced duplicates on
|
|
// multinode deployments, and CREATE UNIQUE INDEX will fail on a table
|
|
// that still contains them. Keep the row with the lowest ID for each
|
|
// (model, provider, mode) combination.
|
|
result := tx.Exec(`
|
|
DELETE FROM governance_model_pricing
|
|
WHERE id NOT IN (
|
|
SELECT MIN(id)
|
|
FROM governance_model_pricing
|
|
GROUP BY model, provider, mode
|
|
)
|
|
`)
|
|
if result.Error != nil {
|
|
return fmt.Errorf("failed to deduplicate model pricing rows: %w", result.Error)
|
|
}
|
|
if result.RowsAffected > 0 {
|
|
log.Printf("[migration] removed %d duplicate row(s) from governance_model_pricing before creating unique index", result.RowsAffected)
|
|
}
|
|
|
|
if !mg.HasIndex(&tables.TableModelPricing{}, "idx_model_provider_mode") {
|
|
if err := mg.CreateIndex(&tables.TableModelPricing{}, "idx_model_provider_mode"); err != nil {
|
|
return fmt.Errorf("failed to create unique index idx_model_provider_mode: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
if mg.HasIndex(&tables.TableModelPricing{}, "idx_model_provider_mode") {
|
|
if err := mg.DropIndex(&tables.TableModelPricing{}, "idx_model_provider_mode"); err != nil {
|
|
return fmt.Errorf("failed to drop unique index idx_model_provider_mode: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_model_pricing_unique_index migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrationNormalizeOtelTraceType rewrites the legacy OTEL plugin trace_type value "otel" to "genai_extension".
|
|
// No-op if the plugin row is missing or trace_type is already correct.
|
|
func migrationNormalizeOtelTraceType(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "normalize_otel_trace_type",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
|
|
var plugin tables.TablePlugin
|
|
err := tx.Where("name = ?", "otel").First(&plugin).Error
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to load otel plugin row: %w", err)
|
|
}
|
|
|
|
cfgMap, ok := plugin.Config.(map[string]any)
|
|
if !ok || len(cfgMap) == 0 {
|
|
return nil
|
|
}
|
|
if tt, _ := cfgMap["trace_type"].(string); tt != "otel" {
|
|
return nil
|
|
}
|
|
|
|
cfgMap["trace_type"] = "genai_extension"
|
|
plugin.Config = cfgMap
|
|
plugin.ConfigJSON = ""
|
|
plugin.EncryptionStatus = tables.EncryptionStatusPlainText
|
|
|
|
if err := tx.Save(&plugin).Error; err != nil {
|
|
return fmt.Errorf("failed to save normalized otel config: %w", err)
|
|
}
|
|
log.Printf("[Migration] Normalized otel trace_type 'otel' to 'genai_extension'")
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error { return nil },
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running normalize_otel_trace_type migration: %s", err.Error())
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrateCalendarAlignedToBudgetsAndRateLimitsTable
|
|
func migrateCalendarAlignedToBudgetsAndRateLimitsTable(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "migrate_calendar_aligned",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mig := tx.Migrator()
|
|
// Adding columns first
|
|
if !mig.HasColumn(&tables.TableBudget{}, "calendar_aligned") {
|
|
if err := mig.AddColumn(&tables.TableBudget{}, "calendar_aligned"); err != nil {
|
|
return fmt.Errorf("failed to add calendar_aligned column to budgets: %w", err)
|
|
}
|
|
}
|
|
// Adding columns first
|
|
if !mig.HasColumn(&tables.TableRateLimit{}, "calendar_aligned") {
|
|
if err := mig.AddColumn(&tables.TableRateLimit{}, "calendar_aligned"); err != nil {
|
|
return fmt.Errorf("failed to add calendar_aligned column to rate_limits: %w", err)
|
|
}
|
|
}
|
|
// Prefill calendar_aligned for existing budgets and rate_limits attached to virtual keys.
|
|
// Use subquery-based raw SQL (compatible with both PostgreSQL and SQLite) to avoid
|
|
// "cached plan must not change result type" (SQLSTATE 0A000): earlier migrations in
|
|
// the same run added columns to these tables, invalidating pgx's prepared-statement cache.
|
|
if err := tx.Exec(`
|
|
UPDATE governance_rate_limits
|
|
SET calendar_aligned = true
|
|
WHERE id IN (
|
|
SELECT rate_limit_id FROM governance_virtual_keys
|
|
WHERE calendar_aligned = true AND rate_limit_id IS NOT NULL
|
|
)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to propagate calendar_aligned to rate limits: %w", err)
|
|
}
|
|
if err := tx.Exec(`
|
|
UPDATE governance_budgets
|
|
SET calendar_aligned = true
|
|
WHERE virtual_key_id IN (
|
|
SELECT id FROM governance_virtual_keys WHERE calendar_aligned = true
|
|
)
|
|
`).Error; err != nil {
|
|
return fmt.Errorf("failed to propagate calendar_aligned to budgets: %w", err)
|
|
}
|
|
log.Printf("[Migration] Prefilled calendar_aligned field for existing budgets and rate limits")
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error { return nil },
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running migrate_calendar_aligned migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrationAddOCRPricingColumns(ctx context.Context, db *gorm.DB) error {
|
|
m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
|
|
ID: "add_ocr_pricing_columns",
|
|
Migrate: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"ocr_cost_per_page",
|
|
"annotation_cost_per_page",
|
|
}
|
|
for _, field := range columns {
|
|
if !mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.AddColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to add column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Rollback: func(tx *gorm.DB) error {
|
|
tx = tx.WithContext(ctx)
|
|
mg := tx.Migrator()
|
|
columns := []string{
|
|
"ocr_cost_per_page",
|
|
"annotation_cost_per_page",
|
|
}
|
|
for _, field := range columns {
|
|
if mg.HasColumn(&tables.TableModelPricing{}, field) {
|
|
if err := mg.DropColumn(&tables.TableModelPricing{}, field); err != nil {
|
|
return fmt.Errorf("failed to drop column %s: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}})
|
|
if err := m.Migrate(); err != nil {
|
|
return fmt.Errorf("error running add_ocr_pricing_columns migration: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|