<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Olivier</title>
    <description>The latest articles on DEV Community by Olivier (@hernanz).</description>
    <link>https://dev.to/hernanz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036001%2Faf4cba7e-a94d-45df-ad03-cb3cfbe87da9.png</url>
      <title>DEV Community: Olivier</title>
      <link>https://dev.to/hernanz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hernanz"/>
    <language>en</language>
    <item>
      <title>Absorber les +50 % de l'API Claude sans couper une feature</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:13:52 +0000</pubDate>
      <link>https://dev.to/hernanz/absorber-les-50-de-lapi-claude-sans-couper-une-feature-4nj</link>
      <guid>https://dev.to/hernanz/absorber-les-50-de-lapi-claude-sans-couper-une-feature-4nj</guid>
      <description>&lt;p&gt;Le 1er septembre 2026, le tarif de lancement de Claude Sonnet 5 s'arrête. L'input passe de 2 $ à 3 $ le million de tokens, l'output de 10 $ à 15 $ : +50 % sur les deux lignes, pour tout le monde qui appelle l'API en paiement à l'usage. La panique par défaut, c'est de couper des fonctionnalités ou de rétrograder vers un modèle plus faible. Il y a mieux, et c'est déjà dans l'API. Deux mécanismes — le prompt caching et le batch — encaissent la hausse à ta place, souvent avec de la marge. Voici le code, les chiffres, et les pièges que j'ai payés pour que tu ne les paies pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qui bouge exactement le 1er septembre
&lt;/h2&gt;

&lt;p&gt;Trois lignes suffisent à raisonner. Le reste du barème (Opus, Haiku, contexte 1M) ne change pas.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Poste Sonnet 5 (par M de tokens)&lt;/th&gt;
&lt;th&gt;Jusqu'au 31 août&lt;/th&gt;
&lt;th&gt;Dès le 1er sept.&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input standard&lt;/td&gt;
&lt;td&gt;2 $&lt;/td&gt;
&lt;td&gt;3 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;10 $&lt;/td&gt;
&lt;td&gt;15 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lecture cache (hit)&lt;/td&gt;
&lt;td&gt;0,20 $&lt;/td&gt;
&lt;td&gt;0,30 $&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retiens la troisième ligne, parce que c'est elle qui gagne la partie. Un cache hit coûte &lt;strong&gt;10 % du prix d'input&lt;/strong&gt;. Même après la hausse, lire depuis le cache à 0,30 $ reste moins cher que l'ancien input plein à 2 $. Autrement dit, le contexte que tu répètes à chaque appel — un system prompt costaud, une doc, des exemples few-shot — peut être payé une fois puis relu pour trois fois rien.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le prompt caching, concrètement
&lt;/h2&gt;

&lt;p&gt;Le principe est simple : tu marques un bloc stable avec &lt;code&gt;cache_control&lt;/code&gt;, et tout ce qui précède ce marqueur est mis en cache. Le premier appel paie une écriture ; les suivants, dans la fenêtre TTL, lisent à 10 %.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;DOCS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_docs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# ~20 000 tokens, identiques à chaque requête
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Assistant support de l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app Lumière.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DOCS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# TTL 5 min
&lt;/span&gt;            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La question de l'utilisateur arrive &lt;strong&gt;après&lt;/strong&gt; le bloc caché : elle est volatile, elle n'est pas mise en cache, et c'est exactement ce qu'on veut. Ne te fie jamais à ton intuition. Vérifie dans &lt;code&gt;usage&lt;/code&gt;, qui ne ment pas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Comment j&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;annule un rendez-vous ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 1er appel : cache_creation_input_tokens=20000, cache_read_input_tokens=0
# appels &amp;lt; 5 min : cache_creation_input_tokens=0, cache_read_input_tokens=20000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si &lt;code&gt;cache_read_input_tokens&lt;/code&gt; reste à zéro alors que tu t'attends à un hit, tu as un bug quelque part — et ce bug te coûte de l'argent en silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les pièges du cache que j'ai payés
&lt;/h2&gt;

&lt;p&gt;Le caching n'est pas magique, et il échoue sans lever d'exception. Voici les cinq façons de croire qu'on cache alors qu'on paie plein tarif.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimum 1024 tokens.&lt;/strong&gt; Sous ce seuil, Sonnet ne met rien en cache et l'API ne dit rien. Tu penses optimiser, tu factures normalement. Contrôle &lt;code&gt;cache_read_input_tokens&lt;/code&gt; sur un vrai appel avant de conclure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'ordre décide de tout.&lt;/strong&gt; Le &lt;code&gt;cache_control&lt;/code&gt; est une frontière : avant, c'est caché ; après, non. Mets le stable en premier (system, docs, few-shots) et le volatil en dernier (question, historique récent). Un identifiant de session glissé en tête casse le préfixe à chaque appel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un token qui change = cache manqué.&lt;/strong&gt; Le hit exige un préfixe identique au byte près. Une date injectée dans le system prompt, un &lt;code&gt;timestamp&lt;/code&gt;, un ordre de clés JSON non déterministe : le cache saute et tu repaies l'écriture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L'écriture coûte plus cher que l'input plein&lt;/strong&gt; — 1,25× en TTL 5 min, 2× en TTL 1 h. Cacher un contexte utilisé une seule fois est une perte sèche. Le cache ne gagne que si le même préfixe est relu plusieurs fois dans la fenêtre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le TTL expire en silence.&lt;/strong&gt; Il se rafraîchit à chaque hit, mais un trafic clairsemé — une requête tous les quarts d'heure — laisse le cache mourir entre deux appels, et tu repaies l'écriture à répétition. Trafic régulier, ou passe en TTL 1 h avec &lt;code&gt;{"type": "ephemeral", "ttl": "1h"}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Le batch pour tout ce qui n'est pas temps réel
&lt;/h2&gt;

&lt;p&gt;Le caching couvre le contexte répété. Le batch couvre une autre moitié du travail : tout ce qui peut attendre. L'API Batch applique &lt;strong&gt;−50 % sur l'input ET l'output&lt;/strong&gt;, en échange d'un traitement asynchrone (garanti sous 24 h, souvent bien plus rapide).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Résume : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu ne batches pas un chatbot en direct, évidemment. Mais l'indexation nocturne, la génération d'embeddings de description, la classification d'un backlog de tickets, les rapports du matin : tout ça part en batch et coûte moitié prix. Et sur les jobs où le même préfixe revient, la remise batch et la remise cache se cumulent.&lt;/p&gt;

&lt;h2&gt;
  
  
  La facture avant/après, en clair
&lt;/h2&gt;

&lt;p&gt;Un cas concret vaut mieux qu'un discours. Bot de support en RAG : 20 000 tokens de contexte réutilisé, 500 tokens de question, 800 tokens de réponse, 10 000 requêtes par mois.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scénario&lt;/th&gt;
&lt;th&gt;Coût/mois&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Août, sans cache (2 $/10 $)&lt;/td&gt;
&lt;td&gt;~490 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Septembre, sans cache (3 $/15 $)&lt;/td&gt;
&lt;td&gt;~735 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Septembre, &lt;strong&gt;avec cache&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;~195 $&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La hausse brute te fait passer de 490 à 735 $, soit +245 $. Mais le contexte caché fait tomber la facture de septembre à 195 $ — moins que ce que tu payais en août au tarif de lancement. La bonne lecture n'est donc pas « l'API augmente de 50 % » : c'est « l'API punit ceux qui envoient 20 000 tokens en plein tarif à chaque appel ». Le +50 % existe. Il ne te concerne que si tu n'as rien optimisé.&lt;/p&gt;

&lt;p&gt;Un dernier réflexe avant de migrer : re-mesure ta consommation réelle sur Sonnet 5 plutôt que de reprendre tes compteurs d'un modèle précédent. Le nombre de tokens facturés pour un même texte dépend du tokenizer du modèle, et il peut différer de ce que tu avais l'habitude de voir. Ta seule source fiable reste le champ &lt;code&gt;usage&lt;/code&gt; renvoyé par l'API. Pas une estimation faite à la main.&lt;/p&gt;

&lt;h2&gt;
  
  
  Par où commencer lundi
&lt;/h2&gt;

&lt;p&gt;Trois gestes, dans l'ordre. Instrumente d'abord : logue &lt;code&gt;input_tokens&lt;/code&gt;, &lt;code&gt;output_tokens&lt;/code&gt;, &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; et &lt;code&gt;cache_read_input_tokens&lt;/code&gt; sur chaque appel, sinon tu optimises à l'aveugle. Isole ensuite ton plus gros préfixe stable et pose-y un &lt;code&gt;cache_control&lt;/code&gt;. Bascule enfin en batch tout ce qui n'a pas besoin d'une réponse dans la seconde.&lt;/p&gt;

&lt;p&gt;Si tu veux le détail de qui paie plus et qui ne paie rien (abonnés Pro/Max, utilisateurs restés sur Sonnet 4.6), on l'a posé à plat dans &lt;a href="https://neo-va.com/blog/hausse-prix-api-claude-septembre-2026/" rel="noopener noreferrer"&gt;la fiche sur la hausse&lt;/a&gt;. Et si tu construis des apps où le coût IA doit tenir en production sans y penser tous les matins, c'est exactement ce qu'on apprend à cadrer dans la &lt;a href="https://neo-va.com/blog/formation-claude-code/" rel="noopener noreferrer"&gt;formation Claude Code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>Voice-to-code 100 % local : Whisper + Claude Code, zéro octet au cloud</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Sun, 09 Aug 2026 15:55:38 +0000</pubDate>
      <link>https://dev.to/hernanz/voice-to-code-100-local-whisper-claude-code-zero-octet-au-cloud-3420</link>
      <guid>https://dev.to/hernanz/voice-to-code-100-local-whisper-claude-code-zero-octet-au-cloud-3420</guid>
      <description>&lt;p&gt;Coder à la voix avec ChatGPT, ça marche. Le hic tient en une ligne : chaque mot que tu dictes part chez OpenAI. Depuis le 23 juillet 2026, Codex se pilote à la voix — il ouvre une pull request, cherche l'origine d'un bug, tout ça dans une phrase. Pratique pour un side-project. Rédhibitoire quand le code appartient à un client.&lt;/p&gt;

&lt;p&gt;On voulait le même confort sans la fuite. Le résultat est un pipeline 100 % local : &lt;strong&gt;faster-whisper&lt;/strong&gt; pour la transcription, &lt;strong&gt;Claude Code&lt;/strong&gt; et sa commande &lt;code&gt;/voice&lt;/code&gt; pour l'agent. Rien ne sort de la machine — ni la voix, ni le contexte, ni le code. Voici la config exacte, la latence qu'on mesure sur un M2, et les deux bugs qui nous ont coûté une demi-journée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi pas simplement Codex vocal ?
&lt;/h2&gt;

&lt;p&gt;Parce que « coder à la voix » cache deux choses qu'on confond tout le temps. Le mode vocal de ChatGPT est fait pour &lt;em&gt;converser&lt;/em&gt; : il répond, il temporise, il reformule. Dicter du code, c'est l'inverse — tu veux une transcription fidèle et muette, qui ne discute pas, ne reformule pas et n'ajoute rien à ce que tu dis. Deux gestes opposés.&lt;/p&gt;

&lt;p&gt;Le vrai stack n'est donc jamais « ChatGPT vocal seul ». C'est un outil de dictée précis d'un côté, un agent de code de l'autre. Codex vocal fait les deux dans le cloud pour 20 €/mois ; un setup local sépare les deux briques et garde tout sur ta machine. Le tour d'horizon complet — prix, outils, cas d'usage — est dans &lt;a href="https://dev.to/coder-a-la-voix-avec-chatgpt/"&gt;le guide de référence&lt;/a&gt; ; ici, on reste sur le terrain technique.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le chemin le plus court : &lt;code&gt;/voice&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Depuis mars 2026, Claude Code embarque un mode vocal. Tu tapes &lt;code&gt;/voice&lt;/code&gt; dans le terminal, tu tiens la barre d'espace, tu parles, tu relâches. La transcription passe par un Whisper local, pas par une API distante.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; /voice
[hold space to talk · release to send]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour 90 % des cas, ça suffit. Tu dictes une intention, l'agent écrit le code, tu relis. Si tu veux garder la main sur le modèle, la langue et le vocabulaire technique, il faut descendre d'un cran et brancher ta propre transcription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le pipeline DIY, brique par brique
&lt;/h2&gt;

