test(openrouter): cover pool filter, per-model tier, legacy config warnings

This commit is contained in:
Anish Sarkar 2026-05-01 17:42:54 +05:30
parent 925c33abd1
commit 2019e90a04
3 changed files with 561 additions and 0 deletions

View file

@ -0,0 +1,215 @@
"""LLMRouterService pool-filter / rebuild tests.
These tests focus on the *config plumbing* (which configs enter the router
pool, rebuild resets state correctly). They stub out the underlying
``litellm.Router`` so we don't need real API keys or network access.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from app.services.llm_router_service import LLMRouterService
pytestmark = pytest.mark.unit
def _fake_yaml_config(
*,
id: int,
model_name: str,
billing_tier: str = "free",
) -> dict:
return {
"id": id,
"name": f"yaml-{id}",
"provider": "OPENAI",
"model_name": model_name,
"api_key": "sk-test",
"api_base": "",
"billing_tier": billing_tier,
"rpm": 100,
"tpm": 100_000,
"litellm_params": {},
}
def _fake_openrouter_config(
*,
id: int,
model_name: str,
billing_tier: str,
router_pool_eligible: bool | None = None,
) -> dict:
"""Build a synthetic dynamic-OR config dict for router-pool tests.
Defaults mirror Strategy 3: premium OR enters the pool, free OR stays
out. Callers can override ``router_pool_eligible`` to simulate legacy
configs or to regression-test the filter mechanics directly.
"""
if router_pool_eligible is None:
router_pool_eligible = billing_tier == "premium"
return {
"id": id,
"name": f"or-{id}",
"provider": "OPENROUTER",
"model_name": model_name,
"api_key": "sk-or-test",
"api_base": "",
"billing_tier": billing_tier,
"rpm": 20 if billing_tier == "free" else 200,
"tpm": 100_000 if billing_tier == "free" else 1_000_000,
"litellm_params": {},
"router_pool_eligible": router_pool_eligible,
}
def _reset_router_singleton() -> None:
instance = LLMRouterService.get_instance()
instance._initialized = False
instance._router = None
instance._model_list = []
instance._premium_model_strings = set()
def test_router_pool_includes_or_premium_excludes_or_free():
"""Strategy 3: premium OR joins the pool, free OR stays out.
Dynamic OpenRouter premium entries opt into load balancing alongside
curated YAML configs. Dynamic OR free entries are intentionally kept
out because OpenRouter's free tier enforces a single account-global
quota bucket that per-deployment router accounting can't represent.
"""
_reset_router_singleton()
configs = [
_fake_yaml_config(id=-1, model_name="gpt-4o", billing_tier="premium"),
_fake_yaml_config(id=-2, model_name="gpt-4o-mini", billing_tier="free"),
_fake_openrouter_config(
id=-10_001, model_name="openai/gpt-4o", billing_tier="premium"
),
_fake_openrouter_config(
id=-10_002,
model_name="meta-llama/llama-3.3-70b:free",
billing_tier="free",
),
]
with patch("app.services.llm_router_service.Router") as mock_router, patch(
"app.services.llm_router_service.LLMRouterService._build_context_fallback_groups"
) as mock_ctx_fb:
mock_ctx_fb.side_effect = lambda ml: (ml, None)
mock_router.return_value = object()
LLMRouterService.initialize(configs)
pool_models = {
dep["litellm_params"]["model"]
for dep in LLMRouterService.get_instance()._model_list
}
# YAML premium + YAML free + dynamic OR premium are all in the pool.
# Dynamic OR free is NOT (shared-bucket rate limits can't be load-balanced).
assert pool_models == {
"openai/gpt-4o",
"openai/gpt-4o-mini",
"openrouter/openai/gpt-4o",
}
prem = LLMRouterService.get_instance()._premium_model_strings
# YAML premium is fingerprinted under both its model_string and its
# ``base_model`` form (existing behavior we don't want to regress).
assert "openai/gpt-4o" in prem
# Dynamic OR premium is now fingerprinted as premium so pool-level
# calls through the router are billed against premium quota.
assert "openrouter/openai/gpt-4o" in prem
assert LLMRouterService.is_premium_model("openrouter/openai/gpt-4o") is True
# Dynamic OR free never enters the pool, so it's never counted as premium.
assert LLMRouterService.is_premium_model(
"openrouter/meta-llama/llama-3.3-70b:free"
) is False
def test_router_pool_filter_mechanics_respect_override():
"""The ``router_pool_eligible`` filter itself works independently of tier.
Regression guard: if a future refactor ever sets the flag False on a
premium config (e.g. for maintenance), that config MUST be skipped by
``initialize`` even though its tier is premium.
"""
_reset_router_singleton()
configs = [
_fake_yaml_config(id=-1, model_name="gpt-4o", billing_tier="premium"),
_fake_openrouter_config(
id=-10_001,
model_name="openai/gpt-4o",
billing_tier="premium",
router_pool_eligible=False, # opt out despite being premium
),
]
with patch("app.services.llm_router_service.Router") as mock_router, patch(
"app.services.llm_router_service.LLMRouterService._build_context_fallback_groups"
) as mock_ctx_fb:
mock_ctx_fb.side_effect = lambda ml: (ml, None)
mock_router.return_value = object()
LLMRouterService.initialize(configs)
pool_models = {
dep["litellm_params"]["model"]
for dep in LLMRouterService.get_instance()._model_list
}
assert pool_models == {"openai/gpt-4o"}
assert LLMRouterService.is_premium_model("openrouter/openai/gpt-4o") is False
def test_rebuild_refreshes_pool_after_configs_change():
_reset_router_singleton()
configs_v1 = [
_fake_yaml_config(id=-1, model_name="gpt-4o", billing_tier="premium"),
]
configs_v2 = configs_v1 + [
_fake_yaml_config(id=-2, model_name="gpt-4o-mini", billing_tier="free"),
]
with patch("app.services.llm_router_service.Router") as mock_router, patch(
"app.services.llm_router_service.LLMRouterService._build_context_fallback_groups"
) as mock_ctx_fb:
mock_ctx_fb.side_effect = lambda ml: (ml, None)
mock_router.return_value = object()
LLMRouterService.initialize(configs_v1)
assert len(LLMRouterService.get_instance()._model_list) == 1
# ``initialize`` should be a no-op here (already initialized).
LLMRouterService.initialize(configs_v2)
assert len(LLMRouterService.get_instance()._model_list) == 1
# ``rebuild`` must clear the guard and re-run with the new configs.
LLMRouterService.rebuild(configs_v2)
assert len(LLMRouterService.get_instance()._model_list) == 2
def test_auto_model_pin_candidates_include_dynamic_openrouter():
"""Dynamic OR configs must remain Auto-mode thread-pin candidates.
Guards against a future regression where someone adds the
``router_pool_eligible`` filter to ``auto_model_pin_service._global_candidates``.
"""
from app.config import config
from app.services.auto_model_pin_service import _global_candidates
or_premium = _fake_openrouter_config(
id=-10_001, model_name="openai/gpt-4o", billing_tier="premium"
)
or_free = _fake_openrouter_config(
id=-10_002,
model_name="meta-llama/llama-3.3-70b:free",
billing_tier="free",
)
original = config.GLOBAL_LLM_CONFIGS
try:
config.GLOBAL_LLM_CONFIGS = [or_premium, or_free]
candidate_ids = {c["id"] for c in _global_candidates()}
assert candidate_ids == {-10_001, -10_002}
finally:
config.GLOBAL_LLM_CONFIGS = original

View file

@ -0,0 +1,236 @@
"""Unit tests for the dynamic OpenRouter integration."""
from __future__ import annotations
import pytest
from app.services.openrouter_integration_service import (
_FREE_ROUTER_ID,
_OPENROUTER_DYNAMIC_MARKER,
_build_free_router_config,
_generate_configs,
_openrouter_tier,
_stable_config_id,
)
pytestmark = pytest.mark.unit
def _minimal_openrouter_model(
*,
model_id: str,
pricing: dict | None = None,
name: str | None = None,
) -> dict:
"""Return a synthetic OpenRouter /api/v1/models entry.
The real API payload includes a lot of fields; we only populate what
``_generate_configs`` actually inspects (architecture, tool support,
context, pricing, id).
"""
return {
"id": model_id,
"name": name or model_id,
"architecture": {"output_modalities": ["text"]},
"supported_parameters": ["tools"],
"context_length": 200_000,
"pricing": pricing or {"prompt": "0.000003", "completion": "0.000015"},
}
# ---------------------------------------------------------------------------
# _openrouter_tier
# ---------------------------------------------------------------------------
def test_openrouter_tier_free_suffix():
assert _openrouter_tier({"id": "foo/bar:free"}) == "free"
def test_openrouter_tier_zero_pricing():
model = {
"id": "foo/bar",
"pricing": {"prompt": "0", "completion": "0"},
}
assert _openrouter_tier(model) == "free"
def test_openrouter_tier_paid():
model = {
"id": "foo/bar",
"pricing": {"prompt": "0.000003", "completion": "0.000015"},
}
assert _openrouter_tier(model) == "premium"
def test_openrouter_tier_missing_pricing_is_premium():
assert _openrouter_tier({"id": "foo/bar"}) == "premium"
assert _openrouter_tier({"id": "foo/bar", "pricing": {}}) == "premium"
# ---------------------------------------------------------------------------
# _stable_config_id
# ---------------------------------------------------------------------------
def test_stable_config_id_deterministic():
taken1: set[int] = set()
taken2: set[int] = set()
a = _stable_config_id("openai/gpt-4o", -10_000, taken1)
b = _stable_config_id("openai/gpt-4o", -10_000, taken2)
assert a == b
assert a < 0
def test_stable_config_id_collision_decrements():
"""When two model_ids hash to the same slot, the second should decrement."""
taken: set[int] = set()
a = _stable_config_id("openai/gpt-4o", -10_000, taken)
# Force a collision by pre-populating ``taken`` with a slot we know will be
# picked.
taken_forced = {a}
b = _stable_config_id("openai/gpt-4o", -10_000, taken_forced)
assert b != a
assert b == a - 1
assert b in taken_forced
def test_stable_config_id_different_models_different_ids():
taken: set[int] = set()
ids = {
_stable_config_id("openai/gpt-4o", -10_000, taken),
_stable_config_id("anthropic/claude-3.5-sonnet", -10_000, taken),
_stable_config_id("google/gemini-2.0-flash", -10_000, taken),
}
assert len(ids) == 3
def test_stable_config_id_survives_catalogue_churn():
"""Removing a model should not shift other models' IDs (the bug we fix)."""
taken1: set[int] = set()
id_a1 = _stable_config_id("openai/gpt-4o", -10_000, taken1)
_ = _stable_config_id("anthropic/claude-3-haiku", -10_000, taken1)
id_c1 = _stable_config_id("google/gemini-2.0-flash", -10_000, taken1)
taken2: set[int] = set()
id_a2 = _stable_config_id("openai/gpt-4o", -10_000, taken2)
id_c2 = _stable_config_id("google/gemini-2.0-flash", -10_000, taken2)
assert id_a1 == id_a2
assert id_c1 == id_c2
# ---------------------------------------------------------------------------
# _generate_configs
# ---------------------------------------------------------------------------
_SETTINGS_BASE: dict = {
"api_key": "sk-or-test",
"id_offset": -10_000,
"rpm": 200,
"tpm": 1_000_000,
"free_rpm": 20,
"free_tpm": 100_000,
"anonymous_enabled_paid": False,
"anonymous_enabled_free": True,
"quota_reserve_tokens": 4000,
"free_router_enabled": False,
}
def test_generate_configs_respects_tier():
"""Premium OR models opt into the router pool; free OR models stay out.
Strategy-3 split: premium participates in LiteLLM Router load balancing,
free stays excluded because OpenRouter enforces a shared global free-tier
bucket that per-deployment router accounting can't represent.
"""
raw = [
_minimal_openrouter_model(model_id="openai/gpt-4o"),
_minimal_openrouter_model(
model_id="meta-llama/llama-3.3-70b-instruct:free",
pricing={"prompt": "0", "completion": "0"},
),
]
cfgs = _generate_configs(raw, dict(_SETTINGS_BASE))
by_model = {c["model_name"]: c for c in cfgs}
paid = by_model["openai/gpt-4o"]
assert paid["billing_tier"] == "premium"
assert paid["rpm"] == 200
assert paid["tpm"] == 1_000_000
assert paid["anonymous_enabled"] is False
assert paid["router_pool_eligible"] is True
assert paid[_OPENROUTER_DYNAMIC_MARKER] is True
free = by_model["meta-llama/llama-3.3-70b-instruct:free"]
assert free["billing_tier"] == "free"
assert free["rpm"] == 20
assert free["tpm"] == 100_000
assert free["anonymous_enabled"] is True
assert free["router_pool_eligible"] is False
def test_generate_configs_includes_free_router_when_enabled():
raw = [_minimal_openrouter_model(model_id="openai/gpt-4o")]
settings = {**_SETTINGS_BASE, "free_router_enabled": True}
cfgs = _generate_configs(raw, settings)
free_router = next(
(c for c in cfgs if c["model_name"] == "openrouter/free"), None
)
assert free_router is not None
assert free_router["id"] == _FREE_ROUTER_ID
assert free_router["billing_tier"] == "free"
assert free_router["router_pool_eligible"] is False
assert free_router["anonymous_enabled"] is True
def test_generate_configs_excludes_free_router_when_disabled():
raw = [_minimal_openrouter_model(model_id="openai/gpt-4o")]
settings = {**_SETTINGS_BASE, "free_router_enabled": False}
cfgs = _generate_configs(raw, settings)
assert not any(c["model_name"] == "openrouter/free" for c in cfgs)
def test_generate_configs_excludes_free_router_without_api_key():
"""Without an API key the free-router entry is useless; skip it."""
raw = [_minimal_openrouter_model(model_id="openai/gpt-4o")]
settings = {**_SETTINGS_BASE, "free_router_enabled": True, "api_key": ""}
cfgs = _generate_configs(raw, settings)
assert not any(c["model_name"] == "openrouter/free" for c in cfgs)
def test_generate_configs_drops_non_text_and_non_tool_models():
raw = [
_minimal_openrouter_model(model_id="openai/gpt-4o"),
{ # image-output model
"id": "openai/dall-e",
"architecture": {"output_modalities": ["image"]},
"supported_parameters": ["tools"],
"context_length": 200_000,
"pricing": {"prompt": "0.01", "completion": "0.01"},
},
{ # text but no tool calling
"id": "openai/completion-only",
"architecture": {"output_modalities": ["text"]},
"supported_parameters": [],
"context_length": 200_000,
"pricing": {"prompt": "0.01", "completion": "0.01"},
},
]
cfgs = _generate_configs(raw, dict(_SETTINGS_BASE))
model_names = [c["model_name"] for c in cfgs]
assert "openai/gpt-4o" in model_names
assert "openai/dall-e" not in model_names
assert "openai/completion-only" not in model_names
def test_build_free_router_config_shape():
cfg = _build_free_router_config(dict(_SETTINGS_BASE))
assert cfg["provider"] == "OPENROUTER"
assert cfg["model_name"] == "openrouter/free"
assert cfg["id"] == _FREE_ROUTER_ID
assert cfg["billing_tier"] == "free"
assert cfg["router_pool_eligible"] is False
assert cfg[_OPENROUTER_DYNAMIC_MARKER] is True

