Skip to content
Merged
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
21 changes: 11 additions & 10 deletions framework/configstore/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -1415,14 +1415,15 @@ 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"`
PricingOverrides []tables.TablePricingOverride `json:"pricing_overrides,omitempty"`
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"`
ComplexityAnalyzerConfig *ComplexityAnalyzerConfig `json:"complexity_analyzer_config,omitempty"`
}
130 changes: 130 additions & 0 deletions framework/configstore/complexityconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package configstore

import (
"encoding/json"
"fmt"
"sort"
"strings"
)

// ComplexityTierBoundaries defines score thresholds for complexity tier classification.
type ComplexityTierBoundaries struct {
SimpleMedium float64 `json:"simple_medium"`
MediumComplex float64 `json:"medium_complex"`
ComplexReasoning float64 `json:"complex_reasoning"`
}

// Validate checks that tier boundaries are ordered and inside the analyzer score range.
func (b *ComplexityTierBoundaries) Validate() error {
if b == nil {
return nil
}
if !(0 < b.SimpleMedium &&
b.SimpleMedium < b.MediumComplex &&
b.MediumComplex < b.ComplexReasoning &&
b.ComplexReasoning < 1) {
return fmt.Errorf(
"tier boundaries must satisfy 0 < simple_medium (%.4f) < medium_complex (%.4f) < complex_reasoning (%.4f) < 1",
b.SimpleMedium, b.MediumComplex, b.ComplexReasoning,
)
}
return nil
}

// ComplexityEditableKeywordConfig contains the user-editable keyword lists.
type ComplexityEditableKeywordConfig struct {
CodeKeywords []string `json:"code_keywords"`
ReasoningKeywords []string `json:"reasoning_keywords"`
TechnicalKeywords []string `json:"technical_keywords"`
SimpleKeywords []string `json:"simple_keywords"`
}

// ComplexityAnalyzerConfig is the persisted runtime configuration for the complexity analyzer.
type ComplexityAnalyzerConfig struct {
TierBoundaries ComplexityTierBoundaries `json:"tier_boundaries"`
Keywords ComplexityEditableKeywordConfig `json:"keywords"`
}

// Validate checks that the config is internally consistent.
func (c *ComplexityAnalyzerConfig) Validate() error {
if c == nil {
return nil
}
if err := c.TierBoundaries.Validate(); err != nil {
return err
}

var missing []string
if len(c.Keywords.CodeKeywords) == 0 {
missing = append(missing, "code_keywords")
}
if len(c.Keywords.ReasoningKeywords) == 0 {
missing = append(missing, "reasoning_keywords")
}
if len(c.Keywords.TechnicalKeywords) == 0 {
missing = append(missing, "technical_keywords")
}
if len(c.Keywords.SimpleKeywords) == 0 {
missing = append(missing, "simple_keywords")
}
if len(missing) > 0 {
return fmt.Errorf("keyword lists must be non-empty: %s", strings.Join(missing, ", "))
}
return nil
}

// Normalized returns a canonical copy suitable for persistence and runtime use.
func (c *ComplexityAnalyzerConfig) Normalized() ComplexityAnalyzerConfig {
if c == nil {
return ComplexityAnalyzerConfig{}
}
return ComplexityAnalyzerConfig{
TierBoundaries: c.TierBoundaries,
Keywords: ComplexityEditableKeywordConfig{
CodeKeywords: normalizeComplexityKeywordList(c.Keywords.CodeKeywords),
ReasoningKeywords: normalizeComplexityKeywordList(c.Keywords.ReasoningKeywords),
TechnicalKeywords: normalizeComplexityKeywordList(c.Keywords.TechnicalKeywords),
SimpleKeywords: normalizeComplexityKeywordList(c.Keywords.SimpleKeywords),
},
}
}

// DecodeComplexityAnalyzerConfig decodes raw JSON into a normalized, validated config.
func DecodeComplexityAnalyzerConfig(data []byte) (*ComplexityAnalyzerConfig, error) {
if len(data) == 0 {
return nil, nil
}

var cfg ComplexityAnalyzerConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal complexity analyzer config: %w", err)
}

