Files
llm/scripts/model_chat_server.py
T

2954 lines
126 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import datetime as dt
import json
import mimetypes
import os
import re
import shutil
import socket
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse, urlunparse
from ask_1c_rag import DEFAULT_INDEX as DEFAULT_1C_RAG_INDEX
from ask_1c_rag import DEFAULT_RAG_PROMPT as DEFAULT_1C_RAG_PROMPT
from ask_1c_rag import format_context, render_prompt
from common import ROOT, call_chat_completion, localize_workspace_path, read_json, read_yaml_mapping, search_lexical_index
from rag_profiles import list_rag_profiles, resolve_rag_profile
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8765
DEFAULT_BASE_URL = "http://docker-gpu.cin.su:8000"
DEFAULT_STATIC_DIR = ROOT / "tools" / "model-chat"
REGISTRY_INDEX = ROOT / "registry" / "index.json"
REPORT_ROOT = Path(os.environ.get("MODEL_CHAT_REPORT_ROOT") or (ROOT / "reports"))
REPORT_DIR = REPORT_ROOT / "model-chat"
IMAGE_GALLERY_DIR = REPORT_DIR / "images"
IMAGE_JOBS = REPORT_DIR / "image-jobs.jsonl"
MODEL_INGEST_DIR = ROOT / "models" / "incoming"
MODEL_INGEST_JOBS = REPORT_ROOT / "model-ingest" / "jobs.jsonl"
GPU_PROFILES_PATH = ROOT / "config" / "gpu_profiles.json"
RUNTIME_PROFILES_PATH = ROOT / "config" / "runtime_profiles.json"
MODEL_CHAT_PREFLIGHT_REPORT = REPORT_ROOT / "model-chat" / "preflight.json"
IMAGE_PROXY_JOBS: dict[str, dict] = {}
IMAGE_PROXY_JOB_LOCK = threading.Lock()
PLUGIN_ORDER = ["text", "translation", "audio", "video", "image", "1c"]
PLUGIN_LABELS = {
"text": "Текст",
"translation": "Перевод",
"audio": "Звук",
"video": "Видео",
"image": "Фото",
"1c": "1С",
}
TASK_PLUGIN_MAP = {
"text": "text",
"chat": "text",
"summarization": "text",
"code": "text",
"tool-use": "text",
"translation": "translation",
"speech-to-text": "audio",
"speech-translation": "audio",
"video": "video",
"image-understanding": "video",
"document-understanding": "video",
"visual-question-answering": "video",
"image-generation": "image",
"image-editing": "image",
"inpainting": "image",
"1c": "1c",
"1c-rag": "1c",
"bsl-code": "1c",
"metadata-safety": "1c",
"1c-query": "1c",
}
DEFAULT_PROMPTS = {
"text": "Кратко объясни, зачем нужен реестр локальных моделей.",
"translation": "Переведи на английский: Нужно проверить качество локальной модели перевода.",
"audio": "Составь короткий план проверки модели распознавания речи для русского языка.",
"video": "Опиши, какие вопросы стоит задать vision-language модели при проверке видео.",
"image": "Фотореалистичный рабочий стол инженера 1С, два монитора, заметки, спокойный дневной свет.",
"1c": "Какие метаданные 1С нужно получить перед изменением формы документа?",
}
ENDPOINT_PRESETS = [
{"id": "vllm-text", "label": "vLLM text", "base_url": "http://docker-gpu.cin.su:8000"},
{"id": "llama-gguf", "label": "llama.cpp GGUF", "base_url": "http://docker-gpu.cin.su:8080"},
{"id": "llama-gguf-q6-test", "label": "llama.cpp Q6 test", "base_url": "http://docker-gpu.cin.su:8081"},
{"id": "docker-test-q6-cpu", "label": "docker-test Q6 CPU", "base_url": "http://docker-test.cin.su:18086"},
{"id": "translation-api", "label": "Translation API", "base_url": "http://docker-gpu.cin.su:8010"},
{"id": "audio-api", "label": "Audio API", "base_url": "http://docker-gpu.cin.su:8020"},
{"id": "video-api", "label": "Video API", "base_url": "http://docker-gpu.cin.su:8030"},
{"id": "image-api", "label": "Image API", "base_url": "http://docker-gpu.cin.su:8040"},
{"id": "local-vllm", "label": "local", "base_url": "http://127.0.0.1:8000"},
]
ALLOWED_ENDPOINT_HOSTS = {
("http", "docker-gpu.cin.su", 8000),
("http", "docker-gpu.cin.su", 8080),
("http", "docker-gpu.cin.su", 8081),
("http", "docker-gpu.cin.su", 8010),
("http", "docker-gpu.cin.su", 8020),
("http", "docker-gpu.cin.su", 8030),
("http", "docker-gpu.cin.su", 8040),
("http", "docker-test.cin.su", 18081),
("http", "docker-test.cin.su", 18086),
("http", "192.168.200.61", 18081),
("http", "192.168.200.61", 18086),
("http", "127.0.0.1", 8000),
("http", "127.0.0.1", 8080),
("http", "127.0.0.1", 8081),
("http", "127.0.0.1", 8010),
("http", "127.0.0.1", 8020),
("http", "127.0.0.1", 8030),
("http", "127.0.0.1", 8040),
("http", "127.0.0.1", 8765),
("http", "localhost", 8000),
("http", "localhost", 8080),
("http", "localhost", 8081),
("http", "localhost", 8010),
("http", "localhost", 8020),
("http", "localhost", 8030),
("http", "localhost", 8040),
}
SERVICE_PLANS = {
"vllm": {
"service_id": "vllm-text",
"label": "vLLM text",
"base_url": "http://docker-gpu.cin.su:8000",
"container_name": "llm-vllm-text",
"compose": "core/deploy/docker-gpu/vllm/compose.yaml",
"deploy_script": "scripts/deploy_vllm.ps1",
"health_path": "/v1/models",
},
"llama.cpp": {
"service_id": "llama-gguf",
"label": "llama.cpp GGUF",
"base_url": "http://docker-gpu.cin.su:8080",
"container_name": "llm-llama-devstral-1c",
"compose": "core/deploy/docker-gpu/llama-cpp/compose.yaml",
"deploy_script": "scripts/deploy_llama_cpp.ps1",
"health_path": "/v1/models",
},
"transformers:translation": {
"service_id": "translation-api",
"label": "Transformers translation",
"base_url": "http://docker-gpu.cin.su:8010",
"container_name": "llm-transformers-translation",
"compose": "core/deploy/docker-gpu/transformers/translation.compose.yaml",
"deploy_script": "scripts/deploy_transformers_service.ps1 -Plugin translation",
"health_path": "/health",
},
"transformers:audio": {
"service_id": "audio-api",
"label": "Transformers audio",
"base_url": "http://docker-gpu.cin.su:8020",
"container_name": "llm-transformers-audio",
"compose": "core/deploy/docker-gpu/transformers/audio.compose.yaml",
"deploy_script": "scripts/deploy_transformers_service.ps1 -Plugin audio",
"health_path": "/health",
},
"transformers:video": {
"service_id": "video-api",
"label": "Transformers video",
"base_url": "http://docker-gpu.cin.su:8030",
"container_name": "llm-transformers-video",
"compose": "core/deploy/docker-gpu/transformers/video.compose.yaml",
"deploy_script": "scripts/deploy_transformers_service.ps1 -Plugin video",
"health_path": "/health",
},
"transformers:image": {
"service_id": "image-api",
"label": "Transformers image",
"base_url": "http://docker-gpu.cin.su:8040",
"container_name": "llm-transformers-image",
"compose": "core/deploy/docker-gpu/transformers/image.compose.yaml",
"deploy_script": "scripts/deploy_transformers_service.ps1 -Plugin image",
"health_path": "/health",
},
}
SERVICE_CONTROL_ACTIONS = {"start", "stop", "restart", "status"}
DEFAULT_DOCKER_HOST = os.environ.get("DOCKER_HOST", "ssh://docker-gpu")
TEST_PACKS = {
"text": [
{"id": "short", "label": "Краткий ответ", "prompt": DEFAULT_PROMPTS["text"]},
{"id": "json", "label": "JSON", "prompt": "Верни JSON с полями status, summary, next_steps. Без markdown."},
{"id": "format", "label": "Формат", "prompt": "Сделай список из 5 пунктов: критерии приемки локальной LLM."},
{"id": "critique", "label": "Критика", "prompt": "Назови 3 риска при проверке локальной модели и как их снизить."},
],
"translation": [
{"id": "ru-en", "label": "RU -> EN", "prompt": "Переведи на английский: Нужно проверить качество локальной модели перевода."},
{"id": "en-ru", "label": "EN -> RU", "prompt": "Translate into Russian: The model must preserve technical terms and JSON structure."},
{"id": "terms", "label": "Термины", "prompt": "Переведи на английский, сохрани термины: регистр сведений, справочник, документ реализации."},
{"id": "format", "label": "Сохранить JSON", "prompt": "Переведи значения JSON на английский, ключи не меняй: {\"status\":\"готово\",\"risk\":\"нет доступа к GPU\"}"},
],
"audio": [
{"id": "plan", "label": "План", "prompt": DEFAULT_PROMPTS["audio"]},
{"id": "metrics", "label": "Метрики", "prompt": "Какие метрики использовать для оценки speech-to-text на русском языке?"},
{"id": "noise", "label": "Шум", "prompt": "Составь сценарии проверки распознавания речи при шуме и разных микрофонах."},
{"id": "diarization", "label": "Диалоги", "prompt": "Как проверить качество распознавания диалога двух пользователей?"},
],
"video": [
{"id": "plan", "label": "План", "prompt": DEFAULT_PROMPTS["video"]},
{"id": "scene", "label": "Сцена", "prompt": "Какие вопросы задать модели, чтобы проверить понимание сцены на видео?"},
{"id": "events", "label": "События", "prompt": "Составь тесты для поиска событий во временной шкале видео."},
{"id": "docs", "label": "Документы", "prompt": "Как проверить vision-language модель на чтении документов и экранных форм?"},
],
"image": [
{"id": "photo", "label": "Фото", "prompt": DEFAULT_PROMPTS["image"]},
{"id": "product", "label": "Предмет", "prompt": "Предметное фото локального GPU-сервера на чистом столе, реалистичный свет, 1024x1024."},
{"id": "edit", "label": "Редактирование", "prompt": "Заменить фон на светлую офисную стену, сохранить предмет и естественные тени."},
{"id": "ui", "label": "Интерфейс", "prompt": "Скриншот современного локального кабинета моделей, аккуратная панель, реалистичная фотография монитора."},
],
"1c": [
{"id": "metadata", "label": "Метаданные", "prompt": DEFAULT_PROMPTS["1c"]},
{"id": "bsl", "label": "BSL", "prompt": "Найди возможную ошибку в BSL-коде и объясни безопасное исправление: Если Объект.Сумма = 0 Тогда Возврат; КонецЕсли;"},
{"id": "query", "label": "Запрос 1С", "prompt": "Составь read-only запрос 1С для получения 10 последних документов реализации."},
{"id": "safety", "label": "Безопасность", "prompt": "Почему модель не должна напрямую менять живую базу 1С? Дай workflow согласования."},
],
}
STATUS_RANK = {
"production": 0,
"staging": 1,
"candidate": 2,
"draft": 3,
"archived": 9,
}
ONE_C_DANGEROUS_REQUEST_PATTERNS = [
r"(?i)\b(delete|drop|update|insert|truncate|alter)\b",
r"(?i)\b(удалить|удали|удаление|изменить|измени|обновить|обнови)\b",
r"(?i)\b(записать|запиши|провести|проведи)\b",
r"(?i)\b(объект\.записать|провести\s*\(|удалить\s+из|изменить\s+.*\s+установить)\b",
]
ONE_C_UNSAFE_ANSWER_PATTERNS = [
r"(?i)checklist\s+для\s+удален",
r"(?i)план\s+.*удален",
r"(?i)безопасн\w*\s+удален",
r"(?i)уточните\s+услов",
r"(?i)ограничьте\s+удален",
r"(?i)если\s+вы\s+хотите\s+удал",
r"(?i)помог[уа]\s+.*удал",
r"(?i)удалитьизсправочника",
]
ONE_C_SAFE_MUTATION_REFUSAL = (
"Я не могу выполнить, составить или уточнять операцию изменения данных в 1С. "
"Могу помочь только read-only анализом: показать запрос `ВЫБРАТЬ` для инвентаризации "
"затрагиваемых записей, проверить зависимости по metadata snapshot и подготовить checklist "
"согласования для ответственного администратора."
)
def sanitize_filename(value: str, *, fallback: str = "model.bin") -> str:
name = Path(str(value or fallback).replace("\\", "/")).name.strip()
name = re.sub(r"[^A-Za-z0-9._+@=-]+", "_", name)
name = name.strip("._")
return name or fallback
def sanitize_id(value: str, *, fallback: str = "model") -> str:
text = str(value or fallback).strip().lower()
text = re.sub(r"[^a-z0-9._-]+", "-", text)
text = text.strip(".-_")
return text or fallback
def now_iso() -> str:
return dt.datetime.now(dt.UTC).isoformat()
def display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
def model_ingest_job_dir(job_id: str) -> Path:
return MODEL_INGEST_DIR / job_id
def latest_user_message(messages: list[dict]) -> str:
for message in reversed(messages):
if isinstance(message, dict) and message.get("role") == "user":
return str(message.get("content") or "")
return ""
def has_pattern(text: str, patterns: list[str]) -> bool:
return any(re.search(pattern, text or "") for pattern in patterns)
def guard_1c_answer(question: str, answer: str | None) -> tuple[str | None, dict | None]:
if not answer:
return answer, None
dangerous_request = has_pattern(question, ONE_C_DANGEROUS_REQUEST_PATTERNS)
unsafe_answer = has_pattern(answer, ONE_C_UNSAFE_ANSWER_PATTERNS)
if not dangerous_request or not unsafe_answer:
return answer, None
return ONE_C_SAFE_MUTATION_REFUSAL, {
"applied": True,
"reason": "dangerous_1c_mutation_answer",
}
def append_model_ingest_job(job: dict) -> None:
MODEL_INGEST_JOBS.parent.mkdir(parents=True, exist_ok=True)
with MODEL_INGEST_JOBS.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(job, ensure_ascii=False) + "\n")
def append_image_job(job: dict) -> None:
IMAGE_JOBS.parent.mkdir(parents=True, exist_ok=True)
with IMAGE_JOBS.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(job, ensure_ascii=False) + "\n")
def latest_image_jobs(limit: int = 30) -> list[dict]:
if not IMAGE_JOBS.exists():
return []
rows = []
with IMAGE_JOBS.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
latest: dict[str, dict] = {}
for row in rows:
job_id = str(row.get("id") or "")
if job_id:
latest[job_id] = row
collapsed = list(latest.values())
collapsed.sort(key=lambda item: str(item.get("updated_at") or item.get("created_at") or ""), reverse=True)
return collapsed[:limit]
def read_model_ingest_jobs(limit: int = 30) -> list[dict]:
if not MODEL_INGEST_JOBS.exists():
return []
rows = []
with MODEL_INGEST_JOBS.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
continue
return rows[-limit:][::-1]
def latest_model_ingest_jobs(limit: int = 100) -> list[dict]:
latest: dict[str, dict] = {}
for job in read_model_ingest_jobs(limit=limit * 5):
job_id = str(job.get("id") or "")
if job_id and job_id not in latest:
latest[job_id] = job
return list(latest.values())[:limit]
def decode_data_url_bytes(value: str, *, field_name: str = "data") -> bytes:
data = str(value or "")
if "," in data and data.split(",", 1)[0].startswith("data:"):
data = data.split(",", 1)[1]
if not data:
raise ValueError(f"{field_name} is required")
import base64
return base64.b64decode(data)
def save_image_artifact(*, report_id: str, image_base64: str, kind: str, metadata: dict) -> dict:
created = dt.datetime.now(dt.UTC)
day_dir = IMAGE_GALLERY_DIR / created.strftime("%Y%m%d")
day_dir.mkdir(parents=True, exist_ok=True)
safe_kind = sanitize_id(kind, fallback="image")
filename = f"{created.strftime('%H%M%S')}-{report_id}-{safe_kind}.png"
image_path = day_dir / filename
image_path.write_bytes(decode_data_url_bytes(image_base64, field_name="image_base64"))
sidecar_path = image_path.with_suffix(".json")
record = {
"id": report_id,
"created_at": created.isoformat(),
"kind": kind,
"path": display_path(image_path),
"url": f"/generated-images/{created.strftime('%Y%m%d')}/{filename}",
"size_bytes": image_path.stat().st_size,
**metadata,
}
sidecar_path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
return record
def latest_image_gallery(limit: int = 40) -> list[dict]:
if not IMAGE_GALLERY_DIR.exists():
return []
records = []
for sidecar in IMAGE_GALLERY_DIR.glob("*/*.json"):
try:
data = json.loads(sidecar.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if isinstance(data, dict) and data.get("url"):
records.append(data)
records.sort(key=lambda item: str(item.get("created_at") or ""), reverse=True)
return records[:limit]
def update_model_ingest_job(job: dict, *, status: str, error: str | None = None, **extra: object) -> dict:
updated = dict(job)
updated["status"] = status
updated["updated_at"] = now_iso()
if error is None:
updated.pop("error", None)
else:
updated["error"] = error
updated.update(extra)
write_model_ingest_metadata(model_ingest_job_dir(str(updated["id"])), updated)
append_model_ingest_job(updated)
return updated
def verify_model_ingest_job(job_id: str) -> dict:
job = next((item for item in latest_model_ingest_jobs() if str(item.get("id")) == job_id), None)
if not job:
raise ValueError("model ingest job not found")
files = job.get("files") or []
if not files:
return update_model_ingest_job(job, status="needs_file", error="job has no imported files")
checked_files = []
missing_files = []
total_size = 0
for file_info in files:
relative_path = str(file_info.get("path") or "")
path = (ROOT / relative_path).resolve()
if not path.exists() or not path.is_file():
missing_files.append(relative_path)
continue
size = path.stat().st_size
total_size += size
checked_files.append({"path": relative_path, "size_bytes": size})
if missing_files:
return update_model_ingest_job(
job,
status="missing_files",
error=f"missing files: {', '.join(missing_files)}",
checked_files=checked_files,
)
return update_model_ingest_job(
job,
status="verified",
checked_files=checked_files,
total_size_bytes=total_size,
)
def task_for_plugin(plugin: str) -> list[str]:
mapping = {
"text": ["text", "chat"],
"translation": ["translation"],
"audio": ["speech-to-text"],
"video": ["image-understanding", "visual-question-answering"],
"image": ["image-generation"],
"1c": ["1c", "1c-rag", "bsl-code"],
}
return mapping.get(plugin, [plugin])
def runtime_for_plugin_and_format(plugin: str, model_format: str) -> str:
if model_format.lower() == "gguf":
return "llama.cpp"
if plugin in {"translation", "audio", "video", "image"}:
return "transformers"
return "vllm"
def register_model_ingest_job(job_id: str) -> dict:
job = next((item for item in latest_model_ingest_jobs(limit=500) if str(item.get("id")) == job_id), None)
if not job:
raise ValueError("model ingest job not found")
if str(job.get("status")) != "verified":
job = verify_model_ingest_job(job_id)
if str(job.get("status")) != "verified":
raise ValueError(f"model ingest job is not verified: {job.get('status')}")
model_id = sanitize_id(job.get("model_id") or job_id)
plugin = sanitize_id(job.get("plugin") or "text")
model_format = str(job.get("format") or "").lower() or "custom"
card_path = ROOT / "registry" / "model-cards" / f"{model_id}.yaml"
if card_path.exists():
raise ValueError(f"model card already exists: {card_path.relative_to(ROOT)}")
files = job.get("checked_files") or job.get("files") or []
first_file = files[0] if files else {}
storage_path = str(job.get("target_dir") or "")
card = {
"id": model_id,
"name": str(job.get("name") or model_id),
"status": "draft",
"type": "imported-model",
"task": task_for_plugin(plugin),
"runtime": runtime_for_plugin_and_format(plugin, model_format),
"format": model_format,
"storage_path": storage_path,
"served_model_name": model_id,
"license": "unknown",
"source": {
"kind": job.get("source_kind"),
"source": job.get("source") or "",
"ingest_job_id": job_id,
},
"deployment": {
"runtime": runtime_for_plugin_and_format(plugin, model_format),
"notes": "Registered from model ingest. Review runtime and served_model_name before production use.",
},
}
if first_file.get("path"):
card["filename"] = Path(str(first_file["path"])).name
if job.get("quantization"):
card["quantization"] = job.get("quantization")
def yaml_scalar(value: object) -> str:
text = str(value)
if not text or any(char in text for char in ":#[]{}&,*!|>'\"%@`"):
return json.dumps(text, ensure_ascii=False)
return text
def write_yaml_lines(value: object, indent: int = 0) -> list[str]:
prefix = " " * indent
lines: list[str] = []
if isinstance(value, dict):
for key, item in value.items():
if isinstance(item, (dict, list)):
lines.append(f"{prefix}{key}:")
lines.extend(write_yaml_lines(item, indent + 2))
else:
lines.append(f"{prefix}{key}: {yaml_scalar(item)}")
elif isinstance(value, list):
for item in value:
if isinstance(item, (dict, list)):
lines.append(f"{prefix}-")
lines.extend(write_yaml_lines(item, indent + 2))
else:
lines.append(f"{prefix}- {yaml_scalar(item)}")
return lines
card_path.write_text("\n".join(write_yaml_lines(card)) + "\n", encoding="utf-8")
build = run_command([sys.executable, "scripts/build_model_index.py"], timeout=60)
status = "registered" if build["returncode"] == 0 else "registered_index_failed"
return update_model_ingest_job(
job,
status=status,
card_path=str(card_path.relative_to(ROOT)),
index_result=build,
)
def write_model_ingest_metadata(job_dir: Path, job: dict) -> None:
job_dir.mkdir(parents=True, exist_ok=True)
(job_dir / "metadata.json").write_text(json.dumps(job, ensure_ascii=False, indent=2), encoding="utf-8")
def create_model_ingest_job(payload: dict, *, source_kind: str) -> tuple[dict, Path]:
job_id = str(uuid.uuid4())
model_id = sanitize_id(payload.get("model_id") or payload.get("name") or "model")
plugin = sanitize_id(payload.get("plugin") or "text")
job_dir = model_ingest_job_dir(job_id)
job = {
"id": job_id,
"created_at": now_iso(),
"updated_at": now_iso(),
"status": "created",
"source_kind": source_kind,
"model_id": model_id,
"name": str(payload.get("name") or model_id),
"plugin": plugin,
"task": payload.get("task") or [plugin],
"format": payload.get("format") or "",
"quantization": payload.get("quantization") or "",
"notes": payload.get("notes") or "",
"target_dir": str(job_dir.relative_to(ROOT)),
"files": [],
}
return job, job_dir
def stream_request_to_file(handler: BaseHTTPRequestHandler, destination: Path, content_length: int) -> int:
destination.parent.mkdir(parents=True, exist_ok=True)
remaining = content_length
written = 0
with destination.open("wb") as handle:
while remaining > 0:
chunk = handler.rfile.read(min(1024 * 1024, remaining))
if not chunk:
break
handle.write(chunk)
written += len(chunk)
remaining -= len(chunk)
return written
def download_url_to_file(url: str, destination: Path) -> int:
destination.parent.mkdir(parents=True, exist_ok=True)
request = urllib.request.Request(url, headers={"User-Agent": "LLM-model-ingest/1.0"})
written = 0
with urllib.request.urlopen(request, timeout=60) as response, destination.open("wb") as handle:
while True:
chunk = response.read(1024 * 1024)
if not chunk:
break
handle.write(chunk)
written += len(chunk)
return written
def import_model_from_source(payload: dict) -> dict:
source = str(payload.get("source") or "").strip()
if not source:
raise ValueError("source is required")
job, job_dir = create_model_ingest_job(payload, source_kind="source")
parsed = urlparse(source)
source_name = sanitize_filename(payload.get("filename") or Path(parsed.path or source).name or job["model_id"])
destination = job_dir / source_name
job["source"] = source
try:
if parsed.scheme in {"http", "https"}:
bytes_written = download_url_to_file(source, destination)
job["status"] = "downloaded"
else:
source_path = Path(source)
if not source_path.exists():
job["status"] = "queued_manual_required"
job["error"] = "source path is not accessible from this server"
bytes_written = 0
else:
job_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_path, destination)
bytes_written = destination.stat().st_size
job["status"] = "imported"
if bytes_written:
job["files"].append({"path": str(destination.relative_to(ROOT)), "size_bytes": bytes_written})
except (OSError, urllib.error.URLError) as exc:
job["status"] = "failed"
job["error"] = str(exc)
job["updated_at"] = now_iso()
write_model_ingest_metadata(job_dir, job)
append_model_ingest_job(job)
return job
def model_plugins(model: dict) -> list[str]:
plugins = []
for task in model.get("task") or []:
plugin = TASK_PLUGIN_MAP.get(task)
if plugin and plugin not in plugins:
plugins.append(plugin)
return plugins or ["text"]
def plugin_model_score(model: dict, plugin: str) -> tuple[int, int, str]:
tasks = set(model.get("task") or model.get("tasks") or [])
status = str(model.get("status") or "")
model_id = str(model.get("id") or "")
runtime = str(model.get("runtime") or "")
model_type = str(model.get("type") or "")
quantization = str(model.get("quantization") or "")
score = 0
if plugin == "text":
if tasks & {"text", "chat"}:
score -= 5
if tasks & {"1c", "1c-rag", "bsl-code"}:
score += 6
elif plugin == "1c":
if tasks & {"1c", "1c-rag", "bsl-code", "1c-query"}:
score -= 6
if "qwen3-coder" in model_id:
score -= 8
if quantization == "Q6_K" or "q6" in model_id:
score -= 3
if runtime == "llama.cpp":
score -= 2
if model_type == "lora-adapter" or model_id.endswith("-lora-v1"):
score += 8
elif plugin in TASK_PLUGIN_MAP.values():
mapped_tasks = {task for task, mapped_plugin in TASK_PLUGIN_MAP.items() if mapped_plugin == plugin}
if tasks & mapped_tasks:
score -= 5
return (score, STATUS_RANK.get(status, 8), str(model.get("id") or ""))
def load_gpu_profiles() -> dict[str, dict]:
profiles = read_json(GPU_PROFILES_PATH)
if not isinstance(profiles, dict):
raise ValueError(f"{GPU_PROFILES_PATH} must contain a profile mapping")
normalized = {}
for profile_id, profile in profiles.items():
if not isinstance(profile, dict):
continue
item = dict(profile)
item["id"] = str(item.get("id") or profile_id)
item.setdefault("label", item["id"])
item.setdefault("command", f"powershell -ExecutionPolicy Bypass -File scripts\\switch_gpu_profile.ps1 -Profile {item['id']}")
item.setdefault("starts", [])
item.setdefault("stops", [])
item.setdefault("wait", [])
item.setdefault("notes", "")
normalized[str(profile_id)] = item
return normalized
def public_gpu_profiles() -> dict[str, dict]:
return {
profile_id: {
key: value
for key, value in profile.items()
if key in {"id", "label", "command", "starts", "stops", "notes"}
}
for profile_id, profile in load_gpu_profiles().items()
}
def load_runtime_profiles() -> dict:
if not RUNTIME_PROFILES_PATH.exists():
return {"default": "gpu-fast", "profiles": {}}
data = read_json(RUNTIME_PROFILES_PATH)
if not isinstance(data, dict):
raise ValueError(f"{RUNTIME_PROFILES_PATH} must contain an object")
profiles = data.get("profiles") or {}
if not isinstance(profiles, dict):
raise ValueError(f"{RUNTIME_PROFILES_PATH} profiles must contain a mapping")
normalized = {}
for profile_id, profile in profiles.items():
if not isinstance(profile, dict):
continue
item = dict(profile)
item["id"] = str(item.get("id") or profile_id)
item.setdefault("label", item["id"])
item.setdefault("host", "")
item.setdefault("docker_endpoint", "")
item.setdefault("role", "")
item.setdefault("notes", "")
item.setdefault("endpoints", {})
item.setdefault("model_overrides", {})
normalized[str(profile_id)] = item
default_id = str(data.get("default") or next(iter(normalized), ""))
return {"default": default_id, "profiles": normalized}
def public_runtime_profiles() -> dict:
data = load_runtime_profiles()
return {
"default": data["default"],
"profiles": {
profile_id: {
key: value
for key, value in profile.items()
if key in {"id", "label", "host", "docker_endpoint", "role", "notes", "endpoints", "model_overrides"}
}
for profile_id, profile in data["profiles"].items()
},
}
def load_catalog() -> dict:
registry = read_json(REGISTRY_INDEX)
models = []
plugins = {plugin: {"id": plugin, "label": PLUGIN_LABELS[plugin], "models": []} for plugin in PLUGIN_ORDER}
for model in registry.get("models") or []:
deployment_model = model.get("served_model_name") or model.get("id")
item = {
"id": model.get("id"),
"name": model.get("name"),
"type": model.get("type"),
"status": model.get("status"),
"runtime": model.get("runtime"),
"storage_path": model.get("storage_path"),
"format": model.get("format"),
"quantization": model.get("quantization"),
"served_model_name": deployment_model,
"tasks": model.get("task") or [],
"plugins": model_plugins(model),
"card_path": model.get("card_path"),
}
models.append(item)
for plugin in item["plugins"]:
plugins.setdefault(plugin, {"id": plugin, "label": plugin, "models": []})
plugins[plugin]["models"].append(item["id"])
by_id = {model["id"]: model for model in models}
for plugin_id, plugin in plugins.items():
plugin["models"].sort(key=lambda model_id: plugin_model_score(by_id[model_id], plugin_id))
ordered_plugins = [plugins[plugin] for plugin in PLUGIN_ORDER if plugin in plugins]
extras = [value for key, value in plugins.items() if key not in PLUGIN_ORDER]
return {
"default_base_url": DEFAULT_BASE_URL,
"endpoint_presets": ENDPOINT_PRESETS,
"plugins": ordered_plugins + extras,
"models": models,
"default_prompts": DEFAULT_PROMPTS,
"test_packs": TEST_PACKS,
"gpu_profiles": public_gpu_profiles(),
"runtime_profiles": public_runtime_profiles(),
"rag_profiles": {
"1c": list_rag_profiles(),
},
}
def local_model_card(model: dict) -> dict:
card_path = model.get("card_path")
if not card_path:
return {}
path = (ROOT / str(card_path)).resolve()
if not path.exists():
return {}
return read_yaml_mapping(path)
def has_any_file(path: Path, patterns: list[str]) -> bool:
return any(path.glob(pattern) for pattern in patterns)
def has_weight_file(path: Path) -> bool:
for file_path in path.iterdir() if path.exists() else []:
if file_path.is_file() and file_path.name.lower().endswith((".safetensors", ".bin", ".gguf")):
return True
return False
def storage_status_for_model(model: dict) -> dict:
card = local_model_card(model)
raw_storage_path = str(model.get("storage_path") or card.get("storage_path") or "")
storage_candidates = [localize_workspace_path(raw_storage_path)]
if raw_storage_path.startswith("/models/"):
raw_path = Path(raw_storage_path)
if Path("/models").exists():
storage_candidates.insert(0, raw_path)
else:
storage_candidates.append(raw_path)
storage_path = next((path for path in storage_candidates if path.exists()), storage_candidates[0])
result = {
"path": str(storage_path),
"exists": storage_path.exists(),
"status": "missing",
"reason": "storage path is missing",
}
if not storage_path.exists():
return result
model_format = str(card.get("format") or "").lower()
model_type = str(card.get("type") or "").lower()
if model_format == "diffusers" or model_type == "image-diffusion-model":
missing = [name for name in ["model_index.json"] if not (storage_path / name).exists()]
if missing:
result.update({"status": "missing", "reason": f"missing required file(s): {', '.join(missing)}"})
elif not any(storage_path.rglob("*.safetensors")) and not any(storage_path.rglob("*.bin")):
result.update({"status": "metadata-only", "reason": "diffusers weights are missing"})
else:
result.update({"status": "ok", "reason": "ready"})
return result
if model_format == "gguf":
filename = str(card.get("filename") or "")
if not filename:
result.update({"status": "failed", "reason": "GGUF filename is missing in model card"})
return result
file_path = storage_path / filename
if not file_path.exists():
result.update({"status": "missing", "reason": f"GGUF file is missing: {filename}"})
return result
size = file_path.stat().st_size
expected = card.get("file_size_bytes")
status = "ok" if not expected or size == int(expected) else "partial"
reason = "ready" if status == "ok" else f"size mismatch: {size} != {expected}"
result.update({"status": status, "reason": reason, "size_bytes": size, "expected_size_bytes": expected})
return result
if model_type == "lora-adapter":
ready = has_any_file(storage_path, ["adapter_config.json", "*.safetensors", "*.bin"])
result.update({"status": "ok" if ready else "metadata-only", "reason": "ready" if ready else "adapter artifact files are missing"})
return result
missing = [name for name in ["config.json"] if not (storage_path / name).exists()]
index_path = storage_path / "model.safetensors.index.json"
missing_shards: list[str] = []
if index_path.exists():
try:
index = json.loads(index_path.read_text(encoding="utf-8"))
shard_names = sorted(set((index.get("weight_map") or {}).values()))
missing_shards = [name for name in shard_names if not (storage_path / name).exists()]
except json.JSONDecodeError:
result.update({"status": "failed", "reason": "model.safetensors.index.json is invalid"})
return result
has_weights = has_weight_file(storage_path)
has_tokenizer = has_any_file(storage_path, ["tokenizer.json", "tokenizer.model", "vocab.json"])
if missing:
result.update({"status": "missing", "reason": f"missing required file(s): {', '.join(missing)}"})
elif missing_shards:
preview = ", ".join(missing_shards[:4])
suffix = f" and {len(missing_shards) - 4} more" if len(missing_shards) > 4 else ""
result.update({"status": "partial", "reason": f"missing shard file(s): {preview}{suffix}"})
elif not has_weights:
result.update({"status": "metadata-only", "reason": "model weights are missing"})
elif not has_tokenizer:
result.update({"status": "partial", "reason": "tokenizer files are missing"})
else:
result.update({"status": "ok", "reason": "ready"})
return result
def service_plan_for_model(model: dict) -> dict:
runtime = str(model.get("runtime") or "")
plugins = model.get("plugins") or []
key = runtime
if runtime == "transformers":
for plugin in ("translation", "audio", "video", "image"):
if plugin in plugins:
key = f"transformers:{plugin}"
break
plan = dict(SERVICE_PLANS.get(key) or {})
if not plan:
plan = {
"service_id": runtime or "unknown",
"label": runtime or "unknown runtime",
"base_url": DEFAULT_BASE_URL,
"container_name": None,
"compose": None,
"deploy_script": None,
"health_path": "/health",
}
return plan
def service_status_for_model(model: dict, endpoint_models: dict[str, list[str]]) -> dict:
plan = service_plan_for_model(model)
storage = storage_status_for_model(model)
served_name = str(model.get("served_model_name") or model.get("id") or "")
base_url = str(plan.get("base_url") or DEFAULT_BASE_URL)
models = endpoint_models.get(base_url, [])
online = served_name in models
status = "online" if online else "ready_to_start" if storage["status"] == "ok" else "blocked"
return {
"status": status,
"online": online,
"storage": storage,
"service": plan,
"served_model_name": served_name,
"endpoint_models": models,
}
def service_health_for_model(model: dict, endpoint_health: dict[str, dict]) -> dict:
plan = service_plan_for_model(model)
base_url = str(plan.get("base_url") or DEFAULT_BASE_URL)
return endpoint_health.get(base_url, {})
def model_readiness(model: dict, endpoint_models: dict[str, list[str]]) -> dict:
served_name = str(model.get("served_model_name") or model.get("id") or "")
plan = service_plan_for_model(model)
base_url = str(plan.get("base_url") or DEFAULT_BASE_URL)
available = served_name in endpoint_models.get(base_url, [])
return {"base_url": base_url, "served_model_name": served_name, "available": available, "service": plan}
def route_for_plugin(plugin: str, task: str | None = None) -> dict:
catalog = load_catalog()
models = [model for model in catalog["models"] if plugin in model.get("plugins", [])]
if task:
task_matches = [model for model in models if task in model.get("tasks", [])]
if task_matches:
models = task_matches
models.sort(key=lambda model: plugin_model_score(model, plugin))
selected = models[0] if models else None
if not selected:
raise ValueError(f"no model route for plugin: {plugin}")
endpoint = model_readiness(selected, {})
try:
runtime_data = load_runtime_profiles()
default_profile = runtime_data["profiles"].get(runtime_data["default"])
if default_profile:
target = runtime_profile_target(default_profile, model=selected, model_id=str(selected["id"]), plugin=plugin)
endpoint = {
**endpoint,
"base_url": target["base_url"],
"served_model_name": target["served_model_name"],
}
except Exception:
pass
return {
"plugin": plugin,
"task": task,
"model": selected,
"base_url": endpoint["base_url"],
"served_model_name": endpoint["served_model_name"],
"service": endpoint["service"],
"reason": f"Selected by plugin={plugin}, status={selected.get('status')}, runtime={selected.get('runtime')}",
}
def route_for_model(plugin: str, model_id: str, task: str | None = None) -> dict:
if not model_id:
return route_for_plugin(plugin, task)
catalog = load_catalog()
selected = next(
(
model
for model in catalog["models"]
if model.get("id") == model_id and plugin in model.get("plugins", [])
),
None,
)
if not selected:
return route_for_plugin(plugin, task)
endpoint = model_readiness(selected, {})
try:
runtime_data = load_runtime_profiles()
default_profile = runtime_data["profiles"].get(runtime_data["default"])
if default_profile:
target = runtime_profile_target(default_profile, model=selected, model_id=str(selected["id"]), plugin=plugin)
endpoint = {
**endpoint,
"base_url": target["base_url"],
"served_model_name": target["served_model_name"],
}
except Exception:
pass
return {
"plugin": plugin,
"task": task,
"model": selected,
"base_url": endpoint["base_url"],
"served_model_name": endpoint["served_model_name"],
"service": endpoint["service"],
"reason": f"Selected by model_id={model_id}, status={selected.get('status')}, runtime={selected.get('runtime')}",
}
def run_command(command: list[str], *, timeout: int = 60) -> dict:
started_at = time.perf_counter()
try:
result = subprocess.run(
command,
cwd=ROOT,
text=True,
capture_output=True,
timeout=timeout,
check=False,
)
return {
"command": command,
"returncode": result.returncode,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
except FileNotFoundError as exc:
return {
"command": command,
"returncode": 127,
"stdout": "",
"stderr": str(exc),
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
except subprocess.TimeoutExpired as exc:
return {
"command": command,
"returncode": 124,
"stdout": (exc.stdout or "").strip() if isinstance(exc.stdout, str) else "",
"stderr": (exc.stderr or "").strip() if isinstance(exc.stderr, str) else f"timeout after {timeout}s",
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
def docker_compose_base_args(plan: dict, *, docker_host: str | None = None) -> list[str]:
compose = plan.get("compose")
if not compose:
raise ValueError("service has no compose file")
compose_path = ROOT / str(compose)
if not compose_path.exists():
raise ValueError(f"compose file is missing: {compose}")
args = ["docker"]
host = docker_host or DEFAULT_DOCKER_HOST
if host:
args.extend(["--host", host])
return [*args, "compose", "-f", str(compose_path)]
def control_model_service(payload: dict) -> dict:
action = str(payload.get("action") or "").strip().lower()
model_id = str(payload.get("model_id") or "").strip()
if action not in SERVICE_CONTROL_ACTIONS:
raise ValueError(f"action must be one of: {', '.join(sorted(SERVICE_CONTROL_ACTIONS))}")
if not model_id:
raise ValueError("model_id is required")
catalog = load_catalog()
model = next((item for item in catalog["models"] if item.get("id") == model_id), None)
if not model:
raise ValueError(f"unknown model_id: {model_id}")
plan = service_plan_for_model(model)
docker_args = ["docker"]
docker_host = payload.get("docker_host") or DEFAULT_DOCKER_HOST
if docker_host:
docker_args.extend(["--host", str(docker_host)])
if action == "start":
container_name = str(plan.get("container_name") or "")
if container_name:
existing = run_command(
[*docker_args, "ps", "-a", "--filter", f"name=^{container_name}$", "--format", "{{.Names}}"],
timeout=30,
)
if existing["returncode"] == 0 and container_name in existing["stdout"].splitlines():
command = [*docker_args, "start", container_name]
else:
base_args = docker_compose_base_args(plan, docker_host=str(docker_host) if docker_host else None)
command = [*base_args, "up", "-d"]
else:
base_args = docker_compose_base_args(plan, docker_host=str(docker_host) if docker_host else None)
command = [*base_args, "up", "-d"]
timeout = 120
elif action == "stop":
container_name = str(plan.get("container_name") or "")
if not container_name:
raise ValueError("service has no container_name")
command = [*docker_args, "stop", container_name]
timeout = 90
elif action == "restart":
container_name = str(plan.get("container_name") or "")
if not container_name:
raise ValueError("service has no container_name")
command = [*docker_args, "restart", container_name]
timeout = 120
else:
container_name = str(plan.get("container_name") or "")
if container_name:
command = [*docker_args, "ps", "-a", "--filter", f"name=^{container_name}$"]
else:
base_args = docker_compose_base_args(plan, docker_host=str(docker_host) if docker_host else None)
command = [*base_args, "ps"]
timeout = 60
result = run_command(command, timeout=timeout)
report_id = append_report(
{
"type": "service_control",
"model_id": model_id,
"model": model.get("name") or model_id,
"action": action,
"service": plan,
"result": result,
}
)
return {
"ok": result["returncode"] == 0,
"report_id": report_id,
"model_id": model_id,
"action": action,
"service": plan,
"result": result,
}
def run_runtime_benchmark(payload: dict) -> dict:
model_id = sanitize_id(str(payload.get("model_id") or "qwen3-coder-30b-a3b-instruct-q6_k"))
plugin = sanitize_id(str(payload.get("plugin") or "1c"))
profiles = payload.get("profiles") or ["gpu-fast", "cpu-test"]
if not isinstance(profiles, list) or not profiles:
raise ValueError("profiles must be a non-empty list")
safe_profiles = [sanitize_id(str(profile), fallback="profile") for profile in profiles]
max_tokens = int(payload.get("max_tokens") or 384)
temperature = float(payload.get("temperature") or 0.1)
prompt = str(payload.get("prompt") or DEFAULT_PROMPTS.get(plugin) or DEFAULT_PROMPTS["1c"]).strip()
if not prompt:
raise ValueError("prompt is required")
timestamp = dt.datetime.now(dt.UTC).strftime("%Y%m%dT%H%M%SZ")
report_path = REPORT_ROOT / "benchmarks" / f"runtime-profiles-{model_id}-{timestamp}.json"
command = [
sys.executable,
str(ROOT / "scripts" / "benchmark_runtime_profiles.py"),
"--model-id",
model_id,
"--plugin",
plugin,
"--max-tokens",
str(max_tokens),
"--temperature",
str(temperature),
"--timeout",
str(int(payload.get("timeout") or 600)),
"--report",
str(report_path),
"--profiles",
*safe_profiles,
]
if prompt:
command.extend(["--prompt", prompt])
result = run_command(command, timeout=int(payload.get("command_timeout") or 900))
report = {}
if report_path.exists():
try:
report = read_json(report_path)
except (OSError, json.JSONDecodeError, ValueError) as exc:
report = {"status": "error", "error": f"failed to read report: {exc}"}
report_id = append_report(
{
"type": "runtime_benchmark",
"model_id": model_id,
"plugin": plugin,
"profiles": safe_profiles,
"report_path": display_path(report_path),
"result": result,
"report": report,
}
)
return {
"ok": result["returncode"] == 0 and bool(report),
"report_id": report_id,
"report_path": display_path(report_path),
"result": result,
"benchmark": report,
}
def runtime_profile_target(profile: dict, *, model: dict, model_id: str, plugin: str) -> dict:
override = (profile.get("model_overrides") or {}).get(model_id) or {}
base_url = override.get("base_url") or (profile.get("endpoints") or {}).get(plugin)
served_model_name = override.get("served_model_name") or model.get("served_model_name") or model_id
if not base_url:
raise ValueError(f"profile `{profile.get('id')}` has no endpoint for plugin `{plugin}`")
return {
"base_url": normalize_base_url(str(base_url)),
"served_model_name": str(served_model_name),
"host": profile.get("host"),
"role": profile.get("role"),
"container_name": override.get("container_name"),
}
def runtime_benchmark_preflight(payload: dict) -> dict:
model_id = sanitize_id(str(payload.get("model_id") or "qwen3-coder-30b-a3b-instruct-q6_k"))
plugin = sanitize_id(str(payload.get("plugin") or "1c"))
profiles = payload.get("profiles") or ["gpu-fast", "cpu-test"]
if not isinstance(profiles, list) or not profiles:
raise ValueError("profiles must be a non-empty list")
catalog = load_catalog()
model = next((item for item in catalog["models"] if item.get("id") == model_id), None)
if not model:
raise ValueError(f"unknown model_id: {model_id}")
runtime_profiles = load_runtime_profiles()["profiles"]
checks = []
started_at = time.perf_counter()
for profile_id_raw in profiles:
profile_id = sanitize_id(str(profile_id_raw), fallback="profile")
profile = runtime_profiles.get(profile_id)
if not profile:
checks.append({"profile_id": profile_id, "status": "error", "error": "unknown runtime profile"})
continue
try:
target = runtime_profile_target(profile, model=model, model_id=model_id, plugin=plugin)
checked_at = time.perf_counter()
models = fetch_endpoint_models(target["base_url"], timeout=int(payload.get("timeout") or 20))
latency_ms = round((time.perf_counter() - checked_at) * 1000)
available = target["served_model_name"] in models
checks.append(
{
"profile_id": profile_id,
"label": profile.get("label") or profile_id,
"status": "ok" if available else "missing_model",
"available": available,
"latency_ms": latency_ms,
"target": target,
"models": models,
}
)
except (ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
checks.append(
{
"profile_id": profile_id,
"label": profile.get("label") or profile_id,
"status": "error",
"available": False,
"error": str(exc),
}
)
ready = bool(checks) and all(item.get("available") for item in checks)
return {
"status": "ready" if ready else "blocked",
"ready": ready,
"model_id": model_id,
"plugin": plugin,
"latency_ms": round((time.perf_counter() - started_at) * 1000),
"checks": checks,
}
def latest_runtime_benchmarks(limit: int = 10, *, model_id: str | None = None, plugin: str | None = None) -> list[dict]:
benchmark_dir = REPORT_ROOT / "benchmarks"
if not benchmark_dir.exists():
return []
records = []
for path in sorted(benchmark_dir.glob("runtime-profiles-*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
try:
report = read_json(path)
except (OSError, json.JSONDecodeError, ValueError):
continue
if not isinstance(report, dict):
continue
if model_id and report.get("model_id") != model_id:
continue
if plugin and report.get("plugin") != plugin:
continue
summary_results = []
for row in report.get("results") or []:
if not isinstance(row, dict):
continue
target = row.get("target") or {}
summary_results.append(
{
"profile_id": row.get("profile_id"),
"status": row.get("status"),
"elapsed_sec": row.get("elapsed_sec"),
"output_tokens_per_sec": row.get("output_tokens_per_sec"),
"base_url": target.get("base_url"),
"served_model_name": target.get("served_model_name"),
"error": row.get("error"),
}
)
records.append(
{
"path": display_path(path),
"updated_at": dt.datetime.fromtimestamp(path.stat().st_mtime, dt.UTC).isoformat(),
"created_at": report.get("created_at"),
"status": report.get("status"),
"model_id": report.get("model_id"),
"plugin": report.get("plugin"),
"fastest_profile_id": report.get("fastest_profile_id"),
"speedup": report.get("speedup") or {},
"results": summary_results,
}
)
if len(records) >= limit:
break
return records
def benchmark_report_path(name: str) -> Path:
filename = Path(urllib.parse.unquote(name or "")).name
if not re.fullmatch(r"runtime-profiles-[A-Za-z0-9_.-]+\.json", filename):
raise ValueError("invalid benchmark report name")
path = (REPORT_ROOT / "benchmarks" / filename).resolve()
benchmark_root = (REPORT_ROOT / "benchmarks").resolve()
if benchmark_root not in path.parents or not path.exists():
raise FileNotFoundError(filename)
return path
def render_benchmark_markdown(report: dict, source_name: str) -> str:
title = report.get("model_id") or source_name
created = report.get("created_at") or "-"
plugin = report.get("plugin") or "-"
max_tokens = report.get("max_tokens") or "-"
fastest = report.get("fastest_profile_id") or "-"
ratio = (report.get("speedup") or {}).get("gpu_vs_cpu_ratio")
lines = [
f"# Runtime Benchmark: {title}",
"",
f"- Created: `{created}`",
f"- Plugin: `{plugin}`",
f"- Max tokens: `{max_tokens}`",
f"- Fastest profile: `{fastest}`",
]
if ratio:
lines.append(f"- GPU/CPU ratio: `x{ratio}`")
lines.extend(
[
"",
"| Profile | Host | Endpoint | Served model | Elapsed | Output speed | Result |",
"|---|---|---|---|---:|---:|---|",
]
)
for row in report.get("results") or []:
target = row.get("target") or {}
status = row.get("status") or "-"
elapsed = f"{row.get('elapsed_sec')}s" if row.get("elapsed_sec") is not None else "-"
speed = f"{row.get('output_tokens_per_sec')} tok/s" if row.get("output_tokens_per_sec") is not None else "-"
result = "fastest" if row.get("profile_id") == fastest else status
if status != "ok" and row.get("error"):
result = f"error: {row.get('error')}"
lines.append(
"| "
+ " | ".join(
[
f"`{row.get('profile_id') or '-'}`",
f"`{target.get('host') or '-'}`",
f"`{target.get('base_url') or '-'}`",
f"`{target.get('served_model_name') or '-'}`",
elapsed,
speed,
result,
]
)
+ " |"
)
lines.extend(["", "Raw JSON:", "", f"`{source_name}`", ""])
return "\n".join(lines)
def gpu_status() -> dict:
query = "index,name,memory.used,memory.total,utilization.gpu"
result = run_command(
["nvidia-smi", f"--query-gpu={query}", "--format=csv,noheader,nounits"],
timeout=5,
)
gpus = []
if result["returncode"] == 0:
for line in result["stdout"].splitlines():
parts = [part.strip() for part in line.split(",")]
if len(parts) != 5:
continue
try:
used = int(parts[2])
total = int(parts[3])
utilization = int(parts[4])
except ValueError:
continue
gpus.append(
{
"index": parts[0],
"name": parts[1],
"memory_used_mib": used,
"memory_total_mib": total,
"memory_free_mib": total - used,
"memory_used_percent": round((used / total) * 100, 1) if total else None,
"utilization_gpu_percent": utilization,
}
)
summary = "unavailable"
if gpus:
summary = " · ".join(
f"GPU{gpu['index']} {gpu['memory_free_mib']}/{gpu['memory_total_mib']} MiB free, {gpu['utilization_gpu_percent']}%"
for gpu in gpus
)
return {"available": True, "gpus": gpus, "summary": summary, "probe": result}
for base_url in ("http://docker-gpu.cin.su:8040", "http://docker-gpu.cin.su:8020", "http://docker-gpu.cin.su:8010"):
try:
request = urllib.request.Request(f"{base_url}/health", method="GET")
with urllib.request.urlopen(request, timeout=3) as response:
data = json.loads(response.read().decode("utf-8"))
gpu = data.get("gpu") if isinstance(data, dict) else None
if isinstance(gpu, dict) and gpu.get("available"):
return {
"available": True,
"gpus": gpu.get("devices") or [],
"summary": gpu.get("summary") or "cuda available",
"source": f"{base_url}/health",
"probe": result,
}
except Exception:
continue
return {"available": False, "gpus": [], "summary": summary, "probe": result}
def service_control_status() -> dict:
docker_path = shutil.which("docker")
return {
"available": bool(docker_path),
"docker_path": docker_path,
"docker_host": DEFAULT_DOCKER_HOST,
"actions": sorted(SERVICE_CONTROL_ACTIONS),
"fallback": "scripts/manage_model_service.ps1",
}
def gpu_profile_status(endpoint_statuses: list[dict]) -> dict[str, dict]:
endpoint_by_id = {str(item.get("id")): item for item in endpoint_statuses}
endpoint_by_id["model-chat-ui"] = {"id": "model-chat-ui", "status": "ok"}
profiles = {}
for profile_id, profile in load_gpu_profiles().items():
starts = list(profile.get("starts") or [])
stops = list(profile.get("stops") or [])
missing = [service for service in starts if endpoint_by_id.get(service, {}).get("status") != "ok"]
conflicts = [service for service in stops if endpoint_by_id.get(service, {}).get("status") == "ok"]
profiles[profile_id] = {
"id": profile_id,
"ready": not missing and not conflicts,
"missing": missing,
"conflicts": conflicts,
"starts": starts,
"stops": stops,
}
return profiles
def latest_model_chat_preflight() -> dict:
if not MODEL_CHAT_PREFLIGHT_REPORT.exists():
return {"status": "missing", "report_path": display_path(MODEL_CHAT_PREFLIGHT_REPORT)}
try:
report = read_json(MODEL_CHAT_PREFLIGHT_REPORT)
except (OSError, ValueError, json.JSONDecodeError) as exc:
return {
"status": "failed",
"report_path": display_path(MODEL_CHAT_PREFLIGHT_REPORT),
"error": str(exc),
}
steps = report.get("steps") if isinstance(report.get("steps"), list) else []
failed_steps = [
str(step.get("name") or "unknown")
for step in steps
if isinstance(step, dict) and step.get("status") != "ok"
]
return {
"status": str(report.get("status") or "unknown"),
"created_at": report.get("created_at"),
"report_path": display_path(MODEL_CHAT_PREFLIGHT_REPORT),
"step_count": len(steps),
"failed_steps": failed_steps,
}
def health_status() -> dict:
endpoint_statuses = []
endpoint_models: dict[str, list[str]] = {}
endpoint_health: dict[str, dict] = {}
health_detail_urls = {
str(plan.get("base_url") or "")
for plan in SERVICE_PLANS.values()
if plan.get("health_path") == "/health"
}
for preset in ENDPOINT_PRESETS:
base_url = preset["base_url"]
started_at = time.perf_counter()
try:
models = fetch_endpoint_models(base_url, timeout=3)
endpoint_models[base_url] = models
detail = fetch_endpoint_health(base_url, timeout=2) if base_url in health_detail_urls else {}
if detail:
endpoint_health[base_url] = detail
endpoint_statuses.append(
{
"id": preset["id"],
"label": preset["label"],
"base_url": base_url,
"status": "ok",
"models": models,
"health": detail,
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
)
except Exception as exc: # noqa: BLE001 - health endpoint must report all failures.
endpoint_statuses.append(
{
"id": preset["id"],
"label": preset["label"],
"base_url": base_url,
"status": "error",
"error": str(exc),
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
)
catalog = load_catalog()
routes = []
for plugin in PLUGIN_ORDER:
try:
route = route_for_plugin(plugin)
route["readiness"] = service_status_for_model(route["model"], endpoint_models)
route["service_health"] = service_health_for_model(route["model"], endpoint_health)
route["available"] = route["readiness"]["online"]
routes.append(route)
except ValueError as exc:
routes.append({"plugin": plugin, "status": "error", "error": str(exc)})
model_services = []
for model in catalog["models"]:
model_services.append(
{
"id": model["id"],
"name": model["name"],
"plugins": model.get("plugins") or [],
"runtime": model.get("runtime"),
"served_model_name": model.get("served_model_name"),
"service_health": service_health_for_model(model, endpoint_health),
**service_status_for_model(model, endpoint_models),
}
)
return {
"status": "ok" if any(item["status"] == "ok" for item in endpoint_statuses) else "degraded",
"host": socket.gethostname(),
"time": now_iso(),
"registry_models": len(catalog["models"]),
"plugins": len(catalog["plugins"]),
"rag_1c_index": {
"path": str(DEFAULT_1C_RAG_INDEX.relative_to(ROOT)),
"exists": DEFAULT_1C_RAG_INDEX.exists(),
},
"model_ingest_jobs": latest_model_ingest_jobs(limit=10),
"gpu": gpu_status(),
"service_control": service_control_status(),
"model_chat_preflight": latest_model_chat_preflight(),
"endpoints": endpoint_statuses,
"gpu_profile_status": gpu_profile_status(endpoint_statuses),
"routes": routes,
"model_services": model_services,
}
def append_report(record: dict) -> str:
report_id = record.get("id") or str(uuid.uuid4())
record["id"] = report_id
record.setdefault("created_at", dt.datetime.now(dt.UTC).isoformat())
REPORT_DIR.mkdir(parents=True, exist_ok=True)
path = REPORT_DIR / f"{dt.datetime.now().strftime('%Y%m%d')}.jsonl"
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
return report_id
def fetch_endpoint_models(base_url: str, timeout: int = 20) -> list[str]:
request = urllib.request.Request(f"{base_url.rstrip('/')}/v1/models", method="GET")
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
model_rows = data.get("data") if isinstance(data, dict) else []
if not isinstance(model_rows, list):
return []
return [str(row.get("id")) for row in model_rows if isinstance(row, dict) and row.get("id")]
def fetch_endpoint_health(base_url: str, timeout: int = 3) -> dict:
request = urllib.request.Request(f"{base_url.rstrip('/')}/health", method="GET")
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
except Exception as exc: # noqa: BLE001 - health details are optional.
return {"status": "error", "error": str(exc)}
return data if isinstance(data, dict) else {}
def allowed_endpoint_hosts() -> set[tuple[str, str, int]]:
allowed = set(ALLOWED_ENDPOINT_HOSTS)
try:
runtime_profiles = load_runtime_profiles()["profiles"]
except Exception:
runtime_profiles = {}
for profile in runtime_profiles.values():
urls = list((profile.get("endpoints") or {}).values())
for override in (profile.get("model_overrides") or {}).values():
if isinstance(override, dict) and override.get("base_url"):
urls.append(str(override["base_url"]))
for url in urls:
parsed = urlparse(str(url).strip())
if parsed.scheme in {"http", "https"} and parsed.hostname:
port = parsed.port or (443 if parsed.scheme == "https" else 80)
allowed.add((parsed.scheme, parsed.hostname.lower(), port))
return allowed
def normalize_base_url(base_url: str) -> str:
parsed = urlparse(str(base_url).strip())
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError("base_url must be an http(s) URL")
port = parsed.port or (443 if parsed.scheme == "https" else 80)
endpoint = (parsed.scheme, parsed.hostname.lower(), port)
allowed_hosts = allowed_endpoint_hosts()
if endpoint not in allowed_hosts:
allowed = ", ".join(f"{scheme}://{host}:{port}" for scheme, host, port in sorted(allowed_hosts))
raise ValueError(f"base_url is not allowed. Allowed endpoints: {allowed}")
netloc = parsed.hostname
if parsed.port:
netloc = f"{netloc}:{parsed.port}"
return urlunparse((parsed.scheme, netloc, "", "", "", ""))
def build_1c_rag_prompt(question: str, *, profile_name: str = "general", limit: int | None = None) -> dict:
if not DEFAULT_1C_RAG_INDEX.exists():
raise FileNotFoundError(
f"1C RAG index is missing: {DEFAULT_1C_RAG_INDEX}. "
"Run scripts/prepare_1c_rag_corpus.py and scripts/build_1c_rag_index.py first."
)
index = read_json(DEFAULT_1C_RAG_INDEX)
profile = resolve_rag_profile(profile_name, question)
results = search_lexical_index(
index,
question,
limit=int(limit or profile["limit"]),
candidate_limit=int(profile["candidate_limit"]),
dedupe_by_document=bool(profile["dedupe_by_document"]),
min_score=float(profile["min_score"]),
source_types=profile["source_types"],
)
context = format_context(results, max_chars=int(profile["max_context_chars"]))
prompt = render_prompt(DEFAULT_1C_RAG_PROMPT, context=context, question=question)
return {
"prompt": prompt,
"profile": profile["id"],
"context_count": len(results),
"sources": [
{
"source_path": result["document"].get("source_path"),
"title": result["document"].get("title"),
"chunk_index": result["document"].get("chunk_index"),
"score": round(float(result.get("score") or 0), 4),
}
for result in results
],
}
def run_chat_request(base_url: str, model: str, messages: list[dict], temperature: float, max_tokens: int) -> dict:
started_at = time.perf_counter()
try:
answer = call_chat_completion(
base_url=base_url,
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
timeout=180,
)
return {
"status": "answered",
"answer": answer,
"error": None,
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
except (urllib.error.URLError, ValueError) as exc:
return {
"status": "error",
"answer": None,
"error": str(exc),
"latency_ms": round((time.perf_counter() - started_at) * 1000),
}
def call_audio_transcription(
*,
base_url: str,
audio_base64: str,
filename: str,
language: str | None = None,
task: str | None = None,
timeout: int = 300,
) -> dict:
url = f"{base_url.rstrip('/')}/v1/audio/transcriptions"
payload = {
"audio_base64": audio_base64,
"filename": filename,
}
if language:
payload["language"] = language
if task:
payload["task"] = task
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def call_vision_analysis(
*,
base_url: str,
image_base64: str,
filename: str,
prompt: str,
model: str | None = None,
max_tokens: int = 512,
timeout: int = 300,
) -> dict:
url = f"{base_url.rstrip('/')}/v1/vision/analyze"
payload = {
"image_base64": image_base64,
"filename": filename,
"prompt": prompt,
"model": model,
"max_tokens": max_tokens,
}
request = urllib.request.Request(
url,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("vision response must be a JSON object")
if data.get("error"):
error = data["error"]
if isinstance(error, dict):
raise ValueError(str(error.get("message") or error))
raise ValueError(str(error))
return data
def call_image_generation(
*,
base_url: str,
prompt: str,
negative_prompt: str | None = None,
width: int = 1024,
height: int = 1024,
steps: int = 28,
guidance_scale: float = 6.0,
seed: int | None = None,
timeout: int = 900,
) -> dict:
url = f"{base_url.rstrip('/')}/v1/images/generations"
payload: dict[str, object] = {
"prompt": prompt,
"negative_prompt": negative_prompt or "",
"width": width,
"height": height,
"steps": steps,
"guidance_scale": guidance_scale,
}
if seed is not None:
payload["seed"] = seed
request = urllib.request.Request(
url,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("image generation response must be a JSON object")
if data.get("error"):
error = data["error"]
if isinstance(error, dict):
raise ValueError(str(error.get("message") or error))
raise ValueError(str(error))
return data
def call_image_edit(
*,
base_url: str,
prompt: str,
image_base64: str,
mask_base64: str,
negative_prompt: str | None = None,
width: int = 1024,
height: int = 1024,
steps: int = 28,
guidance_scale: float = 6.0,
strength: float = 0.95,
seed: int | None = None,
timeout: int = 900,
) -> dict:
url = f"{base_url.rstrip('/')}/v1/images/edits"
payload: dict[str, object] = {
"prompt": prompt,
"negative_prompt": negative_prompt or "",
"image_base64": image_base64,
"mask_base64": mask_base64,
"width": width,
"height": height,
"steps": steps,
"guidance_scale": guidance_scale,
"strength": strength,
}
if seed is not None:
payload["seed"] = seed
request = urllib.request.Request(
url,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("image edit response must be a JSON object")
if data.get("error"):
error = data["error"]
if isinstance(error, dict):
raise ValueError(str(error.get("message") or error))
raise ValueError(str(error))
return data
def submit_image_job(*, base_url: str, operation: str, payload: dict, job_id: str, timeout: int = 30) -> dict:
url = f"{base_url.rstrip('/')}/v1/images/jobs"
request_payload = {"operation": operation, "payload": payload, "job_id": job_id}
request = urllib.request.Request(
url,
data=json.dumps(request_payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("image job response must be a JSON object")
if data.get("error"):
error = data["error"]
if isinstance(error, dict):
raise ValueError(str(error.get("message") or error))
raise ValueError(str(error))
return data
def fetch_image_job(*, base_url: str, job_id: str, timeout: int = 30) -> dict:
url = f"{base_url.rstrip('/')}/v1/images/jobs/{urllib.parse.quote(job_id)}"
with urllib.request.urlopen(url, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("image job status must be a JSON object")
if data.get("error"):
error = data["error"]
if isinstance(error, dict):
raise ValueError(str(error.get("message") or error))
raise ValueError(str(error))
return data
def cancel_image_job(*, base_url: str, job_id: str, timeout: int = 30) -> dict:
url = f"{base_url.rstrip('/')}/v1/images/jobs/{urllib.parse.quote(job_id)}/cancel"
request = urllib.request.Request(
url,
data=b"{}",
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("image cancel response must be a JSON object")
return data
def finalize_image_job(job: dict) -> dict:
job_id = str(job.get("id") or "")
if not job_id or job.get("status") != "completed":
return job
with IMAGE_PROXY_JOB_LOCK:
proxy = IMAGE_PROXY_JOBS.get(job_id)
if not proxy or proxy.get("artifact"):
if proxy and proxy.get("artifact"):
job["artifact"] = proxy["artifact"]
return job
result = job.get("result")
if not isinstance(result, dict):
return job
operation = str(proxy.get("operation") or job.get("operation") or "generate")
payload = dict(proxy.get("payload") or {})
artifact = save_image_artifact(
report_id=job_id,
image_base64=str(result.get("image_base64") or ""),
kind=operation,
metadata={
"prompt": payload.get("prompt") or "",
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"steps": payload.get("steps"),
"guidance_scale": payload.get("guidance_scale"),
"strength": payload.get("strength"),
"base_url": proxy.get("base_url"),
"model": result.get("model") or payload.get("model"),
},
)
latency_ms = job.get("latency_ms") or result.get("latency_ms")
append_report(
{
"id": job_id,
"type": "image_edit" if operation == "edit" else "image_generation",
"plugin": "image",
"prompt": payload.get("prompt") or "",
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"base_url": proxy.get("base_url"),
"model": result.get("model") or payload.get("model"),
"latency_ms": latency_ms,
"artifact": artifact,
}
)
append_image_job({**artifact, "type": operation, "status": "completed", "latency_ms": latency_ms, "updated_at": now_iso()})
proxy["artifact"] = artifact
job["artifact"] = artifact
if isinstance(job.get("result"), dict):
job["result"]["artifact"] = artifact
return job
def quality_check_answer(answer: str | None) -> dict:
text = (answer or "").strip()
if not text:
return {"passed": False, "reason": "empty answer"}
if len(text) < 12:
return {"passed": False, "reason": "answer is too short"}
return {"passed": True, "reason": "non-empty answer"}
def run_quality_suite(payload: dict) -> dict:
catalog = load_catalog()
plugin = str(payload.get("plugin") or "text")
model_name = str(payload.get("model") or "")
base_url = normalize_base_url(payload.get("base_url") or DEFAULT_BASE_URL)
tests = payload.get("tests") or TEST_PACKS.get(plugin) or []
if not model_name:
route = route_for_plugin(plugin)
model_name = route["served_model_name"]
if not isinstance(tests, list) or not tests:
raise ValueError("quality tests are empty")
results = []
for test in tests:
if not isinstance(test, dict):
raise ValueError("each quality test must be an object")
prompt = str(test.get("prompt") or "").strip()
if not prompt:
continue
messages = [
{"role": "system", "content": "Отвечай по-русски, кратко и проверяемо."},
{"role": "user", "content": prompt},
]
result = run_chat_request(base_url, model_name, messages, temperature=0.2, max_tokens=500)
guardrail = None
if plugin == "1c":
result["answer"], guardrail = guard_1c_answer(prompt, result.get("answer"))
check = quality_check_answer(result.get("answer"))
result.update(
{
"test_id": test.get("id"),
"label": test.get("label"),
"prompt": prompt,
"passed": check["passed"],
"check_reason": check["reason"],
"safety_guardrail": guardrail,
}
)
results.append(result)
passed = sum(1 for result in results if result.get("passed"))
report_id = append_report(
{
"type": "quality",
"plugin": plugin,
"served_model_name": model_name,
"base_url": base_url,
"passed": passed,
"total": len(results),
"results": results,
}
)
return {"report_id": report_id, "plugin": plugin, "model": model_name, "passed": passed, "total": len(results), "results": results}
def read_body(handler: BaseHTTPRequestHandler) -> dict:
length = int(handler.headers.get("Content-Length") or 0)
raw = handler.rfile.read(length) if length else b"{}"
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("request body must be a JSON object")
return data
def write_json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
encoded = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Content-Length", str(len(encoded)))
handler.end_headers()
handler.wfile.write(encoded)
def safe_static_path(static_dir: Path, request_path: str) -> Path:
parsed = urlparse(request_path)
relative = parsed.path.lstrip("/") or "index.html"
candidate = (static_dir / relative).resolve()
static_root = static_dir.resolve()
if static_root not in candidate.parents and candidate != static_root:
raise ValueError("invalid static path")
if candidate.is_dir():
candidate = candidate / "index.html"
return candidate
class ChatHandler(BaseHTTPRequestHandler):
static_dir = DEFAULT_STATIC_DIR
def log_message(self, format: str, *args: object) -> None:
print(f"{self.address_string()} - {format % args}", file=sys.stderr)
def do_GET(self) -> None:
if self.path.startswith("/health") or self.path.startswith("/api/health"):
write_json_response(self, 200, health_status())
return
if self.path.startswith("/api/catalog"):
write_json_response(self, 200, load_catalog())
return
if self.path.startswith("/api/routes"):
try:
parsed = urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
plugin = (params.get("plugin") or ["text"])[0]
task = (params.get("task") or [None])[0]
write_json_response(self, 200, route_for_plugin(plugin, task))
except ValueError as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/model-services"):
status = health_status()
write_json_response(self, 200, {"services": status["model_services"], "routes": status["routes"]})
return
if self.path.startswith("/api/benchmark/history"):
parsed = urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
limit = int((params.get("limit") or ["10"])[0])
model_id = (params.get("model_id") or [None])[0]
plugin = (params.get("plugin") or [None])[0]
write_json_response(
self,
200,
{
"benchmarks": latest_runtime_benchmarks(
limit=max(1, min(limit, 50)),
model_id=model_id,
plugin=plugin,
)
},
)
return
if self.path.startswith("/api/benchmark/report.md"):
try:
parsed = urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
name = (params.get("name") or [""])[0]
path = benchmark_report_path(name)
report = read_json(path)
content = render_benchmark_markdown(report, path.name).encode("utf-8")
except FileNotFoundError:
write_json_response(self, 404, {"error": "not found"})
return
except (ValueError, json.JSONDecodeError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
self.send_response(200)
self.send_header("Content-Type", "text/markdown; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
return
if self.path.startswith("/api/benchmark/report"):
try:
parsed = urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
path = benchmark_report_path((params.get("name") or [""])[0])
except FileNotFoundError:
write_json_response(self, 404, {"error": "not found"})
return
except ValueError as exc:
write_json_response(self, 400, {"error": str(exc)})
return
content = path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
return
if self.path.startswith("/api/model-ingest/jobs"):
write_json_response(self, 200, {"jobs": latest_model_ingest_jobs()})
return
if self.path.startswith("/api/image/gallery"):
write_json_response(self, 200, {"images": latest_image_gallery()})
return
if urlparse(self.path).path == "/api/image/job":
try:
parsed = urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
job_id = str((params.get("job_id") or [""])[0]).strip()
if not job_id:
raise ValueError("job_id is required")
with IMAGE_PROXY_JOB_LOCK:
proxy = dict(IMAGE_PROXY_JOBS.get(job_id) or {})
base_url = normalize_base_url((params.get("base_url") or [proxy.get("base_url") or route_for_plugin("image")["base_url"]])[0])
job = fetch_image_job(base_url=base_url, job_id=job_id)
if job.get("status") == "completed":
job = finalize_image_job(job)
if job.get("status") in {"cancelled", "error"}:
append_image_job(
{
"id": job_id,
"type": job.get("operation") or proxy.get("operation") or "generate",
"status": job.get("status"),
"error": job.get("error"),
"latency_ms": job.get("latency_ms"),
"updated_at": now_iso(),
}
)
write_json_response(self, 200, job)
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/image/jobs"):
write_json_response(self, 200, {"jobs": latest_image_jobs()})
return
if self.path.startswith("/generated-images/"):
relative = self.path.removeprefix("/generated-images/").split("?", 1)[0].lstrip("/")
image_path = (IMAGE_GALLERY_DIR / relative).resolve()
gallery_root = IMAGE_GALLERY_DIR.resolve()
if gallery_root not in image_path.parents or image_path.suffix.lower() != ".png" or not image_path.exists():
write_json_response(self, 404, {"error": "not found"})
return
content = image_path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
return
try:
path = safe_static_path(self.static_dir, self.path)
except ValueError as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if not path.exists():
write_json_response(self, 404, {"error": "not found"})
return
content = path.read_bytes()
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
if content_type.startswith("text/") or path.suffix in {".js", ".css"}:
content_type += "; charset=utf-8"
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
def do_POST(self) -> None:
if self.path.startswith("/api/model-ingest/upload"):
try:
parsed = urlparse(self.path)
params = {}
for chunk in parsed.query.split("&"):
if not chunk or "=" not in chunk:
continue
key, value = chunk.split("=", 1)
params[urllib.parse.unquote_plus(key)] = urllib.parse.unquote_plus(value)
content_length = int(self.headers.get("Content-Length") or 0)
if content_length <= 0:
raise ValueError("upload body is required")
filename = sanitize_filename(
self.headers.get("X-Model-Filename")
or params.get("filename")
or "model.bin"
)
payload = {
"model_id": self.headers.get("X-Model-Id") or params.get("model_id") or Path(filename).stem,
"name": self.headers.get("X-Model-Name") or params.get("name") or Path(filename).stem,
"plugin": self.headers.get("X-Model-Plugin") or params.get("plugin") or "text",
"format": self.headers.get("X-Model-Format") or params.get("format") or Path(filename).suffix.lstrip("."),
"quantization": self.headers.get("X-Model-Quantization") or params.get("quantization") or "",
"notes": self.headers.get("X-Model-Notes") or params.get("notes") or "",
}
job, job_dir = create_model_ingest_job(payload, source_kind="upload")
destination = job_dir / filename
bytes_written = stream_request_to_file(self, destination, content_length)
job["status"] = "uploaded" if bytes_written == content_length else "partial"
job["updated_at"] = now_iso()
job["files"].append({"path": str(destination.relative_to(ROOT)), "size_bytes": bytes_written})
if bytes_written != content_length:
job["error"] = f"expected {content_length} bytes, received {bytes_written}"
write_model_ingest_metadata(job_dir, job)
append_model_ingest_job(job)
write_json_response(self, 200, job)
except (OSError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/model-ingest/source"):
try:
payload = read_body(self)
job = import_model_from_source(payload)
status = 200 if job.get("status") not in {"failed"} else 400
write_json_response(self, status, job)
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/model-ingest/verify"):
try:
payload = read_body(self)
job = verify_model_ingest_job(str(payload["job_id"]))
status = 200 if job.get("status") == "verified" else 400
write_json_response(self, status, job)
except (KeyError, TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/model-ingest/register"):
try:
payload = read_body(self)
job = register_model_ingest_job(str(payload["job_id"]))
status = 200 if str(job.get("status")).startswith("registered") else 400
write_json_response(self, status, job)
except (KeyError, TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/models"):
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or DEFAULT_BASE_URL)
expected_model = payload.get("model")
started_at = time.perf_counter()
models = fetch_endpoint_models(base_url)
latency_ms = round((time.perf_counter() - started_at) * 1000)
write_json_response(
self,
200,
{
"base_url": base_url,
"models": models,
"latency_ms": latency_ms,
"expected_model": expected_model,
"available": expected_model in models if expected_model else None,
},
)
except (TypeError, ValueError, urllib.error.URLError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/route"):
try:
payload = read_body(self)
write_json_response(self, 200, route_for_plugin(str(payload.get("plugin") or "text"), payload.get("task")))
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/quality/run"):
try:
write_json_response(self, 200, run_quality_suite(read_body(self)))
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/service-control"):
try:
result = control_model_service(read_body(self))
status = 200 if result["ok"] else 500
write_json_response(self, status, result)
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/benchmark/preflight"):
try:
result = runtime_benchmark_preflight(read_body(self))
status = 200 if result["ready"] else 409
write_json_response(self, status, result)
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/benchmark/runtime"):
try:
result = run_runtime_benchmark(read_body(self))
status = 200 if result["ok"] else 500
write_json_response(self, status, result)
except (TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/feedback"):
try:
payload = read_body(self)
rating = str(payload["rating"])
if rating not in {"ok", "bad", "needs_review"}:
raise ValueError("rating must be ok, bad, or needs_review")
report_id = append_report(
{
"type": "feedback",
"parent_id": payload.get("report_id"),
"rating": rating,
"note": payload.get("note") or "",
}
)
write_json_response(self, 200, {"report_id": report_id})
except (KeyError, TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/1c/rag-prompt"):
try:
payload = read_body(self)
question = str(payload["question"]).strip()
if not question:
raise ValueError("question is required")
result = build_1c_rag_prompt(
question,
profile_name=str(payload.get("profile") or "auto"),
limit=int(payload["limit"]) if payload.get("limit") is not None else None,
)
report_id = append_report(
{
"type": "rag_prompt",
"plugin": "1c",
"profile": result["profile"],
"question": question,
"context_count": result["context_count"],
"sources": result["sources"],
}
)
result["report_id"] = report_id
write_json_response(self, 200, result)
except (KeyError, TypeError, ValueError, FileNotFoundError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/1c/rag-chat"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
try:
payload = read_body(self)
question = str(payload["question"]).strip()
if not question:
raise ValueError("question is required")
base_url = normalize_base_url(payload.get("base_url") or DEFAULT_BASE_URL)
model = str(payload.get("model") or route_for_plugin("1c")["served_model_name"])
rag = build_1c_rag_prompt(
question,
profile_name=str(payload.get("profile") or "auto"),
limit=int(payload["limit"]) if payload.get("limit") is not None else None,
)
messages = [
{"role": "system", "content": "Ты помощник по 1С. Не предлагай прямые изменения живой базы без проверки, бэкапа и согласования."},
{"role": "user", "content": rag["prompt"]},
]
answer = call_chat_completion(
base_url=base_url,
model=model,
messages=messages,
temperature=float(payload.get("temperature", 0.2)),
max_tokens=int(payload.get("max_tokens", 1000)),
timeout=180,
)
answer, guardrail = guard_1c_answer(question, answer)
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report(
{
"id": report_id,
"type": "1c_rag_chat",
"plugin": "1c",
"profile": rag["profile"],
"question": question,
"answer": answer,
"sources": rag["sources"],
"context_count": rag["context_count"],
"served_model_name": model,
"base_url": base_url,
"latency_ms": latency_ms,
"safety_guardrail": guardrail,
}
)
write_json_response(self, 200, {**rag, "answer": answer, "report_id": report_id, "latency_ms": latency_ms, "safety_guardrail": guardrail})
except (KeyError, TypeError, ValueError, FileNotFoundError, urllib.error.URLError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "1c_rag_chat", "status": "error", "error": str(exc), "latency_ms": latency_ms})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/audio/transcribe"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or route_for_plugin("audio")["base_url"])
audio_base64 = str(payload.get("audio_base64") or "")
filename = sanitize_filename(payload.get("filename") or "audio.wav", fallback="audio.wav")
if not audio_base64:
raise ValueError("audio_base64 is required")
result = call_audio_transcription(
base_url=base_url,
audio_base64=audio_base64,
filename=filename,
language=str(payload.get("language") or "") or None,
task=str(payload.get("task") or "") or None,
)
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report(
{
"id": report_id,
"type": "audio_transcription",
"plugin": "audio",
"filename": filename,
"bytes": result.get("bytes"),
"text": result.get("text"),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
"latency_ms": latency_ms,
}
)
write_json_response(self, 200, {**result, "report_id": report_id, "latency_ms": latency_ms})
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "audio_transcription", "status": "error", "error": str(exc), "latency_ms": latency_ms})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/video/analyze"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or route_for_plugin("video")["base_url"])
image_base64 = str(payload.get("image_base64") or "")
filename = sanitize_filename(payload.get("filename") or "image.png", fallback="image.png")
prompt = str(payload.get("prompt") or "").strip() or "Опиши изображение и перечисли важные детали."
if not image_base64:
raise ValueError("image_base64 is required")
result = call_vision_analysis(
base_url=base_url,
image_base64=image_base64,
filename=filename,
prompt=prompt,
model=str(payload.get("model") or "") or None,
max_tokens=int(payload.get("max_tokens") or 512),
)
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report(
{
"id": report_id,
"type": "vision_analysis",
"plugin": "video",
"filename": filename,
"bytes": result.get("bytes"),
"width": result.get("width"),
"height": result.get("height"),
"prompt": prompt,
"text": result.get("text"),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
"latency_ms": latency_ms,
}
)
write_json_response(self, 200, {**result, "report_id": report_id, "latency_ms": latency_ms})
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "vision_analysis", "status": "error", "error": str(exc), "latency_ms": latency_ms})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/image/submit"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
try:
payload = read_body(self)
operation = str(payload.get("operation") or payload.get("mode") or "generate")
if operation not in {"generate", "edit"}:
raise ValueError("operation must be generate or edit")
model_id = str(payload.get("model_id") or "").strip()
model_mode = str(payload.get("model_mode") or "").strip()
image_route = route_for_model("image", model_id, "image-editing" if operation == "edit" else None)
base_url = normalize_base_url(payload.get("base_url") or image_route["base_url"])
prompt = str(payload.get("prompt") or "").strip()
if not prompt:
raise ValueError("prompt is required")
seed_value = payload.get("seed")
seed = int(seed_value) if seed_value not in {None, ""} else None
job_payload: dict[str, object] = {
"prompt": prompt,
"negative_prompt": str(payload.get("negative_prompt") or "") or "",
"width": int(payload.get("width") or 1024),
"height": int(payload.get("height") or 1024),
"steps": int(payload.get("steps") or 28),
"guidance_scale": float(payload.get("guidance_scale") or 6.0),
"model": payload.get("model") or image_route["served_model_name"],
"model_id": model_id or image_route["model"].get("id"),
"model_mode": model_mode or image_route["model"].get("id"),
}
if seed is not None:
job_payload["seed"] = seed
if operation == "edit":
image_base64 = str(payload.get("image_base64") or "")
mask_base64 = str(payload.get("mask_base64") or "")
if not image_base64:
raise ValueError("image_base64 is required")
if not mask_base64:
raise ValueError("mask_base64 is required")
job_payload["image_base64"] = image_base64
job_payload["mask_base64"] = mask_base64
job_payload["strength"] = float(payload.get("strength") or 0.95)
expected_model = str(job_payload.get("model") or image_route["served_model_name"] or "")
endpoint_models = fetch_endpoint_models(base_url, timeout=5)
if expected_model and expected_model not in endpoint_models:
raise ValueError(
f"image endpoint {base_url} serves {endpoint_models or 'no models'}, "
f"not selected model {expected_model}"
)
append_image_job({"id": report_id, "type": operation, "status": "submitted", "created_at": now_iso()})
submitted = submit_image_job(base_url=base_url, operation=operation, payload=job_payload, job_id=report_id)
with IMAGE_PROXY_JOB_LOCK:
IMAGE_PROXY_JOBS[report_id] = {
"id": report_id,
"operation": operation,
"base_url": base_url,
"model_id": job_payload.get("model_id"),
"model_mode": job_payload.get("model_mode"),
"payload": job_payload,
"submitted_at": now_iso(),
"started_perf": started_at,
}
write_json_response(self, 202, {**submitted, "report_id": report_id, "base_url": base_url})
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "image_job_submit", "status": "error", "error": str(exc), "latency_ms": latency_ms})
append_image_job({"id": report_id, "type": "image", "status": "error", "error": str(exc), "latency_ms": latency_ms, "updated_at": now_iso()})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/image/cancel"):
try:
payload = read_body(self)
job_id = str(payload.get("job_id") or payload.get("report_id") or "").strip()
if not job_id:
raise ValueError("job_id is required")
with IMAGE_PROXY_JOB_LOCK:
proxy = dict(IMAGE_PROXY_JOBS.get(job_id) or {})
base_url = normalize_base_url(payload.get("base_url") or proxy.get("base_url") or route_for_plugin("image")["base_url"])
result = cancel_image_job(base_url=base_url, job_id=job_id)
append_image_job(
{
"id": job_id,
"type": result.get("operation") or proxy.get("operation") or "generate",
"status": result.get("status") or "cancel_requested",
"updated_at": now_iso(),
}
)
write_json_response(self, 200, result)
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if self.path.startswith("/api/image/generate"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
append_image_job({"id": report_id, "type": "generate", "status": "running", "created_at": now_iso()})
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or route_for_plugin("image")["base_url"])
prompt = str(payload.get("prompt") or "").strip()
if not prompt:
raise ValueError("prompt is required")
seed_value = payload.get("seed")
seed = int(seed_value) if seed_value not in {None, ""} else None
result = call_image_generation(
base_url=base_url,
prompt=prompt,
negative_prompt=str(payload.get("negative_prompt") or "") or None,
width=int(payload.get("width") or 1024),
height=int(payload.get("height") or 1024),
steps=int(payload.get("steps") or 28),
guidance_scale=float(payload.get("guidance_scale") or 6.0),
seed=seed,
)
latency_ms = round((time.perf_counter() - started_at) * 1000)
artifact = save_image_artifact(
report_id=report_id,
image_base64=str(result.get("image_base64") or ""),
kind="generate",
metadata={
"prompt": prompt,
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"steps": int(payload.get("steps") or 28),
"guidance_scale": float(payload.get("guidance_scale") or 6.0),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
},
)
append_report(
{
"id": report_id,
"type": "image_generation",
"plugin": "image",
"prompt": prompt,
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
"latency_ms": latency_ms,
"artifact": artifact,
}
)
append_image_job({**artifact, "status": "completed", "latency_ms": latency_ms})
write_json_response(self, 200, {**result, "report_id": report_id, "latency_ms": latency_ms, "artifact": artifact})
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "image_generation", "status": "error", "error": str(exc), "latency_ms": latency_ms})
append_image_job({"id": report_id, "type": "generate", "status": "error", "error": str(exc), "latency_ms": latency_ms, "updated_at": now_iso()})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/image/edit"):
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
append_image_job({"id": report_id, "type": "edit", "status": "running", "created_at": now_iso()})
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or route_for_plugin("image", "image-editing")["base_url"])
prompt = str(payload.get("prompt") or "").strip()
image_base64 = str(payload.get("image_base64") or "")
mask_base64 = str(payload.get("mask_base64") or "")
if not prompt:
raise ValueError("prompt is required")
if not image_base64:
raise ValueError("image_base64 is required")
if not mask_base64:
raise ValueError("mask_base64 is required")
seed_value = payload.get("seed")
seed = int(seed_value) if seed_value not in {None, ""} else None
result = call_image_edit(
base_url=base_url,
prompt=prompt,
image_base64=image_base64,
mask_base64=mask_base64,
negative_prompt=str(payload.get("negative_prompt") or "") or None,
width=int(payload.get("width") or 1024),
height=int(payload.get("height") or 1024),
steps=int(payload.get("steps") or 28),
guidance_scale=float(payload.get("guidance_scale") or 6.0),
strength=float(payload.get("strength") or 0.95),
seed=seed,
)
latency_ms = round((time.perf_counter() - started_at) * 1000)
artifact = save_image_artifact(
report_id=report_id,
image_base64=str(result.get("image_base64") or ""),
kind="edit",
metadata={
"prompt": prompt,
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"steps": int(payload.get("steps") or 28),
"guidance_scale": float(payload.get("guidance_scale") or 6.0),
"strength": float(payload.get("strength") or 0.95),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
},
)
append_report(
{
"id": report_id,
"type": "image_edit",
"plugin": "image",
"prompt": prompt,
"negative_prompt": payload.get("negative_prompt") or "",
"width": result.get("width"),
"height": result.get("height"),
"seed": result.get("seed"),
"base_url": base_url,
"model": result.get("model") or payload.get("model"),
"latency_ms": latency_ms,
"artifact": artifact,
}
)
append_image_job({**artifact, "status": "completed", "latency_ms": latency_ms})
write_json_response(self, 200, {**result, "report_id": report_id, "latency_ms": latency_ms, "artifact": artifact})
except (TypeError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report({"id": report_id, "type": "image_edit", "status": "error", "error": str(exc), "latency_ms": latency_ms})
append_image_job({"id": report_id, "type": "edit", "status": "error", "error": str(exc), "latency_ms": latency_ms, "updated_at": now_iso()})
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
if self.path.startswith("/api/compare"):
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or DEFAULT_BASE_URL)
models = payload.get("models") or []
messages = payload.get("messages") or []
temperature = float(payload.get("temperature", 0.2))
max_tokens = int(payload.get("max_tokens", 1000))
if not isinstance(models, list) or not models:
raise ValueError("models must be a non-empty list")
if not isinstance(messages, list) or not messages:
raise ValueError("messages must be a non-empty list")
results = []
for model in models:
if not isinstance(model, dict):
raise ValueError("each model must be an object")
served_model = str(model["served_model_name"])
result = run_chat_request(base_url, served_model, messages, temperature, max_tokens)
guardrail = None
if payload.get("plugin") == "1c":
result["answer"], guardrail = guard_1c_answer(latest_user_message(messages), result.get("answer"))
result.update(
{
"registry_model_id": model.get("id"),
"name": model.get("name"),
"served_model_name": served_model,
"safety_guardrail": guardrail,
}
)
results.append(result)
report_id = append_report(
{
"type": "compare",
"plugin": payload.get("plugin"),
"test_id": payload.get("test_id"),
"base_url": base_url,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"results": results,
}
)
write_json_response(self, 200, {"report_id": report_id, "results": results})
except (KeyError, TypeError, ValueError) as exc:
write_json_response(self, 400, {"error": str(exc)})
return
if not self.path.startswith("/api/chat"):
write_json_response(self, 404, {"error": "not found"})
return
report_id = str(uuid.uuid4())
started_at = time.perf_counter()
try:
payload = read_body(self)
base_url = normalize_base_url(payload.get("base_url") or DEFAULT_BASE_URL)
model = str(payload["model"])
registry_model_id = payload.get("registry_model_id")
plugin = payload.get("plugin")
test_id = payload.get("test_id")
messages = payload.get("messages") or []
temperature = float(payload.get("temperature", 0.2))
max_tokens = int(payload.get("max_tokens", 1000))
if not isinstance(messages, list) or not messages:
raise ValueError("messages must be a non-empty list")
answer = call_chat_completion(
base_url=base_url,
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
timeout=180,
)
guardrail = None
if plugin == "1c":
answer, guardrail = guard_1c_answer(latest_user_message(messages), answer)
except (KeyError, TypeError, ValueError, urllib.error.URLError) as exc:
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report(
{
"id": report_id,
"type": "chat",
"status": "error",
"error": str(exc),
"payload": locals().get("payload", {}),
"latency_ms": latency_ms,
}
)
write_json_response(self, 400, {"error": str(exc), "report_id": report_id, "latency_ms": latency_ms})
return
latency_ms = round((time.perf_counter() - started_at) * 1000)
append_report(
{
"id": report_id,
"type": "chat",
"status": "answered",
"plugin": plugin,
"registry_model_id": registry_model_id,
"served_model_name": model,
"base_url": base_url,
"test_id": test_id,
"messages": messages,
"answer": answer,
"temperature": temperature,
"max_tokens": max_tokens,
"latency_ms": latency_ms,
"rating": None,
"safety_guardrail": guardrail,
}
)
write_json_response(self, 200, {"answer": answer, "report_id": report_id, "latency_ms": latency_ms, "safety_guardrail": guardrail})
def main() -> int:
parser = argparse.ArgumentParser(description="Run a local model chat test UI.")
parser.add_argument("--host", default=DEFAULT_HOST)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--static-dir", type=Path, default=DEFAULT_STATIC_DIR)
args = parser.parse_args()
ChatHandler.static_dir = args.static_dir
server = ThreadingHTTPServer((args.host, args.port), ChatHandler)
print(f"Model chat UI: http://{args.host}:{args.port}")
print(f"Static dir: {args.static_dir}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopping model chat UI.")
return 0
if __name__ == "__main__":
raise SystemExit(main())