diff --git a/home/.config/opencode/config.json b/home/.config/opencode/config.json new file mode 100644 index 0000000..07fbf03 --- /dev/null +++ b/home/.config/opencode/config.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://opencode.ai/config.json", + + "model": "openai/qwen3-coder-30b-a3b-instruct", + "small_model": "openai/gemini-2.5-flash-lite", + + "provider": { + "openai": { + "name": "nuclide-litellm", + "options": { + "apiKey": "{env:OPENAI_API_KEY}", + "baseURL": "{env:OPENAI_BASE_URL}", + "timeout": 600000, + "chunkTimeout": 60000 + } + }, + "anthropic": { + "options": { + "apiKey": "{env:ANTHROPIC_API_KEY}", + "baseURL": "{env:ANTHROPIC_BASE_URL}", + "timeout": 600000 + } + } + }, + + "mcp": { + "nuclide-dev": { + "type": "remote", + "url": "https://mcp.nuclide.systems/group/dev/mcp", + "enabled": true, + "headers": { + "Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}" + } + }, + "nuclide-research": { + "type": "remote", + "url": "https://mcp.nuclide.systems/group/research/mcp", + "enabled": true, + "headers": { + "Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}" + } + }, + "nuclide-personal": { + "type": "remote", + "url": "https://mcp.nuclide.systems/group/personal/mcp", + "enabled": true, + "headers": { + "Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}" + } + }, + "nuclide-image": { + "type": "remote", + "url": "https://mcp.nuclide.systems/group/image/mcp", + "enabled": true, + "headers": { + "Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}" + } + }, + "nuclide-storage": { + "type": "remote", + "url": "https://mcp.nuclide.systems/group/storage/mcp", + "enabled": true, + "headers": { + "Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}" + } + } + } +} diff --git a/home/.config/opencode/sync.py b/home/.config/opencode/sync.py new file mode 100644 index 0000000..95268d3 --- /dev/null +++ b/home/.config/opencode/sync.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +Regenerate ~/.config/opencode/config.json from live nuclide.systems state. + +Queries: + - LiteLLM http://192.168.1.40:14000/v1/models → available models + - MCP GW https://mcp.nuclide.systems/servers → registered server groups + +Run manually or wire into dotfiles install / Coder startup script. + +Requires: MCP_GATEWAY_TOKEN env var (get via https://mcp.nuclide.systems/mcp-config + or set from GATEWAY_API_KEYS in .env) +""" +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +# ── config ──────────────────────────────────────────────────────────────────── +LITELLM_BASE = os.environ.get("OPENAI_BASE_URL", "http://192.168.1.40:14000/v1") +LITELLM_KEY = os.environ.get("OPENAI_API_KEY", "sk-tapirnase") +MCP_BASE = "https://mcp.nuclide.systems" +CONFIG_PATH = Path.home() / ".config" / "opencode" / "config.json" + +# Preferred model order for the default picker +PREFERRED_CODER = ["qwen3-coder-30b-a3b-instruct", "openai-gpt-oss-120b", "gemini-2.5-pro"] +PREFERRED_SMALL = ["gemini-2.5-flash-lite", "mistral-small-latest", "gemini-2.5-flash"] + +# ── helpers ─────────────────────────────────────────────────────────────────── +def _get(url: str, token: str | None = None, api_key: str | None = None) -> dict | list | None: + try: + import urllib.request, urllib.error + req = urllib.request.Request(url) + if token: + req.add_header("Authorization", f"Bearer {token}") + if api_key: + req.add_header("Authorization", f"Bearer {api_key}") + with urllib.request.urlopen(req, timeout=10) as r: + return json.loads(r.read()) + except Exception as e: + print(f" warn: {url}: {e}", file=sys.stderr) + return None + + +def pick_model(available: set[str], preferences: list[str]) -> str | None: + for name in preferences: + if name in available: + return f"openai/{name}" + return None + + +# ── main ────────────────────────────────────────────────────────────────────── +def main() -> None: + print("opencode sync: fetching live state…") + + # 1. LiteLLM models + models_resp = _get(f"{LITELLM_BASE}/models", api_key=LITELLM_KEY) + available: set[str] = set() + if models_resp and "data" in models_resp: + available = {m["id"] for m in models_resp["data"]} + print(f" LiteLLM: {len(available)} models") + else: + print(" LiteLLM: unreachable, using fallback models") + + default_model = pick_model(available, PREFERRED_CODER) or "openai/qwen3-coder-30b-a3b-instruct" + small_model = pick_model(available, PREFERRED_SMALL) or "openai/gemini-2.5-flash-lite" + print(f" default: {default_model}") + print(f" small: {small_model}") + + # 2. MCP gateway groups + gw_token = os.environ.get("MCP_GATEWAY_TOKEN", "") + servers_resp = _get(f"{MCP_BASE}/servers", token=gw_token) + groups: set[str] = set() + if isinstance(servers_resp, list): + for s in servers_resp: + if isinstance(s, dict): + grp = s.get("group") + if grp: + groups.add(grp) + elif isinstance(servers_resp, dict): + for name, cfg in servers_resp.items(): + grp = cfg.get("group") if isinstance(cfg, dict) else None + if grp: + groups.add(grp) + if not groups: + # fallback: static known groups + groups = {"dev", "research", "personal", "image", "storage"} + print(f" MCP GW: unreachable, using static groups: {sorted(groups)}") + else: + print(f" MCP GW: groups = {sorted(groups)}") + + mcp_entries: dict = {} + for group in sorted(groups): + mcp_entries[f"nuclide-{group}"] = { + "type": "remote", + "url": f"{MCP_BASE}/group/{group}/mcp", + "enabled": True, + "headers": {"Authorization": "Bearer {env:MCP_GATEWAY_TOKEN}"}, + } + + # 3. Build config + cfg = { + "$schema": "https://opencode.ai/config.json", + "model": default_model, + "small_model": small_model, + "provider": { + "openai": { + "name": "nuclide-litellm", + "options": { + "apiKey": "{env:OPENAI_API_KEY}", + "baseURL": "{env:OPENAI_BASE_URL}", + "timeout": 600000, + "chunkTimeout": 60000, + }, + }, + "anthropic": { + "options": { + "apiKey": "{env:ANTHROPIC_API_KEY}", + "baseURL": "{env:ANTHROPIC_BASE_URL}", + "timeout": 600000, + }, + }, + }, + "mcp": mcp_entries, + } + + CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + CONFIG_PATH.write_text(json.dumps(cfg, indent=2) + "\n") + print(f" written: {CONFIG_PATH}") + + +if __name__ == "__main__": + main() diff --git a/install.sh b/install.sh index 1ca9080..601f5bb 100755 --- a/install.sh +++ b/install.sh @@ -50,4 +50,9 @@ for BIN in codium code; do echo ": extensions synced" fi done +# 7. opencode config sync (regenerates ~/.config/opencode/config.json from live state) +if command -v python3 >/dev/null 2>&1; then + python3 "$HOME/.config/opencode/sync.py" 2>/dev/null || true +fi + echo "dotfiles installed: zsh + ohmyzsh + symlinks from $SRC/home"