bots-garden/mini-mepublic Fork
⑂ feature/search-in-doc
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 13af864Unverified · on feature/search-in-doc · k33g · 16d ago
providers.md · 42 lines · 9.2 KBmarkdown
Blame HistoryOpen raw

Fournisseurs : une implémentation par protocole — explication

De quoi s'agit-il ?

mm dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Quatre serveurs sont pris en charge d'origine : Docker Model Runner, llama-server de llama.cpp, llmman et Ollama, sous les noms dmr, llamacpp, llmman et ollama. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.

Pourquoi c'est conçu ainsi

Un type de fournisseur par protocole réseau, pas par éditeur. Quand le moteur a été extrait, exactement une fonction et deux constantes de chaîne étaient spécifiques à Docker Model Runner. Tout le reste, le streaming, le watchdog, la nouvelle tentative, le compteur de commandes, se comportait de la même façon face à un faux moteur qui n'est pas DMR non plus. Donc dmr et llamacpp sont deux entrées de données d'un seul type openaiCompat : une URL de base par défaut, une URL de repli optionnelle, une politique de clé, et les mots employés dans les messages d'erreur. Ajouter un autre serveur compatible OpenAI est une entrée de registre ; ajouter un protocole différent serait un nouveau type.

Un fournisseur possède quatre choses. Il résout la configuration en un backend concret (URL après surcharges d'environnement et sonde de repli, clé lue dans l'environnement, nom du modèle) ; il ouvre Genkit avec son plugin ; il sonde le serveur avant la première question ; et il explique une erreur de transport en une ligne.

Le repli existe pour les conteneurs. Sur l'hôte, Docker Model Runner écoute sur localhost:12434 ; depuis un conteneur ou une sandbox, le même serveur est joignable en host.docker.internal. Si l'URL principale ne répond pas sur /models en deux secondes, le repli est utilisé. Une surcharge explicite par l'environnement saute la sonde : qui l'a posée sait où est le serveur. Un fallback: "" explicite le désactive, ce dont une configuration de test a besoin pour rester épinglée à une seule URL.

La clé ne va jamais dans le YAML. Le fichier nomme la variable d'environnement qui contient la clé, ce qui permet de le committer et de le montrer à l'écran. Les serveurs qui ignorent l'en-tête reçoivent une valeur factice, parce que le client OpenAI veut quelque chose à cet endroit. Un 401 est alors expliqué en nommant la variable que le fournisseur a réellement lue, ou, quand aucune n'est configurée, en pointant l'URL.

La sonde est un conseil, jamais un échec. Sur une machine de démo, le serveur est souvent démarré après l'agent. La sonde vérifie la joignabilité, avertit quand le modèle configuré n'est pas dans la liste de DMR, et sur llama.cpp lit la taille de contexte servie sur /props, un niveau au-dessus de /v1, parce que ce point d'accès rapporte ce que le serveur sert, pas ce sur quoi le modèle a été entraîné. Quand la fenêtre est encore inconnue et que la compression est activée, le REPL sonde à nouveau avant la première question.

llmman est une troisième entrée du registre, avec sa propre sonde. llmman (llmman serve, port 17434) est un démon placé devant un llama-server par modèle chargé, et parle plusieurs API ; mm n'utilise que celle d'OpenAI. Tout ce qui est propre à llama.cpp la traverse — avancement de la lecture du prompt, raisonnement, enable_thinking: false pour les résumés — d'où le même traitement que llamacpp. Son /props, en revanche, est un bouchon (n_ctx 4096, pas de chat template, mesuré sur llmman 0.1.498) : la sonde lit donc la fenêtre servie sur /api/ps, pour le modèle configuré, une fois qu'il est chargé. Le contexte d'entraînement de /api/show n'est pas utilisé : LLMMAN_CONTEXT_LENGTH peut servir moins, et une fenêtre trop grande donne une compression qui ne se déclenche jamais avant que le serveur refuse le prompt. Un modèle absent de /v1/models n'est qu'un avertissement : llmman le télécharge à la première requête.

