/** * Bifrost — gateway LLM (remplace les appels directs Mistral) * Endpoint OpenAI-compatible : POST {bifrostUrl}/v1/chat/completions * Auth : header x-bf-vk * * 2 tiers (Mission M3 du build Bifrost ; modèles primaires changés le 28/09, session AEP front 4) : * RAPIDE — défaut, pas de toggle UI mode rapide/approfondi sur le site actuellement * APPROFONDI — activable via body.mode === 'approfondi' (prêt pour un futur toggle front) * * ⚠ 28/09 : Groq a retiré groq/llama-3.1-8b-instant (RAPIDE) et groq/llama-3.3-70b-versatile * (APPROFONDI) — 404 model_not_found via Bifrost. Depuis le 23/09 environ, chaque appel * chatbot tombait sur le premier repli Gemini (stats_usage). Primaires passés sur * groq/openai/gpt-oss-20b et groq/openai/gpt-oss-120b ; replis inchangés. Le worker * /opt/aep-worker tourne sur gpt-oss-20b en prod depuis le 28/09 (WORKER_MODEL, json_object) : * Bifrost accepte ce nom de modèle à slash côté Groq. * ⚠ gpt-oss est un modèle à raisonnement : * - la réponse porte un champ message.reasoning à côté de message.content. Les routes * chatbot ne lisent que content (JSON), usage et extra_fields : le champ est ignoré ; * - les tokens de raisonnement sont décomptés de max_tokens. Sans marge, le JSON de la * réponse peut être tronqué ; rendu en HTTP 200, Bifrost ne bascule pas, JSON.parse * échoue → « Je n'ai pas pu analyser ta demande ». Déjà vu le 15/07 : gpt-oss-120b (via Cerebras), 3 appels * corrects sur 4, « tronque parfois à max_tokens 700 ». D'où REASONING_MARGIN_TOKENS, ajouté par * chaque route à son budget de contenu. * ⚠ openrouter-oai exclu (bug Bifrost confirmé — 404 HTML sur modèles avec slash) * ⚠ gemini-oai exige le préfixe "models/" (sinon 403 silencieux) * ⚠ cerebras/gemma-4-31b RETIRÉ du tier RAPIDE (M4, 15/07) : en JSON mode avec un contexte * réel (prompt + ~20 fiches), le modèle part en boucle de répétition dégénérée ("1 1 1...") * dans ~75% des cas où il sert de fallback, produit un JSON invalide, et Bifrost ne bascule * PAS plus loin dans la chaîne (HTTP 200 côté provider = pas une erreur pour Bifrost). Bug * constaté en prod sur chatbot-reseaux (1 échec/3), reproduit 3/4 sur appel direct Bifrost. * Cerebras reste configuré dans Bifrost (dispo pour tier CODE ou reconfiguration future), * juste plus dans cette chaîne tant que ce n'est pas fiabilisé. */ export const BIFROST_TIER_RAPIDE = { model: 'groq/openai/gpt-oss-20b', fallbacks: [ 'gemini-oai/models/gemini-2.5-flash-lite', 'cohere/command-r-08-2024', ], } export const BIFROST_TIER_APPROFONDI = { model: 'groq/openai/gpt-oss-120b', fallbacks: [ 'gemini-oai/models/gemini-2.5-flash', 'mistral/mistral-large-latest', 'cohere/command-r-plus-08-2024', ], } /** * Tokens ajoutés au budget de contenu de chaque route (max_tokens = contenu + marge) pour * absorber le raisonnement de gpt-oss. Plafond seulement : les replis sans raisonnement * n'écrivent pas plus long, les prompts bornent la réponse (200-250 mots). Rester modeste : * la limite de tokens par minute d'une clé Groq peut compter max_tokens avec le prompt. */ export const REASONING_MARGIN_TOKENS = 800 /** Sélectionne le tier selon le param optionnel body.mode. */ export function pickBifrostTier(mode?: string) { return mode === 'approfondi' ? BIFROST_TIER_APPROFONDI : BIFROST_TIER_RAPIDE } export interface BifrostChatResponse { /** reasoning : présent avec gpt-oss, jamais lu (la réponse utile est dans content). */ choices: { message: { content: string; reasoning?: string }; finish_reason?: string }[] usage?: { prompt_tokens: number; completion_tokens: number } extra_fields?: { provider?: string; resolved_model_used?: string } } /** * Contenu JSON de la réponse. Une troncature (finish_reason 'length') ne lève aucune erreur * et finit en message générique côté usager : on la signale au journal pour la voir. */ export function bifrostContent(res: BifrostChatResponse, route: string): string { const choice = res.choices?.[0] if (choice?.finish_reason === 'length') { console.warn(`[${route}] réponse tronquée (finish_reason=length) : relever REASONING_MARGIN_TOKENS`) } return choice?.message?.content ?? '{}' }