Support local OpenCode Sol provider

This commit is contained in:
Daniel Maddern 2026-08-15 14:16:27 +07:00
parent d123659bd2
commit e11bcd1e59
2 changed files with 50 additions and 29 deletions

View file

@ -27,7 +27,12 @@ class Command(BaseCommand):
"provider": "opencode",
"compute": spark,
"roles": ["PROJECT_BRAIN", "PLANNING", "ARCHAEOLOGY_INTERPRETATION"],
"config": {"command": os.environ.get("ARTIFEX_SOL_OPENCODE_COMMAND", "opencode run --json --no-repo --stdin")},
"config": {
"transport": os.environ.get("ARTIFEX_SOL_TRANSPORT", "local"),
"command": os.environ.get("ARTIFEX_SOL_OPENCODE_COMMAND", "/home/daniel/.opencode/bin/opencode run"),
"use_pty": os.environ.get("ARTIFEX_SOL_USE_PTY", "1") == "1",
"timeout_seconds": int(os.environ.get("ARTIFEX_SOL_TIMEOUT_SECONDS", "120")),
},
},
)
Resource.objects.update_or_create(

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import json
import re
import shlex
import subprocess
import urllib.error
import urllib.request
@ -36,6 +38,17 @@ def extract_json_object(text: str) -> dict[str, Any]:
return _extract_json(text[start : end + 1])
def _clean_opencode_output(text: str) -> str:
text = re.sub(r"\x1b\[[0-9;?]*[ -/]*[@-~]", "", text)
lines = []
for line in text.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("> build"):
continue
lines.append(line)
return "\n".join(lines).strip()
@dataclass
class SolProvider:
resource: Resource
@ -44,44 +57,47 @@ class SolProvider:
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
config = self.resource.config
compute = self.resource.compute
ssh_alias = (compute.config if compute else {}).get("ssh_alias", config.get("ssh_alias", "spark"))
timeout = int(config.get("timeout_seconds", 120))
remote_command = config.get("command", "opencode run --json --no-repo --stdin")
payload = {
"mode": "reasoning_only",
"output_schema": "json_object",
"prompt": request.prompt,
"token_budget": request.token_budget,
}
completed = subprocess.run(
["ssh", str(ssh_alias), str(remote_command)],
input=json.dumps(payload),
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
command = str(config.get("command", "opencode run"))
transport = str(config.get("transport", "ssh"))
use_pty = bool(config.get("use_pty", False))
if transport == "local":
argv = [*shlex.split(command), request.prompt]
if use_pty:
shell_command = " ".join(shlex.quote(part) for part in argv)
argv = ["script", "-q", "-e", "-c", shell_command, "/dev/null"]
completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
else:
ssh_alias = (compute.config if compute else {}).get("ssh_alias", config.get("ssh_alias", "spark"))
remote_command = " ".join(shlex.quote(part) for part in [*shlex.split(command), request.prompt])
completed = subprocess.run(
["ssh", "-tt", str(ssh_alias), remote_command],
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
if completed.returncode != 0:
raise ProviderError(completed.stderr.strip() or "Sol provider failed")
data = _extract_json(completed.stdout)
content = data.get("content") or data.get("response") or data.get("plan")
if isinstance(content, (dict, list)):
content = json.dumps(content)
if not isinstance(content, str):
raise ProviderError("Sol provider response missing string content")
content = _clean_opencode_output(completed.stdout)
if not content:
raise ProviderError("Sol provider response missing content")
return ModelResponseContract(
model=self.resource.name,
content=content,
metadata={"provider": self.provider_name, "usage": data.get("usage", {})},
metadata={"provider": self.provider_name, "usage": {}},
)
def health(self) -> str:
compute = self.resource.compute
ssh_alias = (compute.config if compute else {}).get("ssh_alias", self.resource.config.get("ssh_alias", "spark"))
config = self.resource.config
try:
completed = subprocess.run(
["ssh", str(ssh_alias), "true"], capture_output=True, text=True, timeout=10, check=False
)
if config.get("transport") == "local":
command = shlex.split(str(config.get("command", "opencode")))[0]
completed = subprocess.run([command, "--version"], capture_output=True, text=True, timeout=10, check=False)
else:
compute = self.resource.compute
ssh_alias = (compute.config if compute else {}).get("ssh_alias", self.resource.config.get("ssh_alias", "spark"))
completed = subprocess.run(["ssh", str(ssh_alias), "true"], capture_output=True, text=True, timeout=10, check=False)
except Exception:
return "UNAVAILABLE"
return "AVAILABLE" if completed.returncode == 0 else "UNAVAILABLE"