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.
62 lines
3.3 KiB
Python
62 lines
3.3 KiB
Python
"""Runtime configuration from environment and arguments."""
|
|
|
|
import os
|
|
import re
|
|
|
|
from tai_pkg.consts import Consts
|
|
|
|
__all__ = ["Config"]
|
|
|
|
|
|
class Config:
|
|
def __init__(self, args):
|
|
self.home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai")
|
|
self.profiles_dir = os.path.join(self.home, "profiles")
|
|
self.audio_dir = os.path.join(self.home, "audio")
|
|
self.db_path = os.path.join(self.home, "memory.db")
|
|
self.history_path = os.path.join(self.home, "history")
|
|
self.model = os.environ.get("TAI_MODEL") or Consts.DEFAULT_MODEL
|
|
self.explicit_model = os.environ.get("TAI_MODEL") or ""
|
|
self.backends = Config.parse_backends(os.environ.get("TAI_BACKENDS"))
|
|
self.openrouter_key = os.environ.get("OPENROUTER_API_KEY") or ""
|
|
self.openrouter_models = Config.split_models(os.environ.get("TAI_OPENROUTER_MODELS"))
|
|
self.retry_seconds = float(os.environ.get("TAI_RETRY_SECONDS") or 0) if re.match(r"^\d+(\.\d+)?$", (os.environ.get("TAI_RETRY_SECONDS") or "0").strip()) else 0.0
|
|
self.opencode_enabled = (os.environ.get("TAI_OPENCODE") or "1").strip().lower() not in ("0", "false", "no", "off")
|
|
self.opencode_models = Config.split_models(os.environ.get("TAI_OPENCODE_MODELS"))
|
|
self.devplace_key = os.environ.get("DEVPLACE_API_KEY") or ""
|
|
self.devplace_base = (os.environ.get("TAI_DEVPLACE_BASE") or Consts.DEVPLACE_BASE).rstrip("/")
|
|
self.devplace_models = Config.split_models(os.environ.get("TAI_DEVPLACE_MODELS"))
|
|
self.openrouter_enabled = (os.environ.get("TAI_OPENROUTER") or "1").strip().lower() not in ("0", "false", "no", "off")
|
|
self.pollinations_enabled = (os.environ.get("TAI_POLLINATIONS") or "1").strip().lower() not in ("0", "false", "no", "off")
|
|
self.pollinations_models = Config.split_models(os.environ.get("TAI_POLLINATIONS_MODELS")) or list(Consts.POLLINATIONS_MODELS)
|
|
self.zen_enabled = (os.environ.get("TAI_ZEN") or "").strip().lower() in ("1", "true", "yes", "on")
|
|
self.zen_models = Config.split_models(os.environ.get("TAI_ZEN_MODELS")) or list(Consts.ZEN_FREE_MODELS)
|
|
self.voice = os.environ.get("TAI_VOICE") or Consts.DEFAULT_VOICE
|
|
self.auto_approve = bool(args.yes)
|
|
self.profile = args.profile or "default"
|
|
os.makedirs(self.profiles_dir, exist_ok=True)
|
|
os.makedirs(self.audio_dir, exist_ok=True)
|
|
|
|
@staticmethod
|
|
def split_models(spec):
|
|
return [item.strip() for item in (spec or "").replace("\n", ",").split(",") if item.strip()]
|
|
|
|
@staticmethod
|
|
def parse_backends(spec):
|
|
found = []
|
|
for entry in (spec or "").replace("\n", ";").split(";"):
|
|
fields = [field.strip() for field in entry.split("|")]
|
|
if len(fields) < 3:
|
|
continue
|
|
label, base, key = fields[0], fields[1].rstrip("/"), fields[2]
|
|
if key.startswith("$"):
|
|
key = os.environ.get(key[1:], "")
|
|
model = fields[3].strip() if len(fields) > 3 else ""
|
|
profile = fields[4].strip().lower() if len(fields) > 4 else ""
|
|
if profile not in ("", "opencode"):
|
|
profile = ""
|
|
if not label or not base:
|
|
continue
|
|
found.append({"label": label, "base": base, "key": key, "model": model or "", "profile": profile})
|
|
return found
|