<?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: SentioGate</title>
    <description>The latest articles on DEV Community by SentioGate (@sentiogate).</description>
    <link>https://dev.to/sentiogate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196432%2F4541bd01-3b2b-4d1b-826f-ecee0451650a.webp</url>
      <title>DEV Community: SentioGate</title>
      <link>https://dev.to/sentiogate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sentiogate"/>
    <language>en</language>
    <item>
      <title>Building a Rate-Limit-Aware LLM Router in Python: Retries, Backoff &amp; Capacity Scoring</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:15:51 +0000</pubDate>
      <link>https://dev.to/sentiogate-org/building-a-rate-limit-aware-llm-router-in-python-retries-backoff-capacity-scoring-4f9p</link>
      <guid>https://dev.to/sentiogate-org/building-a-rate-limit-aware-llm-router-in-python-retries-backoff-capacity-scoring-4f9p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Rate-limit-aware routing - circuit breaker failover" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your app has ever fallen over because a provider returned &lt;code&gt;429 Too Many Requests&lt;/code&gt;, and your fallback was "sleep a second and try again", you know the pain. That pattern hurts twice: you hit the provider again while it's already telling you to slow down, and you still don't know how long to wait.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; partly because I kept writing that loop. Here's how it handles rate limits, and how to use it from Python or JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a 429 is information
&lt;/h2&gt;

&lt;p&gt;A 429 isn't a random failure. It's the provider telling you something specific: &lt;em&gt;this model, on this key, is unavailable for a while.&lt;/em&gt; Often it even tells you how long, in a &lt;code&gt;Retry-After&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Most retry code throws that information away. It re-sends the same request to the same model and hopes. A rate-limit-aware router does the opposite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remember&lt;/strong&gt; that this model on this key is throttled, and for how long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route around it&lt;/strong&gt; to the next healthy candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Come back&lt;/strong&gt; once the window has passed, carefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What SentioGate does about it
&lt;/h2&gt;

&lt;p&gt;SentioGate is an OpenAI-compatible router. You send a chat request with &lt;code&gt;model: "auto"&lt;/code&gt; and your own provider keys (BYOK), or use the free community pool, and it picks a model for you. Rate limits are part of that decision, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic cooldowns
&lt;/h3&gt;

&lt;p&gt;When a provider throttles a request, the hosted service puts &lt;strong&gt;that model on that key&lt;/strong&gt; on a cooldown. It follows the provider's own &lt;code&gt;Retry-After&lt;/code&gt; when there is one, so the cooldown lasts as long as the provider asked, within sensible bounds. During the cooldown, that model simply isn't a candidate, and your request moves on without waiting.&lt;/p&gt;

&lt;p&gt;Other failures are handled on their own terms. A key that's out of credits, or a model your key can't access, is set aside instead of being retried over and over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Circuit breakers
&lt;/h3&gt;

&lt;p&gt;Separately from cooldowns, a model that keeps failing gets its breaker tripped, and it's skipped for a while. Breaker state is shared across the whole edge, so once one request finds a sick model, everyone else's requests avoid it too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback to the next healthy model
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;model: "auto"&lt;/code&gt;, SentioGate ranks candidates on capability, price, and measured latency and success rate. Models that are cooling down or tripped drop out of the ranking. If the chosen model fails, the request falls through to the next healthy one. For streams, that happens before the first token reaches you.&lt;/p&gt;

&lt;p&gt;If every candidate is unavailable, you don't get a hang. You get a clear &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; telling you when to try again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The community pool: careful recovery
&lt;/h3&gt;

&lt;p&gt;The free community pool is shared by everyone, so it's handled with extra care. When the upstream throttles it, the pool backs off for as long as the upstream asked. When that window ends, it checks the upstream cautiously before letting traffic flow again. That way, a crowd of waiting clients doesn't stampede the provider the instant the window opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue instead of 429
&lt;/h3&gt;

&lt;p&gt;Some workloads would rather wait than fail. When the shared community pool is full or rate limited, a non-streaming request can opt in to queueing with one header:&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://api.sentiogate.dev/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;$SENTIOGATE_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;"Content-Type: application/json"&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-SentioGate-Allow-Queue: true"&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": "auto", "messages": [{"role": "user", "content": "Summarise this ticket..."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a 429, you get a &lt;code&gt;202&lt;/code&gt; with a &lt;code&gt;job_id&lt;/code&gt; and a &lt;code&gt;poll_url&lt;/code&gt; to collect the result later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it from Python
&lt;/h2&gt;

