From 4076f39c7f0668a67088e7d94fa1fd6470e0804b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 30 Jul 2026 17:22:01 +0900 Subject: [PATCH 01/10] feat(review): add NVIDIA NIM provider --- .../workflows/opencode-review-dispatch.yml | 26 +++++++++++++--- scripts/ci/run_opencode_review_model_pool.sh | 13 +++++++- scripts/ci/test_strix_quick_gate.sh | 6 ++++ tests/test_opencode_agent_contract.py | 9 ++++++ tests/test_opencode_model_pool_runner.py | 31 +++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 1966a7562..ee5555a70 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3341,7 +3341,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", "nvidia-nim", "openai", "openrouter", "github-models"], "lsp": false, "mcp": {}, "permission": { @@ -3528,6 +3528,24 @@ jobs: } } }, + "nvidia-nim": { + "npm": "@ai-sdk/openai-compatible", + "name": "NVIDIA NIM", + "options": { + "baseURL": "https://integrate.api.nvidia.com/v1", + "apiKey": "{env:NVIDIA_NIM_API_KEY}" + }, + "models": { + "nvidia/nemotron-3-ultra-550b-a55b": { + "name": "NVIDIA Nemotron 3 Ultra 550B A55B", + "tool_call": true, + "limit": { + "context": 1000000, + "output": 32768 + } + } + } + }, "openai": { "npm": "@ai-sdk/openai", "name": "OpenAI (direct)", @@ -3838,6 +3856,7 @@ jobs: # in the opencode.jsonc "openai" provider block. OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -3846,8 +3865,7 @@ jobs: # Their trial/free-period data # may be logged, retained, or used for product/model improvement, so # private repositories never include them. The pool then falls - # through to - # DeepSeek V3, the direct GPT-5.6 Luna slot, pinned PAID + # through to NVIDIA NIM, DeepSeek V3, the direct GPT-5.6 Luna slot, pinned PAID # OpenRouter coder models (free-tier candidates hit the shared # free-models-per-day cap and hung for the full candidate timeout, # so the OpenRouter slots use cheap paid models billed against the @@ -3857,7 +3875,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 ' || '' }}nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b 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..fcb7ae84e 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_nvidia_nim_candidate() { + case "$1" in + nvidia-nim/*) 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_nvidia_nim_candidate "$model_candidate" && [ -z "${NVIDIA_NIM_API_KEY:-}" ]; then + printf 'Skipping OpenCode %s because NVIDIA_NIM_API_KEY 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/* | nvidia-nim/*) 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 c05353570..01ab19476 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -599,6 +599,10 @@ 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" '"nvidia-nim"' "opencode review enables its NVIDIA NIM provider" + assert_file_contains "$workflow_file" '"baseURL": "https://integrate.api.nvidia.com/v1"' "opencode review routes NVIDIA NIM through its official hosted endpoint" + assert_file_contains "$workflow_file" '"apiKey": "{env:NVIDIA_NIM_API_KEY}"' "opencode review resolves NVIDIA NIM credentials at runtime" + assert_file_contains "$workflow_file" 'NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' "opencode review exposes NVIDIA NIM credentials only to the model runtime" 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" @@ -860,6 +864,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" "NVIDIA_NIM_API_KEY is not configured" "opencode model pool skips NVIDIA NIM candidates when the org secret is absent" 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" @@ -1197,6 +1202,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review starts the central model pool" + assert_file_contains "$workflow_file" "nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b" "opencode review tries the current NVIDIA NIM Nemotron model" 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 starts with DeepSeek V3 before full-size GPT fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index bd300a839..ccf946a74 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -134,6 +134,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"], + ["nvidia-nim", "nvidia/nemotron-3-ultra-550b-a55b"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["openrouter", "deepseek/deepseek-v3.2"], @@ -160,6 +161,14 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ) assert generated_config_match is not None generated_config = json.loads(generated_config_match.group(1)) + nvidia_provider = generated_config["provider"]["nvidia-nim"] + assert nvidia_provider["options"] == { + "baseURL": "https://integrate.api.nvidia.com/v1", + "apiKey": "{env:NVIDIA_NIM_API_KEY}", + } + assert nvidia_provider["models"]["nvidia/nemotron-3-ultra-550b-a55b"][ + "limit" + ] == {"context": 1000000, "output": 32768} free_models = generated_config["provider"]["opencode-free"]["models"] assert set(free_models) == { "nemotron-3-ultra-free", diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 3fd432242..e4949e67a 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -790,6 +790,37 @@ def test_free_provider_runtime_cap_preserves_queue_budget(tmp_path: Path) -> Non ) in result.stdout +def test_nvidia_nim_candidate_requires_key(tmp_path: Path) -> None: + """NVIDIA NIM is skipped cleanly when its scoped credential is unavailable.""" + result = run_failed_model( + tmp_path, + model_candidates="nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", + ) + + assert result.returncode == 1 + assert "NVIDIA_NIM_API_KEY is not configured" in result.stdout + assert "attempt 1/1" not in result.stdout + + +def test_nvidia_nim_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: + """A stalled hosted NIM cannot consume a full paid-provider cadence slot.""" + result = run_failed_model( + tmp_path, + extra_env={ + "NVIDIA_NIM_API_KEY": "fake-nvidia-key", + "OPENCODE_FREE_RUN_TIMEOUT_SECONDS": "3", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + model_candidates="nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", + ) + + assert result.returncode == 1 + assert ( + "OpenCode nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b 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: From bea81270085fa06fddfbd9b930c9cda1164773e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 30 Jul 2026 17:47:38 +0900 Subject: [PATCH 02/10] feat(review): route public scanners through NVIDIA NIM --- .github/workflows/noema-review.yml | 24 +++++++ .../workflows/opencode-review-dispatch.yml | 2 +- .github/workflows/strix.yml | 70 ++++++++++++++++--- ...opencode_failed_check_fallback_findings.sh | 6 +- scripts/ci/test_strix_quick_gate.sh | 13 +++- tests/test_opencode_agent_contract.py | 11 ++- .../test_required_workflow_queue_contract.py | 5 ++ 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index e52371d43..dd6156274 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -250,6 +250,23 @@ jobs: echo "::add-mask::$app_token" echo "token=$app_token" >>"$GITHUB_OUTPUT" + - name: Resolve Noema target repository visibility + if: env.PR_NUMBER != '' + id: target_visibility + env: + GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + run: | + set -euo pipefail + is_private="$(gh api "repos/${TARGET_REPOSITORY}" --jq '.private')" + case "$is_private" in + true | false) ;; + *) + echo "::error::Noema target repository visibility did not resolve to true or false." + exit 1 + ;; + esac + echo "is_private=$is_private" >>"$GITHUB_OUTPUT" + - name: Run Noema LLM review and submit verdict if: env.PR_NUMBER != '' env: @@ -258,6 +275,8 @@ jobs: NOEMA_LLM_API_URL: ${{ vars.NOEMA_LLM_API_URL || '' }} NOEMA_LLM_MODEL: ${{ vars.NOEMA_LLM_MODEL || '' }} NOEMA_LLM_API_KEY: ${{ secrets.NOEMA_LLM_API_KEY || secrets.OPENAI_API_KEY || '' }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + TARGET_REPOSITORY_PRIVATE: ${{ steps.target_visibility.outputs.is_private }} run: | set -euo pipefail if [ -z "${PR_NUMBER:-}" ]; then @@ -268,6 +287,11 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot submit a verdict." exit 1 fi + if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && [ -z "${NOEMA_LLM_API_URL:-}" ] && [ -z "${NOEMA_LLM_MODEL:-}" ]; then + export NOEMA_LLM_API_URL="https://integrate.api.nvidia.com/v1/chat/completions" + export NOEMA_LLM_MODEL="nvidia/nemotron-3-ultra-550b-a55b" + export NOEMA_LLM_API_KEY="${NVIDIA_NIM_API_KEY:-}" + fi if [ -z "${NOEMA_LLM_API_URL:-}" ] || [ -z "${NOEMA_LLM_MODEL:-}" ] || [ -z "${NOEMA_LLM_API_KEY:-}" ]; then echo "::error::Noema LLM is unconfigured: NOEMA_LLM_API_URL, NOEMA_LLM_MODEL, and NOEMA_LLM_API_KEY (or OPENAI_API_KEY) are required." exit 1 diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index ee5555a70..2ec38f6b8 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3875,7 +3875,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/nemotron-3-ultra-550b-a55b 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 nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b ' || '' }}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/.github/workflows/strix.yml b/.github/workflows/strix.yml index afc040246..4f95a31f6 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -258,6 +258,27 @@ jobs: echo "token=$app_token" } >>"$GITHUB_OUTPUT" + - name: Resolve target repository visibility + id: target_visibility + env: + GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }} + run: | + set -euo pipefail + if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]]; then + echo "::error::Strix target repository must belong to ContextualWisdomLab." + exit 1 + fi + is_private="$(gh api "repos/${TARGET_REPOSITORY}" --jq '.private')" + case "$is_private" in + true | false) ;; + *) + echo "::error::Target repository visibility did not resolve to true or false." + exit 1 + ;; + esac + echo "is_private=$is_private" >>"$GITHUB_OUTPUT" + - name: Materialize target workspace if: github.event_name != 'repository_dispatch' env: @@ -422,11 +443,13 @@ jobs: - name: Gate Strix secrets id: gate env: - STRIX_MODEL: ${{ github.event.client_payload.strix_llm || 'gpt-5.6-luna' }} + STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna') }} STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} STRIX_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + STRIX_NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} STRIX_VERTEX_CREDENTIALS: ${{ secrets.GCP_SA_KEY }} STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} + TARGET_REPOSITORY_PRIVATE: ${{ steps.target_visibility.outputs.is_private }} run: | strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" case "$strix_model" in @@ -469,6 +492,20 @@ jobs: exit 1 fi ;; + nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b) + if [ "$TARGET_REPOSITORY_PRIVATE" != "false" ]; then + echo '::error::NVIDIA NIM hosted trial scans are limited to public repositories.' + exit 1 + fi + echo 'enabled=true' >> "$GITHUB_OUTPUT" + echo 'provider_mode=nvidia_nim' >> "$GITHUB_OUTPUT" + sanitized_nvidia_key="$(printf '%s' "$STRIX_NVIDIA_NIM_API_KEY" | tr -d '\r\n')" + trimmed_nvidia_key="$(printf '%s' "$sanitized_nvidia_key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ -z "$trimmed_nvidia_key" ]; then + echo '::error::NVIDIA_NIM_API_KEY is required for Strix NVIDIA NIM scans.' + exit 1 + fi + ;; vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash) echo 'enabled=true' >> "$GITHUB_OUTPUT" echo 'provider_mode=vertex_ai' >> "$GITHUB_OUTPUT" @@ -480,7 +517,7 @@ jobs: fi ;; *) - echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.' + echo '::error::STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.' exit 1 ;; esac @@ -550,7 +587,7 @@ jobs: - name: Mask LLM API key if: steps.gate.outputs.enabled == 'true' env: - LLM_API_KEY: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || '' }} + LLM_API_KEY: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || steps.gate.outputs.provider_mode == 'nvidia_nim' && secrets.NVIDIA_NIM_API_KEY || '' }} run: | # Sanitize CR/LF before masking to prevent broken ::add-mask:: # commands and potential workflow command injection. @@ -566,7 +603,7 @@ jobs: - name: Prepare LLM API key input file if: steps.gate.outputs.enabled == 'true' env: - LLM_API_KEY_SECRET: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || '' }} + LLM_API_KEY_SECRET: ${{ steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token) || steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY) || steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY || steps.gate.outputs.provider_mode == 'nvidia_nim' && secrets.NVIDIA_NIM_API_KEY || '' }} PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }} run: | sanitized="$(printf '%s' "$LLM_API_KEY_SECRET" | tr -d '\r\n')" @@ -583,6 +620,10 @@ jobs: echo '::error::OPENROUTER_API_KEY is required for Strix OpenRouter scans.' exit 1 fi + if [ -z "$trimmed" ] && [ "$PROVIDER_MODE" = "nvidia_nim" ]; then + echo '::error::NVIDIA_NIM_API_KEY is required for Strix NVIDIA NIM scans.' + exit 1 + fi umask 077 llm_api_key_file="$RUNNER_TEMP/llm_api_key.txt" printf '%s' "$trimmed" > "$llm_api_key_file" @@ -596,6 +637,14 @@ jobs: printf '%s' 'https://openrouter.ai/api/v1' > "$llm_api_base_file" echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" + - name: Prepare NVIDIA NIM API base + if: steps.gate.outputs.provider_mode == 'nvidia_nim' + run: | + umask 077 + llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt" + printf '%s' 'https://integrate.api.nvidia.com/v1' > "$llm_api_base_file" + echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" + - name: Prepare GitHub Models API base if: steps.gate.outputs.provider_mode == 'github_models' run: | @@ -605,7 +654,7 @@ jobs: echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV" - name: Prepare GitHub Models fallback credentials - if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter' + if: steps.gate.outputs.provider_mode == 'openai_direct' || steps.gate.outputs.provider_mode == 'openrouter' || steps.gate.outputs.provider_mode == 'nvidia_nim' env: GITHUB_MODELS_FALLBACK_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} run: | @@ -680,7 +729,7 @@ jobs: - name: Prepare Strix model input file if: steps.gate.outputs.enabled == 'true' env: - STRIX_MODEL: ${{ github.event.client_payload.strix_llm || 'gpt-5.6-luna' }} + STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna') }} run: | umask 077 strix_llm_file="$RUNNER_TEMP/strix_llm.txt" @@ -709,11 +758,14 @@ jobs: openrouter/free | openrouter/openrouter/free) printf '%s' 'openrouter/free' > "$strix_llm_file" ;; + nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b) + printf '%s' "$strix_model" > "$strix_llm_file" + ;; vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash) printf '%s' "$strix_model" > "$strix_llm_file" ;; *) - echo '::error::STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.' + echo '::error::STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model.' exit 1 ;; esac @@ -732,7 +784,7 @@ jobs: STRIX_LLM_FILE: ${{ env.STRIX_LLM_FILE }} STRIX_REPO_ROOT: ${{ runner.temp }}/trusted-workspace LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }} - STRIX_LLM_DEFAULT_PROVIDER: ${{ steps.gate.outputs.provider_mode == 'vertex_ai' && 'vertex_ai' || 'openai' }} + STRIX_LLM_DEFAULT_PROVIDER: ${{ steps.gate.outputs.provider_mode == 'vertex_ai' && 'vertex_ai' || steps.gate.outputs.provider_mode == 'nvidia_nim' && 'nvidia_nim' || 'openai' }} LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }} GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: ${{ env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE }} @@ -750,7 +802,7 @@ jobs: STRIX_LLM_MAX_RETRIES: 1 STRIX_TRANSIENT_RETRY_PER_MODEL: 2 STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60 - STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }} + STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'nvidia_nim' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }} STRIX_GITHUB_MODELS_API_BASE_FILE: ${{ env.STRIX_GITHUB_MODELS_API_BASE_FILE }} STRIX_GITHUB_MODELS_KEY_FILE: ${{ env.STRIX_GITHUB_MODELS_KEY_FILE }} STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" diff --git a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh index c6db1a2db..ccd35a273 100755 --- a/scripts/ci/emit_opencode_failed_check_fallback_findings.sh +++ b/scripts/ci/emit_opencode_failed_check_fallback_findings.sh @@ -956,13 +956,13 @@ extract_strix_failed_check_block "$EVIDENCE_FILE" "$strix_evidence_file" emit_known_missing_string_finding \ "$EVIDENCE_FILE" \ - "github.event.client_payload.strix_llm || 'gpt-5.6-luna'" \ - "Strix PR scans must default to direct OpenAI GPT-5.6 Luna" \ + "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" \ + "Strix public scans must default to NVIDIA NIM while private scans retain the contracted provider" \ ".github/workflows/strix.yml" \ "scripts/ci/test_strix_quick_gate.sh" emit_known_missing_string_finding \ "$EVIDENCE_FILE" \ - "STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" \ + "STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" \ "Strix unsupported-model errors must name the allowed providers" \ ".github/workflows/strix.yml" \ "scripts/ci/test_strix_quick_gate.sh" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 389900d53..08e31a6a7 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -226,6 +226,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "Fetch pull request head for trusted scan" "strix workflow fetches PR head without checkout" assert_file_contains "$workflow_file" "github.event.client_payload.pr_number" "strix workflow consumes default-branch PR-scope evidence payloads" assert_file_contains "$workflow_file" "github.event.client_payload.strix_llm" "strix workflow accepts only repository-dispatch Strix model overrides" + assert_file_contains "$workflow_file" "Resolve target repository visibility" "strix workflow resolves target privacy before selecting hosted trial providers" + assert_file_contains "$workflow_file" "NVIDIA NIM hosted trial scans are limited to public repositories" "strix workflow blocks NVIDIA hosted trial scans for private repositories" assert_file_contains "$workflow_file" "github.event.client_payload.pr_number" "strix workflow can run PR-scoped repository_dispatch evidence" assert_file_contains "$workflow_file" "PR number and head SHA are required for trusted PR-scope Strix evidence" "strix workflow fails closed when manual PR-scope metadata is incomplete" assert_file_contains "$workflow_file" '[[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]' "strix workflow validates PR head SHA before trusted fetch" @@ -278,9 +280,9 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" "STRIX_TOTAL_TIMEOUT_SECONDS:" "strix workflow must not expose total timeout env names in GitHub logs" assert_file_not_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH" "strix workflow must not split Strix PR evidence into separate scanner runs" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM == 'vertex_ai/gemini-3.1-pro-preview-customtools' && 'vertex_ai/gemini-2.5-flash'" "strix workflow must not quarantine the approved Vertex preview model after organization secret visibility is fixed" - assert_file_contains "$workflow_file" "github.event.client_payload.strix_llm || 'gpt-5.6-luna'" "strix workflow defaults PR Strix scans to direct OpenAI GPT-5.6 Luna" + assert_file_contains "$workflow_file" "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" "strix workflow defaults public scans to NVIDIA NIM and keeps private scans on the contracted provider" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" - assert_file_contains "$workflow_file" "STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" + assert_file_contains "$workflow_file" "STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" assert_file_contains "$workflow_file" "vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)" "strix workflow accepts only exact approved organization Vertex AI models" assert_file_contains "$workflow_file" 'STRIX_VERTEX_FALLBACK_MODELS: ""' "strix workflow disables silent Vertex fallbacks so timeout-class failures fail closed" assert_file_contains "$workflow_file" 'STRIX_FAIL_ON_PROVIDER_SIGNAL: "1"' "strix workflow fails closed on timeout, fatal, warning, denied, or provider failure signals" @@ -309,28 +311,33 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "provider_mode=openai_direct" "strix workflow requires direct OpenAI GPT-5 credentials" assert_file_contains "$workflow_file" "provider_mode=github_models" "strix workflow supports GitHub Models provider mode" assert_file_contains "$workflow_file" "provider_mode=openrouter" "strix workflow supports OpenRouter provider mode" + assert_file_contains "$workflow_file" "provider_mode=nvidia_nim" "strix workflow supports NVIDIA NIM provider mode" assert_file_contains "$workflow_file" 'STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}' "strix workflow prefers the organization GitHub Models token secret and falls back to GITHUB_TOKEN" assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'github_models' && (secrets.STRIX_GITHUB_MODELS_TOKEN || github.token)" "strix workflow keeps GitHub Models key routing in provider-scoped key material" assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'openai_direct' && (secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY)" "strix workflow keeps direct OpenAI key routing in provider-scoped key material" assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'openrouter' && secrets.OPENROUTER_API_KEY" "strix workflow includes OpenRouter key routing in provider-scoped key material" + assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'nvidia_nim' && secrets.NVIDIA_NIM_API_KEY" "strix workflow includes NVIDIA NIM key routing in provider-scoped key material" assert_file_not_contains "$workflow_file" "secrets.LLM_API_KEY" "strix workflow must not expose generic LLM_API_KEY for Vertex scans" assert_file_contains "$workflow_file" "STRIX_GITHUB_MODELS_TOKEN is required for GitHub Models Strix scans" "strix workflow fails closed when GitHub Models credentials are absent" assert_file_contains "$workflow_file" "STRIX_OPENAI_API_KEY is required for Strix OpenAI Platform scans" "strix workflow fails closed when direct credentials are absent" assert_file_contains "$workflow_file" "OPENROUTER_API_KEY is required for Strix OpenRouter scans" "strix workflow fails closed when OpenRouter credentials are absent" + assert_file_contains "$workflow_file" "NVIDIA_NIM_API_KEY is required for Strix NVIDIA NIM scans" "strix workflow fails closed when NVIDIA credentials are absent" assert_file_contains "$workflow_file" 'PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }}' "strix workflow passes provider mode through env" assert_file_not_contains "$workflow_file" '[ "${{ steps.gate.outputs.provider_mode }}" = "openai_direct" ]' "strix workflow does not interpolate provider mode inside shell condition" assert_file_contains "$workflow_file" "STRIX_REASONING_EFFORT: high" "strix workflow uses high reasoning effort when the selected provider/model supports it" assert_file_contains "$workflow_file" 'trimmed_openai_key="$(printf '"'"'%s'"'"' "$sanitized_openai_key" | sed '"'"'s/^[[:space:]]*//;s/[[:space:]]*$//'"'"')"' "strix workflow trims whitespace-only OpenAI keys before gate validation" assert_file_contains "$workflow_file" 'printf '"'"'%s'"'"' "$trimmed" > "$llm_api_key_file"' "strix workflow writes trimmed provider API keys into the trusted input file" - assert_file_contains "$workflow_file" 'STRIX_LLM_DEFAULT_PROVIDER: ${{ steps.gate.outputs.provider_mode == '"'"'vertex_ai'"'"' && '"'"'vertex_ai'"'"' || '"'"'openai'"'"' }}' "strix workflow selects the correct default provider" + assert_file_contains "$workflow_file" 'STRIX_LLM_DEFAULT_PROVIDER: ${{ steps.gate.outputs.provider_mode == '"'"'vertex_ai'"'"' && '"'"'vertex_ai'"'"' || steps.gate.outputs.provider_mode == '"'"'nvidia_nim'"'"' && '"'"'nvidia_nim'"'"' || '"'"'openai'"'"' }}' "strix workflow selects the correct default provider" assert_file_contains "$workflow_file" "Prepare GitHub Models API base" "strix workflow prepares the GitHub Models API base only for GitHub Models mode" assert_file_contains "$workflow_file" "https://models.github.ai/inference" "strix workflow routes GitHub Models scans to the inference endpoint" assert_file_contains "$workflow_file" "Prepare OpenRouter API base" "strix workflow prepares the OpenRouter API base when OpenRouter mode is selected" assert_file_contains "$workflow_file" "https://openrouter.ai/api/v1" "strix workflow routes OpenRouter scans to the OpenRouter API endpoint" + assert_file_contains "$workflow_file" "https://integrate.api.nvidia.com/v1" "strix workflow routes NVIDIA NIM scans to the hosted endpoint" assert_file_contains "$workflow_file" "LLM_API_BASE_FILE" "strix workflow passes the GitHub Models API base through a trusted input file" assert_file_not_contains "$workflow_file" '${{ secrets.STRIX_OPENAI_API_KEY || github.token }}' "strix workflow must not use fallback-secret syntax for LLM API keys" assert_file_contains "$workflow_file" "github_models/openai/o3 github_models/openai/gpt-5-chat" "strix workflow keeps GitHub Models fallback on tool-capable OpenAI models without GPT-4.1 downgrade" assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat'" "strix workflow gives direct-OpenAI scans GitHub Models fallbacks so provider quota outages degrade instead of skipping" + assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'nvidia_nim' && 'github_models/openai/o3 github_models/openai/gpt-5-chat'" "strix workflow gives NVIDIA NIM scans contracted fallbacks" assert_file_contains "$workflow_file" "Prepare GitHub Models fallback credentials" "strix workflow provisions GitHub Models fallback credentials for direct-OpenAI scans" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_KEY_FILE" "strix gate reads the optional GitHub Models fallback key file" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_API_BASE_FILE" "strix gate routes github_models fallback models through the GitHub Models endpoint" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index ccf946a74..05b641a9d 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 " + "nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b ' || '' }}" ) 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", + "nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", *candidates_text.removeprefix(conditional_public_candidate).split(), ] candidate_pairs = [candidate.split("/", 1) for candidate in candidates] @@ -126,6 +128,10 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] assert candidate_pairs + assert all( + not candidate.startswith("nvidia-nim/") + for candidate in candidates_text.removeprefix(conditional_public_candidate).split() + ) assert candidate_pairs == [ ["opencode-free", "nemotron-3-ultra-free"], ["opencode-free", "deepseek-v4-flash-free"], @@ -1314,7 +1320,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 " + "nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b ' || ''" ) in workflow assert ( "github-models/deepseek/deepseek-v3-0324 " diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 1ea6376fe..dae188cbd 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -376,6 +376,11 @@ def test_noema_review_credentials_and_llm_configuration_fail_closed() -> None: "NOEMA_LLM_API_KEY: ${{ secrets.NOEMA_LLM_API_KEY || secrets.OPENAI_API_KEY || '' }}" in workflow ) + assert "Resolve Noema target repository visibility" in workflow + assert 'if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ]' in workflow + assert "https://integrate.api.nvidia.com/v1/chat/completions" in workflow + assert 'export NOEMA_LLM_MODEL="nvidia/nemotron-3-ultra-550b-a55b"' in workflow + assert "NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow assert "Noema LLM is unconfigured:" in workflow assert "mark_unconfigured()" not in workflow assert "review skipped until Noema is deployed" not in workflow From 8d7f9f3130767b60d9b7b091a69098e1d5ffede6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 30 Jul 2026 18:35:51 +0900 Subject: [PATCH 03/10] fix(review): preserve provider fallbacks --- .github/workflows/noema-review.yml | 2 +- .github/workflows/strix.yml | 4 ++ scripts/ci/test_strix_quick_gate.sh | 1 + .../test_required_workflow_queue_contract.py | 71 ++++++++++++++++++- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index dd6156274..59b25e343 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -287,7 +287,7 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot submit a verdict." exit 1 fi - if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && [ -z "${NOEMA_LLM_API_URL:-}" ] && [ -z "${NOEMA_LLM_MODEL:-}" ]; then + if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && [ -n "${NVIDIA_NIM_API_KEY:-}" ] && [ -z "${NOEMA_LLM_API_URL:-}" ] && [ -z "${NOEMA_LLM_MODEL:-}" ]; then export NOEMA_LLM_API_URL="https://integrate.api.nvidia.com/v1/chat/completions" export NOEMA_LLM_MODEL="nvidia/nemotron-3-ultra-550b-a55b" export NOEMA_LLM_API_KEY="${NVIDIA_NIM_API_KEY:-}" diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 4f95a31f6..da69668db 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -444,6 +444,7 @@ jobs: id: gate env: STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna') }} + STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }} STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} STRIX_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} STRIX_NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} @@ -452,6 +453,9 @@ jobs: TARGET_REPOSITORY_PRIVATE: ${{ steps.target_visibility.outputs.is_private }} run: | strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]; then + strix_model="gpt-5.6-luna" + fi case "$strix_model" in openai/gpt-5-mini* | openai/gpt-5-nano* | \ openai/openai/gpt-5-mini* | openai/openai/gpt-5-nano* | \ diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 08e31a6a7..594cb52a6 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -281,6 +281,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH" "strix workflow must not split Strix PR evidence into separate scanner runs" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM == 'vertex_ai/gemini-3.1-pro-preview-customtools' && 'vertex_ai/gemini-2.5-flash'" "strix workflow must not quarantine the approved Vertex preview model after organization secret visibility is fixed" assert_file_contains "$workflow_file" "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" "strix workflow defaults public scans to NVIDIA NIM and keeps private scans on the contracted provider" + assert_file_contains "$workflow_file" 'if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]' "strix workflow falls back to the contracted provider when the NVIDIA secret is absent" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" assert_file_contains "$workflow_file" "STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" assert_file_contains "$workflow_file" "vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)" "strix workflow accepts only exact approved organization Vertex AI models" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index dae188cbd..ce1962bd9 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1,4 +1,5 @@ import json +import os import shlex import shutil import subprocess @@ -377,7 +378,10 @@ def test_noema_review_credentials_and_llm_configuration_fail_closed() -> None: in workflow ) assert "Resolve Noema target repository visibility" in workflow - assert 'if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ]' in workflow + assert ( + 'if [ "$TARGET_REPOSITORY_PRIVATE" = "false" ] && ' + '[ -n "${NVIDIA_NIM_API_KEY:-}" ]' + ) in workflow assert "https://integrate.api.nvidia.com/v1/chat/completions" in workflow assert 'export NOEMA_LLM_MODEL="nvidia/nemotron-3-ultra-550b-a55b"' in workflow assert "NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow @@ -387,6 +391,71 @@ def test_noema_review_credentials_and_llm_configuration_fail_closed() -> None: assert "Noema app token is unavailable; review skipped." not in workflow +def test_nvidia_nim_defaults_preserve_existing_fallbacks_without_secret( + tmp_path: Path, +) -> None: + strix_output = tmp_path / "strix-output" + strix = subprocess.run( + [ + "bash", + "-c", + textwrap.dedent( + workflow_step(workflow_text("strix.yml"), "Gate Strix secrets") + .split(" run: |\n", 1)[1] + ), + ], + env={ + **os.environ, + "GITHUB_OUTPUT": str(strix_output), + "STRIX_MODEL": "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b", + "STRIX_MODEL_REQUESTED": "", + "STRIX_OPENAI_API_KEY": "synthetic-openai-key", + "STRIX_OPENROUTER_API_KEY": "", + "STRIX_NVIDIA_NIM_API_KEY": "", + "STRIX_VERTEX_CREDENTIALS": "", + "STRIX_GITHUB_MODELS_TOKEN": "synthetic-models-token", + "TARGET_REPOSITORY_PRIVATE": "false", + }, + capture_output=True, + text=True, + check=False, + ) + assert strix.returncode == 0, strix.stderr + assert "provider_mode=openai_direct" in strix_output.read_text() + + noema_probe = tmp_path / "noema-key" + noema_script = textwrap.dedent( + workflow_step( + workflow_text("noema-review.yml"), + "Run Noema LLM review and submit verdict", + ).split(" run: |\n", 1)[1] + ) + noema = subprocess.run( + [ + "bash", + "-c", + f"trap 'printf %s \"$NOEMA_LLM_API_KEY\" > {shlex.quote(str(noema_probe))}' EXIT\n" + + noema_script, + ], + env={ + **os.environ, + "PR_NUMBER": "1", + "GH_TOKEN": "synthetic-review-token", + "NOEMA_LLM_API_URL": "", + "NOEMA_LLM_MODEL": "", + "NOEMA_LLM_API_KEY": "synthetic-openai-key", + "NVIDIA_NIM_API_KEY": "", + "TARGET_REPOSITORY_PRIVATE": "false", + }, + capture_output=True, + text=True, + check=False, + ) + assert noema.returncode == 1 + assert "Noema LLM is unconfigured" in noema.stdout + assert noema_probe.read_text() == "synthetic-openai-key" + + def test_noema_workflow_run_without_pull_request_skips_before_token_exchange() -> None: workflow = workflow_text("noema-review.yml") From 45df7d28458f9afb8dcd3720905a264776c795e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 30 Jul 2026 18:43:36 +0900 Subject: [PATCH 04/10] fix(review): propagate Strix fallback model --- .github/workflows/strix.yml | 3 ++- scripts/ci/test_strix_quick_gate.sh | 1 + tests/test_required_workflow_queue_contract.py | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index da69668db..5c10afa51 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -456,6 +456,7 @@ jobs: if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]; then strix_model="gpt-5.6-luna" fi + echo "strix_model=$strix_model" >> "$GITHUB_OUTPUT" case "$strix_model" in openai/gpt-5-mini* | openai/gpt-5-nano* | \ openai/openai/gpt-5-mini* | openai/openai/gpt-5-nano* | \ @@ -733,7 +734,7 @@ jobs: - name: Prepare Strix model input file if: steps.gate.outputs.enabled == 'true' env: - STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna') }} + STRIX_MODEL: ${{ steps.gate.outputs.strix_model }} run: | umask 077 strix_llm_file="$RUNNER_TEMP/strix_llm.txt" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 594cb52a6..f7ce9dcfb 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -282,6 +282,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM == 'vertex_ai/gemini-3.1-pro-preview-customtools' && 'vertex_ai/gemini-2.5-flash'" "strix workflow must not quarantine the approved Vertex preview model after organization secret visibility is fixed" assert_file_contains "$workflow_file" "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" "strix workflow defaults public scans to NVIDIA NIM and keeps private scans on the contracted provider" assert_file_contains "$workflow_file" 'if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]' "strix workflow falls back to the contracted provider when the NVIDIA secret is absent" + assert_file_contains "$workflow_file" 'STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}' "strix workflow propagates the gate-selected fallback model to the scanner" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" assert_file_contains "$workflow_file" "STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" assert_file_contains "$workflow_file" "vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)" "strix workflow accepts only exact approved organization Vertex AI models" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index ce1962bd9..15b3bafb4 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -421,7 +421,14 @@ def test_nvidia_nim_defaults_preserve_existing_fallbacks_without_secret( check=False, ) assert strix.returncode == 0, strix.stderr - assert "provider_mode=openai_direct" in strix_output.read_text() + assert { + "provider_mode=openai_direct", + "strix_model=gpt-5.6-luna", + } <= set(strix_output.read_text().splitlines()) + assert ( + "STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}" + in workflow_text("strix.yml") + ) noema_probe = tmp_path / "noema-key" noema_script = textwrap.dedent( From c9649649f9d8b3239cc7575bec52f9223202b865 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 31 Jul 2026 18:12:38 +0900 Subject: [PATCH 05/10] test: enforce Strix timeout invariants --- scripts/ci/test_strix_quick_gate.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 1ceee93c1..9be340cbf 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -14,8 +14,15 @@ REPO_ROOT="$( GATE_SCRIPT="$REPO_ROOT/scripts/ci/strix_quick_gate.sh" FAILURES=0 -TIMEOUT_TEST_PROCESS_SECONDS="${STRIX_TEST_PROCESS_TIMEOUT_SECONDS:-10}" -TIMEOUT_TEST_FAKE_SLEEP_SECONDS="${STRIX_TEST_FAKE_SLEEP_SECONDS:-30}" +TIMEOUT_TEST_PROCESS_SECONDS="${STRIX_TEST_PROCESS_TIMEOUT_SECONDS:-30}" +TIMEOUT_TEST_FAKE_SLEEP_SECONDS="${STRIX_TEST_FAKE_SLEEP_SECONDS:-60}" + +if ! [[ "$TIMEOUT_TEST_PROCESS_SECONDS" =~ ^[1-9][0-9]*$ ]] || + ! [[ "$TIMEOUT_TEST_FAKE_SLEEP_SECONDS" =~ ^[1-9][0-9]*$ ]] || + [ "$TIMEOUT_TEST_FAKE_SLEEP_SECONDS" -le "$TIMEOUT_TEST_PROCESS_SECONDS" ]; then + printf 'STRIX_TEST_FAKE_SLEEP_SECONDS must be a positive integer greater than STRIX_TEST_PROCESS_TIMEOUT_SECONDS.\n' >&2 + exit 2 +fi # Keep local developer/provider secrets from changing fake Strix model routing. unset STRIX_LLM @@ -7898,7 +7905,7 @@ run_timeout_cleanup_case() { #!/usr/bin/env bash set -euo pipefail -sleep 30 & +sleep "${FAKE_STRIX_TIMEOUT_SLEEP_SECONDS:?}" & child_pid=$! printf '%s' "$child_pid" > "${FAKE_STRIX_CHILD_PID_FILE:?}" sleep "${FAKE_STRIX_TIMEOUT_SLEEP_SECONDS:?}" From 5dc981ba9352beaee530b665f7eb5b4cfdf611a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 31 Jul 2026 19:18:59 +0900 Subject: [PATCH 06/10] fix: break OpenCode scheduler check cycle --- .github/workflows/opencode-review-dispatch.yml | 8 ++++++-- scripts/ci/collect_failed_check_evidence.sh | 2 +- scripts/ci/test_strix_quick_gate.sh | 15 ++++++++++++--- tests/test_opencode_agent_contract.py | 3 ++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 395176423..625aa7dc4 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2459,6 +2459,7 @@ jobs: | select((.name // "") != "Required OpenCode Review") | select((.name // "") != "OpenCode PR Review") | select((.name // "") != "metadata-only gate evaluation") + | select((.name // "") != "scan-pr-queue") | select((.checkSuite.workflowRun.workflow.name // "") != "OpenCode Review") | select((.checkSuite.workflowRun.workflow.name // "") != "Required OpenCode Review") | select((.checkSuite.workflowRun.workflow.name // "") != "OpenCode PR Review") @@ -4376,7 +4377,7 @@ jobs: self_check_filter=' def self_check: (.name // "") as $n - | ["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation"] | index($n); + | ["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation", "scan-pr-queue"] | index($n); def latest_peer_checks: [ (.check_runs // [])[] @@ -6186,6 +6187,7 @@ jobs: | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) | select((.status // "") == "completed") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) + | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) @@ -6199,6 +6201,7 @@ jobs: | map(last) | .[]? | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) + | select((.name // "") != "scan-pr-queue") | select((.status // "") != "completed") | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.status // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) ' @@ -6498,7 +6501,7 @@ jobs: | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.workflow // "") == "CodeQL") | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")) | not) + | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review" and ((.workflow // "") == "Noema Review" or (.workflow // "") == "Required Noema Review")) | not) | "- " + (.label // "check") + ": " + (.conclusion // "unknown") + (if (.detailsUrl // "") != "" then " (" + .detailsUrl + ")" else "" end) @@ -6630,6 +6633,7 @@ jobs: | select((.workflow // "") != "Required OpenCode Review") | select((.workflow // "") != "OpenCode PR Review") | select((.name // "") != "metadata-only gate evaluation") + | select((.name // "") != "scan-pr-queue") | select((.status // "") != "COMPLETED") | "- " + (.label // "check") + ": " + (.status // "unknown") + (if (.detailsUrl // "") != "" then " (" + .detailsUrl + ")" else "" end) elif .kind == "status" then diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index cc2e4033e..71c31d7d1 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -519,7 +519,7 @@ gh api graphql \ # so its stable check name is the only safe cycle-breaking key. | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL") | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue" and ((.checkSuite.workflowRun.workflow.name // "") == "PR Review Merge Scheduler" or (.checkSuite.workflowRun.workflow.name // "") == "Required PR Review Merge Scheduler")) | not) + | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("${{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review" and ((.checkSuite.workflowRun.workflow.name // "") == "Noema Review" or (.checkSuite.workflowRun.workflow.name // "") == "Required Noema Review")) | not) | select((.name // "") != "opencode-review") diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 9be340cbf..c7fbbe2f0 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1078,7 +1078,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'map(sort_by(.completedAt // "") | last)' "opencode approval considers only the latest completed statusCheckRollup entry per check label" assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" - assert_file_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode approval ignores cancelled scheduler queue replacement checks without source evidence" + assert_file_contains "$workflow_file" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "opencode approval ignores cancelled scheduler queue replacement checks without workflow metadata" + scheduler_cancelled_filter_count="$(grep -Fc 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "$workflow_file")" + if [ "$scheduler_cancelled_filter_count" -lt 2 ]; then + fail "opencode GraphQL and commit-check fallback both ignore cancelled scheduler queue replacements (found ${scheduler_cancelled_filter_count}, expected at least 2)" + fi + assert_file_not_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode scheduler cancellation classification does not depend on optional workflow metadata" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" assert_file_contains "$workflow_file" 'current_head_manual_strix_success_status()' "opencode approval can identify same-head manual Strix success status evidence" assert_file_contains "$workflow_file" 'manual_run_line="$(latest_current_head_manual_strix_run || true)"' "opencode approval falls back to same-head manual Strix check-run success when commit status publication is unavailable" @@ -1112,7 +1117,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.name // "") != "metadata-only gate evaluation")' "failed-check evidence ignores metadata-only review-state gates even when GitHub misattributes their workflow" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'isRequired(pullRequestId: $prId)' "failed-check evidence reads PR-required status for check runs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL"' "failed-check evidence ignores non-required cancelled CodeQL checks without logs" - assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '(.name // "") == "scan-pr-queue" and ((.checkSuite.workflowRun.workflow.name // "") == "PR Review Merge Scheduler" or (.checkSuite.workflowRun.workflow.name // "") == "Required PR Review Merge Scheduler")' "failed-check evidence ignores cancelled scheduler queue replacement checks" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "failed-check evidence ignores cancelled scheduler queue replacement checks without workflow metadata" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.name // "") | contains("${{"))' "failed-check evidence ignores cancelled matrix-template helper checks without logs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '(.name // "") == "noema-review"' "failed-check evidence ignores cancelled Noema queue replacement checks without source logs" assert_file_contains "$workflow_file" 'select((.name // "") != "metadata-only gate evaluation")' "opencode ignores metadata-only review-state gates without trusting GitHub workflow attribution" @@ -1120,8 +1125,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { if [ "$metadata_gate_filter_count" -lt 3 ]; then fail "opencode pre-model, failed-check, and pending-check collection all ignore metadata-only review-state gates (found ${metadata_gate_filter_count}, expected at least 3)" fi - assert_file_contains "$workflow_file" '["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation"]' "central fast approval ignores its dependent metadata-only review-state gate" + assert_file_contains "$workflow_file" '["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation", "scan-pr-queue"]' "central fast approval ignores its dependent review and scheduler control-plane checks" assert_file_contains "$workflow_file" '["opencode-review","coverage-evidence","metadata-only gate evaluation"]' "opencode supplemental check-run collection ignores review-state helper gates" + scheduler_pending_filter_count="$(grep -Fc 'select((.name // "") != "scan-pr-queue")' "$workflow_file")" + if [ "$scheduler_pending_filter_count" -lt 3 ]; then + fail "opencode pre-model, rollup, and commit-check pending collection all ignore the scheduler control-plane cycle (found ${scheduler_pending_filter_count}, expected at least 3)" + fi assert_file_contains "$workflow_file" '((.name // "") | contains("$" + "{{"))' "opencode failed-check collection ignores cancelled matrix-template helper checks without logs without exposing a raw Actions expression" assert_file_contains "$workflow_file" '(.name // "") == "noema-review"' "opencode failed-check collection ignores cancelled Noema queue replacement checks without source logs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '"strix security scan/"*' "failed-check evidence maps stale Strix workflow helper checks to the manual strix evidence status" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e1fb87c79..ea2a25646 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1520,7 +1520,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): ) assert ( '["opencode-review", "coverage-evidence", "coverage-source-tree", ' - '"required-workflow-bootstrap", "metadata-only gate evaluation"]' in workflow + '"required-workflow-bootstrap", "metadata-only gate evaluation", ' + '"scan-pr-queue"]' in workflow ) assert "falling back to current-head REST check-runs" in workflow From 7030ed4fdf2b80d8aa884c0c4a8a52ac65f083c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 1 Aug 2026 04:41:09 +0900 Subject: [PATCH 07/10] fix(review): ignore scheduler self-check failures --- .../workflows/opencode-review-dispatch.yml | 4 +-- scripts/ci/collect_failed_check_evidence.sh | 2 +- scripts/ci/test_strix_quick_gate.sh | 10 +++--- tests/test_opencode_agent_contract.py | 34 +++++++++++++++++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index dc10206c6..51a8cc0b2 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -6271,7 +6271,7 @@ jobs: | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) | select((.status // "") == "completed") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) + | select((.name // "") != "scan-pr-queue") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) @@ -6585,7 +6585,7 @@ jobs: | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.workflow // "") == "CodeQL") | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) + | select((.name // "") != "scan-pr-queue") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review" and ((.workflow // "") == "Noema Review" or (.workflow // "") == "Required Noema Review")) | not) | "- " + (.label // "check") + ": " + (.conclusion // "unknown") + (if (.detailsUrl // "") != "" then " (" + .detailsUrl + ")" else "" end) diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index 71c31d7d1..51e5f1e5b 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -519,7 +519,7 @@ gh api graphql \ # so its stable check name is the only safe cycle-breaking key. | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL") | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not) + | select((.name // "") != "scan-pr-queue") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("${{"))) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review" and ((.checkSuite.workflowRun.workflow.name // "") == "Noema Review" or (.checkSuite.workflowRun.workflow.name // "") == "Required Noema Review")) | not) | select((.name // "") != "opencode-review") diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index f10505027..b1b306d39 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1083,10 +1083,10 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' assert_file_contains "$workflow_file" 'map(sort_by(.completedAt // "") | last)' "opencode approval considers only the latest completed statusCheckRollup entry per check label" assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" - assert_file_contains "$workflow_file" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "opencode approval ignores cancelled scheduler queue replacement checks without workflow metadata" - scheduler_cancelled_filter_count="$(grep -Fc 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "$workflow_file")" - if [ "$scheduler_cancelled_filter_count" -lt 2 ]; then - fail "opencode GraphQL and commit-check fallback both ignore cancelled scheduler queue replacements (found ${scheduler_cancelled_filter_count}, expected at least 2)" + assert_file_contains "$workflow_file" 'select((.name // "") != "scan-pr-queue")' "opencode approval ignores scheduler queue self-checks for every failed or pending state" + scheduler_self_check_filter_count="$(grep -Fc 'select((.name // "") != "scan-pr-queue")' "$workflow_file")" + if [ "$scheduler_self_check_filter_count" -lt 5 ]; then + fail "opencode GraphQL and commit-check failed/pending paths all ignore scheduler queue self-checks (found ${scheduler_self_check_filter_count}, expected at least 5)" fi assert_file_not_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode scheduler cancellation classification does not depend on optional workflow metadata" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" @@ -1122,7 +1122,7 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.name // "") != "metadata-only gate evaluation")' "failed-check evidence ignores metadata-only review-state gates even when GitHub misattributes their workflow" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'isRequired(pullRequestId: $prId)' "failed-check evidence reads PR-required status for check runs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL"' "failed-check evidence ignores non-required cancelled CodeQL checks without logs" - assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "failed-check evidence ignores cancelled scheduler queue replacement checks without workflow metadata" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.name // "") != "scan-pr-queue")' "failed-check evidence ignores scheduler queue self-checks for every failure conclusion" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.name // "") | contains("${{"))' "failed-check evidence ignores cancelled matrix-template helper checks without logs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '(.name // "") == "noema-review"' "failed-check evidence ignores cancelled Noema queue replacement checks without source logs" assert_file_contains "$workflow_file" 'select((.name // "") != "metadata-only gate evaluation")' "opencode ignores metadata-only review-state gates without trusting GitHub workflow attribution" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 9d35523ef..e6d504beb 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1619,6 +1619,40 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "forced smooth scrolling" in prompt_template +def test_opencode_excludes_queue_self_check_from_every_failed_check_path(): + """Never diagnose the central scheduler's own queue check as a peer failure.""" + workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( + encoding="utf-8" + ) + unconditional_filter = 'select((.name // "") != "scan-pr-queue")' + cancelled_only_filter = ( + 'select(((.conclusion // "" | ascii_downcase) == "cancelled" ' + 'and (.name // "") == "scan-pr-queue") | not)' + ) + + # Both failed-check collectors and both pending-check collectors exclude the + # scheduler check by name, independently of its current state or conclusion. + assert workflow.count(unconditional_filter) >= 5 + assert cancelled_only_filter not in workflow + failed_check_collector = Path( + "scripts/ci/collect_failed_check_evidence.sh" + ).read_text(encoding="utf-8") + assert unconditional_filter in failed_check_collector + assert cancelled_only_filter not in failed_check_collector + + fixtures = [ + {"name": "scan-pr-queue", "conclusion": "CANCELLED"}, + {"name": "scan-pr-queue", "conclusion": "FAILURE"}, + {"name": "real-peer-check", "conclusion": "FAILURE"}, + ] + retained = [ + check + for check in fixtures + if check.get("name", "") != "scan-pr-queue" + ] + assert retained == [{"name": "real-peer-check", "conclusion": "FAILURE"}] + + def test_opencode_job_timeout_contains_full_sequential_review_budget(): """Keep the outer job alive through evidence, review, and publication.""" workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text(encoding="utf-8") From 346a4be046091597b4bec0bdd194400aa1173a03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 1 Aug 2026 05:03:31 +0900 Subject: [PATCH 08/10] fix(review): reserve fallback budget after NIM --- .../workflows/opencode-review-dispatch.yml | 2 + scripts/ci/run_opencode_review_model_pool.sh | 53 +++++++++++++++++-- scripts/ci/test_strix_quick_gate.sh | 4 ++ tests/test_opencode_agent_contract.py | 2 + tests/test_opencode_model_pool_runner.py | 46 ++++++++++++++-- 5 files changed, 101 insertions(+), 6 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 51a8cc0b2..8c32616ed 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -4091,6 +4091,8 @@ jobs: OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS: "5400" OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700" OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1" + OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180" + OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900" OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600" # This installation currently reports a 4k request-body limit for # GitHub Models GPT-5 endpoints even though the public catalog is diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 6dcbd0a14..c92288d63 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -400,7 +400,10 @@ cap_model_run_timeout() { local cap_seconds case "$model_candidate" in - opencode-free/* | nvidia-nim/*) + nvidia-nim/*) + cap_seconds="$(env_integer_or_default OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS 180)" + ;; + opencode-free/*) cap_seconds="$(env_integer_or_default OPENCODE_FREE_RUN_TIMEOUT_SECONDS 3600)" ;; github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) @@ -519,6 +522,8 @@ main() { local uncapped_run_timeout local changed_file_count small_file_threshold medium_file_threshold local invalid_control_cap max_total_attempts total_attempts alive_candidates + local nim_budget_seconds nim_elapsed_seconds nim_remaining_seconds + local nim_attempt_started nim_attempt_elapsed non_nim_candidate_count local -A dead_candidate_reasons invalid_control_counts local -a model_candidates @@ -582,8 +587,23 @@ main() { fi exit 1 fi - printf 'Configured OpenCode model pool: candidates=%s attempts=%s per-model-timeout=%ss retry-budget=%ss max-cycles=%s.\n' \ - "${#model_candidates[@]}" "$attempts" "$original_run_timeout" "$budget_seconds" "$max_cycles" + nim_budget_seconds="$(env_integer_or_default OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS 900)" + nim_elapsed_seconds=0 + non_nim_candidate_count=0 + for model_candidate in "${model_candidates[@]}"; do + if ! is_nvidia_nim_candidate "$model_candidate"; then + non_nim_candidate_count=$((non_nim_candidate_count + 1)) + fi + done + if [ "$non_nim_candidate_count" -gt 0 ] && + [ "$budget_seconds" -gt 0 ] && + [ "$nim_budget_seconds" -ge "$budget_seconds" ]; then + nim_budget_seconds=$((budget_seconds / 2)) + printf 'OpenCode NVIDIA NIM combined runtime budget was capped at %ss so %s non-NIM fallback candidate(s) retain retry budget.\n' \ + "$nim_budget_seconds" "$non_nim_candidate_count" + fi + printf 'Configured OpenCode model pool: candidates=%s attempts=%s per-model-timeout=%ss retry-budget=%ss max-cycles=%s NVIDIA-NIM-combined-budget=%ss.\n' \ + "${#model_candidates[@]}" "$attempts" "$original_run_timeout" "$budget_seconds" "$max_cycles" "$nim_budget_seconds" cycle=1 while :; do @@ -597,6 +617,12 @@ main() { if should_skip_model_candidate "$model_candidate"; then continue fi + if is_nvidia_nim_candidate "$model_candidate" && + [ "$nim_elapsed_seconds" -ge "$nim_budget_seconds" ]; then + printf 'Skipping OpenCode %s because the NVIDIA NIM combined runtime budget of %ss is exhausted; preserving the remaining retry budget for fallback candidates.\n' \ + "$model_candidate" "$nim_budget_seconds" + continue + fi assert_reasoning_effort_for_candidate "$model_candidate" safe_model="${model_candidate//[\/:]/-}" prompt_file="${RUNNER_TEMP}/opencode-review-${safe_model}-prompt.md" @@ -606,6 +632,12 @@ main() { write_prompt "$model_candidate" "$prompt_file" for attempt in $(seq 1 "$attempts"); do now="$SECONDS" + if is_nvidia_nim_candidate "$model_candidate" && + [ "$nim_elapsed_seconds" -ge "$nim_budget_seconds" ]; then + printf 'Stopping OpenCode %s retries because the NVIDIA NIM combined runtime budget of %ss is exhausted.\n' \ + "$model_candidate" "$nim_budget_seconds" + break + fi if [ "$deadline" -gt 0 ] && [ "$now" -ge "$deadline" ]; then printf 'OpenCode model pool retry deadline elapsed before %s attempt %s/%s.\n' "$model_candidate" "$attempt" "$attempts" if finish_pool_without_model; then @@ -629,6 +661,14 @@ main() { if [ "$deadline" -gt 0 ] && [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -gt "$remaining" ]; then OPENCODE_RUN_TIMEOUT_SECONDS="$remaining" fi + if is_nvidia_nim_candidate "$model_candidate"; then + nim_remaining_seconds=$((nim_budget_seconds - nim_elapsed_seconds)) + if [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -gt "$nim_remaining_seconds" ]; then + printf 'OpenCode %s combined NVIDIA NIM budget cap selected %ss instead of %ss so fallback candidates retain retry budget.\n' \ + "$model_candidate" "$nim_remaining_seconds" "$OPENCODE_RUN_TIMEOUT_SECONDS" + OPENCODE_RUN_TIMEOUT_SECONDS="$nim_remaining_seconds" + fi + fi 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 @@ -642,6 +682,7 @@ main() { agent="$OPENCODE_FIRST_ATTEMPT_AGENT" fi run_status=0 + nim_attempt_started="$SECONDS" if run_one_model_attempt "$model_candidate" "$attempt" "$attempts" "$agent" "$prompt_file" "$candidate_output_file" "$opencode_json_file" "$opencode_export_file"; then cp "$candidate_output_file" "$OPENCODE_OUTPUT_FILE" record_review_model "$model_candidate" @@ -650,6 +691,12 @@ main() { else run_status=$? fi + if is_nvidia_nim_candidate "$model_candidate"; then + nim_attempt_elapsed=$((SECONDS - nim_attempt_started)) + nim_elapsed_seconds=$((nim_elapsed_seconds + nim_attempt_elapsed)) + printf 'OpenCode NVIDIA NIM combined runtime used %ss/%ss after %s attempt %s/%s.\n' \ + "$nim_elapsed_seconds" "$nim_budget_seconds" "$model_candidate" "$attempt" "$attempts" + fi if [ "$run_status" -ne 3 ] && is_credit_exhausted_failure "$opencode_json_file" "${opencode_json_file}.stderr"; then dead_candidate_reasons[$model_candidate]="provider credits exhausted (HTTP 402 / payment required)" printf 'OpenCode %s provider credits are exhausted; marking this candidate failed for the rest of the run so retries cannot accrue further spend.\n' "$model_candidate" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b1b306d39..30adfcb59 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -731,9 +731,13 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode primary review preserves legitimate full-hour provider sessions" assert_file_contains "$workflow_file" 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600"' "opencode free-tier failover timeout is hour-class (~3600s)" +assert_file_contains "$workflow_file" 'OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180"' "opencode NVIDIA NIM candidates have a short per-candidate failover timeout" +assert_file_contains "$workflow_file" 'OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900"' "opencode NVIDIA NIM candidates share a bounded combined runtime budget" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_RUN_TIMEOUT_SECONDS:-3600' "opencode pool defaults primary run timeout to hour-class (~3600s) for large repos" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_DYNAMIC_RUN_TIMEOUT_CAP_SECONDS 3600' "opencode pool dynamic timeout cap defaults to hour-class (~3600s)" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS 3600' "opencode free-tier failover timeout is hour-class (~3600s)" +assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS 180' "opencode NVIDIA NIM candidate runtime cap defaults to three minutes" +assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS 900' "opencode NVIDIA NIM combined runtime cap defaults to fifteen minutes" assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "11700"' "opencode model pool exits before the step timeout so the approval gate can publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool exhausts each candidate only once before bounded fallback" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e6d504beb..9a236d161 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1415,6 +1415,8 @@ 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: "1"' in workflow + assert 'OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180"' in workflow + assert 'OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900"' in workflow assert 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "1"' in workflow diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index ac3203eec..fd09efcdf 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -27,6 +27,10 @@ "OPENCODE_EVIDENCE_FILE", "OPENCODE_REQUIRE_ADVERSARIAL_VALIDATION", } +INHERITED_PROVIDER_CREDENTIAL_ENV = { + "NVIDIA_API_KEY", + "NVIDIA_NIM_API_KEY", +} def bash_command() -> str: @@ -170,7 +174,7 @@ def run_failed_model( fake_opencode.chmod(0o755) github_output = tmp_path / "github-output.txt" env = os.environ.copy() - for name in CENTRAL_FALLBACK_ENV: + for name in CENTRAL_FALLBACK_ENV | INHERITED_PROVIDER_CREDENTIAL_ENV: env.pop(name, None) env.update( { @@ -796,8 +800,12 @@ def test_free_provider_runtime_cap_preserves_queue_budget(tmp_path: Path) -> Non ) in result.stdout -def test_nvidia_nim_candidate_requires_key(tmp_path: Path) -> None: +def test_nvidia_nim_candidate_requires_key( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: """NVIDIA NIM is skipped cleanly when its scoped credential is unavailable.""" + monkeypatch.setenv("NVIDIA_NIM_API_KEY", "ambient-scoped-key") + monkeypatch.setenv("NVIDIA_API_KEY", "ambient-provider-key") result = run_failed_model( tmp_path, model_candidates="nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", @@ -814,7 +822,7 @@ def test_nvidia_nim_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: tmp_path, extra_env={ "NVIDIA_NIM_API_KEY": "fake-nvidia-key", - "OPENCODE_FREE_RUN_TIMEOUT_SECONDS": "3", + "OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS": "3", "OPENCODE_RUN_TIMEOUT_SECONDS": "9", }, model_candidates="nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", @@ -827,6 +835,38 @@ def test_nvidia_nim_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: ) in result.stdout +def test_nvidia_nim_combined_budget_preserves_fallback_attempt( + tmp_path: Path, +) -> None: + """Timed-out NIM candidates cannot consume the fallback provider budget.""" + result = run_failed_model( + tmp_path, + extra_env={ + "FAKE_OPENCODE_HANG_SECONDS": "2", + "NVIDIA_NIM_API_KEY": "fake-nvidia-key", + "OPENCODE_FREE_RUN_TIMEOUT_SECONDS": "1", + "OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS": "1", + "OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS": "1", + "OPENCODE_RUN_TIMEOUT_SECONDS": "5", + "OPENCODE_TOTAL_RETRY_BUDGET_SECONDS": "6", + }, + model_candidates=( + "nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b " + "nvidia-nim/nvidia/nemotron-3-super-120b-a12b " + "opencode-free/nemotron-3-ultra-free" + ), + ) + + assert result.returncode == 1 + assert "OpenCode NVIDIA NIM combined runtime used" in result.stdout + assert ( + "Skipping OpenCode nvidia-nim/nvidia/nemotron-3-super-120b-a12b " + "because the NVIDIA NIM combined runtime budget of 1s is exhausted" + in result.stdout + ) + assert "OpenCode opencode-free/nemotron-3-ultra-free attempt 1/1" in result.stdout + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: From 8a8ac4123b242dd62aee4762f1a133f0f3b7144a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 1 Aug 2026 05:45:28 +0900 Subject: [PATCH 09/10] fix(review): enforce scoped NIM credentials --- .../workflows/opencode-review-dispatch.yml | 27 ++++++++++--------- scripts/ci/run_opencode_review_model_pool.sh | 12 +++++---- scripts/ci/test_strix_quick_gate.sh | 7 ++--- tests/test_opencode_agent_contract.py | 24 +++++++++++++---- tests/test_opencode_model_pool_runner.py | 3 ++- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 8c32616ed..885257e5f 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -4034,22 +4034,22 @@ jobs: # in the opencode.jsonc "openai" provider block. OPENCODE_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Org secret NVIDIA_NIM_API_KEY preferred; fallback NVIDIA_API_KEY. - # opencode.jsonc expects env NVIDIA_API_KEY for nvidia-nim/* models. - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY }} + # The scoped NVIDIA_NIM_API_KEY is the only NIM credential source. + # opencode.jsonc expects that same scoped value in NVIDIA_API_KEY. + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" # High-sensitivity review candidates only. Public repositories first - # use OpenCode Zen's anonymous active, zero-cost model endpoints. - # Their trial/free-period data - # may be logged, retained, or used for product/model improvement, so - # private repositories never include them. The pool then falls - # through to - # NVIDIA NIM, OpenCode Zen GPT-5.6 Terra, DeepSeek V3, the direct - # GPT-5.6 Luna slot, and pinned PAID + # try NVIDIA NIM when its scoped secret is available, then OpenCode + # Zen's anonymous active, zero-cost models, followed by the existing + # provider fallbacks. Trial/free-period data may be logged, retained, + # or used for product/model improvement, so private repositories + # include neither NIM nor anonymous free candidates and start at the + # existing keyed fallback list: OpenCode Zen GPT-5.6 Terra, DeepSeek + # V3, the direct GPT-5.6 Luna slot, and pinned PAID # OpenRouter coder models (free-tier candidates hit the shared # free-models-per-day cap and hung for the full candidate timeout, # so the OpenRouter slots use cheap paid models billed against the @@ -4695,9 +4695,10 @@ jobs: # Exposed so the "openai" provider in opencode.jsonc resolves during the # failed-check diagnosis opencode run that shares this config. OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - # Org secret NVIDIA_NIM_API_KEY preferred; fallback NVIDIA_API_KEY. - # opencode.jsonc expects env NVIDIA_API_KEY for nvidia-nim/* models. - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY }} + # The scoped NVIDIA_NIM_API_KEY is the only NIM credential source. + # opencode.jsonc expects that same scoped value in NVIDIA_API_KEY. + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index c92288d63..8aef2b4d7 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -354,10 +354,12 @@ is_nvidia_nim_candidate() { # Org secret name is NVIDIA_NIM_API_KEY (GitHub Actions / org secrets UI). # opencode.jsonc nvidia-nim provider block resolves {env:NVIDIA_API_KEY}. -# Workflow maps secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY → env NVIDIA_API_KEY. -# Normalize here too so local/CLI runs with only NVIDIA_NIM_API_KEY set do not skip nim/*. -if [ -z "${NVIDIA_API_KEY:-}" ] && [ -n "${NVIDIA_NIM_API_KEY:-}" ]; then +# Normalize only the scoped secret and discard any legacy provider credential so +# it cannot activate NIM candidates outside the explicit governance boundary. +if [ -n "${NVIDIA_NIM_API_KEY:-}" ]; then export NVIDIA_API_KEY="$NVIDIA_NIM_API_KEY" +else + unset NVIDIA_API_KEY fi is_low_sensitivity_candidate() { @@ -387,8 +389,8 @@ 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_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" + if is_nvidia_nim_candidate "$model_candidate" && [ -z "${NVIDIA_NIM_API_KEY:-}" ]; then + printf 'Skipping OpenCode %s because scoped NVIDIA_NIM_API_KEY is not configured; falling back to the next provider-qualified candidate.\n' "$model_candidate" return 0 fi return 1 diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 30adfcb59..395c44b01 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -891,7 +891,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" "NVIDIA_API_KEY is not configured" "opencode model pool skips NVIDIA NIM candidates when the normalized provider credential is absent" + assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "scoped NVIDIA_NIM_API_KEY is not configured" "opencode model pool skips NVIDIA NIM candidates when the scoped credential is absent" 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" @@ -1090,7 +1090,7 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' assert_file_contains "$workflow_file" 'select((.name // "") != "scan-pr-queue")' "opencode approval ignores scheduler queue self-checks for every failed or pending state" scheduler_self_check_filter_count="$(grep -Fc 'select((.name // "") != "scan-pr-queue")' "$workflow_file")" if [ "$scheduler_self_check_filter_count" -lt 5 ]; then - fail "opencode GraphQL and commit-check failed/pending paths all ignore scheduler queue self-checks (found ${scheduler_self_check_filter_count}, expected at least 5)" + record_failure "opencode GraphQL and commit-check failed/pending paths all ignore scheduler queue self-checks (found ${scheduler_self_check_filter_count}, expected at least 5)" fi assert_file_not_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode scheduler cancellation classification does not depend on optional workflow metadata" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" @@ -1242,7 +1242,8 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' assert_file_contains "$workflow_file" "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 keeps paid Zen, DeepSeek V3, and full-size GPT fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" - assert_file_contains "$workflow_file" "secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY" "opencode review binds org NVIDIA_NIM_API_KEY into NVIDIA_API_KEY env" + assert_file_not_contains "$workflow_file" "secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY" "opencode review never falls back from the scoped NVIDIA NIM secret to the legacy provider secret" + assert_file_contains "$workflow_file" 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' "opencode review binds only the scoped NVIDIA NIM secret into the provider environment" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "NVIDIA_NIM_API_KEY" "model pool normalizes NVIDIA_NIM_API_KEY to NVIDIA_API_KEY" assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 9a236d161..a1e45a1ab 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -198,6 +198,11 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert nvidia_provider["models"]["nvidia/nemotron-3-ultra-550b-a55b"][ "limit" ] == {"context": 131072, "output": 8192} + scoped_provider_binding = ( + "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" + ) + assert workflow.count(scoped_provider_binding) == 2 + assert "secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY" not in workflow free_models = generated_config["provider"]["opencode-free"]["models"] paid_zen_models = generated_config["provider"]["opencode"]["models"] assert set(free_models) == { @@ -1647,11 +1652,20 @@ def test_opencode_excludes_queue_self_check_from_every_failed_check_path(): {"name": "scan-pr-queue", "conclusion": "FAILURE"}, {"name": "real-peer-check", "conclusion": "FAILURE"}, ] - retained = [ - check - for check in fixtures - if check.get("name", "") != "scan-pr-queue" - ] + extracted_filter = re.search( + rf"^\s+\|\s+({re.escape(unconditional_filter)})$", + workflow, + re.MULTILINE, + ) + assert extracted_filter is not None + jq_result = subprocess.run( + ["jq", "-c", f"[.[] | {extracted_filter.group(1)}]"], + input=json.dumps(fixtures), + capture_output=True, + text=True, + check=True, + ) + retained = json.loads(jq_result.stdout) assert retained == [{"name": "real-peer-check", "conclusion": "FAILURE"}] diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index fd09efcdf..3769fb42d 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -808,11 +808,12 @@ def test_nvidia_nim_candidate_requires_key( monkeypatch.setenv("NVIDIA_API_KEY", "ambient-provider-key") result = run_failed_model( tmp_path, + extra_env={"NVIDIA_API_KEY": "legacy-provider-key"}, model_candidates="nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b", ) assert result.returncode == 1 - assert "NVIDIA_API_KEY is not configured" in result.stdout + assert "scoped NVIDIA_NIM_API_KEY is not configured" in result.stdout assert "attempt 1/1" not in result.stdout From 8b58fa080771aa4c6df1c64487a729f893ffeef4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 1 Aug 2026 05:52:12 +0900 Subject: [PATCH 10/10] test(review): scope NIM secrets to review job --- tests/test_opencode_agent_contract.py | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index a1e45a1ab..9e616d5d5 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -201,7 +201,34 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): scoped_provider_binding = ( "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" ) - assert workflow.count(scoped_provider_binding) == 2 + jobs_text = workflow[workflow.index("\njobs:\n") + len("\njobs:\n") :] + job_headers = list( + re.finditer(r"^ ([A-Za-z0-9_-]+):\n", jobs_text, re.MULTILINE) + ) + job_blocks = { + match.group(1): jobs_text[ + match.start() : ( + job_headers[index + 1].start() + if index + 1 < len(job_headers) + else len(jobs_text) + ) + ] + for index, match in enumerate(job_headers) + } + privileged_review_job = job_blocks["opencode-review-target"] + + assert privileged_review_job.count(scoped_provider_binding) == 2 + assert ( + privileged_review_job.count( + "NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" + ) + == 2 + ) + for job_name, job_block in job_blocks.items(): + if job_name == "opencode-review-target": + continue + assert "secrets.NVIDIA_NIM_API_KEY" not in job_block, job_name + assert "secrets.NVIDIA_API_KEY" not in job_block, job_name assert "secrets.NVIDIA_NIM_API_KEY || secrets.NVIDIA_API_KEY" not in workflow free_models = generated_config["provider"]["opencode-free"]["models"] paid_zen_models = generated_config["provider"]["opencode"]["models"]