View file

@ -0,0 +1,110 @@
"""Tests for deprecated-key warnings and back-compat in
``load_openrouter_integration_settings``.
"""
from __future__ import annotations
from pathlib import Path
import pytest
pytestmark = pytest.mark.unit
def _write_yaml(tmp_path: Path, body: str) -> Path:
cfg_dir = tmp_path / "app" / "config"
cfg_dir.mkdir(parents=True)
cfg_path = cfg_dir / "global_llm_config.yaml"
cfg_path.write_text(body, encoding="utf-8")
return cfg_path
def _patch_base_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
from app import config as config_module
monkeypatch.setattr(config_module, "BASE_DIR", tmp_path)
def test_legacy_billing_tier_emits_warning(monkeypatch, tmp_path, capsys):
_write_yaml(
tmp_path,
"""
openrouter_integration:
enabled: true
api_key: "sk-or-test"
billing_tier: "premium"
""".lstrip(),
)
_patch_base_dir(monkeypatch, tmp_path)
from app.config import load_openrouter_integration_settings
settings = load_openrouter_integration_settings()
captured = capsys.readouterr().out
assert settings is not None
assert "billing_tier is deprecated" in captured
def test_legacy_anonymous_enabled_back_compat(monkeypatch, tmp_path, capsys):
_write_yaml(
tmp_path,
"""
openrouter_integration:
enabled: true
api_key: "sk-or-test"
anonymous_enabled: true
""".lstrip(),
)
_patch_base_dir(monkeypatch, tmp_path)
from app.config import load_openrouter_integration_settings
settings = load_openrouter_integration_settings()
captured = capsys.readouterr().out
assert settings is not None
assert settings["anonymous_enabled_paid"] is True
assert settings["anonymous_enabled_free"] is True
assert "anonymous_enabled is" in captured
assert "deprecated" in captured
def test_new_keys_take_priority_over_legacy_back_compat(
monkeypatch, tmp_path, capsys
):
"""If both legacy and new keys are present, new keys win (setdefault)."""
_write_yaml(
tmp_path,
"""
openrouter_integration:
enabled: true
api_key: "sk-or-test"
anonymous_enabled: true
anonymous_enabled_paid: false
anonymous_enabled_free: false
""".lstrip(),
)
_patch_base_dir(monkeypatch, tmp_path)
from app.config import load_openrouter_integration_settings
settings = load_openrouter_integration_settings()
capsys.readouterr()
assert settings is not None
assert settings["anonymous_enabled_paid"] is False
assert settings["anonymous_enabled_free"] is False
def test_disabled_integration_returns_none(monkeypatch, tmp_path):
_write_yaml(
tmp_path,
"""
openrouter_integration:
enabled: false
api_key: "sk-or-test"
""".lstrip(),
)
_patch_base_dir(monkeypatch, tmp_path)
from app.config import load_openrouter_integration_settings
assert load_openrouter_integration_settings() is None