From 51b81def988ed4e570b5e305dcf53328d52841e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 22:49:50 +0000 Subject: [PATCH] feat(opencode): configure vllm-qwen38 provider for local Qwen3.8-27B --- .gitignore | 2 ++ opencode.json | 23 ++++++++++++++++++----- pilot/ai_review.py | 6 +++--- pilot/cost_model.py | 15 ++++++++------- pilot/opencode_review.py | 8 ++++---- tests/pilot/test_ai_review.py | 7 ++++--- 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 628254c..80b683d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ dist/ __pycache__/ *.pyc .worktrees/ +.claude/ +.opencode/package-lock.json diff --git a/opencode.json b/opencode.json index 11a828d..7b9e95c 100644 --- a/opencode.json +++ b/opencode.json @@ -21,19 +21,32 @@ } } }, - "local": { + "vllm-qwen38": { "npm": "@ai-sdk/openai-compatible", - "name": "Local AI workstation (qwen3.8-27b)", + "name": "Qwen3.8-27B vLLM (RTX 3090, MTP spec-decode)", "options": { "baseURL": "http://192.168.1.79:18020/v1", - "apiKey": "PLACEHOLDER_REPLACED_AT_RUNTIME" + "apiKey": "PLACEHOLDER_REPLACED_AT_RUNTIME", + "timeout": 300000, + "chunkTimeout": 30000 }, "models": { "qwen3.8-27b": { - "name": "Qwen 3.8 27B (local)", + "name": "Qwen3.8-27B (vLLM, MTP, 150k ctx)", + "tools": true, + "thinking": true, + "attachments": false, "limit": { - "context": 57344, + "context": 150000, "output": 8192 + }, + "options": { + "temperature": 0.3, + "topP": 0.8, + "topK": 20, + "repetitionPenalty": 1.05, + "frequencyPenalty": 0, + "presencePenalty": 0 } } } diff --git a/pilot/ai_review.py b/pilot/ai_review.py index 3ea8252..9e82231 100644 --- a/pilot/ai_review.py +++ b/pilot/ai_review.py @@ -489,7 +489,7 @@ def _resolve_display_model(base_model: str, config: dict | None) -> str: 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 + `qwen3.8-27b` → `vllm-qwen38/qwen3.8-27b` (vLLM on RTX 3090 at 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 @@ -505,8 +505,8 @@ def _resolve_display_model(base_model: str, config: dict | None) -> str: cfg_model = (config or {}).get("model") if isinstance(cfg_model, str) and 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. + # through the right provider block (vllm-qwen38 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: diff --git a/pilot/cost_model.py b/pilot/cost_model.py index 4893b7e..1ad7c3b 100644 --- a/pilot/cost_model.py +++ b/pilot/cost_model.py @@ -57,7 +57,7 @@ class Price: """Per-MTok prices. `cache_write` and `cache_read` are absolute rates, not multipliers, so providers with different cache economics stay comparable. - `provider` is the opencode provider name (`headroom`, `local`, ...). It + `provider` is the opencode provider name (`headroom`, `vllm-qwen38`, ...). It doubles as the dispatch key for `.pr-review.json:model` overrides — when a per-repo override is set, `_resolve_display_model` returns `f"{provider}/{key}"` so the opencode subprocess routes correctly. @@ -98,12 +98,13 @@ PRICES: dict[str, Price] = { # xAI Grok — cache_write = input "grok-4.5": Price("Grok 4.5", 2.00, 6.00, 2.00, 0.30), "grok-4.3": Price("Grok 4.3", 1.25, 2.50, 1.25, 0.20), - # Self-hosted — local AI workstation, no per-token charge. provider="local" - # so the opencode subprocess routes via the `local` provider block in - # opencode.json (baseURL=http://192.168.1.79:18020/v1). Equivalent-cost - # column will read $0 — the cost-comparison signal is that the same work - # would bill $X on a paid model. - "qwen3.8-27b": Price("Qwen 3.8 27B (local)", 0.0, 0.0, 0.0, 0.0, provider="local"), + # Self-hosted — AI workstation RTX 3090, vLLM + DFlash2 spec-decode, no + # per-token charge. provider="vllm-qwen38" so the opencode subprocess + # routes via the matching provider block in opencode.json + # (baseURL=http://192.168.1.79:18020/v1). Equivalent-cost column reads $0 + # — the cost-comparison signal is that the same work would bill $X on a + # paid model. + "qwen3.8-27b": Price("Qwen3.8-27B (vLLM, MTP, 150k ctx)", 0.0, 0.0, 0.0, 0.0, provider="vllm-qwen38"), } diff --git a/pilot/opencode_review.py b/pilot/opencode_review.py index 6cd8eda..9d21493 100644 --- a/pilot/opencode_review.py +++ b/pilot/opencode_review.py @@ -397,7 +397,7 @@ def install_config(src: str, dst: str) -> bool: private-network addresses. Real values are supplied at runtime and patched in here. - Env var convention (case-sensitive provider name — `headroom`, `local`): + Env var convention (case-sensitive provider name — `headroom`, `vllm-qwen38`): PRAGENT__BASE_URL — per-provider endpoint override PRAGENT__API_KEY — per-provider API key override @@ -406,9 +406,9 @@ def install_config(src: str, dst: str) -> bool: PRAGENT_MODEL_API_KEY — legacy catchall (same) Per-provider wins over the catchall. The first 2 win when the operator - needs a different endpoint per upstream (e.g. headroom → MiniMax, local → - ai-workstation). The catchall keeps the single-provider deploys from - needing any env config. + needs a different endpoint per upstream (e.g. headroom → MiniMax, + vllm-qwen38 → ai-workstation). The catchall keeps the single-provider + deploys from needing any env config. This is done in Python rather than with opencode's own `{env:VAR}` config templating because the reviewer subprocess runs with an allow-listed diff --git a/tests/pilot/test_ai_review.py b/tests/pilot/test_ai_review.py index a41b6b0..ba91cb1 100644 --- a/tests/pilot/test_ai_review.py +++ b/tests/pilot/test_ai_review.py @@ -462,11 +462,12 @@ def test_resolve_display_model_precedence(monkeypatch): ai_review._resolve_display_model("MiniMax-M2.7", {"model": "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). + # Self-hosted models carry provider="vllm-qwen38" → routes to the + # matching 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" + == "vllm-qwen38/qwen3.8-27b" ) # 3. Env wins over config monkeypatch.setenv("OPENCODE_MODEL", "headroom/MiniMax-M2.7")