Files
Agent-Native/runtime/llm/fallback.go
T
weichenandCursor fb2b3b796c
ci / vet-and-test (pull_request) Successful in 46s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Record tool intent before execution and add file checkpoints
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>
2026-10-03 17:27:04 +00:00

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
}