2026-10-28·by Sijie Wang#standmeet#architecture#design#events

model-call-retry

The model call gets exactly one retry: the httpx transport retries transient failures at most twice from a 400ms base, with a 10s public-tier cap.

The model call's retry: the one synchronous retry left

Parent: events

As-built record in backend/internal/conversation/inference/http_retry.go, backend/internal/infra/httpx/retry_transport.go, backend/internal/infra/httpx/retry_budget.go, backend/internal/conversation/inference/errors.go.

The model call IS retried inside the request: by the httpx transport only, at most 2 retries. This is not a contradiction of retry-has-one-owner: that node's "sent once" rule bans a second retry loop around a synchronous call; the transport's own retry is the one retry allowed. Do not generalize the calendar example in that node: calendar free/busy/insert/delete run with NoRetry, but the model call runs WITH the transport retry.

What retries

  • At most 2 retries (1 initial send + 2 resends), base delay 400 ms.
  • Retried: transport errors except the caller's own context cancel/timeout; responses with status 429 or 5xx.
  • Never retried: once response headers arrive and streaming starts (already-streamed tokens are never re-read); the call's own cancellation or timeout.
  • Wait before each retry: the larger of the own exponential backoff (400 ms, then 800 ms) and the provider's Retry-After header. Retrying earlier than the provider asked makes a ban worse, so the header always wins when larger.
  • Cannot afford the wait: if the wait exceeds the turn's remaining budget, the transport hands the response back at once instead of sleeping the budget away.

Public tier cap

  • Public turns cap total retry waiting at 10 s; past it the turn fails fast with "The AI is busy right now — give it a minute and ask again." Coded / BYOAI turns keep waiting as before.
  • Why: the public tier runs on the owner's free provider; on 2026-09-25 every step came back 429 with a 31–43 s retry-after, and the visitor sat in silence for minutes.
  • Rate-limited means the provider's 429 after retries, or a retry-after longer than the turn will wait; both map to the same busy message.

Visible while retrying

Every backoff logs "llm transient failure — retrying" with attempt, max, status, wait_ms, and wait_from (backoff vs retry-after); the frontend throbber shows "retrying". wait_from is what proves the wait respected the provider rather than happening to be long enough.

Not this mechanism

The write path's 2 s wait + receipt (async-response-contract) is a different path: it covers writes landing in the search index, not the model call. The two must not be mixed: a model-call timeout has no receipt and no index_job_id.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →