mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
refactor(api): split Provider into ProviderGroup and APIKey models
Restructure the provider management system by separating the monolithic Provider model into two distinct entities: - ProviderGroup: defines shared upstream configuration (type, base_url, google settings, models, status) - APIKey: represents individual credentials within a group (api_key, weight, status, auto_ban, ban settings) This change also updates: - Binding model to reference GroupID instead of RouteGroup string - All CRUD handlers for the new provider-group and api-key endpoints - Sync service to rebuild provider snapshots from joined tables - Model registry to aggregate capabilities across group/key pairs - Access handler to validate namespace existence and subset constraints - Migration importer to handle the new schema structure - All related tests to use the new model relationships BREAKING CHANGE: Provider API endpoints replaced with /provider-groups and /api-keys endpoints; Binding.RouteGroup replaced with Binding.GroupID
This commit is contained in:
@@ -18,7 +18,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ez-api/ez-api/internal/model"
|
||||
groupx "github.com/ez-api/foundation/group"
|
||||
"github.com/ez-api/foundation/jsoncodec"
|
||||
"github.com/ez-api/foundation/modelcap"
|
||||
"github.com/ez-api/foundation/routing"
|
||||
@@ -373,6 +372,33 @@ type upstreamCap struct {
|
||||
SupportsTools boolVal
|
||||
}
|
||||
|
||||
func boolValEqual(a, b boolVal) bool {
|
||||
if a.Known != b.Known {
|
||||
return false
|
||||
}
|
||||
if !a.Known {
|
||||
return true
|
||||
}
|
||||
return a.Val == b.Val
|
||||
}
|
||||
|
||||
func intValEqual(a, b intVal) bool {
|
||||
if a.Known != b.Known {
|
||||
return false
|
||||
}
|
||||
if !a.Known {
|
||||
return true
|
||||
}
|
||||
return a.Val == b.Val
|
||||
}
|
||||
|
||||
func capsEqual(a, b upstreamCap) bool {
|
||||
return boolValEqual(a.SupportsVision, b.SupportsVision) &&
|
||||
boolValEqual(a.SupportsTools, b.SupportsTools) &&
|
||||
intValEqual(a.ContextWindow, b.ContextWindow) &&
|
||||
intValEqual(a.MaxOutputTokens, b.MaxOutputTokens)
|
||||
}
|
||||
|
||||
type modelsDevRegistry struct {
|
||||
ByProviderModel map[string]upstreamCap // key: providerID|modelID
|
||||
ByModel map[string]upstreamCap // fallback: modelID
|
||||
@@ -707,9 +733,13 @@ func (a *capAgg) finalize(name string) modelcap.Model {
|
||||
}
|
||||
|
||||
func (s *ModelRegistryService) buildBindingModels(ctx context.Context, reg *modelsDevRegistry) (map[string]modelcap.Model, map[string]string, error) {
|
||||
var providers []model.Provider
|
||||
if err := s.db.Find(&providers).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("load providers: %w", err)
|
||||
var groups []model.ProviderGroup
|
||||
if err := s.db.Find(&groups).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("load provider groups: %w", err)
|
||||
}
|
||||
var apiKeys []model.APIKey
|
||||
if err := s.db.Find(&apiKeys).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("load api keys: %w", err)
|
||||
}
|
||||
var bindings []model.Binding
|
||||
if err := s.db.Find(&bindings).Error; err != nil {
|
||||
@@ -718,21 +748,29 @@ func (s *ModelRegistryService) buildBindingModels(ctx context.Context, reg *mode
|
||||
|
||||
type providerLite struct {
|
||||
id uint
|
||||
group string
|
||||
ptype string
|
||||
models []string
|
||||
}
|
||||
providersByGroup := make(map[string][]providerLite)
|
||||
providersByGroupID := make(map[uint]providerLite)
|
||||
now := time.Now().Unix()
|
||||
for _, p := range providers {
|
||||
if strings.TrimSpace(p.Status) != "" && strings.TrimSpace(p.Status) != "active" {
|
||||
activeKeys := make(map[uint]bool)
|
||||
for _, k := range apiKeys {
|
||||
if strings.TrimSpace(k.Status) != "" && strings.TrimSpace(k.Status) != "active" {
|
||||
continue
|
||||
}
|
||||
if p.BanUntil != nil && p.BanUntil.UTC().Unix() > now {
|
||||
if k.BanUntil != nil && k.BanUntil.UTC().Unix() > now {
|
||||
continue
|
||||
}
|
||||
group := groupx.Normalize(p.Group)
|
||||
rawModels := strings.Split(p.Models, ",")
|
||||
activeKeys[k.GroupID] = true
|
||||
}
|
||||
for _, g := range groups {
|
||||
if strings.TrimSpace(g.Status) != "" && strings.TrimSpace(g.Status) != "active" {
|
||||
continue
|
||||
}
|
||||
if !activeKeys[g.ID] {
|
||||
continue
|
||||
}
|
||||
rawModels := strings.Split(g.Models, ",")
|
||||
var outModels []string
|
||||
for _, m := range rawModels {
|
||||
m = strings.TrimSpace(m)
|
||||
@@ -740,19 +778,20 @@ func (s *ModelRegistryService) buildBindingModels(ctx context.Context, reg *mode
|
||||
outModels = append(outModels, m)
|
||||
}
|
||||
}
|
||||
if group == "" || len(outModels) == 0 {
|
||||
if len(outModels) == 0 {
|
||||
continue
|
||||
}
|
||||
providersByGroup[group] = append(providersByGroup[group], providerLite{
|
||||
id: p.ID,
|
||||
group: group,
|
||||
ptype: strings.TrimSpace(p.Type),
|
||||
providersByGroupID[g.ID] = providerLite{
|
||||
id: g.ID,
|
||||
ptype: strings.TrimSpace(g.Type),
|
||||
models: outModels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
modelsOut := make(map[string]modelcap.Model)
|
||||
payloads := make(map[string]string)
|
||||
capBaseline := make(map[string]upstreamCap)
|
||||
capBaselineOK := make(map[string]bool)
|
||||
|
||||
for _, b := range bindings {
|
||||
if strings.TrimSpace(b.Status) != "" && strings.TrimSpace(b.Status) != "active" {
|
||||
@@ -764,12 +803,8 @@ func (s *ModelRegistryService) buildBindingModels(ctx context.Context, reg *mode
|
||||
continue
|
||||
}
|
||||
key := ns + "." + pm
|
||||
rg := groupx.Normalize(b.RouteGroup)
|
||||
if rg == "" {
|
||||
continue
|
||||
}
|
||||
pgroup := providersByGroup[rg]
|
||||
if len(pgroup) == 0 {
|
||||
group := providersByGroupID[b.GroupID]
|
||||
if group.id == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -782,13 +817,25 @@ func (s *ModelRegistryService) buildBindingModels(ctx context.Context, reg *mode
|
||||
selectorType := routing.SelectorType(strings.TrimSpace(b.SelectorType))
|
||||
selectorValue := strings.TrimSpace(b.SelectorValue)
|
||||
|
||||
for _, p := range pgroup {
|
||||
up, err := routing.ResolveUpstreamModel(selectorType, selectorValue, pm, p.models)
|
||||
if err != nil {
|
||||
continue
|
||||
up, err := routing.ResolveUpstreamModel(selectorType, selectorValue, pm, group.models)
|
||||
if err == nil {
|
||||
cap, ok := lookupModelsDevCap(reg, modelsDevProviderKey(group.ptype), up)
|
||||
if baseOK, seen := capBaselineOK[key]; seen {
|
||||
if !ok || !baseOK || !capsEqual(capBaseline[key], cap) {
|
||||
return nil, nil, fmt.Errorf("bindingKey %s has inconsistent capabilities", key)
|
||||
}
|
||||
} else {
|
||||
capBaselineOK[key] = ok
|
||||
if ok {
|
||||
capBaseline[key] = cap
|
||||
}
|
||||
}
|
||||
cap, ok := lookupModelsDevCap(reg, modelsDevProviderKey(p.ptype), up)
|
||||
agg.merge(cap, ok)
|
||||
} else {
|
||||
if _, seen := capBaselineOK[key]; seen {
|
||||
return nil, nil, fmt.Errorf("bindingKey %s has inconsistent capabilities", key)
|
||||
}
|
||||
capBaselineOK[key] = false
|
||||
}
|
||||
|
||||
out := agg.finalize(key)
|
||||
|
||||
Reference in New Issue
Block a user