127 lines
4.8 KiB
Python
Executable File
127 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render and verify all client-facing profile data from one matrix."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import pathlib
|
|
import re
|
|
|
|
|
|
def load(path: pathlib.Path) -> dict:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
if data.get("version") != 1 or not isinstance(data.get("profiles"), list):
|
|
raise SystemExit("Unsupported profile matrix schema")
|
|
ids: set[str] = set()
|
|
aliases: set[str] = set()
|
|
for item in data["profiles"]:
|
|
required = {"id", "alias", "context", "model_env", "description"}
|
|
missing = required - item.keys()
|
|
if missing:
|
|
raise SystemExit(f"Profile entry missing: {sorted(missing)}")
|
|
if item["id"] in ids or item["alias"] in aliases:
|
|
raise SystemExit(f"Duplicate profile or alias: {item['id']}")
|
|
if not isinstance(item["context"], int) or item["context"] < 8192:
|
|
raise SystemExit(f"Invalid context for {item['id']}")
|
|
ids.add(item["id"])
|
|
aliases.add(item["alias"])
|
|
if data.get("default_profile") not in ids:
|
|
raise SystemExit("default_profile is not defined")
|
|
return data
|
|
|
|
|
|
def write_if_changed(path: pathlib.Path, content: str) -> None:
|
|
if path.exists() and path.read_text(encoding="utf-8") == content:
|
|
return
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def render_router(data: dict) -> str:
|
|
payload = {
|
|
"profiles": {
|
|
item["id"]: {
|
|
"context": item["context"],
|
|
"model_alias": item["alias"],
|
|
}
|
|
for item in data["profiles"]
|
|
}
|
|
}
|
|
return json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
|
|
|
|
def render_docs(data: dict) -> str:
|
|
lines = [
|
|
"# Profilmatrix",
|
|
"",
|
|
"Diese Datei wird aus `config/profile-matrix.json` erzeugt. Änderungen gehören nur in die JSON-Matrix.",
|
|
"",
|
|
f"Standardprofil: **{data['default_profile']}** · globales Ausgabelimit: **{data['max_output_tokens']} Token**",
|
|
"",
|
|
"| Profil | API-Alias | Kontext | Modell | GPU-Verteilung | Vision | MTP |",
|
|
"|---|---|---:|---|---|---|---:|",
|
|
]
|
|
for item in data["profiles"]:
|
|
lines.append(
|
|
f"| {item['id']} | `{item['alias']}` | {item['context']:,} | "
|
|
f"{item.get('model_family', '')} | {item.get('gpu_split', '')} | "
|
|
f"{'ja' if item.get('vision') else 'nein'} | {item.get('mtp', '')} |"
|
|
)
|
|
lines.extend(["", "## Zweck", ""])
|
|
for item in data["profiles"]:
|
|
lines.append(f"- **{item['id']}**: {item['description']}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def verify_compose(data: dict, compose: pathlib.Path) -> None:
|
|
text = compose.read_text(encoding="utf-8")
|
|
expected_cap = f'MAX_GENERATION_TOKENS: "{int(data["max_output_tokens"])}"'
|
|
if expected_cap not in text:
|
|
raise SystemExit("Router output-token cap drift")
|
|
for item in data["profiles"]:
|
|
block_match = re.search(
|
|
rf"(?ms)^ llama-{re.escape(item['id'])}:\n(?P<body>.*?)(?=^ [a-zA-Z0-9_-]+:|\Z)",
|
|
text,
|
|
)
|
|
if not block_match:
|
|
raise SystemExit(f"Compose service llama-{item['id']} is missing")
|
|
block = block_match.group("body")
|
|
if f"- {item['alias']}" not in block:
|
|
raise SystemExit(f"Compose alias drift for {item['id']}")
|
|
env_prefix = item["id"].upper()
|
|
if f"${{{env_prefix}_CONTEXT:-{item['context']}}}" not in block:
|
|
raise SystemExit(f"Compose context drift for {item['id']}")
|
|
|
|
|
|
def main() -> None:
|
|
root = pathlib.Path(__file__).resolve().parents[2]
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--matrix", type=pathlib.Path,
|
|
default=root / "config/profile-matrix.json")
|
|
parser.add_argument("--router", type=pathlib.Path,
|
|
default=root / "router/router_profiles.json")
|
|
parser.add_argument("--docs", type=pathlib.Path,
|
|
default=root / "docs/STANDARD_PROFILE_MATRIX.md")
|
|
parser.add_argument("--compose", type=pathlib.Path,
|
|
default=root / "compose.yaml")
|
|
parser.add_argument("--check", action="store_true")
|
|
args = parser.parse_args()
|
|
data = load(args.matrix)
|
|
router = render_router(data)
|
|
docs = render_docs(data)
|
|
verify_compose(data, args.compose)
|
|
if args.check:
|
|
if args.router.read_text(encoding="utf-8") != router:
|
|
raise SystemExit("router_profiles.json is not generated from the matrix")
|
|
if args.docs.read_text(encoding="utf-8") != docs:
|
|
raise SystemExit("profile documentation is not generated from the matrix")
|
|
else:
|
|
write_if_changed(args.router, router)
|
|
write_if_changed(args.docs, docs)
|
|
print("PROFILE_MATRIX_OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|