Skip to content

Commit 9360f73

Browse files
feat: added multi routing engine support
1 parent 77cee39 commit 9360f73

File tree

20 files changed

+218
-53
lines changed

20 files changed

+218
-53
lines changed

core/schemas/bifrost.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ const (
187187
BifrostContextKeyIsCustomProvider BifrostContextKey = "bifrost-is-custom-provider" // bool (set by bifrost - DO NOT SET THIS MANUALLY))
188188
BifrostContextKeyHTTPRequestType BifrostContextKey = "bifrost-http-request-type" // RequestType (set by bifrost - DO NOT SET THIS MANUALLY))
189189
BifrostContextKeyPassthroughExtraParams BifrostContextKey = "bifrost-passthrough-extra-params" // bool
190-
BifrostContextKeyRoutingEngineUsed BifrostContextKey = "bifrost-routing-engine-used" // string (set by bifrost - DO NOT SET THIS MANUALLY) - either "routing-rule", "governance" or "loadbalancing"
190+
BifrostContextKeyRoutingEnginesUsed BifrostContextKey = "bifrost-routing-engines-used" // []string (set by bifrost - DO NOT SET THIS MANUALLY) - list of routing engines used ("routing-rule", "governance", "loadbalancing", etc.)
191191
)
192192

193193
// NOTE: for custom plugin implementation dealing with streaming short circuit,

core/utils.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,22 @@ func ValidateExternalURL(urlStr string) error {
415415
return nil
416416
}
417417

