SDK & paquet npm
L'API complète du paquet brimkern : un widget de chat en un appel, ou des sessions sans interface et de la génération one-shot pour votre propre UI. Tout tourne sur le GPU du visiteur : aucun serveur, aucune clé d'API, rien ne quitte le navigateur. Pour la visite guidée et la démo live, voir la page SDK.
Installation
Depuis npm. Types TypeScript inclus :
npm i brimkernimport { embed, createSession, generate, preload, status, runtime } from 'brimkern'; // les types, pour TypeScript import type { EmbedConfig, SessionConfig, AskOptions, BrimkernWidget, BrimkernSession, BrimkernEvents, BrimkernEvent, Msg, Source, LoadProgress, ToolSpec, WidgetLabels, } from 'brimkern';
Ou en balise script, sans étape de build. L’IIFE expose la même API sur une globale :
<script src="https://brimkern.com/sdk.js"></script> <script> Brimkern.embed({ title: "Ask us anything" }); </script>
embed(config?)
Monte le widget de chat dans la page (il attend le document si l'appel arrive tôt) et rend une poignée BrimkernWidget — voir Piloter le widget, plus bas. Le modèle ne se télécharge que lorsqu'un visiteur ouvre réellement le widget : votre vitesse de page reste intacte.
systemstringtitlestringgreetingstringaccentstringmodelstringmaxTokensnumberlang'en' | 'fr'examples{ user, assistant }[]knowledge / knowledgeBudgetsee belowworker / workerUrlboolean / stringhistoryMsg[]showSourcesbooleantools('calc' | 'date' | ToolSpec)[]theme / position / width / height / labelssee belowApparence & libellés
Le widget suit votre page au lieu d'imposer son aspect. Tout se règle PAR widget : deux widgets d'une même page peuvent différer de thème, de coin et de couleur.
embed({
theme: 'auto', // 'light' (défaut) | 'dark' | 'auto' — auto suit prefers-color-scheme, en direct
position: 'bottom-left', // 'bottom-right' (défaut) | 'bottom-left'
width: 400, height: 600, // px, bornés à 300-480 × 380-720 ; les petits écrans plafonnent toujours au viewport
labels: { // n'importe quelle langue, n'importe quel ton — les clés absentes gardent les défauts de lang
open: 'Chat öffnen',
placeholder: 'Nachricht eingeben…',
note: 'Lokale KI — läuft auf Ihrer GPU.',
phases: { download: 'Modell wird geladen…' },
},
});lang couvre l'anglais et le français d'origine ; labels est la porte vers toutes les autres langues (open, close, placeholder, note, error, empty, help, sources, mb, phases). Les libellés sont rendus comme du texte, jamais comme du balisage, et theme prend un mot-clé, pas du CSS : rien de ce qu'un intégrateur passe ici ne peut injecter du style ou du balisage dans la page hôte.
Piloter le widget
embed() rend une poignée. C'est elle qui permet de DÉMONTER le widget — ce qui compte dans toute application à navigation côté client, où sinon le widget survit à chaque changement de route et un second embed() empile un second bouton sur la page.
const widget = embed({ title: "Support" }); widget.open(); widget.close(); widget.toggle(); await widget.ask("Do you ship to Canada?"); // comme si le visiteur l'avait tapée widget.setKnowledge(newDocs); // change les fiches, garde la conversation widget.setHistory(saved); // reprend une conversation widget.history // Msg[] widget.el // le panneau, pour un ajustement de style widget.destroy(); // retire le DOM, annule une génération en cours
destroy() laisse le moteur chargé : les poids sont partagés par la page, démonter un widget ne fait donc jamais retélécharger le modèle au suivant. En React, la poignée est exactement ce qu'attend le nettoyage d'un effet :
useEffect(() => {
const widget = embed({ system: "You are our support agent." });
return () => widget.destroy();
}, []);Appeler embed() côté serveur est inoffensif : vous recevez une poignée inerte au lieu d'une erreur, le même code peut donc tourner des deux côtés.
Événements
La poignée du widget comme une session exposent on(event, callback), qui rend sa propre fonction de désabonnement. C'est par là que vous journalisez les conversations, mesurez l'engagement et — le plus utile — apprenez qu'un navigateur de visiteur n'a pas WebGPU, au lieu de laisser cet échec dans une bulle de chat.
const off = widget.on('message', ({ role, content, sources }) => { analytics.track('chat', { role, content }); }); off(); // désabonnement widget.on('progress', (phase, p) => bar.value = p ? p.loaded / p.total : 0); widget.on('ready', () => console.log('modèle chargé')); widget.on('open', () => {}); widget.on('close', () => {}); widget.on('error', (err) => report(err)); // ex. pas de WebGPU sur ce navigateur widget.on('tool', ({ name, result }) => {}); // un outil a produit un résultat pour ce tour
phase est une clé stable — init, download, tokenizer, gpu — jamais une phrase : c'est à vous de la libeller dans la langue de votre page. Un écouteur qui lève est intercepté et journalisé : votre analytics ne peut pas casser le widget. Les sessions reçoivent ready, progress, message, error et tool (open et close n'existent que côté widget), et une session émet progress parce que ask() précharge avant son premier tour : ce premier appel téléchargeait 149 Mo sans aucun moyen de le dire.
Le seul échec qu'un visiteur peut déclencher sans rien faire de mal porte un code de cause : err.code === « no-webgpu » signifie que ce navigateur ne peut pas exécuter l'assistant du tout. C'est le signal pour masquer le widget, pas un bug à corriger — status() répond à la même question avant tout montage. Les erreurs du chemin de génération sont émises ET levées : le catch de votre ask() reste valable.
createSession(config?)
Sans interface : un objet conversation pour votre propre UI. Même config que embed() moins les options visuelles — history compris, pour démarrer sur une conversation rangée — plus temperature.
const session = createSession({ system: "You are a sommelier.", temperature: 0.7 }); const reply = await session.ask("A wine for oysters?", { onToken: (text) => output.textContent += text, // streaming signal: controller.signal, // cancellable }); session.history // the Msg[] so far session.lastSources // les fiches derrière la dernière réponse session.setHistory(saved) // reprend une conversation session.setKnowledge(newDocs) // change les fiches, garde la conversation session.on('message', log) // mêmes événements que le widget session.reset() // same config, blank history session.destroy()
setHistory() et setKnowledge() lèvent pendant une génération : terminez ou annulez le tour d'abord. Les deux laissent le moteur et les poids intacts — changer de catalogue ne coûte pas un téléchargement, et ne coûte plus la conversation non plus. ask() lève également si un tour est déjà en cours sur cette session : une conversation, un tour à la fois.
temperature vaut 0,25 par défaut quand vous passez knowledge, et 0,55 sinon — la même règle que le widget, et elle est mesurée : à 0,55, la lecture d'une ligne de tableau partait une fois sur trois sur la mauvaise colonne. Un assistant qui recopie un chiffre d'une fiche n'a rien à gagner à échantillonner large. Une temperature que vous déclarez continue de primer.
generate(options)
One-shot : un prompt, une réponse, pas d’historique conservé. Prend la config de session plus prompt, onToken, signal et onSources. Il prend UN objet : appelé comme generate("question", {…}) il lève une TypeError au lieu de répondre en silence à la chaîne « undefined ».
const answer = await generate({ system: "Answer in one sentence.", prompt: "Why is the sky blue?", onToken: (text) => process(text), });
Documents de connaissance
Donnez vos contenus à l'assistant : pages, FAQ, fiches produit. Les documents sont découpés en passages dans le navigateur, et seuls les 1 à 3 passages les plus proches de la question du visiteur sont donnés au modèle. Le tri est local (lexical) : rien n'est envoyé où que ce soit.
knowledge: [ "Plain strings work.", { title: "Shipping", text: "Free in France from 60 euros." }, ], knowledgeBudget: 800 // tokens de passages max par question
Outils
Donnez à l'assistant des capacités au-delà de ses fiches : l'arithmétique, la date du jour, ou vos propres fonctions — un stock, l'état d'une commande, le total d'un panier. Le choix de conception est délibéré : le modèle ne décide JAMAIS d'appeler un outil (sous ~3B de paramètres, les appels d'outils émis sont hallucinés — mesuré). La détection est déterministe, votre fonction tourne dans votre page, et le modèle reçoit le résultat comme un fait, exactement comme un passage de connaissance.
embed({
tools: [
'calc', // détecte l’arithmétique du message et injecte le résultat exact
'date', // le modèle connaît la date du jour (ligne stable du prompt système)
{
name: 'stock',
match: /stock|disponible/i, // ou un prédicat : (question) => boolean
run: async (question) => { // tourne dans VOTRE page — sync ou async
const n = await api.stockFor(question);
return `${n} en stock`;
},
},
],
});
widget.on('tool', ({ name, result }) => trace(name, result)); // avant la génération, comme onSourcesLe contrat protège le visiteur : un outil qui lève, qui traîne (plafond 10 s) ou qui ne rend rien est simplement absent du tour — le tour, lui, n'échoue jamais à cause d'un outil. Les résultats sont bornés à 600 caractères : un résultat est un fait, pas un rapport, et le modèle par défaut a une fenêtre courte. Rien ici ne touche le réseau, sauf si VOTRE fonction run le fait.
Sources d'une réponse
Vous pouvez savoir quels passages ont nourri une réponse. Deux raisons : un petit modèle se trompe, et une réponse que le visiteur peut vérifier vaut mieux qu'une réponse simplement affirmée — et quand le vôtre répond de travers, c'est ainsi que vous distinguez un mauvais passage d'une mauvaise lecture du bon.
await session.ask(question, { onSources: (sources) => show(sources), // avant la génération : le tri est local et instantané }); session.lastSources // [{ title, text, score, doc }] embed({ knowledge: docs, showSources: true }); // dans le widget, sous chaque réponse widget.on('message', ({ sources }) => trace(sources)); // ou sans rien afficher
score est la proximité lexicale avec la question, doc l'index du document dans votre tableau knowledge, et l'ordre est celui dans lequel les passages ont été donnés au modèle. Un tableau vide a un sens : aucun passage ne correspondait.
Et ce qui se passe alors dépend du message, parce que deux situations ne doivent pas être confondues. Une question qui demande une information reçoit la réponse honnête — « je n’ai pas cette information » — et c’est la promesse du produit. Le reste, non : « ça va ? », « AIDEZ-MOI », « bonjour » reçoivent une réponse courte et aimable. Un assistant qui oppose un mur à tout ce qui sort de ses fiches est un assistant qu’on ferme, et il suffit de quelques réponses de ce genre d’affilée pour qu’un petit modèle ne fasse plus que les répéter.
preload(), status() & runtime()
preload() télécharge le moteur et le modèle avant la première question : appelez-le sur un survol, ou sur la page tarifs avant l'ouverture du support. onProgress reçoit la phase et, pendant le téléchargement, les octets : de quoi afficher une vraie barre de progression.
await preload({ onProgress: (status, p) => { if (p) bar.style.width = (100 * p.loaded / p.total) + "%"; }, }); status() // 'unavailable' (pas de WebGPU) | 'idle' | 'loading' | 'ready' | 'error'
status() répond de façon synchrone : utile pour décider d'afficher ou non le widget sur les navigateurs sans WebGPU. runtime() dit où l'inférence tourne réellement — « worker », « main », ou « pending » tant que rien n'a démarré — c'est ce qu'on vérifie après avoir passé worker: true pour savoir si le repli s'est déclenché (une CSP d'hôte qui interdit blob: fait retomber sur le thread principal, en silence et volontairement : un widget ne doit jamais cesser de fonctionner à cause d'un choix d'exécution).
Un seul moteur par page
Le moteur est un singleton par URL de modèle : N widgets et N sessions d'une page partagent une seule init WebGPU et un seul jeu de poids en VRAM. Monter un second widget coûte un nœud DOM, pas 149 Mo — et en détruire un laisse les poids chargés pour le suivant. C'est aussi pourquoi worker et workerUrl ne valent que TANT QUE le premier preload ou ask n'a pas eu lieu : une fois le backend créé il est partagé par toute la page, et un embed() ultérieur qui dit le contraire est ignoré avec un avertissement en console plutôt que cru en silence.
Versions & CDN
Épinglez une version si vous préférez que le widget ne change pas sous vos pieds :
https://brimkern.com/sdk-0.6.0.js au lieu de https://brimkern.com/sdk.js <!-- ou depuis les CDN npm --> https://unpkg.com/brimkern@0.6.0/dist/brimkern.iife.js https://cdn.jsdelivr.net/npm/brimkern@0.6.0/dist/brimkern.iife.js
Serveur, licence, liens
Importer le paquet côté serveur ne fait rien tant qu'un navigateur ne l'exécute pas : Next.js, Remix et Astro passent sans garde-fou. Licence MIT, comme tout le moteur.
Brimkern : moteur WebGPU open, conçu par Romain Khanoyan. IA locale, WebGPU, moteurs on-device.