feat: config loading for models.yaml and api_keys.yaml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
79
kischdle/llmux/llmux/config.py
Normal file
79
kischdle/llmux/llmux/config.py
Normal file
@@ -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"]]
|
||||
56
kischdle/llmux/tests/test_config.py
Normal file
56
kischdle/llmux/tests/test_config.py
Normal file
@@ -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"}
|
||||
Reference in New Issue
Block a user