&lt;p&gt;Install the SDK (the SDKs are published as &lt;code&gt;ai-auto-router&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ai-auto-router-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A basic call, with your own key:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# defaults to https://api.sentiogate.dev
&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Explain idempotency keys in two sentences.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;provider_keys&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;openai&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;sk-...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server already does the routing and fallback, so &lt;strong&gt;the SDK doesn't retry by default&lt;/strong&gt;. If you want a client-side safety net, opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ai_auto_router_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoRouterClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RouterClientError&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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_retries&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;max_retry_wait&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&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;RouterClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Still limited after the retries you allowed: you get the code and how long to wait.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opt-in retries only kick in for transient 429s, only after waiting the server's &lt;code&gt;Retry-After&lt;/code&gt;, and never when that wait is longer than your &lt;code&gt;max_retry_wait&lt;/code&gt;. There's no hot loop and no blind sleep.&lt;/p&gt;

&lt;p&gt;To queue on the community pool from Python, pass &lt;code&gt;allow_queue&lt;/code&gt; and poll the job:&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Classify this support ticket...&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_body&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;allow_queue&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&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;status&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;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;job_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_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;while&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;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&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;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&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;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;break&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;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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="n"&gt;job&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And from JavaScript
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hushamdev/ai-auto-router-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxRetryWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;providerKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stream me a haiku&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For streams, opt-in retries only apply before the first chunk arrives, so you never get a half-repeated answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting
&lt;/h2&gt;

&lt;p&gt;There's also a self-hosted Python router that ranks providers using their rate-limit headers, retries transient failures with backoff, and falls back down a chain. Point the SDK's &lt;code&gt;base_url&lt;/code&gt; / &lt;code&gt;baseUrl&lt;/code&gt; at it. The source isn't public yet; if you need it on your own infra, get in touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical advice, whatever you use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/strong&gt; It's the single most useful header in a 429. Handle both forms: seconds and an HTTP date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound every wait.&lt;/strong&gt; Don't trust a &lt;code&gt;0&lt;/code&gt; from a flaky upstream, and don't let one header stall a request for an hour. Clamp what you get.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back before you retry.&lt;/strong&gt; If another model can do the job, switching is almost always faster than waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry in one layer only.&lt;/strong&gt; If your router retries and your HTTP client retries and your app retries, one 429 turns into a burst. That's why SentioGate's SDKs leave retries off unless you ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover gently.&lt;/strong&gt; When a window opens, send one request first, not everything you queued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection-level retry middleware isn't enough.&lt;/strong&gt; It re-sends the same body to the same URL; it can't decide to pick another model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; · &lt;a href="https://sentiogate.app/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;: the free community pool and the $9 Pro plan&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of a series: start with the &lt;a href="https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e"&gt;intro post&lt;/a&gt;, and find the others on the &lt;a href="https://dev.to/sentiogate"&gt;SentioGate dev.to page&lt;/a&gt;. Tell me below what to cover next.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Ollama, vLLM &amp; Private LLMs: Routing Your Own Models Next to Your Cloud Keys</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:15:12 +0000</pubDate>
      <link>https://dev.to/sentiogate-org/ollama-vllm-private-llms-routing-your-own-models-next-to-your-cloud-keys-1eb4</link>
      <guid>https://dev.to/sentiogate-org/ollama-vllm-private-llms-routing-your-own-models-next-to-your-cloud-keys-1eb4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Self-hosted models, routed next to the cloud" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You have cloud keys in your app, and a GPU box in the corner running vLLM or Ollama. Here's how &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; puts both behind one endpoint, with one request format, one stream format and one dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A question I get a lot: "I use the cloud for the hard tasks, but I have my own box for the easy stuff. Can one router handle both, without a separate integration for each?"&lt;/p&gt;

&lt;p&gt;Yes, as long as your box speaks the OpenAI chat-completions API. vLLM, Ollama, LM Studio and most private inference appliances do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just call the API"
&lt;/h2&gt;

&lt;p&gt;Most teams end up with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_difficulty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;      &lt;span class="c1"&gt;# cloud
&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;# local
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you two SDKs, two error shapes, two streaming formats, no failover and no shared telemetry. Every new model means another branch, and every outage means a code change.&lt;/p&gt;

&lt;p&gt;A router fixes the plumbing: one endpoint, one request format, one stream format, and one place to see what ran and what it cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering your endpoint
&lt;/h2&gt;

&lt;p&gt;In SentioGate, your own endpoint is a &lt;strong&gt;custom provider&lt;/strong&gt;. It's a &lt;code&gt;provider_id&lt;/code&gt;, a &lt;code&gt;base_url&lt;/code&gt;, an &lt;code&gt;api_key&lt;/code&gt;, and the list of model IDs it serves.&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;-X&lt;/span&gt; POST https://api.sentiogate.dev/v1/custom-providers &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;$SENTIOGATE_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;"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;'{
    "provider_id": "my-vllm",
    "display_name": "My vLLM",
    "base_url": "https://llm.example.com/v1",
    "api_key": "YOUR_ENDPOINT_KEY",
    "models": ["llama3.1:8b", "qwen2.5:14b"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make it reachable.&lt;/strong&gt; The hosted service calls your model from the edge, so &lt;code&gt;base_url&lt;/code&gt; must be reachable from the internet. A URL like &lt;code&gt;http://localhost:11434&lt;/code&gt; or a LAN hostname won't work. Put the endpoint behind HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect it.&lt;/strong&gt; An inference server on the open internet should require a key. vLLM supports &lt;code&gt;--api-key&lt;/code&gt;, and for Ollama a small reverse proxy that checks a bearer token does the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the credential in &lt;code&gt;api_key&lt;/code&gt;.&lt;/strong&gt; It's encrypted at rest with AES-256-GCM, like every other key in your Vault, and decrypted in memory only when SentioGate calls your endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calling your models
&lt;/h2&gt;

&lt;p&gt;Once registered, you can call a model explicitly as &lt;code&gt;provider_id/model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="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;Summarise this ticket in one line: ...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarise this ticket in one line: ...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your models also become candidates for &lt;code&gt;"model": "auto"&lt;/code&gt;, right next to the cloud models your keys can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;auto&lt;/code&gt; treats your models
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;auto&lt;/code&gt;, SentioGate picks a model per request from everything you've enabled. It looks at whether a model is capable enough for the task, whether the conversation fits its context window, what it costs, and how fast and reliable it has been recently. That last part is measured, not configured: if your box slows down or starts erroring, it drops down the list, and requests fall back to the next candidate before any bytes reach your client.&lt;/p&gt;

&lt;p&gt;You stay in control of the pool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want your own model as the primary?&lt;/strong&gt; Call it explicitly (&lt;code&gt;my-vllm/...&lt;/code&gt;), or enable only your own models in the Vault so &lt;code&gt;auto&lt;/code&gt; chooses among them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want cloud and local side by side?&lt;/strong&gt; Enable both, and &lt;code&gt;auto&lt;/code&gt; picks per request across the whole pool, with fallback between providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want guardrails?&lt;/strong&gt; Your policy (enabled models and spend caps) applies to every request, whichever provider serves it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you never have to guess what happened. Every response says which model was picked and why, in the &lt;code&gt;router&lt;/code&gt; block, or in the &lt;code&gt;X-SentioGate-Router&lt;/code&gt; header when streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else in the pipeline still applies
&lt;/h2&gt;

&lt;p&gt;Your models go through the same request path as the cloud ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; standard OpenAI-style SSE, so your front end doesn't care where the tokens came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback:&lt;/strong&gt; on &lt;code&gt;auto&lt;/code&gt; requests, a failed candidate moves on to the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context compression:&lt;/strong&gt; long chats are kept inside a token budget, exactly as for cloud models, and &lt;code&gt;router.compression&lt;/code&gt; in the response shows what was saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry:&lt;/strong&gt; provider, model, latency and tokens show up in the same dashboard as everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streaming from your own model looks exactly like streaming from OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a haiku about GPUs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Python: &lt;code&gt;router.stream_chat(...)&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The hybrid pattern
&lt;/h2&gt;

&lt;p&gt;The setup I'd recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud models&lt;/strong&gt; for the tasks that need them: hard reasoning and long context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own models&lt;/strong&gt;, called explicitly for the workloads you know they handle well, such as classification, extraction, short summaries and internal tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/strong&gt; wherever you want SentioGate to choose per request and fail over automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can mix all three in one app with one client, and pass cloud keys per request (&lt;code&gt;provider_keys&lt;/code&gt; / &lt;code&gt;providerKeys&lt;/code&gt;) if you'd rather not store them. Keys passed that way are used for that request only.&lt;/p&gt;

&lt;p&gt;It's a simple mental model: explicit when you know what you want, &lt;code&gt;auto&lt;/code&gt; when you want the router to decide, and one response format either way.&lt;/p&gt;

&lt;p&gt;Want everything, router included, on your own infrastructure? The source isn't public yet, so get in touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; · &lt;a href="https://sentiogate.app/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; (&lt;code&gt;ai-auto-router-client&lt;/code&gt;) · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt; (&lt;code&gt;@hushamdev/ai-auto-router-sdk&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running a local box next to your cloud keys? Tell me in the comments what you serve on it. I'm curious which workloads people keep on their own hardware.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How I Cut Long-Chat Token Bills by 85-95% With Semantic Context Compression</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:07:51 +0000</pubDate>
      <link>https://dev.to/sentiogate-org/how-i-cut-long-chat-token-bills-by-85-95-with-semantic-context-compression-35fk</link>
      <guid>https://dev.to/sentiogate-org/how-i-cut-long-chat-token-bills-by-85-95-with-semantic-context-compression-35fk</guid>
      <description>&lt;h1&gt;
  
  
  How I Cut Long-Chat Token Bills by 85–95% With Semantic Context Compression
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Semantic context compression - 85-95% token reduction" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical engineering post on semantic context compression for LLM agents — why "sliding window" truncation breaks agents, how a router detects the threshold, and what a good restore point looks like.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've built an agent that runs for hours or spans hundreds of turns, you know the bill: every turn you re-send the whole conversation to the model. Token cost grows linearly with session length, and eventually you hit the context window and the whole thing breaks.&lt;/p&gt;

&lt;p&gt;The usual fix — &lt;strong&gt;sliding-window truncation&lt;/strong&gt; ("keep the last N messages") — is terrible for agents. You throw away the plan you made in turn 3, the user's stated constraints from turn 10, and the decision you made at turn 47. The model starts repeating itself, contradicting earlier work, and the user notices immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic context compression&lt;/strong&gt; is the fix. And it's one of the features I care about most in &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;&lt;strong&gt;SentioGate&lt;/strong&gt;&lt;/a&gt;, the BYOK LLM router I'm building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "semantic compression" means (vs. naive truncation)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What's kept&lt;/th&gt;
&lt;th&gt;What's lost&lt;/th&gt;
&lt;th&gt;Cost at turn 200&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sliding window (last 20 msgs)&lt;/td&gt;
&lt;td&gt;Recent chatter&lt;/td&gt;
&lt;td&gt;Plan, constraints, decisions&lt;/td&gt;
&lt;td&gt;High — model re-derives context every turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive "summarize older msgs"&lt;/td&gt;
&lt;td&gt;A text blob&lt;/td&gt;
&lt;td&gt;Structured state (files, tools, user intent)&lt;/td&gt;
&lt;td&gt;Medium — summary drifts, loses key facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic compression + restore point&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured state: goals, files, tool calls, user intent, open threads&lt;/td&gt;
&lt;td&gt;Verbose intermediate chatter&lt;/td&gt;
&lt;td&gt;Low — the compact state carries 90%+ of the useful signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: a long conversation isn't a bag of text. It's &lt;strong&gt;state&lt;/strong&gt; — goals, constraints, decisions, open tasks, files being edited, tools being called. If you compress the &lt;em&gt;state&lt;/em&gt; rather than the &lt;em&gt;prose&lt;/em&gt;, you get 85–95% token reduction with near-zero loss of continuity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the router detects the moment to compact
&lt;/h2&gt;

&lt;p&gt;The router watches each session's message tree. Two signals trigger compaction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capacity threshold&lt;/strong&gt; — when the session's context reaches a configured fraction of the model's context window (e.g., 75%), compact before the next turn overflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token threshold&lt;/strong&gt; — when the cumulative input tokens for the session cross a budget, compact proactively even if you're far from the window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The compaction preserves the most recent N messages verbatim (default: 4) and collapses everything older into a structured &lt;strong&gt;restore point&lt;/strong&gt; — a compact, machine-readable summary of the session's state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the restore point looks like
&lt;/h2&gt;

&lt;p&gt;The restore point isn't a free-text summary. It's a structured document with slots for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goals&lt;/strong&gt; — what the agent is trying to accomplish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User intent &amp;amp; constraints&lt;/strong&gt; — anything the user explicitly asked for or ruled out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files / resources&lt;/strong&gt; — files being edited, configs, data sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool state&lt;/strong&gt; — which tools have been called, what's pending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions made&lt;/strong&gt; — choices that affect downstream turns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open threads&lt;/strong&gt; — things not yet resolved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a new model (or a fresh session) picks up the conversation, it reads the restore point instead of the full history. Input tokens drop from, say, 60,000 to 4,000–8,000, and the model has everything it needs to continue without re-deriving context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for model-hopping
&lt;/h2&gt;

&lt;p&gt;A big unlock: because the restore point is model-agnostic, you can &lt;strong&gt;switch models mid-conversation&lt;/strong&gt; — finish a planning turn with a frontier model, then hand the execution to a cheaper / faster model, then back to the frontier model for a final review. Each handoff reads the same compact state. No re-reading of full history.&lt;/p&gt;

&lt;p&gt;That's the "seamless model-hopping" I mentioned in my &lt;a href="https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e"&gt;intro post to SentioGate&lt;/a&gt;. The compaction engine is what makes it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch for (and what I'm still iterating on)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loss of rare-but-critical facts.&lt;/strong&gt; The rare user message that's important later ("no, don't touch the prod DB") can get lost in compression if it's buried. The preserve-recent-N window helps, but I'm working on fact-extraction heuristics that flag "constraint" messages regardless of recency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-session continuity.&lt;/strong&gt; Today, compaction is per-session. Multi-session agents (a "planner" session feeding an "executor" session) need a shared compact state. That's on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality scoring of the compact output.&lt;/strong&gt; The restore point is only as good as the model that wrote it. I'm adding a lightweight QA pass that checks the compact against the original for the most critical facts before it's used.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number I'm most proud of
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;85–95% input-token reduction&lt;/strong&gt; on long sessions, measured across real agent workloads, with no measurable drop in task-completion quality. That's the entire value proposition of the compression engine in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sentiogate.app/" rel="noopener noreferrer"&gt;SentioGate — the BYOK LLM router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing — 0% markup, BYOK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If you're building long-running agents and your token bill is a problem, drop a comment — I'll follow up with a post on the exact scoring algorithm and how to tune the compaction thresholds for your workloads.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Juggling 7 LLM API Keys: One BYOK Router That Ranks, Streams &amp; Compacts for You</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:07:24 +0000</pubDate>
      <link>https://dev.to/sentiogate-org/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-2d6e</link>
      <guid>https://dev.to/sentiogate-org/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-2d6e</guid>
      <description>&lt;h1&gt;
  
  
  Stop Juggling 7 LLM API Keys: One BYOK Router That Ranks, Streams &amp;amp; Compacts for You
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="SentioGate - the BYOK LLM router" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SentioGate&lt;/strong&gt; is a production-ready &lt;strong&gt;BYOK (Bring Your Own Key) routing layer&lt;/strong&gt; for LLM APIs. Supply your provider keys with each request, and the router picks the best provider &lt;em&gt;for that task&lt;/em&gt;, streams OpenAI-compatible responses back, retries and fails over automatically, and — when sessions get long — &lt;strong&gt;compresses context by 85–95%&lt;/strong&gt; without losing continuity.&lt;/p&gt;

&lt;p&gt;One OpenAI-compatible endpoint. No per-token markup. Drop-in SDKs for Python and JavaScript/TypeScript in three lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-line drop-in
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ai-auto-router-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Explain raft consensus in 2 sentences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;provider_keys&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;openai&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;sk-...&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;sk-ant-...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript / TypeScript
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hushamdev/ai-auto-router-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Explain raft consensus in 2 sentences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;providerKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_API_KEY&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both SDKs are built purely on the standard library — no &lt;code&gt;httpx&lt;/code&gt;, no &lt;code&gt;axios&lt;/code&gt;, no heavy runtime. Apache-2.0 licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Multi-provider routing with automatic failover
&lt;/h3&gt;

&lt;p&gt;Route across &lt;strong&gt;OpenAI, Anthropic, Google Gemini, Groq, Mistral, xAI, DeepSeek, OpenRouter&lt;/strong&gt; — and your own self-hosted endpoints — through a single OpenAI-compatible API. The router continuously scores each provider on &lt;strong&gt;cost, latency, reliability, capacity and quality&lt;/strong&gt;, and automatically fails over when a provider rate-limits or errors out. You don't write the retry loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hardened transport
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wall-clock request deadlines so a hung provider can't stall your app&lt;/li&gt;
&lt;li&gt;Transient-error retries with exponential backoff&lt;/li&gt;
&lt;li&gt;Rate-limit-aware capacity scoring read from provider response headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Streaming, OpenAI-style
&lt;/h3&gt;

&lt;p&gt;Pass &lt;code&gt;stream: true&lt;/code&gt; and receive standard OpenAI-style SSE chunks plus a final usage/cost event — so your front-end keeps working even when the router picks a different backend mid-stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Semantic context compression
&lt;/h3&gt;

&lt;p&gt;For long agent / chat sessions, the router detects when your message tree approaches the model's context window and &lt;strong&gt;automatically compresses older history into a compact restore point&lt;/strong&gt;, cutting input tokens by 85–95% while letting a different model pick up the conversation seamlessly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Custom providers — route to &lt;em&gt;any&lt;/em&gt; OpenAI-compatible endpoint
&lt;/h3&gt;

&lt;p&gt;Add &lt;strong&gt;Ollama, vLLM, LM Studio, or a private LLM appliance&lt;/strong&gt; at runtime. They participate in the same scoring / failover pipeline as the cloud providers. Your local models, routed intelligently next to your frontier keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Observability &amp;amp; governance
&lt;/h3&gt;

&lt;p&gt;Prometheus metrics, structured logging, health checks, request tracing, and a built-in encrypted key vault so provider keys stay in scope, in memory, and never on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You're running &lt;strong&gt;multiple LLM providers&lt;/strong&gt; and want one stable API for your app&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;agent / long-chat products&lt;/strong&gt; are burning through token budgets&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;local models&lt;/strong&gt; (Ollama, vLLM) in the same routing pool as the cloud&lt;/li&gt;
&lt;li&gt;You need a &lt;strong&gt;BYOK layer&lt;/strong&gt; so your customers' keys never touch your servers&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;route on real cost/latency/health&lt;/strong&gt; instead of hard-coding a provider&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python SDK&lt;/strong&gt; (Apache-2.0): &lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;&lt;code&gt;ai-auto-router-client&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS/TS SDK&lt;/strong&gt; (Apache-2.0): &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@hushamdev/ai-auto-router-sdk&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product &amp;amp; pricing&lt;/strong&gt;: &lt;a href="https://sentiogate.app/" rel="noopener noreferrer"&gt;sentiogate.app&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running OpenAI + a fallback today, or your token bill is out of control on long agent sessions, the SDKs are two install commands away.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Planning a follow-up on the **semantic compaction algorithm&lt;/em&gt;* and &lt;strong&gt;rate-limit-aware scoring&lt;/strong&gt; in detail — comment below and I'll prioritize whichever you want first.*&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Building a Rate-Limit-Aware LLM Router in Python: Retries, Backoff &amp; Capacity Scoring</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:40:34 +0000</pubDate>
      <link>https://dev.to/sentiogate/building-a-rate-limit-aware-llm-router-in-python-retries-backoff-capacity-scoring-37bl</link>
      <guid>https://dev.to/sentiogate/building-a-rate-limit-aware-llm-router-in-python-retries-backoff-capacity-scoring-37bl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Rate-limit-aware routing - circuit breaker failover" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your app has ever fallen over because a provider returned &lt;code&gt;429 Too Many Requests&lt;/code&gt;, and your fallback was "sleep a second and try again", you know the pain. That pattern hurts twice: you hit the provider again while it's already telling you to slow down, and you still don't know how long to wait.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; partly because I kept writing that loop. Here's how it handles rate limits, and how to use it from Python or JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a 429 is information
&lt;/h2&gt;

&lt;p&gt;A 429 isn't a random failure. It's the provider telling you something specific: &lt;em&gt;this model, on this key, is unavailable for a while.&lt;/em&gt; Often it even tells you how long, in a &lt;code&gt;Retry-After&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Most retry code throws that information away. It re-sends the same request to the same model and hopes. A rate-limit-aware router does the opposite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remember&lt;/strong&gt; that this model on this key is throttled, and for how long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route around it&lt;/strong&gt; to the next healthy candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Come back&lt;/strong&gt; once the window has passed, carefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What SentioGate does about it
&lt;/h2&gt;

&lt;p&gt;SentioGate is an OpenAI-compatible router. You send a chat request with &lt;code&gt;model: "auto"&lt;/code&gt; and your own provider keys (BYOK), or use the free community pool, and it picks a model for you. Rate limits are part of that decision, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic cooldowns
&lt;/h3&gt;

&lt;p&gt;When a provider throttles a request, the hosted service puts &lt;strong&gt;that model on that key&lt;/strong&gt; on a cooldown. It follows the provider's own &lt;code&gt;Retry-After&lt;/code&gt; when there is one, so the cooldown lasts as long as the provider asked, within sensible bounds. During the cooldown, that model simply isn't a candidate, and your request moves on without waiting.&lt;/p&gt;

&lt;p&gt;Other failures are handled on their own terms. A key that's out of credits, or a model your key can't access, is set aside instead of being retried over and over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Circuit breakers
&lt;/h3&gt;

&lt;p&gt;Separately from cooldowns, a model that keeps failing gets its breaker tripped, and it's skipped for a while. Breaker state is shared across the whole edge, so once one request finds a sick model, everyone else's requests avoid it too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback to the next healthy model
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;model: "auto"&lt;/code&gt;, SentioGate ranks candidates on capability, price, and measured latency and success rate. Models that are cooling down or tripped drop out of the ranking. If the chosen model fails, the request falls through to the next healthy one. For streams, that happens before the first token reaches you.&lt;/p&gt;

&lt;p&gt;If every candidate is unavailable, you don't get a hang. You get a clear &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; telling you when to try again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The community pool: careful recovery
&lt;/h3&gt;

&lt;p&gt;The free community pool is shared by everyone, so it's handled with extra care. When the upstream throttles it, the pool backs off for as long as the upstream asked. When that window ends, it checks the upstream cautiously before letting traffic flow again. That way, a crowd of waiting clients doesn't stampede the provider the instant the window opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queue instead of 429
&lt;/h3&gt;

&lt;p&gt;Some workloads would rather wait than fail. When the shared community pool is full or rate limited, a non-streaming request can opt in to queueing with one header:&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://api.sentiogate.dev/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;$SENTIOGATE_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;"Content-Type: application/json"&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-SentioGate-Allow-Queue: true"&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": "auto", "messages": [{"role": "user", "content": "Summarise this ticket..."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a 429, you get a &lt;code&gt;202&lt;/code&gt; with a &lt;code&gt;job_id&lt;/code&gt; and a &lt;code&gt;poll_url&lt;/code&gt; to collect the result later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it from Python
&lt;/h2&gt;

&lt;p&gt;Install the SDK (the SDKs are published as &lt;code&gt;ai-auto-router&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ai-auto-router-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A basic call, with your own key:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# defaults to https://api.sentiogate.dev
&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Explain idempotency keys in two sentences.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;provider_keys&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;openai&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;sk-...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server already does the routing and fallback, so &lt;strong&gt;the SDK doesn't retry by default&lt;/strong&gt;. If you want a client-side safety net, opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ai_auto_router_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoRouterClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RouterClientError&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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_retries&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;max_retry_wait&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&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;RouterClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Still limited after the retries you allowed: you get the code and how long to wait.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opt-in retries only kick in for transient 429s, only after waiting the server's &lt;code&gt;Retry-After&lt;/code&gt;, and never when that wait is longer than your &lt;code&gt;max_retry_wait&lt;/code&gt;. There's no hot loop and no blind sleep.&lt;/p&gt;

&lt;p&gt;To queue on the community pool from Python, pass &lt;code&gt;allow_queue&lt;/code&gt; and poll the job:&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Classify this support ticket...&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_body&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;allow_queue&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&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;status&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;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;job_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_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;while&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;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&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;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&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;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;break&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;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&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="n"&gt;job&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And from JavaScript
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hushamdev/ai-auto-router-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxRetryWaitMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;providerKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stream me a haiku&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For streams, opt-in retries only apply before the first chunk arrives, so you never get a half-repeated answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting
&lt;/h2&gt;

&lt;p&gt;There's also a self-hosted Python router that ranks providers using their rate-limit headers, retries transient failures with backoff, and falls back down a chain. Point the SDK's &lt;code&gt;base_url&lt;/code&gt; / &lt;code&gt;baseUrl&lt;/code&gt; at it. The source isn't public yet; if you need it on your own infra, get in touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical advice, whatever you use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/strong&gt; It's the single most useful header in a 429. Handle both forms: seconds and an HTTP date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound every wait.&lt;/strong&gt; Don't trust a &lt;code&gt;0&lt;/code&gt; from a flaky upstream, and don't let one header stall a request for an hour. Clamp what you get.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back before you retry.&lt;/strong&gt; If another model can do the job, switching is almost always faster than waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry in one layer only.&lt;/strong&gt; If your router retries and your HTTP client retries and your app retries, one 429 turns into a burst. That's why SentioGate's SDKs leave retries off unless you ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover gently.&lt;/strong&gt; When a window opens, send one request first, not everything you queued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection-level retry middleware isn't enough.&lt;/strong&gt; It re-sends the same body to the same URL; it can't decide to pick another model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; · &lt;a href="https://sentiogate.app/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;: the free community pool and the $9 Pro plan&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of a series: start with the &lt;a href="https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e"&gt;intro post&lt;/a&gt;, and find the others on the &lt;a href="https://dev.to/sentiogate"&gt;SentioGate dev.to page&lt;/a&gt;. Tell me below what to cover next.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Ollama, vLLM &amp; Private LLMs: Routing Your Own Models Next to Your Cloud Keys</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:31:12 +0000</pubDate>
      <link>https://dev.to/sentiogate/ollama-vllm-private-llms-routing-self-hosted-models-next-to-your-cloud-keys-c66</link>
      <guid>https://dev.to/sentiogate/ollama-vllm-private-llms-routing-self-hosted-models-next-to-your-cloud-keys-c66</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Self-hosted models, routed next to the cloud" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You have cloud keys in your app, and a GPU box in the corner running vLLM or Ollama. Here's how &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; puts both behind one endpoint, with one request format, one stream format and one dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A question I get a lot: "I use the cloud for the hard tasks, but I have my own box for the easy stuff. Can one router handle both, without a separate integration for each?"&lt;/p&gt;

&lt;p&gt;Yes, as long as your box speaks the OpenAI chat-completions API. vLLM, Ollama, LM Studio and most private inference appliances do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just call the API"
&lt;/h2&gt;

&lt;p&gt;Most teams end up with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_difficulty&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;      &lt;span class="c1"&gt;# cloud
&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;# local
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you two SDKs, two error shapes, two streaming formats, no failover and no shared telemetry. Every new model means another branch, and every outage means a code change.&lt;/p&gt;

&lt;p&gt;A router fixes the plumbing: one endpoint, one request format, one stream format, and one place to see what ran and what it cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering your endpoint
&lt;/h2&gt;

&lt;p&gt;In SentioGate, your own endpoint is a &lt;strong&gt;custom provider&lt;/strong&gt;. It's a &lt;code&gt;provider_id&lt;/code&gt;, a &lt;code&gt;base_url&lt;/code&gt;, an &lt;code&gt;api_key&lt;/code&gt;, and the list of model IDs it serves.&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;-X&lt;/span&gt; POST https://api.sentiogate.dev/v1/custom-providers &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;$SENTIOGATE_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;"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;'{
    "provider_id": "my-vllm",
    "display_name": "My vLLM",
    "base_url": "https://llm.example.com/v1",
    "api_key": "YOUR_ENDPOINT_KEY",
    "models": ["llama3.1:8b", "qwen2.5:14b"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make it reachable.&lt;/strong&gt; The hosted service calls your model from the edge, so &lt;code&gt;base_url&lt;/code&gt; must be reachable from the internet. A URL like &lt;code&gt;http://localhost:11434&lt;/code&gt; or a LAN hostname won't work. Put the endpoint behind HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect it.&lt;/strong&gt; An inference server on the open internet should require a key. vLLM supports &lt;code&gt;--api-key&lt;/code&gt;, and for Ollama a small reverse proxy that checks a bearer token does the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the credential in &lt;code&gt;api_key&lt;/code&gt;.&lt;/strong&gt; It's encrypted at rest with AES-256-GCM, like every other key in your Vault, and decrypted in memory only when SentioGate calls your endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calling your models
&lt;/h2&gt;

&lt;p&gt;Once registered, you can call a model explicitly as &lt;code&gt;provider_id/model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="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;Summarise this ticket in one line: ...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarise this ticket in one line: ...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your models also become candidates for &lt;code&gt;"model": "auto"&lt;/code&gt;, right next to the cloud models your keys can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;auto&lt;/code&gt; treats your models
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;auto&lt;/code&gt;, SentioGate picks a model per request from everything you've enabled. It looks at whether a model is capable enough for the task, whether the conversation fits its context window, what it costs, and how fast and reliable it has been recently. That last part is measured, not configured: if your box slows down or starts erroring, it drops down the list, and requests fall back to the next candidate before any bytes reach your client.&lt;/p&gt;

&lt;p&gt;You stay in control of the pool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want your own model as the primary?&lt;/strong&gt; Call it explicitly (&lt;code&gt;my-vllm/...&lt;/code&gt;), or enable only your own models in the Vault so &lt;code&gt;auto&lt;/code&gt; chooses among them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want cloud and local side by side?&lt;/strong&gt; Enable both, and &lt;code&gt;auto&lt;/code&gt; picks per request across the whole pool, with fallback between providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want guardrails?&lt;/strong&gt; Your policy (enabled models and spend caps) applies to every request, whichever provider serves it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you never have to guess what happened. Every response says which model was picked and why, in the &lt;code&gt;router&lt;/code&gt; block, or in the &lt;code&gt;X-SentioGate-Router&lt;/code&gt; header when streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else in the pipeline still applies
&lt;/h2&gt;

&lt;p&gt;Your models go through the same request path as the cloud ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming:&lt;/strong&gt; standard OpenAI-style SSE, so your front end doesn't care where the tokens came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback:&lt;/strong&gt; on &lt;code&gt;auto&lt;/code&gt; requests, a failed candidate moves on to the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context compression:&lt;/strong&gt; long chats are kept inside a token budget, exactly as for cloud models, and &lt;code&gt;router.compression&lt;/code&gt; in the response shows what was saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry:&lt;/strong&gt; provider, model, latency and tokens show up in the same dashboard as everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streaming from your own model looks exactly like streaming from OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-vllm/qwen2.5:14b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a haiku about GPUs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Python: &lt;code&gt;router.stream_chat(...)&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The hybrid pattern
&lt;/h2&gt;

&lt;p&gt;The setup I'd recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud models&lt;/strong&gt; for the tasks that need them: hard reasoning and long context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own models&lt;/strong&gt;, called explicitly for the workloads you know they handle well, such as classification, extraction, short summaries and internal tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/strong&gt; wherever you want SentioGate to choose per request and fail over automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can mix all three in one app with one client, and pass cloud keys per request (&lt;code&gt;provider_keys&lt;/code&gt; / &lt;code&gt;providerKeys&lt;/code&gt;) if you'd rather not store them. Keys passed that way are used for that request only.&lt;/p&gt;

&lt;p&gt;It's a simple mental model: explicit when you know what you want, &lt;code&gt;auto&lt;/code&gt; when you want the router to decide, and one response format either way.&lt;/p&gt;

&lt;p&gt;Want everything, router included, on your own infrastructure? The source isn't public yet, so get in touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;SentioGate&lt;/a&gt; · &lt;a href="https://sentiogate.app/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; (&lt;code&gt;ai-auto-router-client&lt;/code&gt;) · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt; (&lt;code&gt;@hushamdev/ai-auto-router-sdk&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running a local box next to your cloud keys? Tell me in the comments what you serve on it. I'm curious which workloads people keep on their own hardware.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How I Cut Long-Chat Token Bills by 85-95% With Semantic Context Compression</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:30:47 +0000</pubDate>
      <link>https://dev.to/sentiogate/how-i-cut-long-chat-token-bills-by-85-95-with-semantic-context-compression-57l4</link>
      <guid>https://dev.to/sentiogate/how-i-cut-long-chat-token-bills-by-85-95-with-semantic-context-compression-57l4</guid>
      <description>&lt;h1&gt;
  
  
  How I Cut Long-Chat Token Bills by 85–95% With Semantic Context Compression
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="Semantic context compression - 85-95% token reduction" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical engineering post on semantic context compression for LLM agents — why "sliding window" truncation breaks agents, how a router detects the threshold, and what a good restore point looks like.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've built an agent that runs for hours or spans hundreds of turns, you know the bill: every turn you re-send the whole conversation to the model. Token cost grows linearly with session length, and eventually you hit the context window and the whole thing breaks.&lt;/p&gt;

&lt;p&gt;The usual fix — &lt;strong&gt;sliding-window truncation&lt;/strong&gt; ("keep the last N messages") — is terrible for agents. You throw away the plan you made in turn 3, the user's stated constraints from turn 10, and the decision you made at turn 47. The model starts repeating itself, contradicting earlier work, and the user notices immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic context compression&lt;/strong&gt; is the fix. And it's one of the features I care about most in &lt;a href="https://sentiogate.app" rel="noopener noreferrer"&gt;&lt;strong&gt;SentioGate&lt;/strong&gt;&lt;/a&gt;, the BYOK LLM router I'm building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "semantic compression" means (vs. naive truncation)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What's kept&lt;/th&gt;
&lt;th&gt;What's lost&lt;/th&gt;
&lt;th&gt;Cost at turn 200&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sliding window (last 20 msgs)&lt;/td&gt;
&lt;td&gt;Recent chatter&lt;/td&gt;
&lt;td&gt;Plan, constraints, decisions&lt;/td&gt;
&lt;td&gt;High — model re-derives context every turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive "summarize older msgs"&lt;/td&gt;
&lt;td&gt;A text blob&lt;/td&gt;
&lt;td&gt;Structured state (files, tools, user intent)&lt;/td&gt;
&lt;td&gt;Medium — summary drifts, loses key facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic compression + restore point&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured state: goals, files, tool calls, user intent, open threads&lt;/td&gt;
&lt;td&gt;Verbose intermediate chatter&lt;/td&gt;
&lt;td&gt;Low — the compact state carries 90%+ of the useful signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: a long conversation isn't a bag of text. It's &lt;strong&gt;state&lt;/strong&gt; — goals, constraints, decisions, open tasks, files being edited, tools being called. If you compress the &lt;em&gt;state&lt;/em&gt; rather than the &lt;em&gt;prose&lt;/em&gt;, you get 85–95% token reduction with near-zero loss of continuity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the router detects the moment to compact
&lt;/h2&gt;

&lt;p&gt;The router watches each session's message tree. Two signals trigger compaction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capacity threshold&lt;/strong&gt; — when the session's context reaches a configured fraction of the model's context window (e.g., 75%), compact before the next turn overflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token threshold&lt;/strong&gt; — when the cumulative input tokens for the session cross a budget, compact proactively even if you're far from the window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The compaction preserves the most recent N messages verbatim (default: 4) and collapses everything older into a structured &lt;strong&gt;restore point&lt;/strong&gt; — a compact, machine-readable summary of the session's state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the restore point looks like
&lt;/h2&gt;

&lt;p&gt;The restore point isn't a free-text summary. It's a structured document with slots for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goals&lt;/strong&gt; — what the agent is trying to accomplish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User intent &amp;amp; constraints&lt;/strong&gt; — anything the user explicitly asked for or ruled out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files / resources&lt;/strong&gt; — files being edited, configs, data sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool state&lt;/strong&gt; — which tools have been called, what's pending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions made&lt;/strong&gt; — choices that affect downstream turns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open threads&lt;/strong&gt; — things not yet resolved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a new model (or a fresh session) picks up the conversation, it reads the restore point instead of the full history. Input tokens drop from, say, 60,000 to 4,000–8,000, and the model has everything it needs to continue without re-deriving context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for model-hopping
&lt;/h2&gt;

&lt;p&gt;A big unlock: because the restore point is model-agnostic, you can &lt;strong&gt;switch models mid-conversation&lt;/strong&gt; — finish a planning turn with a frontier model, then hand the execution to a cheaper / faster model, then back to the frontier model for a final review. Each handoff reads the same compact state. No re-reading of full history.&lt;/p&gt;

&lt;p&gt;That's the "seamless model-hopping" I mentioned in my &lt;a href="https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e"&gt;intro post to SentioGate&lt;/a&gt;. The compaction engine is what makes it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch for (and what I'm still iterating on)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loss of rare-but-critical facts.&lt;/strong&gt; The rare user message that's important later ("no, don't touch the prod DB") can get lost in compression if it's buried. The preserve-recent-N window helps, but I'm working on fact-extraction heuristics that flag "constraint" messages regardless of recency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-session continuity.&lt;/strong&gt; Today, compaction is per-session. Multi-session agents (a "planner" session feeding an "executor" session) need a shared compact state. That's on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality scoring of the compact output.&lt;/strong&gt; The restore point is only as good as the model that wrote it. I'm adding a lightweight QA pass that checks the compact against the original for the most critical facts before it's used.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number I'm most proud of
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;85–95% input-token reduction&lt;/strong&gt; on long sessions, measured across real agent workloads, with no measurable drop in task-completion quality. That's the entire value proposition of the compression engine in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sentiogate.app/" rel="noopener noreferrer"&gt;SentioGate — the BYOK LLM router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;JS/TS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing — 0% markup, BYOK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If you're building long-running agents and your token bill is a problem, drop a comment — I'll follow up with a post on the exact scoring algorithm and how to tune the compaction thresholds for your workloads.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Stop Juggling 7 LLM API Keys: One BYOK Router That Ranks, Streams &amp; Compacts for You</title>
      <dc:creator>SentioGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:22:06 +0000</pubDate>
      <link>https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e</link>
      <guid>https://dev.to/sentiogate/stop-juggling-7-llm-api-keys-one-byok-router-that-ranks-streams-compacts-for-you-41e</guid>
      <description>&lt;h1&gt;
  
  
  Stop Juggling 7 LLM API Keys: One BYOK Router That Ranks, Streams &amp;amp; Compacts for You
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw35dpcxqwa46obeinjg8.png" alt="SentioGate - the BYOK LLM router" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SentioGate&lt;/strong&gt; is a production-ready &lt;strong&gt;BYOK (Bring Your Own Key) routing layer&lt;/strong&gt; for LLM APIs. Supply your provider keys with each request, and the router picks the best provider &lt;em&gt;for that task&lt;/em&gt;, streams OpenAI-compatible responses back, retries and fails over automatically, and — when sessions get long — &lt;strong&gt;compresses context by 85–95%&lt;/strong&gt; without losing continuity.&lt;/p&gt;

&lt;p&gt;One OpenAI-compatible endpoint. No per-token markup. Drop-in SDKs for Python and JavaScript/TypeScript in three lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-line drop-in
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ai-auto-router-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AutoRouterClient&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg_live_...&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="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;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;Explain raft consensus in 2 sentences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;provider_keys&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;openai&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;sk-...&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;sk-ant-...&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;print&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript / TypeScript
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hushamdev/ai-auto-router-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AutoRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hushamdev/ai-auto-router-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AutoRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTIOGATE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Explain raft consensus in 2 sentences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;providerKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_API_KEY&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both SDKs are built purely on the standard library — no &lt;code&gt;httpx&lt;/code&gt;, no &lt;code&gt;axios&lt;/code&gt;, no heavy runtime. Apache-2.0 licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Multi-provider routing with automatic failover
&lt;/h3&gt;

&lt;p&gt;Route across &lt;strong&gt;OpenAI, Anthropic, Google Gemini, Groq, Mistral, xAI, DeepSeek, OpenRouter&lt;/strong&gt; — and your own self-hosted endpoints — through a single OpenAI-compatible API. The router continuously scores each provider on &lt;strong&gt;cost, latency, reliability, capacity and quality&lt;/strong&gt;, and automatically fails over when a provider rate-limits or errors out. You don't write the retry loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hardened transport
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wall-clock request deadlines so a hung provider can't stall your app&lt;/li&gt;
&lt;li&gt;Transient-error retries with exponential backoff&lt;/li&gt;
&lt;li&gt;Rate-limit-aware capacity scoring read from provider response headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Streaming, OpenAI-style
&lt;/h3&gt;

&lt;p&gt;Pass &lt;code&gt;stream: true&lt;/code&gt; and receive standard OpenAI-style SSE chunks plus a final usage/cost event — so your front-end keeps working even when the router picks a different backend mid-stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Semantic context compression
&lt;/h3&gt;

&lt;p&gt;For long agent / chat sessions, the router detects when your message tree approaches the model's context window and &lt;strong&gt;automatically compresses older history into a compact restore point&lt;/strong&gt;, cutting input tokens by 85–95% while letting a different model pick up the conversation seamlessly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Custom providers — route to &lt;em&gt;any&lt;/em&gt; OpenAI-compatible endpoint
&lt;/h3&gt;

&lt;p&gt;Add &lt;strong&gt;Ollama, vLLM, LM Studio, or a private LLM appliance&lt;/strong&gt; at runtime. They participate in the same scoring / failover pipeline as the cloud providers. Your local models, routed intelligently next to your frontier keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Observability &amp;amp; governance
&lt;/h3&gt;

&lt;p&gt;Prometheus metrics, structured logging, health checks, request tracing, and a built-in encrypted key vault so provider keys stay in scope, in memory, and never on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You're running &lt;strong&gt;multiple LLM providers&lt;/strong&gt; and want one stable API for your app&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;agent / long-chat products&lt;/strong&gt; are burning through token budgets&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;local models&lt;/strong&gt; (Ollama, vLLM) in the same routing pool as the cloud&lt;/li&gt;
&lt;li&gt;You need a &lt;strong&gt;BYOK layer&lt;/strong&gt; so your customers' keys never touch your servers&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;route on real cost/latency/health&lt;/strong&gt; instead of hard-coding a provider&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python SDK&lt;/strong&gt; (Apache-2.0): &lt;a href="https://pypi.org/project/ai-auto-router-client/" rel="noopener noreferrer"&gt;&lt;code&gt;ai-auto-router-client&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS/TS SDK&lt;/strong&gt; (Apache-2.0): &lt;a href="https://www.npmjs.com/package/@hushamdev/ai-auto-router-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@hushamdev/ai-auto-router-sdk&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product &amp;amp; pricing&lt;/strong&gt;: &lt;a href="https://sentiogate.app/" rel="noopener noreferrer"&gt;sentiogate.app&lt;/a&gt; · &lt;a href="https://sentiogate.app/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running OpenAI + a fallback today, or your token bill is out of control on long agent sessions, the SDKs are two install commands away.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Planning a follow-up on the **semantic compaction algorithm&lt;/em&gt;* and &lt;strong&gt;rate-limit-aware scoring&lt;/strong&gt; in detail — comment below and I'll prioritize whichever you want first.*&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
      <category>openai</category>
    </item>
  </channel>
</rss>