&lt;p&gt;Trois morceaux : capturer l'audio, le transcrire, l'injecter. On enregistre avec &lt;code&gt;sox&lt;/code&gt;, on transcrit avec faster-whisper, on colle le texte dans le prompt de l'agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;sox
pip &lt;span class="nb"&gt;install &lt;/span&gt;faster-whisper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enregistrement à la volée, arrêté au Ctrl-C :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sox &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; clip.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transcription, en forçant le français :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faster_whisper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WhisperModel&lt;/span&gt;

&lt;span class="c1"&gt;# int8 pour tenir sur CPU Apple Silicon (pas de Metal via CTranslate2)
&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WhisperModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compute_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un détail qui compte : sur Mac, faster-whisper tourne sur CPU, pas sur le GPU. Le modèle &lt;code&gt;large-v3&lt;/code&gt; est le plancher de précision en français, mais il rame ; &lt;code&gt;medium&lt;/code&gt; reste le bon compromis. Évite les modèles &lt;code&gt;distil-*&lt;/code&gt;, optimisés pour l'anglais. Sur du français, ils inventent. Si tu veux le GPU, whisper.cpp l'exploite via Metal et va plus vite ; on est resté sur faster-whisper pour son API Python, plus simple à scripter et à biaiser.&lt;/p&gt;

&lt;h2&gt;
  
  
  La latence, chiffrée
&lt;/h2&gt;

&lt;p&gt;On a mesuré le temps entre la fin d'une phrase de 8 secondes et le texte prêt à coller, sur un MacBook M2 16 Go :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modèle&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Temps&lt;/th&gt;
&lt;th&gt;Précision FR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int8&lt;/td&gt;
&lt;td&gt;~1,2 s&lt;/td&gt;
&lt;td&gt;correcte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large-v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int8&lt;/td&gt;
&lt;td&gt;~3,5 s&lt;/td&gt;
&lt;td&gt;meilleure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex vocal (cloud)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;200-450 ms&lt;/td&gt;
&lt;td&gt;excellente&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le cloud gagne la course de vitesse, sans surprise. Mais 1,2 seconde par phrase ne casse pas un flux de code : tu ne dictes pas à 160 mots/minute en continu, tu réfléchis entre deux instructions. Le vrai coût du local n'est pas la latence, c'est la config initiale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bug qui nous a coûté une demi-journée : le franglais
&lt;/h2&gt;

&lt;p&gt;Voilà le piège que personne ne teste. Dicte « crée une fonction getUserData » à un Whisper réglé en français, et tu récupères ça :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crée une fonction get user data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois mots séparés, en minuscules, avec un point ajouté par le modèle. Multiplie ce raté par chaque &lt;code&gt;useState&lt;/code&gt;, chaque &lt;code&gt;endpoint&lt;/code&gt;, chaque nom de variable en anglais, et la transcription entière devient inutilisable au moment de la coller. Le réflexe — épeler, ou dicter la syntaxe à la main — tue alors tout le gain de vitesse. Retour à la case départ.&lt;/p&gt;

&lt;p&gt;La bonne réponse tient en deux règles. D'abord, &lt;strong&gt;on ne dicte jamais les identifiants&lt;/strong&gt; : on décrit l'intention (« crée une fonction qui récupère les données utilisateur ») et on laisse l'agent nommer. Ensuite, pour les termes inévitables, on biaise le décodeur avec un &lt;code&gt;initial_prompt&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clip.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;initial_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Termes techniques : useState, getUserData, TypeScript, PostgreSQL, endpoint, refactor.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce prompt ne transcrit rien — il oriente le vocabulaire. Nos noms de variables anglais sont ressortis corrects dès qu'ils y figuraient. C'est le genre de détail qu'on ne trouve dans aucun tuto grand public, parce qu'aucun ne code réellement en français.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que la voix fait bien, ce qu'elle rate
&lt;/h2&gt;

&lt;p&gt;Après une semaine sur ce setup, le verdict est net. La voix excelle pour &lt;strong&gt;prompter&lt;/strong&gt; l'agent et &lt;strong&gt;naviguer&lt;/strong&gt; ; elle est mauvaise dès qu'il faut de la précision caractère par caractère.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ce qui marche :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;décrire une intention longue sans lâcher le clavier des yeux ;&lt;/li&gt;
&lt;li&gt;lancer des commandes (« ajoute un test pour ce cas », « refactore cette fonction ») ;&lt;/li&gt;
&lt;li&gt;coder quand tes poignets crient — la raison historique du voice coding, née des TMS, pas de la hype.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ce qui casse :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;taper la syntaxe exacte : accolades, indentation, opérateurs ;&lt;/li&gt;
&lt;li&gt;les noms de variables et d'API en anglais (voir plus haut) ;&lt;/li&gt;
&lt;li&gt;la revue de code fine, qui reste un travail d'œil et de tête.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La voix change l'interface, pas le métier. Elle déplace la frappe du clavier vers le micro, mais elle ne supprime ni la décision d'architecture, ni le choix des noms, ni la relecture ligne à ligne. L'agent génère, l'ingénieur tranche — et aucun micro ne tranchera à sa place.&lt;/p&gt;

&lt;h2&gt;
  
  
  On garde quoi ?
&lt;/h2&gt;

&lt;p&gt;Pour du code perso, Codex vocal est plus simple et plus rapide — assume juste que ta voix part dans le cloud. Tout dépend de l'enjeu. Pour du code client ou propriétaire, le pipeline local Whisper + &lt;code&gt;/voice&lt;/code&gt; est le seul qui garantit que rien ne fuit, au prix d'une latence d'une seconde et d'une soirée de config.&lt;/p&gt;

&lt;p&gt;Si tu veux le monter proprement — modèle, vocabulaire, push-to-talk, intégration à ton éditeur — c'est exactement ce qu'on démonte étape par étape dans la &lt;a href="https://dev.to/formation-claude-code/"&gt;formation Claude Code&lt;/a&gt;. Et pour rester côté outils, on a comparé &lt;a href="https://dev.to/claude-code-vs-cursor/"&gt;Claude Code et Cursor&lt;/a&gt; et détaillé le &lt;a href="https://dev.to/mode-vocal-claude-gmail-agenda/"&gt;mode vocal de Claude sur Gmail et l'agenda&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>claudecode</category>
      <category>ia</category>
      <category>productivite</category>
    </item>
    <item>
      <title>Intégrer « Sign in with ChatGPT » : le flux OAuth 2.0 + OIDC, avec le code</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Tue, 04 Aug 2026 19:31:20 +0000</pubDate>
      <link>https://dev.to/hernanz/integrer-sign-in-with-chatgpt-le-flux-oauth-20-oidc-avec-le-code-17df</link>
      <guid>https://dev.to/hernanz/integrer-sign-in-with-chatgpt-le-flux-oauth-20-oidc-avec-le-code-17df</guid>
      <description>&lt;p&gt;Le 2 août 2026, OpenAI a mis « Sign in with ChatGPT » en bêta. Pas de nouveau protocole. C'est OAuth 2.0 + OpenID Connect, flux Authorization Code, exactement le socle de « Sign in with Google ». Si tu as déjà branché un provider OIDC, tu connais 90 % du travail, et les 10 % restants sont précisément là où les intégrations se cassent — vérification du token, nonce, lock-in. C'est de ça qu'on parle.&lt;/p&gt;

