summaryrefslogtreecommitdiff
path: root/internal/llm/util.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-11 00:04:11 +0300
committerPaul Buetow <paul@buetow.org>2026-06-11 00:04:11 +0300
commit247b79114d83e13cdfb2136333a949ff4dbe385b (patch)
treebf7c379620002f7f2731c3565d19959ecaeb9e74 /internal/llm/util.go
parent72ac39cb2bac5c176dd1277c9482334d0441286f (diff)
Add retry and circuit-breaker resilience around LLM HTTP calls (ak0)
Wrap the shared provider HTTP choke point (doJSONRequest in llm/util.go, used by openai, openrouter, anthropic and ollama) with two resilience patterns implemented with the standard library only: - Retry with exponential backoff + jitter (resilience.go): 3 attempts by default, retrying transient network errors and retryable HTTP statuses (429 and 5xx). Client errors (4xx) and successes are returned immediately and never retried. Backoff waits are context-aware so cancellation and deadlines are respected; retried response bodies are drained and closed for connection reuse. - Circuit breaker (circuitbreaker.go, own file as it has >3 methods): classic closed/open/half-open breaker that trips after 5 consecutive transient failures and stays open for a 30s cooldown, then allows a single trial probe. Only transient failures count; 4xx never trips it. A nil breaker is a valid no-op. doJSONRequest now delegates to doJSONRequestResilient; the single-attempt primitive is preserved as doJSONRequestOnce. Adds httptest-based unit tests for retry/backoff/breaker logic with no real network calls; new code coverage is >80%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal/llm/util.go')
-rw-r--r--internal/llm/util.go11
1 files changed, 11 insertions, 0 deletions
diff --git a/internal/llm/util.go b/internal/llm/util.go
index b6e2adc..fe99467 100644
--- a/internal/llm/util.go
+++ b/internal/llm/util.go
@@ -13,7 +13,18 @@ import (
// small helper to keep return type consistent
func nilStringErr(msg string) (string, error) { return "", errors.New(msg) }
+// doJSONRequest is the shared entry point for provider HTTP calls. It applies
+// the default retry-with-backoff policy and the shared circuit breaker so that
+// transient upstream failures (network resets, 429, 5xx) are smoothed over,
+// while client errors (4xx) and successes are returned immediately. The body
+// bytes are passed by value so each retry can rebuild a fresh request.
func doJSONRequest(ctx context.Context, httpClient *http.Client, url string, body []byte, headers map[string]string, accept string) (*http.Response, error) {
+ return doJSONRequestResilient(ctx, httpClient, url, body, headers, accept, defaultRetryPolicy(), sharedBreaker)
+}
+
+// doJSONRequestOnce performs exactly one HTTP POST with the given JSON body and
+// headers. It is the single-attempt primitive used by the resilience layer.
+func doJSONRequestOnce(ctx context.Context, httpClient *http.Client, url string, body []byte, headers map[string]string, accept string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, err