File size: 8,107 Bytes
2d76892
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
193eb98
2d76892
11f019c
2d76892
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
193eb98
2d76892
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
Tests des endpoints GET /api/v1/providers et GET /api/v1/providers/{type}/models.

Stratégie :
  - get_available_providers() et list_models_for_provider() mockés via monkeypatch
    sur models_api_module (nom local après import from … import …)
  - BDD SQLite en mémoire (fixture async_client)

Vérifie :
- GET /api/v1/providers → liste de providers avec available + model_count
- GET /api/v1/providers/{provider_type}/models → 200 ou 404/503
- Comportement avec 0, 1 ou plusieurs providers disponibles
"""
# 1. stdlib
from datetime import datetime, timezone

# 2. third-party
import pytest

# 3. local
import app.api.v1.models_api as models_api_module
from app.schemas.model_config import ModelInfo, ProviderType
from tests.conftest_api import async_client, db_session  # noqa: F401

_NOW = datetime.now(timezone.utc)

_PROVIDERS_ALL_UNAVAILABLE = [
    {"provider_type": "google_ai_studio", "display_name": "Google AI Studio", "available": False, "model_count": 0},
    {"provider_type": "vertex_service_account", "display_name": "Vertex AI (compte de service)", "available": False, "model_count": 0},
    {"provider_type": "mistral", "display_name": "Mistral AI", "available": False, "model_count": 0},
]

_PROVIDERS_GOOGLE_ONLY = [
    {"provider_type": "google_ai_studio", "display_name": "Google AI Studio", "available": True, "model_count": 2},
    {"provider_type": "vertex_service_account", "display_name": "Vertex AI (compte de service)", "available": False, "model_count": 0},
    {"provider_type": "mistral", "display_name": "Mistral AI", "available": False, "model_count": 0},
]

_PROVIDERS_GOOGLE_AND_MISTRAL = [
    {"provider_type": "google_ai_studio", "display_name": "Google AI Studio", "available": True, "model_count": 3},
    {"provider_type": "vertex_service_account", "display_name": "Vertex AI (compte de service)", "available": False, "model_count": 0},
    {"provider_type": "mistral", "display_name": "Mistral AI", "available": True, "model_count": 2},
]

_MOCK_GOOGLE_MODELS = [
    ModelInfo(
        model_id="gemini-2.0-flash",
        display_name="Gemini 2.0 Flash",
        provider=ProviderType.GOOGLE_AI_STUDIO,
        supports_vision=True,
        input_token_limit=1_000_000,
        output_token_limit=8192,
    ),
    ModelInfo(
        model_id="gemini-1.5-pro",
        display_name="Gemini 1.5 Pro",
        provider=ProviderType.GOOGLE_AI_STUDIO,
        supports_vision=True,
        input_token_limit=2_000_000,
        output_token_limit=8192,
    ),
]

_MOCK_MISTRAL_MODELS = [
    ModelInfo(
        model_id="pixtral-large-latest",
        display_name="Pixtral Large",
        provider=ProviderType.MISTRAL,
        supports_vision=True,
        input_token_limit=128_000,
        output_token_limit=None,
    ),
    ModelInfo(
        model_id="pixtral-12b-2409",
        display_name="Pixtral 12B",
        provider=ProviderType.MISTRAL,
        supports_vision=True,
        input_token_limit=128_000,
        output_token_limit=None,
    ),
]


# ---------------------------------------------------------------------------
# GET /api/v1/providers
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_list_providers_returns_list(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_ALL_UNAVAILABLE)
    resp = await async_client.get("/api/v1/providers")
    assert resp.status_code == 200
    assert isinstance(resp.json(), list)


@pytest.mark.asyncio
async def test_list_providers_count(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_ALL_UNAVAILABLE)
    data = (await async_client.get("/api/v1/providers")).json()
    assert len(data) == 3  # 3 providers connus


@pytest.mark.asyncio
async def test_list_providers_fields(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_ALL_UNAVAILABLE)
    data = (await async_client.get("/api/v1/providers")).json()
    p = data[0]
    assert "provider_type" in p
    assert "display_name" in p
    assert "available" in p
    assert "model_count" in p


@pytest.mark.asyncio
async def test_list_providers_all_unavailable(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_ALL_UNAVAILABLE)
    data = (await async_client.get("/api/v1/providers")).json()
    assert all(not p["available"] for p in data)
    assert all(p["model_count"] == 0 for p in data)


@pytest.mark.asyncio
async def test_list_providers_google_available(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_GOOGLE_ONLY)
    data = (await async_client.get("/api/v1/providers")).json()
    google = next(p for p in data if p["provider_type"] == "google_ai_studio")
    assert google["available"] is True
    assert google["model_count"] == 2


@pytest.mark.asyncio
async def test_list_providers_mistral_available(async_client, monkeypatch):
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_GOOGLE_AND_MISTRAL)
    data = (await async_client.get("/api/v1/providers")).json()
    mistral = next(p for p in data if p["provider_type"] == "mistral")
    assert mistral["available"] is True
    assert mistral["model_count"] == 2


@pytest.mark.asyncio
async def test_list_providers_includes_mistral_type(async_client, monkeypatch):
    """Mistral est toujours dans la liste même si indisponible."""
    monkeypatch.setattr("app.services.ai.model_registry.get_available_providers", lambda: _PROVIDERS_ALL_UNAVAILABLE)
    data = (await async_client.get("/api/v1/providers")).json()
    types_ = [p["provider_type"] for p in data]
    assert "mistral" in types_


# ---------------------------------------------------------------------------
# GET /api/v1/providers/{provider_type}/models
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_get_provider_models_google(async_client, monkeypatch):
    monkeypatch.setattr(
        "app.services.ai.model_registry.list_models_for_provider", lambda ptype: _MOCK_GOOGLE_MODELS
    )
    resp = await async_client.get("/api/v1/providers/google_ai_studio/models")
    assert resp.status_code == 200
    assert len(resp.json()) == 2


@pytest.mark.asyncio
async def test_get_provider_models_mistral(async_client, monkeypatch):
    monkeypatch.setattr(
        "app.services.ai.model_registry.list_models_for_provider", lambda ptype: _MOCK_MISTRAL_MODELS
    )
    resp = await async_client.get("/api/v1/providers/mistral/models")
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 2
    ids = [m["model_id"] for m in data]
    assert "pixtral-large-latest" in ids
    assert "pixtral-12b-2409" in ids


@pytest.mark.asyncio
async def test_get_provider_models_unknown_provider(async_client):
    resp = await async_client.get("/api/v1/providers/openai/models")
    assert resp.status_code == 404


@pytest.mark.asyncio
async def test_get_provider_models_not_configured(async_client, monkeypatch):
    """Provider connu mais clé absente → 503."""
    def _raise(ptype):
        raise RuntimeError("Variable d'environnement manquante : MISTRAL_API_KEY")

    monkeypatch.setattr("app.services.ai.model_registry.list_models_for_provider", _raise)
    resp = await async_client.get("/api/v1/providers/mistral/models")
    assert resp.status_code == 503


@pytest.mark.asyncio
async def test_get_provider_models_fields(async_client, monkeypatch):
    monkeypatch.setattr(
        "app.services.ai.model_registry.list_models_for_provider", lambda ptype: _MOCK_MISTRAL_MODELS
    )
    data = (await async_client.get("/api/v1/providers/mistral/models")).json()
    m = data[0]
    assert "model_id" in m
    assert "display_name" in m
    assert "provider" in m
    assert "supports_vision" in m