mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
test(openrouter): cover pool filter, per-model tier, legacy config warnings
This commit is contained in:
parent
925c33abd1
commit
2019e90a04
3 changed files with 561 additions and 0 deletions
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue