Repository navigation
Conversation
… two attempts The OpenAI provider now requests responses in the Responses API background mode and polls them, so no connection stays idle while the model works and a finished (billed) response cannot be lost to a dropped connection. The response id is reported to the caller as soon as the request is accepted, the response is cancelled server-side when the caller gives up or the run's `timeout` budget is exceeded, and `fetch_response(id)` returns an earlier response without a new call. Meta's OpenAI-compatible route has no background mode and keeps the synchronous call. A new `get_response_with_metadata` entry point (ModelRun, model_registry, providers) returns an `LLMResponse` with the provider response id, status, token usage, tool-call count and attempt count; `get_response` keeps returning the text. The Anthropic provider fills the same metadata from its streamed final message. A `CallContext` carries the caller's cancel event and response-id hook. The retry wrapper now makes two attempts in total instead of five, stops early when the cancel event is set, and logs failed attempts at WARNING so they are visible under a default logging setup. Background mode is transport, not a model option, so it is not part of any run's declared options or fingerprint; AGENTS.md records that precedent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013tLtyvBGmSaTwPwS81PuTB
…g response A failed `responses.retrieve` poll is now retried in place (up to ten in a row) instead of propagating to the retry wrapper, which would have started a second, billed generation for a short status request that failed. `wait_for_response(id)` on the providers, the registry and `ModelRun` polls a background response another process started until it finishes, with the same cancellation and budget as a fresh call and never a new create. A resumed forecaster run uses it for a response its first process left running, so a crash or kill cannot lead to the same call being paid twice. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013tLtyvBGmSaTwPwS81PuTB
- A failed background poll is polled again for as long as the response budget lasts instead of escalating after 10 failures, so a poll outage never starts a second (billed) generation; the budget ends the wait with a non-retryable TimeoutError. - Restore the default of 5 attempts and INFO-level retry logging. - An `incomplete` OpenAI response (usually the output-token cap) raises LLMResponseIncomplete, which is never retried. - CallContext.cancel_server_side=False stops waiting on cancellation and leaves the response running so it can be fetched later. - Document that an OpenAI run's `timeout` is the whole-response budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqM83D2dF3X9btmt37ft7C
In background mode an OpenAI retry only follows a failed `create` or a response that ended `failed`, neither of them billed, so it keeps five attempts. Other providers' failed attempts (a dropped Anthropic stream, a synchronous Meta call) are usually billed and lost, so they get two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqM83D2dF3X9btmt37ft7C
A failed attempt may be billed, so it must be visible to callers that configure no handler for utils.llm (Python's default shows WARNING). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqM83D2dF3X9btmt37ft7C
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
On Oct 19 a
dwh-llm-forecasterrun cost $1,148 for zero forecasts. A VPN closed idle connections; the synchronousresponses.createcall sits silent for the minutes the model works, so every call was billed server-side but never returned. The retry wrapper then re-ran each task up to 5 times (times the SDK's own 3), and nothing held a response id, so nothing could be cancelled or fetched afterwards. This PR makes an OpenAI call something the caller holds a handle to from the first second.Paired with dwh-llm-forecaster#24, which consumes it. forecastbench pins
843ae21and is unaffected until it bumps; when it does, it gets background mode automatically with no call-site change.What
OpenAI provider: Responses API background mode.
responses.create(background=True)returns at once; the provider pollsretrieve(2 s backing off to 10 s) until the response leavesqueued/in_progress. No connection is idle while the model works. The response id is handed to the caller the moment the request is accepted. A failed poll is polled again for as long as the budget lasts; the generation it watches is already paid for and is never started again because a short status request failed.MetaProviderkeeps the synchronous call since the Meta route has no background mode.Budget and cancellation. A run's
timeoutoption is now the budget for the whole background response (default 30 min when absent); when it runs out the response is cancelled server-side and the call fails without retry. A caller's cancel event stops the call at the next checkpoint; withcancel_server_side(the default) the response is cancelled, without it the response keeps running so it can be fetched later.fetch_response(id)returns a finished response,wait_for_response(id)polls a still-running one to completion; neither ever creates a new generation.Call metadata. New
get_response_with_metadata(prompt, context=..., **options)onModelRun,model_registryand the providers returns anLLMResponse: text, provider response id, status,LLMUsage(input/output/reasoning tokens, tool-call count), attempts, duration.get_responsekeeps returning the text, so existing callers are unchanged. Anthropic fills the same metadata from its streamed final message.CallContext(utils/llm/types.py) carries the cancel event, thecancel_server_sideflag and the response-id hook.Retries. Attempts are per provider: 5 for OpenAI (in background mode a retry only follows a failed
createor a response that endedfailed, neither billed), 2 for every other provider (a dropped stream is a billed response lost). A response that endsincompleteraisesLLMResponseIncompleteand is never retried: the same request would hit the same cap and be billed again. The wait between attempts ends early on cancel. Failed attempts are logged at WARNING so they show under a caller's default logging.Docs. README section on metadata, cancellation, fetching by id and background mode. AGENTS.md records that transport mechanics (Anthropic streaming, OpenAI background mode) are provider behaviour, not run options, so they stay out of option fingerprints and immutability, and that an OpenAI run's
timeoutis the whole-response budget.Semantic changes to published runs
timeout: 900on the gpt-5.6-sol variants was an SDK per-request timeout (a call cut at 15 min, retried up to 5 times). It is now the budget for the whole background response, after which the response is cancelled and the call fails without retry. Runs withouttimeoutget 30 min (before: the SDK default of 10 min, then retries). The create/retrieve requests themselves keep the SDK default timeout.store=true, the API default; responses are retained 30 days.Commits
650127efeat(llm): background-mode OpenAI calls, call metadata, cancellation, two attemptsab3c7cdfix(llm): retry failed polls in place; wait_for_response for a running responsede47c57fix(llm): poll through failures, keep 5 attempts, never retry incompletefe4adcefix(llm): five attempts for OpenAI, two for every other provider7380e71fix(llm): log failed attempts at WARNINGTests
pytest tests/unit: 185 pass.isort,black,flake8,pydocstyleclean. Pre-existing and untouched:tests/unit/test_archiving.pyfails on Python 3.13 (tarfileDeprecationWarningunderfilterwarnings=error).New tests cover: polling to completion with id and usage extraction; cancel after accept (server-side cancel, no retry, no further poll) and cancel without server-side cancel; budget overrun; a failed poll retried in place and the generation created once; a
failedresponse retried, anincompleteone not;fetch_responseandwait_for_responsenever callingcreate; Anthropic id and usage from the final message; per-provider attempt counts; cancel before and during the retry wait; WARNING-level attempt logs.Live, through dwh-llm-forecaster against the real APIs (Oct 21): four runs on the LEAP survey that failed on Oct 19, three with 3 question groups and one full survey with both gpt-6-astra and claude-opus-5-5 (16 calls, 92 rows). Every OpenAI response id was logged 1–4 s after the run started, before any result existed; one
createand one completion per call, no retries, no duplicate generations; usage on every call; the dashboards show exactly the number of requests made. Cancellation, budget expiry andwait_for_responsehave unit coverage only so far.🤖 Generated with Claude Code
https://claude.ai/code/session_013tLtyvBGmSaTwPwS81PuTB