package llm import ( "context" "errors" "fmt" "io" "testing" "time" ) // TestIsTransientStream 钉住"该重发"与"重发也没用"的分界:前者是网关/传输层的偶发 // 坏流,后者是确定性失败——把后者卷进重试环就是白烧预算 + 把一次明确的错误拖成十分钟。 func TestIsTransientStream(t *testing.T) { cases := []struct { name string err error want bool }{ {"nil", nil, false}, {"整轮空补全", fmt.Errorf("openai: %w", ErrEmptyCompletion), true}, {"流被截断", fmt.Errorf("anthropic: %w", ErrTruncatedStream), true}, {"流卡死到看门狗上限", &StalledError{Idle: 90 * time.Second}, true}, {"包过一层的卡死", fmt.Errorf("stream: %w", &StalledError{Idle: time.Minute}), true}, {"连接被掐", errors.New("read tcp 10.0.0.5:52314->1.2.3.4:443: connection reset by peer"), true}, {"连接被拒", errors.New("dial tcp 1.2.3.4:443: connect: connection refused"), true}, {"读超时", errors.New("read tcp: i/o timeout"), true}, {"裸 EOF", io.EOF, true}, {"网关过载", errors.New("openai: HTTP 503: upstream busy"), true}, {"限流", errors.New("openai: HTTP 429 Too Many Requests"), true}, // 以下都是"重发只会再撞一次同一堵墙"。 {"请求不合法", errors.New(`openai: HTTP 400: {"error":{"message":"inference request is invalid","code":"400002"}}`), false}, {"上游拒了这条请求", errors.New(`openai: HTTP 400: {"error":{"code":"upstream_request_rejected","message":"Bad Request","type":"invalid_request_error"}}`), false}, {"没权限", errors.New("openai: HTTP 401: invalid api key"), false}, {"余额不足", errors.New("anthropic: credit balance is too low"), false}, {"上下文满", errors.New("openai: model_context_window_exceeded"), false}, {"用户取消", context.Canceled, false}, {"外部 deadline", context.DeadlineExceeded, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := IsTransientStream(c.err); got != c.want { t.Errorf("IsTransientStream(%v) = %v, want %v", c.err, got, c.want) } }) } }