From 92020d4d46d2b890795826bae07a2870a7fc1e60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 19:13:04 +0000 Subject: [PATCH] feat(ai_review): route per-repo model override through PRICES provider field --- pilot/ai_review.py | 21 +++++++++++++++++---- tests/pilot/test_ai_review.py | 13 ++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/pilot/ai_review.py b/pilot/ai_review.py index a55107a..3ea8252 100644 --- a/pilot/ai_review.py +++ b/pilot/ai_review.py @@ -483,11 +483,15 @@ def _resolve_display_model(base_model: str, config: dict | None) -> str: Precedence (highest first): 1. `OPENCODE_MODEL` env var — operator override, used as-is (already a - provider-prefixed opencode ref). + provider-prefixed opencode ref like `headroom/MiniMax-M2.7`). 2. `.pr-review.json:model` — per-repo override. Already validated against `cost_model.PRICES` by `parse_repo_config`, so a bare key - like `claude-sonnet-5` is safe to use as the opencode ref AND the - REVIEW_HEADER label. + like `claude-sonnet-5` or `qwen3.8-27b` is safe. Re-prefixed with + the model's `provider` field from `cost_model.Price` (default + `headroom`) so the opencode subprocess routes correctly — e.g. + `qwen3.8-27b` → `local/qwen3.8-27b` (local AI workstation on + 192.168.1.79:18020), `claude-sonnet-5` → `headroom/claude-sonnet-5` + (Anthropic pricing proxy). 3. Default — `f"headroom/{base_model}"` where `base_model` is the bare `OLLAMA_MODEL` (e.g. `"MiniMax-M2.7" → "headroom/MiniMax-M2.7"`). @@ -500,7 +504,16 @@ def _resolve_display_model(base_model: str, config: dict | None) -> str: return env cfg_model = (config or {}).get("model") if isinstance(cfg_model, str) and cfg_model.strip(): - return cfg_model.strip() + # Look up the provider from PRICES so the opencode subprocess routes + # through the right provider block (local vs headroom). Lazy import — + # the ollama path doesn't touch cost_model. + from cost_model import PRICES + provider = PRICES.get(cfg_model.strip()) + if provider is not None: + return f"{provider.provider}/{cfg_model.strip()}" + # parse_repo_config already drops unknowns, but stay defensive: fall + # back to headroom so the review still runs rather than crash. + return f"headroom/{cfg_model.strip()}" return f"headroom/{base_model}" diff --git a/tests/pilot/test_ai_review.py b/tests/pilot/test_ai_review.py index 9f6ccaa..a41b6b0 100644 --- a/tests/pilot/test_ai_review.py +++ b/tests/pilot/test_ai_review.py @@ -451,15 +451,22 @@ def test_parse_repo_config_model_must_be_string(): def test_resolve_display_model_precedence(monkeypatch): - # Order is OPENCODE_MODEL env > config['model'] > headroom/{base}. + # Order is OPENCODE_MODEL env > config['model'] (re-prefixed by provider) > headroom/{base}. monkeypatch.delenv("OPENCODE_MODEL", raising=False) # 1. No env, no config → headroom/ assert ai_review._resolve_display_model("MiniMax-M2.7", None) == "headroom/MiniMax-M2.7" assert ai_review._resolve_display_model("MiniMax-M2.7", {}) == "headroom/MiniMax-M2.7" - # 2. No env, config has model → use config model as-is (already a known key) + # 2. No env, config has a PRICES key → re-prefixed with that model's provider. + # headroom-hosted models default to provider="headroom". assert ( ai_review._resolve_display_model("MiniMax-M2.7", {"model": "claude-sonnet-5"}) - == "claude-sonnet-5" + == "headroom/claude-sonnet-5" + ) + # Self-hosted models carry provider="local" → routes to the `local` + # provider block in opencode.json (AI workstation on 192.168.1.79:18020). + assert ( + ai_review._resolve_display_model("MiniMax-M2.7", {"model": "qwen3.8-27b"}) + == "local/qwen3.8-27b" ) # 3. Env wins over config monkeypatch.setenv("OPENCODE_MODEL", "headroom/MiniMax-M2.7")