2 Commits

Author SHA1 Message Date
Jules Neny
d06c09a476 fix(bifrost): retire cerebras/gemma-4-31b du fallback tier RAPIDE (JSON invalide ~75% du temps) 2026-07-15 22:53:54 +02:00
Jules Neny
4291fe7529 feat(chatbot): branche Entraide/Réseaux/Jobs sur Bifrost au lieu de Mistral direct
Les 3 routes chatbot (chatbot.post.ts, chatbot-reseaux.post.ts, chatbot-taff.post.ts)
appellent désormais ${bifrostUrl}/v1/chat/completions (header x-bf-vk) au lieu de
api.mistral.ai direct. Tier RAPIDE par défaut (groq/llama-3.1-8b-instant + fallbacks
cerebras/gemini-flash-lite/cohere), tier APPROFONDI si body.mode === 'approfondi'
(prêt pour un futur toggle UI, hors-scope ici).

- server/utils/bifrost.ts (nouveau) : mutualise les 2 tiers pour les 3 routes.
- nuxt.config.ts : ajoute bifrostUrl/bifrostVk au runtimeConfig (mistralApiKey
  conservé, juste plus utilisé par ces 3 routes).
- chatbot.post.ts : garde son circuit breaker + logging stats_usage tels quels,
  mais logUsage reflète maintenant le provider/modèle réel ayant répondu
  (extra_fields de Bifrost) et ne calcule un coût que si ce provider est Mistral
  (les autres tiers Bifrost sont free-tier — cout_eur=0 sinon, pour ne pas fausser
  le circuit breaker budget).
- chatbot-reseaux.post.ts / chatbot-taff.post.ts : aucun circuit breaker/logging
  avant, aucun ajouté (asymétrie pré-existante préservée telle quelle).
- chatbot-v2.post.ts (orphelin) et chatbot-pensees.post.ts (proxy LightRAG,
  config runtime séparée) non touchés.

Testé en dev local contre Bifrost (IP Tailscale) : 1 appel réel par route,
réponses conformes, provider réel confirmé dans les logs de test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 22:39:16 +02:00
6 changed files with 100 additions and 218 deletions

188
PILOTE.md
View File

