Skip to content

fix(stella-model): retry an afford-hinted OpenRouter 402 at a reduced max_tokens; refuse underfunded arenabench cloud runs - #3309

Open
macanderson wants to merge 2 commits into
mainfrom
fix-402-resilience
Open

fix(stella-model): retry an afford-hinted OpenRouter 402 at a reduced max_tokens; refuse underfunded arenabench cloud runs#3309
macanderson wants to merge 2 commits into
mainfrom
fix-402-resilience

Conversation

@macanderson

@macanderson macanderson commented Aug 14, 2026

Copy link
Copy Markdown
Owner

What & why

Three paid bench runs were killed by the same mechanism: OpenRouter answers a request whose max_tokens ceiling the credit balance cannot cover with HTTP 402 whose body literally says "You requested up to 128000 tokens, but can only afford 47365" — and Stella classified every 402 as a terminal provider error and aborted the trial, while ArenaBench happily submitted full grids against the empty balance.

Evidence — the three lost runs: h2h891 (16 trials lost), fivetools5 (30/30 lost), gate89high1 (35/89 lost). This PR is the record for the fix; no pre-existing issue tracks it, so it closes nothing.

Two fixes, one per commit:

A — engine-side graceful degrade (crates/stella-model). The shared chat-completions adapter (zai.rs::complete_inner) now resends an OpenRouter 402 carrying the afford hint once, with max_tokens reduced to 90% of the hinted ceiling, when the hint clears an 8192-token floor (below that a turn buys a truncation, not an answer — the finish_reason: length shape from the 2026-07-31 bundle). A hint-less 402, a below-floor hint, and a second 402 abort exactly as before. Gated on actually addressing OpenRouter (serves_openrouter(), the #1285 endpoint-not-identity lesson); the hint is parsed defensively from the gateway's own prose (afford_hint_tokens — no hint / no digits / overflow all mean "no retry").

Declared as the parity matrix's fifth axis, BillingPosture (provider_parity.rs): AffordHintRetry for openrouter with a named witness, TerminalAbort with a reviewable note for the other nine ids — behavior for every other provider is unchanged.

Exemplars followed (per CLAUDE.md): the one-shot resend copies the crate's own rejects_disabled_reasoning mandatory-reasoning recovery shape in complete_inner, and the new axis copies OverflowPosture's recognize-a-wire-signal-and-recover shape, enforcement tests included.

B — ArenaBench preflight (arenabench/). arenabench cloud run now queries GET https://openrouter.ai/api/v1/credits (endpoint shape verified against the live API on 2026-08-14: {"data":{"total_credits":1650,"total_usage":1606.027034237}}; remaining = total_credits - total_usage) before anything is uploaded, resolved, or submitted, and refuses when remaining < trials × --est-per-trial (new flag, default $1.00), naming the balance, the projection, and the --skip-balance-check override in the refusal. It fails OPEN — missing OPENROUTER_API_KEY, network error, HTTP error, or shape surprise each print a "check skipped" warning and proceed, because a broken preflight must not block a funded run. Decisions live in the new stdlib-only arenabench/arenabench/balance.py; the one HTTP call rides an injectable credits seam on CloudExecutor, same shape as ls_remote (cloud.py stays under the 1500-line ratchet: 1487).

The witness

  • This PR includes witness tests (fail on main, pass here)

Both flips were demonstrated by running the tests against the unwired code first:

Acomplete_retries_an_afford_hinted_402_with_reduced_max_tokens (zai/tests/error_classify.rs, wiremock): first response 402 with the afford hint; the success mock matches only a body carrying "max_tokens":42628 (90% of 47365). Before wiring: FAILED — panicked: the afford-hinted 402 must be retried at the reduced ceiling: Terminal("OpenRouter rejected the request (HTTP 402: payment required): … can only afford 47365 …"). After wiring: ok. The other side is pinned twice: the pre-existing hint-less-402 test now .expect(1)s its mock (no retry without a hint), and a_402_afford_hint_below_the_floor_stays_terminal proves the floor.

BTestBalancePreflight (tests/test_cloud.py): before wiring the verb, test_an_underfunded_balance_refuses_before_any_upload_or_submit failed with the transcript showing the run submitting normally (submitted : 1 job(s)); after wiring it refuses with rc 2, zero S3 calls, zero submissions. Sufficient balance submits; a credits seam raising OSError("connection refused") proceeds with balance : check skipped — … connection refused …; --skip-balance-check provably never queries the seam. Pure halves (parsing, refusal boundary, fail-open ladder) pinned in the new tests/test_balance.py.

The gate

Nothing left behind

Ground-rule check

  • No I/O added to stella-core; no new deps (Rust: none; Python: stdlib urllib only, keeping ArenaBench's stdlib-only contract)
  • No new outbound network calls from Stella (the credits call is ArenaBench's submit-side tooling, operator-initiated, key-scoped, and skippable)

Anything reviewers should know?

  • The retry matches OpenRouter's prose ("can only afford N") in the classified error detail, the same way rejects_disabled_reasoning matches upstream wording — there is no machine field for the hint on the wire. The wiremock witness exercises the full classify → parse → resend path, so a rewording of either side fails a test rather than silently disarming the recovery.
  • 90% (not 100%) of the hint: the hint is computed from the balance at rejection time; the margin keeps the retry off the exact boundary the first attempt lost on. Integer math, u64 checked, floor inclusive at 8192.
  • The preflight runs after the SSM credential refusal and before resolve_sut, so an underfunded run also never triggers a CodeBuild build.
  • Rejected alternative: classifying the afford-hinted 402 as a new retryable ProviderError variant — that would put billing policy in stella-protocol and re-issue the same ceiling on retry, which re-rejects identically. The adapter owns the recovery because only it can rewrite max_tokens.

Summary by Sourcery

Handle OpenRouter-specific credit exhaustion more gracefully in both the Stella engine and ArenaBench cloud runs, and extend the provider parity matrix and its tests with a new billing axis and full enforcement for all axes.

New Features:

  • Add a BillingPosture axis in the provider parity matrix to describe how each provider behaves on out-of-credit rejections, including an afford-hint-based retry for OpenRouter.
  • Introduce an OpenRouter balance preflight for arenabench cloud run that estimates run cost against current credits, refusing clearly when underfunded and offering a skip flag.
  • Expose an injectable OpenRouter credits seam on CloudExecutor and wire new CLI flags for estimated per-trial spend and skipping the balance check.

Bug Fixes:

  • Retry a specific class of OpenRouter HTTP 402 responses once with a reduced max_tokens ceiling when an afford hint is present and above a safety floor, instead of treating them as terminal errors that abort otherwise fundable trials.
  • Ensure hint-less or below-floor OpenRouter 402 responses remain terminal and do not trigger retries.

Enhancements:

  • Strengthen provider parity enforcement by adding BillingPosture entries for all existing providers and ensuring all five axes share the same provider coverage, unique IDs, and real witness tests.
  • Add missing enforcement tests for the existing StreamFallbackPosture axis so its witness coverage and uniqueness are checked alongside other axes.
  • Refactor Stella CLI parity tests into a dedicated module to keep config tests within size limits while maintaining per-axis completeness checks.

Documentation:

  • Update AGENTS.md and stella-model README to document the five parity axes, including the new BillingPosture axis and its enforcement, and refresh related descriptions of parity tests and matrix coverage.

Tests:

  • Add targeted tests for OpenRouter 402 afford-hint parsing and retry behavior, including floor handling and non-retryable cases.
  • Add new parity-matrix tests to enforce witness existence, uniqueness, and consistent provider coverage for the stream-fallback and billing axes.
  • Introduce unit and integration tests for the ArenaBench balance preflight logic, covering normal funded/underfunded paths, fail-open scenarios, and CLI wiring of the new flags and credits seam.

…duced max_tokens

OpenRouter answers a request whose max_tokens ceiling the credit balance
cannot cover with HTTP 402 naming the ceiling it can still fund ("You
requested up to 128000 tokens, but can only afford 47365"). Treating
that as a terminal billing error aborted whole bench trials the balance
could still have paid for: runs h2h891 (16 trials lost), fivetools5
(30/30), gate89high1 (35/89).

The shared chat-completions adapter now resends such a rejection ONCE
with max_tokens reduced to 90% of the hint, when the hint clears an
8192-token floor — the same one-shot resend shape as the
mandatory-reasoning recovery, gated on actually addressing OpenRouter
(#1285's endpoint-not-identity lesson). A hint-less 402, a below-floor
hint, and a second 402 all abort exactly as before.

Declared as the parity matrix's fifth axis (BillingPosture):
AffordHintRetry for openrouter with a wiremock witness that fails
without the resend, TerminalAbort with a note everywhere else. The
stream-fallback axis also gains the enforcement the other axes already
had (witness existence, uniqueness, axis coverage, cli completeness),
which it shipped without; the cli completeness tests move to
config/tests/parity.rs so the parent stays under the file-size ratchet.
Three cloud runs were lost whole to an empty OpenRouter balance —
h2h891 (16 trials), fivetools5 (30/30), gate89high1 (35/89): the submit
path fanned out one Batch job per trial while every model call inside
them was going to answer HTTP 402, and each container burned compute to
score a 0.0 indistinguishable from an agent loss.

cloud run now asks GET https://openrouter.ai/api/v1/credits (shape
verified against the live endpoint: data.total_credits -
data.total_usage) before anything is uploaded or submitted, and refuses
when the remaining balance is under trials x --est-per-trial (default
$1.00), naming the balance, the projection, and --skip-balance-check
as the override. The check refuses only what it positively knows: a
missing OPENROUTER_API_KEY, a network failure, or a shape surprise
fails OPEN with a printed warning — a broken preflight must never block
a funded run.

Decisions live in the new stdlib-only arenabench/balance.py; the one
HTTP call is injected through CloudExecutor as the credits seam, the
same shape as ls_remote, so the tests neither read the environment nor
reach the network. cloud.py itself stays under the 1500-line ratchet.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Aug 14, 2026 8:48pm

@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a fifth provider parity axis for billing behavior and implements an OpenRouter-specific afford-hinted 402 retry in the Stella model adapter, while introducing an ArenaBench cloud-run preflight that checks OpenRouter credits and refuses clearly when the projected trial spend exceeds the remaining balance, with supporting tests and docs updates.

Sequence diagram for OpenRouter afford-hinted 402 retry in ZaiProvider.complete_inner

sequenceDiagram
    participant Client
    participant ZaiProvider
    participant OpenRouter

    Client->>ZaiProvider: complete_inner(req, observer)
    ZaiProvider->>OpenRouter: complete_attempt(req, observer, retry=false)
    OpenRouter-->>ZaiProvider: ProviderError::Terminal(detail)

    alt rejects_disabled_reasoning(detail) && id == openrouter && req.reasoning == Some(false)
        ZaiProvider->>OpenRouter: complete_attempt(req, observer, retry=true)
        OpenRouter-->>ZaiProvider: CompletionResult
        ZaiProvider-->>Client: CompletionResult
    else serves_openrouter() && afford_capped_max_tokens(detail)
        ZaiProvider->>ZaiProvider: afford_capped_max_tokens(detail)
        ZaiProvider->>ZaiProvider: set reduced.max_output_tokens
        ZaiProvider->>OpenRouter: complete_attempt(reduced, observer, retry=false)
        OpenRouter-->>ZaiProvider: CompletionResult
        ZaiProvider-->>Client: CompletionResult
    else
        ZaiProvider-->>Client: ProviderError::Terminal(detail)
    end
Loading

Sequence diagram for ArenaBench cloud run OpenRouter balance preflight

sequenceDiagram
    actor Operator
    participant CloudExecutor
    participant balance_preflight as balance.preflight
    participant fetch_credits as fetch_openrouter_credits
    participant OpenRouter

    Operator->>CloudExecutor: _cmd_cloud_run(args)
    CloudExecutor->>CloudExecutor: plans = plan_trials(spec)

    alt args.skip_balance_check is False
        CloudExecutor->>balance_preflight: preflight(trials=len(plans), est_per_trial, api_key, fetch=executor.openrouter_credits)
        alt api_key is None
            balance_preflight-->>CloudExecutor: None (check skipped)
        else fetch_openrouter_credits(api_key) raises BalanceUnknownError
            balance_preflight->>CloudExecutor: out("balance   : check skipped — ...")
            balance_preflight-->>CloudExecutor: None
        else remaining_credits < projected_spend
            balance_preflight->>fetch_credits: fetch_openrouter_credits(api_key)
            fetch_credits->>OpenRouter: GET CREDITS_URL
            OpenRouter-->>fetch_credits: JSON credits
            fetch_credits-->>balance_preflight: payload
            balance_preflight-->>CloudExecutor: refusal_message
            CloudExecutor->>Operator: print("error: " + refusal_message)
            CloudExecutor-->>Operator: return 2
        else remaining_credits >= projected_spend
            balance_preflight-->>CloudExecutor: None (balance covers projection)
        end
    end

    opt submission proceeds
        CloudExecutor->>CloudExecutor: resolve_sut, upload, submit
    end
Loading

File-Level Changes

Change Details Files
Introduce BillingPosture parity axis and enforcement tests to capture each provider’s out-of-credit behavior, including OpenRouter’s afford-hinted retry.
  • Define BillingPosture enum with AffordHintRetry and TerminalAbort variants and document each seeded provider’s billing posture in a BILLING_POSTURE matrix.
  • Add billing_posture accessor and tests enforcing witness existence, provider-id uniqueness, and that all parity axes cover identical provider id sets.
  • Extend parity tests to cover the existing StreamFallbackPosture axis with uniqueness and witness-existence checks, completing its enforcement.
crates/stella-model/src/provider_parity.rs
crates/stella-model/README.md
AGENTS.md
Implement OpenRouter 402 afford-hint parsing and a single reduced-max_tokens retry in the shared Zai chat-completions adapter, with coverage for hintless and below-floor cases.
  • Add AFFORD_RETRY_FLOOR_TOKENS constant and afford_hint_tokens/afford_capped_max_tokens helpers to defensively parse the "can only afford N" prose from OpenRouter’s 402 error detail and compute a 90%-of-N retry ceiling subject to a floor.
  • Refactor complete_inner to hang both the existing mandatory-reasoning retry and the new afford-hinted 402 retry off the first attempt’s ProviderError::Terminal detail, gated by serves_openrouter and available afford-capped ceiling.
  • Extend wiremock-based tests to assert that hintless 402s are not retried, that afford-hinted 402s are retried exactly once at the reduced max_tokens value, that below-floor hints stay terminal, and that afford-hint parsing behaves correctly for real, boundary, and malformed messages.
crates/stella-model/src/zai.rs
crates/stella-model/src/zai/tests/error_classify.rs
Add an ArenaBench OpenRouter balance preflight that checks credits via a seam and refuses underfunded cloud runs before any S3 upload or Batch submission, while failing open on unknown balances.
  • Introduce arenabench.balance module implementing remaining_credits, projected_spend, shortfall_message, an HTTP adapter fetch_openrouter_credits using urllib, and a preflight orchestration that emits human-readable diagnostics and only refuses when remaining credits are known and below projection.
  • Wire CloudExecutor to carry an openrouter_credits seam defaulting to fetch_openrouter_credits, and call balance.preflight from _cmd_cloud_run before planning CodeBuild/S3/Batch work, returning rc 2 on refusal and printing an error message naming balance, projection, and the --skip-balance-check override.
  • Extend CLI for cloud run with --est-per-trial and --skip-balance-check flags, and add hermetic tests for preflight’s pure decisions along with integration tests asserting underfunded refusal, funded success with transcript messaging, fail-open behavior on fetch errors and missing OPENROUTER_API_KEY, and that the skip flag bypasses the seam entirely.
arenabench/arenabench/balance.py
arenabench/arenabench/cloud.py
arenabench/tests/test_balance.py
arenabench/tests/test_cloud.py
Move parity completeness tests out of stella-cli config test god-file into a dedicated module to stay under the line-count ratchet while covering all five axes.
  • Create crates/stella-cli/src/config/tests/parity.rs with one completeness test per axis (cache, reasoning, overflow, stream fallback, billing) asserting that every seeded provider declares a posture in stella-model’s parity matrix.
  • Remove the parity-axis completeness tests from the main config tests module and leave other config tests intact, keeping the parent file under the 1500-line guard.
crates/stella-cli/src/config/tests.rs
crates/stella-cli/src/config/tests/parity.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant