Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect
)
3 changes: 1 addition & 2 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
Expand Down
36 changes: 27 additions & 9 deletions framework/configstore/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,23 @@ func GenerateRoutingRuleHash(r tables.TableRoutingRule) (string, error) {
return hex.EncodeToString(hash.Sum(nil)), nil
}

// GeneratePricingOverrideHash generates a SHA256 hash for a pricing override.
// Skips: CreatedAt, UpdatedAt, ConfigHash (dynamic/meta fields).
func GeneratePricingOverrideHash(p tables.TablePricingOverride) (string, error) {
hash := sha256.New()
hash.Write([]byte(p.ID))
hash.Write([]byte(p.Name))
hash.Write([]byte(p.ScopeKind))
hash.Write([]byte(derefStr(p.VirtualKeyID)))
hash.Write([]byte(derefStr(p.ProviderID)))
hash.Write([]byte(derefStr(p.ProviderKeyID)))
hash.Write([]byte(p.MatchType))
hash.Write([]byte(p.Pattern))
hash.Write([]byte(p.RequestTypesJSON))
hash.Write([]byte(p.PricingPatchJSON))
return hex.EncodeToString(hash.Sum(nil)), nil
}

// GenerateMCPClientHash generates a SHA256 hash for an MCP client.
// This is used to detect changes to MCP clients between config.json and database.
// Skips: ID (autoIncrement), CreatedAt, UpdatedAt (dynamic fields)
Expand Down Expand Up @@ -1093,13 +1110,14 @@ type ConfigMap map[schemas.ModelProvider]ProviderConfig
// GovernanceConfig contains governance entities loaded from the config store or
// reconciled from config.json.
type GovernanceConfig struct {
VirtualKeys []tables.TableVirtualKey `json:"virtual_keys"`
Teams []tables.TableTeam `json:"teams"`
Customers []tables.TableCustomer `json:"customers"`
Budgets []tables.TableBudget `json:"budgets"`
RateLimits []tables.TableRateLimit `json:"rate_limits"`
ModelConfigs []tables.TableModelConfig `json:"model_configs"`
Providers []tables.TableProvider `json:"providers"`
RoutingRules []tables.TableRoutingRule `json:"routing_rules"`
AuthConfig *AuthConfig `json:"auth_config,omitempty"`
VirtualKeys []tables.TableVirtualKey `json:"virtual_keys"`
Teams []tables.TableTeam `json:"teams"`
Customers []tables.TableCustomer `json:"customers"`
Budgets []tables.TableBudget `json:"budgets"`
RateLimits []tables.TableRateLimit `json:"rate_limits"`
ModelConfigs []tables.TableModelConfig `json:"model_configs"`
Providers []tables.TableProvider `json:"providers"`
RoutingRules []tables.TableRoutingRule `json:"routing_rules"`
PricingOverrides []tables.TablePricingOverride `json:"pricing_overrides,omitempty"`
AuthConfig *AuthConfig `json:"auth_config,omitempty"`
}
1 change: 0 additions & 1 deletion framework/configstore/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,6 @@ func migrationAddStoreRawRequestResponseColumn(ctx context.Context, db *gorm.DB)
"concurrency_buffer_json",
"proxy_config_json",
"custom_provider_config_json",
"pricing_overrides_json",
"send_back_raw_request",
"send_back_raw_response",
"store_raw_request_response",
Expand Down
43 changes: 0 additions & 43 deletions framework/configstore/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import (
"testing"
"time"

"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/pricingoverrides"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
Expand Down Expand Up @@ -247,47 +245,6 @@ func TestFindUniqueName_NormalizationAndCollision(t *testing.T) {
assert.Contains(t, logOutput, "MCP Client Name Normalized: 'my-tool' -> 'my_tool2'", "Should log the full transformation")
}

func TestMigrationReconcilePricingOverridesTable_PreservesExistingRows(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)

err = db.AutoMigrate(&tables.TablePricingOverride{})
require.NoError(t, err)

inputCost := 1.25
override := tables.TablePricingOverride{
ID: "override-1",
Name: "Config Override",
ScopeKind: pricingoverrides.ScopeKindGlobal,
MatchType: pricingoverrides.MatchTypeExact,
Pattern: "gpt-4.1",
RequestTypes: []schemas.RequestType{schemas.ChatCompletionRequest},
Patch: pricingoverrides.Patch{
InputCostPerToken: &inputCost,
},
ConfigHash: "config-hash-1",
CreatedAt: time.Now().UTC().Round(time.Second),
UpdatedAt: time.Now().UTC().Round(time.Second),
}
require.NoError(t, db.Create(&override).Error)

require.NoError(t, db.Migrator().DropIndex(&tables.TablePricingOverride{}, "idx_pricing_override_match"))
require.False(t, db.Migrator().HasIndex(&tables.TablePricingOverride{}, "idx_pricing_override_match"))

require.NoError(t, migrationReconcilePricingOverridesTable(context.Background(), db))

var stored []tables.TablePricingOverride
require.NoError(t, db.Order("id").Find(&stored).Error)
require.Len(t, stored, 1)
assert.Equal(t, override.ID, stored[0].ID)
assert.Equal(t, override.Name, stored[0].Name)
require.NotNil(t, stored[0].Patch.InputCostPerToken)
assert.Equal(t, inputCost, *stored[0].Patch.InputCostPerToken)
assert.Equal(t, override.ConfigHash, stored[0].ConfigHash)
assert.True(t, db.Migrator().HasIndex(&tables.TablePricingOverride{}, "idx_pricing_override_scope"))
assert.True(t, db.Migrator().HasIndex(&tables.TablePricingOverride{}, "idx_pricing_override_match"))
}