&lt;p&gt;Six partenaires étaient présents au lancement : Airtable, GitLab, HubSpot, Notion, Supabase, Vercel. OpenAI pousse les devs à tester avec des crédits API (+5 $ en Plus, +50 $ en Pro, d'après une source unique à reconfirmer). L'incitation est réelle. Mais elle ne change rien au vrai sujet : ce que ton backend fait du token une fois qu'il l'a reçu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le flux, en six temps
&lt;/h2&gt;

&lt;p&gt;OpenAI devient un &lt;em&gt;fournisseur d'identité&lt;/em&gt;. Ton app ne voit jamais le mot de passe de l'utilisateur ; elle reçoit un jeton signé qui atteste « cette personne est bien untel ». Le flux Authorization Code, avec PKCE, se déroule ainsi :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ton serveur redirige l'utilisateur vers l'endpoint d'autorisation d'OpenAI, avec &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;redirect_uri&lt;/code&gt;, &lt;code&gt;scope=openid profile email&lt;/code&gt;, un &lt;code&gt;state&lt;/code&gt; et un &lt;code&gt;code_challenge&lt;/code&gt; (PKCE).&lt;/li&gt;
&lt;li&gt;L'utilisateur s'authentifie chez OpenAI et consent au partage.&lt;/li&gt;
&lt;li&gt;OpenAI le renvoie sur ta &lt;code&gt;redirect_uri&lt;/code&gt; avec un &lt;code&gt;code&lt;/code&gt; court et le &lt;code&gt;state&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ton backend échange ce &lt;code&gt;code&lt;/code&gt; contre un &lt;code&gt;access_token&lt;/code&gt; &lt;strong&gt;et&lt;/strong&gt; un &lt;code&gt;id_token&lt;/code&gt; (un JWT).&lt;/li&gt;
&lt;li&gt;Tu vérifies la signature de l'&lt;code&gt;id_token&lt;/code&gt;, puis &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt; et le &lt;code&gt;nonce&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tu lis les claims : &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;picture&lt;/code&gt;. Fin.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Le point clé est à l'étape 4. Le &lt;code&gt;code&lt;/code&gt; ne voyage jamais dans le navigateur au-delà d'un aller simple, et l'échange se fait de serveur à serveur, ton &lt;code&gt;client_secret&lt;/code&gt; à l'appui. C'est ce qui rend le flux Authorization Code plus sûr que l'ancien flux implicite, désormais déconseillé.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le code qui marche (Node + openid-client)
&lt;/h2&gt;

&lt;p&gt;Ne réécris pas la vérification cryptographique à la main. La lib &lt;code&gt;openid-client&lt;/code&gt; gère la découverte, PKCE, le &lt;code&gt;state&lt;/code&gt;, le &lt;code&gt;nonce&lt;/code&gt; et la validation JWKS. Voici la configuration, à partir du document de découverte OIDC :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generators&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// L'issuer exact est fourni dans la console OpenAI quand tu enregistres ton app.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Issuer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://auth.openai.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect_uris&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ton-app.com/auth/chatgpt/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;response_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La redirection génère et stocke les secrets éphémères — &lt;code&gt;code_verifier&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt; — côté session :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/chatgpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code_verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codeVerifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code_challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pkce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorizationUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid profile email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;code_challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code_challenge_method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le callback fait le vrai travail. Un seul appel échange le code, vérifie la signature contre le JWKS d'OpenAI et recale &lt;code&gt;state&lt;/code&gt; comme &lt;code&gt;nonce&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/chatgpt/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pkce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callbackParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ton-app.com/auth/chatgpt/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// { sub, name, email, picture, iss, aud, exp, iat, ... }&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Les trois pièges qui cassent tout
&lt;/h2&gt;

&lt;p&gt;Le flux paraît simple, et c'est bien le problème : on croit avoir fini alors qu'on vient d'ouvrir une faille.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Décoder n'est pas vérifier.&lt;/strong&gt; Un &lt;code&gt;jwt.decode(id_token)&lt;/code&gt; te rend les claims sans contrôler la signature. N'importe qui peut forger un JWT avec &lt;code&gt;email: patron@boite.com&lt;/code&gt;, et sans validation JWKS, tu le gobes. Il faut vérifier la signature contre les clés publiques d'OpenAI, à chaque connexion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le &lt;code&gt;nonce&lt;/code&gt; oublié.&lt;/strong&gt; Sans lui, un &lt;code&gt;id_token&lt;/code&gt; intercepté peut être rejoué. Tu le génères à l'aller, tu le stockes en session, tu vérifies qu'il revient identique — c'est exactement ce que &lt;code&gt;openid-client&lt;/code&gt; fait si tu le lui passes, d'où l'intérêt de ne pas « simplifier » l'appel &lt;code&gt;callback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aud&lt;/code&gt; et &lt;code&gt;iss&lt;/code&gt; non contrôlés.&lt;/strong&gt; Un token parfaitement signé mais émis pour une autre app, ou par un autre émetteur, doit être rejeté. C'est le genre de trou qu'un pentest trouve en dix minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le &lt;code&gt;sub&lt;/code&gt; est ton identifiant stable, pas l'e-mail. Un utilisateur peut changer d'adresse ; le &lt;code&gt;sub&lt;/code&gt; d'OpenAI, non. Clé ta table utilisateurs dessus, sinon tu fusionneras deux comptes le jour d'un changement d'e-mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que tu reçois, et ce que tu paies
&lt;/h2&gt;

&lt;p&gt;Côté données, ton app ne récupère que trois champs : nom, e-mail, photo de profil. C'est peu. Ça t'évite surtout de stocker un mot de passe. Mais l'échange a un prix qu'aucun &lt;code&gt;console.log&lt;/code&gt; ne t'affichera : à chaque connexion, OpenAI voit quelle app, quel appareil, quand et où — et aucune politique de rétention publique n'existe pour ces logs.&lt;/p&gt;

&lt;p&gt;Ajoute le reste de la facture avant d'appuyer sur &lt;em&gt;merge&lt;/em&gt; : c'est une bêta, l'API peut bouger sans préavis, et tu déportes ton login layer chez un tiers. Ce lock-in est le vrai arbitrage, pas la difficulté technique — elle, tu l'as réglée en trois handlers. La question « à qui appartient l'accès à mes utilisateurs ? » rejoint celle de &lt;a href="https://neo-va.com/blog/proprietaire-code-source-application/" rel="noopener noreferrer"&gt;la propriété du code&lt;/a&gt; : on ne la voit pas tant que tout va bien.&lt;/p&gt;

&lt;p&gt;Mon conseil de terrain : implémente-le en secondaire, jamais comme unique porte d'entrée. Garde un e-mail/mot de passe ou un second provider, et fais du &lt;code&gt;sub&lt;/code&gt; OpenAI un lien parmi d'autres vers ton compte. Le jour où la bêta casse, tes utilisateurs se connectent quand même.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Neova est un studio de développement. Nous construisons des applications sur mesure, et l'auth en fait partie — c'est ce qui nous a poussés à disséquer ce flux le jour du lancement. Le décryptage des deux sens de « se connecter avec ChatGPT » est &lt;a href="https://neo-va.com/blog/se-connecter-avec-chatgpt/" rel="noopener noreferrer"&gt;sur le blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>chatgpt</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>GPT-5.6 Luna à 1,40 $/M : on a migré une pipeline de classification, voici la facture</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:11:55 +0000</pubDate>
      <link>https://dev.to/hernanz/gpt-56-luna-a-140-m-on-a-migre-une-pipeline-de-classification-voici-la-facture-3ci</link>
      <guid>https://dev.to/hernanz/gpt-56-luna-a-140-m-on-a-migre-une-pipeline-de-classification-voici-la-facture-3ci</guid>
      <description>&lt;p&gt;Luna, le plus léger des trois modèles GPT-5.6, est passé de 7 $ à 1,40 $ le million de tokens le 30 juillet 2026 — une baisse de 80 % annoncée trois semaines après le lancement de la famille. Pour nous, ce n'était pas une actualité à lire : c'était une pipeline de classification qu'on avait garée sur Terra, faute de rentabilité, qui redevenait viable. On l'a migrée dans la foulée. Sur 100 000 classifications par mois, la facture est tombée de &lt;strong&gt;176 $ à 18 $&lt;/strong&gt; — presque 10×, parce qu'on change de modèle et qu'on encaisse la baisse en même temps. Voici le code, les chiffres réels, et les deux pièges qui bouffent l'économie si tu ne les vois pas venir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi une baisse de 80 % débloque une migration
&lt;/h2&gt;

&lt;p&gt;Un modèle moins cher ne déclenche pas une migration à lui seul. Migrer une pipeline en prod, c'est revalider la qualité sur un jeu de test, réécrire les prompts et surveiller les régressions — du travail réel qui a un coût. Quand Luna est sorti à 7 $ le million en juillet, l'écart avec Terra ne payait pas ce travail. On est donc restés sur Terra. À 1,40 $, l'équation bascule : la sortie tombe de 6 $ à 1,20 $, l'entrée de 1 $ à 0,20 $, et l'économie annuelle dépasse largement le coût de la revalidation. C'est ce que la grille tarifaire ne dit pas. Une baisse n'a de sens que rapportée au travail qu'elle finance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Modèle GPT-5.6&lt;/th&gt;
&lt;th&gt;Input ($/M)&lt;/th&gt;
&lt;th&gt;Output ($/M)&lt;/th&gt;
&lt;th&gt;Avant&lt;/th&gt;
&lt;th&gt;Variation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Luna&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0,20&lt;/td&gt;
&lt;td&gt;1,20&lt;/td&gt;
&lt;td&gt;7 $ (combiné)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−80 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,00&lt;/td&gt;
&lt;td&gt;12,00&lt;/td&gt;
&lt;td&gt;2,50 / 15&lt;/td&gt;
&lt;td&gt;−20 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,00&lt;/td&gt;
&lt;td&gt;30,00&lt;/td&gt;
&lt;td&gt;inchangé&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Le classifieur tient en un appel
&lt;/h2&gt;

&lt;p&gt;Notre tâche est banale : on range des tickets de support entrants dans une quinzaine de catégories, avec un score de confiance. Un prompt système liste les catégories, l'utilisateur envoie le texte du ticket, le modèle renvoie du JSON. Rien d'exotique. Et c'est justement le genre de charge où Luna excelle, parce qu'elle ne demande aucun raisonnement profond.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.6-luna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;# ← garde cet œil ouvert
&lt;/span&gt;        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CATEGORIES_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On renvoie aussi &lt;code&gt;resp.usage&lt;/code&gt; : c'est lui qui contient la vraie facture, pas la grille affichée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège n°1 : le raisonnement se facture en sortie
&lt;/h2&gt;

&lt;p&gt;La première version tournait avec &lt;code&gt;effort: "medium"&lt;/code&gt;, parce qu'on s'était dit qu'un peu de raisonnement ne ferait pas de mal à la précision. Erreur de facturation classique. Sur un modèle de raisonnement, les tokens de réflexion sont comptés en &lt;strong&gt;sortie&lt;/strong&gt;, au prix fort. Pour une classification qui rend 30 tokens de JSON, Luna en générait 450 de raisonnement invisible — quinze fois le volume utile, facturé à 1,20 $ le million. Résultat sur nos 100 000 requêtes : &lt;strong&gt;72 $ au lieu de 18 $&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Et la précision, dans tout ça ? +0,3 point sur notre jeu de test, dans le bruit. On est repassés en &lt;code&gt;minimal&lt;/code&gt;, la facture a fondu, la qualité n'a pas bougé. Pour du tri, le raisonnement est de l'argent jeté par la fenêtre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège n°2 : mesure la vraie facture, pas la théorique
&lt;/h2&gt;

&lt;p&gt;Le prix affiché, c'est le plafond. Deux leviers le font descendre, et aucun ne se voit dans le communiqué d'OpenAI :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Batch&lt;/strong&gt; — dépôt asynchrone, résultat sous 24 h, &lt;strong&gt;−50 %&lt;/strong&gt; sur tout ce qui n'a pas besoin de réponse immédiate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache de prompt&lt;/strong&gt; — notre prompt système fait 320 tokens répétés à l'identique ; mis en cache, ce préfixe est facturé une fraction du tarif au lieu d'être repayé 100 000 fois.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PRIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;# $ par million de tokens
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.6-luna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.20&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.6-terra&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.00&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRIX&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.6-luna&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resultats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Facture réelle : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; $&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cumulés — à la louche, parce que les deux ne se combinent pas parfaitement — nos 18 $ passent sous les 8 $ pour le même volume. Mesure toujours sur &lt;code&gt;usage&lt;/code&gt;, jamais sur la grille : c'est la seule facture qui compte.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quand on est resté sur Terra
&lt;/h2&gt;

&lt;p&gt;Luna n'a pas remplacé Terra partout, et c'est volontaire. Sur une seconde pipeline — de l'extraction d'entités dans des contrats, avec des cas limites tordus et des conséquences métier si on se trompe — Luna perdait 4 points de rappel face à Terra. Quatre points sur de l'extraction juridique, ce n'est pas du bruit : ce sont des erreurs qui remontent chez le client. L'économie de 90 % ne valait pas ce risque.&lt;/p&gt;

&lt;p&gt;On a donc gardé Terra là, Luna sur le tri, et Sol nulle part — parce que rien dans ces deux tâches ne justifie un modèle frontier à 30 $ la sortie. Le bon réflexe n'est jamais « tout migrer vers le moins cher ». C'est mesurer la qualité tâche par tâche, puis choisir par tâche.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'on en retient
&lt;/h2&gt;

&lt;p&gt;La vraie leçon n'est pas le prix du jour. C'est que Luna a perdu 80 % de sa valeur faciale en trois semaines. Et ça peut recommencer, dans un sens ou dans l'autre. Si tu câbles ta pipeline en dur autour d'un modèle et d'un prix, le prochain mouvement du marché te prend de court. On isole donc toujours l'appel LLM derrière une fonction (&lt;code&gt;classify()&lt;/code&gt;, &lt;code&gt;extract()&lt;/code&gt;) et une table de prix : changer de modèle devient une ligne, pas un chantier. C'est ce qui nous a permis de migrer celle-ci en une après-midi.&lt;/p&gt;

&lt;p&gt;La guerre des prix ne fait que commencer, alimentée par la pression des modèles chinois open-weight — on l'a détaillée dans &lt;a href="https://blog.neo-va.com/kimi-k3-vs-claude-pour-coder/" rel="noopener noreferrer"&gt;Kimi K3 vs Claude pour coder&lt;/a&gt;. Pour le décryptage complet de la baisse GPT-5.6 et son impact sur un budget d'automatisation, &lt;a href="https://blog.neo-va.com/gpt-5-6-baisse-prix-openai/" rel="noopener noreferrer"&gt;c'est par ici&lt;/a&gt;. Et si tu veux apprendre à construire ce genre de pipeline proprement, on en a fait une &lt;a href="https://blog.neo-va.com/formation-claude-code/" rel="noopener noreferrer"&gt;formation Claude Code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>openai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Le mode vocal de Claude modifie ton agenda. Le déclencheur, c'est à toi de le coder.</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Thu, 30 Jul 2026 15:08:54 +0000</pubDate>
      <link>https://dev.to/hernanz/le-mode-vocal-de-claude-modifie-ton-agenda-le-declencheur-cest-a-toi-de-le-coder-5f4l</link>
      <guid>https://dev.to/hernanz/le-mode-vocal-de-claude-modifie-ton-agenda-le-declencheur-cest-a-toi-de-le-coder-5f4l</guid>
      <description>&lt;p&gt;Le mode vocal de Claude modifie vraiment ton Google Agenda. Il ne le fait que quand tu parles.&lt;/p&gt;

&lt;p&gt;Depuis la mise à jour du 23 juillet 2026, le vocal tourne sur Opus, Sonnet et Haiku, parle enfin français, et se connecte à Gmail et Agenda via OAuth. Dis « décale mon rendez-vous de 14 h à demain », il modifie l'agenda pour de bon. C'est bluffant à l'usage. Mais si tu penses « automatisation », deux limites tombent tout de suite : il rédige les mails sans les envoyer, et il n'agit &lt;strong&gt;que&lt;/strong&gt; sur ton ordre vocal. Aucun événement, aucun webhook, aucune boucle en arrière-plan ne le réveille. Pour un process répétable, le déclencheur est du code que tu écris — et voici à quoi il ressemble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Où le vocal s'arrête, précisément
&lt;/h2&gt;

&lt;p&gt;Le vocal fonctionne en tour de rôle : écouter → réfléchir → répondre. C'est un assistant synchrone, piloté par la voix. Rien d'autre ne le lance. Anthropic n'a d'ailleurs pas retouché le modèle vocal lui-même dans cette mise à jour ; ce qui change, c'est l'accès aux apps, pas la mécanique conversationnelle.&lt;/p&gt;

&lt;p&gt;Ce qu'il fait, quand tu parles :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;crée / modifie / supprime un événement, trouve des créneaux, gère les récurrences, ajoute un lien Meet ;&lt;/li&gt;
&lt;li&gt;rédige un brouillon dans Gmail ;&lt;/li&gt;
&lt;li&gt;se connecte à une seule app en gratuit (Haiku), à plusieurs en payant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ce qu'il ne fait pas :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;envoyer&lt;/strong&gt; le mail — le clic « Envoyer » reste manuel, à chaque fois ;&lt;/li&gt;
&lt;li&gt;se déclencher sans toi : aucun cron, aucune écoute de la boîte ;&lt;/li&gt;
&lt;li&gt;tourner pendant que tu dors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La ligne de partage tient en un mot. Un assistant attend qu'on lui parle, un agent réagit à un événement ; la différence, ce n'est pas le modèle, c'est le déclencheur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le déclencheur, c'est du code — pas une phrase
&lt;/h2&gt;

&lt;p&gt;« Quand un client écrit pour un rendez-vous, réserve un créneau et confirme » : cette règle n'existe nulle part dans le vocal. Pour l'obtenir, il faut quelque chose qui &lt;strong&gt;écoute&lt;/strong&gt; la boîte en continu. Sur Gmail, ça passe par &lt;code&gt;users.watch&lt;/code&gt; et une notification Pub/Sub à chaque nouveau message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;projects/mon-projet/topics/gmail-inbound&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;labelIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INBOX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux pièges que la doc mentionne à peine. D'abord, &lt;code&gt;watch&lt;/code&gt; &lt;strong&gt;expire au bout de 7 jours&lt;/strong&gt; : si tu ne le renouvelles pas avec un cron, les notifications s'arrêtent en silence, sans erreur. Ensuite, la notification Pub/Sub ne contient &lt;strong&gt;pas&lt;/strong&gt; le mail — juste un &lt;code&gt;historyId&lt;/code&gt;. Tu récupères le contenu réel en rappelant &lt;code&gt;users.history.list&lt;/code&gt; depuis le dernier &lt;code&gt;historyId&lt;/code&gt; connu. Beaucoup de gens perdent une soirée là-dessus en cherchant le corps du message dans le payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extraire l'intention avec l'API Claude
&lt;/h2&gt;

&lt;p&gt;Une fois le mail en main, tu délègues à Claude le seul truc qu'il fait mieux qu'une regex : comprendre une demande écrite en langage humain. Le tool use avec &lt;code&gt;tool_choice&lt;/code&gt; forcé te renvoie du JSON structuré. Pas de la prose à parser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract_meeting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Détecte une demande de rendez-vous dans un email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input_schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;wants_meeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;duration_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;preferred_dates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;attendee_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wants_meeting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tool_choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract_meeting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;emailBody&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_use&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Haiku suffit largement pour ce tri, et il coûte quelques centimes pour mille mails. Garde en tête que ces appels sont facturés au token — exactement comme le vocal, qui décompte chaque conversation de ton quota de plan. L'agent n'est pas gratuit, il est juste prévisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Réserver vraiment : freebusy puis events.insert
&lt;/h2&gt;

&lt;p&gt;Claude a dit oui. Reste à trouver un trou dans l'agenda, puis à poser l'événement. &lt;code&gt;freebusy.query&lt;/code&gt; te donne les blocs occupés, &lt;code&gt;events.insert&lt;/code&gt; crée le rendez-vous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calendar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freebusy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-03T08:00:00+02:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-03T18:00:00+02:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calendars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;busy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// à toi de calculer le créneau libre&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;calendarId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sendUpdates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// sinon l'invité n'est jamais prévenu&lt;/span&gt;
  &lt;span class="na"&gt;conferenceDataVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`RDV avec &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attendee_email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;attendees&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attendee_email&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;conferenceData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le piège qui coûte des rendez-vous fantômes : &lt;code&gt;sendUpdates&lt;/code&gt; vaut &lt;code&gt;false&lt;/code&gt; par défaut. L'événement apparaît dans &lt;strong&gt;ton&lt;/strong&gt; agenda, mais l'invité ne reçoit rien. Tu crois avoir automatisé la confirmation, elle n'est jamais partie. Deuxième piège, le fuseau : sans &lt;code&gt;timeZone&lt;/code&gt; explicite, un &lt;code&gt;dateTime&lt;/code&gt; naïf atterrit à la mauvaise heure selon le serveur. Mets toujours l'IANA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le scope qui bloque l'envoi
&lt;/h2&gt;

&lt;p&gt;Pourquoi le vocal rédige un brouillon au lieu d'envoyer ? Parce que &lt;code&gt;gmail.send&lt;/code&gt; est un &lt;strong&gt;restricted scope&lt;/strong&gt; chez Google. En production, il déclenche une vérification OAuth et un audit de sécurité CASA — long, payant, contraignant. Le connecteur de Claude reste volontairement en &lt;code&gt;gmail.compose&lt;/code&gt; : il crée le brouillon, il ne franchit pas cette porte.&lt;/p&gt;

&lt;p&gt;Si tu construis l'agent qui envoie vraiment, c'est &lt;strong&gt;toi&lt;/strong&gt; qui prends en charge cette vérification, et donc la responsabilité d'une IA qui écrit dans ta boîte sans supervision. Pense aussi à la révocation. Un accès OAuth accordé se coupe dans &lt;code&gt;myaccount.google.com/permissions&lt;/code&gt;, et tu devrais pouvoir le faire en une minute avant de connecter quoi que ce soit à ta messagerie. On détaille cette hygiène des permissions dans &lt;a href="https://blog.neo-va.com/mode-vocal-claude-gmail-agenda/" rel="noopener noreferrer"&gt;notre analyse produit du mode vocal&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assistant pour l'ad-hoc, agent pour le répétable
&lt;/h2&gt;

&lt;p&gt;Le verdict est net. Le vocal de Claude est excellent pour l'imprévu : « décale, résume ma journée, prépare un brouillon pour Paul ». Il gère l'ad-hoc mieux que n'importe quel raccourci clavier. Mais un process qui doit tourner sans toi — client écrit, créneau réservé, confirmation envoyée — n'est pas un problème de commande vocale, c'est un problème d'architecture : un déclencheur, un parseur, deux appels d'API, une gestion d'erreurs. Du code, en somme.&lt;/p&gt;

&lt;p&gt;C'est là que nous prenons le relais. Neova est un studio de développement : nous construisons ces agents sur mesure, avec l'IA et les technologies de pointe, pour qu'un process répétable s'exécute seul. Si tu préfères apprendre à les coder toi-même, on montre exactement cette bascule assistant → agent dans notre &lt;a href="https://blog.neo-va.com/creer-application-avec-claude-code/" rel="noopener noreferrer"&gt;guide pour créer une app avec Claude Code&lt;/a&gt; et dans le &lt;a href="https://blog.neo-va.com/agent-vocal-ia-service-client/" rel="noopener noreferrer"&gt;guide de l'agent vocal côté service client&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Le vocal te fait gagner des minutes. Un agent te rend des heures. À toi de voir laquelle des deux tu veux vraiment.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>node</category>
    </item>
    <item>
      <title>Migrer Claude Code vers Opus 5 : le piège de l'auto-vérification</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:39:04 +0000</pubDate>
      <link>https://dev.to/hernanz/migrer-claude-code-vers-opus-5-le-piege-de-lauto-verification-330f</link>
      <guid>https://dev.to/hernanz/migrer-claude-code-vers-opus-5-le-piege-de-lauto-verification-330f</guid>
      <description>&lt;p&gt;Opus 5 relit son propre code avant de te le rendre. C'est la meilleure nouvelle de cette version, et c'est aussi ce qui a fait grimper notre facture le premier jour. On a migré notre workflow Claude Code d'Opus 4.8 vers Opus 5 trois jours après la sortie du 24 juillet : changer l'identifiant &lt;code&gt;claude-opus-4-8&lt;/code&gt; en &lt;code&gt;claude-opus-5&lt;/code&gt; prend dix secondes, le reste a pris un après-midi. Deux choses ont cassé — nos instructions de vérification dans &lt;code&gt;CLAUDE.md&lt;/code&gt;, devenues redondantes et coûteuses, et un appel API qui renvoyait une 400 parce qu'on coupait la réflexion tout en réclamant l'effort &lt;code&gt;xhigh&lt;/code&gt;. Voici le détail, config à l'appui.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le swap d'identifiant, puis les vrais problèmes
&lt;/h2&gt;

&lt;p&gt;Opus 5 garde la grille tarifaire d'Opus 4.8 : 5 $ le million de tokens en entrée, 25 $ en sortie, réflexion active par défaut et effort réglé sur &lt;code&gt;high&lt;/code&gt;. La fenêtre de contexte monte à 1M de tokens, valeur par défaut comme maximum — il n'existe pas de variante plus courte. Dans Claude Code, le basculement tient en une ligne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/model claude-opus-5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jusqu'ici, rien de piégeux. Les ennuis commencent quand le modèle se met à travailler comme il l'entend, pas comme ton ancien &lt;code&gt;CLAUDE.md&lt;/code&gt; le lui dictait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 1 : Opus 5 se vérifie seul, tes instructions doublent le travail
&lt;/h2&gt;

&lt;p&gt;Opus 5 se relit sans qu'on le lui demande : il repasse sur ses diffs, délègue davantage à des sous-agents, et produit des réponses plus longues que 4.8. Si ton &lt;code&gt;CLAUDE.md&lt;/code&gt; transporte encore les rituels de vérification écrits pour l'ancien modèle, tu paies deux fois le même contrôle — une fois parce que tu l'as demandé, une fois parce que le modèle le fait de toute façon. Résultat : des sorties plus verbeuses, plus de tokens, une facture qui monte. Sans gain de qualité.&lt;/p&gt;

&lt;p&gt;Ce qu'on trimballait, hérité de 4.8 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Avant de rendre du code&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Relis chaque fichier modifié, ligne par ligne.
&lt;span class="p"&gt;-&lt;/span&gt; Relance la suite de tests deux fois pour confirmer.
&lt;span class="p"&gt;-&lt;/span&gt; Rédige un résumé des risques après chaque changement.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce qu'on a gardé pour Opus 5 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Avant de rendre du code&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Lance la suite de tests une fois.
&lt;span class="p"&gt;-&lt;/span&gt; Signale uniquement les tests rouges.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sur une session de feature classique — un CRUD avec validation et tests —, retirer ces trois lignes a fait baisser nos tokens de sortie d'environ un quart. Le code rendu était de qualité identique. La leçon tient en une phrase : ne dis plus à Opus 5 de vérifier, il le fait mieux quand tu le laisses tranquille.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 2 : l'erreur 400 sur xhigh et max
&lt;/h2&gt;

&lt;p&gt;L'échelle d'effort va de &lt;code&gt;low&lt;/code&gt; à &lt;code&gt;max&lt;/code&gt;, en passant par &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt; et &lt;code&gt;xhigh&lt;/code&gt;, avec la réflexion active par défaut. Le piège est logique une fois qu'on l'a vu : si tu désactives la réflexion et que tu réclames quand même &lt;code&gt;xhigh&lt;/code&gt; ou &lt;code&gt;max&lt;/code&gt;, l'API renvoie une 400, parce que les deux se contredisent. Un effort élevé, c'est précisément du raisonnement étendu — le couper puis en exiger le maximum n'a pas de sens.&lt;/p&gt;

&lt;p&gt;Ce qui casse :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 400 Bad Request — réflexion coupée mais effort maximal demandé
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xhigh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce qui passe :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;thinking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xhigh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si tu veux vraiment couper le raisonnement, pour une tâche mécanique où il ne sert à rien, plafonne l'effort à &lt;code&gt;medium&lt;/code&gt;. Au-delà, garde la réflexion active. C'est le genre d'erreur qui ne se voit pas en relisant le code, seulement en lisant le message d'erreur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quel effort pour quoi, dans notre usage
&lt;/h2&gt;

&lt;p&gt;On a fini par calibrer l'effort par type de tâche, plutôt que de laisser &lt;code&gt;high&lt;/code&gt; partout. Voici la grille qu'on applique après trois jours d'usage réel — elle vaut pour nos projets, pas comme vérité universelle :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tâche&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Pourquoi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Renommage, refactor mécanique&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;low&lt;/code&gt; / &lt;code&gt;medium&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;rien à gagner côté raisonnement, on économise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature CRUD standard + tests&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;high&lt;/code&gt; (défaut)&lt;/td&gt;
&lt;td&gt;le bon compromis coût/qualité&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug de concurrence, choix d'archi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xhigh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;le raisonnement étendu paie vraiment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presque rien&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rarement : le coût grimpe pour un gain marginal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le &lt;code&gt;fast mode&lt;/code&gt; mérite un mot : deux fois et demie plus rapide, à 10 $/50 $ le million, mais disponible sur l'API Claude seulement — ni Bedrock, ni Vertex, ni Foundry. Pour une boucle d'itération serrée, le gain de latence vaut le surcoût. Pour un batch de nuit, non.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bilan, après trois jours
&lt;/h2&gt;

&lt;p&gt;La migration technique se résume à un identifiant, deux pièges et une grille d'effort. Le vrai travail est ailleurs. Il faut désapprendre les réflexes de 4.8 : arrêter de sur-instruire un modèle qui en fait déjà plus tout seul. Détail utile au passage, le cache de prompt se déclenche désormais dès 512 tokens contre 1 024 avant — sur un &lt;code&gt;CLAUDE.md&lt;/code&gt; volumineux, ça se sent sur la facture. On continue de mesurer, et on ajustera la grille quand on aura plus de recul sur les tâches longues.&lt;/p&gt;

&lt;p&gt;Si tu montes une équipe sur Claude Code et que tu veux éviter de repayer ces trois jours de tâtonnement, on a condensé nos réglages dans notre &lt;a href="https://blog.neo-va.com/formation-claude-code/" rel="noopener noreferrer"&gt;formation Claude Code&lt;/a&gt;. Et si tu hésites encore entre Claude Code et Cursor pour ce genre de workflow, &lt;a href="https://blog.neo-va.com/claude-code-vs-cursor/" rel="noopener noreferrer"&gt;notre comparatif&lt;/a&gt; tranche.&lt;/p&gt;

&lt;p&gt;Prochaine étape de notre côté : vérifier si &lt;code&gt;xhigh&lt;/code&gt; tient ses promesses sur une refonte d'archi complète, pas seulement sur un bug isolé. On publiera les chiffres.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>coding</category>
      <category>migration</category>
    </item>
    <item>
      <title>Mesurer si une IA cite votre site — le script, et les 4 pièges qui faussent tout</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Sun, 26 Jul 2026 06:37:52 +0000</pubDate>
      <link>https://dev.to/hernanz/mesurer-si-une-ia-cite-votre-site-le-script-et-les-4-pieges-qui-faussent-tout-47l2</link>
      <guid>https://dev.to/hernanz/mesurer-si-une-ia-cite-votre-site-le-script-et-les-4-pieges-qui-faussent-tout-47l2</guid>
      <description>&lt;p&gt;Personne ne sait si les moteurs de réponse citent son site. On mesure les positions Google, les clics, les impressions — jamais les citations. Nous avons construit la sonde qui manquait, et les quatre pièges rencontrés en route valent plus que le code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le principe
&lt;/h2&gt;

&lt;p&gt;Pour chaque requête, on demande au modèle de chercher sur le web, de répondre, puis de rendre la liste des domaines sur lesquels il s'est appuyé. On cherche ensuite notre domaine dans cette liste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promptSources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requete&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`Un dirigeant de PME française tape
cette recherche : « &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ».

Cherche sur le web francophone, puis réponds-lui utilement.

Termine par CE bloc JSON et rien après :
{"sources":["exemple.fr","autre.com"]}

« sources » liste les domaines des pages sur lesquelles tu t'es appuyé,
du plus au moins déterminant.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'appel passe par la CLI &lt;code&gt;claude&lt;/code&gt;, ce qui évite une clé d'API supplémentaire :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--output-format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--setting-sources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebSearch,WebFetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--allowed-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebSearch,WebFetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← sans ces deux lignes, rien ne revient&lt;/span&gt;
&lt;span class="nx"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Piège 1 — ne jamais nommer la marque cherchée
&lt;/h2&gt;

&lt;p&gt;Demander « as-tu cité Néova ? » amène la réponse. On demande les sources, et on y cherche ensuite ce qu'on veut. C'est la différence entre une mesure et une auto-confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 2 — le prompt système contamine tout
&lt;/h2&gt;

&lt;p&gt;Notre pipeline de rédaction injecte un prompt système décrivant l'entreprise. Le réutiliser ici, c'était donner au modèle la réponse avant la question. La sonde n'a &lt;strong&gt;aucun&lt;/strong&gt; prompt système, et &lt;code&gt;--setting-sources&lt;/code&gt; vide pour ne pas hériter du fichier de configuration du projet, qui décrit l'entreprise en détail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 3 — un corpus moissonné, puis nettoyé
&lt;/h2&gt;

&lt;p&gt;Les requêtes viennent de l'autocomplétion Google et YouTube : ce sont des recherches réelles. Mais la moisson brute est sale. Sur 384 requêtes récoltées, 92 ne parlaient pas de logiciel — « accès à la propriété », « métier agent de propreté », « choisir mutuelle ». Le filtre qui manquait n'était pas sur l'intention, mais sur le &lt;strong&gt;sujet&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ANCRE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;application|logiciel|saas|développement|développeur&lt;/span&gt;&lt;span class="err"&gt;|
&lt;/span&gt;&lt;span class="nx"&gt;informatique&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;crm&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;erp&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;]?&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;hébergement&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S'ajoutent les requêtes géolocalisées (elles rendent des annuaires locaux, pas le paysage national), celles d'apprentissage ou d'emploi, et le support d'applications grand public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 4 — le corpus doit être figé
&lt;/h2&gt;

