Files
Cyrene_v2/internal/runtime/model/fallback.go
T
2026-09-06 09:41:06 +08:00

94 lines
2.8 KiB
Go

package model
import (
"context"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
// Step binds a model name to an Adapter for one entry in a fallback chain. An
// empty Model keeps the incoming Request.Model.
type Step struct {
Model string
Adapter Adapter
}
// Chain is an Adapter that tries its steps in order and returns the first
// successful response. It implements Adapter, so it can be used anywhere a
// single provider is expected.
type Chain struct {
steps []Step
}
// NewChain builds a fallback chain from the given steps.
func NewChain(steps ...Step) *Chain {
return &Chain{steps: steps}
}
// NewOpenAIChatChain builds a fallback chain from a single supplier config: the
// primary model first, then cfg.Fallback (when set and distinct), both served
// by an OpenAIChatAdapter against the same OpenAI-compatible endpoint. It
// delegates to the provider-aware builder so the OpenAI path and the factory stay
// in sync. It returns ErrNoModel when no base URL or primary model is configured.
func NewOpenAIChatChain(cfg config.LLMConfig, opts ...OpenAIOption) (*Chain, error) {
return newOpenAIChain(cfg, opts...)
}
// Name returns the chain identifier.
func (c *Chain) Name() string { return "fallback" }
// Chat tries each step in order and returns the first success. If every step
// fails, the last error is returned wrapped as ErrUpstream.
func (c *Chain) Chat(ctx context.Context, req Request, opts Options) (Response, error) {
if len(c.steps) == 0 {
return Response{}, ErrNoSteps
}
var lastErr error
for _, s := range c.steps {
stepReq := req
if s.Model != "" {
stepReq.Model = s.Model
}
resp, err := s.Adapter.Chat(ctx, stepReq, opts)
if err == nil {
return resp, nil
}
lastErr = err
}
return Response{}, fmt.Errorf("model: %w: %v", ErrUpstream, lastErr)
}
// ChatStream tries each step in order. If a step fails before delivering any
// delta, the chain moves to the next step; once a step has delivered output it
// is committed, so a mid-stream failure surfaces as an error rather than mixing
// content across providers.
func (c *Chain) ChatStream(ctx context.Context, req Request, onChunk func(delta string)) (Response, error) {
if len(c.steps) == 0 {
return Response{}, ErrNoSteps
}
var lastErr error
for _, s := range c.steps {
stepReq := req
if s.Model != "" {
stepReq.Model = s.Model
}
delivered := false
wrapped := func(delta string) {
delivered = true
if onChunk != nil {
onChunk(delta)
}
}
resp, err := s.Adapter.ChatStream(ctx, stepReq, wrapped)
if err == nil {
return resp, nil
}
if delivered {
return Response{}, fmt.Errorf("model: %w: stream step %q failed after partial output: %v", ErrUpstream, s.Model, err)
}
lastErr = err
}
return Response{}, fmt.Errorf("model: %w: %v", ErrUpstream, lastErr)
}