@@ -1,188 +0,0 @@
---
type: pilote
projet: AEP — nav-carte (aep.trans-former.fr)
casquette: Stack technique
pilote: ATIS Dev
created: 2026-06-23
updated: 2026-06-28
---
# PILOTE — AEP / nav-carte
> Doc de pilotage vivant du site **aep.trans-former.fr**. Source de vérité opérationnelle : état déployé, chantiers, décisions, dette. Le journal de dev détaillé reste dans `JOURNAL-V2.md`. Ce doc est l'index de pilotage, pas le journal.
---
## 1. Ce que c'est
- **Site** : `https://aep.trans-former.fr` — cartographie politique AEP (Architecture En Plus / écosystème militant).
- **Stack** : Nuxt 3 (Nitro SSR) + Vue + Leaflet + D3. Backend RAG = LightRAG (Docker).
- **Repo** : `1 PROJETS/TECH .../nav-carte/` — remote Gitea `git.trans-former.fr/jules/nav-carte.git`.
- **Pilote** : ATIS Dev (ne code pas, dispatche des agents Sonnet).
---
## 2. Architecture déployée (VÉRIFIÉE 2026-06-23) ⚠ lire avant toute session
Source fréquente de confusion — état réel constaté sur le VPS :
```
aep.trans-former.fr
│ Caddy : reverse_proxy localhost:3333
aep.service ← LE service réel (systemd, active/running, Nitro Node)
├── App dir : /opt/aep
├── ExecStart : node /opt/aep/.output/server/index.mjs
├── EnvironmentFile: /opt/aep/.env (contient RAG_PE_URL, MISTRAL_API_KEY, etc.)
└── Port : 3333
LightRAG PE : localhost:9621 (Docker, health 200 ✓) ← backend du chatbot RAG
PIÈGES / fausses pistes :
✗ nav-carte.service → inactive/dead (ANCIEN nom, abandonné — ne pas chercher de ce côté)
✗ nav-worker.service → FAILED (worker enrichissement IA, mort — à statuer)
✗ /opt/nav-carte → n'existe pas (le code n'est PAS sur le VPS en git)
```
**Mécanisme de déploiement = MANUEL, scp/tar (PAS de git pull sur le VPS) :**
```
1. npm run build (local → génère .output/)
2. scp/rsync .output/ → vps-hetzner:/opt/aep/.output/
3. ssh vps-hetzner "systemctl restart aep.service"
4. vérifier : curl -I https://aep.trans-former.fr/<route>
```
**Disque VPS : 91 % (3,5 G libres) — à surveiller, pas bloquant.**
---
## 3. État git du repo (2026-06-23)
- `main` == `gitea/main` == `cd8fe9e2` (0 ahead / 0 behind, parfaitement sync).
- **Le commit déployé sur le VPS est PÉRIMÉ** (antérieur à `cd8fe9e2`) → `/outils`, `/media`, routes chatbot = 404 live alors que le code existe sur `main`.
- **Piège repos imbriqués** : `nav-carte/` est un sous-repo dans le vault. Le vault parent n'est PLUS un repo git (chirurgie 2026-05-07). Travailler git uniquement au niveau `nav-carte/`.
### Travail local non committé au 2026-06-23
| Fichier | État | Destination |
|---------|------|-------------|
| `components/ChatbotPensees.vue` | untracked | **P0/P1 — à committer + déployer** (chatbot RAG) |
| `server/api/chatbot-pensees.post.ts` | untracked | **P0/P1 — à committer + déployer** (route chatbot, appelle LightRAG :9621) |
| `nuxt.config.ts` | modifié (+1) | **P0/P1** — ajoute `runtimeConfig.ragPeUrl` (requis par la route chatbot) |
| `components/MediaTabVisuel.vue` | modifié (CSS) | **P0/P1** — layout toolbar/carte/chatbot |
| `pages/a-propos.vue` | modifié (+64) | **P0/P1** — contenu À propos |
| `pages/contribuer-job.vue` | untracked (stub) | **P2** — laisser untracked (feature Proposer) |
| `pages/contribuer-reseau.vue` | untracked (stub) | **P2** — laisser untracked |
| `.firecrawl/` | untracked | junk — gitignore |
| `pages/index-v1-backup.vue.tmp.*` | untracked | junk — supprimer |
### Branches dispersées (dette — voir §6)
```
main ← prod
feat/outils-v1 / -build / -media ← travail Outils (en partie déjà sur main)
feat/aep-v1.1-nav-carte ← contient a6fff9a9 "PA3 bouton Proposer contextuel"
feat/aep-pratiques-regeneratives ← contient d10586c4 "page /proposer-pratique" + form
feat/aep-rag-pensees-ecologiques ← travail RAG/chatbot
feat/aep-taff-v1 / feat/aep-v2-restore / feat/codev-mvp / wip-stash-v2-onglet2-20260506
```
---
## 4. Chantiers (état au 2026-06-23)
| Prio | Chantier | Diagnostic | Statut | Nature |
|------|----------|-----------|--------|--------|
| **P0** | Onglet Outils + page /media + redirect /rag manquants live | Build VPS périmé (code committé, pas déployé) | ✅ **Fait 2026-06-23** (commit `f3ab28bd` déployé, routes 200, /rag→301) | Mécanique : build + deploy |
| **P1** | Chatbot RAG cassé | Route + composant non committés ; LightRAG PE :9621 = UP | ✅ **Fait 2026-06-23** (route déployée, testé public OK, réponses sourcées) | Mixte : commit + deploy + test |
| **P2** | Interface Proposer 5 onglets (Écosystème/Réseau/Jobs/Outils/Références RAG) | Feature dispersée sur branches feature jamais mergées ; `main` n'a qu'1 form (`contribuer.vue`) + 2 stubs untracked | ✅ **Fait 2026-06-23** (commit `c860dabd`, déployé, testé 5/5 types 201) | 4 types → NocoDB `orgas` + `submission_type` ; Réfs → `ressources_references` ; `/contribuer` → 301 → `/proposer` |
| **P3** | Google Search Console / indexation | Non diagnostiqué | Reporté (Jules) | Voir `pilotage-indexation-seo.md` (dossier TECH) |
| **dette** | Organisation docs/branches/services | Multi-build folders, 10+ Caddyfile.bak, services morts | À cadrer | Voir §6 |
### Mapping bugs Jules → vraie cause
```
"mauvais commit, RAG/Outils pas up" → FAUX : commit OK. Build VPS périmé. → P0
"bouton Proposer 1 carte / 4" → feature sur branches non mergées. → P2
"chatbot slug rag/media KO" → redirect /rag→/media en code (pas déployé) → P0/P1
+ composant chatbot non committé
+ backend LightRAG PE :9621 = UP ✓
```
---
## 5. Décisions techniques runtime
| # | Décision | Pourquoi |
|---|----------|----------|
| τ.1 | P0+P1 (deploy + chatbot) cette session ; P2 (Proposer) session dédiée | P2 = archéologie multi-branches, dilue le fix de deploy |
| τ.2 | Stubs `contribuer-*` laissés untracked au deploy P0 | Ne pas exposer de formulaires inachevés en prod |
| τ.3 | Commit P0 = chatbot route + composant + nuxt.config + MediaTabVisuel + a-propos uniquement | Périmètre minimal du « remettre up » ; pas de WIP Proposer |
---
## 6. Dette d'organisation (constatée 2026-06-23)
Le système tech est éclaté. Symptômes :
- Dossier projet `TECH - infra VPS, website pro, RAG/` mélange **6+ projets** (nav-carte/AEP, Website Pro/trans-former.fr, RAG Militant, Simulateur autonomie V2, codev, vps-kit…).
- Multiples dossiers `*-build` (cascades méga-dispatch passées : `aep-communaute-build`, `codev-build`, `astro-site-cerveau`).
- 10+ `Caddyfile.bak.*` sur le VPS, services systemd morts (`nav-carte.service`, `nav-worker.service`).
- Branches feature jamais mergées ni nettoyées.
- Plusieurs `.md` de pilotage épars à la racine TECH (`next-steps`, `stack-opencode-v2`, `pilotage-indexation-seo`…).
**Proposition de rangement : voir le plan dans le message de session du 2026-06-23 / à formaliser en note dédiée.**
---
## 7. Journal & refs
- Journal dev détaillé : `JOURNAL-V2.md` (même dossier).
- Mémoire pilote : `2 CASQUETTES/Stack technique/ATIS-Dev.md`.
- Prompt P2 : `0 INBOX/PROMPTS/aep-P2-proposer-4-onglets.md`.
- SEO/indexation : `1 PROJETS/TECH .../pilotage-indexation-seo.md`.
---
## Log de pilotage
### 2026-06-23 — Session diagnostic (ATIS Dev / Opus)
**Accompli :**
- Diagnostic complet : renversé l'hypothèse « mauvais commit » → build VPS périmé + travail local non committé.
- Cartographié l'archi de service réelle (`aep.service`, pas `nav-carte.service`).
- Confirmé LightRAG PE :9621 UP, deploy = scp manuel vers `/opt/aep/.output`.
- Créé ce doc pilote + prompt P2.
**Accompli (suite) :**
- ✅ P0+P1 déployés : commit `f3ab28bd` build+push (agent Sonnet), deploy prod (tar+scp → `/opt/aep/.output`, restart `aep.service`). Vérifié : `/outils`, `/media`, `/contribuer`, `/trouver-du-taf` = 200 ; `/rag` → 301 ; chatbot RAG public OK (réponses sourcées via LightRAG :9621).
- Prompts P2 + hygiène TECH écrits avec préflight git anti-superposition + clause supervision pilote.
**Prochaines étapes :**
- [x] P2 : Proposer 5 onglets — fait 2026-06-23 (commit c860dabd, deploye, teste 5/5 types 201).
- [x] Hygiene TECH : execute 2026-06-23 (agent Sonnet). Voir recap dans `_infra/TECH.md`.
- [ ] **Batch 3 (prochain chantier)** : bouton selecteur 3 cartes + popup 5 axes Jobs + GraphView Carte 1. Prompt : `REPRISE-aep-carto-fix-batch3.md`. Session dediee.
- [ ] AEP nav-carte — verifier chatbots EN PROD : `/` = chatbot Entraide / `/agences` = chatbot Reseaux AEP distincts.
- [ ] P3 : Google Search Console (reporte).
### 2026-06-23 — Session hygiène TECH (AXE 1+2+3) + fix chatbot (Agent Sonnet)
**Accompli :**
- **AXE 1 VPS** : docker prune (156M), décommission listmonk (conteneur + image + 2 volumes + 2 routes Caddy), décommission heyform (volume 528M + compose), décommission uptime (volume 88M + route status), `/opt/migration` supprimé (252M, aucune réf active), nav-worker + nav-carte services nettoyés, Caddyfile.bak réduit de 17→4 fichiers. Disque 91%→87% (+~1.3 GB).
- **AXE 2 Docs** : `_archive-build/` créé (3 dossiers `*-build` déplacés), `_infra/TECH.md` créé (inventaire VPS complet), `INDEX.md` créé, 3 squelettes `PILOTE.md` (Website Pro, RAG Militant, Simulateur autonomie V2). 4 `.md` épars routés.
- **AXE 3 Git** : 4 branches à 0 commit supprimées (`feat/outils-v1`, `-build`, `-media`, `feat/codev-mvp`). `feat/outils-v1-build` worktree retiré.
- **Fix chatbot régresse** : `auteurs-pensees.json` (171 auteurs) restauré en prod (scp), versionné sur `main` (commit `04b02351`), push gitea. Worktree `nav-carte-media` retiré, les 4 fichiers untrackés sauvés sur `feat/outils-v1-media` push gitea (branche préservée).
### 2026-06-23 — Session P2 : Proposer 5 onglets (ATIS Dev / Sonnet)
**Accompli :**
- Interface Proposer avec 5 tabs (Écosystème/Réseau/Jobs/Outils/Références RAG)
- 4 types (ecosysteme/reseau/job/outil) → NocoDB `orgas` table avec `submission_type`
- Références → table dédiée NocoDB `ressources_references`
- `/contribuer` → 301 redirect vers `/proposer`
- 6 corrections pilote appliquées
- Build + deploy VPS (scp .output/ + systemctl restart aep)
- Tests : 1 par type (4×201), 1 référence (201), test records supprimés (404)
- Routes vérifiées : /proposer (200), /contribuer (301), /api/submit (201), /api/submit/reference (201)
- ✅ Tout est bon, GO pilote obtenu, P2 terminé
**Prochaine étape :**
- [ ] Push `main``gitea/main` (P2 + fixes chatbot non encore poussés)
- [ ] P3 : Google Search Console (reporté)