&lt;p&gt;Premier réflexe : moissonner à chaque édition. Erreur. Si les questions changent d'un mois à l'autre, on ne sait plus si le chiffre bouge parce que le marché bouge ou parce qu'on a changé de questions.&lt;/p&gt;

&lt;p&gt;Pire : Google bride l'autocomplétion quand on la sollicite trop vite. Une moisson a rendu 384 requêtes, la suivante une heure plus tard en a rendu 10, dont une exploitable — et le corpus s'est figé à &lt;strong&gt;une&lt;/strong&gt; question sans broncher. D'où ce garde-fou :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frais&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;PLANCHER&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ancien&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✗ moisson trop maigre (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;frais&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, plancher &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PLANCHER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// on refuse d'écrire plutôt que de figer un corpus mort&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un corpus dégénéré figé, c'est une étude morte qui a l'air vivante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ça a donné
&lt;/h2&gt;

&lt;p&gt;99 requêtes, 196 domaines cités, top 10 à 26 % des citations. Aucune autorité installée sur ce marché. Coût : environ 0,24 $ d'équivalent par requête.&lt;/p&gt;

&lt;p&gt;Et notre propre site : zéro citation. On l'a publié tel quel — &lt;a href="https://blog.neo-va.com/barometre-citations-ia/" rel="noopener noreferrer"&gt;l'étude complète est ici&lt;/a&gt;, méthode et limites incluses.&lt;/p&gt;

