<?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>"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>