View File

@@ -16,6 +16,8 @@ export default defineNuxtConfig({
commentTableId: process.env.COMMENT_TABLE_ID || process.env.AVIS_TABLE_ID,
statsTableId: process.env.STATS_TABLE_ID || 'mbbq7n47ixy19mc',
mistralApiKey: process.env.MISTRAL_API_KEY,
bifrostUrl: process.env.BIFROST_URL || 'http://127.0.0.1:8080',
bifrostVk: process.env.BIFROST_VK,
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
resendApiKey: process.env.RESEND_API_KEY,
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',

View File

@@ -6,6 +6,7 @@
// @ts-ignore — JSON import résolu par Rollup
import reseauxData from '../../public/data/reseaux-bifurcation.json'
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
interface Structure {
id: string
@@ -61,6 +62,7 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event)
const question: string = (body?.question ?? '').trim()
if (!question || question.length < 5) throw createError({ statusCode: 400, message: 'Question trop courte.' })
const tier = pickBifrostTier(body?.mode)
const structures: Structure[] = ((reseauxData as any).structures ?? [])
const keywords = extractKeywords(question)
@@ -82,18 +84,20 @@ export default defineEventHandler(async (event) => {
const systemPrompt = SYSTEM_PROMPT.replace('{{STRUCTURES_JSON}}', JSON.stringify(context, null, 0))
const mistralApiKey = config.mistralApiKey as string
if (!mistralApiKey) throw createError({ statusCode: 500, message: 'Clé API Mistral manquante.' })
const bifrostUrl = config.bifrostUrl as string
const bifrostVk = config.bifrostVk as string
if (!bifrostVk) throw createError({ statusCode: 500, message: 'Clé Bifrost manquante.' })
let mistralRaw: string
try {
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
'https://api.mistral.ai/v1/chat/completions',
const res = await $fetch<BifrostChatResponse>(
`${bifrostUrl}/v1/chat/completions`,
{
method: 'POST',
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
headers: { 'x-bf-vk': bifrostVk, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'mistral-small-latest',
model: tier.model,
fallbacks: tier.fallbacks,
temperature: 0.3,
max_tokens: 700,
response_format: { type: 'json_object' },

View File

@@ -7,6 +7,7 @@
// @ts-ignore — JSON import résolu par Vite/Rollup
import taffData from '../../public/data/plateformes-taff.json'
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
interface PlateformeMinimal {
id: string
@@ -66,6 +67,7 @@ export default defineEventHandler(async (event) => {
if (!question || question.length < 5) {
throw createError({ statusCode: 400, statusMessage: 'Question trop courte.' })
}
const tier = pickBifrostTier(body?.mode)
// Données bundlées statiquement à la compilation (import JSON)
const plateformes: PlateformeMinimal[] = ((taffData as any).plateformes ?? []).map((p: any) => ({
@@ -91,20 +93,22 @@ export default defineEventHandler(async (event) => {
const systemPrompt = SYSTEM_PROMPT.replace('{{PLATEFORMES_JSON}}', JSON.stringify(context, null, 0))
const mistralApiKey = config.mistralApiKey as string
if (!mistralApiKey) {
throw createError({ statusCode: 500, statusMessage: 'Clé API Mistral manquante.' })
const bifrostUrl = config.bifrostUrl as string
const bifrostVk = config.bifrostVk as string
if (!bifrostVk) {
throw createError({ statusCode: 500, statusMessage: 'Clé Bifrost manquante.' })
}
let mistralRaw: string
try {
const res = await $fetch<{ choices: { message: { content: string } }[] }>(
'https://api.mistral.ai/v1/chat/completions',
const res = await $fetch<BifrostChatResponse>(
`${bifrostUrl}/v1/chat/completions`,
{
method: 'POST',
headers: { Authorization: `Bearer ${mistralApiKey}`, 'Content-Type': 'application/json' },
headers: { 'x-bf-vk': bifrostVk, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'mistral-small-latest',
model: tier.model,
fallbacks: tier.fallbacks,
temperature: 0.3,
max_tokens: 700,
response_format: { type: 'json_object' },

View File

@@ -19,6 +19,7 @@
import { checkRateLimitJson } from '~/server/utils/rateLimitJson'
import { checkBudget, calcCoutMistralSmall } from '~/server/utils/circuitBreaker'
import { pickBifrostTier, type BifrostChatResponse } from '~/server/utils/bifrost'
// ── Types ──────────────────────────────────────────────────────────────────────
@@ -131,18 +132,19 @@ async function logUsage(params: {
nocodbUrl: string
nocodbToken: string
statsTableId: string
model: string
tokensIn: number
tokensOut: number
coutEur: number
}) {
const { nocodbUrl, nocodbToken, statsTableId, tokensIn, tokensOut, coutEur } = params
const { nocodbUrl, nocodbToken, statsTableId, model, tokensIn, tokensOut, coutEur } = params
const logUrl = `${nocodbUrl}/api/v2/tables/${statsTableId}/records`
try {
await $fetch(logUrl, {
method: 'POST',
headers: { 'xc-token': nocodbToken, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'mistral-small-latest',
model,
endpoint: 'chatbot',
tokens_in: tokensIn,
tokens_out: tokensOut,
@@ -180,6 +182,7 @@ export default defineEventHandler(async (event) => {
const body = await readBody(event)
const question: string = (body?.question ?? '').trim()
const filters: { fonction?: string; echelle?: string } = body?.filters ?? {}
const tier = pickBifrostTier(body?.mode)
if (!question || question.length < 3) {
throw createError({
@@ -247,32 +250,32 @@ export default defineEventHandler(async (event) => {
JSON.stringify(fichesContext, null, 0),
)
// 6. Appel Mistral Small
const mistralApiKey = config.mistralApiKey as string
// 6. Appel Bifrost (gateway LLM — remplace l'appel Mistral direct)
const bifrostUrl = config.bifrostUrl as string
const bifrostVk = config.bifrostVk as string
if (!mistralApiKey) {
if (!bifrostVk) {
throw createError({
statusCode: 500,
statusMessage: 'Clé API Mistral manquante.',
statusMessage: 'Clé Bifrost manquante.',
})
}
let mistralRaw: string
let tokensIn = 0
let tokensOut = 0
let realModel = tier.model
try {
const mistralRes = await $fetch<{
choices: { message: { content: string } }[]
usage?: { prompt_tokens: number; completion_tokens: number }
}>('https://api.mistral.ai/v1/chat/completions', {
const bifrostRes = await $fetch<BifrostChatResponse>(`${bifrostUrl}/v1/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${mistralApiKey}`,
'x-bf-vk': bifrostVk,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'mistral-small-latest',
model: tier.model,
fallbacks: tier.fallbacks,
temperature: 0.3,
max_tokens: 600,
response_format: { type: 'json_object' },
@@ -283,11 +286,16 @@ export default defineEventHandler(async (event) => {
}),
})
mistralRaw = mistralRes.choices?.[0]?.message?.content ?? '{}'
tokensIn = mistralRes.usage?.prompt_tokens ?? 0
tokensOut = mistralRes.usage?.completion_tokens ?? 0
mistralRaw = bifrostRes.choices?.[0]?.message?.content ?? '{}'
tokensIn = bifrostRes.usage?.prompt_tokens ?? 0
tokensOut = bifrostRes.usage?.completion_tokens ?? 0
realModel =
bifrostRes.extra_fields?.resolved_model_used ??
(bifrostRes.extra_fields?.provider
? `${bifrostRes.extra_fields.provider}/${tier.model.split('/').slice(1).join('/')}`
: tier.model)
} catch (e: any) {
console.error('[chatbot] Erreur Mistral Small:', e?.message ?? e)
console.error('[chatbot] Erreur Bifrost:', e?.message ?? e)
throw createError({
statusCode: 502,
statusMessage: 'Erreur appel IA — réessaie dans quelques instants.',
@@ -317,11 +325,16 @@ export default defineEventHandler(async (event) => {
}
// 8. Log usage (non bloquant)
const coutEur = calcCoutMistralSmall(tokensIn, tokensOut)
// Coût réel calculable seulement si le provider ayant répondu est Mistral —
// les autres tiers Bifrost (Groq, Cerebras, Gemini free tier, Cohere) sont free-tier,
// donc cout_eur=0 pour ne pas fausser le circuit breaker avec un tarif Mistral inapplicable.
const isMistral = realModel.toLowerCase().startsWith('mistral')
const coutEur = isMistral ? calcCoutMistralSmall(tokensIn, tokensOut) : 0
logUsage({
nocodbUrl: config.nocodbUrl as string,
nocodbToken: config.nocodbToken as string,
statsTableId,
model: realModel,
tokensIn,
tokensOut,
coutEur,

47
server/utils/bifrost.ts Normal file
View File

@@ -0,0 +1,47 @@
/**
* Bifrost — gateway LLM (remplace les appels directs Mistral)
* Endpoint OpenAI-compatible : POST {bifrostUrl}/v1/chat/completions
* Auth : header x-bf-vk
*
* 2 tiers validés (Mission M3, build Bifrost) :
* 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)
*
* ⚠ 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/llama-3.1-8b-instant',
fallbacks: [
'gemini-oai/models/gemini-2.5-flash-lite',
'cohere/command-r-08-2024',
],
}
export const BIFROST_TIER_APPROFONDI = {
model: 'groq/llama-3.3-70b-versatile',
fallbacks: [
'gemini-oai/models/gemini-2.5-flash',
'mistral/mistral-large-latest',
'cohere/command-r-plus-08-2024',
],
}
/** 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 {
choices: { message: { content: string } }[]
usage?: { prompt_tokens: number; completion_tokens: number }
extra_fields?: { provider?: string; resolved_model_used?: string }
}