&lt;h2&gt;
  
  
  La limite à dire à voix haute
&lt;/h2&gt;

&lt;p&gt;Cette sonde interroge &lt;strong&gt;un&lt;/strong&gt; moteur. ChatGPT et Perplexity demandent chacun une clé d'API payante. Publier « voici notre présence dans ChatGPT » à partir d'une mesure faite ailleurs serait malhonnête, et se ferait démolir en commentaires. La structure prévoit l'ajout d'autres moteurs ; le chiffre, lui, dit exactement ce qu'il mesure.&lt;/p&gt;

</description>
      <category>ia</category>
      <category>seo</category>
      <category>node</category>
      <category>francais</category>
    </item>
    <item>
      <title>Agent vocal IA sous 500 ms : l'architecture réelle STT LLM TTS</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:02:24 +0000</pubDate>
      <link>https://dev.to/hernanz/agent-vocal-ia-sous-500-ms-larchitecture-reelle-stt-llm-tts-cnc</link>
      <guid>https://dev.to/hernanz/agent-vocal-ia-sous-500-ms-larchitecture-reelle-stt-llm-tts-cnc</guid>
      <description>&lt;p&gt;&lt;strong&gt;500 ms.&lt;/strong&gt; C'est le budget total entre l'instant où l'appelant se tait et le premier son de la réponse. Au-delà de 800 ms, l'oreille décroche : la conversation sonne robotique, l'appelant coupe la parole ou raccroche.&lt;/p&gt;

&lt;p&gt;Un agent vocal pour le service client, c'est trois briques en série — reconnaissance vocale (STT), modèle de langage (LLM), synthèse vocale (TTS) — plus une boucle d'outils pour interroger ton CRM. Tenir le budget dépend de deux choses, et presque jamais celles qu'on croit. Le LLM n'est pas le goulet d'étranglement. Ce qui casse tout, c'est la détection de fin de parole et l'appel au CRM dans la boucle. Voici l'architecture qu'on déploie en production, le code qui compte, et les pièges qu'on ne paie qu'une fois.&lt;/p&gt;

&lt;h2&gt;
  
  
  Où part vraiment la latence ?
&lt;/h2&gt;

&lt;p&gt;Décompose une réponse « instantanée » et tu vois où le temps se perd :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Détection de fin de parole (endpointing)&lt;/strong&gt; : 200–800 ms — le vrai tueur&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STT final&lt;/strong&gt; : 50–150 ms (l'essentiel se fait en streaming pendant que l'appelant parle)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM, premier token (TTFT)&lt;/strong&gt; : 200–600 ms selon le modèle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS, premier octet audio (TTFB)&lt;/strong&gt; : 100–300 ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aller-retour réseau + téléphonie&lt;/strong&gt; : 2 × 50–150 ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionne en série, sans rien optimiser : tu es entre 1,5 et 2 secondes. Injouable. La règle qui sauve le budget tient en un mot : &lt;strong&gt;tu n'attends jamais qu'une brique finisse avant de lancer la suivante.&lt;/strong&gt; Tout se streame, et l'endpointing se règle au millième.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le vrai tueur : l'endpointing
&lt;/h2&gt;

&lt;p&gt;Le VAD (Voice Activity Detection) te dit si quelqu'un parle. L'endpointing décide &lt;em&gt;quand il a fini&lt;/em&gt;. Naïvement, on attend un silence fixe — disons 700 ms — avant de considérer le tour terminé. Résultat : 700 ms ajoutés à chaque réponse, avant même que le STT ne rende son verdict.&lt;/p&gt;

&lt;p&gt;Le réglage a un compromis brutal. Un seuil trop court coupe l'appelant en plein mot, surtout quand il hésite ou épelle un numéro. Un seuil trop long ajoute du délai à chaque tour. On descend le silence fixe à 300–500 ms et on ajoute un &lt;strong&gt;endpointing sémantique&lt;/strong&gt; : si la transcription partielle forme une phrase complète (« je veux suivre ma commande »), on coupe tôt ; si elle finit sur « euh… mon numéro c'est », on attend. Deepgram et LiveKit exposent ce réglage ; sinon, une heuristique sur la ponctuation du STT fait déjà 80 % du travail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streamer, sinon tout s'effondre
&lt;/h2&gt;

&lt;p&gt;Le second levier, c'est de ne jamais attendre la fin d'une réponse LLM pour parler. Dès qu'une phrase est complète, on l'envoie au TTS pendant que le modèle génère la suite :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;llm_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# tokens du LLM en flux
&lt;/span&gt;        &lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.!?…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="c1"&gt;# phrase terminée
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# on parle déjà
&lt;/span&gt;            &lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'appelant entend la première phrase pendant que le LLM écrit la troisième. Sur une réponse de trois phrases, ce seul changement fait gagner 600 à 900 ms perçus. Des frameworks comme &lt;strong&gt;Pipecat&lt;/strong&gt; ou &lt;strong&gt;LiveKit Agents&lt;/strong&gt; câblent ce pipeline pour toi — utile, mais comprends ce qu'ils font, sinon le premier bug de latence en prod te bloque une journée.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'appel CRM : le piège du silence
&lt;/h2&gt;

