mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +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