normalized := cfg.Normalized()
if err := normalized.Validate(); err != nil {
return nil, fmt.Errorf("invalid complexity analyzer config: %w", err)
}
return &normalized, nil
}

func normalizeComplexityKeywordList(values []string) []string {
if len(values) == 0 {
return nil
}

seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
normalized := strings.ToLower(strings.TrimSpace(value))
if normalized == "" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
out = append(out, normalized)
}
sort.Strings(out)
return out
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
71 changes: 61 additions & 10 deletions framework/configstore/rdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -4810,6 +4810,7 @@ func (s *RDBConfigStore) GetGovernanceConfig(ctx context.Context) (*GovernanceCo
return nil, nil
}
var authConfig *AuthConfig
var complexityAnalyzerConfig *ComplexityAnalyzerConfig
if len(governanceConfigs) > 0 {
// Checking if username and password is present
var username *string
Expand All @@ -4826,6 +4827,18 @@ func (s *RDBConfigStore) GetGovernanceConfig(ctx context.Context) (*GovernanceCo
isEnabled = entry.Value == "true"
case tables.ConfigDisableAuthOnInferenceKey:
disableAuthOnInference = entry.Value == "true"
case tables.ConfigComplexityAnalyzerConfigKey:
if strings.TrimSpace(entry.Value) == "" {
continue
}
decoded, err := DecodeComplexityAnalyzerConfig([]byte(entry.Value))
if err != nil {
if s.logger != nil {
s.logger.Warn("failed to load complexity analyzer config from governance_config: %v", err)
}
continue
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
complexityAnalyzerConfig = decoded
}
}
if username != nil && password != nil {
Expand All @@ -4838,19 +4851,57 @@ func (s *RDBConfigStore) GetGovernanceConfig(ctx context.Context) (*GovernanceCo
}
}
return &GovernanceConfig{
VirtualKeys: virtualKeys,
Teams: teams,
Customers: customers,
Budgets: budgets,
RateLimits: rateLimits,
ModelConfigs: modelConfigs,
Providers: providers,
RoutingRules: routingRules,
PricingOverrides: pricingOverrides,
AuthConfig: authConfig,
VirtualKeys: virtualKeys,
Teams: teams,
Customers: customers,
Budgets: budgets,
RateLimits: rateLimits,
ModelConfigs: modelConfigs,
Providers: providers,
RoutingRules: routingRules,
PricingOverrides: pricingOverrides,
AuthConfig: authConfig,
ComplexityAnalyzerConfig: complexityAnalyzerConfig,
}, nil
}

// GetComplexityAnalyzerConfig retrieves the typed complexity analyzer config.
func (s *RDBConfigStore) GetComplexityAnalyzerConfig(ctx context.Context) (*ComplexityAnalyzerConfig, error) {
configEntry, err := s.GetConfig(ctx, tables.ConfigComplexityAnalyzerConfigKey)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, nil
}
return nil, err
}
if configEntry == nil || strings.TrimSpace(configEntry.Value) == "" {
return nil, nil
}
return DecodeComplexityAnalyzerConfig([]byte(configEntry.Value))
}

// UpdateComplexityAnalyzerConfig normalizes, validates, and persists the typed analyzer config.
func (s *RDBConfigStore) UpdateComplexityAnalyzerConfig(ctx context.Context, config *ComplexityAnalyzerConfig, tx ...*gorm.DB) error {
if config == nil {
return fmt.Errorf("complexity analyzer config is nil")
}

normalized := config.Normalized()
if err := normalized.Validate(); err != nil {
return err
}

raw, err := json.Marshal(normalized)
if err != nil {
return fmt.Errorf("failed to marshal complexity analyzer config: %w", err)
}

return s.UpdateConfig(ctx, &tables.TableGovernanceConfig{
Key: tables.ConfigComplexityAnalyzerConfigKey,
Value: string(raw),
}, tx...)
}