Ollama est une quatrième entrée, et montre pourquoi chaque serveur garde la sienne. Le /v1 d'Ollama ressemble aux autres, mais, mesuré sur Ollama 0.35.0, il diffère sur trois points. Le raisonnement arrive dans delta.reasoning, que le tap lit déjà. Aucun avancement de lecture du prompt ne revient, donc return_progress n'est pas envoyé. Et chat_template_kwargs est ignoré : le résumé d'une compaction a raisonné jusqu'à sa limite de tokens et est revenu vide ; c'est reasoning_effort: "none" qu'Ollama respecte, et avec lui la même compaction a réussi. Voilà pourquoi la façon de couper le raisonnement est un champ par fournisseur, et non une conséquence de « est llama.cpp ». mm ne s'en sert que pour les résumés ; pour chaque tour, l'utilisateur écrit la même clé propre au serveur dans sampling, qui transmet depuis Genkit 1.12 les clés que l'API OpenAI ne connaît pas (voir Couper le raisonnement d'un modèle). Comme llmman, Ollama rapporte la fenêtre servie sur /api/ps une fois le modèle chargé (les deux partagent cette lecture) ; contrairement à llmman, il ne télécharge pas un modèle absent, d'où l'avertissement de la sonde qui dit ollama pull.

Les erreurs sont réécrites dans les mots du serveur. Une pile de messages imbriqués est du bruit ; « tool calls need llama-server started with --jinja » est tout le diagnostic. Le fournisseur reconnaît les refus de connexion, l'option --jinja manquante, les 401/403, les 404 avec une indication de pull ou d'alias, les 429 et les 5xx. Les erreurs inconnues reviennent inchangées : un message brut vaut mieux qu'une mauvaise piste.

Un flux que le serveur interrompt est relancé, et ses mots sont gardés. llama.cpp signale un échec au milieu d'une réponse diffusée par un événement SSE error: (le statut 200 est déjà parti). openai-go ne lit que les lignes data: : le message disparaissait derrière unexpected end of JSON input. Le tap qui lit déjà le flux pour le raisonnement récupère aussi ces événements, et l'appel au modèle qui a échoué est relancé une fois — cet appel seulement : les outils déjà exécutés dans le tour ne sont pas rejoués, et le texte déjà diffusé par la tentative interrompue n'est pas retiré. S'il échoue encore, l'erreur donne le message du serveur (the model server reported an error while generating: Failed to parse input at pos …), ou indique que la connexion a été coupée. Dans les deux cas l'historique est conservé : demander de continuer reprend le travail.

Les outils partent dans le même ordre à chaque requête. Un serveur llama.cpp garde le prompt qu'il vient de lire et, à la requête suivante, ne relit que ce qui a changé après le plus long début commun. Le template de chat place les outils vers le haut — dans le bloc système pour les templates de type Qwen — donc leur ordre décide de la part de l'historique qui survit. Genkit construit la liste des outils de la requête à partir d'une map Go, dont l'ordre est aléatoire : chaque question d'une session envoyait les mêmes outils dans un autre ordre, et le serveur relisait le prompt entier (ticket 53 ; toujours le cas dans Genkit v1.13.1). mm trie désormais les outils par nom juste avant chaque appel au modèle. Mesuré avec Mellum 12B sur Docker Model Runner : à partir de la deuxième question, une question lit une vingtaine de nouveaux tokens en un quart de seconde, là où elle relisait environ 3 400 tokens en sept secondes chaque fois que l'ordre changeait.

Alternatives écartées

  • Un fournisseur par éditeur avec du code moteur dupliqué. Écarté : mesurée, la partie spécifique à l'éditeur tenait en trois identifiants.
  • Mettre la clé d'API dans le fichier de configuration. Écarté parce que le fichier est fait pour être committé et montré.
  • Échouer au démarrage quand le serveur est arrêté. Écarté parce que démarrer le serveur en second est la situation normale d'une démo.
  • Garder l'ordre dans lequel main.go déclare les outils. Écarté : Genkit perd cet ordre avant de construire la requête, et un ordre qui dépend de la déclaration changerait de toute façon avec la configuration et les serveurs MCP. Le nom est la seule clé qui ne dépend que des outils eux-mêmes.
  • Attendre une version de Genkit qui corrige le problème. Écarté pour l'instant : la v1.13.1 et la branche principale construisent toujours la liste à partir d'une map.

Liens avec le reste

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Fournisseurs : une implémentation par protocole — explication

## De quoi s'agit-il ?

`mm` dialogue avec un serveur de modèle local via l'API chat-completions d'OpenAI. Quatre serveurs sont pris en charge d'origine : Docker Model Runner, `llama-server` de llama.cpp, llmman et Ollama, sous les noms `dmr`, `llamacpp`, `llmman` et `ollama`. Le paquet moteur était autrefois lié à Docker Model Runner ; l'abstraction de fournisseur est ce qui l'a généralisé.

