fix(opencode): free-tier failover timeout hour-class (~3600s) - #683
Conversation
Large-repo free-tier failover was still capped at 600s while primary paths already use ≥3600s. Align OPENCODE_FREE_RUN_TIMEOUT_SECONDS to 3600 in the dispatch workflow and pool default, update contract/quick gate asserts, and document NVIDIA_NIM_API_KEY → NVIDIA_API_KEY mapping for nvidia-nim/* candidates.
📝 WalkthroughWalkthroughOpenCode 무료 모델의 실행 제한 시간이 600초에서 3600초로 늘어났습니다. NVIDIA NIM 인증 키 매핑 주석이 워크플로와 모델 풀 스크립트에 추가되었습니다. 관련 검증 기대값도 갱신되었습니다. ChangesOpenCode 실행 및 인증 설정
Estimated code review effort: 1 (Trivial) | ~5 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/test_opencode_agent_contract.py (1)
1377-1377: 📐 Maintainability & Code Quality | 🔵 Trivial전체 테스트 스위트를 실행하세요.
개발 의존성이 준비된 환경에서
python -m pytest를 실행하세요. 이 계약 테스트는 워크플로와 model pool runner의 timeout 설정을 함께 검증합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_opencode_agent_contract.py` at line 1377, Run the complete test suite with python -m pytest in an environment where all development dependencies are installed, including the contract test covering OPENCODE_FREE_RUN_TIMEOUT_SECONDS and the model pool runner timeout configuration.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/opencode-review-dispatch.yml:
- Line 4092: Adjust the OPENCODE_FREE_RUN_TIMEOUT_SECONDS setting and the
related dynamic pool budget or candidate ordering so all seven opencode-free
candidates can be attempted without exhausting the pool before nvidia-nim or
paid fallbacks run. Preserve fallback execution by ensuring the combined
free-candidate timeout fits within each supported total budget of 2100, 3900,
and 7200 seconds.
---
Nitpick comments:
In `@tests/test_opencode_agent_contract.py`:
- Line 1377: Run the complete test suite with python -m pytest in an environment
where all development dependencies are installed, including the contract test
covering OPENCODE_FREE_RUN_TIMEOUT_SECONDS and the model pool runner timeout
configuration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f4cc43f0-219f-4040-bde4-97efcd8eeac5
📒 Files selected for processing (4)
.github/workflows/opencode-review-dispatch.ymlscripts/ci/run_opencode_review_model_pool.shscripts/ci/test_strix_quick_gate.shtests/test_opencode_agent_contract.py
| OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700" | ||
| OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1" | ||
| OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "600" | ||
| OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
path = Path(".github/workflows/opencode-review-dispatch.yml")
text = path.read_text()
candidate_line = re.search(r"^\s*OPENCODE_MODEL_CANDIDATES:.*$", text, re.MULTILINE).group(0)
free_count = candidate_line.count("opencode-free/")
def value(name: str) -> int:
return int(re.search(rf"^\s*{name}:\s*\"(\d+)\"$", text, re.MULTILINE).group(1))
free_window = free_count * value("OPENCODE_FREE_RUN_TIMEOUT_SECONDS")
budget = min(value("OPENCODE_TOTAL_RETRY_BUDGET_SECONDS"),
value("OPENCODE_POOL_STEP_TIMEOUT_SECONDS"))
if free_window > budget:
raise SystemExit(
f"free candidates require {free_window}s, but the pool budget is {budget}s"
)
PYRepository: ContextualWisdomLab/.github
Length of output: 227
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- workflow references ---'
rg -n -C 8 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|TOTAL_RETRY_BUDGET_SECONDS|POOL_STEP_TIMEOUT_SECONDS)|run_opencode_review_model_pool|free candidate|free 후보' \
.github/workflows/opencode-review-dispatch.yml
printf '%s\n' '--- model-pool script references ---'
rg -n -C 12 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|TOTAL_RETRY_BUDGET_SECONDS|POOL_STEP_TIMEOUT_SECONDS)|timeout|budget|candidate' \
scripts/ci/run_opencode_review_model_pool.sh
printf '%s\n' '--- contract-test references ---'
rg -n -C 10 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|TOTAL_RETRY_BUDGET_SECONDS|POOL_STEP_TIMEOUT_SECONDS)|run_opencode_review_model_pool|timeout|budget|candidate' \
tests/test_opencode_agent_contract.py
printf '%s\n' '--- file outlines ---'
ast-grep outline scripts/ci/run_opencode_review_model_pool.sh --lang bash 2>/dev/null || true
ast-grep outline tests/test_opencode_agent_contract.py --lang python 2>/dev/null || trueRepository: ContextualWisdomLab/.github
Length of output: 50385
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- workflow references ---'
rg -n -C 8 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|TOTAL_RETRY_BUDGET_SECONDS|POOL_STEP_TIMEOUT_SECONDS)|run_opencode_review_model_pool|free candidate|free 후보' \
.github/workflows/opencode-review-dispatch.yml
printf '%s\n' '--- model-pool script references ---'
rg -n -C 12 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|TOTAL_RETRY_BUDGET_SECONDS|POOL_STEP_TIMEOUT_SECONDS)|timeout|budget|candidate' \
scripts/ci/run_opencode_review_model_pool.sh
printf '%s\n' '--- contract-test references ---'
rg -n -C 10 \
'OPENCODE_(MODEL_CANDIDATES|FREE_RUN_TIMEOUT_SECONDS|OPENCODE_TOTAL_RETRY_BUDGET_SECONDS|OPENCODE_POOL_STEP_TIMEOUT_SECONDS)|run_opencode_review_model_pool|timeout|budget|candidate' \
tests/test_opencode_agent_contract.py
printf '%s\n' '--- file outlines ---'
ast-grep outline scripts/ci/run_opencode_review_model_pool.sh --lang bash 2>/dev/null || true
ast-grep outline tests/test_opencode_agent_contract.py --lang python 2>/dev/null || trueRepository: ContextualWisdomLab/.github
Length of output: 50385
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text()
runner = Path("scripts/ci/run_opencode_review_model_pool.sh").read_text()
candidate_line = re.search(
r'^\s*OPENCODE_MODEL_CANDIDATES:\s*"([^"]+)"$', workflow, re.MULTILINE
).group(1)
free_candidates = re.findall(r'\bopencode-free/\S+', candidate_line)
free_timeout = int(re.search(
r'^\s*OPENCODE_FREE_RUN_TIMEOUT_SECONDS:\s*"(\d+)"$',
workflow, re.MULTILINE
).group(1))
outer_timeout = int(re.search(
r'^\s*OPENCODE_POOL_STEP_TIMEOUT_SECONDS:\s*"(\d+)"$',
workflow, re.MULTILINE
).group(1))
# Extract the budgets selected by the dynamic-cadence branches in the runner.
branch_budgets = {
"small": int(re.search(
r'original_run_timeout=.*?'
r'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS (\d+)',
runner, re.DOTALL
).group(1)),
"medium": int(re.search(
r'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS (\d+)',
runner
).group(1)),
"large": int(re.search(
r'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS (\d+)',
runner
).group(1)),
"unknown": int(re.search(
r'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS (\d+)',
runner
).group(1)),
}
print(f"free_candidates={len(free_candidates)}")
print(f"free_timeout={free_timeout}s")
print(f"configured_outer_timeout={outer_timeout}s")
print(f"free_max_window={len(free_candidates) * free_timeout}s")
print(f"dynamic_budgets={branch_budgets}")
# Model the runner's one-attempt, deadline-checked loop when every free
# candidate consumes its allowed timeout. This is arithmetic only; it does
# not execute repository code or contact providers.
for cadence, budget in branch_budgets.items():
elapsed = 0
attempted = 0
for _ in free_candidates:
if elapsed >= budget:
break
elapsed += min(free_timeout, budget - elapsed)
attempted += 1
print(
f"{cadence}: attempted_free_candidates={attempted}, "
f"elapsed={elapsed}s, fallback_reached={'yes' if attempted == len(free_candidates) else 'no'}"
)
PY
printf '%s\n' '--- relevant contract-test range ---'
sed -n '260,380p' tests/test_opencode_agent_contract.pyRepository: ContextualWisdomLab/.github
Length of output: 6462
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text()
runner = Path("scripts/ci/run_opencode_review_model_pool.sh").read_text()
candidate_line = re.search(
r'^\s*OPENCODE_MODEL_CANDIDATES:\s*"([^"]+)"$', workflow, re.MULTILINE
).group(1)
free_candidates = re.findall(r'\bopencode-free/\S+', candidate_line)
free_timeout = int(re.search(
r'^\s*OPENCODE_FREE_RUN_TIMEOUT_SECONDS:\s*"(\d+)"$',
workflow, re.MULTILINE
).group(1))
outer_timeout = int(re.search(
r'^\s*OPENCODE_POOL_STEP_TIMEOUT_SECONDS:\s*"(\d+)"$',
workflow, re.MULTILINE
).group(1))
branch_budgets = {
"small": int(re.search(
r'OPENCODE_SMALL_CHANGE_TOTAL_BUDGET_SECONDS (\d+)', runner
).group(1)),
"medium": int(re.search(
r'OPENCODE_MEDIUM_CHANGE_TOTAL_BUDGET_SECONDS (\d+)', runner
).group(1)),
"large": int(re.search(
r'OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS (\d+)', runner
).group(1)),
"unknown": int(re.search(
r'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS (\d+)', runner
).group(1)),
}
print(f"free_candidates={len(free_candidates)}")
print(f"free_timeout={free_timeout}s")
print(f"configured_outer_timeout={outer_timeout}s")
print(f"free_max_window={len(free_candidates) * free_timeout}s")
print(f"dynamic_budgets={branch_budgets}")
for cadence, budget in branch_budgets.items():
elapsed = 0
attempted = 0
for _ in free_candidates:
if elapsed >= budget:
break
elapsed += min(free_timeout, budget - elapsed)
attempted += 1
print(
f"{cadence}: attempted_free_candidates={attempted}, "
f"elapsed={elapsed}s, fallback_reached="
f"{'yes' if attempted == len(free_candidates) else 'no'}"
)
PY
printf '%s\n' '--- relevant contract-test range ---'
sed -n '260,380p' tests/test_opencode_agent_contract.pyRepository: ContextualWisdomLab/.github
Length of output: 6462
Free 후보의 timeout과 동적 pool 예산을 함께 조정하세요.
공개 PR에서는 7개의 opencode-free/* 후보가 fallback보다 먼저 실행됩니다. 동적 cadence는 전체 예산을 2100초, 3900초 또는 7200초로 줄입니다. Free 후보가 timeout되면 1~2개만 실행한 뒤 pool이 종료되어 nvidia-nim/* 또는 유료 fallback에 도달하지 못합니다. Free timeout, 후보 수·순서 또는 전체 예산을 조정하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/opencode-review-dispatch.yml at line 4092, Adjust the
OPENCODE_FREE_RUN_TIMEOUT_SECONDS setting and the related dynamic pool budget or
candidate ordering so all seven opencode-free candidates can be attempted
without exhausting the pool before nvidia-nim or paid fallbacks run. Preserve
fallback execution by ensuring the combined free-candidate timeout fits within
each supported total budget of 2100, 3900, and 7200 seconds.
Summary
OPENCODE_FREE_RUN_TIMEOUT_SECONDSfrom 600 to 3600 in the OpenCode dispatch workflow and the model-pool default foropencode-free/*failover candidates.NVIDIA_NIM_API_KEY→ envNVIDIA_API_KEYmapping (workflow + pool normalize) sonvidia-nim/*is not skipped when only the org NIM secret is set.Why
Primary/dynamic large-repo OpenCode paths already allow ≥3600s; free-tier failover was still stuck near 600s, aborting long reviews on large repos.
Test plan
pytest tests/test_opencode_agent_contract.py -k free/timeout(local)Summary by CodeRabbit