""" Routage réel des modèles par palier IA (spec « Stratégie LLM par abonnement »). - PLANS expose ai_models_essential / ai_models_premium (gamme officielle). - Un plan Pro (moteur openrouter) résout toujours un modèle Essentielle, jamais un modèle Premium. - Le défaut admin (ai_tiers..default_model) prime sur le défaut du plan, sans redéploiement. """ import pytest from pathlib import Path from fastapi.testclient import TestClient from models.subscription import ( DEFAULT_AI_MODELS_ESSENTIAL, DEFAULT_AI_MODELS_PREMIUM, PLANS, PlanType, ) from routes.translate_routes import ( _allowed_providers_for_plan, _plan_ai_models, _resolve_tier_model, ) @pytest.fixture def client(monkeypatch, tmp_path: Path): """TestClient with JSON auth and rate limiting disabled.""" import services.auth_service as auth_svc from middleware.rate_limiting import RateLimitManager monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json") monkeypatch.setattr(auth_svc, "USE_DATABASE", False) monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False) async def _check_request_allow(self, request): return True, "ok", "test" async def _check_translation_allow(self, request, file_size_mb=0): return True, "ok" monkeypatch.setattr(RateLimitManager, "check_request", _check_request_allow) monkeypatch.setattr(RateLimitManager, "check_translation", _check_translation_allow) from main import app return TestClient(app, raise_server_exceptions=True) def _settings(ai_tiers: dict | None = None): from routes.admin_routes import SettingsConfig if ai_tiers is None: return SettingsConfig() return SettingsConfig.model_validate({"ai_tiers": ai_tiers}) # --------------------------------------------------------------------------- # Gamme officielle dans PLANS # --------------------------------------------------------------------------- def test_plans_expose_official_model_ranges(): essential = PLANS[PlanType.PRO]["ai_models_essential"] assert essential == [ "deepseek/deepseek-v4-flash", "z-ai/glm-5.3-flash", "minimax/minimax-m3", ] business = PLANS[PlanType.BUSINESS] assert business["ai_models_essential"] == essential assert business["ai_models_premium"] == [ "anthropic/claude-sonnet-5", "deepseek/deepseek-v4-pro", "z-ai/glm-5.3", ] def test_old_single_model_keys_removed(): assert "ai_model_essential" not in PLANS[PlanType.PRO] assert "ai_model_essential" not in PLANS[PlanType.BUSINESS] assert "ai_model_premium" not in PLANS[PlanType.BUSINESS] # --------------------------------------------------------------------------- # Résolution par palier # --------------------------------------------------------------------------- def test_pro_resolves_essential_plan_default(): assert _resolve_tier_model(_settings(), "pro", premium=False) == ( "deepseek/deepseek-v4-flash" ) def test_business_resolves_premium_plan_default(): assert _resolve_tier_model(_settings(), "business", premium=True) == ( "anthropic/claude-sonnet-5" ) def test_admin_tier_default_overrides_plan_default(): cfg = _settings( {"essential": {"models": [], "default_model": "z-ai/glm-5.3-flash"}} ) assert _resolve_tier_model(cfg, "pro", premium=False) == "z-ai/glm-5.3-flash" def test_admin_tier_models_first_used_when_default_empty(): cfg = _settings( { "essential": { "models": ["minimax/minimax-m3", "deepseek/deepseek-v4-flash"], "default_model": "", } } ) assert _resolve_tier_model(cfg, "pro", premium=False) == "minimax/minimax-m3" def test_pro_never_resolves_premium_model(): cfg = _settings( { "essential": {"models": [], "default_model": ""}, "premium": {"models": [], "default_model": ""}, } ) for plan_str in ("pro", "PlanType.PRO"): model = _resolve_tier_model(cfg, plan_str, premium=False) assert model in DEFAULT_AI_MODELS_ESSENTIAL assert model not in DEFAULT_AI_MODELS_PREMIUM def test_pro_plan_does_not_allow_premium_provider(): allowed = _allowed_providers_for_plan(PlanType.PRO) assert "openrouter" in allowed assert "openrouter_premium" not in allowed def test_plan_fallback_for_plans_without_range(): # Free/Enterprise n'ont pas de liste : retombée sur la gamme officielle. assert _plan_ai_models(PlanType.FREE, premium=False) == DEFAULT_AI_MODELS_ESSENTIAL assert _plan_ai_models(PlanType.ENTERPRISE, premium=True) == DEFAULT_AI_MODELS_PREMIUM # --------------------------------------------------------------------------- # GET /plans expose les gammes (affichage public) # --------------------------------------------------------------------------- def test_get_plans_exposes_model_lists(client): r = client.get("/api/v1/auth/plans") assert r.status_code == 200 plans = {p["id"]: p for p in r.json()["data"]["plans"]} assert plans["pro"]["ai_models_essential"] == [ "deepseek/deepseek-v4-flash", "z-ai/glm-5.3-flash", "minimax/minimax-m3", ] assert plans["business"]["ai_models_premium"][0] == "anthropic/claude-sonnet-5" assert "ai_model_essential" not in plans["pro"]