diff --git a/control_plane/resources/management/commands/seed_spark_resources.py b/control_plane/resources/management/commands/seed_spark_resources.py index 3f40250..796f118 100644 --- a/control_plane/resources/management/commands/seed_spark_resources.py +++ b/control_plane/resources/management/commands/seed_spark_resources.py @@ -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( diff --git a/model_router/providers.py b/model_router/providers.py index 08a85d6..1b7142e 100644 --- a/model_router/providers.py +++ b/model_router/providers.py @@ -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"