diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index e765a99dc..082172e7c 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3413,7 +3413,7 @@ jobs: "$schema": "https://opencode.ai/config.json", "model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5", "small_model": "nvidia-nim/meta/llama-3.3-70b-instruct", - "enabled_providers": ["nvidia-nim", "opencode-free", "opencode", "openai", "openrouter", "github-models"], + "enabled_providers": ["nvidia-nim", "opencode-free", "omniroute", "opencode", "openai", "openrouter", "github-models"], "lsp": false, "mcp": {}, "permission": { @@ -3600,6 +3600,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": { + "combo": { + "name": "OmniRoute Combo (free gateway)", + "tool_call": true, + "limit": { + "context": 200000, + "output": 32768 + } + } + } + }, "opencode": { "npm": "@ai-sdk/openai", "name": "OpenCode Zen", @@ -4037,6 +4055,8 @@ jobs: # opencode.jsonc expects env NVIDIA_API_KEY for nvidia-nim/* models. NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_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" @@ -4057,7 +4077,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 ' || '' }}nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode/gpt-5.6-terra 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/combo ' || '' }}nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode/gpt-5.6-terra 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 e83bb981d..1afda46cd 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_nvidia_nim_candidate() { case "$1" in nvidia-nim/*) return 0 ;; @@ -387,6 +394,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 if is_nvidia_nim_candidate "$model_candidate" && [ -z "${NVIDIA_API_KEY:-}" ]; then printf 'Skipping OpenCode %s because NVIDIA_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate" return 0 @@ -400,7 +411,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 3600)" ;; 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 61af0dd63..9ebbf501f 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" @@ -724,6 +729,7 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' 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/combo ' || ''" "opencode review adds the keyless OmniRoute gateway as an early public-only candidate" assert_file_contains "$workflow_file" "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode/gpt-5.6-terra 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 paid Zen and 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" @@ -865,6 +871,7 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' 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 d5d48b25b..d717b6ad5 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/combo ' || '' }}" ) 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/combo", *candidates_text.removeprefix(conditional_public_candidate).split(), ] candidate_pairs = [candidate.split("/", 1) for candidate in candidates] @@ -137,6 +139,13 @@ 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", "combo"], + ["nvidia-nim", "nvidia/llama-3.3-nemotron-super-49b-v1.5"], + ["nvidia-nim", "nvidia/llama-3.1-nemotron-ultra-253b-v1"], + ["nvidia-nim", "nvidia/nemotron-3-super-120b-a12b"], + ["nvidia-nim", "meta/llama-3.3-70b-instruct"], + ["nvidia-nim", "deepseek-ai/deepseek-v4-pro"], + ["nvidia-nim", "mistralai/codestral-22b-instruct-v0.1"], ["opencode", "gpt-5.6-terra"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], @@ -210,6 +219,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) == {"combo"} + assert omniroute_models["combo"]["tool_call"] is True + assert omniroute_models["combo"]["limit"] == { + "context": 200000, + "output": 32768, + } for model_name, model_config in free_models.items(): if model_config.get("reasoning") is True: assert model_config["options"]["reasoningEffort"] == "high", model_name @@ -240,6 +259,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 @@ -1338,7 +1360,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/combo ' || ''" ) in workflow assert ( "github-models/deepseek/deepseek-v3-0324 " @@ -1440,6 +1463,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 c0e2d1a3e..d863bf3dc 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -796,6 +796,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/combo", + extra_env={"OMNIROUTE_API_BASE_URL": ""}, + ) + + assert result.returncode == 1 + assert ( + "Skipping OpenCode omniroute/combo 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/combo", + ) + + assert result.returncode == 1 + assert ( + "OpenCode omniroute/combo 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: