<?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: Timur</title>
    <description>The latest articles on DEV Community by Timur (@timur_hitou).</description>
    <link>https://dev.to/timur_hitou</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%2F4052554%2Fac1c1cf3-e145-4e41-9ace-d9df5d31bb12.png</url>
      <title>DEV Community: Timur</title>
      <link>https://dev.to/timur_hitou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timur_hitou"/>
    <language>en</language>
    <item>
      <title>200 OK, content: null — what actually breaks when you build on AI APIs</title>
      <dc:creator>Timur</dc:creator>
      <pubDate>Wed, 29 Jul 2026 07:07:08 +0000</pubDate>
      <link>https://dev.to/timur_hitou/200-ok-content-null-what-actually-breaks-when-you-build-on-ai-apis-3dml</link>
      <guid>https://dev.to/timur_hitou/200-ok-content-null-what-actually-breaks-when-you-build-on-ai-apis-3dml</guid>
      <description>&lt;p&gt;One morning our SEO copy generator started failing with &lt;code&gt;AttributeError: 'NoneType' object has no attribute 'strip'&lt;/code&gt;. The API call had returned &lt;strong&gt;200 OK&lt;/strong&gt; with &lt;code&gt;finish_reason: "stop"&lt;/code&gt;. A completely healthy-looking response, except &lt;code&gt;message.content&lt;/code&gt; was &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some context before we chase that null. We build &lt;a href="https://hitou.site" rel="noopener noreferrer"&gt;Hitou&lt;/a&gt;, a small web product that writes and produces a personalized song about someone you love: an LLM writes lyrics from the story you tell, a music model sings them, and a few minutes later there's a track with a gift page. The happy path took a few weeks. Everything since has been learning, in production, how AI APIs fail.&lt;/p&gt;

&lt;p&gt;This post is the field notes: the failure modes that actually reached (or almost reached) paying users, and the guards that stopped them from happening twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline in one paragraph
&lt;/h2&gt;

&lt;p&gt;A wizard collects the story (who the song is about, the occasion, the inside jokes). An LLM turns that into structured lyrics — verses, chorus, style notes — as JSON. The lyrics go to Suno v5 through a provider API, which returns two rendered takes plus word-level timestamps. We cut a preview, build a karaoke-style highlight track from the timestamps, and serve the whole thing from a FastAPI app. Two music providers sit behind a switch: a primary and a fallback, so one vendor having a bad day doesn't stop orders.&lt;/p&gt;

&lt;p&gt;Sounds simple. The interesting part is that every arrow in that pipeline is a third-party API that can fail in ways the status code will never tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story #1: 200 OK, &lt;code&gt;content: null&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Back to that null. Here's the thing about LLM routers (we use OpenRouter): one model slug is served by many independent hosts, and the router picks one per request. We eventually ran the same prompt across every host serving that model. &lt;strong&gt;Exactly one of them was broken.&lt;/strong&gt; It returned the entire completion in the &lt;code&gt;reasoning&lt;/code&gt; field and &lt;code&gt;null&lt;/code&gt; in &lt;code&gt;content&lt;/code&gt;, wrapped in a clean 200 with a normal &lt;code&gt;finish_reason&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three lessons that now live in our code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your exception guard is probably shaped wrong.&lt;/strong&gt; We had &lt;code&gt;except (KeyError, IndexError, TypeError)&lt;/code&gt; around the response parsing. None of those fire here: the key exists, the value is &lt;code&gt;null&lt;/code&gt;. The failure only surfaced two calls later, as an &lt;code&gt;AttributeError&lt;/code&gt; in unrelated-looking code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the host, not just the model.&lt;/strong&gt; The response body (and every streaming chunk) carries a &lt;code&gt;provider&lt;/code&gt; field. Without logging it, "the model is flaky" and "one host out of 33 is broken" are indistinguishable — and the second one has a config-level fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate emptiness after stripping decoration.&lt;/strong&gt; The same class of host bug can return a bare fenced &lt;code&gt;json&lt;/code&gt; code block with nothing inside it. If you check &lt;code&gt;if not content&lt;/code&gt; before stripping the fence, the garbage passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix was boring, which is the point: a per-request &lt;code&gt;provider: {"ignore": [...]}&lt;/code&gt; list, driven by an env var, plus an account-level ignore list in the router's dashboard as the no-deploy emergency lever.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story #2: the song titled "None"
&lt;/h2&gt;

