76ba500417
CI / docker (push) Successful in 2m4s
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
"github.com/rose_cat707/luminary/internal/store/cache"
|
|
)
|
|
|
|
type routeTarget struct {
|
|
ProviderID uint
|
|
ProviderKeyID uint // 0 = auto load balance
|
|
}
|
|
|
|
func (s *Service) resolveRoute(ingressID uint, modelName string) *routeTarget {
|
|
rules := s.cache.ListRoutingRules(ingressID)
|
|
var best *model.RoutingRule
|
|
for i := range rules {
|
|
r := rules[i]
|
|
if !r.Enabled {
|
|
continue
|
|
}
|
|
if !matchPattern(r.ModelPattern, modelName) {
|
|
continue
|
|
}
|
|
if best == nil || r.Priority > best.Priority {
|
|
best = &r
|
|
}
|
|
}
|
|
if best == nil {
|
|
return nil
|
|
}
|
|
return &routeTarget{ProviderID: best.ProviderID, ProviderKeyID: best.ProviderKeyID}
|
|
}
|
|
|
|
func matchPattern(pattern, model string) bool {
|
|
if pattern == "" || pattern == "*" {
|
|
return true
|
|
}
|
|
if strings.HasSuffix(pattern, "*") {
|
|
return strings.HasPrefix(model, strings.TrimSuffix(pattern, "*"))
|
|
}
|
|
return pattern == model
|
|
}
|
|
|
|
func (s *Service) findProvidersForModel(modelName string, allowed []string, category model.ProviderCategory, route *routeTarget) ([]model.Provider, error) {
|
|
if route != nil {
|
|
if p, ok := s.cache.GetProvider(route.ProviderID); ok && p.Category == category && p.Enabled {
|
|
if cache.MatchesList(allowed, p.Name) || cache.MatchesList(allowed, string(p.Type)) || cache.MatchesList(allowed, "*") {
|
|
return []model.Provider{p}, nil
|
|
}
|
|
}
|
|
}
|
|
var matched []model.Provider
|
|
for _, p := range s.cache.ListProviders() {
|
|
if p.Category != category || !model.IsCategorySupported(p.Category) || !p.Enabled {
|
|
continue
|
|
}
|
|
if !cache.MatchesList(allowed, p.Name) && !cache.MatchesList(allowed, string(p.Type)) {
|
|
continue
|
|
}
|
|
if s.providerSupportsModel(&p, modelName) {
|
|
matched = append(matched, p)
|
|
}
|
|
}
|
|
if len(matched) == 0 {
|
|
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
|
if p, ok := s.cache.GetProviderByName(parts[0]); ok && p.Category == category && p.Enabled {
|
|
if s.providerSupportsModel(&p, parts[1]) {
|
|
return []model.Provider{p}, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, errNoProvider(modelName)
|
|
}
|
|
return matched, nil
|
|
}
|
|
|
|
func (s *Service) providerSupportsModel(p *model.Provider, modelName string) bool {
|
|
models := s.cache.GetModels(p.ID)
|
|
if len(models) == 0 {
|
|
for _, k := range s.cache.ListProviderKeys(p.ID) {
|
|
allowed := cache.ParseJSONList(k.ModelsJSON)
|
|
if cache.MatchesList(allowed, modelName) || cache.MatchesList(allowed, "*") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for _, m := range models {
|
|
if m.Enabled && modelEntryMatches(m, modelName) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func errNoProvider(model string) error {
|
|
return &gatewayError{msg: "no provider found for model " + model}
|
|
}
|
|
|
|
type gatewayError struct{ msg string }
|
|
|
|
func (e *gatewayError) Error() string { return e.msg }
|