221 lines
7.9 KiB
Python
221 lines
7.9 KiB
Python
"""Built-in default provider resolution in GatewayConfig.
|
|
|
|
The built-in default is tokenrhythm, but configs authored while openrouter
|
|
was the default must keep resolving to openrouter: an unset ``llm.provider``
|
|
falls back to openrouter when provider-coupled fields were explicitly set,
|
|
when ``squilla_router.tier_profile`` pins openrouter, or when the
|
|
environment carries only the openrouter credential. The resolution is
|
|
load-time only and must never be baked into config.toml by unrelated saves.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.gateway.config import (
|
|
LEGACY_DEFAULT_LLM_BASE_URL,
|
|
LEGACY_DEFAULT_LLM_MODEL,
|
|
LEGACY_DEFAULT_LLM_PROVIDER,
|
|
GatewayConfig,
|
|
_default_tiers,
|
|
)
|
|
|
|
TOKENRHYTHM_BASE_URL = "https://tokenrhythm.studio/v1"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Provider resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_keyless_default_resolves_tokenrhythm() -> None:
|
|
cfg = GatewayConfig()
|
|
assert cfg.llm.provider == "tokenrhythm"
|
|
assert cfg.llm.model == "deepseek-v4-pro"
|
|
assert cfg.llm.base_url == TOKENRHYTHM_BASE_URL
|
|
|
|
|
|
def test_openrouter_env_key_alone_keeps_legacy_default(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
|
cfg = GatewayConfig()
|
|
assert cfg.llm.provider == LEGACY_DEFAULT_LLM_PROVIDER
|
|
assert cfg.llm.model == LEGACY_DEFAULT_LLM_MODEL
|
|
assert cfg.llm.base_url == LEGACY_DEFAULT_LLM_BASE_URL
|
|
# The legacy default keeps the packaged openrouter ladder untouched.
|
|
assert cfg.squilla_router.tiers == _default_tiers()
|
|
|
|
|
|
def test_tokenrhythm_env_key_resolves_tokenrhythm(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("TOKENRHYTHM_API_KEY", "sk_tr_test")
|
|
cfg = GatewayConfig()
|
|
assert cfg.llm.provider == "tokenrhythm"
|
|
|
|
|
|
def test_both_env_keys_prefer_tokenrhythm(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
|
monkeypatch.setenv("TOKENRHYTHM_API_KEY", "sk_tr_test")
|
|
cfg = GatewayConfig()
|
|
assert cfg.llm.provider == "tokenrhythm"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"llm_overrides",
|
|
[
|
|
{"api_key": "sk-or-raw"},
|
|
{"api_key_env": "MY_OPENROUTER_KEY"},
|
|
{"model": "anthropic/claude-sonnet-5"},
|
|
{"base_url": "https://openrouter.ai/api/v1"},
|
|
],
|
|
)
|
|
def test_provider_coupled_fields_without_provider_mean_openrouter(
|
|
llm_overrides: dict,
|
|
) -> None:
|
|
# These fields were authored against the pre-tokenrhythm default and must
|
|
# keep binding to openrouter when the provider was never named.
|
|
cfg = GatewayConfig(llm=llm_overrides)
|
|
assert cfg.llm.provider == LEGACY_DEFAULT_LLM_PROVIDER
|
|
if "model" not in llm_overrides:
|
|
assert cfg.llm.model == LEGACY_DEFAULT_LLM_MODEL
|
|
else:
|
|
assert cfg.llm.model == llm_overrides["model"]
|
|
if "base_url" not in llm_overrides:
|
|
assert cfg.llm.base_url == LEGACY_DEFAULT_LLM_BASE_URL
|
|
|
|
|
|
def test_openrouter_tier_profile_without_provider_means_openrouter() -> None:
|
|
# tier_profile must match the provider, so a persisted openrouter profile
|
|
# from the pre-tokenrhythm era must keep loading (not raise).
|
|
cfg = GatewayConfig(squilla_router={"tier_profile": "openrouter"})
|
|
assert cfg.llm.provider == LEGACY_DEFAULT_LLM_PROVIDER
|
|
assert cfg.squilla_router.tier_profile == "openrouter"
|
|
|
|
|
|
def test_explicit_openrouter_backfills_legacy_model_and_base_url() -> None:
|
|
# provider = "openrouter" written while model/base_url fell back to the
|
|
# old field defaults must keep meaning those defaults.
|
|
cfg = GatewayConfig(llm={"provider": "openrouter", "api_key": "x"})
|
|
assert cfg.llm.model == LEGACY_DEFAULT_LLM_MODEL
|
|
assert cfg.llm.base_url == LEGACY_DEFAULT_LLM_BASE_URL
|
|
|
|
|
|
def test_explicit_openrouter_keeps_explicit_model_and_base_url() -> None:
|
|
cfg = GatewayConfig(
|
|
llm={
|
|
"provider": "openrouter",
|
|
"model": "z-ai/glm-5.2",
|
|
"base_url": "https://proxy.example/v1",
|
|
}
|
|
)
|
|
assert cfg.llm.model == "z-ai/glm-5.2"
|
|
assert cfg.llm.base_url == "https://proxy.example/v1"
|
|
|
|
|
|
def test_explicit_other_provider_is_untouched(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
assert cfg.llm.provider == "deepseek"
|
|
assert cfg.llm.model == "deepseek-chat"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Default router ladder under the tokenrhythm default
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_tokenrhythm_default_binds_router_tiers_to_tokenrhythm() -> None:
|
|
cfg = GatewayConfig()
|
|
tiers = cfg.squilla_router.tiers
|
|
expected_models = {
|
|
"c0": "deepseek-v4-flash",
|
|
"c1": "deepseek-v4-pro",
|
|
"c2": "kimi-k2.7-code",
|
|
"c3": "glm-5.2",
|
|
"image_model": "kimi-k2.6",
|
|
}
|
|
for name, model in expected_models.items():
|
|
assert tiers[name]["provider"] == "tokenrhythm"
|
|
assert tiers[name]["model"] == model
|
|
assert cfg.squilla_router.tier_profile is None
|
|
|
|
|
|
def test_tokenrhythm_curated_ladder_is_not_rebound_to_the_direct_model() -> None:
|
|
# The curated preset is fully model-bound, so an explicit llm.model stays
|
|
# the direct/fallback model only — the same contract as the legacy-nine
|
|
# packaged profiles.
|
|
cfg = GatewayConfig(llm={"provider": "tokenrhythm", "model": "glm-5.2"})
|
|
assert cfg.llm.model == "glm-5.2"
|
|
assert cfg.squilla_router.tiers["c1"]["model"] == "deepseek-v4-pro"
|
|
|
|
|
|
def test_tokenrhythm_custom_tiers_are_preserved() -> None:
|
|
custom = {"c1": {"provider": "tokenrhythm", "model": "kimi-k2.6"}}
|
|
cfg = GatewayConfig(llm={"provider": "tokenrhythm"}, squilla_router={"tiers": custom})
|
|
assert cfg.squilla_router.tiers["c1"]["model"] == "kimi-k2.6"
|
|
|
|
|
|
def test_tokenrhythm_disabled_router_keeps_default_tiers() -> None:
|
|
cfg = GatewayConfig(squilla_router={"enabled": False})
|
|
assert cfg.squilla_router.tiers == _default_tiers()
|
|
|
|
|
|
def test_legacy_nine_auto_profile_still_applies() -> None:
|
|
# The direct-provider auto tier_profile default must survive the new
|
|
# validators running ahead of it.
|
|
cfg = GatewayConfig(llm={"provider": "deepseek", "model": "deepseek-chat"})
|
|
assert cfg.squilla_router.tier_profile == "deepseek"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resolution must not be baked into config.toml
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolved_legacy_provider_is_not_persisted(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
import tomllib
|
|
|
|
from opensquilla.onboarding.config_store import load_config, persist_config
|
|
|
|
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
|
target = tmp_path / "config.toml"
|
|
target.write_text('log_level = "INFO"\n', encoding="utf-8")
|
|
|
|
cfg = load_config(target)
|
|
assert cfg.llm.provider == LEGACY_DEFAULT_LLM_PROVIDER
|
|
|
|
cfg.log_level = "DEBUG"
|
|
persist_config(cfg, path=target, backup=False)
|
|
|
|
data = tomllib.loads(target.read_text(encoding="utf-8"))
|
|
assert data["log_level"] == "DEBUG"
|
|
assert "llm" not in data
|
|
assert "squilla_router" not in data
|
|
|
|
|
|
def test_resolved_tokenrhythm_tiers_are_not_persisted(tmp_path: Path) -> None:
|
|
import tomllib
|
|
|
|
from opensquilla.onboarding.config_store import load_config, persist_config
|
|
|
|
target = tmp_path / "config.toml"
|
|
target.write_text('log_level = "INFO"\n', encoding="utf-8")
|
|
|
|
cfg = load_config(target)
|
|
assert cfg.squilla_router.tiers["c1"]["provider"] == "tokenrhythm"
|
|
|
|
cfg.log_level = "DEBUG"
|
|
persist_config(cfg, path=target, backup=False)
|
|
|
|
data = tomllib.loads(target.read_text(encoding="utf-8"))
|
|
assert "squilla_router" not in data
|
|
assert "llm" not in data
|