Repository navigation
Conversation
Contributor
|
@subidhkhanal is attempting to deploy a commit to the allan-feng's projects Team on Vercel. A member of the Team first needs to authorize it. |
RESPONSE_INVALID also covers contract breaks that fail identically on every attempt: a request the client refuses before sending, or a result that fails validation after it was settled. Retrying the bare category re-billed the second case and logged the first as an empty model reply. - The adapters (and the service's backstop) raise LLMExecutionError.for_empty_reply(); callers retry on is_empty_reply(). - Single-prompt loop: an execution-layer empty reply goes straight to the rescue call (2 calls, not 5), as pipeline_runner does. It is released at zero (Open-Finance-Lab#556), so same-ceiling retries were unbilled generations. The legacy client keeps the LLM_ESCALATE_CEILING_ON_RETRY count. There is one request path and one retry branch. - pipeline_runner decision steps use the same predicate. - Analytics files the empty reply as `empty_response`, not `internal_error`. This covers the allow-list and the SQLite/Postgres CHECKs (now guarded), and the label is carried across the child's run_failed line. - Tests: the real SDK adapters over MockTransport, and an end-to-end DeepSeek/CommonStack run that checks the ledger, H6 and the thinking-off setting. A rescue that also comes back empty, and a contract break, are covered as well. The shared harness moves to tests/_execution_fixtures.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Fixes #600
Problem. The single-prompt decision loop retries an empty model reply, then makes one final "rescue" call at a higher output ceiling (
RECOVERY_MAX_OUTPUT_TOKENS). It only ever recognised an empty reply as the legacyAttributeError("No text content…"). Under the unified execution layer, every adapter now rejects an empty reply before that error can be raised, asLLMExecutionError(RESPONSE_INVALID)(execution/service.py:671). Nothing on this path caught that, so the retries and the rescue call never ran — one empty reply ended the run. Two DeepSeek V4 Pro runs via CommonStack died this way in prod on 2026-10-01 (DJIA 30, 60m): both failed on the first call, after 26s and 31s respectively, withreturncode=1and noagent_runsrow written.pipeline_runner.py:880-890and:910-919already handle this correctly. This path, which the leaderboard'sllm_agentalso uses, never got the same handling.Fix, in
domain/backtesting/portfolio_manager.py:LLMExecutionErrorwith categoryRESPONSE_INVALIDand treats it exactly like the legacy empty-reply case: retry at the configured ceiling, then the rescue call. Every other category (billing, credentials, quota) is re-raised immediately and still ends the run.RESPONSE_INVALID, the first reply is kept instead of crashing, matchingpipeline_runner's equivalent branch.LLM_ESCALATE_CEILING_ON_RETRYsetting, same as before this fix.Tests, in
test_escalate_ceiling.py: the existing fake request can now raiseLLMExecutionError(RESPONSE_INVALID)(what the real adapters produce) alongside the legacyAttributeError. New cases:RESPONSE_INVALIDerror (e.g.BILLING_FAILED) is not retried and still ends the run;AnthropicCompatibleExecutionClientandLLMExecutionServicewith a scripted adapter returning empty text, run in strict/fail_closedmode as prod runs it — confirming the fix at the actual boundary, not just against a stand-in.I verified the new tests catch the bug: against unmodified
main, 5 of the 6 new tests fail (the request loop stops after one empty reply instead of retrying). With the fix, all 27 tests in the file pass.Verification run locally:
1091 passed, 2 skipped, 0 failures.
Known trade-off, stated per the issue's own fix sketch (reuse the same retry/rescue budget): with the default (non-escalating) setting, an empty reply can now cost up to 5 provider calls for that bar instead of ending the run after 1 — this restores the retry behaviour the code was written for rather than introducing new policy.
LLM_ESCALATE_CEILING_ON_RETRY=1cuts this to 2 calls. Note that #556 (an empty reply is released at zero even though the provider billed it) means these retried calls are not yet reflected in a run's recorded spend — a separate, already-filed issue.Scope note, from a self-review after this diff: a few related things are left out on purpose to keep this PR narrow —
RESPONSE_INVALIDalso covers non-empty-reply failures (a malformed request, a bad adapter response), so the new retry treats those the same way the issue's sketch asks for the empty-reply case to be treated. Giving "empty reply" its own error category, so request/response bugs aren't silently retried under the same label, would be a cleaner design but touches the execution layer andpipeline_runnertoo — worth its own issue;llm_calls/token totals, which is exactly fix(llm): an empty reply is released at zero though the provider billed it #556's scope, not reintroduced here.Review follow-up (maintainer commit on this branch). This replaces the "retry count is unchanged", "Known trade-off" and "Scope note" paragraphs above.
LLMExecutionError.for_empty_reply(). The category is stillRESPONSE_INVALID. The three adapters raise it, because that is where prod's empty replies are actually rejected (notservice.py), and the service raises it as a backstop. Retries are keyed onis_empty_reply(), so a refused request, or a result that fails validation after settlement, is no longer retried or billed again. This applies to both the single-prompt and pipeline paths.LLM_ESCALATE_CEILING_ON_RETRYsays. The legacy client is unchanged.empty_responseinstead ofinternal_error. This touches the allow-list and the SQLite/Postgres CHECKs, and a guard test covers them.3e27402ekeeps response errors fatal there.