From 22d367bd5a9b9db1f70e18ba408857f80f5089e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 11:36:07 +0000 Subject: [PATCH] feat(review): add keyless OmniRoute free-gateway provider to the OpenCode review pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The central OpenCode review pool depends on provider keys / free tiers that can lapse (GitHub Models retirement, unset OPENAI/OPENROUTER secrets), which starves the pool of any working model and leaves every PR's opencode-review unable to produce approval evidence. Add OmniRoute — the org's free OpenAI-compatible gateway — as a keyless recovery path that needs no per-provider secret. - enabled_providers gains "omniroute"; a new "omniroute" provider block (@ai-sdk/openai-compatible, model "auto", tool_call, 200000/32768 limit) resolves its base URL from the org variable OMNIROUTE_API_BASE_URL and an optional OMNIROUTE_API_KEY secret — keyless by default. - omniroute/auto is appended to the public-only segment of OPENCODE_MODEL_CANDIDATES (never the private-repo segment): a free gateway with best-effort upstreams must not see private-repo code, matching the existing opencode-free gating. - run_opencode_review_model_pool.sh skips omniroute candidates when OMNIROUTE_API_BASE_URL is unset (fail-safe fall-through) and caps its runtime with the bounded free-provider failover window. - Contract tests (test_opencode_agent_contract.py, test_opencode_model_pool_runner.py, test_strix_quick_gate.sh) updated to pin the new provider, candidate, env wiring, and skip behavior. Activation requires the org to set vars.OMNIROUTE_API_BASE_URL to its OmniRoute deployment; OmniRoute should front at least one tool-calling-capable upstream so the reviewer can emit a structured verdict. Verified: coverage run -m pytest tests -> 712 passed, coverage 100% (fail_under=100), interrogate 100%, scripts/ci/test_strix_quick_gate.sh PASS. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016wtuYFp4E22QnEU1bFMhsr --- .../workflows/opencode-review-dispatch.yml | 24 +++++++++++- scripts/ci/run_opencode_review_model_pool.sh | 13 ++++++- scripts/ci/test_strix_quick_gate.sh | 7 ++++ tests/test_opencode_agent_contract.py | 22 ++++++++++- tests/test_opencode_model_pool_runner.py | 38 +++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 91ecded91..4a6cafc44 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3340,7 +3340,7 @@ jobs: "$schema": "https://opencode.ai/config.json", "model": "github-models/deepseek/deepseek-r1-0528", "small_model": "github-models/deepseek/deepseek-v3-0324", - "enabled_providers": ["opencode-free", "openai", "openrouter", "github-models"], + "enabled_providers": ["opencode-free", "omniroute", "openai", "openrouter", "github-models"], "lsp": false, "mcp": {}, "permission": { @@ -3495,6 +3495,24 @@ jobs: } } }, + "omniroute": { + "npm": "@ai-sdk/openai-compatible", + "name": "OmniRoute Free Gateway", + "options": { + "baseURL": "{env:OMNIROUTE_API_BASE_URL}", + "apiKey": "{env:OMNIROUTE_API_KEY}" + }, + "models": { + "auto": { + "name": "OmniRoute Auto (free gateway)", + "tool_call": true, + "limit": { + "context": 200000, + "output": 32768 + } + } + } + }, "openai": { "npm": "@ai-sdk/openai", "name": "OpenAI (direct)", @@ -3805,6 +3823,8 @@ jobs: # in the opencode.jsonc "openai" provider block. OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OMNIROUTE_API_BASE_URL: ${{ vars.OMNIROUTE_API_BASE_URL }} + OMNIROUTE_API_KEY: ${{ secrets.OMNIROUTE_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -3824,7 +3844,7 @@ jobs: # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free ' || '' }}github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free omniroute/auto ' || '' }}github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 3c4583069..8be74665f 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -345,6 +345,13 @@ is_openrouter_candidate() { esac } +is_omniroute_candidate() { + case "$1" in + omniroute/*) return 0 ;; + *) return 1 ;; + esac +} + is_low_sensitivity_candidate() { case "$1" in openai/*-mini | openai/*-nano | \ @@ -372,6 +379,10 @@ should_skip_model_candidate() { printf 'Skipping OpenCode %s because OPENROUTER_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate" return 0 fi + if is_omniroute_candidate "$model_candidate" && [ -z "${OMNIROUTE_API_BASE_URL:-}" ]; then + printf 'Skipping OpenCode %s because OMNIROUTE_API_BASE_URL is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate" + return 0 + fi return 1 } @@ -381,7 +392,7 @@ cap_model_run_timeout() { local cap_seconds case "$model_candidate" in - opencode-free/*) + opencode-free/* | omniroute/*) cap_seconds="$(env_integer_or_default OPENCODE_FREE_RUN_TIMEOUT_SECONDS 600)" ;; github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index a04ae001d..47b47941e 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -599,6 +599,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'is_private: ${{ steps.validate.outputs.is_private }}' "opencode review carries validated repository privacy into model routing" assert_file_contains "$workflow_file" '"opencode-free"' "opencode review enables its anonymous Zen free provider" assert_file_contains "$workflow_file" '"baseURL": "https://opencode.ai/zen/v1"' "opencode review routes the free provider through the official Zen endpoint" + assert_file_contains "$workflow_file" '"omniroute"' "opencode review enables the keyless OmniRoute free gateway provider" + assert_file_contains "$workflow_file" '"baseURL": "{env:OMNIROUTE_API_BASE_URL}"' "opencode review resolves the OmniRoute base URL from an org variable instead of a hardcoded endpoint" + assert_file_contains "$workflow_file" '"apiKey": "{env:OMNIROUTE_API_KEY}"' "opencode review keeps the OmniRoute key optional through env resolution" + assert_file_contains "$workflow_file" 'OMNIROUTE_API_BASE_URL: ${{ vars.OMNIROUTE_API_BASE_URL }}' "opencode review wires the OmniRoute base URL from the org variable" + assert_file_contains "$workflow_file" 'OMNIROUTE_API_KEY: ${{ secrets.OMNIROUTE_API_KEY }}' "opencode review wires the optional OmniRoute key from secrets" assert_file_contains "$workflow_file" '"north-mini-code-free"' "opencode review declares the current Zen coding model" assert_file_contains "$workflow_file" "needs.validate-pr-metadata.outputs.is_private == 'false'" "opencode review limits data-retaining free models to public repositories" assert_file_matches "$workflow_file" 'uses:[[:space:]]+actions/checkout@[0-9a-fA-F]{40}([[:space:]]|$)' "opencode review workflow pins checkout to a full commit SHA" @@ -719,6 +724,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool" assert_file_not_contains "$workflow_file" "steps.opencode_review_model_pool.outcome == 'success'" "opencode approval gate still runs after model pool failure to publish a reason" assert_file_contains "$workflow_file" "opencode-free/north-mini-code-free" "opencode review starts public repository reviews with a free coding model" + assert_file_contains "$workflow_file" "opencode-free/mimo-v2.5-free omniroute/auto ' || ''" "opencode review adds the keyless OmniRoute gateway as an early public-only candidate" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review retains DeepSeek V3 before full-size GPT fallbacks" assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree" assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback" @@ -860,6 +866,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode model pool has no configured model candidates." "opencode model pool fails fast when no candidates are configured" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OPENAI_API_KEY is not configured" "opencode model pool skips native OpenAI candidates when the org secret is absent" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OPENROUTER_API_KEY is not configured" "opencode model pool skips OpenRouter candidates when the org secret is absent" + assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OMNIROUTE_API_BASE_URL is not configured" "opencode model pool skips the keyless OmniRoute gateway when its base URL is not configured" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "configured max cycle count" "opencode model pool exits before the job timeout after configured cycles" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS:-1500' "opencode model pool keeps a bounded default retry budget unless the workflow explicitly disables it" assert_file_not_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path no longer documents a final exhausted state" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 12ffcb5ef..65cf1e052 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -98,7 +98,8 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "opencode-free/laguna-s-2.1-free " "opencode-free/ling-3.0-flash-free " "opencode-free/big-pickle " - "opencode-free/mimo-v2.5-free ' || '' }}" + "opencode-free/mimo-v2.5-free " + "omniroute/auto ' || '' }}" ) candidates_text = candidates_match.group(1) assert candidates_text.startswith(conditional_public_candidate) @@ -110,6 +111,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "opencode-free/ling-3.0-flash-free", "opencode-free/big-pickle", "opencode-free/mimo-v2.5-free", + "omniroute/auto", *candidates_text.removeprefix(conditional_public_candidate).split(), ] candidate_pairs = [candidate.split("/", 1) for candidate in candidates] @@ -134,6 +136,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ["opencode-free", "ling-3.0-flash-free"], ["opencode-free", "big-pickle"], ["opencode-free", "mimo-v2.5-free"], + ["omniroute", "auto"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["openrouter", "deepseek/deepseek-v3.2"], @@ -197,6 +200,16 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "context": 200000, "output": 32000, } + omniroute_provider = generated_config["provider"]["omniroute"] + assert omniroute_provider["options"]["baseURL"] == "{env:OMNIROUTE_API_BASE_URL}" + assert omniroute_provider["options"]["apiKey"] == "{env:OMNIROUTE_API_KEY}" + omniroute_models = omniroute_provider["models"] + assert set(omniroute_models) == {"auto"} + assert omniroute_models["auto"]["tool_call"] is True + assert omniroute_models["auto"]["limit"] == { + "context": 200000, + "output": 32768, + } assert github_candidate_models == [ "deepseek/deepseek-v3-0324", "openai/gpt-4.1", @@ -218,6 +231,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert '"apiKey": "{env:OPENAI_API_KEY}"' in workflow assert '"openrouter": {' in workflow assert '"apiKey": "{env:OPENROUTER_API_KEY}"' in workflow + assert '"omniroute": {' in workflow + assert '"baseURL": "{env:OMNIROUTE_API_BASE_URL}"' in workflow + assert '"apiKey": "{env:OMNIROUTE_API_KEY}"' in workflow for model_name in direct_openai_models + openrouter_models + github_candidate_models: assert f'"{model_name}": {{' in workflow @@ -1299,7 +1315,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): "opencode-free/laguna-s-2.1-free " "opencode-free/ling-3.0-flash-free " "opencode-free/big-pickle " - "opencode-free/mimo-v2.5-free ' || ''" + "opencode-free/mimo-v2.5-free " + "omniroute/auto ' || ''" ) in workflow assert ( "github-models/deepseek/deepseek-v3-0324 " @@ -1401,6 +1418,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "is_low_sensitivity_candidate" in model_pool_runner assert "mini/nano review models are disabled" in model_pool_runner assert "OPENAI_API_KEY is not configured" in model_pool_runner + assert "OMNIROUTE_API_BASE_URL is not configured" in model_pool_runner assert "configured max cycle count" in model_pool_runner assert ( "OpenCode dynamic review cadence selected %ss per attempt" in model_pool_runner diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 3fd432242..fde2de50e 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -790,6 +790,44 @@ def test_free_provider_runtime_cap_preserves_queue_budget(tmp_path: Path) -> Non ) in result.stdout +def test_omniroute_candidate_requires_base_url(tmp_path: Path) -> None: + """The keyless OmniRoute gateway is skipped when its base URL is not configured.""" + result = run_failed_model( + tmp_path, + model_candidates="omniroute/auto", + extra_env={"OMNIROUTE_API_BASE_URL": ""}, + ) + + assert result.returncode == 1 + assert ( + "Skipping OpenCode omniroute/auto because OMNIROUTE_API_BASE_URL is not " + "configured; falling back to the next provider-qualified candidate." + ) in result.stdout + assert ( + "OpenCode model pool exhausted before producing a valid control conclusion." + in result.stdout + ) + + +def test_omniroute_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: + """A stalled keyless OmniRoute gateway cannot consume a full paid-provider slot.""" + result = run_failed_model( + tmp_path, + extra_env={ + "OMNIROUTE_API_BASE_URL": "https://omniroute.example/v1", + "OPENCODE_FREE_RUN_TIMEOUT_SECONDS": "3", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + model_candidates="omniroute/auto", + ) + + assert result.returncode == 1 + assert ( + "OpenCode omniroute/auto 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: