<?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: Seven</title>
    <description>The latest articles on DEV Community by Seven (@seven7763).</description>
    <link>https://dev.to/seven7763</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%2F4025722%2F90454edb-090f-4e2c-95d7-1d90108f07d4.jpg</url>
      <title>DEV Community: Seven</title>
      <link>https://dev.to/seven7763</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seven7763"/>
    <language>en</language>
    <item>
      <title>Four ways reasoning models hide their thinking (and what that does to your bill)</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:40:23 +0000</pubDate>
      <link>https://dev.to/seven7763/four-ways-reasoning-models-hide-their-thinking-and-what-that-does-to-your-bill-410e</link>
      <guid>https://dev.to/seven7763/four-ways-reasoning-models-hide-their-thinking-and-what-that-does-to-your-bill-410e</guid>
      <description>&lt;p&gt;Same question, four endpoints: "show your reasoning." Four different answers. Not four opinions — four wire formats, four billing behaviors, and four ways your audit trail goes blind. This piece is the map I wish I had before I assumed any of them. &lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=en_launch" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, an OpenAI-compatible gateway — one key, many models, all four shapes behind one endpoint.&lt;/strong&gt; Bias declared up front; the captures below include results that make my own gateway look wrong, and every probe here runs the same way against anyone's endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape 1 — shared budget (GLM)
&lt;/h3&gt;

&lt;p&gt;GLM's reasoning-capable IDs run the chain of thought &lt;em&gt;inside&lt;/em&gt; the completion. &lt;code&gt;max_tokens&lt;/code&gt; is the budget for thinking &lt;strong&gt;and&lt;/strong&gt; the answer together. Undersize it and you don't get an error — you get empty &lt;code&gt;content&lt;/code&gt; with &lt;code&gt;finish_reason: "length"&lt;/code&gt;. The model "said" nothing because it spent everything thinking.&lt;/p&gt;

&lt;p&gt;The trap is this: it looks exactly like a dead endpoint. The tell is in the finish reason — &lt;code&gt;length&lt;/code&gt; means budget, not outage. Size the cap for the thinking first; the answer gets what's left.&lt;/p&gt;

&lt;p&gt;Behind the gateway, same shape, same warning label — and the accounting gets weirder. I sent &lt;code&gt;max_tokens: 8&lt;/code&gt; at &lt;code&gt;glm-4.7-flash&lt;/code&gt; through my own endpoint (free-group capture, 2026-09-16; signals not proof):&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="nl"&gt;"message"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"reasoning_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.  **Analyze the User"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"length"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The budget-trap textbook sample, alive at my own party: eight tokens of pure thinking, the answer starved to an empty string. One detail to note for Shape 4, though: that same response reported &lt;code&gt;completion_tokens_details.reasoning_tokens: 0&lt;/code&gt; — even with the whole budget visibly spent on thinking. A counter that is present is not a counter that works; verify before you build a dashboard on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape 2 — separate field (DeepSeek)
&lt;/h3&gt;

&lt;p&gt;DeepSeek's R-series puts reasoning in a dedicated &lt;code&gt;reasoning_content&lt;/code&gt; field on the message, separate from &lt;code&gt;content&lt;/code&gt;. Clean — until the second turn of a tool loop. DeepSeek's API documents that the previous assistant message must be echoed back &lt;em&gt;including&lt;/em&gt; its &lt;code&gt;reasoning_content&lt;/code&gt;, or the request fails with a 400.&lt;/p&gt;

&lt;p&gt;Build your history from the raw message objects, never from a trimmed summary of the visible text. The moment a "helpful" history compactor strips the reasoning field, every subsequent call fails in a way that looks like a gateway problem.&lt;/p&gt;

&lt;p&gt;Except the split itself is a property of the endpoint you're actually talking to, not of the model's name. I called &lt;code&gt;deepseek-r1-32b&lt;/code&gt; through my gateway twice (free-group captures, 2026-09-16; signals not proof). Both times &lt;code&gt;reasoning_content&lt;/code&gt; was absent entirely — the thinking came back fused into &lt;code&gt;content&lt;/code&gt;, inside think-style delimiters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content: "&amp;lt;think&amp;gt;\nFirst, the farmer initially has 17 sheep.\n…\n&amp;lt;/think&amp;gt;\n\n**Solution:** …"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 400-echo rule is DeepSeek's documented API contract — against the official endpoint. On a translated channel that never produced the field in the first place, there is nothing to echo and nothing to enforce, and my captures confirmed it: no error, just a fused blob. The lesson generalizes past my gateway: before your multi-turn logic depends on &lt;code&gt;reasoning_content&lt;/code&gt; existing, check that &lt;em&gt;your&lt;/em&gt; endpoint actually returns it. The model ID is not the contract; the wire is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape 3 — explicit flag (MiniMax)
&lt;/h3&gt;

&lt;p&gt;MiniMax reasoning IDs keep thinking out of the answer text only when asked: pass &lt;code&gt;extra_body={"reasoning_split": True}&lt;/code&gt; and the chain of thought arrives in &lt;code&gt;reasoning_details&lt;/code&gt;. Without the flag, the split is not guaranteed — reasoning may stay fused into &lt;code&gt;content&lt;/code&gt; or be dropped.&lt;/p&gt;

&lt;p&gt;If your pipeline needs machine-separable reasoning, set the flag on every call. Hope is not a configuration — and on some endpoints, neither is the flag.&lt;/p&gt;

&lt;p&gt;No MiniMax model exists in my free-group catalog, so I probed the flag mechanism on two reasoning IDs that do, &lt;code&gt;mimo-v2.5&lt;/code&gt; and &lt;code&gt;qwen3-30b&lt;/code&gt; (free-group captures, 2026-09-16; signals not proof). On my gateway, &lt;code&gt;mimo-v2.5&lt;/code&gt; with &lt;code&gt;reasoning_split: true&lt;/code&gt; and without it produced the same response shape — thinking fused into &lt;code&gt;content&lt;/code&gt;, no separate field, no error, no acknowledgement. The flag was silently swallowed. Then &lt;code&gt;qwen3-30b&lt;/code&gt; with &lt;code&gt;chat_template_kwargs: {"enable_thinking": false}&lt;/code&gt;: thinking came back anyway in a &lt;code&gt;reasoning_content&lt;/code&gt; field, while &lt;code&gt;content&lt;/code&gt; was empty and &lt;code&gt;finish_reason&lt;/code&gt; was &lt;code&gt;"stop"&lt;/code&gt;. A wrong answer that looks like a finished one.&lt;/p&gt;

&lt;p&gt;Two explicit instructions in the request, both silently overridden. That's the honest qualifier for Shape 3 as a family: the flag says what the vendor's &lt;em&gt;docs&lt;/em&gt; promise; it does not guarantee what &lt;em&gt;your&lt;/em&gt; endpoint honors. Verify the response shape every time — a configuration you can't observe being applied is a wish, not a setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape 4 — usage accounting only (Grok)
&lt;/h3&gt;

&lt;p&gt;xAI reports thinking as an accounting detail, not a message field: &lt;code&gt;usage.completion_tokens_details.reasoning_tokens&lt;/code&gt; is meant to count the completion tokens consumed by reasoning. The chain of thought itself is never returned.&lt;/p&gt;

&lt;p&gt;That should make Grok the cheapest of the four to log — the numbers arrive in your usage stream already — and the least inspectable, since there is nothing to read, only to count. If your billing dashboards read only top-level usage numbers, thinking spend is invisible to them.&lt;/p&gt;

&lt;p&gt;"Should." Here is one of three &lt;code&gt;grok-4.3&lt;/code&gt; calls through my gateway (free-group capture, 2026-09-16; signals not proof). The message: visible step-by-step reasoning inlined in &lt;code&gt;content&lt;/code&gt;, no reasoning field anywhere. The usage:&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="nl"&gt;"completion_tokens_details"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&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;The field is present and the count is zero on all three captures. So the Shape 4 caveat, earned the hard way: field present ≠ counting happening. An audit trail built on "the field exists, therefore the tokens are attributed" inherits whatever the implementation quietly doesn't do. Check that the counter actually moves before you trust it to allocate cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  What each shape costs you
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Visible reasoning&lt;/th&gt;
&lt;th&gt;Separable for parsing&lt;/th&gt;
&lt;th&gt;Correct billing surface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GLM&lt;/td&gt;
&lt;td&gt;sometimes (&lt;code&gt;reasoning_content&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;per-request&lt;/td&gt;
&lt;td&gt;inside &lt;code&gt;max_tokens&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;per docs: always (field)&lt;/td&gt;
&lt;td&gt;yes, where honored&lt;/td&gt;
&lt;td&gt;separate field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax&lt;/td&gt;
&lt;td&gt;only with the flag&lt;/td&gt;
&lt;td&gt;yes, flag honored&lt;/td&gt;
&lt;td&gt;standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;td&gt;only via counter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reasoning_tokens&lt;/code&gt;, if it counts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cost attribution, audit trails, client parsing and history management all break differently against each row. The GLM budget trap and the DeepSeek echo requirement are the two that turn into 2am pages; the Grok accounting gap is the one that turns into a quietly wrong bill. And the qualifier column is the point: "per docs", "where honored", "if it counts" — every row assumes a contract, and the captures above show a contract on each row moving under translation.&lt;/p&gt;

&lt;h3&gt;
  
  
  A four-line probe
&lt;/h3&gt;

&lt;p&gt;You do not have to memorize any of this. Send one fixed prompt at temperature 0 and dump the three surfaces that differ:&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;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&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;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# room for GLM-style shared budgets
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reason step by step: what is 17*23?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&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;choices&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;message&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;finish_reason:&lt;/span&gt;&lt;span class="sh"&gt;"&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;choices&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;finish_reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message keys:  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;       &lt;span class="c1"&gt;# reasoning_content? reasoning_details?
&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;usage details: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens_details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# reasoning_tokens?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading the output: empty content with &lt;code&gt;finish_reason: "length"&lt;/code&gt; is Shape 1 (raise the cap). A &lt;code&gt;reasoning_content&lt;/code&gt; key on the message is Shape 2 — and from then on, echo it in multi-turn. A &lt;code&gt;reasoning_details&lt;/code&gt; field means Shape 3 — set the flag and &lt;em&gt;verify it stuck&lt;/em&gt;. &lt;code&gt;reasoning_tokens&lt;/code&gt; in the usage details with nothing on the message is Shape 4 — and a zero there does not mean nothing was thought; I watched Shape 4 channels report 0 while reasoning in plain sight. Run this when you pin the model, and again whenever the provider ships a new version; the shape is part of the contract, and contracts move.&lt;/p&gt;

&lt;h3&gt;
  
  
  The general rule
&lt;/h3&gt;

&lt;p&gt;Never assume reasoning is visible, separable, or billable the same way twice. The four shapes above are current as of this writing, and providers move — which is exactly why the probe comes before the assumption, every time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>reasoning</category>
      <category>api</category>
    </item>
    <item>
      <title>Anthropic Messages is a different protocol, not a flag — and shims leak</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Wed, 16 Sep 2026 01:44:50 +0000</pubDate>
      <link>https://dev.to/seven7763/anthropic-messages-is-a-different-protocol-not-a-flag-and-shims-leak-467g</link>
      <guid>https://dev.to/seven7763/anthropic-messages-is-a-different-protocol-not-a-flag-and-shims-leak-467g</guid>
      <description>&lt;p&gt;The failure has a shape. Your Claude-based agent connects to the new endpoint, answers the first question well, executes a tool, comes back, executes another — and then somewhere around step seven it stops calling tools and starts &lt;em&gt;narrating&lt;/em&gt; that it would call a tool. Or it loses the thread of a long file edit. Or your prompt-cache hit rate, which you were counting on, is quietly zero and nothing in the response says so.&lt;/p&gt;

&lt;p&gt;What usually happened is that the endpoint doesn't speak Anthropic's Messages protocol. It speaks OpenAI chat completions and translates. &lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=en_launch" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a gateway that implements &lt;code&gt;/v1/messages&lt;/code&gt; alongside the OpenAI protocol&lt;/strong&gt; — which is exactly why I've spent an unreasonable amount of time in the gap between the two. The tests at the end are written so you can point them at any endpoint, including mine.&lt;/p&gt;

&lt;p&gt;To be fair to shims: if all you do is single-turn chat with text in and text out, a translation layer is genuinely fine, and it's how a lot of tooling gets broad model coverage cheaply. The argument below is about agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two protocols disagree about what a message is
&lt;/h3&gt;

&lt;p&gt;In OpenAI's chat completions, an assistant turn is &lt;code&gt;content: string | null&lt;/code&gt; plus a separate &lt;code&gt;tool_calls&lt;/code&gt; array. In Anthropic Messages, an assistant turn is an &lt;strong&gt;ordered array of typed content blocks&lt;/strong&gt;: &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;tool_use&lt;/code&gt;, &lt;code&gt;thinking&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, and others, interleaved in the order the model produced them.&lt;/p&gt;

&lt;p&gt;That ordering is real information. "Let me check the config file" → &lt;code&gt;tool_use&lt;/code&gt; → "and also the lockfile" → &lt;code&gt;tool_use&lt;/code&gt; is one assistant turn with four blocks. Flatten it into a string plus a call array and you've thrown away which text belonged before which call. On a single turn nobody notices. Fifteen turns into an agent loop, the transcript the model sees is subtly not the transcript it wrote, and behaviour drifts.&lt;/p&gt;

&lt;h3&gt;
  
  
  tool_use and tool_result are shaped differently on purpose
&lt;/h3&gt;

&lt;p&gt;Three concrete mismatches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;input&lt;/code&gt; is a JSON object.&lt;/strong&gt; Anthropic's &lt;code&gt;tool_use&lt;/code&gt; block carries &lt;code&gt;input: {...}&lt;/code&gt;. OpenAI carries &lt;code&gt;function.arguments&lt;/code&gt; as a &lt;strong&gt;JSON-encoded string&lt;/strong&gt;. A shim has to serialise and deserialise on every hop. Usually that round-trips fine; when it doesn't — key ordering, float formatting, non-ASCII escaping — the bytes change, and byte-identical prefixes are exactly what prompt caching depends on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_result&lt;/code&gt; belongs to a user turn.&lt;/strong&gt; In Anthropic's model, the tool output comes back as a &lt;code&gt;tool_result&lt;/code&gt; content block inside a &lt;code&gt;user&lt;/code&gt; message, referencing &lt;code&gt;tool_use_id&lt;/code&gt;. OpenAI uses a dedicated &lt;code&gt;role: "tool"&lt;/code&gt; message. Mapping between them is mechanical but lossy in one direction: Anthropic's &lt;code&gt;tool_result&lt;/code&gt; has an &lt;code&gt;is_error: true&lt;/code&gt; flag, and OpenAI's has nowhere to put it. Through a shim, a failed tool call arrives as an ordinary string that happens to start with "Error:", and the model has to infer from prose what the protocol was willing to tell it directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple parallel &lt;code&gt;tool_use&lt;/code&gt; blocks&lt;/strong&gt; in one assistant turn, each needing its own &lt;code&gt;tool_result&lt;/code&gt; block in the following user turn, in an order the API validates. Shims that queue calls one at a time serialise what should have been parallel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Streaming is a typed event protocol
&lt;/h3&gt;

&lt;p&gt;Anthropic's SSE stream isn't a sequence of interchangeable chunks. It's &lt;code&gt;message_start&lt;/code&gt;, then per block &lt;code&gt;content_block_start&lt;/code&gt; / &lt;code&gt;content_block_delta&lt;/code&gt; / &lt;code&gt;content_block_stop&lt;/code&gt; carrying an &lt;code&gt;index&lt;/code&gt;, then &lt;code&gt;message_delta&lt;/code&gt; (which is where &lt;code&gt;stop_reason&lt;/code&gt; and the final output token count live), then &lt;code&gt;message_stop&lt;/code&gt;, with &lt;code&gt;ping&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; events possible anywhere.&lt;/p&gt;

&lt;p&gt;The delta types matter too: &lt;code&gt;text_delta&lt;/code&gt; for prose, &lt;strong&gt;&lt;code&gt;input_json_delta&lt;/code&gt; for tool arguments streaming in as partial JSON&lt;/strong&gt;, &lt;code&gt;thinking_delta&lt;/code&gt; and &lt;code&gt;signature_delta&lt;/code&gt; for extended thinking. The Anthropic SDK's streaming helpers accumulate these into typed objects for you. A shim re-wrapping OpenAI chunks can usually produce plausible &lt;code&gt;text_delta&lt;/code&gt; events; what it typically cannot produce faithfully is incremental &lt;code&gt;input_json_delta&lt;/code&gt;, so tool arguments show up all at once at the end. If you render tool calls live in a UI, that's a visible regression. If you gate on partial arguments, it's a broken feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;system&lt;/code&gt; is a parameter, and that's where caching lives
&lt;/h3&gt;

&lt;p&gt;Anthropic takes the system prompt as a &lt;strong&gt;top-level &lt;code&gt;system&lt;/code&gt; field&lt;/strong&gt;, either a string or an array of content blocks. The array form is the important one, because that's where you attach &lt;code&gt;cache_control: {"type": "ephemeral"}&lt;/code&gt; to mark a cacheable prefix.&lt;/p&gt;

&lt;p&gt;There is no place in OpenAI's message list to express that. So a shim converts your system blocks into a &lt;code&gt;{"role": "system"}&lt;/code&gt; message and &lt;strong&gt;the cache markers evaporate&lt;/strong&gt;. Nothing errors. Your requests just cost what they cost and take as long as they take, and &lt;code&gt;usage.cache_read_input_tokens&lt;/code&gt; — the field that would have told you — isn't in an OpenAI-shaped response at all. For a coding agent that resends a large system prompt and file context on every turn, this is the single most expensive silent difference in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;stop_reason&lt;/code&gt; has six values
&lt;/h3&gt;

&lt;p&gt;Anthropic returns &lt;code&gt;end_turn&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;, &lt;code&gt;stop_sequence&lt;/code&gt;, &lt;code&gt;tool_use&lt;/code&gt;, &lt;code&gt;pause_turn&lt;/code&gt;, or &lt;code&gt;refusal&lt;/code&gt;. OpenAI returns &lt;code&gt;stop&lt;/code&gt;, &lt;code&gt;length&lt;/code&gt;, &lt;code&gt;tool_calls&lt;/code&gt;, or &lt;code&gt;content_filter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Agent loops branch on &lt;code&gt;stop_reason == "tool_use"&lt;/code&gt;. Through a shim that maps everything into OpenAI's smaller vocabulary and back, you can end up with &lt;code&gt;end_turn&lt;/code&gt; on a turn that actually contained a tool call, at which point the loop exits and the agent "decides" to stop working. &lt;code&gt;pause_turn&lt;/code&gt; and &lt;code&gt;refusal&lt;/code&gt; have no OpenAI equivalent whatsoever and get mapped to something approximate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thinking blocks are signed
&lt;/h3&gt;

&lt;p&gt;With extended thinking enabled, the response contains &lt;code&gt;thinking&lt;/code&gt; blocks with a &lt;code&gt;signature&lt;/code&gt;. On subsequent turns of a tool-use loop you must pass those blocks back &lt;strong&gt;verbatim&lt;/strong&gt;, signature included. A shim that flattens thinking into text, or drops it as "internal", breaks that contract — you'll get an error on the next turn, or thinking will be silently disabled, and the model you're paying reasoning tokens for will stop reasoning.&lt;/p&gt;

&lt;p&gt;One thing my captures taught me: the &lt;code&gt;signature&lt;/code&gt; field is itself part of what an implementation can quietly not produce. On my gateway's &lt;code&gt;/v1/messages&lt;/code&gt;, the &lt;code&gt;thinking&lt;/code&gt; block came back with its text but no &lt;code&gt;signature&lt;/code&gt; key at all (free-group capture, 2026-09-16; signals not proof). The shape looks right; the contract that depends on the shape is not fully honoured. If you build anything that replays thinking blocks, check for the field's presence before you trust the round trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it costs you
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol feature&lt;/th&gt;
&lt;th&gt;Through a shim&lt;/th&gt;
&lt;th&gt;How you notice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordered content blocks&lt;/td&gt;
&lt;td&gt;Flattened&lt;/td&gt;
&lt;td&gt;Drift in long agent transcripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;is_error&lt;/code&gt; on tool_result&lt;/td&gt;
&lt;td&gt;Lost&lt;/td&gt;
&lt;td&gt;Model retries a tool that can't succeed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input_json_delta&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batched to the end&lt;/td&gt;
&lt;td&gt;Tool args appear all at once in the UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache_control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dropped&lt;/td&gt;
&lt;td&gt;Nothing — no error, no cache field, higher cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop_reason: tool_use&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approximated&lt;/td&gt;
&lt;td&gt;Agent loop exits early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;thinking&lt;/code&gt; + signature&lt;/td&gt;
&lt;td&gt;Stripped&lt;/td&gt;
&lt;td&gt;Next-turn error, or thinking silently off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;max_tokens&lt;/code&gt; enforced&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;A gateway can pass shape tests and still not enforce this one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Five tests
&lt;/h3&gt;

&lt;p&gt;Test 1 is the one-liner, with a caveat I earned by running it. Anthropic's API documents &lt;code&gt;max_tokens&lt;/code&gt; as required, and a strict native endpoint returns a 400 when you omit it. But a 200 is not a shim confession: enforcement varies across implementations — my own gateway's &lt;code&gt;/v1/messages&lt;/code&gt; path answered the request happily without &lt;code&gt;max_tokens&lt;/code&gt; (free-tier capture from 2026-09-16, signals not proof). So read this one one-sidedly: a 400 with an Anthropic-shaped error body is a strong native signal; a 200 sends you to tests 2–5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.daoxe.com          &lt;span class="c"&gt;# note: no /v1 suffix here&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&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;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_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;"anthropic-version: 2023-06-01"&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":"&amp;lt;exact-id&amp;gt;","messages":[{"role":"user","content":"hi"}]}'&lt;/span&gt;
&lt;span class="c"&gt;# 400 + Anthropic error shape: strong native signal&lt;/span&gt;
&lt;span class="c"&gt;# 200: inconclusive — enforcement varies; run the rest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test 2 — the error envelope. Anthropic's shape is &lt;code&gt;{"type":"error","error":{"type":"invalid_request_error","message":"..."}}&lt;/code&gt;. An OpenAI-shaped &lt;code&gt;{"error":{"code":...}}&lt;/code&gt; is a tell.&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_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;"anthropic-version: 2023-06-01"&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":"&amp;lt;exact-id&amp;gt;","max_tokens":16,"messages":[],"nonsense_field":1}'&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The envelope is the better tell than any single field inside it. On my gateway the Messages path returned the right outer envelope but leaked its internals (&lt;code&gt;"type":"new_api_error"&lt;/code&gt;, not the Messages error vocabulary), while the chat-completions path returned the OpenAI-shaped &lt;code&gt;code&lt;/code&gt; field — one request each, and you can see which door you came in.&lt;/p&gt;

&lt;p&gt;Test 3 — tool_use fidelity. Check that &lt;code&gt;stop_reason&lt;/code&gt; is &lt;code&gt;tool_use&lt;/code&gt; and that the block's &lt;code&gt;input&lt;/code&gt; is an &lt;strong&gt;object&lt;/strong&gt;, not a string.&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_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;"anthropic-version: 2023-06-01"&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":"&amp;lt;exact-id&amp;gt;","max_tokens":256,
    "tools":[{"name":"get_weather","description":"Current weather",
      "input_schema":{"type":"object","properties":{"city":{"type":"string"}},
      "required":["city"]}}],
    "tool_choice":{"type":"tool","name":"get_weather"},
    "messages":[{"role":"user","content":"Weather in Osaka?"}]
  }'&lt;/span&gt; | jq &lt;span class="s1"&gt;'{stop_reason, input_type: (.content[] | select(.type=="tool_use") | .input | type)}'&lt;/span&gt;
&lt;span class="c"&gt;# want: {"stop_reason":"tool_use","input_type":"object"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;input_type&lt;/code&gt; half is the one that usually separates the two worlds — a string where an object belongs is a translation scar. The &lt;code&gt;stop_reason&lt;/code&gt; half is the stricter criterion, and don't assume any particular endpoint passes it: on my gateway's &lt;code&gt;/v1/messages&lt;/code&gt;, forced &lt;code&gt;tool_choice&lt;/code&gt; produced a proper &lt;code&gt;tool_use&lt;/code&gt; block with an object &lt;code&gt;input&lt;/code&gt;, but &lt;code&gt;stop_reason: "end_turn"&lt;/code&gt; (free-group capture, 2026-09-16; signals, not proof). That is exactly the failure described above, arriving through the right-shaped door.&lt;/p&gt;

&lt;p&gt;Test 4 — streaming event names. Grep the raw stream for the typed events.&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;-sN&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_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;"anthropic-version: 2023-06-01"&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":"&amp;lt;exact-id&amp;gt;","max_tokens":64,"stream":true,
       "messages":[{"role":"user","content":"Count to five."}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'^event: .*'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="c"&gt;# want at least: message_start, content_block_start, content_block_delta,&lt;/span&gt;
&lt;span class="c"&gt;#                content_block_stop, message_delta, message_stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the five, this one drew the cleanest line when I ran both paths of my gateway on the same day: the &lt;code&gt;/v1/messages&lt;/code&gt; side emitted all six event names; the &lt;code&gt;/v1/chat/completions&lt;/code&gt; side emitted &lt;strong&gt;zero&lt;/strong&gt; &lt;code&gt;event:&lt;/code&gt; lines — plain &lt;code&gt;data:&lt;/code&gt; chunks, because that's what the OpenAI wire format is. (Free-group captures, 2026-09-16; signals not proof.) A shim can fake most of the field names one response at a time; re-typing a whole event protocol under every chunk is work almost nobody does.&lt;/p&gt;

&lt;p&gt;Test 5 — prompt caching. Send a long system prompt as a block array with &lt;code&gt;cache_control&lt;/code&gt;, twice, and read the usage fields. (Providers enforce a minimum cacheable prefix length, so make the system text genuinely long — a few thousand tokens.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"model":"&amp;lt;exact-id&amp;gt;","max_tokens":16,
 "system":[{"type":"text","text":"&amp;lt;several-thousand-tokens-of-stable-context&amp;gt;",
            "cache_control":{"type":"ephemeral"}}],
 "messages":[{"role":"user","content":"ok"}]}'&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in &lt;/span&gt;1 2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/v1/messages"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: &lt;/span&gt;&lt;span class="nv"&gt;$ANTHROPIC_API_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;"anthropic-version: 2023-06-01"&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="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REQ&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.usage | {cache_creation_input_tokens, cache_read_input_tokens}'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="c"&gt;# an OpenAI-shaped response has no cache fields at all — that absence is the tell&lt;/span&gt;
&lt;span class="c"&gt;# if the fields are present, what you have learned is that the *schema* is Anthropic's.&lt;/span&gt;
&lt;span class="c"&gt;# You have not learned that caching works. Keep reading.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is a correction I have to make to my own draft. The expected pattern is &lt;code&gt;creation &amp;gt; 0&lt;/code&gt; on the first call and &lt;code&gt;read &amp;gt; 0&lt;/code&gt; on the second. I ran this twice against my gateway's &lt;code&gt;/v1/messages&lt;/code&gt; path (free-tier capture, 2026-09-16) and got &lt;code&gt;creation=0, read=5472&lt;/code&gt; on &lt;strong&gt;pass one&lt;/strong&gt;, and &lt;code&gt;creation=0, read=3355&lt;/code&gt; on pass two. A non-zero read count before any creation count is not prompt caching — it looks like an upstream implicit cache being echoed into Anthropic-shaped field names. Which is a more interesting failure than the easy one: an endpoint can present the correct field names, and even plausible-looking numbers, and still not honour &lt;code&gt;cache_control&lt;/code&gt; at all. The presence of a field tells you about the schema. It tells you nothing about the semantics, and it is exactly the kind of signal an agent has no way to sanity-check at runtime. So the honest version of this test is: &lt;strong&gt;absence of the fields proves a shim; presence proves nothing&lt;/strong&gt; — verify actual caching against your bill or the provider's own docs.&lt;/p&gt;

&lt;p&gt;Read the battery as signal, not as a quiz score. Positive tells are stronger than negative ones: a &lt;code&gt;tool_use&lt;/code&gt; block whose &lt;code&gt;input&lt;/code&gt; is an object, all six stream event names, an Anthropic error envelope — each one is hard to fake and easy to check. Absences and 200s are weaker than they look: an endpoint can emit the right field names, as mine's caching counters and missing thinking signature show. The tests that catch a shim are the ones that look for what's &lt;em&gt;not there&lt;/em&gt; — no &lt;code&gt;event:&lt;/code&gt; lines, no cache fields, a string where an object belongs — because those are the parts a translation layer has to actively rebuild to hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code against a custom endpoint
&lt;/h3&gt;

&lt;p&gt;Claude Code reads the base URL and credential from the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.daoxe.com
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_AUTH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...     &lt;span class="c"&gt;# sent as Authorization: Bearer&lt;/span&gt;
&lt;span class="c"&gt;# or: export ANTHROPIC_API_KEY=sk-...  # sent as x-api-key&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;exact-id-from-the-catalogue&amp;gt;
claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes. The base URL has &lt;strong&gt;no &lt;code&gt;/v1&lt;/code&gt; suffix&lt;/strong&gt; — Claude Code appends &lt;code&gt;/v1/messages&lt;/code&gt; itself, and a doubled &lt;code&gt;/v1/v1/messages&lt;/code&gt; is the most common first-run mistake. And Claude Code uses a second small model for background work; the environment variable that names it has been renamed across releases, so check &lt;code&gt;claude config&lt;/code&gt; in your version rather than copying a variable name from a blog post.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest version
&lt;/h3&gt;

&lt;p&gt;A translation shim is a reasonable engineering decision for text chat, and if that's your workload you will never see any of this. The moment you're running a tool loop, streaming tool arguments to a UI, relying on cached prefixes, or using extended thinking, the protocol stops being an implementation detail — every one of those features lives in a field that has no home on the other side of the translation.&lt;/p&gt;

&lt;p&gt;Run the five tests against whatever you're about to build on. The whole battery is a few minutes of curl, and every one of the degradations above is a runtime bug you'd otherwise discover in production, three months later, with no error message pointing at the protocol.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>We pointed our model-honesty probe at our own gateway. It said SUSPICIOUS.</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:16:29 +0000</pubDate>
      <link>https://dev.to/seven7763/we-pointed-our-model-honesty-probe-at-our-own-gateway-it-said-suspicious-58o5</link>
      <guid>https://dev.to/seven7763/we-pointed-our-model-honesty-probe-at-our-own-gateway-it-said-suspicious-58o5</guid>
      <description>&lt;p&gt;We built a CLI tool that gives you signals about whether an LLM API endpoint is serving the model it claims — or quietly handing you a smaller, quantized, or context-truncated substitute. In early September we pointed it at our own gateway.&lt;/p&gt;

&lt;p&gt;It flagged us. &lt;code&gt;SUSPICIOUS — Hard to explain if the claim were true.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This post is the full record: what the tool said, why it was wrong, how we knew the tool was the wrong one, and what changed. Every detail below is quoted from the raw JSON of those runs, and the fix itself is an open commit in the tool's public repo — so you can check my quoting instead of trusting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=probe_story" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, the gateway that got flagged.&lt;/strong&gt; The tool is provider-neutral and open source — &lt;a href="https://github.com/seven7763/llm-honesty-probe" rel="noopener noreferrer"&gt;github.com/seven7763/llm-honesty-probe&lt;/a&gt; — and this is written as an engineer's post-mortem, not a victory lap. There is no version of this story where we come out looking flawless; the honest one starts with our own tool failing on us.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the probe actually does
&lt;/h3&gt;

&lt;p&gt;One paragraph of background, then we get to the incident. The probe runs a fixed battery of behavioral checks against any OpenAI- or Anthropic-compatible endpoint: a capability floor (easy tasks with answers the tool already knows), long-context recall (it plants the needle, so it knows the right answer), output determinism and backend-identity stability at temperature 0, a tokenizer fingerprint from &lt;code&gt;usage&lt;/code&gt; deltas, and a self-report it deliberately weights near zero because "what model are you?" is trivially spoofed. Your key is read only from an environment variable, the endpoint host is masked on shareable verdict cards, and every signal is explicitly labeled what it is: &lt;strong&gt;heuristics, not proof.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The run
&lt;/h3&gt;

&lt;p&gt;2026-09-04, against one of our own groups — &lt;code&gt;glm-5.3-flash&lt;/code&gt;, the group this working session was itself running on at the time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; llm_honesty_probe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--base-url&lt;/span&gt; https://jp.daoxe.com/v1 &lt;span class="nt"&gt;--claimed-model&lt;/span&gt; glm-5.3-flash &lt;span class="nt"&gt;--card&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;===  SUSPICIOUS  ===
Hard to explain if the claim were true.

[-] Tokenizer fingerprint
      No verdict — add --compare for a definitive check.
[!] Capability floor
      Failed 2/4 easy reasoning tasks a full-tier model rarely misses. (medium)
[!] Long context
      Lost a needle in long context (possible truncation). (medium)
[+] Consistency
      Stable model field / fingerprint across calls.
[-] Self-report (spoofable, low weight)
      Claimed model family unknown; can't compare self-report.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two failed signals, both "possible downgrade", neither deniable-looking. The arithmetic task answered &lt;code&gt;396&lt;/code&gt;. Multistep answered &lt;code&gt;58&lt;/code&gt;. The string-reversal task answered… nothing. The character-counting task: nothing. An empty &lt;code&gt;content&lt;/code&gt; with &lt;code&gt;finish_reason: "length"&lt;/code&gt;. The strict-JSON test — the prompt demands exactly &lt;code&gt;{"a":42,"b":"ok"}&lt;/code&gt; — returned &lt;code&gt;{"a":&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And then we ran it a second time, same version, same endpoint, and the card shifted under us: one floor task tripped instead of two; the strict-JSON test passed this time; the needle at 8,000 characters still failed; and the report's own headline softened to "Only low-confidence flags; likely noise, but worth a second run." The three temperature-0 calls came back identical this time — all three of them the empty string. The same traffic gave the same tool two different stories. This is the part where a marketing team would quietly not publish the card.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three tells
&lt;/h3&gt;

&lt;p&gt;What turned the investigation from "is our gateway dishonest?" into "is our &lt;em&gt;probe&lt;/em&gt; dishonest?" was three details in the raw JSON, none of which the card shows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The server quoted our request back at us.&lt;/strong&gt; The tokenizer probe reported itself inconclusive with this detail string:&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="err"&gt;Can't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fingerprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tokenizer:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="err"&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"max_tokens
must be greater than 2 …"&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;"invalid_request"&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;We had our own code send &lt;code&gt;max_tokens=1&lt;/code&gt; for the fingerprint calls — an old OpenAI-compat habit. Our gateway requires &lt;code&gt;max_tokens &amp;gt; 2&lt;/code&gt;. No server lies like that; that 400 is the sound of &lt;em&gt;our tool&lt;/em&gt; misusing &lt;em&gt;our API contract&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The truncated JSON ended exactly at the budget.&lt;/strong&gt; &lt;code&gt;{"a":&lt;/code&gt; is three tokens of an answer that needs about eight. Three tokens in, because the probe — at that time — capped those completions at 64 tokens (32 on the long-context calls). An answer that stops precisely at the budget with &lt;code&gt;finish_reason: "length"&lt;/code&gt; is the signature of &lt;em&gt;running out of allowed tokens&lt;/em&gt;, not of a weak model. The empty replies in the reasoning tasks had the same shape: &lt;code&gt;content: ""&lt;/code&gt;, &lt;code&gt;finish_reason: "length"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The asymmetry.&lt;/strong&gt; If the backend had really been swapped for a weaker model, why did arithmetic (&lt;code&gt;396&lt;/code&gt;) and multistep (&lt;code&gt;58&lt;/code&gt;) pass while reversal and counting came back empty? A weaker model gets them &lt;em&gt;wrong&lt;/em&gt; — it doesn't get them &lt;em&gt;blank&lt;/em&gt;. "Wrong" is a capability signal. "Empty at exactly the token limit" is a plumbing signal.&lt;/p&gt;

&lt;p&gt;The root cause was in the routing, and it was innocent: this group can serve reasoning-style backends, which spend completion tokens on hidden thinking before any visible answer. The budget went to thinking. The probe read the empty visible reply as "failed a task a full-tier model rarely misses." That is a false positive — a bug in &lt;em&gt;our&lt;/em&gt; caller-side assumptions, filed against &lt;em&gt;our&lt;/em&gt; server. One check in the raw archive settles who was wrong: the second v0.2.0 run — where the budget-starved replies mostly stopped happening — scored the capability floor merely &lt;em&gt;inconclusive&lt;/em&gt;, "could be formatting rather than capability," while the needle call at 8k still failed at the then-32-token needle cap. Same bug, different face.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix, and the re-run
&lt;/h3&gt;

&lt;p&gt;Same day, v0.2.1 (commit &lt;code&gt;cb15e61&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shared guard — &lt;code&gt;budget_starved()&lt;/code&gt; — that recognizes "OK, but empty content with &lt;code&gt;finish_reason: length&lt;/code&gt;" and forces those probes to &lt;strong&gt;inconclusive&lt;/strong&gt;. Budget starvation is a property of the caller's &lt;code&gt;max_tokens&lt;/code&gt;, not of the server's honesty, and it must never be able to produce &lt;code&gt;SUSPICIOUS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Default per-call budgets raised so short factual answers actually fit (our re-run passed &lt;code&gt;--max-tokens 512&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The tokenizer probe now requests 4 tokens instead of 1, clearing the gateway-validation 400.&lt;/li&gt;
&lt;li&gt;The built-in mock server grew a &lt;code&gt;reasoning=True&lt;/code&gt; mode that reproduces the starvation shape, plus regression tests asserting it can never flag suspicion. 20/20 pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Re-run with the fixed tool: needle recall at 2,000 and 8,000 characters — both retrieved. Capability floor: 4/4, exact JSON. Consistency: identical output on all 3 calls at temperature 0. Verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;===  PASS  ===
No downgrade signals in this run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what is &lt;em&gt;not&lt;/em&gt; on that card. The tokenizer fingerprint still says "No verdict — add --compare," because in single-endpoint mode there's no reference to diff against; it now measures the vector cleanly, finds its closest public match, and still refuses to call it — by design, the claimed family has no public reference table entry. Self-report remains inconclusive and low-weight forever, because it's the one signal a dishonest endpoint can pre-write. We did not upgrade the accusation into a certificate. We turned a wrong red into an honest gray.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the wrong flag is the better story
&lt;/h3&gt;

&lt;p&gt;Here's the uncomfortable logic. Any tool can print &lt;code&gt;PASS&lt;/code&gt;; it's one string. A tool that has been &lt;em&gt;publicly wrong about a concrete endpoint, caught, root-caused, fixed in an open commit, and re-run&lt;/em&gt; has shown you something no clean card can: its red flags are fallible, so they get investigated instead of believed, and the investigation itself is checkable. If the only version of this story you'd ever seen were "we ran the probe on ourselves and it passed," you should trust it &lt;em&gt;less&lt;/em&gt; — that's what a rigged scoreboard looks like.&lt;/p&gt;

&lt;p&gt;The false positive also taught us something about who this tool hurts. We built it to catch relays that quietly downgrade you. A &lt;em&gt;miss&lt;/em&gt; leaves a bad provider unpunished today and time-series diffs catch it tomorrow. A &lt;em&gt;false accusation&lt;/em&gt; frames an honest provider with a screenshot someone posts in a forum — and once a community learns that these cards convict the innocent, every flag starts getting ignored, including the true ones. Over-flagging doesn't just hurt the flagged; it degrades the signal for everyone. "Treat a flag as a reason to look closer, not a conviction" is easy to print and much harder to engineer for, because the defaults are asymmetric: the probe weights failure more than success, so the failure modes you feed it decide who gets accused. &lt;code&gt;budget_starved()&lt;/code&gt; is the fix for one such mode. It is not the fix for all of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we are not claiming
&lt;/h3&gt;

&lt;p&gt;The discipline of "signals, not proof" points at us twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;SUSPICIOUS&lt;/code&gt; was wrong. That does not mean the current &lt;code&gt;PASS&lt;/code&gt; is &lt;em&gt;right&lt;/em&gt; in any certificate sense — it means this battery, run once, on one group, showed no downgrade signals at that moment. A single run is a snapshot; silent degradation is a time-series problem, which is what the &lt;code&gt;--json&lt;/code&gt; diff mode in cron is for.&lt;/li&gt;
&lt;li&gt;One group is not the gateway. Our other groups went untested — the test account simply didn't have the balance for more.&lt;/li&gt;
&lt;li&gt;Differential mode is the stronger tool, and we didn't use it: point &lt;code&gt;--compare&lt;/code&gt; at the official endpoint and the tokenizer question can become a real verdict instead of an inconclusive. We'd rather say "not settled" than let our own marketing settle it.&lt;/li&gt;
&lt;li&gt;A provider that mirrored the official tokenizer, matched the capability floor, and held long context would pass this battery. We can't prove intent from the outside — ours or anyone's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: clone it, run it against us, run it against the endpoint you actually pay for, and compare both cards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/seven7763/llm-honesty-probe
&lt;span class="nb"&gt;cd &lt;/span&gt;llm-honesty-probe
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; llm_honesty_probe &lt;span class="nt"&gt;--self-test&lt;/span&gt; &lt;span class="nt"&gt;--card&lt;/span&gt;   &lt;span class="c"&gt;# no key needed — see both verdict shapes first&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Questions, corrections, or "your probe wrongly flagged my honest relay": the public support channel is Telegram &lt;strong&gt;@daoxe_ai&lt;/strong&gt;, and the issue tracker is on the repo. The bug reports we want most are the ones about false positives — the next wrong &lt;code&gt;SUSPICIOUS&lt;/code&gt; is exactly as damaging as the one we just ate.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Five coding agents, one endpoint: where the base URL field actually lives</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:53:30 +0000</pubDate>
      <link>https://dev.to/seven7763/five-coding-agents-one-endpoint-where-the-base-url-field-actually-lives-3cka</link>
      <guid>https://dev.to/seven7763/five-coding-agents-one-endpoint-where-the-base-url-field-actually-lives-3cka</guid>
      <description>&lt;p&gt;Every one of these tools supports a custom OpenAI-compatible endpoint. That's not the interesting part. The interesting part is that the field is in five different places, three of them undocumented in the UI, and each tool fails in a way that doesn't look like a configuration error.&lt;/p&gt;

&lt;p&gt;I run a gateway, so I see the support tickets from all five. &lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch&amp;amp;utm_term=en" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;.&lt;/strong&gt; Nothing below is specific to it — swap in any endpoint that speaks &lt;code&gt;/v1/chat/completions&lt;/code&gt;. Menu paths in desktop tools drift every few releases, so the paths below were re-verified against each vendor's current documentation on 2026-09-09; if a path has moved since, the &lt;em&gt;gotcha&lt;/em&gt; underneath it almost certainly hasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deliberately not covered: GitHub Copilot.&lt;/strong&gt; Its BYOK flow is a different mechanism with different limits and it deserves its own write-up. Everything here is the "custom base URL" family.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ground rule: never type a model ID from memory
&lt;/h3&gt;

&lt;p&gt;Before touching any tool, get the exact strings your key can call:&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;-s&lt;/span&gt; https://api.daoxe.com/v1/models &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;$LLM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'"id"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the failures below trace back to an ID that is &lt;em&gt;almost&lt;/em&gt; right. &lt;code&gt;claude-sonnet-4-5&lt;/code&gt; and &lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt; are not interchangeable, and a tool that gets a 404 from a model list it can't see will usually surface it as "connection failed".&lt;/p&gt;

&lt;p&gt;And those IDs are a moving target in their own right — vendors rename, deprecate, and quietly re-point them. I wrote up that side of the problem — pinning IDs, detecting drift, what to log when the &lt;code&gt;model&lt;/code&gt; field stops matching what you asked for — in &lt;a href="https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0"&gt;Model IDs are a dependency. Pin them like one.&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Settings → Models. Enter your key in the &lt;strong&gt;OpenAI API Key&lt;/strong&gt; box, expand it, enable &lt;strong&gt;Override OpenAI Base URL&lt;/strong&gt;, and set it to your endpoint's &lt;code&gt;/v1&lt;/code&gt; root. Then add your model with &lt;strong&gt;+ Add model&lt;/strong&gt;, typing the exact ID.&lt;/p&gt;

&lt;p&gt;Three gotchas, in the order people hit them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verification calls a model, not the base URL.&lt;/strong&gt; The "Verify" button issues a real completion. If the default OpenAI model names are still enabled in the list and your endpoint doesn't serve those exact IDs, verification fails even though your custom model works perfectly. Add your model first, toggle the stock ones off, then verify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom key does not make every feature custom.&lt;/strong&gt; Chat and inline edit go to your endpoint. Tab completion and the editor's own indexing/retrieval features are Cursor-side services and keep working the way they always did. This surprises people who expected the custom key to cover their entire bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic shape isn't obvious.&lt;/strong&gt; Depending on your privacy settings, requests may still transit Cursor's infrastructure rather than going browser-direct to your endpoint. If that matters to you — corporate egress rules, IP allowlisting on the endpoint side — test it with a throwaway key before you assume.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Symptom of a wrong ID:&lt;/strong&gt; a generic red toast with no upstream error text. Cursor swallows the body.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; the Cline sidebar → settings gear → &lt;strong&gt;API Provider&lt;/strong&gt; → &lt;code&gt;OpenAI Compatible&lt;/code&gt;. Fill &lt;strong&gt;Base URL&lt;/strong&gt; (&lt;code&gt;.../v1&lt;/code&gt;), &lt;strong&gt;API Key&lt;/strong&gt;, &lt;strong&gt;Model&lt;/strong&gt;. Then open the model configuration section underneath, which is where the real work is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fill in the context window yourself.&lt;/strong&gt; For a model Cline doesn't recognise, it can't infer the window, and its context manager will either truncate too early (you lose file content mid-task) or too late (the upstream returns a context-length error halfway through a multi-file edit). This field is the single highest-value thing on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the image-support flag honestly.&lt;/strong&gt; Turning it on for a text-only model produces a 400 the first time you paste a screenshot, several steps into a task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed diffs are a model problem wearing a config costume.&lt;/strong&gt; "The SEARCH block doesn't match" repeated three times in a row usually means the model behind your ID isn't strong enough at exact-match diff editing, not that the endpoint is broken. Try the same ID via &lt;code&gt;curl&lt;/code&gt; with a diff-editing prompt before you blame the wiring.&lt;/li&gt;
&lt;li&gt;Cline shows a per-request cost estimate derived from the price fields you type in. Those are &lt;strong&gt;local display values&lt;/strong&gt;; they don't come from the endpoint and they don't affect billing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nice property:&lt;/strong&gt; Cline surfaces the raw request in an expandable block in the chat — when something's wrong, look there first; it's the best request log of the five. If your build doesn't show it, the proxy trick at the end of this article covers you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Continue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; &lt;code&gt;~/.continue/config.yaml&lt;/code&gt; (older installs use &lt;code&gt;config.json&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.1&lt;/span&gt;
&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deep model&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;exact-id-from-/v1/models&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;apiBase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;chat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;edit&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;apply&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Embeddings&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;exact-embedding-id&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;apiBase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;embed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;provider: openai&lt;/code&gt; plus &lt;code&gt;apiBase&lt;/code&gt; is the generic path.&lt;/strong&gt; You are not claiming the model is from OpenAI; you're telling Continue which wire protocol to speak. Include &lt;code&gt;/v1&lt;/code&gt; in &lt;code&gt;apiBase&lt;/code&gt; — Continue appends &lt;code&gt;/chat/completions&lt;/code&gt; to whatever you give it, and a missing &lt;code&gt;/v1&lt;/code&gt; produces a 404 that looks like an auth failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not give a chat model the &lt;code&gt;autocomplete&lt;/code&gt; role.&lt;/strong&gt; This is the most expensive mistake in this article. Autocomplete fires continuously as you type and expects a fill-in-the-middle model; a chat model will return prose, Continue will show nonsense ghost text, and you'll generate a request every few keystrokes. If you have no FIM-capable model, leave autocomplete unconfigured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;embed&lt;/code&gt; is separate.&lt;/strong&gt; If &lt;code&gt;@codebase&lt;/code&gt; returns irrelevant chunks, check that an embeddings model is actually configured rather than silently falling back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where to look when it breaks:&lt;/strong&gt; enable the &lt;strong&gt;Continue: Enable Console&lt;/strong&gt; setting and run &lt;strong&gt;Focus on Continue Console View&lt;/strong&gt; from the command palette for prompt logs; the core log lives at &lt;code&gt;~/.continue/logs/core.log&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; &lt;code&gt;zed: open settings&lt;/code&gt; (the JSON file) — or, in recent builds, &lt;code&gt;agent: open settings&lt;/code&gt; → &lt;strong&gt;Add Provider&lt;/strong&gt; under LLM Providers, which writes the same block for you. Zed does &lt;strong&gt;not&lt;/strong&gt; discover models — you declare them, either way.&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language_models"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"openai_compatible"&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="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gateway"&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="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"api_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.daoxe.com/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"available_models"&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="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id-from-/v1/models&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"display_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deep model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"max_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"max_output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&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="w"&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="w"&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="w"&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;The API key is not in this file.&lt;/strong&gt; Zed resolves the key from an environment variable named after the provider (&lt;code&gt;DAOXE_API_KEY&lt;/code&gt; for a provider id &lt;code&gt;daoxe&lt;/code&gt;) — set it in your shell or launch environment. A perfectly correct &lt;code&gt;settings.json&lt;/code&gt; with no key set produces "no models available" and no error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_tokens&lt;/code&gt; is the context window and it is mandatory.&lt;/strong&gt; Zed has no catalogue to look it up in. Guess high and you get upstream 400s on long sessions; guess low and Zed refuses to attach files it could have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declaring &lt;code&gt;"tools": true&lt;/code&gt; for a model that doesn't do tool calling&lt;/strong&gt; gives you an agent that opens a thread, thinks, and does nothing. Check tool support against the endpoint first.&lt;/li&gt;
&lt;li&gt;The provider key under &lt;code&gt;language_models&lt;/code&gt; has been renamed at least once across Zed versions. If the block above is rejected, run &lt;code&gt;zed: open default settings&lt;/code&gt; and copy the current key name from there rather than trusting any blog post, including this one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Aider
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; environment variables, CLI flags, or &lt;code&gt;.aider.conf.yml&lt;/code&gt;. All three work; pick one and stick to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.daoxe.com/v1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
aider &lt;span class="nt"&gt;--model&lt;/span&gt; openai/&amp;lt;exact-id&amp;gt; &lt;span class="nt"&gt;--weak-model&lt;/span&gt; openai/&amp;lt;cheap-exact-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, checked into the repo root as &lt;code&gt;.aider.conf.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai/&amp;lt;exact-id&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;weak-model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai/&amp;lt;cheap-exact-id&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;openai-api-base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;
&lt;span class="na"&gt;edit-format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diff&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;openai/&lt;/code&gt; prefix is not decoration.&lt;/strong&gt; Aider routes through LiteLLM, and the prefix is what says "treat this as a generic OpenAI-protocol endpoint". Drop it and LiteLLM tries to pattern-match your ID against its own registry, which can send the request somewhere else entirely or attach parameters your upstream rejects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--weak-model&lt;/code&gt; is the classic delayed failure.&lt;/strong&gt; Aider uses a second, cheaper model for commit messages and chat summarisation. If you don't set it, the default may not exist on your endpoint — and because it's only used &lt;em&gt;after&lt;/em&gt; your first successful edit, everything looks fine right up until the commit step. Set it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown models trigger metadata warnings.&lt;/strong&gt; Silence them properly with &lt;code&gt;.aider.model.metadata.json&lt;/code&gt; in the repo root:
&lt;/li&gt;
&lt;/ul&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openai/&amp;lt;exact-id&amp;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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_cost_per_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_cost_per_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"litellm_provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat"&lt;/span&gt;&lt;span class="w"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaving the cost fields at &lt;code&gt;0&lt;/code&gt; disables aider's local spend estimate, which is better than a wrong estimate. Check your real usage on the provider's side.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If streaming misbehaves through a corporate proxy, &lt;code&gt;--no-stream&lt;/code&gt; isolates it in one flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Symptom → cause
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Where it usually comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Connection failed" with no detail&lt;/td&gt;
&lt;td&gt;Wrong model ID, or &lt;code&gt;apiBase&lt;/code&gt; missing &lt;code&gt;/v1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works, then dies at commit / summary time&lt;/td&gt;
&lt;td&gt;Aider &lt;code&gt;--weak-model&lt;/code&gt; not set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost text is prose, token use spikes&lt;/td&gt;
&lt;td&gt;Chat model given Continue's &lt;code&gt;autocomplete&lt;/code&gt; role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent opens a thread and stalls&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"tools": true&lt;/code&gt; declared for a non-tool model (Zed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context errors mid-task, only on big files&lt;/td&gt;
&lt;td&gt;Context window not set (Cline) or &lt;code&gt;max_tokens&lt;/code&gt; wrong (Zed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"SEARCH block doesn't match", repeatedly&lt;/td&gt;
&lt;td&gt;Model capability, not configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify button fails, manual calls work&lt;/td&gt;
&lt;td&gt;Cursor validating a stock model ID your endpoint doesn't serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No models listed, config looks perfect&lt;/td&gt;
&lt;td&gt;Zed key not entered in the panel UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When the tool won't tell you what it sent
&lt;/h3&gt;

&lt;p&gt;Three of these five hide the request body. The fastest way out is to put something in the middle: run a local HTTP proxy, point the tool's base URL at &lt;code&gt;http://127.0.0.1:8080/v1&lt;/code&gt;, and forward to the real endpoint. Ten minutes of setup answers questions that hours of toggling settings won't — particularly "is it even sending the model ID I typed" and "which extra parameters is this tool adding".&lt;/p&gt;

&lt;h3&gt;
  
  
  The part that isn't configuration
&lt;/h3&gt;

&lt;p&gt;All five of these will connect in under five minutes once you know where the field is. What actually determines whether the setup is good is whether the model behind that ID does diff editing well, holds a long tool-use loop without drifting, and returns the context window it claims. That's an evaluation problem, and it's the one worth your afternoon — the config is a ten-line diff.&lt;/p&gt;

&lt;p&gt;One practical consequence of doing it this way: the model becomes a config string in five files instead of five separate integrations, so trying a different one next month is an edit, not a migration. That's the whole reason to route through a single endpoint, and it's a much more honest reason than any benchmark table.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of an ongoing "verify your endpoint" series. Once you've pointed a tool at an endpoint, the next question is whether it actually honours the OpenAI wire format — streaming shape, tool calls, error bodies — which is what &lt;a href="https://dev.to/seven7763/openai-compatible-is-a-spectrum-not-a-boolean-heres-an-11-check-conformance-suite-2mhj"&gt;"OpenAI-compatible" is a spectrum, not a boolean&lt;/a&gt; tests with an eleven-check script. And because every setup above hinges on an exact model ID, &lt;a href="https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0"&gt;Model IDs are a dependency. Pin them like one.&lt;/a&gt; covers the catalogue side: pinning, drift detection, and what to log when the &lt;code&gt;model&lt;/code&gt; field stops matching.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vscode</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>"OpenAI-compatible" is a spectrum, not a boolean — here's an 11-check conformance suite</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:46:31 +0000</pubDate>
      <link>https://dev.to/seven7763/openai-compatible-is-a-spectrum-not-a-boolean-heres-an-11-check-conformance-suite-2mhj</link>
      <guid>https://dev.to/seven7763/openai-compatible-is-a-spectrum-not-a-boolean-heres-an-11-check-conformance-suite-2mhj</guid>
      <description>&lt;p&gt;Two endpoints both say "OpenAI-compatible" on the tin. You point your app at the first one and everything works. You point it at the second one and everything works too — for about a week. Then a streamed tool call comes back with &lt;code&gt;arguments&lt;/code&gt; split across chunks in a way your accumulator didn't expect, your JSON parser throws inside a retry loop, and the retry loop hammers the endpoint because the error body doesn't have the field your backoff code reads.&lt;/p&gt;

&lt;p&gt;Nothing lied to you. "OpenAI-compatible" was never a boolean. It's a surface area, and every implementation covers a different subset of it.&lt;/p&gt;

&lt;p&gt;I maintain a gateway, so I read a lot of compatibility bug reports. &lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch&amp;amp;utm_term=en" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a multi-model gateway that speaks the OpenAI protocol among others.&lt;/strong&gt; The checklist below is deliberately written so you can run it against us and against anyone else, and the script at the end doesn't know or care which endpoint you point it at. If it makes us look bad on a check, that's the correct output.&lt;/p&gt;

&lt;h3&gt;
  
  
  The nine surfaces
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Streaming deltas.&lt;/strong&gt; The reference behaviour is specific: the first &lt;code&gt;chat.completion.chunk&lt;/code&gt; carries &lt;code&gt;choices[0].delta.role = "assistant"&lt;/code&gt; and usually empty content, middle chunks carry &lt;code&gt;delta.content&lt;/code&gt; fragments, the last content-bearing chunk carries &lt;code&gt;finish_reason&lt;/code&gt;, and the stream terminates with a literal &lt;code&gt;data: [DONE]&lt;/code&gt; line. Implementations diverge on all four. Some never send the role chunk, which breaks clients that use it to open a message. Some put &lt;code&gt;finish_reason&lt;/code&gt; on a trailing chunk with an empty &lt;code&gt;delta&lt;/code&gt;. Some omit &lt;code&gt;[DONE]&lt;/code&gt; entirely and just close the connection, which is fine for a client that reads to EOF and fatal for one that blocks waiting for the sentinel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tool / function calling.&lt;/strong&gt; Two traps here. First, &lt;code&gt;function.arguments&lt;/code&gt; is a &lt;strong&gt;JSON-encoded string&lt;/strong&gt;, not an object — endpoints that "helpfully" return a parsed object break every client that calls &lt;code&gt;json.loads&lt;/code&gt; on it. Second, in streaming mode, tool calls arrive as fragments that you reassemble by the &lt;code&gt;index&lt;/code&gt; field, with &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;function.name&lt;/code&gt; typically only present on the first fragment. An endpoint that re-sends &lt;code&gt;id&lt;/code&gt; on every fragment, or that omits &lt;code&gt;index&lt;/code&gt; when there's only one call, will work with your naive accumulator and fail the moment a model emits two parallel calls. Also check that &lt;code&gt;finish_reason&lt;/code&gt; is &lt;code&gt;tool_calls&lt;/code&gt; and not &lt;code&gt;stop&lt;/code&gt; — agent loops branch on that value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;response_format&lt;/code&gt;.&lt;/strong&gt; Three tiers, and they're commonly conflated: no support, &lt;code&gt;{"type": "json_object"}&lt;/code&gt; (valid JSON, any shape), and &lt;code&gt;{"type": "json_schema", "json_schema": {..., "strict": true}}&lt;/code&gt; (constrained decoding against your schema). The dangerous middle case is an endpoint that &lt;strong&gt;accepts the parameter and ignores it&lt;/strong&gt;. You get prose with a code fence around it, your parser fails one request in fifty, and it looks like a model quality problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;logprobs&lt;/code&gt; / &lt;code&gt;top_logprobs&lt;/code&gt;.&lt;/strong&gt; Usually the first thing a proxy layer drops, because almost nobody notices. If you do classification by comparing token probabilities, or you use logprobs for confidence gating, this is load-bearing and you should test it explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;code&gt;temperature&lt;/code&gt; and sampling params.&lt;/strong&gt; Reasoning-style models reject &lt;code&gt;temperature&lt;/code&gt; outright on some upstreams, accept-and-ignore it on others, and honour it on a third set. All three are defensible; not knowing which one you're on is not. The same applies to &lt;code&gt;top_p&lt;/code&gt;, &lt;code&gt;presence_penalty&lt;/code&gt;, and &lt;code&gt;max_tokens&lt;/code&gt; vs &lt;code&gt;max_completion_tokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;stop&lt;/code&gt; sequences.&lt;/strong&gt; Does the endpoint honour an array of stop strings? Is the stop sequence included in or excluded from the returned content (the reference excludes it)? Does &lt;code&gt;finish_reason&lt;/code&gt; come back as &lt;code&gt;"stop"&lt;/code&gt;? A surprising number of shims implement &lt;code&gt;stop&lt;/code&gt; by post-truncating the full completion, which means you pay for tokens you never see — and the usage numbers will show it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Usage accounting.&lt;/strong&gt; Non-streaming responses should carry &lt;code&gt;usage.prompt_tokens&lt;/code&gt;, &lt;code&gt;completion_tokens&lt;/code&gt;, &lt;code&gt;total_tokens&lt;/code&gt;. Streaming responses only include usage if you pass &lt;code&gt;stream_options: {"include_usage": true}&lt;/code&gt;, and then it arrives in a final chunk with an &lt;strong&gt;empty&lt;/strong&gt; &lt;code&gt;choices&lt;/code&gt; array — a shape that crashes clients which assume &lt;code&gt;choices[0]&lt;/code&gt; always exists. If you do cost attribution per request, also check whether cached-prompt and reasoning-token breakdowns survive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Error-body shape.&lt;/strong&gt; Every retry layer you've ever written parses this, and nobody tests it. The reference is &lt;code&gt;{"error": {"message": ..., "type": ..., "param": ..., "code": ...}}&lt;/code&gt; with an HTTP status that matches the semantics. Real-world variations: a 200 with an error object in the body (fatal for retry logic — you'll cheerfully return an error string to your user), an HTML error page from an intermediate proxy, or a 500 where a 400 belonged, which turns a permanent client error into an infinite retry storm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. &lt;code&gt;/v1/models&lt;/code&gt; fidelity.&lt;/strong&gt; Does it exist, does it return &lt;code&gt;{"object": "list", "data": [{"id": ...}]}&lt;/code&gt;, and — the part that matters — does it list the IDs your key can actually call? A catalogue endpoint that returns everything the vendor sells, rather than everything your key is entitled to, is worse than no catalogue at all, because you'll build CI checks on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The script
&lt;/h3&gt;

&lt;p&gt;Stdlib only, Python 3.8+, no install. It runs eleven checks and prints a verdict table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;compat_probe.py — how OpenAI-compatible is this endpoint, really?

    export LLM_BASE_URL=https://api.example.com/v1
    export LLM_API_KEY=sk-...
    python3 compat_probe.py --model &amp;lt;exact-model-id&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;LLM_BASE_URL&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="nf"&gt;rstrip&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="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;LLM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&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;_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&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="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;90&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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;encode&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="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&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;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KEY&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;data&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# still a readable file object
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&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="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;replace&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getcode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&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="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saw_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;False&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;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;line&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="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;replace&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;body&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;body&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="n"&gt;saw_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;break&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;chunks&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="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;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saw_done&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&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="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;OUT&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PARTIAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;
    &lt;span class="n"&gt;ask&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;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Say hi.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# 1 — catalogue
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models_endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&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;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids, target listed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ids&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="c1"&gt;# 2 — basic completion + model echo + usage
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&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="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="ow"&gt;or&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;usage&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="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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;basic_chat&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;msg&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_echo&lt;/span&gt;&lt;span class="sh"&gt;"&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&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;body&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;model&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;model&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;asked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;body&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;model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n/a&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&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_fields&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;usage&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;usage&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;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&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;usage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# 3 — streaming shape
&lt;/span&gt;    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&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="ow"&gt;or&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;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;role&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&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="ow"&gt;or&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;finish_reason&lt;/span&gt;&lt;span class="sh"&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_shape&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;chunks&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;role&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;fin&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;done&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks, role_chunk=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, finish_reason=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, [DONE]=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;done&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="c1"&gt;# 4 — usage on stream
&lt;/span&gt;    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream_options&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;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_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;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final usage chunk present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;absent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 5 — tool calling
&lt;/span&gt;    &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&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;function&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;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="s"&gt;get_weather&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;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&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;properties&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;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&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;required&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;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}}}&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weather in Osaka? Use the tool.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&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;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;=&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="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="ow"&gt;or&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;choice&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;message&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="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="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;args_is_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calls&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;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="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="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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record&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="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calls&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;args_is_str&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;choice&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;finish_reason&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="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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n=&lt;/span&gt;&lt;span class="si"&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;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; arguments_is_string=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args_is_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; finish=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;choice&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;finish_reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# 6 — json mode
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: true} as JSON.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&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;POST&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="o"&gt;=&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="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="ow"&gt;or&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;message&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="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;content&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="nf"&gt;isinstance&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;parsed&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;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, parses=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parsed&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="c1"&gt;# 7 — strict schema
&lt;/span&gt;    &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema&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;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="s"&gt;r&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;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&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;properties&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;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean&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;required&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;ok&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;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_schema_strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&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;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="c1"&gt;# 8 — logprobs
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logprobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_logprobs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lp&lt;/span&gt; &lt;span class="o"&gt;=&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="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="ow"&gt;or&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;logprobs&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="nf"&gt;isinstance&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logprobs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;lp&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;lp&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="c1"&gt;# 9 — temperature: accepted / rejected / ignored is three different worlds
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected with 400 (reasoning model?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="k"&gt;else&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;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&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="c1"&gt;# 10 — stop sequences
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Count: one two three four five&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;stop&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;three&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&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="o"&gt;=&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="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="ow"&gt;or&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;message&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="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;content&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="nf"&gt;isinstance&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&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="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop_sequences&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;three&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;text&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;stop string leaked into content: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;three&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;text&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="c1"&gt;# 11 — error shape on a model that cannot exist
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;definitely-not-a-model-xyz&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;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;shaped&lt;/span&gt; &lt;span class="o"&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&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;body&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_shape&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;shaped&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;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, error object: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shaped&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="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&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;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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;OUT&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&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;verdict&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;7&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;detail&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="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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;PARTIAL is not a failure — it is a behaviour you now have to design around.&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="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on running it. It costs a handful of tiny completions, so run it against a cheap model first to confirm your base URL and key are right. And run it against &lt;strong&gt;the exact model ID you plan to ship&lt;/strong&gt;, because conformance varies per model on the same endpoint — reasoning models in particular reject parameters that their non-reasoning siblings accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the output
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FAIL&lt;/code&gt; on &lt;code&gt;basic_chat&lt;/code&gt; or &lt;code&gt;error_shape&lt;/code&gt; is a real problem. &lt;code&gt;FAIL&lt;/code&gt; on &lt;code&gt;logprobs&lt;/code&gt; is only a problem if you use logprobs. The point of the table isn't a score; it's that you now have a written record of which surfaces you're allowed to depend on, which you can re-run after any provider change and diff.&lt;/p&gt;

&lt;p&gt;Three habits that follow from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check it into CI.&lt;/strong&gt; One nightly run, output committed as an artifact. Compatibility regressions are silent by nature — nobody sends a changelog entry saying "we stopped forwarding &lt;code&gt;logprobs&lt;/code&gt;".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert the model echo in production&lt;/strong&gt;, not just in the probe. If you asked for one ID and the response's &lt;code&gt;model&lt;/code&gt; field says another, you want that in your logs the day it starts happening, not the week you notice quality dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat &lt;code&gt;PARTIAL&lt;/code&gt; as a design input.&lt;/strong&gt; If &lt;code&gt;stream_usage&lt;/code&gt; is absent, your cost attribution needs a non-streaming path or a local tokenizer estimate. That's a two-hour decision now or a month-end reconciliation mystery later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What this can't tell you
&lt;/h3&gt;

&lt;p&gt;The probe tests the &lt;strong&gt;protocol&lt;/strong&gt;, not the model behind it. An endpoint can pass all eleven checks and still be routing you to a smaller model, a quantized build, or a truncated context window — the wire format would be perfect either way. That's a different investigation with different tools, and the honest framing there is that behavioural probes give you &lt;em&gt;signals, not proof&lt;/em&gt;: nobody outside the provider can see which weights served your request. I wrote up the catalogue side of that problem separately — pinning model IDs, detecting drift, and what to log when the &lt;code&gt;model&lt;/code&gt; field stops matching what you asked for — in &lt;a href="https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0"&gt;Model IDs are a dependency. Pin them like one.&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But protocol conformance is the part you can settle in an afternoon, with a script that fits on one screen, and it's the part that will wake you up at 3am if you skip it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of an ongoing "verify your endpoint" series. The companion piece on model-ID drift is &lt;a href="https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0"&gt;Model IDs are a dependency. Pin them like one.&lt;/a&gt;, and if you're evaluating a third-party endpoint before your first production call, &lt;a href="https://dev.to/seven7763/ten-questions-to-answer-before-you-route-production-traffic-through-someone-elses-llm-endpoint-oml"&gt;Ten questions to answer before you route production traffic through someone else's LLM endpoint&lt;/a&gt; is the buyer-side checklist.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Ten questions to answer before you route production traffic through someone else's LLM endpoint</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Mon, 07 Sep 2026 19:36:11 +0000</pubDate>
      <link>https://dev.to/seven7763/ten-questions-to-answer-before-you-route-production-traffic-through-someone-elses-llm-endpoint-oml</link>
      <guid>https://dev.to/seven7763/ten-questions-to-answer-before-you-route-production-traffic-through-someone-elses-llm-endpoint-oml</guid>
      <description>&lt;p&gt;Every LLM endpoint's landing page says fast, reliable, compatible, secure. Those words are free. This is the list of questions that aren't free — the ones where the answer takes a vendor real work to be able to give, and where you can verify the answer yourself in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure up front: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch&amp;amp;utm_term=en" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a multi-model gateway.&lt;/strong&gt; At the end I run this checklist against it, and two rows don't pass. That's the point of publishing it — a checklist that flatters whoever wrote it isn't a checklist, it's a spec sheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Protocol coverage — native or translated?
&lt;/h3&gt;

&lt;p&gt;Ask which wire protocols the endpoint implements and which ones it &lt;em&gt;translates into&lt;/em&gt;. These are different products. A translation layer is fine for text chat and lossy for agents: tool-call block ordering, streamed tool arguments, prompt-cache markers and stop-reason fidelity all live in fields that have no equivalent on the other side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; for the Anthropic protocol, omit the required &lt;code&gt;max_tokens&lt;/code&gt; from a &lt;code&gt;/v1/messages&lt;/code&gt; call. A native implementation returns 400; a translation layer often fills in a default and returns 200. For the OpenAI protocol, check whether streamed tool calls arrive as incremental fragments with an &lt;code&gt;index&lt;/code&gt;, or all at once at the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad answer:&lt;/strong&gt; "we're fully compatible with everything."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model ID transparency
&lt;/h3&gt;

&lt;p&gt;Does &lt;code&gt;GET /v1/models&lt;/code&gt; return the exact IDs &lt;em&gt;your key&lt;/em&gt; can call — not the vendor's full catalogue? Does the response echo a &lt;code&gt;model&lt;/code&gt; field, and does it match what you asked for? And critically: what happens when you send an ID that doesn't exist?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; send a deliberately mangled model ID. If you get a completion instead of an error, the endpoint is picking on your behalf, and every ID you send is advisory rather than binding. That single fact changes how much of the rest of this list you can trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad answer:&lt;/strong&gt; a model list on a marketing page, with no catalogue endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Provenance — where does the capacity come from?
&lt;/h3&gt;

&lt;p&gt;Any aggregator is reselling access it obtained somehow. The question isn't whether there's a supply chain; it's whether they'll tell you what's in it. Some capacity is direct vendor access, some is enterprise-cloud resale, some is reverse-engineered consumer endpoints with materially different reliability. All three can be legitimate purchases — at very different risk levels, for very different workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; ask for the labelling scheme, in writing, and check whether it appears in the product itself or only in a support conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad answer:&lt;/strong&gt; silence, or "we don't disclose our infrastructure." Undisclosed provenance means you cannot reason about your own availability.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Can you get your own data out?
&lt;/h3&gt;

&lt;p&gt;Usage per key, per model, per day. Ideally an export rather than a dashboard screenshot. You need this for two things you'll definitely do: reconciling a bill you don't understand, and attributing spend to a team or a feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; try to export a month. If it's a dashboard-only number, plan on logging every request yourself from day one — you can't retrofit history.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Incident behaviour
&lt;/h3&gt;

&lt;p&gt;How do you find out something is degraded? Three tiers, in descending order of comfort: a published availability page with history, an announcement channel, or nothing.&lt;/p&gt;

&lt;p&gt;Here's the thing worth internalising: &lt;strong&gt;an availability figure on a marketing page is a claim, not a measurement.&lt;/strong&gt; It's not necessarily false, but it isn't evidence, and you shouldn't put it in a design document. Whatever tier your vendor is in, run your own probe — a one-token completion against each model you depend on, every five minutes, appended to a JSONL file. After a month you own a success rate and a latency distribution measured from your own network, which is a strictly better artifact than any number on any vendor page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; your own probe log. Nothing else counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Key handling
&lt;/h3&gt;

&lt;p&gt;Six sub-questions: can you issue multiple keys, scope them (per model, per group, per spend cap), rotate without downtime, revoke instantly, see last-used time, and does revocation actually take effect immediately?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; issue a second key, use it once, revoke it, and immediately retry. If it still works sixty seconds later, you've learned something important about the caching layer between you and their auth.&lt;/p&gt;

&lt;p&gt;Then audit where your keys physically live. On a normal developer laptop a gateway key ends up in an editor's settings store, a shell profile, two dotfiles, a CI secret, and at least one Docker environment. Write that list down before you distribute a key to a team, not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Data handling
&lt;/h3&gt;

&lt;p&gt;Retention period for prompts and completions, whether your traffic can be used for training, whether that's per-account configurable, and what the sub-processor situation is for the upstream vendors behind the aggregator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get this in writing.&lt;/strong&gt; I'm deliberately not paraphrasing anyone's policy — including my own employer's — in a blog post you might then rely on. Ask, get a written answer, keep it, and re-ask on renewal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad answer:&lt;/strong&gt; a verbal assurance in a chat window.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Rate limits and behaviour under load
&lt;/h3&gt;

&lt;p&gt;Are limits documented per key or per model? What does the endpoint do when you exceed them — a clean 429 with &lt;code&gt;Retry-After&lt;/code&gt;, a 500, or a hang? Do errors have a stable machine-readable shape your retry layer can classify?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt; fire a small burst of concurrent requests at a cheap model and read the failures. A 429 carrying &lt;code&gt;Retry-After&lt;/code&gt; is a mature implementation. A 500 where a 429 belonged will turn your backoff into a retry storm, because your code will classify it as transient-and-worth-hammering.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Billing you can reconcile
&lt;/h3&gt;

&lt;p&gt;You want three things: a mechanism you can state in one sentence, a live price reference rather than a number in a PDF, and per-request usage data you can multiply out yourself.&lt;/p&gt;

&lt;p&gt;Be actively suspicious of "cheapest". Price is the easiest thing on this list to change and the easiest to advertise; it tells you nothing about the nine criteria above it, and a number well below everyone else's floor is usually a statement about provenance (criterion 3), not about efficiency.&lt;/p&gt;

&lt;p&gt;For the record, since I'm asking you to run this against my employer: DaoXE tops up at a &lt;strong&gt;flat 1:1 top-up rate across Alipay, WeChat Pay, USDT, bank card (Visa·Mastercard), Apple Pay and Google Pay&lt;/strong&gt; — one unit of local currency buys one dollar of credit — models bill at their USD list prices, and the live numbers are at &lt;code&gt;https://daoxe.com/pricing&lt;/code&gt; — check them there rather than trusting any figure in an article, including this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. The exit plan
&lt;/h3&gt;

&lt;p&gt;The last question is the one that makes the other nine survivable: &lt;strong&gt;how many lines of code does it take to leave?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "change a base URL and a key in one config file", the switching cost is near zero and every other risk on this list becomes a manageable one. If the answer involves a proprietary SDK, vendor-specific request extensions, or a routing DSL you can't reproduce elsewhere, you've bought a migration.&lt;/p&gt;

&lt;p&gt;Four habits that keep the number low: keep the base URL and model IDs in config, not in code; prefer standard protocol fields over vendor extensions; keep your own request/response logs; and maintain a golden eval set of twenty to fifty real requests so that "is the replacement good enough" is a script you run rather than an argument you have.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scorecard, run against my own employer
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;DaoXE&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Protocol coverage&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;OpenAI (&lt;code&gt;/v1/chat/completions&lt;/code&gt;, &lt;code&gt;/v1/responses&lt;/code&gt;, &lt;code&gt;/v1/embeddings&lt;/code&gt;, &lt;code&gt;/v1/images/generations&lt;/code&gt;, &lt;code&gt;/v1/videos&lt;/code&gt;), Anthropic Messages (&lt;code&gt;/v1/messages&lt;/code&gt;), Gemini (&lt;code&gt;/v1beta/models/{model}:generateContent&lt;/code&gt;), plus &lt;code&gt;/v1/rerank&lt;/code&gt; — implemented, not translated. Verify with the &lt;code&gt;max_tokens&lt;/code&gt; test above.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Model ID transparency&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /v1/models&lt;/code&gt; returns the exact IDs your key can call. Catalogue size drifts constantly — hundreds of models across roughly 25 vendors — so treat any published count, including ours, as stale.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Provenance&lt;/td&gt;
&lt;td&gt;Pass, with a caveat you should read&lt;/td&gt;
&lt;td&gt;Channels are labelled in the catalogue: full-power, official relay, reverse-engineered ("quality not guaranteed"), promotional. The reverse-engineered tier genuinely exists. Labelling it is the honest part; choosing it for production is your call, and I'd say no.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Usage / log export&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ask&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Don't take my word for the current state — ask on Telegram &lt;code&gt;@daoxe_ai&lt;/code&gt; and log your own requests regardless.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Incident behaviour&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Does not pass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;There is no public status page, so run your own cron probe. The availability figure on the marketing page is a company claim, not a measurement, and I won't repeat it here as if it were one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Key handling&lt;/td&gt;
&lt;td&gt;Test it yourself&lt;/td&gt;
&lt;td&gt;Run the issue → use → revoke → retry sequence on your own account. Note that sign-up is open, with password login, no email verification, and OAuth via GitHub, Telegram or Passkey — convenient, and worth factoring into your own account-security posture.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Data handling&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ask, in writing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same rule I'd apply to any vendor. Ask &lt;code&gt;@daoxe_ai&lt;/code&gt;, keep the answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Rate limits under load&lt;/td&gt;
&lt;td&gt;Test it yourself&lt;/td&gt;
&lt;td&gt;Burst a cheap model, read the failure bodies, check for &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Billing&lt;/td&gt;
&lt;td&gt;Pass on mechanism&lt;/td&gt;
&lt;td&gt;One-sentence mechanism, live prices on the pricing page, per-request usage in the response body.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Exit plan&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Standard protocols, model IDs are strings, base URL is one config line. Leaving is the same edit as arriving — which is the only version of this claim worth making.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One more thing that belongs on any honest vendor row: DaoXE is &lt;strong&gt;not available in mainland China&lt;/strong&gt;. That's the official line and it's a hard constraint, not a caveat to work around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using this without turning it into a ritual
&lt;/h3&gt;

&lt;p&gt;Ten criteria is too many for a side project and about right for anything with users. If you only have an hour, do these four: the mangled-model-ID test (criterion 2), the provenance question (3), start the probe (5), and count the lines it would take to leave (10). Those four catch the failures that are expensive to discover later; the rest catch the failures that are merely annoying.&lt;/p&gt;

&lt;p&gt;And apply it symmetrically. If a vendor's answer to criterion 3 or 7 is a shrug, that's data — but so is a checklist published by a vendor that somehow scores ten out of ten on its own list.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>architecture</category>
      <category>career</category>
    </item>
    <item>
      <title>Model IDs are a dependency. Pin them like one.</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Mon, 07 Sep 2026 15:12:43 +0000</pubDate>
      <link>https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0</link>
      <guid>https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-db0</guid>
      <description>&lt;p&gt;You typed &lt;code&gt;claude-sonnet-4-5&lt;/code&gt; from memory, it returned a completion, and you shipped. Six weeks later the outputs are subtly different, or a deploy fails with a 404 on a Tuesday morning, and the git blame points at a string literal in a file nobody has opened since.&lt;/p&gt;

&lt;p&gt;Model IDs are a runtime dependency on someone else's release schedule, and almost nobody treats them like one. We pin &lt;code&gt;requests==2.32.3&lt;/code&gt; and then interpolate a model name from a config file we last checked in March.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch&amp;amp;utm_term=en" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a gateway that fronts hundreds of models from roughly 25 vendors&lt;/strong&gt; — which means I get to watch upstream renames arrive from several directions at once. Everything below works against any endpoint that implements &lt;code&gt;GET /v1/models&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three ways a wrong ID fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mode A — a clean error.&lt;/strong&gt; &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;400&lt;/code&gt; with &lt;code&gt;"code": "model_not_found"&lt;/code&gt;. This is the &lt;em&gt;good&lt;/em&gt; outcome. You find out in development, in the first second, and you fix a typo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode B — alias resolution.&lt;/strong&gt; You ask for &lt;code&gt;gpt-4o&lt;/code&gt;, the provider resolves it to a dated build, and the response's &lt;code&gt;model&lt;/code&gt; field says which one. Fine and normal — as long as you read the echo. If you never look, you've silently accepted whatever the alias points at this month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode C — nearest-match routing.&lt;/strong&gt; Some endpoints and proxies try to be helpful: they don't recognise your ID, so they route to the closest thing they do recognise. You asked for a flagship and got something else, with a 200 and no indication anywhere in the response that a substitution happened. This is the mode that costs real money and real trust, and it's invisible in exactly the way that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ten-second test for Mode C
&lt;/h3&gt;

&lt;p&gt;Send an ID that cannot possibly exist, formed by mangling a real one:&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;-s&lt;/span&gt; https://api.daoxe.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;$LLM_API_KEY&lt;/span&gt;&lt;span class="s2"&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":"claude-sonnet-4-5-TYPO-NOT-A-MODEL",
       "max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; d=json.load(sys.stdin); \
print("ERROR:", d["error"]["message"][:80]) if "error" in d else print("SERVED BY:", d.get("model"))'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get an error, every model ID you send is authoritative. If you get a completion, &lt;strong&gt;every model ID you send is advisory&lt;/strong&gt; — the endpoint reserves the right to pick for you, and no amount of pinning on your side will help until you know the rule it uses. Run this before you build anything else in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dated versus floating
&lt;/h3&gt;

&lt;p&gt;Two ID styles, two different risks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Floating (&lt;code&gt;claude-sonnet-4-5&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Dated (&lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour&lt;/td&gt;
&lt;td&gt;changes under you, no code change&lt;/td&gt;
&lt;td&gt;stable while it exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifespan&lt;/td&gt;
&lt;td&gt;long&lt;/td&gt;
&lt;td&gt;retired on a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fails as&lt;/td&gt;
&lt;td&gt;silent quality drift&lt;/td&gt;
&lt;td&gt;a loud 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use for&lt;/td&gt;
&lt;td&gt;exploration, prototypes&lt;/td&gt;
&lt;td&gt;anything with a recorded eval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The policy that follows: &lt;strong&gt;exploration floats, production is dated, and every eval artifact records the ID that actually served it&lt;/strong&gt; — read from the response's &lt;code&gt;model&lt;/code&gt; field, not from what you asked for. That last part is what lets you answer "was this eval run on the same build as that one" three months later.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /v1/models&lt;/code&gt; is the source of truth, and it's per-key
&lt;/h3&gt;

&lt;p&gt;Not the docs, not the pricing page, not a blog post. The catalogue endpoint returns the IDs &lt;em&gt;your key&lt;/em&gt; is entitled to call, which is frequently a different set from what the vendor sells:&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;-s&lt;/span&gt; https://api.daoxe.com/v1/models &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;$LLM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; [print(m["id"]) for m in json.load(sys.stdin)["data"]]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat worth knowing up front: most implementations return IDs and little else. Context windows, tool support, vision support and modality are usually &lt;strong&gt;not&lt;/strong&gt; in there, so you still need a small capability sidecar of your own. Don't infer capabilities from the name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock the catalogue
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;check_models.py — fail CI when a model ID this repo depends on disappears.

    python3 check_models.py --write      # re-pin (review the diff!)
    python3 check_models.py              # verify
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="sh"&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;catalogue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&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;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;m&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The IDs this repo actually uses — derived from the alias map, never hand-listed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.lock.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;catalogue&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalogue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinned &lt;/span&gt;&lt;span class="si"&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;live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&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="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalogue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;live_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;required_ids&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;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live_set&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note   new in catalogue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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;for&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;live_set&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warn   left the catalogue (unused here): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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="n"&gt;gone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;live_set&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;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR  required by models.json but not in the catalogue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gone&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it nightly and on every pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;model-catalogue&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 check_models.py&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;
          &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two failure signals mean different things. A &lt;strong&gt;nightly&lt;/strong&gt; failure means an upstream retired or renamed something you depend on, and you found out before your users did. A &lt;strong&gt;pull-request&lt;/strong&gt; failure means someone added a role pointing at an ID that isn't callable — a typo caught at review time instead of at 2am.&lt;/p&gt;

&lt;h3&gt;
  
  
  The alias layer
&lt;/h3&gt;

&lt;p&gt;Nothing in your application code should contain a model name. Code refers to &lt;em&gt;roles&lt;/em&gt;; one file maps roles to exact IDs.&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pinned_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bulk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"embed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-embedding-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dated build; re-pin only with an eval run linked in the PR"&lt;/span&gt;&lt;span class="w"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="nd"&gt;@functools.lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&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;def&lt;/span&gt; &lt;span class="nf"&gt;_roles&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&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;model_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&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="nf"&gt;_roles&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&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;KeyError&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;no model pinned for role &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; — add it to models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things fall out of this for free. A rename becomes a &lt;strong&gt;one-line diff in a reviewable file&lt;/strong&gt; instead of a search-and-replace across forty call sites. &lt;code&gt;check_models.py&lt;/code&gt; can derive its required set from the same file, so the lock and the code can't disagree. And &lt;code&gt;git log models.json&lt;/code&gt; becomes a readable history of what your product was running and when — which is the artifact you'll want the first time someone asks why output quality changed in July.&lt;/p&gt;

&lt;p&gt;Then stop the IDs from leaking back in. One test, one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# tests/no_hardcoded_model_ids.sh — fails if a model-looking literal appears in src/&lt;/span&gt;
&lt;span class="o"&gt;!&lt;/span&gt; rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!models.json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'"(gpt|claude|gemini|deepseek|qwen|kimi|grok|llama)-[a-z0-9._-]+"'&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When an upstream retires a model mid-project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find out from CI, not from users.&lt;/strong&gt; That's what the nightly is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the label before you pick a successor.&lt;/strong&gt; Vendors publish migration notes; gateways publish catalogue metadata. Don't infer the replacement from the name — &lt;code&gt;-mini&lt;/code&gt;, &lt;code&gt;-fast&lt;/code&gt;, &lt;code&gt;-pro&lt;/code&gt; and numeric bumps are marketing, not a capability contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze a golden set.&lt;/strong&gt; Twenty to fifty real requests from your own traffic with their recorded outputs. If you don't have one yet, this is the moment it becomes worth an afternoon; you will need it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the candidate against the golden set&lt;/strong&gt; and diff the things that actually break in production: output format compliance, tool-call rate, refusal rate, response length, and latency. Not vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One PR, one line.&lt;/strong&gt; Change &lt;code&gt;models.json&lt;/code&gt;, link the eval run in the description, let CI re-verify the lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the corpse.&lt;/strong&gt; Leave the old ID in the git history of &lt;code&gt;models.lock.json&lt;/code&gt; and in your eval artifacts. When you're asked to reproduce a result from two quarters ago, that record is the only thing that will let you say what actually served it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Variants and channels on a gateway
&lt;/h3&gt;

&lt;p&gt;If you route through a gateway, two IDs that look like the same model may reach different upstream channels with different reliability characteristics. Some catalogues label this explicitly — the one I work on tags channels as full-power, official relay, reverse-engineered ("quality not guaranteed"), or promotional, and that labelling is the whole point: you can choose a cheaper or less stable path deliberately, or refuse to. What you should never do is &lt;strong&gt;pattern-match a suffix and assume you know what it means.&lt;/strong&gt; Read the catalogue metadata, pick the exact string, pin it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift under a pinned ID
&lt;/h3&gt;

&lt;p&gt;Pinning protects you from renames. It does not protect you from an upstream changing what sits behind a name it didn't change. The only handle you have from the outside is behavioural: record how a pinned ID answers a fixed battery of prompts at temperature 0, re-run it on a schedule, and diff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/seven7763/llm-honesty-probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be precise about what that gives you: &lt;strong&gt;signals, not proof.&lt;/strong&gt; No probe run from outside can prove which weights served a request, and a change in output can have several innocent explanations. What it does produce is a timestamped record that something changed on a date, which is a much stronger position to argue from than "it feels worse lately."&lt;/p&gt;

&lt;p&gt;Pin the ID, alias the role, lock the catalogue, watch the behaviour. It's an afternoon of work, and it converts your loudest class of production surprise into a CI failure.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Model IDs are a dependency. Pin them like one.</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Mon, 07 Sep 2026 11:19:26 +0000</pubDate>
      <link>https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-2gcc</link>
      <guid>https://dev.to/seven7763/model-ids-are-a-dependency-pin-them-like-one-2gcc</guid>
      <description>&lt;p&gt;You typed &lt;code&gt;claude-sonnet-4-5&lt;/code&gt; from memory, it returned a completion, and you shipped. Six weeks later the outputs are subtly different, or a deploy fails with a 404 on a Tuesday morning, and the git blame points at a string literal in a file nobody has opened since.&lt;/p&gt;

&lt;p&gt;Model IDs are a runtime dependency on someone else's release schedule, and almost nobody treats them like one. We pin &lt;code&gt;requests==2.32.3&lt;/code&gt; and then interpolate a model name from a config file we last checked in March.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch&amp;amp;utm_term=en" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a gateway that fronts hundreds of models from roughly 25 vendors&lt;/strong&gt; — which means I get to watch upstream renames arrive from several directions at once. Everything below works against any endpoint that implements &lt;code&gt;GET /v1/models&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three ways a wrong ID fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mode A — a clean error.&lt;/strong&gt; &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;400&lt;/code&gt; with &lt;code&gt;"code": "model_not_found"&lt;/code&gt;. This is the &lt;em&gt;good&lt;/em&gt; outcome. You find out in development, in the first second, and you fix a typo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode B — alias resolution.&lt;/strong&gt; You ask for &lt;code&gt;gpt-4o&lt;/code&gt;, the provider resolves it to a dated build, and the response's &lt;code&gt;model&lt;/code&gt; field says which one. Fine and normal — as long as you read the echo. If you never look, you've silently accepted whatever the alias points at this month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode C — nearest-match routing.&lt;/strong&gt; Some endpoints and proxies try to be helpful: they don't recognise your ID, so they route to the closest thing they do recognise. You asked for a flagship and got something else, with a 200 and no indication anywhere in the response that a substitution happened. This is the mode that costs real money and real trust, and it's invisible in exactly the way that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ten-second test for Mode C
&lt;/h3&gt;

&lt;p&gt;Send an ID that cannot possibly exist, formed by mangling a real one:&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;-s&lt;/span&gt; https://api.daoxe.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;$LLM_API_KEY&lt;/span&gt;&lt;span class="s2"&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":"claude-sonnet-4-5-TYPO-NOT-A-MODEL",
       "max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; d=json.load(sys.stdin); \
print("ERROR:", d["error"]["message"][:80]) if "error" in d else print("SERVED BY:", d.get("model"))'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get an error, every model ID you send is authoritative. If you get a completion, &lt;strong&gt;every model ID you send is advisory&lt;/strong&gt; — the endpoint reserves the right to pick for you, and no amount of pinning on your side will help until you know the rule it uses. Run this before you build anything else in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dated versus floating
&lt;/h3&gt;

&lt;p&gt;Two ID styles, two different risks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Floating (&lt;code&gt;claude-sonnet-4-5&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Dated (&lt;code&gt;claude-sonnet-4-5-20250929&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Behaviour&lt;/td&gt;
&lt;td&gt;changes under you, no code change&lt;/td&gt;
&lt;td&gt;stable while it exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifespan&lt;/td&gt;
&lt;td&gt;long&lt;/td&gt;
&lt;td&gt;retired on a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fails as&lt;/td&gt;
&lt;td&gt;silent quality drift&lt;/td&gt;
&lt;td&gt;a loud 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use for&lt;/td&gt;
&lt;td&gt;exploration, prototypes&lt;/td&gt;
&lt;td&gt;anything with a recorded eval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The policy that follows: &lt;strong&gt;exploration floats, production is dated, and every eval artifact records the ID that actually served it&lt;/strong&gt; — read from the response's &lt;code&gt;model&lt;/code&gt; field, not from what you asked for. That last part is what lets you answer "was this eval run on the same build as that one" three months later.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /v1/models&lt;/code&gt; is the source of truth, and it's per-key
&lt;/h3&gt;

&lt;p&gt;Not the docs, not the pricing page, not a blog post. The catalogue endpoint returns the IDs &lt;em&gt;your key&lt;/em&gt; is entitled to call, which is frequently a different set from what the vendor sells:&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;-s&lt;/span&gt; https://api.daoxe.com/v1/models &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;$LLM_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; [print(m["id"]) for m in json.load(sys.stdin)["data"]]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat worth knowing up front: most implementations return IDs and little else. Context windows, tool support, vision support and modality are usually &lt;strong&gt;not&lt;/strong&gt; in there, so you still need a small capability sidecar of your own. Don't infer capabilities from the name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock the catalogue
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;check_models.py — fail CI when a model ID this repo depends on disappears.

    python3 check_models.py --write      # re-pin (review the diff!)
    python3 check_models.py              # verify
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="sh"&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;catalogue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&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;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;m&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The IDs this repo actually uses — derived from the alias map, never hand-listed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.lock.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;catalogue&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalogue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&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;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pinned &lt;/span&gt;&lt;span class="si"&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;live&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ids to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&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="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalogue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;live_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;required_ids&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;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;live_set&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note   new in catalogue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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;for&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;live_set&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warn   left the catalogue (unused here): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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="n"&gt;gone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;live_set&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;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR  required by models.json but not in the catalogue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&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="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gone&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it nightly and on every pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;model-catalogue&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3 check_models.py&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;
          &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two failure signals mean different things. A &lt;strong&gt;nightly&lt;/strong&gt; failure means an upstream retired or renamed something you depend on, and you found out before your users did. A &lt;strong&gt;pull-request&lt;/strong&gt; failure means someone added a role pointing at an ID that isn't callable — a typo caught at review time instead of at 2am.&lt;/p&gt;

&lt;h3&gt;
  
  
  The alias layer
&lt;/h3&gt;

&lt;p&gt;Nothing in your application code should contain a model name. Code refers to &lt;em&gt;roles&lt;/em&gt;; one file maps roles to exact IDs.&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pinned_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bulk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"embed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;exact-embedding-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deep"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dated build; re-pin only with an eval run linked in the PR"&lt;/span&gt;&lt;span class="w"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="nd"&gt;@functools.lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&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;def&lt;/span&gt; &lt;span class="nf"&gt;_roles&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&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;model_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&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="nf"&gt;_roles&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&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;KeyError&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;no model pinned for role &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; — add it to models.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things fall out of this for free. A rename becomes a &lt;strong&gt;one-line diff in a reviewable file&lt;/strong&gt; instead of a search-and-replace across forty call sites. &lt;code&gt;check_models.py&lt;/code&gt; can derive its required set from the same file, so the lock and the code can't disagree. And &lt;code&gt;git log models.json&lt;/code&gt; becomes a readable history of what your product was running and when — which is the artifact you'll want the first time someone asks why output quality changed in July.&lt;/p&gt;

&lt;p&gt;Then stop the IDs from leaking back in. One test, one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# tests/no_hardcoded_model_ids.sh — fails if a model-looking literal appears in src/&lt;/span&gt;
&lt;span class="o"&gt;!&lt;/span&gt; rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!models.json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'"(gpt|claude|gemini|deepseek|qwen|kimi|grok|llama)-[a-z0-9._-]+"'&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When an upstream retires a model mid-project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find out from CI, not from users.&lt;/strong&gt; That's what the nightly is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the label before you pick a successor.&lt;/strong&gt; Vendors publish migration notes; gateways publish catalogue metadata. Don't infer the replacement from the name — &lt;code&gt;-mini&lt;/code&gt;, &lt;code&gt;-fast&lt;/code&gt;, &lt;code&gt;-pro&lt;/code&gt; and numeric bumps are marketing, not a capability contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze a golden set.&lt;/strong&gt; Twenty to fifty real requests from your own traffic with their recorded outputs. If you don't have one yet, this is the moment it becomes worth an afternoon; you will need it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the candidate against the golden set&lt;/strong&gt; and diff the things that actually break in production: output format compliance, tool-call rate, refusal rate, response length, and latency. Not vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One PR, one line.&lt;/strong&gt; Change &lt;code&gt;models.json&lt;/code&gt;, link the eval run in the description, let CI re-verify the lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the corpse.&lt;/strong&gt; Leave the old ID in the git history of &lt;code&gt;models.lock.json&lt;/code&gt; and in your eval artifacts. When you're asked to reproduce a result from two quarters ago, that record is the only thing that will let you say what actually served it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Variants and channels on a gateway
&lt;/h3&gt;

&lt;p&gt;If you route through a gateway, two IDs that look like the same model may reach different upstream channels with different reliability characteristics. Some catalogues label this explicitly — the one I work on tags channels as full-power, official relay, reverse-engineered ("quality not guaranteed"), or promotional, and that labelling is the whole point: you can choose a cheaper or less stable path deliberately, or refuse to. What you should never do is &lt;strong&gt;pattern-match a suffix and assume you know what it means.&lt;/strong&gt; Read the catalogue metadata, pick the exact string, pin it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift under a pinned ID
&lt;/h3&gt;

&lt;p&gt;Pinning protects you from renames. It does not protect you from an upstream changing what sits behind a name it didn't change. The only handle you have from the outside is behavioural: record how a pinned ID answers a fixed battery of prompts at temperature 0, re-run it on a schedule, and diff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/seven7763/llm-honesty-probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be precise about what that gives you: &lt;strong&gt;signals, not proof.&lt;/strong&gt; No probe run from outside can prove which weights served a request, and a change in output can have several innocent explanations. What it does produce is a timestamped record that something changed on a date, which is a much stronger position to argue from than "it feels worse lately."&lt;/p&gt;

&lt;p&gt;Pin the ID, alias the role, lock the catalogue, watch the behaviour. It's an afternoon of work, and it converts your loudest class of production surprise into a CI failure.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your LLM API will fail in production. Here's the fallback system that catches it.</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:28:43 +0000</pubDate>
      <link>https://dev.to/seven7763/your-llm-api-will-fail-in-production-heres-the-fallback-system-that-catches-it-15hj</link>
      <guid>https://dev.to/seven7763/your-llm-api-will-fail-in-production-heres-the-fallback-system-that-catches-it-15hj</guid>
      <description>&lt;p&gt;Every LLM API fails. Not "might fail." Fails.&lt;/p&gt;

&lt;p&gt;OpenAI returns 429s during peak hours. Anthropic has regional outages. Google's Gemini API occasionally decides your key isn't authorized for the model you've been calling all week. A provider you depend on goes down for 45 minutes and your agent pipeline — the one you told your team was "production-ready" — sits there dead, mid-task, with no idea what happened.&lt;/p&gt;

&lt;p&gt;You already know retry logic. This article is about what comes &lt;em&gt;after&lt;/em&gt; retry: how to build a multi-model fallback system that keeps your application running when the primary model is unreachable, and how to avoid the mistakes that make fallback &lt;em&gt;worse&lt;/em&gt; than just failing.&lt;/p&gt;

&lt;p&gt;I'll show real Python patterns, the gotchas that break them in practice, and how to test your fallback without waiting for an actual outage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure up front:&lt;/strong&gt; I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, a multi-model API gateway. The patterns in this article work against any provider — official APIs, third-party gateways, or self-hosted endpoints. Use them wherever your key works.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Why retry alone isn't enough
&lt;/h2&gt;

&lt;p&gt;The simplest thing that sometimes works:&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;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&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="n"&gt;max_retries&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="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIError&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&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;raise&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles transient network blips and brief rate limiting. It does not handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sustained outages.&lt;/strong&gt; If the provider is down for 30 minutes, exponential backoff just means your caller eventually raises an exception after 14 seconds instead of immediately. Your agent is still dead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quota exhaustion.&lt;/strong&gt; Your key hits its monthly limit. Retrying won't fix it. It'll just burn your error budget faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model deprecation.&lt;/strong&gt; The provider sunsets the model version you pinned. Every call fails with &lt;code&gt;model_not_found&lt;/code&gt;. Retry spins uselessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional blocks.&lt;/strong&gt; The endpoint is up — just not from where your server lives. The client sees a timeout and retries into the same wall.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retry is the first layer. Fallback is the second.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The model-level fallback pattern (simple, with sharp edges)
&lt;/h2&gt;

&lt;p&gt;The basic idea: if model A fails, try model B.&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;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;FALLBACK_CHAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&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;https://api.openai.com/v1&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;key_env&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;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&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;base_url&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;https://api.anthropic.com/v1/messages&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;key_env&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;ANTHROPIC_API_KEY&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;protocol&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;anthropic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&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;base_url&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;https://generativelanguage.googleapis.com/v1beta&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;key_env&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;GEMINI_API_KEY&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;protocol&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;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_with_fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FALLBACK_CHAIN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key_env&lt;/span&gt;&lt;span class="sh"&gt;"&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="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
            &lt;span class="p"&gt;)&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;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&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;RuntimeError&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;All &lt;/span&gt;&lt;span class="si"&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;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; models failed. Last error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works — until it doesn't. Here's where it breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 1: Protocol incompatibility
&lt;/h3&gt;

&lt;p&gt;Anthropic's API uses the Messages protocol, not Chat Completions. Calling it through an OpenAI-shaped client with &lt;code&gt;base_url=https://api.anthropic.com/v1/messages&lt;/code&gt; produces a 404 or a garbled error. You need a protocol adapter, or you need to use a gateway that translates between protocols.&lt;/p&gt;

&lt;p&gt;The honest fix: either maintain separate client code paths per provider, or use a gateway that presents every model behind a single protocol. Both approaches work. The single-protocol path means fewer branches to test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 2: The system prompt disappears
&lt;/h3&gt;

&lt;p&gt;Some models handle system prompts differently. Claude native uses a top-level &lt;code&gt;system&lt;/code&gt; parameter. OpenAI uses a &lt;code&gt;system&lt;/code&gt; role in the messages array. Gemini puts system instructions in a &lt;code&gt;systemInstruction&lt;/code&gt; config field. When you fall back across providers, your carefully crafted system prompt might arrive as a user message — or not at all.&lt;/p&gt;

&lt;p&gt;Test this explicitly. Send a message that asks the model to quote the system prompt verbatim. If it can't, your fallback is silently dropping instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 3: Tool calling breaks between families
&lt;/h3&gt;

&lt;p&gt;Anthropic and OpenAI have different tool calling schemas. The function definitions that work on GPT may produce &lt;code&gt;tool_use&lt;/code&gt; blocks on Claude that your parser doesn't recognize. If your agent depends on structured tool calls, cross-provider fallback is risky without a normalization layer.&lt;/p&gt;

&lt;p&gt;For agent pipelines that use tool calling heavily, fallback within the same model family first — GPT family before Claude family before Gemini — and only cross families when the task doesn't require structured output.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Routing by capability, not just availability
&lt;/h2&gt;

&lt;p&gt;A smarter fallback system doesn't blindly try models in a fixed order. It selects the fallback based on what the task actually needs.&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;CAPABILITY_PROFILES&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;reasoning&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;claude-sonnet-4-6&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;gpt-5.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&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;coding&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;claude-sonnet-4-6&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;gpt-5.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-v4-pro&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;fast_chat&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;claude-haiku-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.4&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;gemini-3.5-flash&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;long_context&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;gemini-2.5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&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;gpt-5.5&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;MODEL_CONFIGS&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;claude-sonnet-4-6&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;base_url&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;...&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;key_env&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;...&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;protocol&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;anthropic&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;gpt-5.5&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;base_url&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;...&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;key_env&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;...&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;protocol&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;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# ... etc
&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;call_by_capability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capability&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;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CAPABILITY_PROFILES&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;capability&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&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;ValueError&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;Unknown capability: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;capability&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="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MODEL_CONFIGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model_id&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key_env&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&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;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&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;RuntimeError&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;All &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; models failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your code declares intent — "I need long-context reasoning" — and the dispatcher picks appropriate models. This is more resilient than a fixed order, and it makes your code self-documenting about what each call actually requires.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The gateway pattern: one endpoint, many models
&lt;/h2&gt;

&lt;p&gt;Managing separate API keys, base URLs, and protocol adapters for a half-dozen providers is operational friction. The alternative is a single endpoint that presents many models behind one API surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# One base URL. One key. All models available.
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.daoxe.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAOXE_API_KEY&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;FALLBACK_CHAIN&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;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# primary
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# fallback 1
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# fallback 2
&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;call_single_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FALLBACK_CHAIN&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&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="mf"&gt;30.0&lt;/span&gt;
            &lt;span class="p"&gt;)&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;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&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;RuntimeError&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;All models in chain failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage is not having to manage per-provider credentials, protocol quirks, or billing relationships. The trade-off is that the gateway itself becomes a single point of failure — if the gateway goes down, all models behind it become unreachable. (Mitigations: monitor the gateway's health separately, and keep a direct-to-provider key as an escape hatch for critical paths.)&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Circuit breaker: when even fallback should stop
&lt;/h2&gt;

&lt;p&gt;Blindly cycling through fallback models under load can make things worse. If your primary model is returning 429s because you're over your rate limit, hammering the fallback models immediately is antisocial — and it might exhaust your quota on &lt;em&gt;all&lt;/em&gt; providers in a cascade.&lt;/p&gt;

&lt;p&gt;A circuit breaker stops calling a model after N consecutive failures and only re-enables it after a cooldown.&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;time&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;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failure_threshold&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_open_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;bool&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;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_open_until&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_open_until&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_failures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_failures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_open_until&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_failures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_open_until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into the fallback dispatcher:&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;breaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failure_threshold&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;cooldown_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;30.0&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;call_with_breaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&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;breaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# circuit open, skip
&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;breaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&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;breaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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 models unavailable (circuit open or failed)&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;Now when a model goes down repeatedly, the system stops trying it for 30 seconds, giving it time to recover without wasting your remaining quota on guaranteed failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Observability: you can't fix what you can't see
&lt;/h2&gt;

&lt;p&gt;A fallback system without instrumentation is a debugging nightmare. When a call succeeds on the third model in the chain, you need to know &lt;em&gt;that it happened&lt;/em&gt; and &lt;em&gt;why the first two failed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At minimum, log:&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;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_fallback&lt;/span&gt;&lt;span class="sh"&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;call_instrumented&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;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;logger&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;fallback_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;extra&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;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chain&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fallback_depth&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completion_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_call_failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;extra&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;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;e&lt;/span&gt;&lt;span class="p"&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;critical&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_models_exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&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;chain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chain&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;RuntimeError&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 models in chain failed&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;Critical metric to track over time: &lt;strong&gt;fallback rate&lt;/strong&gt; (what percentage of calls hit a fallback model). If this creeps above 1-2%, your primary provider has a reliability problem. If it spikes suddenly, something is broken. Either way, you want to know before your users do.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Testing your fallback without waiting for an outage
&lt;/h2&gt;

&lt;p&gt;The worst time to discover your fallback system is broken is during an actual outage. Test it artificially.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 1: The unreachable endpoint
&lt;/h3&gt;

&lt;p&gt;Point your primary model at a dead URL and verify the fallback fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set primary to a port where nothing listens&lt;/span&gt;
&lt;span class="nv"&gt;PRIMARY_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:19999/v1"&lt;/span&gt; python test_fallback.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Method 2: The error-injection proxy
&lt;/h3&gt;

&lt;p&gt;Write a tiny proxy that returns errors for a percentage of requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# error_proxy.py — run with: python error_proxy.py
# Proxies requests to your real endpoint, but returns 503 for ~30% of them.
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ErrorProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&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;do_POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&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="s"&gt;error&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="s"&gt;message&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="s"&gt;injected failure&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="s"&gt;type&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="s"&gt;server_error&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="k"&gt;return&lt;/span&gt;
        &lt;span class="c1"&gt;# ... proxy to real endpoint
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your application against this proxy. If the fallback doesn't fire within a few requests, something is misconfigured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Method 3: The key rotation test
&lt;/h3&gt;

&lt;p&gt;Rotate your primary API key to an invalid value and verify the system fails over to the next model. This catches the case where your fallback is configured but the credentials are wrong — a failure mode that's silent until the primary actually goes down.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. The honest limitations
&lt;/h2&gt;

&lt;p&gt;No fallback system is transparent to the caller. Here's what you actually trade off:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Each fallback attempt adds connection + inference time. A fast call that normally takes 800ms can take 3+ seconds if the first two models fail. For real-time applications, set a total timeout and surface to the user that a fallback is in progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost unpredictability.&lt;/strong&gt; Fallback models may have different pricing. Your monthly bill can spike if your primary model has a bad week and all traffic shifts to a more expensive fallback. Track spend per model and set alerts when fallback-model spend exceeds a threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavioral drift.&lt;/strong&gt; Different models produce different outputs. A system prompt that Claude handles elegantly might produce aggressive refusals on Gemini. A chain-of-thought instruction that works on GPT may be ignored by DeepSeek. Test your fallback models with your actual prompts before depending on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism is gone.&lt;/strong&gt; Even with &lt;code&gt;temperature=0&lt;/code&gt;, different models produce different outputs. If your application depends on deterministic or near-deterministic behavior, cross-model fallback fundamentally changes the semantics of the call. Your fallback is keeping the system &lt;em&gt;running&lt;/em&gt;, not keeping it &lt;em&gt;identical&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no zero-dependency option.&lt;/strong&gt; A fallback system that spans multiple providers means you depend on all of them — their uptime, their pricing changes, their model deprecations, their API migrations. Your operational surface area multiplies.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. What to do right now
&lt;/h2&gt;

&lt;p&gt;If you take nothing else from this article, do these three things today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Identify which LLM calls in your codebase are single-model and single-provider.&lt;/strong&gt; Anywhere you hardcode &lt;code&gt;model="gpt-5.5"&lt;/code&gt; with no alternative path is a single point of failure. Count them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick one non-critical path and add a two-model fallback chain.&lt;/strong&gt; Not your payment processing. Not your auth flow. A summarization step, a classification call, a "suggest alternative phrasing" feature. Ship it, instrument it, watch the logs for a week. Get comfortable with the pattern before you apply it to critical paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add a health-check endpoint that probes your primary model every 60 seconds and exposes the result.&lt;/strong&gt; A simple &lt;code&gt;/health/llm&lt;/code&gt; that calls your primary model with a trivial prompt and returns &lt;code&gt;{"status": "ok", "latency_ms": 420}&lt;/code&gt; or &lt;code&gt;{"status": "degraded", "error": "timeout"}&lt;/code&gt;. Wire this into whatever monitoring you already use. You want to know about provider degradation before your users do — and before your fallback system has to prove itself under fire.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;The goal of a fallback system is not perfection. It's to turn a hard failure — your agent crashes, your pipeline stops, your user sees an error — into a soft degradation the caller might not even notice. That's worth the complexity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you liked this, you might also want &lt;code&gt;llm-honesty-probe&lt;/code&gt; (&lt;a href="https://github.com/seven7763/llm-honesty-probe" rel="noopener noreferrer"&gt;github.com/seven7763/llm-honesty-probe&lt;/a&gt;), an open-source tool that checks whether your API endpoint is actually serving the model it claims. Fallback + verification = you're covered on both reliability and trust.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>OpenAI cut prices 80%. Here's why I'm not celebrating.</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:23:40 +0000</pubDate>
      <link>https://dev.to/seven7763/openai-cut-prices-80-heres-why-im-not-celebrating-449o</link>
      <guid>https://dev.to/seven7763/openai-cut-prices-80-heres-why-im-not-celebrating-449o</guid>
      <description>&lt;p&gt;This week, OpenAI slashed Luna (GPT-5.6) pricing by 80% — from $7 to $1.40 per million tokens. Anthropic shipped Opus 5 at half the price of its predecessor. The LLM API market is in a genuine price war, and for once, developers are winning.&lt;/p&gt;

&lt;p&gt;I should be celebrating. I'm not. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden math of cheaper models
&lt;/h2&gt;

&lt;p&gt;When official API prices drop, the economics of model-swapping change in a way nobody talks about.&lt;/p&gt;

&lt;p&gt;A bad relay has a simple business model: bill you for Model X, serve you Model Y, pocket the difference. When Model X costs $15/M tokens and Model Y costs $0.15/M, the margin is $14.85. That's already attractive.&lt;/p&gt;

&lt;p&gt;When Model X drops to $1.40? The absolute margin shrinks — and that's what most people see. But the &lt;em&gt;relative&lt;/em&gt; margin — the ratio of what you're charged vs what's actually served — can actually grow if budget models get cheaper still. And they have.&lt;/p&gt;

&lt;p&gt;The point isn't the exact math. The point is: &lt;strong&gt;price competition between legitimate providers doesn't eliminate the incentive to swap models. It often masks it.&lt;/strong&gt; When everyone's prices are dropping, a relay that's 30% cheaper than official looks like aggressive discounting, not fraud.&lt;/p&gt;

&lt;h2&gt;
  
  
  "What model are you?" doesn't work
&lt;/h2&gt;

&lt;p&gt;If you're still asking an API endpoint to identify itself, stop. That's a system prompt — trivially spoofed. Every relay operator knows to make the model say "I am Claude, created by Anthropic."&lt;/p&gt;

&lt;p&gt;What works: &lt;strong&gt;behavioral fingerprinting at temp=0.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give the model a fixed set of tasks that different models handle differently — reasoning problems, long-context recall, strict JSON formatting, refusal boundaries. Run them at temperature 0 to remove sampling noise. Diff the results against the official API.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. Here's the actual tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/seven7763/llm-honesty-probe
&lt;span class="nb"&gt;cd &lt;/span&gt;llm-honesty-probe
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; llm_honesty_probe &lt;span class="nt"&gt;--self-test&lt;/span&gt; &lt;span class="nt"&gt;--card&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs a battery of deterministic probes and produces a verdict card: PASS, SUSPICIOUS, or DEGRADED — with the specific probes that triggered each finding. No API key leaves your machine. No judgment calls required. The self-test runs without any API key and takes about 30 seconds.&lt;/p&gt;

&lt;p&gt;To test your actual endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://your-endpoint/v1"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-your-key"&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; llm_honesty_probe &lt;span class="nt"&gt;--compare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters more now, not less
&lt;/h2&gt;

&lt;p&gt;The price war is genuinely good news. Lower costs mean more experiments, more products, more people who can afford to build with LLMs. I want all of that.&lt;/p&gt;

&lt;p&gt;But cheaper official APIs also mean more users signing up for the first time — users who don't know what "Claude" is supposed to sound like, what GPT-5.6's reasoning depth should feel like, or what latency to expect. These are exactly the users a model-swapping relay wants: no baseline for comparison.&lt;/p&gt;

&lt;p&gt;The defense isn't trust. It's &lt;strong&gt;verification that costs less than being wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test your endpoint right now
&lt;/h2&gt;

&lt;p&gt;If you're using any third-party API endpoint — mine included — run the probe. The self-test takes 30 seconds. The compare against your endpoint takes about 2 minutes.&lt;/p&gt;

&lt;p&gt;If everything passes, you've got evidence — not proof, but signals — that you're getting what you pay for. If something flags, you know exactly which probe to investigate.&lt;/p&gt;

&lt;p&gt;Either way, you know more than you did before. That's the whole point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, an OpenAI-compatible LLM gateway. I also maintain &lt;code&gt;llm-honesty-probe&lt;/code&gt;, the open-source tool mentioned in this post. The probe treats every endpoint identically — including daoxe. Point it at anyone. If it flags something on my own service, I want to know before you do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The price war is real. The trust problem is real. They're related, and fixing one doesn't automatically fix the other.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
    <item>
      <title>Deploy a real AI chat app in one click — key stays server-side (Vercel/Render/Railway)</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:40:49 +0000</pubDate>
      <link>https://dev.to/seven7763/deploy-a-real-ai-chat-app-in-one-click-key-stays-server-side-vercelrenderrailway-391k</link>
      <guid>https://dev.to/seven7763/deploy-a-real-ai-chat-app-in-one-click-key-stays-server-side-vercelrenderrailway-391k</guid>
      <description>&lt;p&gt;Most "deploy an AI app in 5 minutes" tutorials quietly do the one thing you must never do: put an API key where a browser can see it. This post is three one-click starters that are correct by construction — the key is a &lt;strong&gt;deploy-time environment variable, server-side only&lt;/strong&gt;, never in client code, never committed to git.&lt;/p&gt;

&lt;p&gt;Each template is &lt;strong&gt;provider-neutral&lt;/strong&gt;: it defaults to an OpenAI-compatible endpoint (&lt;code&gt;https://daoxe.com/v1&lt;/code&gt;) but points at any compatible endpoint by changing one variable. Pick the one that matches what you're building.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=daoxe-deploy-templates" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, the OpenAI-compatible gateway these default to. Everything below works against any compatible endpoint — the default and the "powered by" link are one-line changes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The rule that makes these safe
&lt;/h2&gt;

&lt;p&gt;An LLM API key is a &lt;strong&gt;billing credential&lt;/strong&gt;. Anything shipped to a browser is public. So every template here follows the same discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DAOXE_API_KEY&lt;/code&gt; (and any tokens) are &lt;strong&gt;deploy-time env vars&lt;/strong&gt;, declared in each platform's config with &lt;strong&gt;no baked-in value&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env.example&lt;/code&gt; files ship &lt;strong&gt;empty placeholders&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The key is used &lt;strong&gt;server-side only&lt;/strong&gt; — an Edge function, a proxy server, or a bot process holds it; the browser never sees it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a tutorial ever tells you to paste your key into front-end JavaScript, close the tab.&lt;/p&gt;




&lt;h2&gt;
  
  
  Template 1 — Streaming chat UI on Vercel
&lt;/h2&gt;

&lt;p&gt;A minimal chat UI backed by an Edge function that holds the key and streams tokens back to the browser. Good for: a demo, an internal tool, a landing-page assistant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/seven7763/daoxe-vercel-chat" rel="noopener noreferrer"&gt;seven7763/daoxe-vercel-chat&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy:&lt;/strong&gt; click the Vercel button in the repo README; Vercel prompts you for two env vars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DAOXE_API_KEY&lt;/code&gt; — your key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DAOXE_MODEL&lt;/code&gt; — an exact id from &lt;code&gt;GET /v1/models&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it — you get a URL with a working streaming chat. The browser calls &lt;em&gt;your&lt;/em&gt; Edge function; the Edge function calls the gateway with the key. The key never leaves the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser ──▶ /api/chat (Edge, holds key) ──▶ https://daoxe.com/v1/chat/completions
        ◀── streamed tokens ◀──────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional &lt;code&gt;DAOXE_ALLOWED_MODELS&lt;/code&gt; lets you restrict which models the function will serve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Template 2 — OpenAI-compatible proxy on Render
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't want a UI — you want to &lt;strong&gt;share one key with a team or a script&lt;/strong&gt; without handing out the real credential. This template is a tiny OpenAI-compatible proxy: it exposes &lt;code&gt;/v1/chat/completions&lt;/code&gt;, injects the real key server-side, and gates access with a token you hand out instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/seven7763/daoxe-render-proxy" rel="noopener noreferrer"&gt;seven7763/daoxe-render-proxy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy:&lt;/strong&gt; click the Render button in the repo README; set &lt;code&gt;DAOXE_API_KEY&lt;/code&gt; (the real key) and a share token. Now teammates point their OpenAI-compatible tools at &lt;em&gt;your&lt;/em&gt; proxy URL with the share token — they get model access, never your billing credential. It also serves a live &lt;code&gt;/healthz&lt;/code&gt; page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Template 3 — Discord bot on Railway
&lt;/h2&gt;

&lt;p&gt;A Discord bot that answers &lt;code&gt;/ask&lt;/code&gt; with an LLM. Good for: team assistants, support bots, community helpers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/seven7763/daoxe-railway-discord-bot" rel="noopener noreferrer"&gt;seven7763/daoxe-railway-discord-bot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy:&lt;/strong&gt; from the public repo, open Railway → &lt;em&gt;New Project&lt;/em&gt; → &lt;em&gt;Deploy from GitHub&lt;/em&gt; (or create a Template once), then set &lt;code&gt;DAOXE_API_KEY&lt;/code&gt;, &lt;code&gt;DAOXE_MODEL&lt;/code&gt;, and your Discord bot token. The bot process holds the key; Discord users just type &lt;code&gt;/ask&lt;/code&gt;. (This one has a single runtime dependency, &lt;code&gt;discord.js&lt;/code&gt;.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Env vars cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DAOXE_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all three&lt;/td&gt;
&lt;td&gt;Billing credential. Server-side only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DAOXE_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vercel-chat, railway bot&lt;/td&gt;
&lt;td&gt;Exact id from &lt;code&gt;GET /v1/models&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DAOXE_ALLOWED_MODELS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;vercel-chat (optional)&lt;/td&gt;
&lt;td&gt;Comma-separated allowlist.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share / Discord tokens&lt;/td&gt;
&lt;td&gt;render proxy / railway bot&lt;/td&gt;
&lt;td&gt;Separate from the billing key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(Repos are public. Vercel/Render one-click buttons live in each README; Railway is "Deploy from GitHub" from the public repo — no private template id required.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What this buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Three one-click AI starters: &lt;strong&gt;Vercel&lt;/strong&gt; (streaming chat), &lt;strong&gt;Render&lt;/strong&gt; (OpenAI-compatible proxy), &lt;strong&gt;Railway&lt;/strong&gt; (Discord bot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key never in the browser&lt;/strong&gt; — the failure mode most tutorials ship by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One key → many models&lt;/strong&gt; behind a single OpenAI-compatible base URL, with fallback and one bill if you want it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't trust the pitch — deploy one template, open the network tab, and confirm the key is not in any client request. Then point the same templates at &lt;em&gt;any&lt;/em&gt; OpenAI-compatible endpoint and compare.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>serverless</category>
      <category>javascript</category>
    </item>
    <item>
      <title>One key, two ecosystems: routing between DeepSeek/Kimi/Qwen and Claude/GPT/Gemini by task</title>
      <dc:creator>Seven</dc:creator>
      <pubDate>Sun, 19 Jul 2026 22:40:57 +0000</pubDate>
      <link>https://dev.to/seven7763/one-key-two-ecosystems-routing-between-deepseekkimiqwen-and-claudegptgemini-by-task-2kk3</link>
      <guid>https://dev.to/seven7763/one-key-two-ecosystems-routing-between-deepseekkimiqwen-and-claudegptgemini-by-task-2kk3</guid>
      <description>&lt;p&gt;Picking a single model family for an app is a false choice. Some Chinese-origin models are strong and cost-efficient at certain tasks; some Western models lead at others. If you're building outside mainland China, the practical move isn't to bet on one — it's to &lt;strong&gt;route each request to the right one&lt;/strong&gt;, and make switching a one-line change instead of a new integration.&lt;/p&gt;

&lt;p&gt;This guide shows how to do that through a single &lt;strong&gt;OpenAI-compatible endpoint&lt;/strong&gt;, so DeepSeek, Kimi, Qwen, Claude, GPT and Gemini all sit behind one key and one bill — and how to choose between them per task without guessing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure &amp;amp; scope:&lt;/strong&gt; I work on &lt;a href="https://daoxe.com/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=en_launch" rel="noopener noreferrer"&gt;daoxe&lt;/a&gt;, an OpenAI-compatible gateway. It is &lt;strong&gt;not available in mainland China&lt;/strong&gt; — this article is written for developers &lt;em&gt;outside&lt;/em&gt; it (overseas devs, overseas Chinese, and users in Russia, Brazil, Vietnam, Turkey and elsewhere) who want access to both ecosystems in one place. Everything here is standard OpenAI-compatible config; point it at any endpoint that carries the models you want.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why route at all
&lt;/h2&gt;

&lt;p&gt;Three reasons a single-model app leaves value on the table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Task fit.&lt;/strong&gt; Models have uneven strengths — reasoning, long-context, code, multilingual, structured output, vision. The best model for summarizing a 200-page PDF isn't necessarily the best for a tricky refactor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; For high-volume, low-stakes calls (classification, extraction, routing itself), a cheaper model is often &lt;em&gt;good enough&lt;/em&gt;, and the savings compound. Reserve the expensive model for the calls that need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience.&lt;/strong&gt; If one provider is rate-limited or degraded, being able to fail over to another family keeps you shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The blocker has always been integration cost: different SDKs, keys, billing, and response quirks per vendor. An OpenAI-compatible endpoint removes it — model choice becomes a string.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mechanism: model choice is a string
&lt;/h2&gt;

&lt;p&gt;Every call uses the same shape; only the &lt;code&gt;model&lt;/code&gt; field changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://daoxe.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;$DAOXE_API_KEY&lt;/span&gt;&lt;span class="s2"&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":"&amp;lt;MODEL_ID&amp;gt;","messages":[{"role":"user","content":"..."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List what your key can actually call — the authoritative, account-scoped source is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://daoxe.com/v1/models &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$DAOXE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use those &lt;strong&gt;exact ids&lt;/strong&gt;. Families you'll typically see include Chinese-origin ones (DeepSeek, Kimi, Qwen, and others) and Western ones (Claude, GPT, Gemini). Don't hardcode ids from this post — they drift; read them from &lt;code&gt;/v1/models&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple task-based router (Python)
&lt;/h2&gt;

&lt;p&gt;You don't need a framework. A dict from task → model id gets you 90% of the value:&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;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://daoxe.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DAOXE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Map task types to EXACT ids from GET /v1/models (examples — replace with yours).
&lt;/span&gt;&lt;span class="n"&gt;ROUTES&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;cheap_bulk&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;deepseek-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# high-volume extraction/classification
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long_context&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;kimi-k2.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# very long inputs
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="c1"&gt;# hard multi-step problems
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&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;gpt-5.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# tricky refactors
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# fast, inexpensive fallback
&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;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&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;prompt&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ROUTES&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;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;httpx&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="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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;KEY&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="n"&gt;json&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;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&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;60&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="nf"&gt;raise_for_status&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="nf"&gt;json&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cheap_bulk&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;Extract the invoice total as JSON.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&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;Prove or disprove: ...&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;Because it's one endpoint and one key, adding a route is adding a dict entry — no new SDK, no new credential. Add a &lt;code&gt;try/except&lt;/code&gt; that falls back to another family on error and you've got cheap resilience too.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to choose the route (without vibes)
&lt;/h2&gt;

&lt;p&gt;Don't route on reputation — route on evidence for &lt;em&gt;your&lt;/em&gt; workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build a tiny eval set&lt;/strong&gt; of real prompts per task type with known-good answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run each candidate model&lt;/strong&gt; through it, and record accuracy alongside latency (p50/p95) and token cost. (A small harness does this; I wrote up a reproducible gateway benchmark separately.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the cheapest model that clears your quality bar&lt;/strong&gt; for each task — not the most expensive one you can afford.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-check periodically.&lt;/strong&gt; New model versions ship constantly; a route that was optimal last quarter may not be now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On price specifically: I'm not going to quote numbers here because they change. Check the provider's pricing page and put real &lt;code&gt;$/1M&lt;/code&gt; figures next to your latency and accuracy — the right route is a three-way trade-off, not a single-axis choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't forget to verify what you're getting
&lt;/h2&gt;

&lt;p&gt;Routing across families makes verification &lt;em&gt;more&lt;/em&gt; important, not less: with several models behind one endpoint, you want to confirm each id actually serves what it claims and isn't being silently swapped or truncated. Test &lt;strong&gt;behavior&lt;/strong&gt;, not the model's self-report — a tokenizer fingerprint, a capability-floor check, and a long-context needle test catch the common cheats. (There are open-source probes for this; I covered the method in a separate post.) A good endpoint is fine with you running these against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest positioning
&lt;/h2&gt;

&lt;p&gt;A few things to be straight about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is for outside mainland China.&lt;/strong&gt; daoxe doesn't serve mainland China; the audience here is overseas developers and users in regions where getting &lt;em&gt;both&lt;/em&gt; ecosystems in one place — and paying for it — is otherwise fiddly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Chinese vs Western" is about model origin, not a quality ranking.&lt;/strong&gt; Route on measured task fit and cost, not on where a model came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One key is a convenience, not magic.&lt;/strong&gt; It removes integration and billing friction; it doesn't make a small model into a big one. Match the model to the task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;daoxe is the endpoint I use for this — OpenAI-compatible at &lt;code&gt;https://daoxe.com/v1&lt;/code&gt;, native Anthropic Messages for Claude-native tools, one key across many models (Chinese-origin and Western), one bill, and a design meant to be verified. See the &lt;a href="https://daoxe.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; for current models and prices; use &lt;code&gt;GET /v1/models&lt;/code&gt; for what your key can call.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't pick one family — &lt;strong&gt;route each task&lt;/strong&gt; to the best/cheapest model.&lt;/li&gt;
&lt;li&gt;One OpenAI-compatible endpoint makes model choice a &lt;strong&gt;string&lt;/strong&gt;: one key, one bill, no per-vendor SDKs.&lt;/li&gt;
&lt;li&gt;Choose routes on a &lt;strong&gt;small eval set&lt;/strong&gt; (accuracy + p50/p95 + $/1M), not reputation; re-check as versions ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; each model behaviorally, and remember this is for use &lt;strong&gt;outside mainland China&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>deepseek</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