// GetAuthConfig retrieves the auth configuration from the database.
func (s *RDBConfigStore) GetAuthConfig(ctx context.Context) (*AuthConfig, error) {
var username *string
Expand Down
120 changes: 120 additions & 0 deletions framework/configstore/rdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@ func setupRDBTestStore(t *testing.T) *RDBConfigStore {
&tables.TableKey{},
&tables.TableBudget{},
&tables.TableRateLimit{},
&tables.TableModelConfig{},
&tables.TableRoutingRule{},
&tables.TableRoutingTarget{},
&tables.TablePricingOverride{},
&tables.TableVirtualKey{},
&tables.TableVirtualKeyProviderConfig{},
&tables.TableVirtualKeyProviderConfigKey{},
&tables.TableModelConfig{},
&tables.TableCustomer{},
&tables.TableTeam{},
&tables.TableClientConfig{},
&tables.TableGovernanceConfig{},
&tables.TablePlugin{},
&tables.TableMCPClient{},
&tables.TableVirtualKeyMCPConfig{},
Expand Down Expand Up @@ -65,6 +70,121 @@ func setupRDBTestStore(t *testing.T) *RDBConfigStore {
return s
}

func testComplexityAnalyzerConfig() *ComplexityAnalyzerConfig {
return &ComplexityAnalyzerConfig{
TierBoundaries: ComplexityTierBoundaries{
SimpleMedium: 0.10,
MediumComplex: 0.30,
ComplexReasoning: 0.70,
},
Keywords: ComplexityEditableKeywordConfig{
CodeKeywords: []string{" Function ", "api", "API"},
ReasoningKeywords: []string{"tradeoffs"},
TechnicalKeywords: []string{"latency"},
SimpleKeywords: []string{"hello"},
},
}
}

func TestRDBConfigStore_ComplexityAnalyzerConfigRoundTrip(t *testing.T) {
store := setupRDBTestStore(t)
ctx := context.Background()

require.NoError(t, store.UpdateComplexityAnalyzerConfig(ctx, testComplexityAnalyzerConfig()))

got, err := store.GetComplexityAnalyzerConfig(ctx)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, ComplexityTierBoundaries{
SimpleMedium: 0.10,
MediumComplex: 0.30,
ComplexReasoning: 0.70,
}, got.TierBoundaries)
assert.Equal(t, []string{"api", "function"}, got.Keywords.CodeKeywords)
}

func TestRDBConfigStore_GetGovernanceConfigIncludesComplexityAnalyzerConfig(t *testing.T) {
store := setupRDBTestStore(t)
ctx := context.Background()

require.NoError(t, store.UpdateComplexityAnalyzerConfig(ctx, testComplexityAnalyzerConfig()))

governanceConfig, err := store.GetGovernanceConfig(ctx)
require.NoError(t, err)
require.NotNil(t, governanceConfig)
require.NotNil(t, governanceConfig.ComplexityAnalyzerConfig)
assert.Equal(t, 0.70, governanceConfig.ComplexityAnalyzerConfig.TierBoundaries.ComplexReasoning)
}

func TestRDBConfigStore_UpdateComplexityAnalyzerConfigRejectsInvalidConfig(t *testing.T) {
store := setupRDBTestStore(t)
ctx := context.Background()

tests := []struct {
name string
mutate func(*ComplexityAnalyzerConfig)
}{
{
name: "simple medium below minimum",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.TierBoundaries.SimpleMedium = -0.1
},
},
{
name: "medium complex at minimum",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.TierBoundaries.MediumComplex = 0
},
},
{
name: "complex reasoning at maximum",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.TierBoundaries.ComplexReasoning = 1.0
},
},
{
name: "boundaries out of order",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.TierBoundaries.ComplexReasoning = cfg.TierBoundaries.MediumComplex - 0.1
},
},
{
name: "empty code keywords",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.Keywords.CodeKeywords = nil
},
},
{
name: "empty reasoning keywords",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.Keywords.ReasoningKeywords = nil
},
},
{
name: "empty technical keywords",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.Keywords.TechnicalKeywords = nil
},
},
{
name: "empty simple keywords",
mutate: func(cfg *ComplexityAnalyzerConfig) {
cfg.Keywords.SimpleKeywords = nil
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
invalid := testComplexityAnalyzerConfig()
tt.mutate(invalid)

err := store.UpdateComplexityAnalyzerConfig(ctx, invalid)
require.Error(t, err)
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// =============================================================================
// Provider and Key Tests
// =============================================================================
Expand Down
Loading
Loading