418+
// AppendToContext appends a value to the context list value.
419+
// Parameters:
420+
// - ctx: The Bifrost context
421+
// - key: The key to append the value to
422+
// - value: The value to append
423+
func AppendToContextList[T any](ctx *schemas.BifrostContext, key schemas.BifrostContextKey, value T) {
424+
if ctx == nil {
425+
return
426+
}
427+
existingValues, ok := ctx.Value(key).([]T)
428+
if !ok {
429+
existingValues = []T{}
430+
}
431+
ctx.SetValue(key, append(existingValues, value))
432+
}
433+
418434
// isLocalhost checks if a hostname is localhost or a loopback address
419435
func isLocalhost(hostname string) bool {
420436
return hostname == "localhost" ||

docs/features/telemetry.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Base Labels:
6262
- `method`: Request type (`chat`, `text`, `embedding`, `speech`, `transcription`)
6363
- `virtual_key_id`: Virtual key ID
6464
- `virtual_key_name`: Virtual key name
65-
- `routing_engine_used`: Routing engine used ("routing-rule", "governance", "loadbalancing")
65+
- `routing_engines_used`: Comma-separated routing engines used ("routing-rule", "governance", "loadbalancing")
6666
- `routing_rule_id`: Routing rule ID that matched the request
6767
- `routing_rule_name`: Routing rule name that matched the request
6868
- `selected_key_id`: Selected key ID

docs/openapi/openapi.json

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146579,9 +146579,12 @@
146579146579
"type": "string",
146580146580
"nullable": true
146581146581
},
146582-
"routing_engine_used": {
146583-
"type": "string",
146584-
"description": "The routing engine used for this request (routing-rule, governance, or loadbalancing)",
146582+
"routing_engines_used": {
146583+
"type": "array",
146584+
"items": {
146585+
"type": "string"
146586+
},
146587+
"description": "Array of routing engines used for this request (routing-rule, governance, or loadbalancing)",
146585146588
"nullable": true
146586146589
},
146587146590
"routing_rule_id": {
@@ -179768,9 +179771,12 @@
179768179771
"type": "string",
179769179772
"nullable": true
179770179773
},
179771-
"routing_engine_used": {
179772-
"type": "string",
179773-
"description": "The routing engine used for this request (routing-rule, governance, or loadbalancing)",
179774+
"routing_engines_used": {
179775+
"type": "array",
179776+
"items": {
179777+
"type": "string"
179778+
},
179779+
"description": "Array of routing engines used for this request (routing-rule, governance, or loadbalancing)",
179774179780
"nullable": true
179775179781
},
179776179782
"routing_rule_id": {
@@ -181285,9 +181291,12 @@
181285181291
"type": "string",
181286181292
"nullable": true
181287181293
},
181288-
"routing_engine_used": {
181289-
"type": "string",
181290-
"description": "The routing engine used for this request (routing-rule, governance, or loadbalancing)",
181294+
"routing_engines_used": {
181295+
"type": "array",
181296+
"items": {
181297+
"type": "string"
181298+
},
181299+
"description": "Array of routing engines used for this request (routing-rule, governance, or loadbalancing)",
181291181300
"nullable": true
181292181301
},
181293181302
"routing_rule_id": {

docs/openapi/schemas/management/logging.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ LogEntry:
3737
virtual_key_name:
3838
type: string
3939
nullable: true
40-
routing_engine_used:
41-
type: string
42-
description: The routing engine used for this request (routing-rule, governance, or loadbalancing)
40+
routing_engines_used:
41+
type: array
42+
items:
43+
type: string
44+
description: Array of routing engines used for this request (routing-rule, governance, or loadbalancing)
4345
nullable: true
4446
routing_rule_id:
4547
type: string

framework/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- feat: rename routing_engine_used column to routing_engines_used for multi-engine tracking (parsed as comma-separated string)

framework/logstore/migrations.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error {
127127
if err := migrationAddRoutingEngineUsedColumn(ctx, db); err != nil {
128128
return err
129129
}
130+
if err := migrationAddRoutingEnginesUsedColumn(ctx, db); err != nil {
131+
return err
132+
}
130133
return nil
131134
}
132135

@@ -1060,8 +1063,10 @@ func migrationAddRoutingEngineUsedColumn(ctx context.Context, db *gorm.DB) error
10601063
Migrate: func(tx *gorm.DB) error {
10611064
tx = tx.WithContext(ctx)
10621065
migrator := tx.Migrator()
1063-
if !migrator.HasColumn(&Log{}, "routing_engine_used") {
1064-
if err := migrator.AddColumn(&Log{}, "routing_engine_used"); err != nil {
1066+
// Only add the column if it doesn't exist
1067+
if !migrator.HasColumn(&Log{}, "routing_engine_used") && !migrator.HasColumn(&Log{}, "routing_engines_used") {
1068+
// Use raw SQL to avoid GORM struct field dependency
1069+
if err := tx.Exec("ALTER TABLE logs ADD COLUMN routing_engine_used VARCHAR(255)").Error; err != nil {
10651070
return err
10661071
}
10671072
}
@@ -1084,3 +1089,52 @@ func migrationAddRoutingEngineUsedColumn(ctx context.Context, db *gorm.DB) error
10841089
}
10851090
return nil
10861091
}
1092+
1093+
func migrationAddRoutingEnginesUsedColumn(ctx context.Context, db *gorm.DB) error {
1094+
opts := *migrator.DefaultOptions
1095+
opts.UseTransaction = true
1096+
m := migrator.New(db, &opts, []*migrator.Migration{{
1097+
ID: "logs_add_routing_engines_used_column",
1098+
Migrate: func(tx *gorm.DB) error {
1099+
tx = tx.WithContext(ctx)
1100+
migrator := tx.Migrator()
1101+
1102+
hasOldColumn := migrator.HasColumn(&Log{}, "routing_engine_used")
1103+
hasNewColumn := migrator.HasColumn(&Log{}, "routing_engines_used")
1104+
1105+
if hasOldColumn && !hasNewColumn {
1106+
// Rename old column to new if new doesn't exist yet
1107+
if err := migrator.RenameColumn(&Log{}, "routing_engine_used", "routing_engines_used"); err != nil {
1108+
return fmt.Errorf("failed to rename routing_engine_used to routing_engines_used: %w", err)
1109+
}
1110+
} else if hasOldColumn && hasNewColumn {
1111+
// Both columns exist - drop the old one (new column is already in use)
1112+
if err := migrator.DropColumn(&Log{}, "routing_engine_used"); err != nil {
1113+
return fmt.Errorf("failed to drop old routing_engine_used column: %w", err)
1114+
}
1115+
}
1116+
// If only new column exists, do nothing (already migrated)
1117+
1118+
return nil
1119+
},
1120+
Rollback: func(tx *gorm.DB) error {
1121+
tx = tx.WithContext(ctx)
1122+
migrator := tx.Migrator()
1123+
1124+
hasNewColumn := migrator.HasColumn(&Log{}, "routing_engines_used")
1125+
hasOldColumn := migrator.HasColumn(&Log{}, "routing_engine_used")
1126+
1127+
if hasNewColumn && !hasOldColumn {
1128+
// Rename new column back to old if old doesn't exist
1129+
if err := migrator.RenameColumn(&Log{}, "routing_engines_used", "routing_engine_used"); err != nil {
1130+
return fmt.Errorf("failed to rename routing_engines_used back to routing_engine_used: %w", err)
1131+
}
1132+
}
1133+
// If old column was dropped, recreate it would be complex, so we skip
1134+
1135+
return nil
1136+
},
1137+
}})
1138+
1139+
return m.Migrate()
1140+
}

framework/logstore/rdb.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66
"errors"
77
"fmt"
8+
"strings"
89
"time"
910

1011
"github.com/maximhq/bifrost/core/schemas"
@@ -65,7 +66,36 @@ func (s *RDBLogStore) applyFilters(baseQuery *gorm.DB, filters SearchFilters) *g
6566
baseQuery = baseQuery.Where("routing_rule_id IN ?", filters.RoutingRuleIDs)
6667
}
6768
if len(filters.RoutingEngineUsed) > 0 {
68-
baseQuery = baseQuery.Where("routing_engine_used IN ?", filters.RoutingEngineUsed)
69+
// Query routing engines (comma-separated values) - find logs containing ANY of the specified engines
70+
// Use delimiter-aware matching to avoid partial token matches
71+
var engineConditions []string
72+
var engineArgs []interface{}
73+
74+
// Use dialect-aware concatenation expression
75+
dialect := s.db.Dialector.Name()
76+
var concatExpr string
77+
switch dialect {
78+
case "sqlite":
79+
// SQLite: use || operator for string concatenation
80+
concatExpr = "',' || routing_engines_used || ','"
81+
default:
82+
// MySQL, Postgres, and others: use CONCAT function
83+
concatExpr = "CONCAT(',', routing_engines_used, ',')"
84+
}
85+
86+
for _, engine := range filters.RoutingEngineUsed {
87+
engine = strings.TrimSpace(engine)
88+
if engine == "" {
89+
continue // Skip empty engine filters
90+
}
91+
// Match whole comma-separated tokens: expr LIKE '%,engine,%'
92+
engineConditions = append(engineConditions, concatExpr+" LIKE ?")
93+
engineArgs = append(engineArgs, "%,"+engine+",%")
94+
}
95+
// Build OR condition: (expr LIKE ? OR expr LIKE ? ...)
96+
if len(engineConditions) > 0 {
97+
baseQuery = baseQuery.Where(strings.Join(engineConditions, " OR "), engineArgs...)
98+
}
6999
}
70100
if filters.StartTime != nil {
71101
baseQuery = baseQuery.Where("timestamp >= ?", *filters.StartTime)

framework/logstore/tables.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ type Log struct {
9090
SelectedKeyName string `gorm:"type:varchar(255)" json:"selected_key_name"`
9191
VirtualKeyID *string `gorm:"type:varchar(255);index:idx_logs_virtual_key_id" json:"virtual_key_id"`
9292
VirtualKeyName *string `gorm:"type:varchar(255)" json:"virtual_key_name"`
93-
RoutingEngineUsed *string `gorm:"type:varchar(255);index:idx_logs_routing_engine_used" json:"routing_engine_used"`
93+
RoutingEnginesUsedStr *string `gorm:"type:varchar(255);column:routing_engines_used" json:"-"` // Comma-separated routing engines
9494
RoutingRuleID *string `gorm:"type:varchar(255);index:idx_logs_routing_rule_id" json:"routing_rule_id"`
9595
RoutingRuleName *string `gorm:"type:varchar(255)" json:"routing_rule_name"`
9696
InputHistory string `gorm:"type:text" json:"-"` // JSON serialized []schemas.ChatMessage
@@ -126,6 +126,7 @@ type Log struct {
126126
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
127127

128128
// Virtual fields for JSON output - these will be populated when needed
129+
RoutingEnginesUsed []string `gorm:"-" json:"routing_engines_used,omitempty"` // Virtual field deserialized from JSON
129130
InputHistoryParsed []schemas.ChatMessage `gorm:"-" json:"input_history,omitempty"`
130131
ResponsesInputHistoryParsed []schemas.ResponsesMessage `gorm:"-" json:"responses_input_history,omitempty"`
131132
OutputMessageParsed *schemas.ChatMessage `gorm:"-" json:"output_message,omitempty"`
@@ -189,6 +190,14 @@ func (l *Log) AfterFind(tx *gorm.DB) error {
189190

190191
// SerializeFields converts Go structs to JSON strings for storage
191192
func (l *Log) SerializeFields() error {
193+
// Serialize routing engines to comma-separated string
194+
if len(l.RoutingEnginesUsed) > 0 {
195+
engineStr := strings.Join(l.RoutingEnginesUsed, ",")
196+
l.RoutingEnginesUsedStr = &engineStr
197+
} else {
198+
l.RoutingEnginesUsedStr = nil
199+
}
200+
192201
if l.InputHistoryParsed != nil {
193202
if data, err := json.Marshal(l.InputHistoryParsed); err != nil {
194203
return err
@@ -457,6 +466,13 @@ func (l *Log) DeserializeFields() error {
457466
}
458467
}
459468

469+
if l.RoutingEnginesUsedStr != nil && *l.RoutingEnginesUsedStr != "" {
470+
// Parse comma-separated routing engines
471+
l.RoutingEnginesUsed = strings.Split(*l.RoutingEnginesUsedStr, ",")
472+
} else {
473+
l.RoutingEnginesUsed = []string{}
474+
}
475+
460476
return nil
461477
}
462478

plugins/governance/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- feat: added multi level routing support for routing rules + vk based provider routing

0 commit comments

Comments
 (0)