{"openapi":"3.1.0","info":{"title":"SHAU LLM API","version":"1.0.0","summary":"API d'inférence sur modèles de langage open source auto-hébergés.","description":"API d'inférence sur un parc de modèles de langage **open source**, hébergés sur l'infrastructure SHAU.\n\n## Authentification\nToutes les routes d'inférence exigent une clé API, transmise dans l'en-tête\n`Authorization: Bearer sk-shau-...`. Les clés se créent depuis la console, onglet **Clés API**.\nLa console elle-même s'authentifie par cookie de session, obtenu via un lien de connexion envoyé par email.\n\n## Un seul modèle en mémoire à la fois\nLe serveur ne dispose pas d'assez de mémoire pour tenir deux modèles simultanément.\nLes appels sont donc **sérialisés** : si un autre appel occupé le moteur, le votre attend son tour.\nChanger de modèle implique de décharger le précédent puis de charger le nouveau, ce qui ajoute\nun délai de chargement (visible dans `metrics.load_ms`). Un modèle inactif est décharge automatiquement.\nConséquence pratique : **regroupez les appels par modèle** plutôt que d'alterner.\n\n## Compatibilité OpenAI\nLes routes `/v1/chat/completions` et `/v1/completions` suivent le format OpenAI : un client existant\nfonctionne en changeant simplement `base_url` et `api_key`. Chaque réponse est enrichie d'un objet\n`metrics` contenant notamment la vitesse de génération en jetons par seconde.\n\n## Raisonnement\nCertains modèles (Qwen3.5, Granite 4.2) produisent une trace de raisonnement avant leur réponse.\nElle est **désactivée par défaut** : sans cela, une petite valeur de `max_tokens` serait entièrement\nconsommée par le raisonnement et la réponse reviendrait vide. Passez `think: true` pour l'activer ;\nla trace est alors renvoyée dans le champ `thinking`, distinct de `content`.","contact":{"name":"SHAU","url":"https://shau.fr"}},"servers":[{"url":"https://llm.mysalor.fr","description":"Production"}],"tags":[{"name":"Modèles","description":"Catalogue et état des modèles."},{"name":"Inférence","description":"Génération de texte."},{"name":"Contrôle qualité","description":"Vérification de fondement et évaluation des réponses."},{"name":"Mesure","description":"Vitesse de génération et consommation."},{"name":"Runtime","description":"État du moteur, chargement et déchargement."},{"name":"Compte","description":"Connexion, clés API, notifications."}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"sk-shau-...","description":"Clé API créée depuis la console. En-tête : `Authorization: Bearer sk-shau-...`"},"SessionCookie":{"type":"apiKey","in":"cookie","name":"llm_session","description":"Session de la console web."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Description de l'erreur, en français."},"type":{"type":"string","enum":["invalid_request","unauthorized","forbidden","rate_limit","server_error"]},"code":{"type":"string","description":"Code machine, par ex. model_not_found."},"param":{"type":"string","description":"Paramètre fautif, si applicable."}},"required":["message","type","code"]}}},"Metrics":{"type":"object","description":"Mesures de performance de l'appel, calculées à partir des compteurs du moteur.","properties":{"prompt_tokens":{"type":"integer","nullable":true,"description":"Jetons lus en entrée."},"completion_tokens":{"type":"integer","nullable":true,"description":"Jetons générés."},"total_tokens":{"type":"integer","nullable":true},"load_ms":{"type":"integer","nullable":true,"description":"Temps de chargement du modèle en mémoire (0 s'il etait déjà résident)."},"prompt_eval_ms":{"type":"integer","nullable":true,"description":"Temps de lecture du prompt."},"eval_ms":{"type":"integer","nullable":true,"description":"Temps de génération seul."},"total_ms":{"type":"integer","nullable":true,"description":"Durée totale cote moteur."},"tokens_per_second":{"type":"number","nullable":true,"description":"Vitesse de génération : completion_tokens / eval_ms. C'est la mesure de référence."},"prompt_tokens_per_second":{"type":"number","nullable":true,"description":"Vitesse de lecture du prompt."},"time_to_first_token_ms":{"type":"integer","nullable":true,"description":"Délai avant le premier jeton : chargement + lecture du prompt."}}},"ModelId":{"type":"string","enum":["qwen3.5:9b","qwen3.5:4b","gemma4:e4b","llama3.2:3b","llama3.2:1b","granite4.1-guardian:8b","granite4.2:8b"],"default":"qwen3.5:4b","description":"Identifiant du modèle."},"Message":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["system","user","assistant","tool"]},"content":{"type":"string"}}},"Model":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["model"]},"name":{"type":"string"},"owned_by":{"type":"string"},"role":{"type":"string","enum":["flagship","balanced","alternative","baseline","guardian","judge"]},"role_label":{"type":"string"},"tagline":{"type":"string"},"description":{"type":"string"},"strengths":{"type":"array","items":{"type":"string"}},"recommended_for":{"type":"string"},"speed_class":{"type":"string","enum":["fastest","fast","medium","slow"]},"capabilities":{"type":"array","items":{"type":"string"}},"parameter_size":{"type":"string","nullable":true},"quantization":{"type":"string","nullable":true},"size_bytes":{"type":"integer","nullable":true},"size_gb":{"type":"number","nullable":true},"installed":{"type":"boolean"},"loaded":{"type":"boolean","description":"Le modèle est actuellement résident en mémoire."}}},"Usage":{"type":"object","properties":{"prompt_tokens":{"type":"integer"},"completion_tokens":{"type":"integer"},"total_tokens":{"type":"integer"}}}}},"paths":{"/v1/models":{"get":{"tags":["Modèles"],"operationId":"listModels","summary":"Lister les modèles disponibles","description":"Renvoie le catalogue complet, avec pour chaque modèle sa taille reelle sur disque, sa quantisation et s'il est actuellement charge en mémoire.","responses":{"200":{"description":"Catalogue des modèles.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Model"}}}}}}}}}},"/v1/models/{model}":{"get":{"tags":["Modèles"],"operationId":"getModel","summary":"Detail d'un modèle","description":"Ajoute au descriptif la fenêtre de contexte maximale et la licence declaree par le modèle.","parameters":[{"name":"model","in":"path","required":true,"schema":{"$ref":"#/components/schemas/ModelId"}}],"responses":{"200":{"description":"Detail du modèle.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Model"},{"type":"object","properties":{"context_length":{"type":"integer","nullable":true},"license":{"type":"string","nullable":true}}}]}}}},"404":{"description":"Modèle inconnu.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/chat/completions":{"post":{"tags":["Inférence"],"operationId":"createChatCompletion","summary":"Générer une réponse de chat","description":"Route principale, compatible avec le format OpenAI.\n\nSi `stream` vaut `true`, la réponse est un flux **Server-Sent Events** : chaque événement `data:`\nporte un delta de texte, le dernier événement porte `usage` et `metrics`, puis `data: [DONE]` clot le flux.\n\nLa réponse complète contient toujours `metrics.tokens_per_second`, la vitesse de génération mesurée.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"model":{"$ref":"#/components/schemas/ModelId"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/Message"},"minItems":1,"maxItems":200},"system":{"type":"string","description":"Consigne système ajoutee en tête de conversation (raccourci)."},"stream":{"type":"boolean","default":false,"description":"Diffuser la réponse en Server-Sent Events."},"think":{"type":"boolean","default":false,"description":"Activer la trace de raisonnement sur les modèles qui la supportent. Desactivee par défaut."},"format":{"description":"Contraindre la sortie : `\"json\"`, un schema JSON, ou `{type:'json_schema', json_schema:{schema}}`."},"tools":{"type":"array","items":{"type":"object"},"description":"Definitions d'outils, pour les modèles qui declarent la capacité `tools`."},"temperature":{"type":"number","description":"Aléatoire de l'échantillonnage. 0 = déterministe, 2 = très libre.","minimum":0,"maximum":2,"default":0.8},"top_p":{"type":"number","description":"Noyau de probabilité : ne considère que les jetons cumulant cette probabilité.","minimum":0,"maximum":1,"default":0.9},"top_k":{"type":"integer","description":"Nombre maximal de jetons candidats à chaque étape. 0 = désactivé.","minimum":0,"maximum":200,"default":40},"min_p":{"type":"number","description":"Probabilité minimale relative au meilleur jeton. Alternative a top_p.","minimum":0,"maximum":1,"default":0},"max_tokens":{"type":"integer","description":"Nombre maximal de jetons générés. Alias : num_predict.","minimum":1,"maximum":8192,"default":1024},"repeat_penalty":{"type":"number","description":"Pénalise la répétition de jetons déjà produits.","minimum":0,"maximum":2,"default":1.1},"repeat_last_n":{"type":"integer","description":"Fenêtre sur laquelle la pénalité de répétition est appliquée. -1 = taille du contexte.","minimum":-1,"maximum":4096,"default":64},"presence_penalty":{"type":"number","description":"Pénalise les jetons déjà apparus, indépendamment du nombre d'occurrences.","minimum":-2,"maximum":2,"default":0},"frequency_penalty":{"type":"number","description":"Pénalise les jetons proportionnellement a leur fréquence.","minimum":-2,"maximum":2,"default":0},"seed":{"type":"integer","description":"Graine aléatoire. Une même graine + temperature 0 donne un résultat reproductible.","minimum":0,"maximum":2147483647},"num_ctx":{"type":"integer","description":"Taille de la fenêtre de contexte en jetons. Plus elle est grande, plus la RAM et la latence augmentent.","minimum":256,"maximum":32768,"default":4096},"stop":{"description":"Séquences qui interrompent la génération (8 maximum).","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"maxItems":8}]}}},"examples":{"simple":{"summary":"Appel minimal","value":{"model":"qwen3.5:4b","messages":[{"role":"user","content":"Résumé la photosynthèse en deux phrases."}]}},"parametre":{"summary":"Avec paramètres","value":{"model":"qwen3.5:9b","messages":[{"role":"system","content":"Tu reponds en français, de manière concise."},{"role":"user","content":"Qu'est-ce qu'un index en base de données ?"}],"temperature":0.2,"max_tokens":300,"seed":42}},"json":{"summary":"Sortie JSON contrainte","value":{"model":"qwen3.5:4b","messages":[{"role":"user","content":"Donne trois villes francaises avec leur population."}],"format":{"type":"json_schema","json_schema":{"schema":{"type":"object","properties":{"villes":{"type":"array","items":{"type":"object","properties":{"nom":{"type":"string"},"population":{"type":"integer"}},"required":["nom","population"]}}},"required":["villes"]}}}}},"flux":{"summary":"En streaming","value":{"model":"llama3.2:3b","messages":[{"role":"user","content":"Raconte une histoire courte."}],"stream":true}}}}}},"responses":{"200":{"description":"Réponse générée. En mode flux, le corps est un flux SSE.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["chat.completion"]},"created":{"type":"integer"},"model":{"type":"string"},"choices":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"message":{"type":"object","properties":{"role":{"type":"string"},"content":{"type":"string"},"thinking":{"type":"string","nullable":true}}},"finish_reason":{"type":"string","enum":["stop","length"]}}}},"usage":{"$ref":"#/components/schemas/Usage"},"metrics":{"$ref":"#/components/schemas/Metrics"},"parameters":{"type":"object","description":"Paramètres réellement appliqués."}}}},"text/event-stream":{"schema":{"type":"string","description":"Suite d'événements `data: {...}` terminée par `data: [DONE]`."}}}},"400":{"description":"Paramètre invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Clé API absente ou invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Modèle inconnu.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Limite de débit atteinte.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Moteur occupé : délai d'attente dépassé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/completions":{"post":{"tags":["Inférence"],"operationId":"createCompletion","summary":"Compléter un texte","description":"Génération à partir d'un prompt brut, sans structure de conversation. Accepte les mêmes paramètres que le chat.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"model":{"$ref":"#/components/schemas/ModelId"},"prompt":{"type":"string"},"system":{"type":"string"},"suffix":{"type":"string","description":"Texte place après la génération (remplissage)."},"stream":{"type":"boolean","default":false},"format":{},"temperature":{"type":"number","description":"Aléatoire de l'échantillonnage. 0 = déterministe, 2 = très libre.","minimum":0,"maximum":2,"default":0.8},"top_p":{"type":"number","description":"Noyau de probabilité : ne considère que les jetons cumulant cette probabilité.","minimum":0,"maximum":1,"default":0.9},"top_k":{"type":"integer","description":"Nombre maximal de jetons candidats à chaque étape. 0 = désactivé.","minimum":0,"maximum":200,"default":40},"min_p":{"type":"number","description":"Probabilité minimale relative au meilleur jeton. Alternative a top_p.","minimum":0,"maximum":1,"default":0},"max_tokens":{"type":"integer","description":"Nombre maximal de jetons générés. Alias : num_predict.","minimum":1,"maximum":8192,"default":1024},"repeat_penalty":{"type":"number","description":"Pénalise la répétition de jetons déjà produits.","minimum":0,"maximum":2,"default":1.1},"repeat_last_n":{"type":"integer","description":"Fenêtre sur laquelle la pénalité de répétition est appliquée. -1 = taille du contexte.","minimum":-1,"maximum":4096,"default":64},"presence_penalty":{"type":"number","description":"Pénalise les jetons déjà apparus, indépendamment du nombre d'occurrences.","minimum":-2,"maximum":2,"default":0},"frequency_penalty":{"type":"number","description":"Pénalise les jetons proportionnellement a leur fréquence.","minimum":-2,"maximum":2,"default":0},"seed":{"type":"integer","description":"Graine aléatoire. Une même graine + temperature 0 donne un résultat reproductible.","minimum":0,"maximum":2147483647},"num_ctx":{"type":"integer","description":"Taille de la fenêtre de contexte en jetons. Plus elle est grande, plus la RAM et la latence augmentent.","minimum":256,"maximum":32768,"default":4096},"stop":{"description":"Séquences qui interrompent la génération (8 maximum).","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"},"maxItems":8}]}}},"example":{"model":"llama3.2:3b","prompt":"Les trois lois de la robotique sont :","max_tokens":200,"temperature":0.5}}}},"responses":{"200":{"description":"Texte généré.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"choices":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"}}}},"usage":{"$ref":"#/components/schemas/Usage"},"metrics":{"$ref":"#/components/schemas/Metrics"}}}}}},"400":{"description":"Paramètre invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Non authentifie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/guardian/risks":{"get":{"tags":["Contrôle qualité"],"operationId":"listGuardianRisks","summary":"Lister les risques vérifiables","description":"Énumère les risques que granite4.1-guardian:8b sait évaluer, avec les champs obligatoires de chacun.","responses":{"200":{"description":"Liste des risques.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"requires":{"type":"array","items":{"type":"string"}}}}}}}}}}}}},"/v1/guardian/check":{"post":{"tags":["Contrôle qualité"],"operationId":"guardianCheck","summary":"Vérifier qu'une réponse est bien fondée","description":"Soumet un échange à **granite4.1-guardian:8b**, qui rend un verdict binaire sur le risque demande.\n\nLe cas le plus courant est `groundedness` : vérifier qu'une réponse produite à partir de documents\nn'invente rien. Fournissez alors `context` (les documents) et `assistant_message` (la réponse à vérifier).\n\n`risk_detected: true` signifie que le problème est présent. Le champ `passed` donne la lecture inverse,\nplus directe : `true` = la réponse est jugee saine.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"risk":{"type":"string","enum":["groundedness","answer_relevance","context_relevance","harm","social_bias","jailbreak","violence","profanity","sexual_content","unethical_behavior"],"default":"groundedness","description":"Risque à évaluer."},"user_message":{"type":"string","description":"Question ou message de l'utilisateur."},"assistant_message":{"type":"string","description":"Réponse à vérifier."},"context":{"description":"Documents de référence (chaîne ou tableau de chaines).","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"num_ctx":{"type":"integer","description":"Taille de la fenêtre de contexte en jetons. Plus elle est grande, plus la RAM et la latence augmentent.","minimum":256,"maximum":32768,"default":4096}}},"examples":{"groundedness":{"summary":"Réponse fondée sur un document","value":{"risk":"groundedness","context":"Le siège social de la société est situé à Fort-de-France, en Martinique.","user_message":"Ou se trouve le siège social ?","assistant_message":"Le siège social est à Fort-de-France."}},"hallucination":{"summary":"Réponse inventée","value":{"risk":"groundedness","context":"Le siège social est à Fort-de-France.","user_message":"Ou se trouve le siège social ?","assistant_message":"Le siège social est à Paris, dans le 9e arrondissement, depuis 1998."}},"nuisible":{"summary":"Détection de contenu nuisible","value":{"risk":"harm","user_message":"Comment fabriquer une arme chez moi ?"}}}}}},"responses":{"200":{"description":"Verdict rendu.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string"},"model":{"type":"string"},"risk":{"type":"string"},"risk_label":{"type":"string"},"risk_detected":{"type":"boolean","nullable":true,"description":"true = le risque est présent."},"passed":{"type":"boolean","nullable":true,"description":"Lecture inverse : true = aucun problème détecté."},"interpretation":{"type":"string"},"confidence":{"type":"string","enum":["high","low","unknown"]},"raw_verdict":{"type":"string"},"metrics":{"$ref":"#/components/schemas/Metrics"}}}}}},"400":{"description":"Risque inconnu ou champ obligatoire manquant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/evaluate":{"post":{"tags":["Contrôle qualité"],"operationId":"evaluateAnswer","summary":"Noter la qualité d'une réponse","description":"Soumet une question et une réponse à **granite4.2:8b**, qui rend une note sur 10 argumentée,\ndetaillee sur quatre critères : exactitude, exhaustivité, clarté et pertinence.\n\nLa sortie est contrainte par un schema JSON : elle est toujours exploitable programmatiquement.\nFournir une `référence` (réponse attendue) rend la notation nettement plus fiable.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["question","answer"],"properties":{"question":{"type":"string","description":"Question initialement posée."},"answer":{"type":"string","description":"Réponse à évaluer."},"reference":{"type":"string","description":"Réponse de référence, facultative mais recommandee."},"criteria":{"type":"string","description":"Critères supplementaires imposes par l'appelant."},"evaluated_model":{"type":"string","description":"Modèle ayant produit la réponse, repris tel quel dans le résultat."},"temperature":{"type":"number","description":"Aléatoire de l'échantillonnage. 0 = déterministe, 2 = très libre.","minimum":0,"maximum":2,"default":0.8}}},"example":{"question":"Qu'est-ce que le protocole HTTP ?","answer":"HTTP est un protocole de transfert de documents pour le web.","reference":"HTTP est un protocole applicatif client-serveur sans état, utilisé pour transférer des ressources sur le web.","evaluated_model":"llama3.2:1b"}}}},"responses":{"200":{"description":"Évaluation rendue.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string"},"model":{"type":"string"},"score":{"type":"number","description":"Note globale sur 10."},"verdict":{"type":"string","enum":["excellent","bon","acceptable","insuffisant","mauvais"]},"reasoning":{"type":"string"},"criteria":{"type":"object","properties":{"accuracy":{"type":"number"},"completeness":{"type":"number"},"clarity":{"type":"number"},"relevance":{"type":"number"}}},"strengths":{"type":"array","items":{"type":"string"}},"weaknesses":{"type":"array","items":{"type":"string"}},"metrics":{"$ref":"#/components/schemas/Metrics"}}}}}},"400":{"description":"question ou answer manquant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Le modèle juge n'a pas renvoyé de JSON exploitable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/benchmark":{"post":{"tags":["Mesure"],"operationId":"runBenchmark","summary":"Mesurer la vitesse de génération","description":"Exécute le même prompt sur un ou plusieurs modèles et renvoie la vitesse mesurée en jetons par seconde.\n\nLes modèles sont traites **l'un après l'autre** : un seul tient en mémoire à la fois.\nLe premier appel de chaque modèle inclut son chargement, signale par `cold_start`.\nComparer plusieurs gros modèles peut donc prendre plusieurs minutes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"models":{"oneOf":[{"$ref":"#/components/schemas/ModelId"},{"type":"array","items":{"$ref":"#/components/schemas/ModelId"}}],"description":"Un modèle ou une liste de modèles."},"prompt":{"type":"string","description":"Prompt de test. Un prompt par défaut est utilisé si absent."},"runs":{"type":"integer","minimum":1,"maximum":3,"default":1,"description":"Nombre de passes par modèle."},"max_tokens":{"type":"integer","description":"Nombre maximal de jetons générés. Alias : num_predict.","minimum":1,"maximum":8192,"default":1024},"num_ctx":{"type":"integer","description":"Taille de la fenêtre de contexte en jetons. Plus elle est grande, plus la RAM et la latence augmentent.","minimum":256,"maximum":32768,"default":4096},"temperature":{"type":"number","description":"Aléatoire de l'échantillonnage. 0 = déterministe, 2 = très libre.","minimum":0,"maximum":2,"default":0.8}}},"example":{"models":["llama3.2:1b","llama3.2:3b","qwen3.5:4b"],"prompt":"Explique en trois phrases ce qu'est une API REST.","max_tokens":120}}}},"responses":{"200":{"description":"Mesures effectuées.","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string"},"prompt":{"type":"string"},"runs_per_model":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"tokens_per_second_avg":{"type":"number","nullable":true},"tokens_per_second_best":{"type":"number","nullable":true},"first_run_cold":{"type":"boolean","nullable":true},"error":{"type":"string","nullable":true},"details":{"type":"array","items":{"$ref":"#/components/schemas/Metrics"}}}}},"fastest":{"type":"string","nullable":true}}}}}}}}},"/v1/usage":{"get":{"tags":["Mesure"],"operationId":"getUsage","summary":"Consommation et vitesses moyennes","description":"Agrégé les appels enregistrés : nombre de requêtes, jetons consommés et vitesse moyenne, par modèle et par jour.","parameters":[{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":365,"default":30},"description":"Profondeur de la fenêtre."},{"name":"scope","in":"query","schema":{"type":"string","enum":["self","all"],"default":"self"},"description":"`all` agrège tous les comptes (administrateur uniquement)."}],"responses":{"200":{"description":"Statistiques.","content":{"application/json":{"schema":{"type":"object","properties":{"window_days":{"type":"integer"},"totals":{"type":"object"},"by_model":{"type":"array","items":{"type":"object"}},"by_day":{"type":"array","items":{"type":"object"}}}}}}}}}},"/v1/status":{"get":{"tags":["Runtime"],"operationId":"getStatus","summary":"État du moteur","description":"Indique quel modèle est résident en mémoire, si le moteur est occupé et combien d'appels attendent leur tour.","security":[],"responses":{"200":{"description":"État courant.","content":{"application/json":{"schema":{"type":"object","properties":{"resident_model":{"type":"string","nullable":true,"description":"Modèle actuellement charge en mémoire, le cas échéant."},"busy":{"type":"boolean"},"current_model":{"type":"string","nullable":true},"queue_length":{"type":"integer","description":"Appels en attente du moteur."},"idle_unload_ms":{"type":"integer","description":"Délai d'inactivité avant déchargement automatique."},"catalog_installed":{"type":"array","items":{"type":"string"}},"catalog_missing":{"type":"array","items":{"type":"string"}}}}}}}}}},"/v1/models/{model}/load":{"post":{"tags":["Runtime"],"operationId":"loadModel","summary":"Précharger un modèle","description":"Charge le modèle en mémoire, en dechargeant le précédent. Utile pour absorber le temps de chargement avant une série d'appels.","parameters":[{"name":"model","in":"path","required":true,"schema":{"$ref":"#/components/schemas/ModelId"}}],"responses":{"200":{"description":"Modèle charge.","content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"},"loaded":{"type":"boolean"},"switched":{"type":"boolean"},"already_resident":{"type":"boolean"},"load_ms":{"type":"integer"}}}}}}}}},"/v1/models/{model}/unload":{"post":{"tags":["Runtime"],"operationId":"unloadModel","summary":"Décharger un modèle","description":"Libère immédiatement la mémoire occupee par le modèle, sans attendre le délai d'inactivité.","parameters":[{"name":"model","in":"path","required":true,"schema":{"$ref":"#/components/schemas/ModelId"}}],"responses":{"200":{"description":"Modèle décharge.","content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string"},"unloaded":{"type":"boolean"}}}}}}}}},"/v1/unload":{"post":{"tags":["Runtime"],"operationId":"unloadAll","summary":"Libérer toute la mémoire","description":"Décharge tout modèle résident.","responses":{"200":{"description":"Mémoire libérée.","content":{"application/json":{"schema":{"type":"object","properties":{"unloaded_count":{"type":"integer"},"freed":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/auth/request-link":{"post":{"tags":["Compte"],"operationId":"requestLoginLink","summary":"Demander un lien de connexion","description":"Envoie un lien de connexion à usage unique. La réponse est identique que l'adresse ait accès ou non, pour ne pas révéler qui est membre.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Demande prise en compte.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"message":{"type":"string"}}}}}}}}},"/api/access-requests":{"post":{"tags":["Compte"],"operationId":"requestAccess","summary":"Demander l'accès à la plateforme","description":"Dépose une demande d'accès. Une notification est créée dans la console de l'administrateur, doublée d'un email.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"message":{"type":"string","maxLength":1000,"description":"Présentation ou usage envisagé."}}}}}},"responses":{"201":{"description":"Demande enregistrée.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"request_id":{"type":"integer"},"message":{"type":"string"}}}}}},"400":{"description":"Adresse invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Trop de demandes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api-keys":{"get":{"tags":["Compte"],"operationId":"listApiKeys","summary":"Lister ses clés API","description":"Les clés ne sont jamais relisibles : seul leur préfixe est conservé en clair.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Clés du compte.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"prefix":{"type":"string"},"created_at":{"type":"string"},"last_used_at":{"type":"string","nullable":true},"revoked_at":{"type":"string","nullable":true}}}}}}}}}}},"post":{"tags":["Compte"],"operationId":"createApiKey","summary":"Créer une clé API","description":"**La clé en clair n'est renvoyée qu'à la création.** Conservez-la immédiatement : elle n'est plus lisible ensuite.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80}}}}}},"responses":{"201":{"description":"Clé créée.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"key":{"type":"string","description":"Valeur complète, affichée une seule fois."}}}}}}}}},"/api/api-keys/{id}":{"delete":{"tags":["Compte"],"operationId":"revokeApiKey","summary":"Révoquer une clé API","security":[{"SessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Clé révoquée.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"404":{"description":"Clé introuvable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/notifications":{"get":{"tags":["Compte"],"operationId":"listNotifications","summary":"Lister ses notifications","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Notifications.","content":{"application/json":{"schema":{"type":"object","properties":{"unread":{"type":"integer"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"kind":{"type":"string"},"title":{"type":"string"},"body":{"type":"string","nullable":true},"read_at":{"type":"string","nullable":true},"created_at":{"type":"string"}}}}}}}}}}}}}}