diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 14fcf266..cdc986cc 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": { + "deepseek-v4-flash-free": { + "name": "DeepSeek V4 Flash Free", + "tool_call": true, + "limit": { + "context": 256000, + "output": 64000 + } + }, "north-mini-code-free": { "name": "North Mini Code Free", "tool_call": true, @@ -3694,9 +3702,11 @@ jobs: NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" # High-sensitivity review candidates only. Public repositories first - # use OpenCode Zen's anonymous North Mini Code free endpoint. Zen's - # free-period privacy terms permit retention, so private repositories - # never include this candidate. The pool then falls through to + # 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 # 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, @@ -3707,7 +3717,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/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/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/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index b9dbb106..9e8399cc 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -92,11 +92,13 @@ 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/north-mini-code-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/deepseek-v4-flash-free", "opencode-free/north-mini-code-free", *candidates_text.removeprefix(conditional_public_candidate).split(), ] @@ -115,6 +117,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["opencode-free", "deepseek-v4-flash-free"], ["opencode-free", "north-mini-code-free"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], @@ -135,6 +138,20 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert set(github_candidate_models).issubset(set(github_models)) assert '"context": 256000' in workflow assert '"output": 64000' in workflow + generated_config_match = re.search( + r"jq -n '(\{.*?\})' >\"\$\{OPENCODE_REVIEW_WORKDIR\}/opencode\.jsonc\"", + workflow, + re.DOTALL, + ) + assert generated_config_match is not None + generated_config = json.loads(generated_config_match.group(1)) + free_models = generated_config["provider"]["opencode-free"]["models"] + deepseek_model = free_models["deepseek-v4-flash-free"] + north_model = free_models["north-mini-code-free"] + assert deepseek_model["tool_call"] is True + assert "response_format" not in deepseek_model.get("options", {}) + assert north_model["tool_call"] is True + assert "response_format" not in north_model["options"] assert github_candidate_models == [ "deepseek/deepseek-v3-0324", "openai/gpt-4.1", @@ -1212,7 +1229,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): ) assert ( "needs.validate-pr-metadata.outputs.is_private == 'false' && " - "'opencode-free/north-mini-code-free ' || ''" + "'opencode-free/deepseek-v4-flash-free " + "opencode-free/north-mini-code-free ' || ''" ) in workflow assert ( "github-models/deepseek/deepseek-v3-0324 "