From 690ad46d88c0444dfcc5766d403a8c383b09ca27fd90697cba1e0cf5915a37f7 Mon Sep 17 00:00:00 2001 From: tlg Date: Sat, 4 Apr 2026 07:30:13 +0200 Subject: [PATCH] feat: config loading for models.yaml and api_keys.yaml Co-Authored-By: Claude Sonnet 4.6 --- kischdle/llmux/llmux/config.py | 79 +++++++++++++++++++++++++++++ kischdle/llmux/tests/test_config.py | 56 ++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 kischdle/llmux/llmux/config.py create mode 100644 kischdle/llmux/tests/test_config.py diff --git a/kischdle/llmux/llmux/config.py b/kischdle/llmux/llmux/config.py new file mode 100644 index 0000000..e42a066 --- /dev/null +++ b/kischdle/llmux/llmux/config.py @@ -0,0 +1,79 @@ +import os +from dataclasses import dataclass, field +from pathlib import Path + +import yaml + + +def _config_dir() -> Path: + return Path(os.environ.get("LLMUX_CONFIG_DIR", "/config")) + + +@dataclass +class PhysicalModel: + type: str # "llm", "asr", "tts" + backend: str # "transformers", "llamacpp", "chatterbox" + estimated_vram_gb: float + model_id: str = "" + model_file: str = "" + mmproj_file: str = "" + supports_vision: bool = False + supports_tools: bool = False + default_language: str = "" + variant: str = "" + + +@dataclass +class VirtualModel: + physical: str + params: dict = field(default_factory=dict) + + +@dataclass +class ApiKey: + key: str + name: str + + +def load_models_config( + config_path: Path | None = None, +) -> tuple[dict[str, PhysicalModel], dict[str, VirtualModel]]: + if config_path is None: + config_path = _config_dir() / "models.yaml" + + with open(config_path) as f: + raw = yaml.safe_load(f) + + physical: dict[str, PhysicalModel] = {} + for model_id, attrs in raw["physical_models"].items(): + physical[model_id] = PhysicalModel( + type=attrs["type"], + backend=attrs["backend"], + estimated_vram_gb=attrs["estimated_vram_gb"], + model_id=attrs.get("model_id", ""), + model_file=attrs.get("model_file", ""), + mmproj_file=attrs.get("mmproj_file", ""), + supports_vision=attrs.get("supports_vision", False), + supports_tools=attrs.get("supports_tools", False), + default_language=attrs.get("default_language", ""), + variant=attrs.get("variant", ""), + ) + + virtual: dict[str, VirtualModel] = {} + for model_name, attrs in raw["virtual_models"].items(): + virtual[model_name] = VirtualModel( + physical=attrs["physical"], + params=attrs.get("params", {}), + ) + + return physical, virtual + + +def load_api_keys(config_path: Path | None = None) -> list[ApiKey]: + if config_path is None: + config_path = _config_dir() / "api_keys.yaml" + + with open(config_path) as f: + raw = yaml.safe_load(f) + + return [ApiKey(key=entry["key"], name=entry["name"]) for entry in raw["api_keys"]] diff --git a/kischdle/llmux/tests/test_config.py b/kischdle/llmux/tests/test_config.py new file mode 100644 index 0000000..ab807e7 --- /dev/null +++ b/kischdle/llmux/tests/test_config.py @@ -0,0 +1,56 @@ +from llmux.config import load_models_config, load_api_keys, PhysicalModel, VirtualModel + + +def test_load_models_config_returns_physical_and_virtual(): + physical, virtual = load_models_config() + assert isinstance(physical, dict) + assert isinstance(virtual, dict) + assert len(physical) == 9 + assert len(virtual) == 16 + + +def test_physical_model_has_required_fields(): + physical, _ = load_models_config() + qwen = physical["qwen3.5-9b-fp8"] + assert qwen.type == "llm" + assert qwen.backend == "transformers" + assert qwen.model_id == "lovedheart/Qwen3.5-9B-FP8" + assert qwen.estimated_vram_gb == 9 + assert qwen.supports_vision is True + assert qwen.supports_tools is True + + +def test_physical_model_llamacpp_has_gguf_fields(): + physical, _ = load_models_config() + uncensored = physical["qwen3.5-9b-fp8-uncensored"] + assert uncensored.backend == "llamacpp" + assert uncensored.model_file == "Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q8_0.gguf" + assert uncensored.mmproj_file == "mmproj-Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-BF16.gguf" + + +def test_virtual_model_maps_to_physical(): + _, virtual = load_models_config() + thinking = virtual["Qwen3.5-9B-FP8-Thinking"] + assert thinking.physical == "qwen3.5-9b-fp8" + assert thinking.params == {"enable_thinking": True} + + +def test_virtual_model_gpt_oss_has_system_prompt(): + _, virtual = load_models_config() + low = virtual["GPT-OSS-20B-Low"] + assert low.physical == "gpt-oss-20b" + assert low.params == {"system_prompt_prefix": "Reasoning: low"} + + +def test_virtual_model_without_params(): + _, virtual = load_models_config() + ct = virtual["cohere-transcribe"] + assert ct.physical == "cohere-transcribe" + assert ct.params == {} + + +def test_load_api_keys(): + keys = load_api_keys() + assert len(keys) == 3 + assert all(k.key.startswith("sk-llmux-") for k in keys) + assert {k.name for k in keys} == {"Open WebUI", "Remote Whisper clients", "OpenCode"}