<?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: Harvey He</title>
    <description>The latest articles on DEV Community by Harvey He (@keheai_harvey).</description>
    <link>https://dev.to/keheai_harvey</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%2F4103047%2F11f6c976-39e8-4e9a-bb45-ded16ef22c7a.png</url>
      <title>DEV Community: Harvey He</title>
      <link>https://dev.to/keheai_harvey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keheai_harvey"/>
    <language>en</language>
    <item>
      <title>Six Languages Broke Our "Make AI Text Sound Human" Feature. Here's What Actually Went Wrong</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Fri, 09 Oct 2026 03:33:39 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/six-languages-broke-our-make-ai-text-sound-human-feature-heres-what-actually-went-wrong-2022</link>
      <guid>https://dev.to/keheai_harvey/six-languages-broke-our-make-ai-text-sound-human-feature-heres-what-actually-went-wrong-2022</guid>
      <description>&lt;p&gt;We run a small text-rewriting tool. English feedback was decent. Then we turned on the other twelve languages and support tickets changed shape completely.&lt;/p&gt;

&lt;p&gt;Nobody complained that "the text is still robotic". They complained about things like &lt;em&gt;"why is there a space before my question mark"&lt;/em&gt;, &lt;em&gt;"my boss name is spelled wrong in Hebrew"&lt;/em&gt;, and &lt;em&gt;"this is insulting in German"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here are the six failures worth writing down. All of them were invisible to our English test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Shorter is better" is an English habit
&lt;/h2&gt;

&lt;p&gt;Our English pass was tuned to cut filler and shorten sentences. Applied to German it shredded perfectly natural compound nouns. Applied to Japanese it did the opposite — the rewrite came out &lt;em&gt;longer&lt;/em&gt; than the source because our "simplify" rule kept splitting clauses that Japanese keeps joined with commas.&lt;/p&gt;

&lt;p&gt;A 100-word English paragraph is roughly 130 words in German and about 70 characters in Chinese. There is no single length target. If your rewrite rules reference word or character counts, they are English rules wearing a neutral costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Punctuation is a fingerprint, and it differs per script
&lt;/h2&gt;

