feat(gateway): add debug logging with file storage and retention feat(gateway): add audit logging for user actions feat(gateway): add request ID tracking and rate limit headers feat(gateway): add model aliases and load balancing strategies feat(gateway): add config hot-reload via SIGHUP feat(gateway): add CORS support feat(gateway): add data export API and dashboard endpoints feat(gateway): add dashboard pages for audit and debug logs feat(gateway): add concurrent request limiting per token feat(gateway): add streaming timeout support feat(gateway): add migration support for new schema fields
144 lines
3 KiB
Go
144 lines
3 KiB
Go
package provider
|
|
|
|
import (
|
|
"math/rand"
|
|
"sort"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// LoadBalancer reorders routes for load distribution.
|
|
type LoadBalancer interface {
|
|
Reorder(routes []Route) []Route
|
|
}
|
|
|
|
// NewLoadBalancer creates a load balancer by strategy name.
|
|
func NewLoadBalancer(strategy string) LoadBalancer {
|
|
switch strategy {
|
|
case "round-robin":
|
|
return &RoundRobinBalancer{}
|
|
case "random":
|
|
return &RandomBalancer{}
|
|
case "least-cost":
|
|
return &LeastCostBalancer{}
|
|
default:
|
|
return &FirstBalancer{}
|
|
}
|
|
}
|
|
|
|
// FirstBalancer is a no-op that preserves original order.
|
|
type FirstBalancer struct{}
|
|
|
|
func (b *FirstBalancer) Reorder(routes []Route) []Route {
|
|
return routes
|
|
}
|
|
|
|
// RoundRobinBalancer rotates routes within same-priority groups.
|
|
type RoundRobinBalancer struct {
|
|
counter atomic.Uint64
|
|
}
|
|
|
|
func (b *RoundRobinBalancer) Reorder(routes []Route) []Route {
|
|
if len(routes) <= 1 {
|
|
return routes
|
|
}
|
|
|
|
result := make([]Route, len(routes))
|
|
copy(result, routes)
|
|
|
|
// Group by priority and rotate within each group
|
|
groups := groupByPriority(result)
|
|
idx := 0
|
|
count := b.counter.Add(1)
|
|
for _, group := range groups {
|
|
if len(group) > 1 {
|
|
offset := int(count) % len(group)
|
|
for j := 0; j < len(group); j++ {
|
|
result[idx] = group[(j+offset)%len(group)]
|
|
idx++
|
|
}
|
|
} else {
|
|
result[idx] = group[0]
|
|
idx++
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// RandomBalancer shuffles routes within same-priority groups.
|
|
type RandomBalancer struct{}
|
|
|
|
func (b *RandomBalancer) Reorder(routes []Route) []Route {
|
|
if len(routes) <= 1 {
|
|
return routes
|
|
}
|
|
|
|
result := make([]Route, len(routes))
|
|
copy(result, routes)
|
|
|
|
groups := groupByPriority(result)
|
|
idx := 0
|
|
for _, group := range groups {
|
|
rand.Shuffle(len(group), func(i, j int) {
|
|
group[i], group[j] = group[j], group[i]
|
|
})
|
|
for _, r := range group {
|
|
result[idx] = r
|
|
idx++
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// LeastCostBalancer sorts by price within same-priority groups.
|
|
type LeastCostBalancer struct{}
|
|
|
|
func (b *LeastCostBalancer) Reorder(routes []Route) []Route {
|
|
if len(routes) <= 1 {
|
|
return routes
|
|
}
|
|
|
|
result := make([]Route, len(routes))
|
|
copy(result, routes)
|
|
|
|
groups := groupByPriority(result)
|
|
idx := 0
|
|
for _, group := range groups {
|
|
sort.Slice(group, func(i, j int) bool {
|
|
costI := group[i].InputPrice + group[i].OutputPrice
|
|
costJ := group[j].InputPrice + group[j].OutputPrice
|
|
return costI < costJ
|
|
})
|
|
for _, r := range group {
|
|
result[idx] = r
|
|
idx++
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// groupByPriority splits routes into groups of same priority, preserving order.
|
|
func groupByPriority(routes []Route) [][]Route {
|
|
if len(routes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var groups [][]Route
|
|
currentPriority := routes[0].Priority
|
|
currentGroup := []Route{routes[0]}
|
|
|
|
for i := 1; i < len(routes); i++ {
|
|
if routes[i].Priority == currentPriority {
|
|
currentGroup = append(currentGroup, routes[i])
|
|
} else {
|
|
groups = append(groups, currentGroup)
|
|
currentPriority = routes[i].Priority
|
|
currentGroup = []Route{routes[i]}
|
|
}
|
|
}
|
|
groups = append(groups, currentGroup)
|
|
|
|
return groups
|
|
}
|