mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
240 lines
6.0 KiB
Go
240 lines
6.0 KiB
Go
// Package provider also provides the top-level registry that owns all sources,
|
|
// routes model requests (explicit or AUTO), and supports hot reload.
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Registry holds all configured providers and routes model requests.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
providers []*Provider
|
|
byModel map[string]*Provider // modelID -> provider
|
|
defaultM string // default model id ("" means AUTO)
|
|
}
|
|
|
|
func NewRegistry(providers []*Provider, defaultModel string) *Registry {
|
|
r := &Registry{byModel: map[string]*Provider{}, defaultM: defaultModel}
|
|
r.set(providers)
|
|
return r
|
|
}
|
|
|
|
// Replace atomically swaps the provider set (hot reload).
|
|
func (r *Registry) Replace(providers []*Provider) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.set(providers)
|
|
}
|
|
|
|
func (r *Registry) set(providers []*Provider) {
|
|
r.providers = providers
|
|
r.byModel = map[string]*Provider{}
|
|
for _, p := range providers {
|
|
for _, m := range p.Models() {
|
|
r.byModel[strings.ToLower(m)] = p
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Providers() []*Provider {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]*Provider, len(r.providers))
|
|
copy(out, r.providers)
|
|
return out
|
|
}
|
|
|
|
func (r *Registry) Count() int {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return len(r.providers)
|
|
}
|
|
|
|
// ModelList returns all exposed model ids (chat + image).
|
|
func (r *Registry) ModelList() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
seen := map[string]bool{}
|
|
for _, p := range r.providers {
|
|
for _, m := range p.Models() {
|
|
if !seen[m] {
|
|
seen[m] = true
|
|
}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for m := range seen {
|
|
out = append(out, m)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Resolve returns the ordered candidate providers to try for a request,
|
|
// honoring explicit model selection or AUTO (priority order, healthy first).
|
|
//
|
|
// model "" or "AUTO" -> all sources sorted by (priority desc, healthy first).
|
|
// Otherwise the owning provider, if healthy; else its source anyway.
|
|
func (r *Registry) Resolve(model string) []*Provider {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
model = strings.TrimSpace(model)
|
|
if model == "" || strings.EqualFold(model, "AUTO") {
|
|
// priority chain across all models
|
|
type cand struct {
|
|
prov *Provider
|
|
priority int
|
|
}
|
|
var cands []cand
|
|
seen := map[string]bool{}
|
|
for _, p := range r.providers {
|
|
prio := -1
|
|
for _, m := range p.cfg.Models {
|
|
if m.Priority > prio {
|
|
prio = m.Priority
|
|
}
|
|
}
|
|
if prio < 0 {
|
|
prio = 0
|
|
}
|
|
cands = append(cands, cand{p, prio})
|
|
seen[p.Name()] = true
|
|
}
|
|
sort.SliceStable(cands, func(i, j int) bool {
|
|
if cands[i].priority != cands[j].priority {
|
|
return cands[i].priority > cands[j].priority
|
|
}
|
|
// healthy preferred at same priority
|
|
return cands[i].prov.Available() && !cands[j].prov.Available()
|
|
})
|
|
out := make([]*Provider, 0, len(cands))
|
|
for _, c := range cands {
|
|
out = append(out, c.prov)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// explicit model
|
|
if p, ok := r.byModel[strings.ToLower(model)]; ok {
|
|
// switch to the owning source but pin the model via request
|
|
return []*Provider{p}
|
|
}
|
|
// unknown model -> fall back to default/AUTO chain
|
|
return r.AUTOChain()
|
|
}
|
|
|
|
// AUTOChain returns the priority-sorted providers for AUTO.
|
|
func (r *Registry) AUTOChain() []*Provider {
|
|
return r.Resolve("AUTO")
|
|
}
|
|
|
|
// ProviderForModel returns the provider owning the model id (nil if unknown).
|
|
func (r *Registry) ProviderForModel(model string) *Provider {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
p, ok := r.byModel[strings.ToLower(strings.TrimSpace(model))]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return p
|
|
}
|
|
|
|
// ProviderForSlot returns the provider for a (model, source) slot. When
|
|
// source is empty it behaves like ProviderForModel (owner of the model id);
|
|
// when source is set it returns only that exact source (nil if the source
|
|
// does not serve the model).
|
|
func (r *Registry) ProviderForSlot(model, source string) *Provider {
|
|
model = strings.ToLower(strings.TrimSpace(model))
|
|
source = strings.TrimSpace(source)
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
if source == "" {
|
|
p, ok := r.byModel[model]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return p
|
|
}
|
|
for _, p := range r.providers {
|
|
if !strings.EqualFold(p.Name(), source) {
|
|
continue
|
|
}
|
|
for _, m := range p.cfg.Models {
|
|
if strings.EqualFold(m.ID, model) {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Default returns the highest-priority available provider.
|
|
func (r *Registry) Default() *Provider {
|
|
chain := r.AUTOChain()
|
|
if len(chain) == 0 {
|
|
return nil
|
|
}
|
|
return chain[0]
|
|
}
|
|
|
|
// ModelStatus is a web-UI friendly snapshot per source.
|
|
type SourceStatus struct {
|
|
Name string `json:"name"`
|
|
Adapter string `json:"adapter"`
|
|
BaseURL string `json:"base_url"`
|
|
Models []string `json:"models"`
|
|
Available bool `json:"available"`
|
|
Healthy bool `json:"healthy"`
|
|
MaxConcurrent int `json:"max_concurrent"`
|
|
LiveAvailable bool `json:"live_available"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
LastChecked int64 `json:"last_checked,omitempty"`
|
|
}
|
|
|
|
// ProbeAll runs a live reachability check for every provider (in parallel).
|
|
func (r *Registry) ProbeAll(ctx context.Context) {
|
|
r.mu.RLock()
|
|
providers := append([]*Provider(nil), r.providers...)
|
|
r.mu.RUnlock()
|
|
var wg sync.WaitGroup
|
|
for _, p := range providers {
|
|
wg.Add(1)
|
|
go func(p *Provider) {
|
|
defer wg.Done()
|
|
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
p.Probe(probeCtx)
|
|
}(p)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func (r *Registry) Status() []SourceStatus {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]SourceStatus, 0, len(r.providers))
|
|
for _, p := range r.providers {
|
|
live, lastErr, lastAt := p.LastProbe()
|
|
s := SourceStatus{
|
|
Name: p.Name(),
|
|
Adapter: p.Adapter(),
|
|
BaseURL: p.Config().BaseURL,
|
|
Models: p.Models(),
|
|
Available: p.Available(),
|
|
Healthy: p.Available(),
|
|
MaxConcurrent: p.MaxConcurrent(),
|
|
LiveAvailable: live,
|
|
LastError: lastErr,
|
|
LastChecked: lastAt,
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
} |