&lt;p&gt;The em dash obsession that AI detectors flag is an English signal. In French, the giveaway is different — and so is the &lt;em&gt;correct&lt;/em&gt; typography:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;What our tool shipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;French&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bonjour !&lt;/code&gt; (narrow NBSP before &lt;code&gt;! ? ; :&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bonjour!&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;German&lt;/td&gt;
&lt;td&gt;&lt;code&gt;„Anführungszeichen"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"Anführungszeichen"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;¿Qué?&lt;/code&gt; / &lt;code&gt;¡Vamos!&lt;/code&gt; (opening marks)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Qué?&lt;/code&gt; / &lt;code&gt;Vamos!&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chinese&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;，。&lt;/code&gt; full-width&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;, .&lt;/code&gt; half-width&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Half-width punctuation in Chinese text is the single loudest "a foreigner or a machine wrote this" signal there is. We shipped it for a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your "AI-tell" word list does not translate
&lt;/h2&gt;

&lt;p&gt;Every language has its own overused vocabulary, and it is almost never a translation of the English list. Machine-translating "delve / tapestry / bustling" into Spanish produces words nobody overuses. Meanwhile the actual Spanish tells — &lt;code&gt;en el mundo actual&lt;/code&gt;, &lt;code&gt;es importante destacar&lt;/code&gt; — were not on our list at all.&lt;/p&gt;

&lt;p&gt;You have to build the list per language from real text, not translate the English one. There is no shortcut here.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Formality is a required field, not a style option
&lt;/h2&gt;

&lt;p&gt;English has one "you". Many languages do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;French: &lt;code&gt;tu&lt;/code&gt; vs &lt;code&gt;vous&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;German: &lt;code&gt;du&lt;/code&gt; vs &lt;code&gt;Sie&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Spanish: &lt;code&gt;tú&lt;/code&gt; vs &lt;code&gt;usted&lt;/code&gt;, and &lt;code&gt;vos&lt;/code&gt; in Argentina&lt;/li&gt;
&lt;li&gt;Chinese: &lt;code&gt;你&lt;/code&gt; vs &lt;code&gt;您&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Defaulting to the informal form is not "casual", it is rude — the German one in particular. If the input does not tell you the register, the honest move is to ask or stay formal. Guessing friendly is a bug, not a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. RTL is not "same text, right aligned"
&lt;/h2&gt;

&lt;p&gt;Arabic and Hebrew need the direction handled in the layout, not just in the CSS &lt;code&gt;direction&lt;/code&gt; property. Mixed content is where it breaks: an English product name inside an Arabic sentence, a phone number next to a parenthesis. They reorder visually, so a name that reads correctly in the database renders wrong on screen and in a quoted email.&lt;/p&gt;

&lt;p&gt;If you cannot test with a native reader, at minimum wrap Latin runs and numbers with proper isolates and check the rendered string, not the source string.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Numbers, dates, and units
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;1,000&lt;/code&gt; means one thousand in the US and one in Germany. &lt;code&gt;3/4/2026&lt;/code&gt; is two different dates depending on the country. Temperature units, paper sizes, and currency placement all differ.&lt;/p&gt;

&lt;p&gt;This is the boring one, and it is also the one that produces refund requests, because a wrong date in a contract line is not a stylistic problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lint we ended up writing
&lt;/h2&gt;

&lt;p&gt;We stopped trying to be clever in the rewriter and moved all of this into a per-language checker that the rewriter must pass:&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;CHECKS&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;fr&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;missing_nbsp_before_punct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;search&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;[A-Za-z\u00C0-\u017F][!?;:]&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zh&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;halfwidth_punct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;re&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;search&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;[\u4e00-\u9fff],[\u4e00-\u9fff]&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;de&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;straight_quotes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'"'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&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;missing_opening_mark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&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;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;and&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                           &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;and&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&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;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CHECKS&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="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a human who actually speaks the language reads the output. Not a translator — a reader. Every language we shipped without that step came back with a ticket.&lt;/p&gt;

&lt;p&gt;The lesson is not "AI text rewriting is hard in other languages". It is that an English-only test suite will tell you everything is fine right up until a customer quotes you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool we build this into is ShipCopy, if you want to see the multilingual side in action: &lt;a href="https://launchcraft.io/?ref=devto" rel="noopener noreferrer"&gt;launchcraft.io&lt;/a&gt; — 10 rewrites free, no card.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>nlp</category>
      <category>writing</category>
      <category>llm</category>
    </item>
    <item>
      <title>Burstiness and n-grams: a 40-line Python AI-text detector, and where it breaks</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Wed, 30 Sep 2026 10:07:22 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/burstiness-and-n-grams-a-40-line-python-ai-text-detector-and-where-it-breaks-25gl</link>
      <guid>https://dev.to/keheai_harvey/burstiness-and-n-grams-a-40-line-python-ai-text-detector-and-where-it-breaks-25gl</guid>
      <description>&lt;p&gt;Every writer who pastes drafts into an AI humanizer eventually asks the same thing: how do I know the text actually sounds human before I send it? I built a small detector to answer that, and the interesting part was not the code — it was watching where it confidently fooled itself.&lt;/p&gt;

&lt;p&gt;Here is the whole thing, runnable as-is.&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;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# split on . ! ? followed by whitespace; keeps the delimiter
&lt;/span&gt;    &lt;span class="k"&gt;return&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="nf"&gt;strip&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&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;split&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;(?&amp;lt;=[.!?])\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&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;s&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;def&lt;/span&gt; &lt;span class="nf"&gt;burstiness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&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="nf"&gt;split&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;None&lt;/span&gt;  &lt;span class="c1"&gt;# too short to mean anything
&lt;/span&gt;    &lt;span class="n"&gt;mean&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="n"&gt;lens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;var&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="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lens&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;var&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;  &lt;span class="c1"&gt;# coefficient of variation
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;repeated_ngrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&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;findall&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;[a-z&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;grams&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; &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;words&lt;/span&gt;&lt;span class="p"&gt;[&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;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;n&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="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;n&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grams&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&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;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&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;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;burstiness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;repeated_ngrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# low burstiness + repeated phrases =&amp;gt; suspicious
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;human&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I shipped the proxy on a Tuesday. Two days later the stream broke. Turned out the client buffered wrong. Fixed it by Friday.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;robot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The implementation of the proxy was completed successfully. The system was designed to be robust. The architecture ensures reliability. The solution provides scalability. The approach demonstrates effectiveness.&lt;/span&gt;&lt;span class="sh"&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="s"&gt;human:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;human&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# usually higher (varied lengths)
&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="s"&gt;robot:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# usually lower (uniform, repetitive)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it and you will see the robot sample scores lower almost every time. Good enough to sort a pile of drafts, right? Three things broke that assumption in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 1: short texts are noise.&lt;/strong&gt; &lt;code&gt;burstiness&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; under 3 sentences, and even at 3–4 sentences the coefficient of variation swings wildly. A two-sentence human reply can look more "AI" than a ten-sentence marketing page. I had to treat anything under ~40 words as "unknown" instead of scoring it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 2: genre beats author.&lt;/strong&gt; Legal summaries, release notes, and API docs are supposed to be uniform. My detector flagged real human technical writing as AI because the domain is naturally low-burstiness. Burstiness alone is a genre signal, not an authorship signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 3: the detector is the easiest thing to game.&lt;/strong&gt; Swapping two sentences, splitting one long one into two, or changing "utilize" to "use" drops the repeated-ngram count to zero. That is exactly what a cheap humanizer does — it moves the score without moving the prose.&lt;/p&gt;

&lt;p&gt;So detection helped me triage, but it never told me the text was &lt;em&gt;good&lt;/em&gt;. The part that actually changed how the output reads was a fixed set of rewrite moves: vary sentence openings, cut the second "that", replace noun-stacks with verbs, and break the rhythm every third sentence. We baked those into a tool we use daily — &lt;a href="https://launchcraft.io/shipcopy" rel="noopener noreferrer"&gt;ShipCopy&lt;/a&gt; — but the patterns work fine by hand once you have seen them fail a detector a few times.&lt;/p&gt;

&lt;p&gt;The takeaway: a 40-line detector is a fine smoke test. It is not a judge of quality, and anyone who promises the text is human is selling the score, not the sentence.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>"Your first 100 users are reading your launch post. This one mistake loses most of them."</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Thu, 17 Sep 2026 22:04:32 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/your-first-100-users-are-reading-your-launch-post-this-one-mistake-loses-most-of-them-aon</link>
      <guid>https://dev.to/keheai_harvey/your-first-100-users-are-reading-your-launch-post-this-one-mistake-loses-most-of-them-aon</guid>
      <description>&lt;p&gt;I shipped a product last year and got 12 users from my launch post. Twelve. Not because the product was bad. Because the post read like the model that wrote it.&lt;/p&gt;

&lt;p&gt;Three months later I rewrote it like a person would talk, kept the same product, same link. Got 140. Same traffic, ten times the pull.&lt;/p&gt;

&lt;p&gt;Here's the mistake, and how to fix it without spending a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake: you described the product, not the reader
&lt;/h2&gt;

&lt;p&gt;Most launch posts open with what the thing does. "An AI tool that helps you write better copy." That sentence is true and useless. The reader's brain files it under "another AI tool" and scrolls.&lt;/p&gt;

&lt;p&gt;The posts that converted for me did the opposite. They opened with a moment the reader already lived through:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You spent two hours writing your Product Hunt draft and it still sounds like a press release."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line got replies. Because someone read it and thought "yeah, that was this morning."&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is smaller than you think
&lt;/h2&gt;

&lt;p&gt;You don't need a copywriter. You need to move one sentence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take your current first paragraph. If it mentions your product name or what it does, that's the problem.&lt;/li&gt;
&lt;li&gt;Replace it with a specific thing your user does badly, in their own words. Pick the most annoying one.&lt;/li&gt;
&lt;li&gt;Keep the rest. Don't rewrite the whole thing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. One sentence moved. The rest of your post was probably fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where non-native English founders get stuck
&lt;/h2&gt;

&lt;p&gt;If English isn't your first language, this is harder, not because you write badly, but because the AI draft you start from is smooth and wrong in the same way every time. It uses words like "leverage" and "seamless" that mark you as a template instantly.&lt;/p&gt;

&lt;p&gt;The trick isn't better English. It's fewer impressive words. "Our tool helps you" beats "Our robust solution empowers." Every time.&lt;/p&gt;

&lt;p&gt;I got tired of doing this by hand on every post, email, and reply, so I built a small thing, ShipCopy, that takes the draft and returns the human version. Paste, get it back, ten free uses, no card. It won't write your product. It just takes the robot edges off what you already wrote.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://launchcraft.io/?ref=devto" rel="noopener noreferrer"&gt;launchcraft.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's the launch post you're stuck on right now? Link it in the comments, I'll tell you which sentence to move.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>marketing</category>
      <category>ai</category>
      <category>writing</category>
    </item>
    <item>
      <title>"Streaming through an OpenAI-compatible proxy: 6 bugs that only show up in production"</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Thu, 17 Sep 2026 03:16:10 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/streaming-through-an-openai-compatible-proxy-6-bugs-that-only-show-up-in-production-2dk0</link>
      <guid>https://dev.to/keheai_harvey/streaming-through-an-openai-compatible-proxy-6-bugs-that-only-show-up-in-production-2dk0</guid>
      <description>&lt;p&gt;I run a small OpenAI-compatible gateway. Non-streaming requests worked on day one. Getting streaming right took me about three weeks of bug reports from my own tools.&lt;/p&gt;

&lt;p&gt;The annoying part: every bug below passes unit tests. They only show up with a real client, a real network, and a model that thinks for 30 seconds before saying anything.&lt;/p&gt;

&lt;p&gt;Here's what broke, and the code that fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;chunk["choices"][0]&lt;/code&gt; throws IndexError
&lt;/h2&gt;

&lt;p&gt;If you pass &lt;code&gt;stream_options: {"include_usage": true}&lt;/code&gt;, OpenAI sends a final chunk that carries token counts and an &lt;strong&gt;empty&lt;/strong&gt; &lt;code&gt;choices&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any code that assumes &lt;code&gt;choices[0]&lt;/code&gt; exists dies right there, at the very end of the stream, after the user already read the full answer. Worst possible moment to 500.&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;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;chunk&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&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;chunk&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;record_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&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;Second half of this bug: not every upstream supports &lt;code&gt;include_usage&lt;/code&gt;. SiliconFlow honors it, some others accept the field and silently drop it. If billing depends on that final chunk, you need a fallback. I run &lt;code&gt;tiktoken&lt;/code&gt; on the accumulated text when no usage chunk shows up, and mark the record as estimated.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. TCP doesn't care about your event boundaries
&lt;/h2&gt;

&lt;p&gt;An SSE event is &lt;code&gt;data: {...}\n\n&lt;/code&gt;. A single &lt;code&gt;read()&lt;/code&gt; gives you half an event, or three events glued together. If you do &lt;code&gt;line.split("data: ")[1]&lt;/code&gt;, sooner or later you hand truncated JSON to &lt;code&gt;json.loads&lt;/code&gt; and the whole stream dies on one packet boundary.&lt;/p&gt;

&lt;p&gt;Buffer until you see the blank line:&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;buf&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;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_any&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&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;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&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;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[DONE]&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="k"&gt;yield&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;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;buf&lt;/code&gt; variable is the entire trick. Two details: some upstreams send &lt;code&gt;[DONE]&lt;/code&gt; without a trailing newline, so flush the leftover buffer on exit; and &lt;code&gt;decode&lt;/code&gt; on a raw bytes chunk can split a multi-byte UTF-8 character in half. Use an incremental decoder or accept "ignore" and lose the occasional character in a Chinese or emoji response. I lost a few characters before switching to &lt;code&gt;codecs.getincrementaldecoder("utf-8")()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;tool_calls&lt;/code&gt; arrive in pieces and you merge by &lt;code&gt;index&lt;/code&gt;, not &lt;code&gt;id&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one cost me a weekend. When the model calls a function, arguments stream in as fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_a1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"get_weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&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="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ci"&lt;/span&gt;&lt;span class="p"&gt;}}]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ty&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Xi"&lt;/span&gt;&lt;span class="p"&gt;}}]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"index"&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="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"'an&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;}}]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; appear only in the first fragment. Later fragments carry &lt;code&gt;index&lt;/code&gt; and nothing else. Key your accumulator on &lt;code&gt;id&lt;/code&gt; and you end up with one tool call whose &lt;code&gt;id&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; and a JSON string you can't parse.&lt;/p&gt;