&lt;p&gt;C'est là que la moitié des démos s'effondrent au contact du réel. Répondre « votre commande arrive jeudi » suppose d'interroger ton CRM en plein appel. Le LLM décide d'appeler une fonction, tu interroges l'API, tu renvoies le résultat au LLM, il reprend : deux allers-retours modèle plus la latence du CRM. Facile 1 à 2 secondes de silence complet — que l'appelant lit comme une ligne coupée.&lt;/p&gt;

&lt;p&gt;La parade tient en une ligne : &lt;strong&gt;combler le vide avant d'appeler le CRM.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# on parle AVANT l'appel réseau, pas après
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Je vérifie ça, un instant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# 200–500 ms
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tool_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# on relance le LLM avec la donnée, et on re-streame
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stream_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La phrase de transition n'est pas cosmétique : elle masque toute la latence du tour d'outil. Et garde tes schémas de &lt;code&gt;tool&lt;/code&gt; minces — un CRM qui répond en 200 ms plutôt que 500 change plus la sensation que n'importe quel changement de modèle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transférer à un humain sans le faire répéter
&lt;/h2&gt;

&lt;p&gt;Un agent honnête connaît ses limites. On déclenche l'escalade sur trois signaux : demande explicite, deux tours de suite sous un seuil de confiance, ou détection de frustration. Le code est trivial ; le piège est ailleurs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turns_low_conf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parler_a_un_humain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;turns_low_conf&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le vrai piège, c'est le transfert « froid » : l'agent passe l'appel, et l'humain décroche sans contexte. L'appelant répète tout depuis le début. C'est la raison n°1 pour laquelle les gens détestent les voicebots. Fais un transfert « chaud » : pousse un résumé de la conversation au conseiller (pop CRM, ticket pré-rempli, ou en-têtes SIP REFER). Il reprend là où la machine s'est arrêtée, et l'appelant ne le vit pas comme un échec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le français, les numéros de commande et le DTMF
&lt;/h2&gt;

&lt;p&gt;Le STT français atteint 93–95 % en conditions réelles sur du langage courant. Il s'effondre sur l'alphanumérique. Un numéro de commande épelé (« A comme Anatole, B, 1, 2, 3 ») ou un e-mail dicté partent en bouillie, et aucun re-prompt ne rattrape ça de façon fiable.&lt;/p&gt;

&lt;p&gt;Ne t'obstine pas sur la reconnaissance vocale pour ces cas. &lt;strong&gt;Bascule sur le clavier (DTMF) :&lt;/strong&gt; « Tapez votre numéro de commande sur votre téléphone. » Les touches sont déterministes, la reconnaissance est parfaite, et l'appelant préfère taper huit chiffres plutôt que les répéter trois fois. C'est moins élégant qu'une conversation pure — c'est surtout ce qui marche.&lt;/p&gt;

&lt;h2&gt;
  
  
  La ligne que l'AI Act impose en dur
&lt;/h2&gt;

&lt;p&gt;Depuis le 2 août 2026, l'article 50 du règlement européen sur l'IA oblige à informer clairement l'interlocuteur qu'il parle à une machine. L'interdit est absolu. Si l'appelant demande « vous êtes un humain ? », mentir est une infraction. Les sanctions montent jusqu'à 35 M€ ou 7 % du CA mondial, et le RGPD s'ajoute par-dessus puisque tu traites de la voix.&lt;/p&gt;

&lt;p&gt;Conséquence technique directe : &lt;strong&gt;ne délègue pas cette obligation au LLM.&lt;/strong&gt; Un modèle probabiliste finira par oublier la phrase un appel sur mille. La divulgation se code en dur, jouée avant le premier token généré :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GREETING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bonjour, vous êtes en relation avec l&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;assistant vocal &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;de [Entreprise]. Je peux répondre ou vous passer un conseiller.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GREETING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# en dur, jamais généré par le modèle
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La conformité ne se joue pas aux probabilités.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions fréquentes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quel STT/TTS pour le français ?&lt;/strong&gt; Côté STT, Deepgram et Whisper large tiennent 93–95 % sur du courant ; côté TTS, Cartesia ou ElevenLabs descendent sous 300 ms de TTFB en streaming. Teste sur &lt;em&gt;tes&lt;/em&gt; appels réels, pas sur des démos studio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faut-il fine-tuner le LLM ?&lt;/strong&gt; Rarement. Un bon prompt système avec tes procédures, plus le tool-calling vers ton CRM, couvre la majorité des cas de niveau 1. Le fine-tuning se justifie quand le vocabulaire métier est très spécifique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS ou sur-mesure ?&lt;/strong&gt; Volume faible et cas standard : un SaaS no-code sous 200 €/mois suffit, ne code pas ça toi-même. Dès qu'il faut brancher un SI propriétaire, contrôler la donnée ou éviter le lock-in, le sur-mesure reprend l'avantage. On détaille l'arbitrage dans le &lt;a href="https://blog.neo-va.com/agent-vocal-ia-service-client/" rel="noopener noreferrer"&gt;guide complet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment gérer les interruptions (barge-in) ?&lt;/strong&gt; Détecte la voix pendant que le TTS joue, coupe la synthèse immédiatement et vide le buffer audio. Sans ça, l'agent continue de parler par-dessus l'appelant — le défaut qui trahit un robot en une seconde.&lt;/p&gt;

&lt;p&gt;On a câblé cette architecture plusieurs fois, et la difficulté n'est jamais le « hello world » : c'est l'endpointing, la latence du CRM et le transfert propre. Si tu veux apprendre à construire ce genre d'agent avec Claude Code — du pipeline au tool-calling —, notre &lt;a href="https://blog.neo-va.com/formation-claude-code/" rel="noopener noreferrer"&gt;formation Claude Code&lt;/a&gt; prend le sujet du côté du code. Et si tu préfères qu'on le branche directement sur ton SI, c'est notre métier de studio.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Brancher Kimi K3 dans une boucle agentique : les 4 trucs qui ont cassé</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:33:19 +0000</pubDate>
      <link>https://dev.to/hernanz/brancher-kimi-k3-dans-une-boucle-agentique-les-4-trucs-qui-ont-casse-3k81</link>
      <guid>https://dev.to/hernanz/brancher-kimi-k3-dans-une-boucle-agentique-les-4-trucs-qui-ont-casse-3k81</guid>
      <description>&lt;p&gt;L'API Kimi K3 parle le dialecte Anthropic. Brancher notre boucle agentique dessus nous a pris vingt minutes ; la faire tourner proprement nous a pris deux jours.&lt;/p&gt;

&lt;p&gt;On a passé une feature réelle dans les deux : ajout d'un export CSV filtré dans un dashboard Next.js + Supabase, six fichiers touchés, tests inclus. Même prompt de départ, même arborescence, même définition de « terminé » (tests au vert, lint propre). Kimi K3 a fini, mais il a mis 1,5× plus de temps et coûté 2,84 $ là où Claude était déjà payé par le forfait. Voilà le détail, les mesures, et les quatre endroits où le montage casse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le montage : vingt minutes
&lt;/h2&gt;

&lt;p&gt;Moonshot expose un endpoint compatible avec le format &lt;code&gt;messages&lt;/code&gt; d'Anthropic, donc si ta boucle est déjà écrite contre le SDK Anthropic, tu ne changes que deux lignes :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KIMI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KIMI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.moonshot.ai/anthropic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KIMI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KIMI_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kimi-k3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# mêmes schémas que chez Anthropic
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notre banc, pour que ce soit reproductible :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;boucle maison, 12 outils (&lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;…), pas de framework&lt;/li&gt;
&lt;li&gt;streaming activé, un seul thread, aucune parallélisation d'outils&lt;/li&gt;
&lt;li&gt;même machine, même connexion, mesures prises entre 14 h et 17 h le 21/07&lt;/li&gt;
&lt;li&gt;contexte de départ : 34 fichiers épinglés, ~28 000 tokens&lt;/li&gt;
&lt;li&gt;au 22/07, les poids ne sont pas publics : tout passe par l'API hébergée&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le premier run est parti du premier coup, et c'est précisément ce qui rend la suite piégeuse : ça &lt;em&gt;marche&lt;/em&gt;, donc on ne regarde pas la facture tout de suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 1 — le cache de prompt ne se déclenche pas comme chez Anthropic
&lt;/h2&gt;

&lt;p&gt;Notre premier run a coûté &lt;strong&gt;7,10 $&lt;/strong&gt;. Le second, à travail identique, &lt;strong&gt;2,84 $&lt;/strong&gt;. Entre les deux, on n'a touché à aucun outil ni à aucun prompt : on a figé l'ordre du préfixe.&lt;/p&gt;

&lt;p&gt;Chez Anthropic, tu marques explicitement les blocs à mettre en cache avec &lt;code&gt;cache_control&lt;/code&gt;. Kimi ne fonctionne pas comme ça : le cache est un &lt;strong&gt;cache de préfixe automatique&lt;/strong&gt;, facturé 0,30 $/M au lieu de 3 $/M en entrée, et il ne se déclenche que si les premiers octets de la requête sont &lt;em&gt;strictement&lt;/em&gt; identiques d'un tour à l'autre. Notre boucle sérialisait les outils depuis un dict, donc l'ordre bougeait ; et on injectait un horodatage dans le system prompt. Deux détails, une facture multipliée par dix sur l'entrée.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# le préfixe doit être identique octet pour octet, à chaque tour
&lt;/span&gt;&lt;span class="n"&gt;PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_BLOCK_STATIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;span class="c1"&gt;# l'horodatage, les compteurs, l'état : à la FIN, jamais au début
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le champ &lt;code&gt;cache_control&lt;/code&gt; est accepté sans erreur, mais il ne fait rien. C'est le pire cas de figure : pas d'exception, juste une facture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 2 — les arguments d'outils ne reviennent pas toujours propres
&lt;/h2&gt;

&lt;p&gt;Sur 46 tours, on a eu &lt;strong&gt;3 blocs &lt;code&gt;tool_use&lt;/code&gt; dont les arguments arrivaient en chaîne de caractères&lt;/strong&gt; au lieu d'objet, dont un enveloppé dans des barrières de code markdown. Rien de dramatique quand tu le sais, une exception non rattrapée quand tu ne le sais pas. Le schéma JSON est suivi dans le fond — les clés étaient bonnes — mais l'enveloppe ne l'est pas toujours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_tool_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^```

(?:json)?|

