diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index d2a0d1cc6..94f5cbb7c 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3758,6 +3758,7 @@ jobs: OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "5400" OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700" OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "0" + OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "600" # This installation currently reports a 4k request-body limit for # GitHub Models GPT-5 endpoints even though the public catalog is # larger. Keep the exact runtime failure visible without spending a diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index b709ca807..3c4583069 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -381,6 +381,9 @@ cap_model_run_timeout() { local cap_seconds case "$model_candidate" in + opencode-free/*) + cap_seconds="$(env_integer_or_default OPENCODE_FREE_RUN_TIMEOUT_SECONDS 600)" + ;; github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS 45)" ;; @@ -610,7 +613,7 @@ main() { uncapped_run_timeout="$OPENCODE_RUN_TIMEOUT_SECONDS" OPENCODE_RUN_TIMEOUT_SECONDS="$(cap_model_run_timeout "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS")" if [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -lt "$uncapped_run_timeout" ]; then - printf 'OpenCode %s runtime cap selected %ss instead of %ss because this installation has returned a constrained request-body limit for that endpoint.\n' \ + printf 'OpenCode %s runtime cap selected %ss instead of %ss because this provider has a bounded failover window.\n' \ "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS" "$uncapped_run_timeout" fi export OPENCODE_RUN_TIMEOUT_SECONDS diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index fb9e6d3fb..0e791907f 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1293,6 +1293,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "5400"' in workflow assert 'OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "0"' in workflow + assert 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "600"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow @@ -1351,7 +1352,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "while :" in model_pool_runner assert "should_skip_model_candidate" in model_pool_runner assert "cap_model_run_timeout" in model_pool_runner - assert "constrained request-body limit" in model_pool_runner + assert "bounded failover window" in model_pool_runner assert "run_central_adversarial_harness" not in model_pool_runner assert "finish_pool_without_model" in model_pool_runner assert "central-current-head-adversarial-harness" not in model_pool_runner diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 5e8233487..3fd432242 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -759,7 +759,7 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert result.returncode == 1 assert ( "OpenCode github-models/openai/gpt-5 runtime cap selected 3s instead of 9s " - "because this installation has returned a constrained request-body limit for that endpoint." + "because this provider has a bounded failover window." ) in result.stdout attempt_budget = re.search( r"OpenCode github-models/openai/gpt-5 attempt 1/1 using (\d+)s run timeout " @@ -772,6 +772,24 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert run_timeout <= remaining_budget <= 30 +def test_free_provider_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: + """A stalled free provider cannot consume a full paid-provider cadence slot.""" + result = run_failed_model( + tmp_path, + extra_env={ + "OPENCODE_FREE_RUN_TIMEOUT_SECONDS": "3", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + model_candidates="opencode-free/nemotron-3-ultra-free", + ) + + assert result.returncode == 1 + assert ( + "OpenCode opencode-free/nemotron-3-ultra-free runtime cap selected 3s " + "instead of 9s because this provider has a bounded failover window." + ) in result.stdout + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: