Files
tai/tai_pkg/grok.py
T
retoor 8df5cf7a45 Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original
tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
2026-10-10 19:25:02 +02:00

79 lines
3.3 KiB
Python

"""Grok CLI backend."""
import json
import os
import shutil
import tempfile
from tai_pkg.clirun import Cli
from tai_pkg.errors import BackendError
from tai_pkg.opencode import Opencode
__all__ = ["Grok"]
class Grok:
"""Grok."""
@staticmethod
def grok_binary():
return os.environ.get("GROK_BIN") or shutil.which("grok") or shutil.which("grokcli")
@staticmethod
def grok_parse_output(stdout):
try:
data = json.loads(stdout or "")
except ValueError:
return Opencode.opencode_parse(stdout)
if isinstance(data, dict):
if data.get("is_error") or data.get("error"):
detail = data.get("error") or data.get("result") or data.get("text") or "unknown error"
raise BackendError("grok: %s" % str(detail)[:300])
if isinstance(data.get("content"), str) or isinstance(data.get("tool_calls"), list):
if "content" in data or "tool_calls" in data:
return data
for key in ("text", "result", "output", "answer", "content"):
value = data.get(key)
if isinstance(value, str) and value.strip():
return Opencode.opencode_parse(value)
return Opencode.opencode_parse(stdout)
@staticmethod
def grok_run(prompt, timeout):
binary = Grok.grok_binary()
if not binary:
raise BackendError("grok CLI not found (GROK_BIN or PATH)")
with tempfile.TemporaryDirectory(prefix="tai-grok-") as folder:
prompt_path = os.path.join(folder, "prompt.txt")
with open(prompt_path, "w", encoding="utf-8") as handle:
handle.write(prompt)
variants = [
[binary, "--prompt-file", prompt_path, "--output-format", "json", "--no-auto-update"],
[binary, "--prompt-file", prompt_path, "--output-format", "json"],
[binary, "-p", prompt, "--output-format", "json"],
[binary, "-p", prompt],
]
last_error = ""
for argv in variants:
done = Cli._cli_run(argv, None, timeout, "grok")
if done.returncode != 0 and Cli._cli_unknown_flag(done.stderr) and argv is not variants[-1]:
last_error = (done.stderr or "").strip()[:200]
continue
if done.returncode != 0 and not (done.stdout or "").strip():
blob = ((done.stderr or "") + "\n" + (done.stdout or "")).strip()
if Cli._cli_auth_failure(blob):
raise BackendError("grok not authenticated (set XAI_API_KEY or run grok login): %s" % blob[:200])
raise BackendError("grok exit %d: %s" % (done.returncode, blob[:300] or last_error or "no output"))
if not (done.stdout or "").strip():
blob = (done.stderr or "").strip()
raise BackendError("grok returned no output: %s" % (blob[:300] or last_error or "empty stdout"))
return done.stdout or ""
raise BackendError("grok failed: %s" % (last_error or "no usable output"))
@staticmethod
def grok_health():
binary = Grok.grok_binary()
if not binary:
return "missing: grok CLI not found"
return Cli._cli_version_only_health(binary, "grok")