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
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
package provider
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"llm-gateway/internal/config"
|
|
)
|
|
|
|
// Route maps a model to a specific provider with pricing.
|
|
type Route struct {
|
|
Provider Provider
|
|
ProviderModel string
|
|
Priority int
|
|
InputPrice float64 // per 1M tokens
|
|
OutputPrice float64 // per 1M tokens
|
|
}
|
|
|
|
// Registry maps model names to provider routes.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
routes map[string][]Route
|
|
balancers map[string]LoadBalancer
|
|
aliases map[string]string // alias -> canonical name
|
|
order []string // preserves config order (canonical names only)
|
|
}
|
|
|
|
func NewRegistry(cfg *config.Config) (*Registry, error) {
|
|
r := &Registry{}
|
|
if err := r.buildFromConfig(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Registry) buildFromConfig(cfg *config.Config) error {
|
|
// Build providers
|
|
providers := make(map[string]Provider)
|
|
for _, pc := range cfg.Providers {
|
|
providers[pc.Name] = NewOpenAIProvider(pc.Name, pc.BaseURL, pc.APIKey, pc.Timeout)
|
|
}
|
|
|
|
// Build routes
|
|
routes := make(map[string][]Route)
|
|
balancers := make(map[string]LoadBalancer)
|
|
aliases := make(map[string]string)
|
|
order := make([]string, 0, len(cfg.Models))
|
|
|
|
for _, mc := range cfg.Models {
|
|
var modelRoutes []Route
|
|
for _, rc := range mc.Routes {
|
|
p, ok := providers[rc.Provider]
|
|
if !ok {
|
|
return fmt.Errorf("model %s: unknown provider %s", mc.Name, rc.Provider)
|
|
}
|
|
pc := cfg.ProviderByName(rc.Provider)
|
|
priority := pc.Priority
|
|
modelRoutes = append(modelRoutes, Route{
|
|
Provider: p,
|
|
ProviderModel: rc.Model,
|
|
Priority: priority,
|
|
InputPrice: rc.Pricing.Input,
|
|
OutputPrice: rc.Pricing.Output,
|
|
})
|
|
}
|
|
// Sort by priority (lower = higher priority)
|
|
sort.Slice(modelRoutes, func(i, j int) bool {
|
|
return modelRoutes[i].Priority < modelRoutes[j].Priority
|
|
})
|
|
routes[mc.Name] = modelRoutes
|
|
order = append(order, mc.Name)
|
|
|
|
// Load balancer
|
|
balancers[mc.Name] = NewLoadBalancer(mc.LoadBalancing)
|
|
|
|
// Register aliases
|
|
for _, alias := range mc.Aliases {
|
|
aliases[alias] = mc.Name
|
|
}
|
|
}
|
|
|
|
r.mu.Lock()
|
|
r.routes = routes
|
|
r.balancers = balancers
|
|
r.aliases = aliases
|
|
r.order = order
|
|
r.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reload rebuilds routes from new config. Used for hot-reload.
|
|
func (r *Registry) Reload(cfg *config.Config) error {
|
|
return r.buildFromConfig(cfg)
|
|
}
|
|
|
|
// Lookup returns the routes for a model name (resolving aliases).
|
|
func (r *Registry) Lookup(model string) ([]Route, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
// Resolve alias
|
|
canonical := model
|
|
if alias, ok := r.aliases[model]; ok {
|
|
canonical = alias
|
|
}
|
|
|
|
routes, ok := r.routes[canonical]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Apply load balancer
|
|
if balancer, ok := r.balancers[canonical]; ok {
|
|
routes = balancer.Reorder(routes)
|
|
}
|
|
|
|
return routes, true
|
|
}
|
|
|
|
// ModelNames returns all registered model names in config order (including aliases).
|
|
func (r *Registry) ModelNames() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
var names []string
|
|
for _, name := range r.order {
|
|
names = append(names, name)
|
|
}
|
|
// Add aliases
|
|
for alias := range r.aliases {
|
|
names = append(names, alias)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// RouteInfo exposes route details for dashboard display.
|
|
type RouteInfo struct {
|
|
ProviderName string `json:"provider_name"`
|
|
ProviderModel string `json:"provider_model"`
|
|
Priority int `json:"priority"`
|
|
InputPrice float64 `json:"input_price"`
|
|
OutputPrice float64 `json:"output_price"`
|
|
}
|
|
|
|
// ModelRouteInfo exposes a model and its routes for dashboard display.
|
|
type ModelRouteInfo struct {
|
|
Name string `json:"name"`
|
|
Aliases []string `json:"aliases,omitempty"`
|
|
Routes []RouteInfo `json:"routes"`
|
|
}
|
|
|
|
// AllRoutes returns all models and their routes in config order.
|
|
func (r *Registry) AllRoutes() []ModelRouteInfo {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
// Build reverse alias map
|
|
modelAliases := make(map[string][]string)
|
|
for alias, canonical := range r.aliases {
|
|
modelAliases[canonical] = append(modelAliases[canonical], alias)
|
|
}
|
|
|
|
results := make([]ModelRouteInfo, 0, len(r.order))
|
|
for _, name := range r.order {
|
|
routes := r.routes[name]
|
|
info := ModelRouteInfo{
|
|
Name: name,
|
|
Aliases: modelAliases[name],
|
|
}
|
|
for _, rt := range routes {
|
|
info.Routes = append(info.Routes, RouteInfo{
|
|
ProviderName: rt.Provider.Name(),
|
|
ProviderModel: rt.ProviderModel,
|
|
Priority: rt.Priority,
|
|
InputPrice: rt.InputPrice,
|
|
OutputPrice: rt.OutputPrice,
|
|
})
|
|
}
|
|
results = append(results, info)
|
|
}
|
|
return results
|
|
}
|