Repository navigation
Run: request_retry sends a failed model request again after a wait - #353
Merged
Merged
Conversation
A provider down for minutes fails every request a run makes, and the retries Imp already makes come within seconds of the failure. With Imp.Run.start/3's request_retry: [waits: [...], reserve: ms], a request that fails with an error Imp.Errors.retryable?/1 accepts is sent again inside Imp.LM.request/2 after each wait in turn. Under a deadline the last wait repeats while it leaves reserve of the deadline; without one the list is the bound. Each attempt is its own model_request and model_response, and a failed response followed by another attempt carries retry_in_ms, so the record shows the failure, the wait and the next attempt. Streamed requests are not sent again. Off unless given.
…ty waits An LM that wraps an LM (an optimizer's budget, a rollout LM, a host's own wrapper) calls Imp.LM.generate inside generate, and each level retried, so k retries became (k+1)^2 attempts. Only the outermost request in a process now waits and sends again; an inner one returns its failure. A provider's retry-after longer than the run's wait is waited instead, still under the deadline and reserve check. waits must be a non-empty list: an empty one under a deadline raised in List.last/1. The docs say that a timed-out attempt may be billed without a response to record, so a retry can be billed twice.
# Conflicts: # CHANGELOG.md # decisions.md
deepfates
added a commit
that referenced
this pull request
Oct 17, 2026
Version 0.16.0 with install lines ~> 0.16.0 and tag links for v0.16.0. The changelog's Unreleased section goes under 0.16.0, and the release notes describe #353, #303 and #354. A minor release: request_retry: is a new option on Imp.Run.start/3, Imp.Core reads a call's charge from metadata :cost or req_llm.charge instead of usage "cost", and a forced submit with invalid arguments goes to the extractor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When a provider is down for minutes (a 502 with the connection refused upstream, a 429 that lasts), every request a run makes fails. The retries Imp already makes don't help, because they all come within a second or two of the failure: ReqLLM's own retry, and ReActV2's one more step or last request. A host whose turns have a deadline of several minutes would rather wait and try again inside the same run than fail at once.
What
Imp.Run.start/3gets a new option,request_retry: [waits: [ms, ...], reserve: ms]. It is off unless given.Imp.LM.request/2, if a request fails with an error thatImp.Errors.retryable?/1accepts (408, 425, 429, 5xx, or a failed connection), it is sent again after the next wait.Imp.Deadlinewould still havereserveleft after the wait (60 s by default). Otherwise the failure is returned as before.:model_requestand:model_response, with its ownmodel_call_id. A failed response that is followed by another attempt carriesretry_in_msin its metadata. Anyone reading the run sees the failure, the wait and the next attempt.Imp.stream/3throughImp.LM.record/3) are not sent again, because their chunks may already have reached the caller.Imp.LM.request/2asks for it only after a request has failed, so a request that succeeds costs nothing extra.Imp.Errors.retryable?/1and adds no new classification.After review
Imp.LM.generate/3insidegenerate/3) would turn k retries into (k+1)² attempts.retry-afteris honoured. The wait is now max(wait,retry-after), still under the deadline and reserve check. The header is read with the parserImp.Clients.ReqLLMBatchalready uses, exposed asretry_after_ms/1.waitsmust be a non-empty list. An empty list under a deadline raised anArithmeticError; it is now refused when the run starts.Tests
test/run_request_retry_test.exscovers eight cases (the first five here; the three after review are a wrapper LM, refusing empty waits, and a 429 withretry-after: 1waiting 1000 ms):retry_in_ms10 and 30, and the third response has noretry_in_ms.mix checkpasses: 3718 tests, 0 failures.mix quality.checkandmix dialyzer.checkalso pass. The CHANGELOG has an entry under Unreleased, and decisions.md has a new row.How a host uses it
A host that runs agent turns under a deadline of several minutes can pass, for example,
request_retry: [waits: [15_000, 45_000, 120_000, 240_000], reserve: 60_000]. A provider outage then costs a few requests per turn instead of the turn itself.