## Pourquoi c'est conçu ainsi

**Un type de fournisseur par protocole réseau, pas par éditeur.** Quand le moteur a été extrait, exactement une fonction et deux constantes de chaîne étaient spécifiques à Docker Model Runner. Tout le reste, le streaming, le watchdog, la nouvelle tentative, le compteur de commandes, se comportait de la même façon face à un faux moteur qui n'est pas DMR non plus. Donc `dmr` et `llamacpp` sont deux entrées de données d'un seul type `openaiCompat` : une URL de base par défaut, une URL de repli optionnelle, une politique de clé, et les mots employés dans les messages d'erreur. Ajouter un autre serveur compatible OpenAI est une entrée de registre ; ajouter un protocole différent serait un nouveau type.

**Un fournisseur possède quatre choses.** Il résout la configuration en un backend concret (URL après surcharges d'environnement et sonde de repli, clé lue dans l'environnement, nom du modèle) ; il ouvre Genkit avec son plugin ; il sonde le serveur avant la première question ; et il explique une erreur de transport en une ligne.

**Le repli existe pour les conteneurs.** Sur l'hôte, Docker Model Runner écoute sur `localhost:12434` ; depuis un conteneur ou une sandbox, le même serveur est joignable en `host.docker.internal`. Si l'URL principale ne répond pas sur `/models` en deux secondes, le repli est utilisé. Une surcharge explicite par l'environnement saute la sonde : qui l'a posée sait où est le serveur. Un `fallback: ""` explicite le désactive, ce dont une configuration de test a besoin pour rester épinglée à une seule URL.

**La clé ne va jamais dans le YAML.** Le fichier nomme la variable d'environnement qui contient la clé, ce qui permet de le committer et de le montrer à l'écran. Les serveurs qui ignorent l'en-tête reçoivent une valeur factice, parce que le client OpenAI veut quelque chose à cet endroit. Un 401 est alors expliqué en nommant la variable que le fournisseur a réellement lue, ou, quand aucune n'est configurée, en pointant l'URL.

**La sonde est un conseil, jamais un échec.** Sur une machine de démo, le serveur est souvent démarré après l'agent. La sonde vérifie la joignabilité, avertit quand le modèle configuré n'est pas dans la liste de DMR, et sur llama.cpp lit la taille de contexte servie sur `/props`, un niveau au-dessus de `/v1`, parce que ce point d'accès rapporte ce que le serveur sert, pas ce sur quoi le modèle a été entraîné. Quand la fenêtre est encore inconnue et que la compression est activée, le REPL sonde à nouveau avant la première question.

**llmman est une troisième entrée du registre, avec sa propre sonde.** [llmman](https://github.com/llmmanorg/llmman) (`llmman serve`, port 17434) est un démon placé devant un `llama-server` par modèle chargé, et parle plusieurs API ; `mm` n'utilise que celle d'OpenAI. Tout ce qui est propre à llama.cpp la traverse — avancement de la lecture du prompt, raisonnement, `enable_thinking: false` pour les résumés — d'où le même traitement que `llamacpp`. Son `/props`, en revanche, est un bouchon (`n_ctx` 4096, pas de chat template, mesuré sur llmman 0.1.498) : la sonde lit donc la fenêtre servie sur `/api/ps`, pour le modèle configuré, une fois qu'il est chargé. Le contexte d'entraînement de `/api/show` n'est pas utilisé : `LLMMAN_CONTEXT_LENGTH` peut servir moins, et une fenêtre trop grande donne une compression qui ne se déclenche jamais avant que le serveur refuse le prompt. Un modèle absent de `/v1/models` n'est qu'un avertissement : llmman le télécharge à la première requête.

**Ollama est une quatrième entrée, et montre pourquoi chaque serveur garde la sienne.** Le `/v1` d'Ollama ressemble aux autres, mais, mesuré sur Ollama 0.35.0, il diffère sur trois points. Le raisonnement arrive dans `delta.reasoning`, que le tap lit déjà. Aucun avancement de lecture du prompt ne revient, donc `return_progress` n'est pas envoyé. Et `chat_template_kwargs` est ignoré : le résumé d'une compaction a raisonné jusqu'à sa limite de tokens et est revenu vide ; c'est `reasoning_effort: "none"` qu'Ollama respecte, et avec lui la même compaction a réussi. Voilà pourquoi la façon de couper le raisonnement est un champ par fournisseur, et non une conséquence de « est llama.cpp ». `mm` ne s'en sert que pour les résumés ; pour chaque tour, l'utilisateur écrit la même clé propre au serveur dans `sampling`, qui transmet depuis Genkit 1.12 les clés que l'API OpenAI ne connaît pas (voir [Couper le raisonnement d'un modèle](../how-to/turn-reasoning-off.md)). Comme llmman, Ollama rapporte la fenêtre servie sur `/api/ps` une fois le modèle chargé (les deux partagent cette lecture) ; contrairement à llmman, il ne télécharge pas un modèle absent, d'où l'avertissement de la sonde qui dit `ollama pull`.

