package main import ( "context" _ "embed" "errors" "fmt" "net/http" "strconv" "strings" "time" ) //go:embed prompt.md var reviewPrompt string // A Backend runs the review agent of one provider. The agent reads the repository through a // read-only Gitea connection of that provider and answers with the report. type Backend interface { // Prepare makes the backend ready to review with the given models of it. It is retried until // it succeeds. Prepare(ctx context.Context, models []string) error // Ask runs one review conversation with model and returns the agent's answer. Ask(ctx context.Context, model, task string) (Answer, error) } type Answer struct { Text string ToolCalls int Violations []string } // apiError is a non-2xx answer of a provider API. Whatever limit headers come back are kept so // they can be logged and used. type apiError struct { Provider string Method, Path string Status int Body string RetryAfter time.Duration HasRetryAfter bool Limits map[string]string } func (e *apiError) Error() string { return fmt.Sprintf("%s %s %s: HTTP %d: %s", e.Provider, e.Method, e.Path, e.Status, e.Body) } // newAPIError reads the status, the Retry-After and the limit headers of a failed response. func newAPIError(provider, method, path string, resp *http.Response, body []byte) *apiError { e := &apiError{Provider: provider, Method: method, Path: path, Status: resp.StatusCode, Body: snippet(body), Limits: limitHeaders(resp.Header)} if secs, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil && secs >= 0 { e.RetryAfter, e.HasRetryAfter = time.Duration(secs)*time.Second, true } return e } // limitHeaders collects the Retry-After and rate limit headers of a response. func limitHeaders(h http.Header) map[string]string { out := map[string]string{} for k, v := range h { l := strings.ToLower(k) if l == "retry-after" || strings.Contains(l, "ratelimit") || strings.Contains(l, "rate-limit") { out[k] = strings.Join(v, ",") } } return out } // streamError is a response stream that broke off or reported an error before the answer was // complete. type streamError struct { Provider string Err error } func (e *streamError) Error() string { return fmt.Sprintf("%s stream: %v", e.Provider, e.Err) } func (e *streamError) Unwrap() error { return e.Err } // retryable says whether err is worth asking again after: a rate limit (429), a gateway or // overload failure (502, 503, 504, 529), or a response stream that broke off. func retryable(err error) bool { if api := asAPIError(err); api != nil { switch api.Status { case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout, 529: return true } return false } var s *streamError return errors.As(err, &s) && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) } func asAPIError(err error) *apiError { var api *apiError if errors.As(err, &api) { return api } return nil }