<?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: Tanbir Ramim</title>
    <description>The latest articles on DEV Community by Tanbir Ramim (@tanbirramim).</description>
    <link>https://dev.to/tanbirramim</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4128103%2F0d6ecdf9-65aa-47cf-a298-8f2b30beaaa3.png</url>
      <title>DEV Community: Tanbir Ramim</title>
      <link>https://dev.to/tanbirramim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanbirramim"/>
    <language>en</language>
    <item>
      <title>I built a small Python library to add retries, caching, fallbacks, budgets, and guardrails around native LLM SDK calls</title>
      <dc:creator>Tanbir Ramim</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:54:33 +0000</pubDate>
      <link>https://dev.to/tanbirramim/i-built-a-small-python-library-to-add-retries-caching-fallbacks-budgets-and-guardrails-around-f75</link>
      <guid>https://dev.to/tanbirramim/i-built-a-small-python-library-to-add-retries-caching-fallbacks-budgets-and-guardrails-around-f75</guid>
      <description>&lt;p&gt;I got tired of writing the same LLM boilerplate in every project, so I made a library&lt;/p&gt;

&lt;p&gt;Three lines to call an LLM in a prototype. Then you go to production and suddenly you're writing the same 200 lines you wrote last time:&lt;/p&gt;

&lt;p&gt;Retry logic because OpenAI throws 429s at you. Cost tracking because someone left a loop running and burned $40 on a Saturday. Caching because your support bot answers "how do I reset my password?" eight hundred times a day and you're paying for each one. PII scrubbing because customer emails keep showing up in prompts. Output parsing because the model returns markdown when you asked for JSON, and now your frontend is on fire.&lt;/p&gt;

&lt;p&gt;I kept copy-pasting this stuff between projects until I finally pulled it into a library: callm.&lt;/p&gt;

&lt;p&gt;It's just a decorator&lt;/p&gt;

&lt;p&gt;I didn't want to learn a new client or replace the SDK. The whole idea is that you keep writing normal OpenAI/Anthropic code and slap a decorator on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

python
import openai
from pydantic import BaseModel
from callm import callm

client = openai.OpenAI(max_retries=0)

class Summary(BaseModel):
    title: str
    bullets: list[str]

@callm(
    cache=True,
    retry=3,
    fallback=["anthropic/claude-sonnet-5"],
    max_cost=0.25,
    block_pii=True,
    detect_injection=True,
    output_schema=Summary,
)
def summarize(text: str):
    return client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": text}],
    )

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You call summarize(), you get back a validated Summary. That's it. Outside the decorator, your SDK works exactly like before — nothing monkey-patched, nothing weird.&lt;/p&gt;

&lt;p&gt;What's actually going on under the hood&lt;/p&gt;

&lt;p&gt;When your function runs, the call passes through a stack of middleware:&lt;/p&gt;

&lt;p&gt;Security first — prompt injection scoring, then PII gets masked (&lt;a href="mailto:jane@acme.com"&gt;jane@acme.com&lt;/a&gt; → [EMAIL_1]). This happens before anything hits the cache or the network.&lt;br&gt;
Cache check — exact-match lookup based on the masked request, params, and schema.&lt;br&gt;
Validation — response gets parsed into your Pydantic model. If it fails, the model gets called again with the validation errors attached (usually fixes it on the second try).&lt;br&gt;
Fallback — if OpenAI is just not having it today, the request gets translated and sent to Claude instead. Your code still gets back an OpenAI ChatCompletion object, so nothing breaks downstream.&lt;br&gt;
Cost guard — estimated cost is checked against max_cost before the request actually goes out. No more surprise bills.&lt;br&gt;
Retry — exponential backoff with jitter, and it actually reads the provider's rate-limit headers instead of guessing.&lt;/p&gt;

&lt;p&gt;Everything gets logged locally (tokens, cost, cache hits, retries — never your actual prompts), and you can run callm stats to see what you're spending per provider, model, or function.&lt;/p&gt;

&lt;p&gt;A couple of design choices I want to explain&lt;/p&gt;

&lt;p&gt;The cache is exact-match on purpose. I know semantic caching sounds cool, but think about it: "Summarize &lt;a href="https://example.com/post-1" rel="noopener noreferrer"&gt;https://example.com/post-1&lt;/a&gt;" and "Summarize &lt;a href="https://example.com/post-2" rel="noopener noreferrer"&gt;https://example.com/post-2&lt;/a&gt;" look almost identical to an embedding model. A semantic cache would cheerfully hand you the wrong summary and you wouldn't notice for a while. callm does support semantic matching if you want it, but you have to opt in, and it'll never match across different system prompts or conversation histories.&lt;/p&gt;

&lt;p&gt;Fallback won't silently break your request. If you're using tools, images, or a structured response format, callm will only fall back to another model from the same provider. Translating tool schemas across providers is a can of worms, and I'd rather fail loudly than give you a subtly wrong result.&lt;/p&gt;

&lt;p&gt;"Cool, but does it add latency?"&lt;/p&gt;

&lt;p&gt;The repo has an offline benchmark that runs the real OpenAI SDK against a fake server:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What I measured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Overhead per call (default settings)    +0.06 ms&lt;br&gt;
Cost savings with cache, FAQ-style traffic  91% lower&lt;br&gt;
Cost savings with cache, mostly unique prompts  2% lower&lt;br&gt;
Success rate with 20% random 503s: plain SDK → retry=2 → + fallback 79.9% → 99.4% → 100%&lt;/p&gt;

&lt;p&gt;The cache numbers are the obvious takeaway: caching is huge if your requests repeat, and basically irrelevant if they don't. Run callm stats on your own traffic before you count on those savings.&lt;/p&gt;

&lt;p&gt;Give it a spin&lt;br&gt;
bash&lt;br&gt;
pip install "callm-toolkit[openai,anthropic,validation]"&lt;/p&gt;

&lt;p&gt;There's an offline demo in the repo (examples/offline_demo.py) that walks through a retry, a cache hit, a fallback, a blocked expensive call, and a flagged injection — no API key needed.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/TanbirRamim/callm" rel="noopener noreferrer"&gt;https://github.com/TanbirRamim/callm&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://tanbirramim.github.io/callm/" rel="noopener noreferrer"&gt;https://tanbirramim.github.io/callm/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Author: Tanbir Hossain Ramim&lt;/p&gt;

&lt;p&gt;It's MIT-licensed and pretty new. If you try it on a real workload and something breaks or feels off, open an issue — that kind of feedback is exactly what I need right now.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdlvsv5imozfr2mlg6ve.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsdlvsv5imozfr2mlg6ve.gif" alt=" " width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

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