```$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux autres différences à traiter avant de lancer quoi que ce soit sur un dépôt réel :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;appels parallèles&lt;/strong&gt; : K3 renvoie volontiers plusieurs blocs &lt;code&gt;tool_use&lt;/code&gt; dans une même réponse. Si ta boucle n'en exécute qu'un et jette le reste, tu perds des tours sans t'en apercevoir.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stop_reason&lt;/code&gt;&lt;/strong&gt; : la valeur &lt;code&gt;max_tokens&lt;/code&gt; tombe plus souvent qu'avec Claude sur les gros patchs. Prévois la reprise, sinon tu écris des fichiers tronqués.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Piège 3 — la lenteur compose
&lt;/h2&gt;

&lt;p&gt;Le débit annoncé, 62 tokens/s, est en dessous de la médiane du marché (71 selon Artificial Analysis). Sur une réponse isolée, ça ne se sent pas. Dans une boucle qui enchaîne quarante tours, en revanche, ça se paie deux fois : le modèle est plus lent &lt;strong&gt;et&lt;/strong&gt; il a eu besoin de plus de tours pour converger.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mesure (même feature)&lt;/th&gt;
&lt;th&gt;Kimi K3 (API)&lt;/th&gt;
&lt;th&gt;Claude (Claude Code)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tours d'agent&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens de sortie cumulés&lt;/td&gt;
&lt;td&gt;31 200&lt;/td&gt;
&lt;td&gt;24 900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Débit médian&lt;/td&gt;
&lt;td&gt;62 t/s&lt;/td&gt;
&lt;td&gt;78 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-to-first-token médian&lt;/td&gt;
&lt;td&gt;1,9 s&lt;/td&gt;
&lt;td&gt;1,1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temps au mur&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11 min 50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 min 50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests au vert au premier passage&lt;/td&gt;
&lt;td&gt;non (2 reprises)&lt;/td&gt;
&lt;td&gt;oui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coût direct de la tâche&lt;/td&gt;
&lt;td&gt;2,84 $&lt;/td&gt;
&lt;td&gt;inclus au forfait&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;-20 % de débit et +21 % de tours, ça ne fait pas -41 % : ça fait un facteur 1,5 sur le temps réel. C'est la seule métrique que je regarde encore, parce que c'est celle que tu subis quand tu attends devant ton terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piège 4 — la fenêtre d'un million est un piège de facturation
&lt;/h2&gt;

&lt;p&gt;Un contexte de 1 M tokens, dans une boucle agentique, c'est une invitation à ne jamais compacter. Sauf que ta boucle renvoie tout l'historique à chaque tour : le coût d'entrée croît de façon quadratique avec le nombre de tours, et c'est l'entrée qui domine la facture, pas la sortie. Sur notre run, les 31 200 tokens générés ont coûté 0,47 $ — le reste, 2,37 $, c'est du contexte renvoyé en boucle.&lt;/p&gt;

&lt;p&gt;Avec un forfait mensuel, cette dérive est invisible et tu peux te permettre d'être négligent ; au token, elle est linéaire sur ta carte bleue. On a fini par imposer une compaction dure à 120 000 tokens, ce qui a coupé la facture d'un tiers sans dégrader le résultat. Le raisonnement économique complet — forfait contre compteur, et pourquoi tous les comparatifs se trompent de grille — est &lt;a href="https://blog.neo-va.com/kimi-k3-vs-claude-pour-coder/" rel="noopener noreferrer"&gt;dans notre article de fond&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'on garde
&lt;/h2&gt;

&lt;p&gt;K3 est premier au Frontend Code Arena, et sur du composant one-shot bien cadré, ça se voit : le HTML/CSS sort propre, souvent mieux structuré. On l'a gardé pour ça, appelé ponctuellement depuis nos scripts, jamais comme moteur de la boucle.&lt;/p&gt;

&lt;p&gt;Pour un chantier multi-fichiers avec tests, on reste sur Claude Code — pas par attachement, mais parce que le harness fait le gros du travail et qu'aucune économie au token ne rattrape 4 minutes de plus par feature, plusieurs fois par jour. Si tu veux le détail de ce qu'un harness ajoute réellement à un modèle, on l'a documenté dans notre &lt;a href="https://blog.neo-va.com/creer-application-avec-claude-code/" rel="noopener noreferrer"&gt;guide Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Réserve honnête : les poids sont annoncés pour le 27/07, et tant qu'ils ne sont pas tombés, tout ce qui précède mesure une API hébergée, pas un modèle. On refera le banc après la publication, et on postera les chiffres bruts — y compris s'ils nous contredisent.&lt;/p&gt;

&lt;p&gt;Tu veux monter ce genre de boucle proprement plutôt que de la déboguer deux jours ? &lt;a href="https://blog.neo-va.com/formation-claude-code/" rel="noopener noreferrer"&gt;Notre formation Claude Code&lt;/a&gt; part exactement de là.&lt;/p&gt;

</description>
      <category>kimi</category>
      <category>claude</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Ce qui coûte vraiment cher dans une app mobile — mesuré en heures, pas en euros</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Mon, 20 Jul 2026 23:55:54 +0000</pubDate>
      <link>https://dev.to/hernanz/ce-qui-coute-vraiment-cher-dans-une-app-mobile-mesure-en-heures-pas-en-euros-403h</link>
      <guid>https://dev.to/hernanz/ce-qui-coute-vraiment-cher-dans-une-app-mobile-mesure-en-heures-pas-en-euros-403h</guid>
      <description>&lt;p&gt;Les grilles tarifaires françaises te donnent des euros. Inutilisable quand tu veux comprendre pourquoi un projet dérape.&lt;/p&gt;

&lt;p&gt;On a repris le suivi de temps de nos derniers projets mobiles et on a tout reventilé &lt;strong&gt;en heures par brique fonctionnelle&lt;/strong&gt;. Pas en euros : les heures, elles, ne dépendent pas du TJM de celui qui te fait le devis.&lt;/p&gt;

&lt;p&gt;Verdict court : ce qui coûte cher n'est presque jamais ce que le client croit.&lt;/p&gt;

&lt;h2&gt;
  
  
  La ventilation réelle, en heures
&lt;/h2&gt;

&lt;p&gt;Base : une app React Native + Expo, back-end Supabase, iOS + Android, un projet type de ~700 h.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brique&lt;/th&gt;
&lt;th&gt;Heures&lt;/th&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email + OAuth + reset + refresh token)&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;6 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Écrans de liste / détail / formulaires (×18 écrans)&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;23 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifications push (serveur + client + deep links)&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;8 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paiement in-app (StoreKit + Play Billing + réconciliation serveur)&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;13 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline-first + synchronisation&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;19 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back-office admin&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;10 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests, CI, publication stores, allers-retours review&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;14 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cadrage, suivi, corrections post-recette&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;8 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deux lignes pèsent &lt;strong&gt;32 %&lt;/strong&gt; du projet à elles seules : offline-first et paiement. Ce sont aussi les deux que les clients citent en dernier, souvent en fin de réunion, avec un « ah oui, et il faudra que ça marche dans le métro ».&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi l'offline coûte 130 h
&lt;/h2&gt;

&lt;p&gt;Parce que ce n'est pas une fonctionnalité, c'est une décision d'architecture qui contamine chaque écran.&lt;/p&gt;

&lt;p&gt;Un &lt;code&gt;fetch&lt;/code&gt; classique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;setTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La même chose en offline-first, version honnête :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. lecture locale immédiate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM tasks WHERE deleted_at IS NULL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;setTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 2. push des mutations en attente&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM outbox ORDER BY created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apply_op&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CONFLICT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveConflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← le vrai chantier&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE FROM outbox WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. pull incrémental&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLastSyncedAt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remote&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mergeIntoLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resolveConflict&lt;/code&gt; est la ligne qui coûte. Deux appareils modifient la même entité hors ligne : qui gagne ? Last-write-wins est simple et faux dès qu'il y a de l'argent ou du stock. Un merge par champ demande un modèle de données pensé pour, donc une reprise de toutes les tables.&lt;/p&gt;

&lt;p&gt;Ajoute la suppression logique (&lt;code&gt;deleted_at&lt;/code&gt; partout, sinon un enregistrement supprimé ressuscite à la synchro suivante), les migrations du schéma &lt;strong&gt;local&lt;/strong&gt; sur des appareils qui ont sauté trois versions, et une file d'attente qui survit à un kill de l'app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le piège qu'on a mangé :&lt;/strong&gt; on a livré une première version avec un &lt;code&gt;outbox&lt;/code&gt; en mémoire. Un utilisateur ferme l'app dans le tunnel, ses 6 dernières actions disparaissent. Silencieusement. Détecté trois semaines après la mise en production, par recoupement de logs. Rattrapage : 22 h.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi le paiement coûte 90 h
&lt;/h2&gt;

&lt;p&gt;Le SDK, c'est une demi-journée. Le reste, c'est la réconciliation.&lt;/p&gt;

&lt;p&gt;Un achat validé côté client ne prouve rien. Il faut vérifier le reçu côté serveur, gérer les webhooks de renouvellement, les remboursements, les périodes de grâce, les essais gratuits, les changements de plan, et le cas où l'utilisateur a acheté sur iOS puis se connecte sur Android.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webhook Apple — l'état d'abonnement ne vient JAMAIS du client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notif&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyAppleSignedPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signedPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DID_RENEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBSCRIBED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setEntitlement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalTransactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DID_FAIL_TO_RENEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;// grâce, pas révocation immédiate&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setEntitlement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalTransactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gracePeriodExpiresDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REFUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revokeAndAudit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalTransactionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiplie par deux : Google Play a sa propre sémantique, ses propres états, son propre vocabulaire. Deux implémentations, deux jeux de tests, deux environnements de sandbox capricieux.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Piège rencontré :&lt;/strong&gt; les sandbox Apple compressent les durées d'abonnement (un mois = quelques minutes). Un test de renouvellement écrit sans en tenir compte passe au vert en sandbox et échoue en production. On l'a découvert sur un utilisateur réel qui a perdu son accès premium après 30 jours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Où l'assistance IA fait gagner du temps — et où elle n'en fait pas gagner
&lt;/h2&gt;

&lt;p&gt;On travaille quotidiennement avec Claude Code. Les gains sont réels mais &lt;strong&gt;très inégalement répartis&lt;/strong&gt;, et ça recoupe précisément les données publiées.&lt;/p&gt;

&lt;p&gt;D'après le rapport DORA (Google Cloud), qui cite les travaux de Stanford : &lt;strong&gt;35-40 % de gain sur les tâches simples et sur du code neuf, moins de 10 % sur du legacy complexe&lt;/strong&gt;. Le même rapport mesure &lt;strong&gt;+20 % de pull requests par auteur&lt;/strong&gt; en un an, et une adoption autour de 90 %. Stack Overflow 2025 donne 84 % d'utilisateurs, 51 % au quotidien.&lt;/p&gt;

&lt;p&gt;Notre propre répartition, sur les briques du tableau plus haut :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brique&lt;/th&gt;
&lt;th&gt;Gain observé&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Écrans liste/détail/formulaires&lt;/td&gt;
&lt;td&gt;~40 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back-office admin&lt;/td&gt;
&lt;td&gt;~40 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;~30 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests et CI&lt;/td&gt;
&lt;td&gt;~25 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifications push&lt;/td&gt;
&lt;td&gt;~15 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paiement&lt;/td&gt;
&lt;td&gt;~10 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline-first / conflits&lt;/td&gt;
&lt;td&gt;quasi nul&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La logique est nette : &lt;strong&gt;plus la brique est standard, plus le gain est fort&lt;/strong&gt;. Générer 18 écrans CRUD cohérents avec le design system, c'est du temps massivement récupéré. Décider de la stratégie de résolution de conflits sur ton modèle métier, c'est un travail de conception que personne ne fait à ta place.&lt;/p&gt;

&lt;p&gt;Le corollaire qui intéresse le client : le gain porte sur les postes qui ne pesaient déjà pas très lourd. Il déplace le prix, il ne l'effondre pas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le contre-point, à citer honnêtement.&lt;/strong&gt; L'essai randomisé METR de juillet 2025 (arXiv 2507.09089) avait mesuré des développeurs expérimentés &lt;strong&gt;19 % plus lents&lt;/strong&gt; avec l'IA, alors qu'ils s'estimaient 20 % plus rapides. METR a reconnu en février 2026 une faille méthodologique (biais de sélection). Le chiffre ne tient donc plus tel quel — mais l'écart perception/réalité, lui, reste un bon rappel : mesure ton temps, ne l'estime pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'on en fait concrètement
&lt;/h2&gt;

&lt;p&gt;Trois réflexes qu'on applique en cadrage :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Demander « offline ? » et « paiement ? » à la minute 5&lt;/strong&gt;, pas à la fin. Ces deux réponses déterminent un tiers du budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chiffrer en heures par brique&lt;/strong&gt;, jamais en forfait global. Le client voit ce qu'il achète et peut arbitrer ligne par ligne.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sortir l'offline-first de la v1&lt;/strong&gt; quand c'est possible. Un cache lecture seule + un message clair couvre 80 % des usages pour 15 h au lieu de 130.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si tu veux la traduction en euros, la ventilation complète et le coût sur 3 ans : &lt;a href="https://blog.neo-va.com/prix-developpement-application-mobile/" rel="noopener noreferrer"&gt;Prix développement application mobile 2026 : les vrais chiffres&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Et sur notre outillage : &lt;a href="https://blog.neo-va.com/creer-application-avec-claude-code/" rel="noopener noreferrer"&gt;créer une application avec Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vous avez mesuré vos briques ? Vos ratios diffèrent des nôtres ? Les commentaires sont faits pour ça.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>reactnative</category>
      <category>ai</category>
      <category>pricing</category>
    </item>
    <item>
      <title>Découper une app métier en tranches livrables (et ne plus jamais faire de tunnel)</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:47:59 +0000</pubDate>
      <link>https://dev.to/hernanz/decouper-une-app-metier-en-tranches-livrables-et-ne-plus-jamais-faire-de-tunnel-1gp6</link>
      <guid>https://dev.to/hernanz/decouper-une-app-metier-en-tranches-livrables-et-ne-plus-jamais-faire-de-tunnel-1gp6</guid>
      <description>&lt;p&gt;Le tunnel de trois mois est le meilleur moyen de rater une application métier.&lt;/p&gt;

&lt;p&gt;On cadre, on développe, on montre à la fin. Et à la fin, l'utilisateur dit la phrase qui coûte cher : « ah, mais nous on ne fait pas comme ça. »&lt;/p&gt;

&lt;p&gt;Voici le découpage qu'on utilise pour éviter ça. Rien de révolutionnaire, mais quelques règles précises qui changent le résultat.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle de la tranche verticale
&lt;/h2&gt;

&lt;p&gt;Une tranche livrable traverse &lt;strong&gt;toute&lt;/strong&gt; la pile : base de données, logique, interface. Elle est utilisable de bout en bout par un vrai utilisateur.&lt;/p&gt;

&lt;p&gt;C'est l'inverse du découpage par couche, où l'on fait « toute la base » puis « toute l'API » puis « tout le front ». Ce découpage-là ne produit rien de testable avant la fin.&lt;/p&gt;

&lt;p&gt;❌ Semaine 1 : le schéma complet de la base&lt;br&gt;
❌ Semaine 2 : toutes les routes API&lt;br&gt;
❌ Semaine 3 : tous les écrans&lt;/p&gt;

&lt;p&gt;✅ Semaine 1 : créer une commande et la voir dans une liste — écran compris&lt;br&gt;
✅ Semaine 2 : la modifier et changer son statut&lt;br&gt;
✅ Semaine 3 : la recherche et les filtres&lt;/p&gt;

&lt;p&gt;Après la première semaine, quelqu'un peut ouvrir l'outil et faire quelque chose de réel. C'est là que remontent les vrais retours.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le format de spec qui tient en un fichier
&lt;/h2&gt;

&lt;p&gt;Chaque tranche a un fichier. Toujours la même structure, et elle sert autant à l'équipe qu'à l'assistant de code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Tranche 03 — Changement de statut d'une commande&lt;/span&gt;

&lt;span class="gu"&gt;## Ce que l'utilisateur peut faire&lt;/span&gt;
Ouvrir une commande, passer son statut de "en préparation" à "expédiée",
et voir qui l'a changé et quand.

&lt;span class="gu"&gt;## Règles métier&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Seuls les rôles &lt;span class="sb"&gt;`préparateur`&lt;/span&gt; et &lt;span class="sb"&gt;`admin`&lt;/span&gt; peuvent changer un statut.
&lt;span class="p"&gt;-&lt;/span&gt; Un retour en arrière est interdit sauf pour &lt;span class="sb"&gt;`admin`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Chaque changement écrit une ligne dans &lt;span class="sb"&gt;`commande_historique`&lt;/span&gt;.

&lt;span class="gu"&gt;## Hors périmètre (important)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Pas de notification e-mail (tranche 07).
&lt;span class="p"&gt;-&lt;/span&gt; Pas d'export (tranche 09).

&lt;span class="gu"&gt;## Fini quand&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Un préparateur change un statut et voit l'historique se remplir.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Un commercial n'a pas le bouton.
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Un retour en arrière est refusé avec un message clair.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La section &lt;strong&gt;Hors périmètre&lt;/strong&gt; est celle qui fait le plus de travail. Elle empêche le glissement de périmètre, qui représente couramment 30 à 50 % de charge supplémentaire sur un projet mal tenu.&lt;/p&gt;

&lt;p&gt;La section &lt;strong&gt;Fini quand&lt;/strong&gt; évite l'autre classique : le désaccord sur ce que « terminé » veut dire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi ce format marche bien avec un assistant de code
&lt;/h2&gt;

&lt;p&gt;On travaille avec Claude Code au quotidien. Ce format de fichier est celui qui donne les meilleurs résultats, pour une raison simple : un assistant échoue surtout quand le contexte est implicite.&lt;/p&gt;

&lt;p&gt;Les règles métier écrites noir sur blanc (« un retour en arrière est interdit sauf admin ») produisent du code correct du premier coup. Les mêmes règles laissées dans la tête du chef de projet produisent du code plausible et faux.&lt;/p&gt;

&lt;p&gt;Le « Hors périmètre » a un effet secondaire utile : il empêche l'assistant d'anticiper. Sans cette section, il ajoute spontanément une notification e-mail parce que ça semble logique — et vous héritez d'un code que personne n'a demandé, ni relu.&lt;/p&gt;

&lt;p&gt;Un point sur lequel on ne transige pas : les critères de « Fini quand » deviennent des tests avant d'être considérés comme validés. Environ 45 % du code généré par IA contient au moins une vulnérabilité selon les analyses publiées en 2026. Ce chiffre ne condamne pas l'outil, il rappelle juste où mettre la relecture humaine.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'ordre des tranches n'est pas neutre
&lt;/h2&gt;

&lt;p&gt;On classe toujours dans cet ordre :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;La tranche qui prouve l'usage.&lt;/strong&gt; Celle qui, si elle ne convainc pas, invalide le projet entier. Elle passe en premier, quand le budget est encore intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les tranches à risque technique.&lt;/strong&gt; Intégration avec un système existant, import de données sales, API tierce mal documentée. Jamais à la fin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le confort.&lt;/strong&gt; Filtres, exports, tableaux de bord. Utiles, mais aucun projet ne meurt de leur absence en semaine 4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;L'erreur fréquente est de commencer par l'authentification et l'administration des comptes, parce que c'est « la base ». C'est trois semaines avant de montrer quoi que ce soit d'intéressant. Une authentification minimale suffit largement au départ.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ça change concrètement
&lt;/h2&gt;

&lt;p&gt;Le bénéfice n'est pas la vitesse brute. C'est que l'erreur de cadrage se paie en une semaine au lieu de trois mois.&lt;/p&gt;

&lt;p&gt;Sur une app métier standard, ça donne une première version utilisable en 6 à 10 semaines, avec des retours utilisateurs dès la deuxième.&lt;/p&gt;

&lt;p&gt;Et surtout : le jour où le client dit « nous on ne fait pas comme ça », il le dit assez tôt pour que ça ne coûte presque rien.&lt;/p&gt;




&lt;p&gt;On détaille les étapes complètes d'un projet, les délais et les budgets réels ici : &lt;a href="https://blog.neo-va.com/creer-une-application-sur-mesure/" rel="noopener noreferrer"&gt;créer une application sur mesure&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>productivity</category>
      <category>ai</category>
      <category>french</category>
    </item>
    <item>
      <title>J'ai automatisé la production d'articles SEO avec Claude Code — et le plus important, c'est le garde-fou</title>
      <dc:creator>Olivier</dc:creator>
      <pubDate>Sun, 19 Jul 2026 05:42:07 +0000</pubDate>
      <link>https://dev.to/hernanz/jai-automatise-la-production-darticles-seo-avec-claude-code-et-le-plus-important-cest-le-421f</link>
      <guid>https://dev.to/hernanz/jai-automatise-la-production-darticles-seo-avec-claude-code-et-le-plus-important-cest-le-421f</guid>
      <description>&lt;p&gt;Google a effacé jusqu'à 80 % du trafic de sites qui publiaient du contenu IA en masse. C'est le motif &lt;em&gt;scaled content abuse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On voulait quand même automatiser la production d'articles. La question n'était donc pas « comment produire vite », mais &lt;strong&gt;« comment empêcher la machine de publier de la bouillie »&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Voilà l'architecture qu'on a retenue, et le morceau de code qui compte vraiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le pipeline
&lt;/h2&gt;

&lt;p&gt;Six étapes, chacune tenue par un agent Claude Code spécialisé avec son propre contexte :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Chasseur      → mine les mots-clés (questions réelles, faible concurrence)
2. Cartographe   → lit la page 1 Google, relève les questions traitées
3. Rédacteur     → écrit selon un guide de voix versionné
4. Éditeur ⛔    → contrôle qualité, BLOQUE si en dessous du standard
5. Générateur    → markdown → HTML + JSON-LD + sitemap + RSS
6. Publication   → déploiement + ping IndexNow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'étape 4 est la seule qui compte vraiment. Sans elle, on a juste une ferme de contenu qui se fait désindexer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le garde-fou, en code
&lt;/h2&gt;

&lt;p&gt;L'erreur classique, c'est de demander à un LLM « est-ce que cet article est bon ? ». Un modèle qui juge sa propre production est complaisant.&lt;/p&gt;

&lt;p&gt;On a donc mis un script déterministe. Pas d'IA, pas d'opinion, des mesures :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;minWords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// en dessous = contenu mince&lt;/span&gt;
  &lt;span class="na"&gt;introMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;introMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// zone "answer-first"&lt;/span&gt;
  &lt;span class="na"&gt;minH2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// structure&lt;/span&gt;
  &lt;span class="na"&gt;minFaq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// schema FAQPage&lt;/span&gt;
  &lt;span class="na"&gt;titleMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;// au-delà, Google tronque&lt;/span&gt;
  &lt;span class="na"&gt;descMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;descMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                         &lt;span class="c1"&gt;// formules bannies par la charte&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dans le monde d'aujourd'hui&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;il est important de noter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en conclusion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et les vérifications qui font sortir le script en code 1 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontmatter complet&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`frontmatter: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" manquant`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// substance réelle&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;#*&amp;gt;|_`&lt;/span&gt;&lt;span class="se"&gt;\-]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minWords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; mots (min &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minWords&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// liens internes valides — un lien mort et ça ne passe pas&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\]\((\/[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\/\)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`lien interne cassé: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// formules interdites&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`formule interdite : « &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; »`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le script de publication l'appelle en premier :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