**Les erreurs sont réécrites dans les mots du serveur.** Une pile de messages imbriqués est du bruit ; « tool calls need llama-server started with --jinja » est tout le diagnostic. Le fournisseur reconnaît les refus de connexion, l'option `--jinja` manquante, les 401/403, les 404 avec une indication de pull ou d'alias, les 429 et les 5xx. Les erreurs inconnues reviennent inchangées : un message brut vaut mieux qu'une mauvaise piste.

**Un flux que le serveur interrompt est relancé, et ses mots sont gardés.** llama.cpp signale un échec au milieu d'une réponse diffusée par un événement SSE `error:` (le statut 200 est déjà parti). openai-go ne lit que les lignes `data:` : le message disparaissait derrière `unexpected end of JSON input`. Le tap qui lit déjà le flux pour le raisonnement récupère aussi ces événements, et l'appel au modèle qui a échoué est relancé une fois — cet appel seulement : les outils déjà exécutés dans le tour ne sont pas rejoués, et le texte déjà diffusé par la tentative interrompue n'est pas retiré. S'il échoue encore, l'erreur donne le message du serveur (`the model server reported an error while generating: Failed to parse input at pos …`), ou indique que la connexion a été coupée. Dans les deux cas l'historique est conservé : demander de continuer reprend le travail.

**Les outils partent dans le même ordre à chaque requête.** Un serveur llama.cpp garde le prompt qu'il vient de lire et, à la requête suivante, ne relit que ce qui a changé après le plus long début commun. Le template de chat place les outils vers le haut — dans le bloc système pour les templates de type Qwen — donc leur ordre décide de la part de l'historique qui survit. Genkit construit la liste des outils de la requête à partir d'une map Go, dont l'ordre est aléatoire : chaque question d'une session envoyait les mêmes outils dans un autre ordre, et le serveur relisait le prompt entier (ticket 53 ; toujours le cas dans Genkit v1.13.1). `mm` trie désormais les outils par nom juste avant chaque appel au modèle. Mesuré avec Mellum 12B sur Docker Model Runner : à partir de la deuxième question, une question lit une vingtaine de nouveaux tokens en un quart de seconde, là où elle relisait environ 3 400 tokens en sept secondes chaque fois que l'ordre changeait.

## Alternatives écartées

- **Un fournisseur par éditeur avec du code moteur dupliqué.** Écarté : mesurée, la partie spécifique à l'éditeur tenait en trois identifiants.
- **Mettre la clé d'API dans le fichier de configuration.** Écarté parce que le fichier est fait pour être committé et montré.
- **Échouer au démarrage quand le serveur est arrêté.** Écarté parce que démarrer le serveur en second est la situation normale d'une démo.
- **Garder l'ordre dans lequel `main.go` déclare les outils.** Écarté : Genkit perd cet ordre avant de construire la requête, et un ordre qui dépend de la déclaration changerait de toute façon avec la configuration et les serveurs MCP. Le nom est la seule clé qui ne dépend que des outils eux-mêmes.
- **Attendre une version de Genkit qui corrige le problème.** Écarté pour l'instant : la v1.13.1 et la branche principale construisent toujours la liste à partir d'une map.

## Liens avec le reste

- Les deux fichiers de configuration qui paramètrent chaque fournisseur sont listés dans la [référence de la configuration](../reference/configuration.md).
- La fenêtre apprise par la sonde est ce contre quoi la [compression du contexte](context-compression.md) mesure.
- Changer de serveur pour une exécution est couvert dans [comment faire tourner sur llama.cpp](../how-to/run-with-llama-cpp.md) [comment faire tourner sur llmman](../how-to/run-with-llmman.md) et [comment faire tourner sur Ollama](../how-to/run-with-ollama.md).
- Vérifier l'ordre des outils et mesurer le gain de cache sur votre propre serveur : [vérifier que les outils partent dans un ordre stable](../how-to/verify-tool-order.md).