diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index cdc986cc0..763604415 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3361,6 +3361,14 @@ jobs: "baseURL": "https://opencode.ai/zen/v1" }, "models": { + "nemotron-3-ultra-free": { + "name": "Nemotron 3 Ultra Free", + "tool_call": true, + "limit": { + "context": 1000000, + "output": 128000 + } + }, "deepseek-v4-flash-free": { "name": "DeepSeek V4 Flash Free", "tool_call": true, @@ -3702,11 +3710,11 @@ jobs: NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" # High-sensitivity review candidates only. Public repositories first - # use OpenCode Zen's anonymous DeepSeek V4 Flash Free endpoint, then - # North Mini Code Free as fallback. During their free periods, - # DeepSeek data may be used for improvement and North data may be - # retained and used for improvement, so private repositories never - # include either candidate. The pool then falls through to + # use OpenCode Zen's anonymous Nemotron 3 Ultra, DeepSeek V4 Flash, + # and North Mini Code free 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 # 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, @@ -3717,7 +3725,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/deepseek-v4-flash-free opencode-free/north-mini-code-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 ' || '' }}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/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 5b1ed0a23..fafe680ce 100644 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -17,7 +17,10 @@ def _is_candidate_lock_name(name: str) -> bool: """Return whether a file name is a possible pip requirements lock.""" - return name == "requirements.lock" or fnmatch.fnmatch(name, "requirements*.txt") + return name == "requirements.lock" or ( + fnmatch.fnmatch(name, "requirements*.txt") + and not fnmatch.fnmatch(name, "requirements-*-ci-hashes.txt") + ) def _requirement_lines(content: bytes) -> list[str]: diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index b79ced1f5..ff66279b0 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -130,6 +130,9 @@ def test_lock_name_candidates_are_pip_requirements_files() -> None: assert materializer._is_candidate_lock_name("requirements.lock") assert materializer._is_candidate_lock_name("requirements-dev.txt") assert materializer._is_candidate_lock_name("requirements.txt") + assert not materializer._is_candidate_lock_name( + "requirements-opencode-review-ci-hashes.txt" + ) assert not materializer._is_candidate_lock_name("uv.lock") assert not materializer._is_candidate_lock_name("pyproject.toml") diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 9e8399cc4..9d9b90803 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -92,12 +92,14 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidates_match is not None conditional_public_candidate = ( "${{ needs.validate-pr-metadata.outputs.is_private == 'false' " - "&& 'opencode-free/deepseek-v4-flash-free " + "&& 'opencode-free/nemotron-3-ultra-free " + "opencode-free/deepseek-v4-flash-free " "opencode-free/north-mini-code-free ' || '' }}" ) candidates_text = candidates_match.group(1) assert candidates_text.startswith(conditional_public_candidate) candidates = [ + "opencode-free/nemotron-3-ultra-free", "opencode-free/deepseek-v4-flash-free", "opencode-free/north-mini-code-free", *candidates_text.removeprefix(conditional_public_candidate).split(), @@ -117,6 +119,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["opencode-free", "nemotron-3-ultra-free"], ["opencode-free", "deepseek-v4-flash-free"], ["opencode-free", "north-mini-code-free"], ["github-models", "deepseek/deepseek-v3-0324"], @@ -146,8 +149,12 @@ 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)) free_models = generated_config["provider"]["opencode-free"]["models"] + nemotron_model = free_models["nemotron-3-ultra-free"] deepseek_model = free_models["deepseek-v4-flash-free"] north_model = free_models["north-mini-code-free"] + assert nemotron_model["tool_call"] is True + assert nemotron_model["limit"] == {"context": 1000000, "output": 128000} + assert "response_format" not in nemotron_model.get("options", {}) assert deepseek_model["tool_call"] is True assert "response_format" not in deepseek_model.get("options", {}) assert north_model["tool_call"] is True @@ -1229,7 +1236,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): ) assert ( "needs.validate-pr-metadata.outputs.is_private == 'false' && " - "'opencode-free/deepseek-v4-flash-free " + "'opencode-free/nemotron-3-ultra-free " + "opencode-free/deepseek-v4-flash-free " "opencode-free/north-mini-code-free ' || ''" ) in workflow assert (