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:
root
2026-08-10 00:10:19 +08:00
parent 859d310ad3
commit d48b993010
10 changed files with 663 additions and 210 deletions

View File

@ -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() {

View File

@ -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
}