package routing import ( "fmt" "log" ) // ── Fallback Runner ───────────────────────── // DispatchFunc is called by the fallback runner to attempt a request // against a specific candidate. Returns nil on success, error on failure. // The caller is responsible for setting up SSE headers, streaming, etc. // This is NOT the actual dispatch — it's the "try this candidate" callback. type DispatchFunc func(candidate Candidate) error // RunWithFallback tries candidates in order until one succeeds. // Returns the candidate that succeeded, the decision (updated with fallback // depth), and any error if all candidates fail. // // maxRetries controls how many fallback attempts are made beyond the first. // maxRetries=0 means only try the first candidate (no fallback). // maxRetries=-1 means try all candidates. func RunWithFallback(candidates []Candidate, decision *Decision, maxRetries int, dispatch DispatchFunc) (Candidate, *Decision, error) { if len(candidates) == 0 { return Candidate{}, decision, fmt.Errorf("no routing candidates available") } // Determine how many candidates to try limit := 1 // just the first if maxRetries < 0 { limit = len(candidates) // try all } else if maxRetries > 0 { limit = 1 + maxRetries if limit > len(candidates) { limit = len(candidates) } } var lastErr error for i := 0; i < limit; i++ { c := candidates[i] err := dispatch(c) if err == nil { // Success if decision != nil { decision.Selected = c.ConfigID decision.FallbackDepth = i } return c, decision, nil } lastErr = err if i < limit-1 { log.Printf("[routing] candidate %s (%s) failed: %v — trying next", c.ConfigID, c.ProviderID, err) } } return Candidate{}, decision, fmt.Errorf("all %d routing candidates failed; last error: %w", limit, lastErr) }