mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
feat(auto): AUTO-only priority chain with tiered slots + live source probing; CSV export w/ key names; audit persistence; fix prompt token accounting & deepseek thinking
This commit is contained in:
@ -57,9 +57,14 @@ type Provider struct {
|
||||
adapter string
|
||||
client *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
sem chan struct{}
|
||||
health health
|
||||
mu sync.Mutex
|
||||
sem chan struct{}
|
||||
health health
|
||||
lastProbe struct {
|
||||
ok bool
|
||||
err string
|
||||
at int64
|
||||
}
|
||||
}
|
||||
|
||||
func New(cfg config.Source, vm *lua.VM) *Provider {
|
||||
@ -175,6 +180,48 @@ func (p *Provider) Available() bool {
|
||||
return p.health.available()
|
||||
}
|
||||
|
||||
// Probe performs a lightweight reachability + auth check against the source
|
||||
// using its best chat model (1-token). It records the result for Status().
|
||||
func (p *Provider) Probe(ctx context.Context) (bool, string) {
|
||||
ok := false
|
||||
msg := ""
|
||||
model := p.bestChatModel()
|
||||
if pm := p.ModelByID(model); pm != nil && pm.Kind == "image" {
|
||||
model = ""
|
||||
}
|
||||
if model == "" {
|
||||
if ms := p.Models(); len(ms) > 0 {
|
||||
model = ms[0]
|
||||
}
|
||||
}
|
||||
if model != "" {
|
||||
_, err := p.Chat(ctx, &types.ChatRequest{
|
||||
Model: model,
|
||||
Messages: []types.ChatMessage{{Role: "user", Content: types.StringContent("hi")}},
|
||||
MaxTokens: 1,
|
||||
})
|
||||
if err == nil {
|
||||
ok = true
|
||||
} else {
|
||||
msg = err.Error()
|
||||
}
|
||||
} else {
|
||||
msg = "no chat model configured"
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.lastProbe.ok = ok
|
||||
p.lastProbe.err = msg
|
||||
p.lastProbe.at = time.Now().Unix()
|
||||
p.mu.Unlock()
|
||||
return ok, msg
|
||||
}
|
||||
|
||||
func (p *Provider) LastProbe() (bool, string, int64) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.lastProbe.ok, p.lastProbe.err, p.lastProbe.at
|
||||
}
|
||||
|
||||
// ReportStatus records an upstream HTTP status for backoff decisions.
|
||||
func (p *Provider) ReportStatus(code int) {
|
||||
p.mu.Lock()
|
||||
@ -331,21 +378,23 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
||||
}()
|
||||
|
||||
ch := make(chan types.UnifiedChunk, 64)
|
||||
sel := <-rc
|
||||
if sel.err != nil {
|
||||
p.reportError()
|
||||
p.Release()
|
||||
return nil, sel.err
|
||||
}
|
||||
if sel.resp.StatusCode != 200 {
|
||||
raw, _ := io.ReadAll(sel.resp.Body)
|
||||
sel.resp.Body.Close()
|
||||
p.ReportStatus(sel.resp.StatusCode)
|
||||
p.Release()
|
||||
return nil, fmt.Errorf("api error %d: %s", sel.resp.StatusCode, truncate(string(raw), 500))
|
||||
}
|
||||
go func() {
|
||||
defer p.Release()
|
||||
defer close(ch)
|
||||
sel := <-rc
|
||||
if sel.err != nil {
|
||||
p.reportError()
|
||||
return
|
||||
}
|
||||
defer sel.resp.Body.Close()
|
||||
if sel.resp.StatusCode != 200 {
|
||||
raw, _ := io.ReadAll(sel.resp.Body)
|
||||
p.ReportStatus(sel.resp.StatusCode)
|
||||
_ = raw
|
||||
return
|
||||
}
|
||||
scanner := bufio.NewScanner(sel.resp.Body)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Registry holds all configured providers and routes model requests.
|
||||
@ -143,6 +145,36 @@ func (r *Registry) ProviderForModel(model string) *Provider {
|
||||
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()
|
||||
@ -161,6 +193,27 @@ type SourceStatus struct {
|
||||
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, 6*time.Second)
|
||||
defer cancel()
|
||||
p.Probe(probeCtx)
|
||||
}(p)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *Registry) Status() []SourceStatus {
|
||||
@ -168,16 +221,20 @@ func (r *Registry) Status() []SourceStatus {
|
||||
defer r.mu.RUnlock()
|
||||
out := make([]SourceStatus, 0, len(r.providers))
|
||||
for _, p := range r.providers {
|
||||
s := SourceStatus{
|
||||
Name: p.Name(),
|
||||
Adapter: p.Adapter(),
|
||||
BaseURL: p.Config().BaseURL,
|
||||
Models: p.Models(),
|
||||
Available: p.Available(),
|
||||
Healthy: p.Available(),
|
||||
MaxConcurrent: p.MaxConcurrent(),
|
||||
}
|
||||
out = append(out, s)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user