&lt;p&gt;A week later, the same bug shape came back one level deeper. This one cost real money.&lt;/p&gt;

&lt;p&gt;The LLM response was now validated: non-null, non-empty after stripping code fences, parsed JSON, all the required sections in place. What our checks didn't cover was &lt;strong&gt;leaf types&lt;/strong&gt;: a key can be present, the structure can look right, and the value can still be &lt;code&gt;null&lt;/code&gt;. And our code did this:&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;lyrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&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;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;lyrics&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks defensive, right? It isn't. The default in &lt;code&gt;.get()&lt;/code&gt; only applies when the key is &lt;strong&gt;missing&lt;/strong&gt;. When the payload is &lt;code&gt;{"lyrics": null}&lt;/code&gt;, the key exists, &lt;code&gt;.get()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, and &lt;code&gt;str(None)&lt;/code&gt; is the string &lt;code&gt;"None"&lt;/code&gt; — eight characters, non-empty, sails straight past &lt;code&gt;if not lyrics&lt;/code&gt; and past every &lt;code&gt;or "fallback"&lt;/code&gt; downstream.&lt;/p&gt;

&lt;p&gt;So we paid a music API to professionally sing the word "None". A related null in a word-timestamps payload put the literal word "None" into a paying customer's karaoke highlight. Nothing crashed. No exception handler in the codebase had anything to catch.&lt;/p&gt;

&lt;p&gt;The correct idiom is one token different:&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;lyrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&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;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;lyrics&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;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After it bit us twice in one week, we stopped trusting review to catch it and wrote a test that walks the AST of the whole codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_offenders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Flag every str(&amp;lt;x&amp;gt;.get(&amp;lt;key&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;literal&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)) call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;found&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;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;str&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="ow"&gt;and&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attribute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="ow"&gt;and&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;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
                    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineno&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why AST and not grep? Because grep missed a real offender: a multi-line call with indexing in the middle (&lt;code&gt;str(messages[-1].get("content", ""))&lt;/code&gt;). The AST doesn't care how the call is formatted.&lt;/p&gt;

&lt;p&gt;We deliberately gate only &lt;code&gt;str(...)&lt;/code&gt;. &lt;code&gt;float(None)&lt;/code&gt; raises a loud &lt;code&gt;TypeError&lt;/code&gt; that any error path catches; &lt;code&gt;str(None)&lt;/code&gt; degrades &lt;em&gt;silently&lt;/em&gt; and ships to a user. Gate the quiet failures — the loud ones catch themselves.&lt;/p&gt;

&lt;p&gt;"Just put pydantic on the boundary" is a fair response, and strict response models would catch this too. In practice we have several third-party boundaries (an LLM router, two music providers, a transcription API), they evolve at different speeds, and not all of them have earned a full typed model yet. The AST gate is one 30-line test that covers every &lt;code&gt;str(.get())&lt;/code&gt; in the codebase — including the boundaries nobody has gotten around to modelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story #3: word timestamps lie (in three specific ways)
&lt;/h2&gt;

