9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
260 lines
8.2 KiB
Python
260 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Render ODS runtime config surfaces deterministically.
|
|
|
|
The first purpose of this script is read-only comparison: installers and
|
|
runtime mutators can ask what config should look like without writing files.
|
|
Follow-up wiring can then replace ad-hoc heredocs one surface at a time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_MODEL = "qwen3.5-9b"
|
|
DEFAULT_GGUF = "Qwen3.5-9B-Q4_K_M.gguf"
|
|
DEFAULT_CONTEXT = 131072
|
|
DEFAULT_LITELLM_KEY = "sk-lemonade"
|
|
NO_KEY = "no-key"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RenderInputs:
|
|
model: str
|
|
gguf_file: str
|
|
lemonade_model_id: str
|
|
lemonade_api_base: str
|
|
gpu_backend: str
|
|
ods_mode: str
|
|
llm_base_url: str
|
|
litellm_key: str
|
|
opencode_port: int
|
|
context_length: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RenderedFile:
|
|
surface: str
|
|
path: str
|
|
content: str
|
|
|
|
|
|
def ensure_trailing_newline(text: str) -> str:
|
|
return text if text.endswith("\n") else f"{text}\n"
|
|
|
|
|
|
def lemonade_model_id(inputs: RenderInputs) -> str:
|
|
if inputs.lemonade_model_id:
|
|
return inputs.lemonade_model_id
|
|
return f"extra.{inputs.gguf_file}"
|
|
|
|
|
|
def hermes_model_id(inputs: RenderInputs) -> str:
|
|
if inputs.ods_mode == "lemonade" or inputs.gpu_backend == "amd":
|
|
return lemonade_model_id(inputs)
|
|
return inputs.gguf_file or inputs.model
|
|
|
|
|
|
def opencode_key(inputs: RenderInputs) -> str:
|
|
return inputs.litellm_key if inputs.ods_mode == "lemonade" else NO_KEY
|
|
|
|
|
|
def render_litellm_lemonade(inputs: RenderInputs) -> RenderedFile:
|
|
model = lemonade_model_id(inputs)
|
|
api_base = inputs.lemonade_api_base.rstrip("/") or "http://llama-server:8080/api/v1"
|
|
content = f"""model_list:
|
|
- model_name: default
|
|
litellm_params:
|
|
model: openai/{model}
|
|
api_base: {api_base}
|
|
api_key: {inputs.litellm_key}
|
|
extra_body:
|
|
chat_template_kwargs:
|
|
enable_thinking: false
|
|
|
|
- model_name: "*"
|
|
litellm_params:
|
|
model: openai/{model}
|
|
api_base: {api_base}
|
|
api_key: {inputs.litellm_key}
|
|
extra_body:
|
|
chat_template_kwargs:
|
|
enable_thinking: false
|
|
|
|
litellm_settings:
|
|
drop_params: true
|
|
set_verbose: false
|
|
request_timeout: 900
|
|
stream_timeout: 900
|
|
"""
|
|
return RenderedFile("litellm-lemonade", "config/litellm/lemonade.yaml", content)
|
|
|
|
|
|
def render_hermes(inputs: RenderInputs) -> RenderedFile:
|
|
model = hermes_model_id(inputs)
|
|
content = f"""model:
|
|
default: "{model}"
|
|
provider: "custom"
|
|
base_url: "{inputs.llm_base_url}"
|
|
context_length: {inputs.context_length}
|
|
|
|
auxiliary:
|
|
compression:
|
|
context_length: {inputs.context_length}
|
|
|
|
compression:
|
|
enabled: true
|
|
threshold: 0.75
|
|
target_ratio: 0.50
|
|
protect_last_n: 40
|
|
"""
|
|
return RenderedFile("hermes", "data/hermes/config.yaml", content)
|
|
|
|
|
|
def render_perplexica(inputs: RenderInputs) -> RenderedFile:
|
|
model = lemonade_model_id(inputs) if inputs.ods_mode == "lemonade" else (inputs.gguf_file or inputs.model)
|
|
base_url = inputs.llm_base_url.rstrip("/") or "http://llama-server:8080"
|
|
if not (base_url.endswith("/v1") or base_url.endswith("/api/v1")):
|
|
base_url = f"{base_url}/v1"
|
|
payload = {
|
|
"modelProviders": [
|
|
{
|
|
"id": "openai",
|
|
"type": "openai",
|
|
"name": "ODS",
|
|
"config": {
|
|
"apiKey": opencode_key(inputs),
|
|
"baseURL": base_url,
|
|
},
|
|
"chatModels": [{"key": model, "name": model}],
|
|
}
|
|
],
|
|
"preferences": {
|
|
"defaultChatProvider": "openai",
|
|
"defaultChatModel": model,
|
|
"defaultEmbeddingProvider": "transformers",
|
|
"defaultEmbeddingModel": "Xenova/all-MiniLM-L6-v2",
|
|
},
|
|
"setupComplete": True,
|
|
}
|
|
return RenderedFile(
|
|
"perplexica",
|
|
"data/perplexica/settings.seed.json",
|
|
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
)
|
|
|
|
|
|
def render_opencode(inputs: RenderInputs) -> RenderedFile:
|
|
payload = {
|
|
"provider": "openai-compatible",
|
|
"baseURL": inputs.llm_base_url,
|
|
"apiKey": opencode_key(inputs),
|
|
"model": lemonade_model_id(inputs) if inputs.ods_mode == "lemonade" else inputs.model,
|
|
"port": inputs.opencode_port,
|
|
}
|
|
return RenderedFile(
|
|
"opencode",
|
|
".opencode/auth.json",
|
|
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
)
|
|
|
|
|
|
def render_env(inputs: RenderInputs) -> RenderedFile:
|
|
lines = [
|
|
f"ODS_MODE={inputs.ods_mode}",
|
|
f"LLM_BACKEND={'lemonade' if inputs.ods_mode == 'lemonade' else 'llama-server'}",
|
|
f"LLM_MODEL={inputs.model}",
|
|
f"GGUF_FILE={inputs.gguf_file}",
|
|
f"GPU_BACKEND={inputs.gpu_backend}",
|
|
f"LLM_API_URL={inputs.llm_base_url}",
|
|
f"CTX_SIZE={inputs.context_length}",
|
|
f"MAX_CONTEXT={inputs.context_length}",
|
|
]
|
|
return RenderedFile("env", ".env.generated", "\n".join(lines) + "\n")
|
|
|
|
|
|
RENDERERS: dict[str, Callable[[RenderInputs], RenderedFile]] = {
|
|
"env": render_env,
|
|
"opencode": render_opencode,
|
|
"litellm-lemonade": render_litellm_lemonade,
|
|
"perplexica": render_perplexica,
|
|
"hermes": render_hermes,
|
|
}
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--surface", choices=["all", *sorted(RENDERERS)], default="all")
|
|
parser.add_argument("--model", default=DEFAULT_MODEL)
|
|
parser.add_argument("--gguf-file", default=DEFAULT_GGUF)
|
|
parser.add_argument("--lemonade-model-id", default="")
|
|
parser.add_argument("--lemonade-api-base", default="http://llama-server:8080/api/v1")
|
|
parser.add_argument("--gpu-backend", choices=["amd", "apple", "cpu", "nvidia"], default="nvidia")
|
|
parser.add_argument("--ods-mode", choices=["local", "cloud", "hybrid", "lemonade"], default="local")
|
|
parser.add_argument("--llm-base-url", default="http://llama-server:8080/v1")
|
|
parser.add_argument("--litellm-key", default=DEFAULT_LITELLM_KEY)
|
|
parser.add_argument("--opencode-port", type=int, default=3003)
|
|
parser.add_argument("--context-length", type=int, default=DEFAULT_CONTEXT)
|
|
parser.add_argument("--format", choices=["json", "paths"], default="json")
|
|
parser.add_argument("--output-root", default=".", help="Root directory used with --write")
|
|
parser.add_argument("--write", action="store_true", help="Write rendered files under --output-root")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def select_surfaces(surface: str) -> list[str]:
|
|
if surface == "all":
|
|
return ["env", "opencode", "litellm-lemonade", "perplexica", "hermes"]
|
|
return [surface]
|
|
|
|
|
|
def render(args: argparse.Namespace) -> dict[str, object]:
|
|
inputs = RenderInputs(
|
|
model=args.model,
|
|
gguf_file=args.gguf_file,
|
|
lemonade_model_id=args.lemonade_model_id,
|
|
lemonade_api_base=args.lemonade_api_base,
|
|
gpu_backend=args.gpu_backend,
|
|
ods_mode=args.ods_mode,
|
|
llm_base_url=args.llm_base_url,
|
|
litellm_key=args.litellm_key,
|
|
opencode_port=args.opencode_port,
|
|
context_length=args.context_length,
|
|
)
|
|
files = [RENDERERS[name](inputs) for name in select_surfaces(args.surface)]
|
|
written: list[str] = []
|
|
if args.write:
|
|
output_root = Path(args.output_root)
|
|
for item in files:
|
|
target = output_root / item.path
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(ensure_trailing_newline(item.content), encoding="utf-8")
|
|
written.append(str(target))
|
|
return {
|
|
"version": "1",
|
|
"mode": "write" if args.write else "dry-run",
|
|
"inputs": asdict(inputs),
|
|
"files": [asdict(RenderedFile(item.surface, item.path, ensure_trailing_newline(item.content))) for item in files],
|
|
"written": written,
|
|
}
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = parse_args(argv)
|
|
payload = render(args)
|
|
if args.format == "paths":
|
|
for item in payload["files"]:
|
|
print(item["path"])
|
|
else:
|
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|