mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 11:07:03 +00:00
202 lines
5.9 KiB
Go
202 lines
5.9 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
)
|
|
|
|
// FallbackCandidate is one runtime provider/model fallback option.
|
|
type FallbackCandidate struct {
|
|
ProviderName string
|
|
Model string
|
|
Provider Provider
|
|
}
|
|
|
|
// ModelFallbackProvider wraps a primary provider with ordered fallback
|
|
// provider/model candidates. The primary candidate is always tried first.
|
|
type ModelFallbackProvider struct {
|
|
primary FallbackCandidate
|
|
fallbacks []FallbackCandidate
|
|
classifier ErrorClassifier
|
|
tracker *CooldownTracker
|
|
maxAttempts int
|
|
}
|
|
|
|
type FallbackCallInfo struct {
|
|
Streamed bool
|
|
}
|
|
|
|
type FallbackAfterCall func(*ChatResponse, error, FallbackCallInfo)
|
|
type FallbackBeforeCall func(ctx context.Context, entry FallbackCandidate, req ChatRequest) (after FallbackAfterCall, err error)
|
|
|
|
func NewModelFallbackProvider(primary FallbackCandidate, fallbacks []FallbackCandidate, maxAttempts int, cooldownEnabled bool) *ModelFallbackProvider {
|
|
var tracker *CooldownTracker
|
|
if cooldownEnabled {
|
|
tracker = NewCooldownTracker(0)
|
|
}
|
|
return &ModelFallbackProvider{
|
|
primary: primary,
|
|
fallbacks: fallbacks,
|
|
classifier: NewDefaultClassifier(),
|
|
tracker: tracker,
|
|
maxAttempts: maxAttempts,
|
|
}
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) PrimaryProvider() Provider {
|
|
return p.primary.Provider
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) Name() string {
|
|
if p.primary.Provider != nil {
|
|
return p.primary.Provider.Name()
|
|
}
|
|
return p.primary.ProviderName
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) DefaultModel() string {
|
|
if p.primary.Model != "" {
|
|
return p.primary.Model
|
|
}
|
|
if p.primary.Provider != nil {
|
|
return p.primary.Provider.DefaultModel()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
|
|
return p.runOrdered(ctx, req, func(ctx context.Context, entry FallbackCandidate, req ChatRequest) (*ChatResponse, error) {
|
|
nextReq := req
|
|
nextReq.Model = entry.Model
|
|
return entry.Provider.Chat(ctx, nextReq)
|
|
})
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) ChatWithHook(ctx context.Context, req ChatRequest, before FallbackBeforeCall) (*ChatResponse, error) {
|
|
return p.runOrdered(ctx, req, func(ctx context.Context, entry FallbackCandidate, req ChatRequest) (*ChatResponse, error) {
|
|
nextReq := req
|
|
nextReq.Model = entry.Model
|
|
after, err := before(ctx, entry, nextReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := entry.Provider.Chat(ctx, nextReq)
|
|
if after != nil {
|
|
after(resp, err, FallbackCallInfo{})
|
|
}
|
|
return resp, err
|
|
})
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error) {
|
|
return p.runOrdered(ctx, req, func(ctx context.Context, entry FallbackCandidate, req ChatRequest) (*ChatResponse, error) {
|
|
nextReq := req
|
|
nextReq.Model = entry.Model
|
|
streamed := false
|
|
resp, err := entry.Provider.ChatStream(ctx, nextReq, func(chunk StreamChunk) {
|
|
if chunk.Content != "" || chunk.Thinking != "" || len(chunk.Images) > 0 {
|
|
streamed = true
|
|
}
|
|
onChunk(chunk)
|
|
})
|
|
if streamed && err != nil {
|
|
return nil, noFallbackAfterStreamError{err: err}
|
|
}
|
|
return resp, err
|
|
})
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) ChatStreamWithHook(ctx context.Context, req ChatRequest, onChunk func(StreamChunk), before FallbackBeforeCall) (*ChatResponse, error) {
|
|
return p.runOrdered(ctx, req, func(ctx context.Context, entry FallbackCandidate, req ChatRequest) (*ChatResponse, error) {
|
|
nextReq := req
|
|
nextReq.Model = entry.Model
|
|
after, err := before(ctx, entry, nextReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
streamed := false
|
|
resp, err := entry.Provider.ChatStream(ctx, nextReq, func(chunk StreamChunk) {
|
|
if chunk.Content != "" || chunk.Thinking != "" || len(chunk.Images) > 0 {
|
|
streamed = true
|
|
}
|
|
onChunk(chunk)
|
|
})
|
|
if after != nil {
|
|
after(resp, err, FallbackCallInfo{Streamed: streamed})
|
|
}
|
|
if streamed && err != nil {
|
|
return nil, noFallbackAfterStreamError{err: err}
|
|
}
|
|
return resp, err
|
|
})
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) runOrdered(
|
|
ctx context.Context,
|
|
req ChatRequest,
|
|
call func(context.Context, FallbackCandidate, ChatRequest) (*ChatResponse, error),
|
|
) (*ChatResponse, error) {
|
|
candidates := p.orderedCandidates(req.Model)
|
|
var attempts []FailoverAttempt
|
|
for i, entry := range candidates {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
if p.maxAttempts > 0 && i >= p.maxAttempts {
|
|
break
|
|
}
|
|
key := CooldownKey(entry.ProviderName, entry.Model)
|
|
if p.tracker != nil && !p.tracker.IsAvailable(key) && !p.tracker.ShouldProbe(key) {
|
|
continue
|
|
}
|
|
resp, err := call(ctx, entry, req)
|
|
if err == nil {
|
|
if p.tracker != nil {
|
|
p.tracker.RecordSuccess(key)
|
|
}
|
|
return resp, nil
|
|
}
|
|
if streamErr, ok := err.(noFallbackAfterStreamError); ok {
|
|
return nil, streamErr.err
|
|
}
|
|
classification := ClassifyHTTPError(p.classifier, err)
|
|
attempts = append(attempts, FailoverAttempt{
|
|
Candidate: ModelCandidate{Provider: entry.ProviderName, Model: entry.Model, ProfileID: entry.ProviderName + "/" + entry.Model},
|
|
Classification: classification,
|
|
Err: err,
|
|
})
|
|
if p.tracker != nil && classification.Kind == "reason" {
|
|
p.tracker.RecordFailure(key, classification.Reason)
|
|
}
|
|
if classification.Kind == "context_overflow" || classification.Reason == FailoverUnknown {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, &FailoverSummaryError{Attempts: attempts}
|
|
}
|
|
|
|
func (p *ModelFallbackProvider) orderedCandidates(requestModel string) []FallbackCandidate {
|
|
primary := p.primary
|
|
if requestModel != "" {
|
|
primary.Model = requestModel
|
|
}
|
|
out := []FallbackCandidate{primary}
|
|
for _, fallback := range p.fallbacks {
|
|
if fallback.Provider == nil || fallback.ProviderName == "" || fallback.Model == "" {
|
|
continue
|
|
}
|
|
if fallback.ProviderName == primary.ProviderName && fallback.Model == primary.Model {
|
|
continue
|
|
}
|
|
out = append(out, fallback)
|
|
}
|
|
return out
|
|
}
|
|
|
|
type noFallbackAfterStreamError struct {
|
|
err error
|
|
}
|
|
|
|
func (e noFallbackAfterStreamError) Error() string {
|
|
return e.err.Error()
|
|
}
|