Skip to content

Kill hung runs early when a pinned pool model is delisted#621

Merged
seonghobae merged 3 commits into
mainfrom
seonghobae-pool-watchdog-model-unavailable
Jul 24, 2026
Merged

Kill hung runs early when a pinned pool model is delisted#621
seonghobae merged 3 commits into
mainfrom
seonghobae-pool-watchdog-model-unavailable

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up hardening after #619/#622 pinned OpenRouter models in the OpenCode review pool. Two commits:

Commit 1 — kill hung runs when a pinned model is delisted

  • Watchdog (has_fatal_provider_error_event): also matches model-unavailable signatures (no endpoints, not a valid model, model_not_found, ModelNotFoundError) on structured "type":"error" events, so a delisted model is killed within the poll interval instead of burning the candidate run budget.
  • is_fatal_provider_failure: same signatures → skip remaining attempts (class=model-unavailable in sanitized metadata).
  • Filename sanitization: per-candidate file names also replace : (OpenRouter :free suffix broke Windows filenames / actions/upload-artifact).

Commit 2 — bound paid-provider spend (live incident response)

Run 30120972549 cycled the pool 102 times over ~3h, re-sending the full review prompt to both paid OpenRouter models on every cycle and exhausting the org's entire $20 credit; run 30122360773 then cycled 700+ attempts into HTTP 402 until the retry budget elapsed. Three spend guards, no timing constants touched:

  1. Credit exhaustion is dead-for-run: HTTP 402 / Insufficient credits / payment required (structured error events + stderr diagnostics) marks that candidate failed for the rest of the run — no more per-cycle re-attempts against a provider with no credit. New class=credit-exhausted failure metadata.
  2. Invalid-control-output cap per candidate (OPENCODE_INVALID_CONTROL_OUTPUT_CAP, default 3): control-rejected output now returns a distinct status; a candidate that repeatedly fails control validation is marked failed for the run instead of retrying forever on identical rejections.
  3. Per-run attempt ceiling (OPENCODE_POOL_MAX_TOTAL_ATTEMPTS, default 30 ≈ 3 cycles of the live 10-candidate pool): hard bound on total provider attempts per run. When every candidate is marked failed, the pool exits immediately instead of sleeping between empty cycles.

All guards are env-disable-able (=0). Timeouts, deadlines, and context caps are untouched.

Tests

New: test_delisted_openrouter_model_error_kills_hung_run_early, test_credit_exhausted_402_ends_pool_without_further_spend, test_invalid_control_output_cap_marks_candidate_failed, test_attempt_ceiling_bounds_provider_spend.

tests/test_opencode_model_pool_runner.py: 21 passed; tests/test_opencode_agent_contract.py + shell-syntax: 36 passed (1 deselected locally: pre-existing WSL-bash hang on Windows, unrelated); bash -n clean.

Extend the model-pool watchdog and fatal-failure classification with
model-unavailable signatures (OpenRouter 'No endpoints found' /
'not a valid model ID', OpenAI-style model_not_found,
ProviderModelNotFoundError) so a delisted pinned free model is killed
within the poll interval and skipped instead of burning the candidate
run budget. Sanitize ':' in per-candidate file names so ':free'
candidates survive Windows and actions/upload-artifact. Review timing
(timeouts, deadlines, context caps) is untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 12:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the OpenCode model-pool runner so it can fail over quickly when a pinned OpenRouter free-tier model is delisted (model-unavailable errors), avoiding long hangs that consume the entire retry budget and delay org-wide PR governance workflows.

Changes:

  • Extend fatal-provider detection to include model-unavailable signatures (e.g., “No endpoints found”, “not a valid model”, model_not_found) and treat them as non-retryable for the current model candidate.
  • Classify these failures as class=model-unavailable in sanitized diagnostics while keeping provider-controlled content suppressed.
  • Sanitize per-candidate filenames to replace : as well as /, preventing Windows and actions/upload-artifact path issues for :free model suffixes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
scripts/ci/run_opencode_review_model_pool.sh Detects model-unavailable error events to kill hung runs early, skips futile retries for delisted models, emits model-unavailable diagnostics, and sanitizes : in candidate-derived filenames.
tests/test_opencode_model_pool_runner.py Adds a regression test ensuring an OpenRouter “No endpoints found … :free” error kills a hung run quickly, skips remaining attempts, and logs class=model-unavailable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings July 24, 2026 23:33
Live run 30120972549 cycled the pool 102 times over ~3h re-sending the
full review prompt to paid OpenRouter models and exhausted the org's
entire credit; run 30122360773 kept cycling 700+ attempts into HTTP 402
until the retry budget elapsed. Three spend guards (timing untouched):

- Credit exhaustion (HTTP 402 / 'Insufficient credits' / payment
  required) marks the candidate failed for the rest of the run via
  structured error events and stderr diagnostics, with a new
  class=credit-exhausted failure metadata label.
- Control-rejected output now returns a distinct status and is capped
  per candidate (OPENCODE_INVALID_CONTROL_OUTPUT_CAP, default 3) so a
  model that cannot satisfy the control contract stops burning paid
  requests on identical rejections.
- A per-run provider attempt ceiling
  (OPENCODE_POOL_MAX_TOTAL_ATTEMPTS, default 30 = ~3 cycles of the live
  10-candidate pool) ends the pool as exhausted before unlimited
  cycles can accrue spend; when every candidate is marked failed the
  pool ends immediately instead of sleeping between empty cycles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

fi
[ -s "$opencode_json_file" ] || return 1
grep -Eiq 'budget limit|insufficient_quota' "$opencode_json_file"
grep -Eiq 'budget limit|insufficient_quota|insufficient credits|payment required|model_not_found|model not found|ModelNotFoundError|not a valid model|no endpoints' "$opencode_json_file"
@seonghobae
seonghobae merged commit b613873 into main Jul 24, 2026
48 checks passed
@seonghobae
seonghobae deleted the seonghobae-pool-watchdog-model-unavailable branch July 24, 2026 23:40
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.

2 participants