Why: a crash after a tool was about to run left the call unpaired, and workspace writes could not be rewound. Evidence: GOWORK=off go test ./runtime/ ./runtime/llm/ ./runtime/checkpoint/ ./runtime/telemetry/ ./runtime/tools/ ./runtime/session/ ./adapters/native/ Impact: pending calls rerun only when both the stored and current replay declarations are safe; otherwise the result is an error. Empty fallback lists and a nil checkpoint log keep the previous behavior. Refs: feature/durable-rewind Co-authored-by: Cursor <cursoragent@cursor.com>
80 lines
1.8 KiB
Go
80 lines
1.8 KiB
Go
package llm
|
|
|
|
import "context"
|
|
|
|
// WithFallback 在尚未转发任何增量时,遇到 5xx 就改用链上下一个模型。
|
|
// fallbacks 为空时返回原来的 StreamFunc,不改变行为。
|
|
func WithFallback(base StreamFunc, fallbacks []string, onSwitch func(from, to string)) StreamFunc {
|
|
chain := nonEmpty(fallbacks)
|
|
if base == nil || len(chain) == 0 {
|
|
return base
|
|
}
|
|
return func(ctx context.Context, model string, messages []Message, tools []ToolDecl, opts StreamOptions) <-chan Event {
|
|
out := make(chan Event)
|
|
go func() {
|
|
defer close(out)
|
|
models := append([]string{model}, chain...)
|
|
for i, current := range models {
|
|
forwarded := false
|
|
switchNext := false
|
|
for ev := range base(ctx, current, messages, tools, opts) {
|
|
if isDelta(ev) {
|
|
forwarded = true
|
|
}
|
|
if ev.Kind == KindError && !forwarded && serverError(ev) && i+1 < len(models) {
|
|
switchNext = true
|
|
if onSwitch != nil {
|
|
onSwitch(current, models[i+1])
|
|
}
|
|
break
|
|
}
|
|
select {
|
|
case out <- ev:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
if ev.Kind == KindMessageEnd || ev.Kind == KindError {
|
|
return
|
|
}
|
|
}
|
|
if !switchNext {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return out
|
|
}
|
|
}
|
|
|
|
func nonEmpty(in []string) []string {
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, s := range in {
|
|
s = trimModel(s)
|
|
if s == "" || seen[s] {
|
|
continue
|
|
}
|
|
seen[s] = true
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func trimModel(s string) string {
|
|
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
|
|
s = s[1:]
|
|
}
|
|
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
|
s = s[:len(s)-1]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func isDelta(ev Event) bool {
|
|
return ev.Kind == KindTextDelta || ev.Kind == KindReasoningDelta || ev.Kind == KindToolCallDelta
|
|
}
|
|
|
|
func serverError(ev Event) bool {
|
|
return ev.HTTPStatus >= 500 && ev.HTTPStatus <= 599
|
|
}
|