Spaces:
Build error
Build error
Claude commited on
feat: supprimer la saisie de clé API dans l'UI — les clés viennent des secrets HF
Browse files- Supprime POST /api/v1/settings/api-key, ApiKeyRequest, ApiKeyResponse,
et _validate_api_key (inutiles : clés dans variables d'env, R06)
- GET /api/v1/models utilise directement les credentials de settings
- Admin.tsx/ModelSection : supprime le champ mot de passe et le bouton
« Vérifier » ; charge la liste des modèles au montage automatiquement ;
affiche un message explicite si aucun modèle n'est disponible
- api.ts : supprime validateApiKey() et l'interface ApiKeyResponse
- Tests : remplace les tests api-key par un test vérifiant que l'endpoint
supprimé retourne bien 404/405 ; 505 passed, 3 skipped
https://claude.ai/code/session_018woyEHc8HG2th7V4ewJ4Kg
- backend/app/api/v1/models_api.py +4 -52
- backend/tests/test_api_models.py +7 -74
- frontend/src/lib/api.ts +0 -10
- frontend/src/pages/Admin.tsx +31 -56
backend/app/api/v1/models_api.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
Endpoints de gestion des modèles IA (R10 — préfixe /api/v1/).
|
| 3 |
|
| 4 |
-
|
| 5 |
-
GET /api/v1/models → liste les modèles disponibles
|
| 6 |
POST /api/v1/models/refresh → force la mise à jour de la liste
|
| 7 |
PUT /api/v1/corpora/{id}/model → associe un modèle à un corpus
|
| 8 |
GET /api/v1/corpora/{id}/model → modèle actif d'un corpus
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
"""
|
| 13 |
# 1. stdlib
|
| 14 |
import logging
|
|
@@ -32,18 +31,6 @@ router = APIRouter(tags=["models"])
|
|
| 32 |
|
| 33 |
# ── Schémas ───────────────────────────────────────────────────────────────────
|
| 34 |
|
| 35 |
-
class ApiKeyRequest(BaseModel):
|
| 36 |
-
api_key: str
|
| 37 |
-
provider_type: str = "google_ai_studio"
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
class ApiKeyResponse(BaseModel):
|
| 41 |
-
valid: bool
|
| 42 |
-
provider: str
|
| 43 |
-
model_count: int
|
| 44 |
-
error: str | None = None
|
| 45 |
-
|
| 46 |
-
|
| 47 |
class ModelSelectRequest(BaseModel):
|
| 48 |
model_id: str
|
| 49 |
provider_type: str
|
|
@@ -66,46 +53,11 @@ class ModelsRefreshResponse(BaseModel):
|
|
| 66 |
refreshed_at: datetime
|
| 67 |
|
| 68 |
|
| 69 |
-
# ── Validation de clé API (isolé pour les tests) ──────────────────────────────
|
| 70 |
-
|
| 71 |
-
def _validate_api_key(api_key: str, provider_type: str) -> tuple[bool, int, str | None]:
|
| 72 |
-
"""Essaie de lister les modèles avec la clé fournie.
|
| 73 |
-
|
| 74 |
-
Retourne (valid, model_count, error_message).
|
| 75 |
-
Fonction isolée au niveau module pour être patchable dans les tests.
|
| 76 |
-
"""
|
| 77 |
-
try:
|
| 78 |
-
from google import genai # import local pour éviter l'import top-level
|
| 79 |
-
client = genai.Client(api_key=api_key)
|
| 80 |
-
raw_models = list(client.models.list())
|
| 81 |
-
vision_count = sum(
|
| 82 |
-
1 for m in raw_models if "gemini" in (getattr(m, "name", "") or "").lower()
|
| 83 |
-
)
|
| 84 |
-
return True, vision_count, None
|
| 85 |
-
except Exception as exc:
|
| 86 |
-
return False, 0, str(exc)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
| 90 |
|
| 91 |
-
@router.post("/settings/api-key", response_model=ApiKeyResponse)
|
| 92 |
-
async def validate_api_key(body: ApiKeyRequest) -> ApiKeyResponse:
|
| 93 |
-
"""Valide qu'une clé API fonctionne (appel list_models).
|
| 94 |
-
|
| 95 |
-
La clé N'EST PAS stockée (R06). Elle reste dans les variables d'env.
|
| 96 |
-
"""
|
| 97 |
-
valid, count, error = _validate_api_key(body.api_key, body.provider_type)
|
| 98 |
-
return ApiKeyResponse(
|
| 99 |
-
valid=valid,
|
| 100 |
-
provider=body.provider_type,
|
| 101 |
-
model_count=count,
|
| 102 |
-
error=error,
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
@router.get("/models", response_model=list[dict])
|
| 107 |
async def get_models() -> list[dict]:
|
| 108 |
-
"""Liste tous les modèles disponibles sur les providers configurés."""
|
| 109 |
models = list_all_models()
|
| 110 |
return [m.model_dump() for m in models]
|
| 111 |
|
|
|
|
| 1 |
"""
|
| 2 |
Endpoints de gestion des modèles IA (R10 — préfixe /api/v1/).
|
| 3 |
|
| 4 |
+
GET /api/v1/models → liste les modèles disponibles via les credentials env
|
|
|
|
| 5 |
POST /api/v1/models/refresh → force la mise à jour de la liste
|
| 6 |
PUT /api/v1/corpora/{id}/model → associe un modèle à un corpus
|
| 7 |
GET /api/v1/corpora/{id}/model → modèle actif d'un corpus
|
| 8 |
|
| 9 |
+
Les clés API vivent exclusivement dans les secrets HuggingFace (variables d'environnement).
|
| 10 |
+
L'interface ne demande jamais de clé à l'utilisateur (R06).
|
| 11 |
"""
|
| 12 |
# 1. stdlib
|
| 13 |
import logging
|
|
|
|
| 31 |
|
| 32 |
# ── Schémas ───────────────────────────────────────────────────────────────────
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
class ModelSelectRequest(BaseModel):
|
| 35 |
model_id: str
|
| 36 |
provider_type: str
|
|
|
|
| 53 |
refreshed_at: datetime
|
| 54 |
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
@router.get("/models", response_model=list[dict])
|
| 59 |
async def get_models() -> list[dict]:
|
| 60 |
+
"""Liste tous les modèles disponibles sur les providers configurés en environnement."""
|
| 61 |
models = list_all_models()
|
| 62 |
return [m.model_dump() for m in models]
|
| 63 |
|
backend/tests/test_api_models.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
Tests des endpoints /api/v1/models
|
| 3 |
|
| 4 |
Stratégie :
|
| 5 |
-
- Appels Google AI mockés via monkeypatch sur
|
| 6 |
- BDD SQLite en mémoire pour les endpoints qui touchent la BDD (PUT/GET model)
|
| 7 |
|
| 8 |
Vérifie :
|
| 9 |
-
- POST /api/v1/settings/api-key → valid/invalid
|
| 10 |
- GET /api/v1/models → liste mockée
|
| 11 |
- POST /api/v1/models/refresh → mise à jour + timestamp
|
| 12 |
- PUT /api/v1/corpora/{id}/model → création + mise à jour
|
|
@@ -63,83 +62,17 @@ async def _make_corpus(db, slug="models-test"):
|
|
| 63 |
|
| 64 |
|
| 65 |
# ---------------------------------------------------------------------------
|
| 66 |
-
# POST /api/v1/settings/api-key
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
|
| 69 |
@pytest.mark.asyncio
|
| 70 |
-
async def
|
| 71 |
-
|
| 72 |
-
models_api_module, "_validate_api_key",
|
| 73 |
-
lambda key, provider: (True, 3, None),
|
| 74 |
-
)
|
| 75 |
-
response = await async_client.post(
|
| 76 |
-
"/api/v1/settings/api-key",
|
| 77 |
-
json={"api_key": "AIza-test-key", "provider_type": "google_ai_studio"},
|
| 78 |
-
)
|
| 79 |
-
assert response.status_code == 200
|
| 80 |
-
data = response.json()
|
| 81 |
-
assert data["valid"] is True
|
| 82 |
-
assert data["model_count"] == 3
|
| 83 |
-
assert data["provider"] == "google_ai_studio"
|
| 84 |
-
assert data["error"] is None
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
@pytest.mark.asyncio
|
| 88 |
-
async def test_api_key_invalid(async_client, monkeypatch):
|
| 89 |
-
monkeypatch.setattr(
|
| 90 |
-
models_api_module, "_validate_api_key",
|
| 91 |
-
lambda key, provider: (False, 0, "API key not valid"),
|
| 92 |
-
)
|
| 93 |
response = await async_client.post(
|
| 94 |
"/api/v1/settings/api-key",
|
| 95 |
-
json={"api_key": "
|
| 96 |
)
|
| 97 |
-
assert response.status_code
|
| 98 |
-
data = response.json()
|
| 99 |
-
assert data["valid"] is False
|
| 100 |
-
assert data["model_count"] == 0
|
| 101 |
-
assert data["error"] is not None
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
@pytest.mark.asyncio
|
| 105 |
-
async def test_api_key_not_stored_in_db(async_client, db_session, monkeypatch):
|
| 106 |
-
"""La clé ne doit apparaître nulle part dans la BDD (R06)."""
|
| 107 |
-
monkeypatch.setattr(
|
| 108 |
-
models_api_module, "_validate_api_key",
|
| 109 |
-
lambda key, provider: (True, 2, None),
|
| 110 |
-
)
|
| 111 |
-
await async_client.post(
|
| 112 |
-
"/api/v1/settings/api-key",
|
| 113 |
-
json={"api_key": "secret-key-AIza123", "provider_type": "google_ai_studio"},
|
| 114 |
-
)
|
| 115 |
-
# Vérifie que la clé n'est pas dans model_configs
|
| 116 |
-
from sqlalchemy import text
|
| 117 |
-
result = await db_session.execute(text("SELECT * FROM model_configs"))
|
| 118 |
-
rows = result.fetchall()
|
| 119 |
-
for row in rows:
|
| 120 |
-
row_str = str(row)
|
| 121 |
-
assert "secret-key-AIza123" not in row_str
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
@pytest.mark.asyncio
|
| 125 |
-
async def test_api_key_missing_body_422(async_client):
|
| 126 |
-
response = await async_client.post("/api/v1/settings/api-key", json={})
|
| 127 |
-
assert response.status_code == 422
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
@pytest.mark.asyncio
|
| 131 |
-
async def test_api_key_default_provider_type(async_client, monkeypatch):
|
| 132 |
-
"""provider_type est optionnel (default: google_ai_studio)."""
|
| 133 |
-
monkeypatch.setattr(
|
| 134 |
-
models_api_module, "_validate_api_key",
|
| 135 |
-
lambda key, provider: (True, 1, None),
|
| 136 |
-
)
|
| 137 |
-
response = await async_client.post(
|
| 138 |
-
"/api/v1/settings/api-key",
|
| 139 |
-
json={"api_key": "AIza-test"},
|
| 140 |
-
)
|
| 141 |
-
assert response.status_code == 200
|
| 142 |
-
assert response.json()["provider"] == "google_ai_studio"
|
| 143 |
|
| 144 |
|
| 145 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1 |
"""
|
| 2 |
+
Tests des endpoints /api/v1/models (Sprint 4 — Session B).
|
| 3 |
|
| 4 |
Stratégie :
|
| 5 |
+
- Appels Google AI mockés via monkeypatch sur list_all_models
|
| 6 |
- BDD SQLite en mémoire pour les endpoints qui touchent la BDD (PUT/GET model)
|
| 7 |
|
| 8 |
Vérifie :
|
|
|
|
| 9 |
- GET /api/v1/models → liste mockée
|
| 10 |
- POST /api/v1/models/refresh → mise à jour + timestamp
|
| 11 |
- PUT /api/v1/corpora/{id}/model → création + mise à jour
|
|
|
|
| 62 |
|
| 63 |
|
| 64 |
# ---------------------------------------------------------------------------
|
| 65 |
+
# POST /api/v1/settings/api-key → supprimé (clés dans secrets HF, R06)
|
| 66 |
# ---------------------------------------------------------------------------
|
| 67 |
|
| 68 |
@pytest.mark.asyncio
|
| 69 |
+
async def test_settings_api_key_endpoint_removed(async_client):
|
| 70 |
+
"""L'endpoint /api/v1/settings/api-key ne doit plus exister (404 ou 405)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
response = await async_client.post(
|
| 72 |
"/api/v1/settings/api-key",
|
| 73 |
+
json={"api_key": "AIza-test", "provider_type": "google_ai_studio"},
|
| 74 |
)
|
| 75 |
+
assert response.status_code in (404, 405)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
# ---------------------------------------------------------------------------
|
frontend/src/lib/api.ts
CHANGED
|
@@ -2,13 +2,6 @@ const BASE_URL: string = import.meta.env.VITE_API_URL ?? ''
|
|
| 2 |
|
| 3 |
// ── Types ─────────────────────────────────────────────────────────────────────
|
| 4 |
|
| 5 |
-
export interface ApiKeyResponse {
|
| 6 |
-
valid: boolean
|
| 7 |
-
provider: string
|
| 8 |
-
model_count: number
|
| 9 |
-
error: string | null
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
export interface ModelInfo {
|
| 13 |
model_id: string
|
| 14 |
display_name: string
|
|
@@ -240,9 +233,6 @@ export const listProfiles = (): Promise<CorpusProfile[]> =>
|
|
| 240 |
export const createCorpus = (input: CreateCorpusInput): Promise<Corpus> =>
|
| 241 |
post('/api/v1/corpora', input)
|
| 242 |
|
| 243 |
-
export const validateApiKey = (apiKey: string): Promise<ApiKeyResponse> =>
|
| 244 |
-
post('/api/v1/settings/api-key', { api_key: apiKey })
|
| 245 |
-
|
| 246 |
export const listModels = (): Promise<ModelInfo[]> =>
|
| 247 |
get('/api/v1/models')
|
| 248 |
|
|
|
|
| 2 |
|
| 3 |
// ── Types ─────────────────────────────────────────────────────────────────────
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
export interface ModelInfo {
|
| 6 |
model_id: string
|
| 7 |
display_name: string
|
|
|
|
| 233 |
export const createCorpus = (input: CreateCorpusInput): Promise<Corpus> =>
|
| 234 |
post('/api/v1/corpora', input)
|
| 235 |
|
|
|
|
|
|
|
|
|
|
| 236 |
export const listModels = (): Promise<ModelInfo[]> =>
|
| 237 |
get('/api/v1/models')
|
| 238 |
|
frontend/src/pages/Admin.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
| 3 |
fetchCorpora,
|
| 4 |
listProfiles,
|
| 5 |
createCorpus,
|
| 6 |
-
validateApiKey,
|
| 7 |
listModels,
|
| 8 |
selectModel,
|
| 9 |
ingestImages,
|
|
@@ -202,41 +201,27 @@ interface ModelSectionProps {
|
|
| 202 |
}
|
| 203 |
|
| 204 |
function ModelSection({ corpora, selectedCorpusId, onSelectCorpus }: ModelSectionProps) {
|
| 205 |
-
const [apiKey, setApiKey] = useState('')
|
| 206 |
-
const [validating, setValidating] = useState(false)
|
| 207 |
-
const [apiKeyValid, setApiKeyValid] = useState(false)
|
| 208 |
-
const [validateError, setValidateError] = useState<string | null>(null)
|
| 209 |
-
const [validateInfo, setValidateInfo] = useState<string | null>(null)
|
| 210 |
const [models, setModels] = useState<ModelInfo[]>([])
|
|
|
|
|
|
|
| 211 |
const [selectedModelId, setSelectedModelId] = useState('')
|
| 212 |
const [savingModel, setSavingModel] = useState(false)
|
| 213 |
const [saveError, setSaveError] = useState<string | null>(null)
|
| 214 |
const [saveSuccess, setSaveSuccess] = useState<string | null>(null)
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
setModels([])
|
| 222 |
-
setValidating(true)
|
| 223 |
-
try {
|
| 224 |
-
const resp = await validateApiKey(apiKey)
|
| 225 |
-
if (resp.valid) {
|
| 226 |
-
setApiKeyValid(true)
|
| 227 |
-
setValidateInfo(`Clé valide — ${resp.model_count} modèle(s) détecté(s)`)
|
| 228 |
-
const ms = await listModels()
|
| 229 |
setModels(ms)
|
| 230 |
if (ms.length > 0) setSelectedModelId(ms[0].model_id)
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
setValidating(false)
|
| 238 |
-
}
|
| 239 |
-
}
|
| 240 |
|
| 241 |
const handleSelectModel = async (e: FormEvent) => {
|
| 242 |
e.preventDefault()
|
|
@@ -266,35 +251,25 @@ function ModelSection({ corpora, selectedCorpusId, onSelectCorpus }: ModelSectio
|
|
| 266 |
<h2 className="text-lg font-semibold text-stone-800 mb-6">Configurer le modèle IA</h2>
|
| 267 |
<CorpusSelector corpora={corpora} value={selectedCorpusId} onChange={onSelectCorpus} />
|
| 268 |
|
| 269 |
-
{
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
</
|
| 285 |
-
|
| 286 |
-
{validateInfo && <SuccessMsg message={validateInfo} />}
|
| 287 |
-
<button
|
| 288 |
-
type="submit"
|
| 289 |
-
disabled={validating || !apiKey}
|
| 290 |
-
className="bg-stone-800 text-white px-5 py-2 rounded text-sm font-medium hover:bg-stone-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
| 291 |
-
>
|
| 292 |
-
{validating ? 'Vérification…' : 'Vérifier la clé'}
|
| 293 |
-
</button>
|
| 294 |
-
</form>
|
| 295 |
|
| 296 |
-
{
|
| 297 |
-
{apiKeyValid && models.length > 0 && (
|
| 298 |
<form onSubmit={(e) => void handleSelectModel(e)} className="space-y-4 max-w-md">
|
| 299 |
<div>
|
| 300 |
<label className="block text-xs font-semibold text-stone-500 uppercase tracking-wide mb-1">
|
|
|
|
| 3 |
fetchCorpora,
|
| 4 |
listProfiles,
|
| 5 |
createCorpus,
|
|
|
|
| 6 |
listModels,
|
| 7 |
selectModel,
|
| 8 |
ingestImages,
|
|
|
|
| 201 |
}
|
| 202 |
|
| 203 |
function ModelSection({ corpora, selectedCorpusId, onSelectCorpus }: ModelSectionProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
const [models, setModels] = useState<ModelInfo[]>([])
|
| 205 |
+
const [loadingModels, setLoadingModels] = useState(true)
|
| 206 |
+
const [loadError, setLoadError] = useState<string | null>(null)
|
| 207 |
const [selectedModelId, setSelectedModelId] = useState('')
|
| 208 |
const [savingModel, setSavingModel] = useState(false)
|
| 209 |
const [saveError, setSaveError] = useState<string | null>(null)
|
| 210 |
const [saveSuccess, setSaveSuccess] = useState<string | null>(null)
|
| 211 |
|
| 212 |
+
useEffect(() => {
|
| 213 |
+
setLoadingModels(true)
|
| 214 |
+
setLoadError(null)
|
| 215 |
+
listModels()
|
| 216 |
+
.then((ms) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
setModels(ms)
|
| 218 |
if (ms.length > 0) setSelectedModelId(ms[0].model_id)
|
| 219 |
+
})
|
| 220 |
+
.catch((err) => {
|
| 221 |
+
setLoadError(err instanceof Error ? err.message : 'Erreur inconnue')
|
| 222 |
+
})
|
| 223 |
+
.finally(() => setLoadingModels(false))
|
| 224 |
+
}, [])
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
const handleSelectModel = async (e: FormEvent) => {
|
| 227 |
e.preventDefault()
|
|
|
|
| 251 |
<h2 className="text-lg font-semibold text-stone-800 mb-6">Configurer le modèle IA</h2>
|
| 252 |
<CorpusSelector corpora={corpora} value={selectedCorpusId} onChange={onSelectCorpus} />
|
| 253 |
|
| 254 |
+
{loadingModels && (
|
| 255 |
+
<p className="text-sm text-stone-400">Chargement des modèles disponibles…</p>
|
| 256 |
+
)}
|
| 257 |
+
|
| 258 |
+
{!loadingModels && loadError && (
|
| 259 |
+
<ErrorMsg message={loadError} />
|
| 260 |
+
)}
|
| 261 |
+
|
| 262 |
+
{!loadingModels && !loadError && models.length === 0 && (
|
| 263 |
+
<p className="text-sm text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-2">
|
| 264 |
+
Aucun modèle détecté. Vérifiez que les secrets{' '}
|
| 265 |
+
<code className="font-mono">AI_PROVIDER</code> et{' '}
|
| 266 |
+
<code className="font-mono">VERTEX_API_KEY</code> (ou{' '}
|
| 267 |
+
<code className="font-mono">GOOGLE_AI_STUDIO_API_KEY</code>) sont bien configurés
|
| 268 |
+
dans les secrets HuggingFace.
|
| 269 |
+
</p>
|
| 270 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
+
{!loadingModels && models.length > 0 && (
|
|
|
|
| 273 |
<form onSubmit={(e) => void handleSelectModel(e)} className="space-y-4 max-w-md">
|
| 274 |
<div>
|
| 275 |
<label className="block text-xs font-semibold text-stone-500 uppercase tracking-wide mb-1">
|