node check.mjs      &lt;span class="c"&gt;# ⛔ sort en 1 → tout s'arrête ici&lt;/span&gt;
node build.mjs
wrangler pages deploy dist &lt;span class="nt"&gt;--project-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;…
node indexnow.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; + code de sortie 1 = la publication est &lt;strong&gt;structurellement impossible&lt;/strong&gt; si un article ne passe pas. Pas de « on verra », pas d'exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le garde-fou a attrapé dès le premier passage
&lt;/h2&gt;

&lt;p&gt;Sur nos trois premiers articles, écrits pourtant avec soin :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 meta descriptions trop longues&lt;/strong&gt; (180 à 214 caractères). Google les aurait tronquées. Invisible à l'œil nu, évident pour le script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un lien interne cassé&lt;/strong&gt; après un changement d'architecture d'URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aucun humain relisant à 23h n'aurait vu ça.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les pièges rencontrés
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Le LLM juge mal son propre travail.&lt;/strong&gt; D'où le script déterministe. L'IA écrit, le code mesure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le fuseau horaire.&lt;/strong&gt; &lt;code&gt;new Date('2026-07-17T00:00:00Z').toLocaleDateString('fr-FR')&lt;/code&gt; affichait le 16 juillet. Il faut forcer &lt;code&gt;timeZone: 'UTC'&lt;/code&gt; — sinon toutes les dates de publication décalent d'un jour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le résidu de build.&lt;/strong&gt; Un dossier oublié dans le répertoire de déploiement a mis en ligne un site entier au mauvais endroit. Depuis, le script de build est le seul à écrire dans ce dossier, et on vérifie son contenu avant chaque envoi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le calibrage des règles.&lt;/strong&gt; La première version mesurait le « premier paragraphe » pour vérifier la réponse directe. Avec un style à paragraphes courts, elle criait au loup en permanence. On mesure maintenant le bloc entier avant le premier &lt;code&gt;##&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que je retiens
&lt;/h2&gt;

&lt;p&gt;La partie difficile n'était pas de faire écrire l'IA. C'est devenu trivial.&lt;/p&gt;

&lt;p&gt;La partie difficile, c'est de &lt;strong&gt;rendre la médiocrité impossible à publier&lt;/strong&gt;. Un seuil chiffré, un code de sortie, et la discipline de ne jamais le contourner.&lt;/p&gt;

&lt;p&gt;L'autre chose : le volume ne va pas sur le domaine principal. Deux à trois articles par semaine, pas trente. Le volume part sur des plateformes à forte autorité — comme celle-ci. On automatise ce qu'on possède, on soigne à la main ce qu'on emprunte.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;On est un studio de développement. On construit des applications sur mesure et on documente ce qu'on apprend en le faisant. Si l'architecture complète vous intéresse, elle est détaillée sur &lt;a href="https://blog.neo-va.com/" rel="noopener noreferrer"&gt;notre blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