&lt;p&gt;Key on &lt;code&gt;index&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;calls&lt;/span&gt; &lt;span class="o"&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;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_calls&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&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;id&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="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="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;arguments&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;tc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;fn&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="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="n"&gt;slot&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fn&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="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&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;fn&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;json.loads(slot["arguments"])&lt;/code&gt; once the stream ends. And note that parallel tool calls use index 0, 1, 2. If you keep a single slot you silently drop the second and third call. Coding agents hit this path constantly, which is why "the model said it edited the file but nothing changed" is usually a proxy bug, not a model bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. You cannot retry after you've sent the headers
&lt;/h2&gt;

&lt;p&gt;My failover logic originally retried on any error. But once you've written &lt;code&gt;200 OK&lt;/code&gt; and streamed 40 tokens to the client, there is no retry. The status line is already gone. You can drop the connection or emit a fake error event, and both look like a crash to the user.&lt;/p&gt;

&lt;p&gt;So I added a first-token gate: don't commit the response until the first byte from upstream arrives.&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;open_upstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;ep&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRIMARY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FALLBACK&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;first&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;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&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;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;__anext__&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&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;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;          &lt;span class="c1"&gt;# now it's safe to send 200
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upstream %s died before first byte: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all upstreams down&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;Before this, mid-stream failures ran around 1 in 300 requests. After, failures moved to the window before any user-visible output, which the OpenAI SDK retries on its own. Broken answers went to effectively zero.&lt;/p&gt;