&lt;p&gt;Suno v5 returns word-level timestamps with the audio. That's what our karaoke highlight is built on. The contract looks clean: &lt;code&gt;{word, startS, endS}&lt;/code&gt; per token, and the data mostly matches it. "Mostly" is the whole job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phantom first lines.&lt;/strong&gt; The aligner sometimes emits the opening line &lt;em&gt;twice&lt;/em&gt;: one copy with all word starts compressed into under 2 seconds (raced over the intro instrumental), then the real sung line right after. Detection: two adjacent identical lines where the first spans &amp;lt;2s. Drop the compressed one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Squeezed lines.&lt;/strong&gt; A sung line packed into 0.85 seconds — six words — with the stolen time dumped onto the &lt;em&gt;next&lt;/em&gt; word, stretched to ~3s. The trick is detecting it relative to the track's own tempo: we compare each line's per-word pacing against the median inter-word step, and redistribute the line evenly across its real window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ends absorb silence.&lt;/strong&gt; A word's &lt;code&gt;endS&lt;/code&gt; can swallow an instrumental gap; we've measured 11 seconds. If your highlight follows word ends, it drifts badly. Follow the starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The umbrella policy above all three: &lt;strong&gt;no karaoke beats broken karaoke.&lt;/strong&gt; The normalizer returns &lt;code&gt;None&lt;/code&gt; on anything it can't trust — too few words, character-error-rate above threshold (healthy sung tracks measure ~0.20-0.25 on the vendor's CER metric; we reject above 0.4), non-monotonic timestamps, timestamps past the track duration. The page quietly renders without the feature. Graceful degradation is a product decision, not just an ops one.&lt;/p&gt;

&lt;h2&gt;
  
  
  War story #4: generation takes 3-9 minutes. Sometimes it takes 30.
&lt;/h2&gt;

&lt;p&gt;Music generation is slow and &lt;em&gt;occasionally&lt;/em&gt; very slow: same API, same payload, 25-30 minutes instead of the usual 3-9. Early on we treated the long tail as failure — time out, refund, re-create. That's exactly wrong, because re-creating a paid render doubles the cost while the original job is often still cooking.&lt;/p&gt;

&lt;p&gt;What we run now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resume, never re-create.&lt;/strong&gt; Every generation stores its provider task id. Anything that looks stuck is &lt;em&gt;resumed&lt;/em&gt; — polled on the same task — not re-submitted. The provider switch resolves adapters by name, so an in-flight job always resumes on the provider that created it, even if we've flipped the primary since.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reconciler as the safety net.&lt;/strong&gt; A background loop sweeps orders that stopped moving (process restarts mid-poll, crashed workers, the lot) and re-attaches to their provider tasks. The deploy script waits on a health check that includes the reconciler's heartbeat, so "the app is up but the sweeper is dead" fails the deploy instead of failing a customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetric fallback.&lt;/strong&gt; Primary healthy → new orders go primary, fall back on error. Primary manually switched off → the chain is the fallback &lt;em&gt;only&lt;/em&gt;. A kill switch that still quietly routes to the "disabled" provider isn't a kill switch. Resumes bypass the chain entirely (see above).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we'd tell our past selves
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Status codes are decoration.&lt;/strong&gt; Every incident above arrived wrapped in a 200. Validate the &lt;em&gt;shape and content&lt;/em&gt; of what you got, at the boundary, before it enters your system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;str(x.get(k, ""))&lt;/code&gt; is a bug.&lt;/strong&gt; Write &lt;code&gt;str(x.get(k) or "")&lt;/code&gt;. If the codebase is bigger than one file, write the AST gate — it's 30 lines and it never gets tired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify the host behind the model.&lt;/strong&gt; Routed APIs mean your "flaky model" may be one broken machine with a config-level fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer degradation over garbage.&lt;/strong&gt; A missing feature block is fine. The word "None" sung in a birthday song is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume beats retry&lt;/strong&gt; for anything long-running and paid. Store the task id; treat "stuck" as "re-attach", not "re-buy".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic engineering. It's the unglamorous layer between "the demo works" and "strangers pay for it" — which, if you're gluing LLMs and generative audio together in 2026, is where most of the actual work turns out to live.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're curious what all this plumbing produces: &lt;a href="https://hitou.site" rel="noopener noreferrer"&gt;hitou.site&lt;/a&gt; — it turns a story about someone into a finished song in a few minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
