Rien ne plantait. C'est ce qui rendait ce bug intéressant. Dans Microsoft Agent Framework, un appel Gemini en streaming pouvait se terminer sans aucun nom de modèle, alors que le même appel sans streaming le rapportait correctement.

L'effet invisible

Le parseur sans streaming se rabattait sur le modèle du client quand Gemini n'envoyait pas de model_version. Le parseur en streaming, 33 lignes plus bas, transmettait chunk.model_version tel quel. Les chunks n'en portent souvent aucun, donc ChatResponse.model finissait à None.

La couche d'observabilité lit ce champ pour gen_ai.response.model, une dimension des histogrammes de tokens et de durée. Les clés absentes sont retirées avant l'enregistrement des métriques. Sur les flux Gemini, la ventilation par modèle disparaissait, sans la moindre erreur.

Version un : une seule ligne

model=chunk.model_version or self.model,

La parité avec le chemin sans streaming, en une ligne. La description de ma pull request posait déjà la question gênante : le modèle par défaut du client est-il vraiment le bon repli, quand un appel peut changer de modèle par ses options ?

Premier trou : le mauvais modèle

Le relecteur automatique a répondu avec un exemple. Une requête gemini-2.5-pro envoyée par un client configuré pour gemini-2.5-flash serait étiquetée flash. Le repli devait être le modèle résolu pour cette requête, pas celui du client.

Second trou : le bon modèle, écrasé

Puis un mainteneur a trouvé le plus subtil. Si un premier chunk annonce gemini-2.5-pro-002 et que le dernier chunk d'usage ne porte aucune version, appliquer le repli à chaque chunk remplace la version observée par l'alias demandé. L'agrégateur garde la dernière valeur non nulle, un seul chunk final suffisait.

Ce qui a été fusionné

fallback_model = model
...
if chunk.model_version:
    fallback_model = chunk.model_version
yield self._process_chunk(chunk, fallback_model)

Le repli vit désormais à l'échelle du flux : dès que le serveur nomme une version, les chunks suivants qui l'omettent la conservent, et avant cela c'est le modèle résolu pour la requête qui s'applique. Quatre tests fixent les quatre cas : le modèle venu du chunk, le repli sur le modèle configuré, le repli sur le modèle demandé, et une version vue plus tôt qui doit survivre.

Ce que j'en retiens

La revue n'est pas là pour attraper des fautes de frappe. Elle attrape les effets de second ordre d'un changement, le cas où une ligne juste rencontre un agrégateur trois fichiers plus loin. J'avais posé la bonne question dans ma description et j'y avais mal répondu dans mon code. Le correctif s'est amélioré parce que deux autres lecteurs ne l'ont pas laissé passer.

Ayoub Hammoudi, Ingénieur logiciel, full-stack et plateformes IA. github.com/Ayoubhm07