func TestFindUniqueName_MultipleNormalizationsToSameBase(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
Expand Down
18 changes: 9 additions & 9 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -1302,20 +1302,20 @@ func (s *RDBConfigStore) DeleteModelPrices(ctx context.Context, tx ...*gorm.DB)
return txDB.WithContext(ctx).Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&tables.TableModelPricing{}).Error
}

func (s *RDBConfigStore) GetPricingOverrides(ctx context.Context, filter PricingOverrideFilter) ([]tables.TablePricingOverride, error) {
func (s *RDBConfigStore) GetPricingOverrides(ctx context.Context, filters PricingOverrideFilters) ([]tables.TablePricingOverride, error) {
var overrides []tables.TablePricingOverride
q := s.db.WithContext(ctx).Model(&tables.TablePricingOverride{})
if filter.ScopeKind != nil {
q = q.Where("scope_kind = ?", *filter.ScopeKind)
if filters.ScopeKind != nil {
q = q.Where("scope_kind = ?", *filters.ScopeKind)
}
if filter.VirtualKeyID != nil {
q = q.Where("virtual_key_id = ?", *filter.VirtualKeyID)
if filters.VirtualKeyID != nil {
q = q.Where("virtual_key_id = ?", *filters.VirtualKeyID)
}
if filter.ProviderID != nil {
q = q.Where("provider_id = ?", *filter.ProviderID)
if filters.ProviderID != nil {
q = q.Where("provider_id = ?", *filters.ProviderID)
}
if filter.ProviderKeyID != nil {
q = q.Where("provider_key_id = ?", *filter.ProviderKeyID)
if filters.ProviderKeyID != nil {
q = q.Where("provider_key_id = ?", *filters.ProviderKeyID)
}
if err := q.Order("created_at ASC").Find(&overrides).Error; err != nil {
return nil, s.parseGormError(err)
Expand Down
18 changes: 9 additions & 9 deletions framework/configstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/maximhq/bifrost/framework/configstore/tables"
"github.com/maximhq/bifrost/framework/logstore"
"github.com/maximhq/bifrost/framework/migrator"
"github.com/maximhq/bifrost/framework/pricingoverrides"
"github.com/maximhq/bifrost/framework/vectorstore"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -60,6 +59,14 @@ type CustomersQueryParams struct {
Search string
}

// PricingOverrideFilters holds the filters for pricing overrides.
type PricingOverrideFilters struct {
ScopeKind *string
VirtualKeyID *string
ProviderID *string
ProviderKeyID *string
}

// ConfigStore is the interface for the config store.
type ConfigStore interface {
// Health check
Expand Down Expand Up @@ -220,7 +227,7 @@ type ConfigStore interface {
DeleteModelPrices(ctx context.Context, tx ...*gorm.DB) error

// Governance pricing overrides CRUD
GetPricingOverrides(ctx context.Context, filter PricingOverrideFilter) ([]tables.TablePricingOverride, error)
GetPricingOverrides(ctx context.Context, filters PricingOverrideFilters) ([]tables.TablePricingOverride, error)
GetPricingOverrideByID(ctx context.Context, id string) (*tables.TablePricingOverride, error)
CreatePricingOverride(ctx context.Context, override *tables.TablePricingOverride, tx ...*gorm.DB) error
UpdatePricingOverride(ctx context.Context, override *tables.TablePricingOverride, tx ...*gorm.DB) error
Expand Down Expand Up @@ -317,13 +324,6 @@ type ConfigStore interface {
Close(ctx context.Context) error
}

type PricingOverrideFilter struct {
ScopeKind *pricingoverrides.ScopeKind
VirtualKeyID *string
ProviderID *string
ProviderKeyID *string
}

// NewConfigStore creates a new config store based on the configuration
func NewConfigStore(ctx context.Context, config *Config, logger schemas.Logger) (ConfigStore, error) {
if config == nil {
Expand Down
116 changes: 18 additions & 98 deletions framework/configstore/tables/pricingoverride.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,36 @@ package tables

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/pricingoverrides"
"gorm.io/gorm"
)

// TablePricingOverride is the persistence model for governance pricing
// overrides.
// TablePricingOverride is the persistence model for governance pricing overrides.
type TablePricingOverride struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ScopeKind pricingoverrides.ScopeKind `gorm:"type:varchar(50);index:idx_pricing_override_scope;not null" json:"scope_kind"`
VirtualKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"virtual_key_id,omitempty"`
ProviderID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_id,omitempty"`
ProviderKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_key_id,omitempty"`
MatchType pricingoverrides.MatchType `gorm:"type:varchar(20);index:idx_pricing_override_match;not null" json:"match_type"`
Pattern string `gorm:"type:varchar(255);not null" json:"pattern"`
RequestTypesJSON string `gorm:"type:text" json:"-"`
PricingPatchJSON string `gorm:"type:text" json:"-"`
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash,omitempty"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`

RequestTypes []schemas.RequestType `gorm:"-" json:"request_types,omitempty"`
Patch pricingoverrides.Patch `gorm:"-" json:"patch,omitempty"`
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ScopeKind string `gorm:"type:varchar(50);index:idx_pricing_override_scope;not null" json:"scope_kind"`
VirtualKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"virtual_key_id,omitempty"`
ProviderID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_id,omitempty"`
ProviderKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_key_id,omitempty"`
MatchType string `gorm:"type:varchar(20);index:idx_pricing_override_match;not null" json:"match_type"`
Pattern string `gorm:"type:varchar(255);not null" json:"pattern"`
RequestTypesJSON string `gorm:"type:text" json:"-"`
PricingPatchJSON string `gorm:"type:text" json:"pricing_patch,omitempty"`
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash,omitempty"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`

RequestTypes []schemas.RequestType `gorm:"-" json:"request_types,omitempty"`
}

// TableName returns the backing table name for governance pricing overrides.
func (TablePricingOverride) TableName() string { return "governance_pricing_overrides" }

// BeforeSave validates and serializes the sparse pricing override fields before
// the row is persisted.
// BeforeSave serializes virtual fields into their JSON columns before persistence.
func (p *TablePricingOverride) BeforeSave(tx *gorm.DB) error {
p.Name = strings.TrimSpace(p.Name)
if p.Name == "" {
return fmt.Errorf("name is required")
}

if err := pricingoverrides.ValidateScopeKind(p.ScopeKind, p.VirtualKeyID, p.ProviderID, p.ProviderKeyID); err != nil {
return err
}

normalizedPattern, err := pricingoverrides.ValidatePattern(p.MatchType, p.Pattern)
if err != nil {
return err
}
p.Pattern = normalizedPattern

if err := pricingoverrides.ValidateRequestTypes(p.RequestTypes); err != nil {
return err
}

if err := pricingoverrides.ValidatePatchNonNegative(p.Patch); err != nil {
return err
}

if len(p.RequestTypes) > 0 {
b, err := json.Marshal(p.RequestTypes)
if err != nil {
Expand All @@ -70,66 +41,15 @@ func (p *TablePricingOverride) BeforeSave(tx *gorm.DB) error {
} else {
p.RequestTypesJSON = ""
}

b, err := json.Marshal(p.Patch)
if err != nil {
return err
}
p.PricingPatchJSON = string(b)

return nil
}

// AfterFind restores the request type and patch fields from their persisted
// JSON columns.
// AfterFind restores virtual fields from their persisted JSON columns.
func (p *TablePricingOverride) AfterFind(tx *gorm.DB) error {
if p.RequestTypesJSON != "" {
if err := json.Unmarshal([]byte(p.RequestTypesJSON), &p.RequestTypes); err != nil {
return err
}
}
if p.PricingPatchJSON != "" {
if err := json.Unmarshal([]byte(p.PricingPatchJSON), &p.Patch); err != nil {
return err
}
}
return nil
}

// ToPricingOverride converts the persisted row into the shared pricing override
// contract used by runtime components.
func (p TablePricingOverride) ToPricingOverride() pricingoverrides.Override {
return pricingoverrides.Override{
ID: p.ID,
Name: p.Name,
ScopeKind: p.ScopeKind,
VirtualKeyID: p.VirtualKeyID,
ProviderID: p.ProviderID,
ProviderKeyID: p.ProviderKeyID,
MatchType: p.MatchType,
Pattern: p.Pattern,
RequestTypes: p.RequestTypes,
Patch: p.Patch,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
}

// TablePricingOverrideFromPricingOverride converts the shared runtime override
// contract into its persistence representation.
func TablePricingOverrideFromPricingOverride(override pricingoverrides.Override) TablePricingOverride {
return TablePricingOverride{
ID: override.ID,
Name: override.Name,
ScopeKind: override.ScopeKind,
VirtualKeyID: override.VirtualKeyID,
ProviderID: override.ProviderID,
ProviderKeyID: override.ProviderKeyID,
MatchType: override.MatchType,
Pattern: override.Pattern,
RequestTypes: override.RequestTypes,
Patch: override.Patch,
CreatedAt: override.CreatedAt,
UpdatedAt: override.UpdatedAt,
}
}
Loading