Files
2026-07-13 13:12:33 +08:00

225 lines
7.7 KiB
Python

"""``[models.*]`` config overrides wired into the shared model catalog.
``ModelOverrideConfig`` (schema, ``gateway/config.py``) is flattened by
``model_override_entries`` into the ``ModelCatalog.set_user_overrides`` key
shape and installed onto the shared catalog at gateway boot
(``build_services``) and re-applied on every config hot-apply path
(``config.set``/``patch``/``apply``/``reload`` — see
``rpc_config._sync_model_catalog_overrides``), so ``resolve_model_price`` and
``get_capabilities`` honor operator cost/metadata overrides without a
restart.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from opensquilla.gateway.boot import (
apply_model_catalog_overrides,
build_services,
model_override_entries,
)
from opensquilla.gateway.config import GatewayConfig, ModelOverrideConfig
from opensquilla.gateway.rpc_config import _sync_model_catalog_overrides
from opensquilla.provider.model_catalog import ModelCatalog, set_shared_catalog, shared_catalog
from opensquilla.sandbox.integration import reset_runtime
@pytest.fixture(autouse=True)
def _clear_shared_catalog():
set_shared_catalog(None)
yield
set_shared_catalog(None)
# ---------------------------------------------------------------------------
# model_override_entries — pure flattening helper
# ---------------------------------------------------------------------------
def test_model_override_entries_shape() -> None:
cfg = GatewayConfig()
cfg.models = {
"deepseek": {
"deepseek-v4-pro": ModelOverrideConfig(
input_cost_per_mtok=0.2, cache_read_cost_per_mtok=0.002
)
}
}
entries = model_override_entries(cfg)
assert entries == {
"deepseek/deepseek-v4-pro": {
"input_cost_per_mtok": 0.2,
"cache_read_cost_per_mtok": 0.002,
}
}
def test_model_override_entries_drops_thinking_level_map() -> None:
cfg = GatewayConfig()
cfg.models = {
"openrouter": {
"z-ai/glm-5.2": ModelOverrideConfig(
input_cost_per_mtok=0.5,
thinking_level_map={"high": "high"},
)
}
}
entries = model_override_entries(cfg)
assert entries == {"openrouter/z-ai/glm-5.2": {"input_cost_per_mtok": 0.5}}
def test_model_override_entries_skips_empty_overrides() -> None:
"""An all-None override (or one that only sets thinking_level_map) must
not produce an empty entry — resolve_entry treats an empty dict as
"no override" the same as no key at all, but skipping it here keeps
set_user_overrides' installed map free of dead keys."""
cfg = GatewayConfig()
cfg.models = {"custom": {"idle-model": ModelOverrideConfig()}}
assert model_override_entries(cfg) == {}
def test_model_override_entries_lowercases_keys() -> None:
cfg = GatewayConfig()
cfg.models = {"OpenRouter": {"Z-AI/GLM-5.2": ModelOverrideConfig(input_cost_per_mtok=0.5)}}
entries = model_override_entries(cfg)
assert entries == {"openrouter/z-ai/glm-5.2": {"input_cost_per_mtok": 0.5}}
# ---------------------------------------------------------------------------
# apply_model_catalog_overrides — resilient installation onto a catalog
# ---------------------------------------------------------------------------
def test_apply_model_catalog_overrides_installs_onto_catalog() -> None:
cfg = GatewayConfig()
cfg.models = {
"deepseek": {
"deepseek-v4-pro": ModelOverrideConfig(
input_cost_per_mtok=0.2,
output_cost_per_mtok=0.4,
cache_read_cost_per_mtok=0.002,
)
}
}
catalog = ModelCatalog()
apply_model_catalog_overrides(catalog, cfg)
entry = catalog.resolve_entry("deepseek-v4-pro", provider="deepseek")
assert entry.source == "user"
assert entry.input_cost_per_mtok == pytest.approx(0.2)
assert entry.output_cost_per_mtok == pytest.approx(0.4)
assert entry.cache_read_cost_per_mtok == pytest.approx(0.002)
def test_apply_model_catalog_overrides_survives_bad_value_and_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A ValueError from ModelCatalog.set_user_overrides (unknown field,
type mismatch, ...) must not propagate — boot/hot-apply stays up — and
must be logged rather than dropped silently."""
class _ExplodingCatalog(ModelCatalog):
def set_user_overrides(self, overrides):
raise ValueError("invalid model catalog override for 'x/y': boom")
cfg = GatewayConfig()
cfg.models = {"x": {"y": ModelOverrideConfig(input_cost_per_mtok=0.1)}}
catalog = _ExplodingCatalog()
import structlog
with structlog.testing.capture_logs() as captured:
apply_model_catalog_overrides(catalog, cfg) # must not raise
warnings = [entry for entry in captured if entry["log_level"] == "warning"]
assert warnings, "expected a structlog warning naming the bad override"
assert any("boom" in str(entry.get("error", "")) for entry in warnings)
def test_apply_model_catalog_overrides_noop_for_empty_config() -> None:
catalog = ModelCatalog()
apply_model_catalog_overrides(catalog, GatewayConfig())
entry = catalog.resolve_entry("some-model", provider="deepseek")
assert entry.source != "user"
# ---------------------------------------------------------------------------
# Boot wiring — build_services installs overrides onto the shared catalog.
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _drop_sandbox_runtime():
yield
reset_runtime()
@pytest.mark.asyncio
async def test_build_services_wires_model_overrides_into_shared_catalog(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "state"))
def fail_background_sandbox_setup(coro):
close = getattr(coro, "close", None)
if callable(close):
close()
raise AssertionError("unit tests must not schedule real sandbox setup")
monkeypatch.setattr(
"opensquilla.gateway.boot.create_background_task",
fail_background_sandbox_setup,
)
config = GatewayConfig(
memory={"flush_enabled": False},
sandbox={"auto_setup": False},
)
config.models = {
"deepseek": {
"deepseek-v4-pro": ModelOverrideConfig(
input_cost_per_mtok=0.2, cache_write_cost_per_mtok=0.6
)
}
}
services = await build_services(
config=config, session_db_path=":memory:", seed_agent_workspaces=False
)
try:
assert services.model_catalog is not None
assert shared_catalog() is services.model_catalog
entry = services.model_catalog.resolve_entry("deepseek-v4-pro", provider="deepseek")
assert entry.source == "user"
assert entry.input_cost_per_mtok == pytest.approx(0.2)
assert entry.cache_write_cost_per_mtok == pytest.approx(0.6)
finally:
await services.close()
# ---------------------------------------------------------------------------
# Hot-apply re-application (config.set/patch/apply/reload → rpc_config).
# ---------------------------------------------------------------------------
def test_sync_model_catalog_overrides_reapplies_onto_shared_catalog() -> None:
catalog = ModelCatalog()
set_shared_catalog(catalog)
cfg = GatewayConfig()
cfg.models = {
"deepseek": {
"deepseek-v4-pro": ModelOverrideConfig(input_cost_per_mtok=0.3),
}
}
_sync_model_catalog_overrides(cfg)
entry = shared_catalog().resolve_entry("deepseek-v4-pro", provider="deepseek")
assert entry.source == "user"
assert entry.input_cost_per_mtok == pytest.approx(0.3)