&lt;p&gt;One caveat. Reasoning models can sit quiet for 20 to 30 seconds while they think, so a flat 8-second gate kills them. I use 8s for models I know start fast and 30s for the reasoning ones. Measure yours, don't copy mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Idle connections get killed by whatever sits in front of you
&lt;/h2&gt;

&lt;p&gt;Long think time with zero bytes on the wire means someone hangs up. Nginx defaults to a 60-second read timeout. Cloudflare is stricter. The user sees a truncated answer and blames your service.&lt;/p&gt;

&lt;p&gt;Fix: send an SSE comment line every 15 seconds. It's a spec-valid no-op and the OpenAI SDK skips it.&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;HEARTBEAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: ping&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Race the upstream read against a 15s timer and write the comment when the timer wins. Also set &lt;code&gt;X-Accel-Buffering: no&lt;/code&gt; on your own response. Without that header nginx buffers your carefully streamed chunks into one blob, and the user waits for the entire answer anyway. One header, and the stream suddenly feels fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. When the client leaves, cancel the upstream
&lt;/h2&gt;

&lt;p&gt;The user closes the tab halfway through. Starlette gives you &lt;code&gt;await request.is_disconnected()&lt;/code&gt;, but while you're iterating the upstream body you never check it. The upstream keeps generating, you keep paying, and it holds a connection slot.&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;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;upstream_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_disconnected&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;upstream_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;yield&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found a few hundred of these zombie reads in one day of logs, after a frontend bug spammed reconnects every second. Cheap to fix, real money at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second way to see all of it yourself
&lt;/h2&gt;

&lt;p&gt;Skip the SDK and look at raw bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-N&lt;/span&gt; https://keheai.com/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"deepseek-ai/DeepSeek-V3","stream":true,
       "stream_options":{"include_usage":true},
       "messages":[{"role":"user","content":"count from 1 to 5"}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-N&lt;/code&gt; disables curl's own buffering. You'll see the chunk boundaries as they land, plus that empty-choices usage chunk at the end. Twenty seconds of this teaches more than a day of reading SDK source.&lt;/p&gt;




&lt;p&gt;Every bug on this list lives in the gap between "OpenAI-compatible" and actually compatible. The spec is loose, the SDKs are forgiving, and failures only appear when someone's workflow depends on the stream finishing.&lt;/p&gt;

&lt;p&gt;I built this gateway into keheai.com. It's the same ~200 lines of Python, now serving a few small teams, free tier is 330k tokens if you want to poke at it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>"Your AI launch post reads like a robot. Here's the 10-minute fix."</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:06:33 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/your-ai-launch-post-reads-like-a-robot-heres-the-10-minute-fix-368l</link>
      <guid>https://dev.to/keheai_harvey/your-ai-launch-post-reads-like-a-robot-heres-the-10-minute-fix-368l</guid>
      <description>&lt;p&gt;You pasted your product into ChatGPT, asked for a launch post, and got something that starts with "In today's fast-paced digital landscape." Nobody reads past that line.&lt;/p&gt;

&lt;p&gt;I've done this for every launch I ship. The model gives you smooth, correct, forgettable text. The fix isn't a better prompt. It's knowing the five habits that make AI writing sound like a person wrote it.&lt;/p&gt;

&lt;p&gt;Here's what I actually change before anything goes live.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Kill the hedge words first
&lt;/h2&gt;

&lt;p&gt;AI loves a certain vocabulary. These show up in almost every draft:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;leverage, robust, seamless, delve, navigate, unlock, empower, supercharge, game-changer, in today's [X] landscape&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They say nothing. Replace "leverage our robust API" with "our API handles it." One real verb beats three abstract nouns.&lt;/p&gt;

&lt;p&gt;Before: "We leverage a robust, seamless pipeline to empower your team."&lt;br&gt;
After: "Our pipeline does the work so your team doesn't have to."&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Vary your sentence length
&lt;/h2&gt;

&lt;p&gt;AI writes in the same rhythm: medium sentence, medium sentence, medium sentence. Human writing lurches. Short. Then a longer one that explains the short one.&lt;/p&gt;

&lt;p&gt;Try this on any paragraph: make at least one sentence under six words. It changes how the whole thing feels.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. One outcome, not a feature list
&lt;/h2&gt;

&lt;p&gt;The draft will give you five bullet points. Pick the one thing the reader gets. The rest go in docs, not the pitch.&lt;/p&gt;

&lt;p&gt;Bad: "Fast, secure, scalable, affordable, easy to integrate."&lt;br&gt;
Good: "Ships in an afternoon, not a sprint."&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write like you're explaining it to a friend
&lt;/h2&gt;

&lt;p&gt;Read the sentence out loud. If you'd never say it that way in a call, rewrite it. "Utilize" becomes "use." "Facilitate" becomes "help." This one habit removes most of the robot smell.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Specific numbers beat vague upside
&lt;/h2&gt;

&lt;p&gt;"Improve your workflow" means nothing. "Cut deploy time from 40 minutes to 6" means everything.&lt;/p&gt;

&lt;p&gt;If you don't have the number yet, say what you're measuring. Honesty reads as human.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that used to eat my time
&lt;/h2&gt;

&lt;p&gt;Doing this by hand on every launch post, every email, every Product Hunt reply added up to real hours. So I built a small tool, ShipCopy, that takes the AI draft and returns copy that sounds like a person wrote it. Paste text in, get the human version back. Ten free uses, no card.&lt;/p&gt;

&lt;p&gt;It won't write your product for you. It just takes the robot edges off what you already have.&lt;/p&gt;

&lt;p&gt;If you want to try it: &lt;a href="https://launchcraft.io/?ref=devto" rel="noopener noreferrer"&gt;launchcraft.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's your go-to trick for de-roboting AI text? Drop it in the comments, I'm collecting them.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>startup</category>
      <category>marketing</category>
    </item>
    <item>
      <title>用 200 行 Python 搭一个 OpenAI 兼容网关（双上游自动容灾）</title>
      <dc:creator>Harvey He</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:51:26 +0000</pubDate>
      <link>https://dev.to/keheai_harvey/yong-200-xing-python-da-ge-openai-jian-rong-wang-guan-shuang-shang-you-zi-dong-rong-zai--48g3</link>
      <guid>https://dev.to/keheai_harvey/yong-200-xing-python-da-ge-openai-jian-rong-wang-guan-shuang-shang-you-zi-dong-rong-zai--48g3</guid>
      <description>&lt;p&gt;上周 DeepSeek 官方接口凌晨抖了 20 分钟，我一个跑批任务全红了。那天之后我决定不再把命系在单个供应商上。&lt;/p&gt;

&lt;p&gt;如果你也用 OpenAI SDK 调 DeepSeek / Qwen，最省事的容灾方案不是重写代码，而是前面挡一层 OpenAI 兼容网关：换 base_url 就行，业务代码一行不动。&lt;/p&gt;

&lt;p&gt;我是这么搭的（约 200 行 Python + FastAPI）：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;统一 /v1/chat/completions 入口，把请求转给上游&lt;/li&gt;
&lt;li&gt;配两个上游：硅基流动（主）+ DeepSeek 官方（备）&lt;/li&gt;
&lt;li&gt;主上游超时 8 秒或返回 5xx，自动切备&lt;/li&gt;
&lt;li&gt;每用户配额和日志落在本地 SQLite&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;核心切换就 20 行：&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;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&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;upstream&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PRIMARY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FALLBACK&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Upstream5xx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;log&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;switch &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;upstream&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;踩过的坑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;上游返回的 usage 字段名不一样，要统一成 OpenAI 格式，不然你的 tokenizer 算错&lt;/li&gt;
&lt;li&gt;流式为 SSE，切上游时要保证已发出的 chunk 不被截断&lt;/li&gt;
&lt;li&gt;配额别用 float 存，用整数 token 数，否则并发下会出现负额度&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你不想自己维护这套，也可以用现成的 keheai.com —— 双上游容灾、免费 33 万 tokens 试用、自助发 key。我搭它就是为了自己用，顺手开放出来。&lt;/p&gt;

&lt;p&gt;你想自己搭的话，上面这套够跑中小流量了。要压测脚本我也可以发。&lt;/p&gt;




&lt;p&gt;&lt;em&gt;More from this solo builder: &lt;a href="https://dev.to/keheai_harvey/your-ai-launch-post-reads-like-a-robot-heres-the-10-minute-fix-368l"&gt;if you launch on x producthunt and dev.to you already know the size problem&lt;/a&gt; — &lt;a href="https://dev.to/keheai_harvey/streaming-through-an-openai-compatible-proxy-6-bugs-that-only-show-up-in-production-2dk0"&gt;all my launch notes here&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>llm</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
