<?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: Sergey Inozemtsev</title>
    <description>The latest articles on DEV Community by Sergey Inozemtsev (@inozem).</description>
    <link>https://dev.to/inozem</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%2F2431738%2Fe9e02497-8195-4b93-98e8-fe2be2ae4a88.png</url>
      <title>DEV Community: Sergey Inozemtsev</title>
      <link>https://dev.to/inozem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inozem"/>
    <language>en</language>
    <item>
      <title>Gemini returned 429 at 2.3% of its documented quota. My retry loop ran for 4 days.</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:33:06 +0000</pubDate>
      <link>https://dev.to/inozem/gemini-returned-429-at-23-of-its-documented-quota-my-retry-loop-ran-for-4-days-18f7</link>
      <guid>https://dev.to/inozem/gemini-returned-429-at-23-of-its-documented-quota-my-retry-loop-ran-for-4-days-18f7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Documented quota: 5,000 RPM&lt;/li&gt;
&lt;li&gt;Actual load at peak (from Cloud Console): ~115 RPM (2.3% of quota)&lt;/li&gt;
&lt;li&gt;Budget: $6/month&lt;/li&gt;
&lt;li&gt;Cost after 4 days: ~$90&lt;/li&gt;
&lt;li&gt;Root cause: Gemini applying quota limits from the wrong service — bug confirmed in forum threads going back to September 2025&lt;/li&gt;
&lt;li&gt;Fix: batch embeddings + API rate caps&lt;/li&gt;
&lt;li&gt;Cost after fix: back to $6/month&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What TLBrain is
&lt;/h2&gt;

&lt;p&gt;TLBrain is a RAG memory system I built for my wife's ghostwriting business. She conducts 10–15 client calls per week. The information was getting lost between sessions — Claude couldn't remember clients across conversations.&lt;/p&gt;

&lt;p&gt;The solution: record calls via TL;DV → auto-transcribe → sync to a vector database → query via a remote MCP server connected to Claude Cowork.&lt;/p&gt;

&lt;p&gt;The sync pipeline looks like this:&lt;/p&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%2F1jxds2krkdq1qvypeodn.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%2F1jxds2krkdq1qvypeodn.png" alt="GCP Quota Console — gemini-embedding-2 peak usage 2.3% of 5,000 RPM quota while 429 errors were firing" width="619" height="829"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cloud Tasks handles the queue. Each transcript goes through a Firestore state machine: &lt;code&gt;queued → downloading → imported → syncing → synced&lt;/code&gt;. If a task fails, Cloud Tasks retries it automatically.&lt;/p&gt;

&lt;p&gt;That retry behavior is important. It's what turned a quota bug into a ~$90 incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline ran fine for a month
&lt;/h2&gt;

&lt;p&gt;200+ conversations indexed. ~$44 total (one-time backfill). Ongoing cost: $6/month.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;gemini-embedding-2&lt;/code&gt; started returning 429s.&lt;/p&gt;




&lt;h2&gt;
  
  
  The failure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 RESOURCE_EXHAUSTED: Quota exceeded for quota metric 
'aiplatform.googleapis.com/global_embed_content_requests_per_minute_per_base_model'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline kept running — Cloud Tasks kept enqueuing jobs, Sync Service kept picking them up. But embeddings were silently failing. New transcripts weren't being indexed. Search degraded. My wife noticed that Claude stopped remembering recent calls.&lt;/p&gt;

&lt;p&gt;The first sign something was wrong: search results were getting stale.&lt;/p&gt;




&lt;h2&gt;
  
  
  First diagnosis: wrong
&lt;/h2&gt;

&lt;p&gt;My first assumption — I'm hitting the rate limit. I reduced parallel sync containers from 2 to 1. Still 429.&lt;/p&gt;

&lt;p&gt;I checked Gemini's documented quota: &lt;strong&gt;5,000 RPM&lt;/strong&gt; for &lt;code&gt;gemini-embedding-2&lt;/code&gt;. Cloud Console showed peak usage at &lt;strong&gt;2.3% of quota — ~115 RPM&lt;/strong&gt;. Even with one container, nowhere near the limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;I opened Google Cloud Console to check my actual usage metrics.&lt;/p&gt;

&lt;p&gt;Near zero.&lt;/p&gt;

&lt;p&gt;429 quota errors. Near-zero usage shown in the dashboard. That contradiction ruled out my code. Something else was happening.&lt;/p&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%2Froyjgo9vvxqzztrve5lq.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%2Froyjgo9vvxqzztrve5lq.png" alt="TLBrain sync pipeline: TL;DV → Import Service → Cloud Tasks → Sync Service → Gemini LLM → gemini-embedding-2 → Qdrant and Firestore → MCP Server → Claude Cowork" width="551" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I searched for the exact error. Found forum threads going back to &lt;strong&gt;September 2025&lt;/strong&gt; — multiple developers, multiple Gemini models, same pattern: 429 at a fraction of the documented quota, dashboard showing minimal usage.&lt;/p&gt;

&lt;p&gt;That's when the error message clicked. Look at the quota metric name again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aiplatform.googleapis.com/global_embed_content_requests_per_minute_per_base_model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm calling the Gemini API — &lt;code&gt;generativelanguage.googleapis.com&lt;/code&gt;. But the quota being enforced belongs to &lt;code&gt;aiplatform.googleapis.com&lt;/code&gt; — Vertex AI. A completely different service with different (lower) limits.&lt;/p&gt;

&lt;p&gt;Gemini was routing my embedding requests through Vertex AI internally and applying Vertex AI quota limits against them. That's why my dashboard showed near-zero usage: the generativelanguage quota was barely touched. The Vertex AI quota was what was exhausted — and I had no visibility into that counter at all.&lt;/p&gt;

&lt;p&gt;The diagnosis: &lt;strong&gt;Gemini applying quota limits from the wrong service&lt;/strong&gt;. No official acknowledgment at the time of writing (9 months later).&lt;/p&gt;

&lt;p&gt;Forum threads reporting the same pattern, going back to September 2025 — no official fix as of the time of writing (9 months later):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discuss.ai.google.dev/t/critical-bug-paid-project-tier-1-but-stuck-on-free-tier-token-limit/110767" rel="noopener noreferrer"&gt;Dec 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.ai.google.dev/t/gemini-api-429-resource-exhausted-error-on-tier-1/114413" rel="noopener noreferrer"&gt;Jan 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.ai.google.dev/t/issue-persistent-429-resource-exhausted-on-paid-tier-1/131248" rel="noopener noreferrer"&gt;Mar 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why it cost ~$90: anatomy of the loop
&lt;/h2&gt;

&lt;p&gt;Here's where it gets expensive.&lt;/p&gt;

&lt;p&gt;Each Sync Service task processes a full transcript document window by window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each window:
    1. generate summary via Gemini LLM        ← billed ✓
    2. generate facts via Gemini LLM          ← billed ✓
    3. embed [summary + facts] via Gemini Embeddings  ← 429 ✗ → task fails
    → write to Qdrant + Firestore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 fails on the first window. Cloud Tasks sees the failure. Cloud Tasks retries the entire task from the beginning.&lt;/p&gt;

&lt;p&gt;Which means every retry restarted the loop from window 1 — regenerating summaries and facts for every window using Gemini LLM before even reaching embeddings again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[task starts]
  window 1: summary ← billed | facts ← billed | embed ← 429, task fails ✗
[Cloud Tasks retries]
  window 1: summary ← billed again | facts ← billed again | embed ← 429, task fails ✗
[repeat for 4 days]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some tasks had retried 50+ times before I noticed.&lt;/p&gt;

&lt;p&gt;I noticed after 4 days and stopped the sync service manually.&lt;/p&gt;

&lt;p&gt;The retry backoff at the time: seconds. Not minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Three changes. No exotic patterns — just things I should have had from the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Batch embeddings
&lt;/h3&gt;

&lt;p&gt;Instead of N separate &lt;code&gt;embed_content&lt;/code&gt; calls in a loop, one call with a list of all texts from the window.&lt;/p&gt;

&lt;p&gt;A transcript can have up to 400 utterances. Processing happens window by window — keeping everything in memory to batch at the transcript level would require 500MB+ containers. Not practical for Cloud Run.&lt;/p&gt;

&lt;p&gt;So the unit of batching is the window, not the transcript. For each window: generate summary + facts, then embed everything from that window in one request, then move on.&lt;/p&gt;

&lt;p&gt;Before the fix, each text was embedded separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: separate embed call per item
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_summary_and_facts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;summary_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# 1 call
&lt;/span&gt;    &lt;span class="n"&gt;fact_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# N calls
&lt;/span&gt;    &lt;span class="c1"&gt;# total: ~10 calls per window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After: all texts from one window go in a single batch call, then the next window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# After: one batch call per window
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_summary_and_facts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 1 call per window
&lt;/span&gt;    &lt;span class="c1"&gt;# memory released after each window
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed_content&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;gemini-embedding-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EmbedContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_dimensionality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same data, ~10x fewer API calls. Memory stays flat across the entire transcript.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. API rate cap on the Sync Service
&lt;/h3&gt;

&lt;p&gt;Added a hard limit on Gemini API calls per time window. If the sync service hits the cap, it stops processing new tasks — but the &lt;strong&gt;MCP server and search keep running&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the architectural separation doing its job: sync service going down doesn't affect the main service. My wife can still query Claude. New transcripts just aren't indexed until the cap resets.&lt;/p&gt;

&lt;p&gt;Set a daily request cap directly in Google AI Studio quota settings — no code changes needed. If the sync service hits the cap, it stops processing, but the MCP server and search keep running.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Increased backoff for 429 errors
&lt;/h3&gt;

&lt;p&gt;Changed retry behavior from seconds to minutes.&lt;/p&gt;

&lt;p&gt;If a quota error occurs, the next retry should be far enough away to matter — not a tight loop that amplifies the problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: [1, 2, 4] seconds   — applied to all errors
After:  [30, 60, 120] seconds — applied specifically to 429 errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quota reset cycles are per-minute. A 4-second backoff doesn't help — it just hammers the same exhausted counter.&lt;/p&gt;




&lt;h2&gt;
  
  
  After the fix
&lt;/h2&gt;

&lt;p&gt;4 transcripts had been stuck in the failed state during the incident. Restarted them manually. Zero 429s.&lt;/p&gt;

&lt;p&gt;Cost went back to $6/month.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Third-party services can silently break their own contracts.&lt;/strong&gt;&lt;br&gt;
I planned for my code failing. I didn't plan for Gemini's quota system misfiring. I do now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dashboard showing zero doesn't mean zero — it means the bug is external.&lt;/strong&gt;&lt;br&gt;
This was the key diagnostic step. When your usage metrics don't match the errors you're getting, the problem is outside your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Retry loops amplify cost asymmetrically.&lt;/strong&gt;&lt;br&gt;
The part that failed (embeddings) wasn't the expensive part. The part that ran successfully on every retry (LLM summarization) was. Always check what runs &lt;em&gt;before&lt;/em&gt; the failure point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Architectural isolation saved the main service.&lt;/strong&gt;&lt;br&gt;
Sync stopping didn't break search. The two services are separate Cloud Run instances. That decision — made before this incident — is the reason my wife could still use the tool while I fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Backoff in seconds for quota errors is wrong.&lt;/strong&gt;&lt;br&gt;
Quota reset cycles are measured in minutes. Retrying every 10 seconds doesn't help — it just burns through whatever quota you have left.&lt;/p&gt;




&lt;h2&gt;
  
  
  The refund
&lt;/h2&gt;

&lt;p&gt;I submitted a support request to Google about the erroneous charges. They refunded ~$90 — the full amount of the erroneous charges.&lt;/p&gt;

&lt;p&gt;I didn't expect it. I asked anyway.&lt;/p&gt;

&lt;p&gt;There's a lucky coincidence worth naming: I used Gemini for both summarization and embeddings. The same provider that had the quota bug was also the one billing me for the runaway loop. That made the refund case coherent — Google caused the problem and Google charged me for it.&lt;/p&gt;

&lt;p&gt;If I had used Claude or GPT for summarization, those charges would have been legitimate — those providers did the work correctly. I couldn't have disputed them.&lt;/p&gt;

&lt;p&gt;Though there's a flip side: Anthropic and OpenAI work on prepaid credits. The loop would have stopped when the balance ran out, not after 4 days. A natural spending cap I didn't have with GCP's postpaid billing.&lt;/p&gt;

&lt;p&gt;Postpaid: can spiral, but you can dispute after the fact. Prepaid: naturally capped, but no recourse. Neither is obviously safer — they just fail differently.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;TLBrain is open source: &lt;a href="https://github.com/Inozem/tlbrain" rel="noopener noreferrer"&gt;github.com/Inozem/tlbrain&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>googlecloud</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Claude Fable 5 refuses tool calls based on semantics, not logic</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:14:40 +0000</pubDate>
      <link>https://dev.to/inozem/claude-fable-5-refuses-tool-calls-based-on-semantics-not-logic-kd3</link>
      <guid>https://dev.to/inozem/claude-fable-5-refuses-tool-calls-based-on-semantics-not-logic-kd3</guid>
      <description>&lt;p&gt;I was migrating &lt;a href="https://github.com/Inozem/llm_api_adapter/" rel="noopener noreferrer"&gt;llm_api_adapter&lt;/a&gt; — an open-source universal adapter for LLM APIs — to support Claude Fable 5. The tool was harmless.&lt;br&gt;
The logic was identical to what worked on Opus 4.8. Then one e2e test came&lt;br&gt;
back with 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="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="n"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "refusal"
&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="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# None
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception. HTTP 200. The tool call just never happened.&lt;/p&gt;

&lt;p&gt;Fable 5 didn't refuse because of what the tool &lt;em&gt;does&lt;/em&gt; — it refused because of&lt;br&gt;
what it &lt;em&gt;sounds like&lt;/em&gt; it does.&lt;/p&gt;


&lt;h2&gt;
  
  
  The setup
&lt;/h2&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;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.models.messages.chat_message&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.models.tools.tool_spec&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ToolSpec&lt;/span&gt;

&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&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="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;claude-fable-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;ToolSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_secret_word_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return the hidden score for a token.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&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="p"&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;adapter&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="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;What is the secret score for token &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strawberry_v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&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="n"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "refusal"
&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="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# None
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same prompt on Opus 4.8: &lt;code&gt;finish_reason: "tool_use"&lt;/code&gt;, correct arguments.&lt;/p&gt;


&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;ToolSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_word_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return the score for a word.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&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="p"&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;adapter&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="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;What is the score for the word &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strawberry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&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="n"&gt;finish_reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# "tool_use"
&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="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# {'word': 'strawberry'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Identical logic. Different semantics. Different outcome.&lt;/p&gt;

&lt;p&gt;The obvious hypothesis: Fable 5's classifiers flag security-adjacent words. So&lt;br&gt;
I renamed the tool, removed "secret" from the prompt, and moved on.&lt;/p&gt;

&lt;p&gt;But that's the &lt;em&gt;what&lt;/em&gt;, not the &lt;em&gt;how&lt;/em&gt;. I wanted to know the mechanism.&lt;/p&gt;


&lt;h2&gt;
  
  
  3 isolation tests
&lt;/h2&gt;

&lt;p&gt;I kept the original code and removed one element at a time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I removed&lt;/th&gt;
&lt;th&gt;What remained unchanged&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;secret&lt;/code&gt; from tool name only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token&lt;/code&gt; arg + "secret score for token" in prompt&lt;/td&gt;
&lt;td&gt;✅ tool_use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;secret&lt;/code&gt; from name + prompt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token&lt;/code&gt; as argument name&lt;/td&gt;
&lt;td&gt;✅ tool_use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing from name&lt;/td&gt;
&lt;td&gt;Cleaned prompt and argument name&lt;/td&gt;
&lt;td&gt;✅ tool_use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Everything original&lt;/td&gt;
&lt;td&gt;❌ refusal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Removing &lt;em&gt;any single element&lt;/em&gt; was enough to get a tool call through.&lt;/p&gt;


&lt;h2&gt;
  
  
  What's actually happening
&lt;/h2&gt;

&lt;p&gt;Fable 5's classifier is not a keyword filter. It's a combinatorial pattern detector.&lt;/p&gt;

&lt;p&gt;The combination of &lt;code&gt;get_secret_word_score&lt;/code&gt; + argument named &lt;code&gt;token&lt;/code&gt; + prompt&lt;br&gt;
containing "secret score for token" reads as an auth/security operation — the&lt;br&gt;
kind of thing you'd see when extracting credentials or scoring access tokens.&lt;/p&gt;

&lt;p&gt;Each word on its own is harmless. Together, they cross a threshold.&lt;/p&gt;

&lt;p&gt;This makes sense given how Anthropic built Fable 5. It shares the same&lt;br&gt;
underlying model as Mythos 5 — the difference is that Fable ships with safety&lt;br&gt;
classifiers that evaluate &lt;em&gt;intent&lt;/em&gt;, not execution. The classifier doesn't care&lt;br&gt;
what &lt;code&gt;get_secret_word_score&lt;/code&gt; actually does. It cares what it &lt;em&gt;looks like&lt;/em&gt; it&lt;br&gt;
does, based on everything in the request: tool name, description, argument&lt;br&gt;
names, and the prompt together.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://platform.claude.com/docs/en/about-claude/models/migration-guide" rel="noopener noreferrer"&gt;Fable 5 migration guide&lt;/a&gt; calls the migration "mostly drop-in" and lists safety classifier refusals as one of four key changes to watch. What it doesn't explain is how those classifiers actually evaluate your tool specs.&lt;/p&gt;


&lt;h2&gt;
  
  
  Practical takeaway
&lt;/h2&gt;

&lt;p&gt;Don't think of your tool spec as a technical contract. Think of it as a&lt;br&gt;
description of intent — because that's how the classifier reads it.&lt;/p&gt;

&lt;p&gt;Before you name a tool, ask: if someone unfamiliar with my codebase read this&lt;br&gt;
tool name, description, and argument names together, what would they think this&lt;br&gt;
code is &lt;em&gt;trying to do&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;If it sounds like credential extraction, auth bypass, or scoring access tokens —&lt;br&gt;
the classifier will think so too.&lt;/p&gt;

&lt;p&gt;A few patterns that can combine badly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;secret&lt;/code&gt; / &lt;code&gt;hidden&lt;/code&gt; / &lt;code&gt;private&lt;/code&gt; + &lt;code&gt;token&lt;/code&gt; / &lt;code&gt;key&lt;/code&gt; + &lt;code&gt;score&lt;/code&gt; / &lt;code&gt;check&lt;/code&gt; / &lt;code&gt;verify&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth&lt;/code&gt; / &lt;code&gt;access&lt;/code&gt; in tool names with argument names that look like identifiers
None of these words are banned. The problem is accumulation.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  One integration note
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stop_reason: "refusal"&lt;/code&gt; comes back as HTTP 200. If you're only handling&lt;br&gt;
exceptions, you're silently dropping tool calls in production.&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finish_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refusal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# fall back to another model or surface the error explicitly
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API supports a &lt;code&gt;fallbacks&lt;/code&gt; parameter (currently in beta) to handle this&lt;br&gt;
server-side automatically — worth checking if you're running Fable 5 at scale.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>claude</category>
      <category>api</category>
    </item>
    <item>
      <title>RAG on call transcripts: utterance-aware chunking and hybrid retrieval in production</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Mon, 01 Jun 2026 12:27:19 +0000</pubDate>
      <link>https://dev.to/inozem/i-built-a-conversational-rag-memory-for-my-wifes-linkedin-agency-for-44-2c4b</link>
      <guid>https://dev.to/inozem/i-built-a-conversational-rag-memory-for-my-wifes-linkedin-agency-for-44-2c4b</guid>
      <description>&lt;p&gt;The LLM that indexes the transcripts and the LLM that answers questions never share output. One generates summaries. The other reads only the original utterances. That separation is the whole design.&lt;/p&gt;




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

&lt;p&gt;TLBrain indexes call transcripts and gives Claude a persistent, searchable memory of every client conversation.&lt;/p&gt;

&lt;p&gt;When my wife asks &lt;em&gt;"what did we discuss with Acme last month?"&lt;/em&gt; — Claude queries the index, retrieves the relevant transcript segments, and answers with actual context from the calls.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TL;DV records a call and fires a webhook&lt;/li&gt;
&lt;li&gt;An import service converts the transcript into a Google Doc and places it in the right client
folder in Google Drive — automatically&lt;/li&gt;
&lt;li&gt;A sync service picks up new and changed documents, parses them, generates summaries and facts
via Gemini, and indexes everything into Qdrant&lt;/li&gt;
&lt;li&gt;A remote MCP server connects to Claude — accessible as a tool in both Claude.ai chat and Claude Cowork, on any device&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By default, Claude only gets the relevant segments retrieved for each query — but there's also a tool to fetch the full transcript when needed.&lt;/p&gt;




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

&lt;p&gt;232 transcripts indexed for &lt;strong&gt;$44&lt;/strong&gt; — one-time cost. Each new transcript costs ~$0.19 to index.&lt;/p&gt;

&lt;p&gt;Infrastructure runs entirely on free tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud Run&lt;/li&gt;
&lt;li&gt;Firestore&lt;/li&gt;
&lt;li&gt;Google Drive&lt;/li&gt;
&lt;li&gt;Qdrant Cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four stay within free tier limits for a small agency workload.&lt;/p&gt;

&lt;p&gt;The only meaningful cost is &lt;strong&gt;Gemini API (Tier 1)&lt;/strong&gt; — generation and embeddings during indexing. Embeddings are generated only for summaries and facts — not for utterances. Utterances are stored with BM25 sparse vectors and retrieved by range. This keeps both cost and vector storage size low. Free tier has strict rate limits that would make indexing 200+ transcripts impractical.&lt;/p&gt;

&lt;p&gt;A few details that keep the cost low: embeddings use &lt;code&gt;text-embedding-004&lt;/code&gt; with &lt;code&gt;output_dimensionality=768&lt;/code&gt; — 4× cheaper than the default 3072. Summary and facts are generated in a single Gemini request per window. And if a file hasn't changed, it's skipped entirely — Gemini is never called again for the same content.&lt;/p&gt;

&lt;p&gt;The $0.19 per transcript is a one-time indexing cost — you pay to embed the conversation once, not every time Claude searches it. Stop recording calls for a month, go on vacation, pause the business — the system costs nothing during that time. You only pay again when new transcripts are indexed. That said, this assumes you stay within the free tiers — once your call volume grows beyond those limits, infrastructure costs will kick in.&lt;/p&gt;

&lt;p&gt;Without an index, asking Claude about a specific client means pasting entire transcripts into the context window — hitting message limits fast, and starting from scratch every session. TLBrain sends Claude only the relevant fragments: up to 75 utterances out of hundreds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just use Claude Projects?
&lt;/h2&gt;

&lt;p&gt;Claude Projects is the obvious first answer. But it has two hard limits for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context ceiling&lt;/strong&gt; — paste enough transcripts and you hit the limit. Every new session starts from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No structure&lt;/strong&gt; — there's no concept of clients, dates, or searchable facts. Everything is a flat pile of documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine she needs to find what a client said about budget across three calls from different months. With Claude Projects, she'd need to manually find the right transcripts, paste them one by one, and hope they fit in context. With 232 calls in the archive, that's not a workflow — that's a research project. TLBrain returns the relevant fragments in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TL;DV API  ←─────────────────  Reconciliation (Cloud Run, daily)
     ↓
Webhook Handler (Cloud Function)
     ↓
Import Service (Cloud Run)
     ↓
Google Drive + Firestore  ←───  Sync Checker (Cloud Function, every 15 min)
     ↓
Vector Sync (Cloud Run)
     ↓
Qdrant Cloud + Firestore
     ↓
MCP Server (Cloud Run)
     ↓
Claude (chat / Cowork)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Firestore is used throughout as the state store — tracking import status, content hashes, and sync state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six services sounds like a lot — but each split is intentional. The webhook handler must respond to TL;DV in under 2 seconds or the delivery is marked as failed, so import runs in a separate service. The MCP server is isolated from the sync pipeline so a slow indexing job never blocks Claude's queries. Services are also split by runtime pattern: Cloud Functions wake up, check something, and go back to sleep — no idle cost. Cloud Run containers handle long-running tasks and stay warm longer: the MCP server keeps its instance alive for 15 minutes after the last request, so there are no cold starts during an active session.&lt;/p&gt;

&lt;p&gt;Client folders in Google Drive are the source of truth for data organization. Each subfolder under the root is a client name. The sync service doesn't know about TL;DV — it only reads Google Docs from Drive. The import service and sync service are fully decoupled.&lt;/p&gt;

&lt;p&gt;A daily reconciliation job cross-checks TL;DV's API against Firestore and queues anything the webhook missed — so no transcript gets lost silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  One unexpected benefit
&lt;/h2&gt;

&lt;p&gt;If a transcript contains transcription errors, I simply edit the Google Doc.&lt;/p&gt;

&lt;p&gt;The sync service detects the change, regenerates summaries, re-embeds affected chunks, and updates Qdrant automatically.&lt;/p&gt;

&lt;p&gt;No admin panel required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why traditional RAG chunking fails on conversations
&lt;/h2&gt;

&lt;p&gt;Standard RAG splits text by token count. That works for documents but breaks on transcripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speaker boundaries are lost&lt;/li&gt;
&lt;li&gt;replies get split mid-thought&lt;/li&gt;
&lt;li&gt;retrieval returns fragments without context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix: treat each utterance as the atomic unit.&lt;/strong&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="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utterance&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;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1BxK...drive_file_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Acme Corp&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;dialog_date&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;2025-03-12&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;speaker&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;Alice&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;text&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;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re planning to launch in Q3, budget is around 5000.&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;order_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&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;sha256_of_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But utterances alone lose context. &lt;em&gt;"Price is 5000"&lt;/em&gt; is meaningless without the surrounding conversation. So I generate summaries over sliding windows using anchor-based windowing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;anchor_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;half_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor_step&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;half_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;half_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;window_utterances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;center_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_index&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;covered_range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;utterances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utterances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window_utterances&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Summaries and facts are generated in English regardless of the original language — so Claude always queries in English for consistent retrieval quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why semantic search alone isn't enough
&lt;/h2&gt;

&lt;p&gt;With summaries indexed, semantic search works well — most of the time.&lt;/p&gt;

&lt;p&gt;The failure case: &lt;em&gt;"what was the price she mentioned?"&lt;/em&gt; Specific numbers, names, short factual statements — these rarely survive summarization. The summary might say &lt;em&gt;"discussed pricing"&lt;/em&gt; but the actual figure only lives in the raw utterance.&lt;/p&gt;

&lt;p&gt;Semantic miss. Keyword hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: three parallel searches.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Facts handle structured values like prices and dates. BM25 catches what semantic search misses — exact company names, abbreviations, or foreign words that don't survive summarization. If a client mentioned a specific vendor by name, semantic search might return "discussed partnerships" — BM25 finds the exact utterance.&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;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;semantic_future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;search_summaries_and_facts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;date_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;keyword_future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;keyword_search_utterances&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;date_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;semantic_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;semantic_future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# dense, score &amp;gt;= 0.6
&lt;/span&gt;&lt;span class="n"&gt;keyword_hits&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyword_future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;# BM25, no threshold
&lt;/span&gt;
&lt;span class="c1"&gt;# Pin: user_facts bypass score threshold entirely
&lt;/span&gt;&lt;span class="n"&gt;user_fact_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_user_facts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pinned_hits&lt;/span&gt; &lt;span class="o"&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;user_fact_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hits_by_doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user_fact_hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hits_by_doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hits_by_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_id&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hit_count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits_by_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;pinned_hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;search_summaries_for_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hit_count&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; instead of &lt;code&gt;asyncio.gather&lt;/code&gt; — the Qdrant Python SDK is synchronous. Real parallelism here comes from a thread pool, not coroutines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each search serves a different purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dense (semantic)&lt;/strong&gt; over summaries and facts — finds topically relevant conversations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BM25 (keyword)&lt;/strong&gt; over raw utterances — catches exact matches that don't survive summarization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin&lt;/strong&gt; over user-added facts — forces specific documents into results regardless of score&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results are merged, overlapping ranges within the same document are combined, and the final&lt;br&gt;
utterances are fetched by index range — no second search needed.&lt;/p&gt;

&lt;p&gt;The output is a list of segments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"doc_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1BxKmN9vQ2rTzAp_drivefile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dialog_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-03-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"segments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dialog"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"So what's the timeline looking like?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nl"&gt;"order_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I need to be live by end of Q3."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="nl"&gt;"order_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I're planning to launch in Q3, budget is 5000."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"order_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"That works. Can you send a proposal by Friday?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"order_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"speaker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sure, I'll have it over by Thursday."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nl"&gt;"order_index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what gets sent to Claude as context. Not the full transcript — just the relevant&lt;br&gt;
fragments.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Today TLBrain indexes 232 conversations and gives Claude access to years of client history — without loading entire transcripts into context.&lt;/p&gt;

&lt;p&gt;The whole project is open source: &lt;strong&gt;&lt;a href="https://github.com/Inozem/tlbrain" rel="noopener noreferrer"&gt;TLBrain on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the next post I'll cover how I turned this into a production remote MCP server with Google OAuth on Cloud Run.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>python</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>One Tool Calling Interface for OpenAI, Claude, and Gemini</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Thu, 12 Mar 2026 09:56:46 +0000</pubDate>
      <link>https://dev.to/inozem/one-tool-calling-interface-for-openai-claude-and-gemini-2l1c</link>
      <guid>https://dev.to/inozem/one-tool-calling-interface-for-openai-claude-and-gemini-2l1c</guid>
      <description>&lt;p&gt;&lt;strong&gt;llm-api-adapter&lt;/strong&gt; is an open‑source Python library designed to simplify working with multiple LLM providers.&lt;/p&gt;

&lt;p&gt;Many AI applications today need to support multiple LLM providers.&lt;/p&gt;

&lt;p&gt;Common reasons include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cost optimization&lt;/li&gt;
&lt;li&gt;fallback when a provider is unavailable&lt;/li&gt;
&lt;li&gt;access to different model capabilities&lt;/li&gt;
&lt;li&gt;experimentation with new models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, the moment you try to support &lt;strong&gt;OpenAI, Claude, and Gemini&lt;/strong&gt;, the integration becomes messy.&lt;/p&gt;

&lt;p&gt;Tool calling alone already breaks portability:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Tool format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tool_calls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tool_use&lt;/code&gt; blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;functionCall&lt;/code&gt; / &lt;code&gt;functionResponse&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are not just syntax differences.\&lt;br&gt;
They require &lt;strong&gt;different request structures, response parsing, and execution loops&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Supporting multiple providers usually leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider-specific integration logic&lt;/li&gt;
&lt;li&gt;provider-specific request/response handling&lt;/li&gt;
&lt;li&gt;duplicated tool execution flows&lt;/li&gt;
&lt;li&gt;multiple SDK dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is more code, more bugs, and much harder provider switching.&lt;/p&gt;

&lt;p&gt;To simplify this, I built &lt;strong&gt;llm-api-adapter&lt;/strong&gt; — a small Python library that provides &lt;strong&gt;one unified interface for multiple LLM APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Define tools once and run the same application logic across &lt;strong&gt;OpenAI, Anthropic, and Gemini&lt;/strong&gt;.&lt;/p&gt;


&lt;h1&gt;
  
  
  Architecture
&lt;/h1&gt;

&lt;p&gt;The adapter acts as a &lt;strong&gt;translation layer&lt;/strong&gt; between your application and LLM providers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Application Logic
                     │
                     ▼
           UniversalLLMAPIAdapter
                     │
                     ▼
          Provider Translation Layer
                     │
                     ▼
 ┌─────────────┬─────────────┬─────────────┐
 │   OpenAI    │  Anthropic  │   Gemini    │
 │ tool_calls  │  tool_use   │ functionCall│
 └─────────────┴─────────────┴─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your application communicates with &lt;strong&gt;one interface&lt;/strong&gt;, while the adapter converts requests and responses to the provider-specific formats.&lt;/p&gt;




&lt;h1&gt;
  
  
  Installation
&lt;/h1&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;llm-api-adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  The "Strawberry" problem
&lt;/h1&gt;

&lt;p&gt;A classic example showing why tool calling matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How many "r" letters are in "strawberry"?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct answer is &lt;strong&gt;3&lt;/strong&gt;, but models often fail because they reason over &lt;strong&gt;tokens&lt;/strong&gt;, not characters.&lt;/p&gt;

&lt;p&gt;Best practice is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let the LLM reason, but delegate deterministic tasks to code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly what &lt;strong&gt;tool calling&lt;/strong&gt; enables.&lt;/p&gt;




&lt;h1&gt;
  
  
  Defining a tool once
&lt;/h1&gt;

&lt;p&gt;With &lt;strong&gt;llm-api-adapter&lt;/strong&gt;, tools are defined using a &lt;strong&gt;provider-agnostic schema&lt;/strong&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;llm_api_adapter.models.tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ToolSpec&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;ToolSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_letter_in_word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Count how many times a specific letter appears in a word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minLength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxLength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&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;letter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&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;The adapter automatically converts this schema to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI &lt;code&gt;tools&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic &lt;code&gt;tool_use&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gemini &lt;code&gt;functionCall&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Running the same code across providers
&lt;/h1&gt;

&lt;p&gt;The application logic remains identical.&lt;/p&gt;

&lt;p&gt;Only the &lt;strong&gt;provider name, model, and API key&lt;/strong&gt; change.&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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.models.messages.chat_message&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ToolMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&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;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_letter_in_word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;gpt-5.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="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;claude-haiku-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;google_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="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;How many &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; letters are in &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strawberry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&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;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&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="n"&gt;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;ToolMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;previous_response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; / &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ---&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Example output
&lt;/h1&gt;

&lt;p&gt;Even though the models use different tokenization internally, they all trigger the tool correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- openai / gpt-5.2 ---
There are 3 letters "r" in "strawberry".

--- anthropic / claude-haiku-4-5 ---
There are 3 "r" letters in "strawberry".

--- google / gemini-2.5-flash ---
There are three "r" letters in "strawberry".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Without vs with an adapter
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Without Adapter&lt;/th&gt;
&lt;th&gt;With llm-api-adapter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool definitions&lt;/td&gt;
&lt;td&gt;Provider specific&lt;/td&gt;
&lt;td&gt;One universal schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool execution&lt;/td&gt;
&lt;td&gt;Custom logic per provider&lt;/td&gt;
&lt;td&gt;Unified interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response parsing&lt;/td&gt;
&lt;td&gt;Different formats&lt;/td&gt;
&lt;td&gt;Single response model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider switching&lt;/td&gt;
&lt;td&gt;Rewrite code&lt;/td&gt;
&lt;td&gt;Change model string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Multiple SDKs&lt;/td&gt;
&lt;td&gt;One library&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  Why this matters
&lt;/h1&gt;

&lt;p&gt;Supporting multiple LLM providers normally requires &lt;strong&gt;separate integrations and duplicated logic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A unified interface lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep application logic provider-agnostic&lt;/li&gt;
&lt;li&gt;switch models without rewriting code&lt;/li&gt;
&lt;li&gt;simplify agent architectures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of adapting your code to each provider, you adapt the providers to your code.&lt;/p&gt;




&lt;h1&gt;
  
  
  GitHub
&lt;/h1&gt;

&lt;p&gt;The project is open source.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Inozem/llm_api_adapter" rel="noopener noreferrer"&gt;https://github.com/Inozem/llm_api_adapter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will find full documentation, examples, and the source code in the repository.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>openai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Clean Architecture for AI Agents with Convo-Lang</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Tue, 10 Feb 2026 20:47:41 +0000</pubDate>
      <link>https://dev.to/inozem/clean-architecture-for-ai-with-convo-lang-93l</link>
      <guid>https://dev.to/inozem/clean-architecture-for-ai-with-convo-lang-93l</guid>
      <description>&lt;h2&gt;
  
  
  Decoupling Orchestration from Reasoning
&lt;/h2&gt;

&lt;p&gt;In this post, I’ll show how to design a &lt;strong&gt;clean, maintainable architecture for AI systems&lt;/strong&gt; using Convo-Lang.&lt;/p&gt;

&lt;p&gt;As a concrete example, I’ll use a &lt;strong&gt;hallucination-resistant AI agent that analyzes a job description, evaluates candidate fit against detailed professional experience, and generates a tailored resume only when the role is actually relevant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this setup, &lt;strong&gt;all reasoning and decision logic lives in Convo-Lang&lt;/strong&gt;, while &lt;strong&gt;Python is used strictly for orchestration&lt;/strong&gt; — loading inputs, executing agents, and wiring the pipeline together.&lt;/p&gt;

&lt;p&gt;The goal of the example is not the resume itself. The goal is to demonstrate how to &lt;strong&gt;decouple orchestration from reasoning&lt;/strong&gt; and build an AI system that is easy to understand, extend, and maintain over time.&lt;/p&gt;

&lt;p&gt;The full working example is available in the Convo-Lang repository.&lt;/p&gt;

&lt;p&gt;You can explore the complete code here: &lt;a href="https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can clone it, run it locally, and experiment with it by simply replacing the job description and writing your own experience profile — the sample inputs live in the &lt;code&gt;data/&lt;/code&gt; folder.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Convo-Lang actually is
&lt;/h2&gt;

&lt;p&gt;Convo-Lang is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a prompt template engine&lt;/li&gt;
&lt;li&gt;a thin wrapper around chat completions&lt;/li&gt;
&lt;li&gt;a “nicer way to write prompts”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Convo-Lang is a &lt;strong&gt;domain-specific language for LLM reasoning and agent workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It allows you to define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit agent roles&lt;/li&gt;
&lt;li&gt;typed input and output contracts&lt;/li&gt;
&lt;li&gt;deterministic logic&lt;/li&gt;
&lt;li&gt;schema-enforced outputs&lt;/li&gt;
&lt;li&gt;multi-agent pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this lives in &lt;code&gt;.convo&lt;/code&gt; files — &lt;strong&gt;outside of application code&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why resumes are a good stress test
&lt;/h2&gt;

&lt;p&gt;Resume generation is a hostile domain for hallucinations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventing skills is unacceptable&lt;/li&gt;
&lt;li&gt;inventing companies or roles is unacceptable&lt;/li&gt;
&lt;li&gt;inventing dates is unacceptable&lt;/li&gt;
&lt;li&gt;decisions must be explainable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single “smart prompt” is the worst possible approach here.&lt;/p&gt;

&lt;p&gt;So instead of asking &lt;em&gt;how to prompt&lt;/em&gt;, I started by asking:&lt;br&gt;
&lt;strong&gt;how should this system be modeled?&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Modeling the system as Convo-Lang agents
&lt;/h2&gt;

&lt;p&gt;The solution is built as &lt;strong&gt;five Convo-Lang agents&lt;/strong&gt;, each responsible for exactly one thing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JobDescriptionAnalyzer&lt;/strong&gt;&lt;br&gt;
Turns raw job text into structured requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CandidateProfileAnalyzer&lt;/strong&gt;&lt;br&gt;
Converts free-form experience text into factual, structured data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ProfileJobMatcher&lt;/strong&gt;&lt;br&gt;
Matches experience to requirements and explicitly lists gaps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ResumeWriter&lt;/strong&gt;&lt;br&gt;
Generates a resume strictly from verified data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FitEvaluator&lt;/strong&gt;&lt;br&gt;
Decides whether applying makes sense.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lives in its own &lt;code&gt;.convo&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;has a single responsibility&lt;/li&gt;
&lt;li&gt;communicates only through typed contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is not cosmetic.&lt;br&gt;
It is the foundation of reliability.&lt;/p&gt;


&lt;h2&gt;
  
  
  Typed contracts instead of “return JSON please”
&lt;/h2&gt;

&lt;p&gt;In most LLM systems, structured output is a suggestion.&lt;/p&gt;

&lt;p&gt;In Convo-Lang, it is a &lt;strong&gt;contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a real schema used by the &lt;code&gt;CandidateProfileAnalyzer&lt;/code&gt; agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;define
ProfileData = struct(
  workExperience: array(
    struct(
      title: string
      companyName: string
      firstDate: string
      lastDate?: string
      summary: string
      experience: array(string)
    )
  )
  projects?: array(
    struct(
      title: string
      firstDate: string
      lastDate?: string
      experience: array(string)
    )
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This immediately changes system behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required fields must exist&lt;/li&gt;
&lt;li&gt;optional fields are explicit&lt;/li&gt;
&lt;li&gt;invented fields are invalid&lt;/li&gt;
&lt;li&gt;downstream agents can trust the data shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hallucinations don’t silently propagate.&lt;br&gt;
They violate the contract.&lt;/p&gt;


&lt;h2&gt;
  
  
  Validating inputs before any reasoning happens
&lt;/h2&gt;

&lt;p&gt;Hallucinations often start &lt;strong&gt;before generation&lt;/strong&gt;.&lt;br&gt;
They start when invalid or ambiguous input quietly enters the system.&lt;/p&gt;

&lt;p&gt;Convo-Lang allows agents to &lt;strong&gt;validate inputs explicitly&lt;/strong&gt;, before any reasoning takes place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;define
JobData = struct(
  title: string
  mustRequirements: array(string)
  niceToHaveRequirements: array(string)
  keywords: array(string)
)

&amp;gt;do
jobData = new(JobData job_data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line enforces a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checks that &lt;code&gt;job_data&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;validates required fields&lt;/li&gt;
&lt;li&gt;enforces correct types&lt;/li&gt;
&lt;li&gt;rejects malformed input early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the input does not match &lt;code&gt;JobData&lt;/code&gt;, the agent does not proceed.&lt;/p&gt;

&lt;p&gt;The model never reasons over invalid data.&lt;/p&gt;

&lt;p&gt;Here, &lt;strong&gt;input validation is part of the agent’s contract&lt;/strong&gt;, not an afterthought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Explainable matching instead of opaque scoring
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ProfileJobMatcher&lt;/code&gt; agent does not produce a mysterious score.&lt;/p&gt;

&lt;p&gt;It produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only relevant roles and projects&lt;/li&gt;
&lt;li&gt;explicit &lt;code&gt;matchReasons&lt;/code&gt; for each item&lt;/li&gt;
&lt;li&gt;two concrete gap lists: must-have and nice-to-have
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MatchData = struct(
  coverageProfileData: struct(
    workExperience: array(
      title: string
      companyName: string
      firstDate: string
      lastDate?: string
      summary: string
      experience: array(string)
      matchReasons: array(string)
    )
    projects?: array(...)
  )
  gaps: struct(
    mustRequirements: array(string)
    niceToHaveRequirements: array(string)
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is hidden.&lt;br&gt;
Every match and every gap is inspectable.&lt;/p&gt;

&lt;p&gt;This output becomes the single source of truth for all downstream steps.&lt;/p&gt;


&lt;h2&gt;
  
  
  Deterministic logic inside the agent (not in prose)
&lt;/h2&gt;

&lt;p&gt;A key feature of Convo-Lang is that &lt;strong&gt;deterministic logic lives next to reasoning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;FitEvaluator&lt;/code&gt;, the final decision is not guessed.&lt;br&gt;
It is calculated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;do
jobData = new(JobData job_data)
matchData = new(MatchData match_data)

totalConfidence = 100
jobRequirementsAmount = jobData.mustRequirements.length
requirementPoints = div(totalConfidence jobRequirementsAmount)

requirementGapAmount = matchData.gaps.mustRequirements.length
mainConfidence = mul(
  sub(jobRequirementsAmount requirementGapAmount)
  requirementPoints
)

decision = "apply"

if (lt(mainConfidence 70)) then (
  decision = "skip"
)
elif (lt(mainConfidence 90)) then (
  decision = "maybe apply"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is business logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;readable&lt;/li&gt;
&lt;li&gt;reviewable&lt;/li&gt;
&lt;li&gt;testable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM explains the decision — but it does not invent the rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Schema-enforced output with &lt;code&gt;@json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Convo-Lang does not rely on “please return JSON”.&lt;/p&gt;

&lt;p&gt;It enforces it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@json RecommendationData
&amp;gt;user
Help the candidate decide whether applying for this job makes sense.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output does not match &lt;code&gt;RecommendationData&lt;/code&gt;, it is invalid.&lt;/p&gt;

&lt;p&gt;Structured output is no longer a best-effort promise.&lt;br&gt;
It is a guarantee.&lt;/p&gt;




&lt;h2&gt;
  
  
  Python as an orchestrator, not a reasoning layer
&lt;/h2&gt;

&lt;p&gt;So where does Python fit into this architecture?&lt;/p&gt;

&lt;p&gt;Python is intentionally boring.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contain prompts&lt;/li&gt;
&lt;li&gt;contain business rules&lt;/li&gt;
&lt;li&gt;interpret free-form model output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loads input data&lt;/li&gt;
&lt;li&gt;executes agents&lt;/li&gt;
&lt;li&gt;passes validated JSON between them&lt;/li&gt;
&lt;li&gt;handles I/O
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;job_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_job_description_analyzer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;profile_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_candidate_profile_analyzer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;match_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_profile_job_matcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;resume_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_resume_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_fit_evaluator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All intelligence lives in &lt;code&gt;.convo&lt;/code&gt;.&lt;br&gt;
Python is just the runtime.&lt;/p&gt;

&lt;p&gt;This separation is deliberate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this separation matters
&lt;/h2&gt;

&lt;p&gt;By keeping reasoning in Convo-Lang and orchestration in Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI logic becomes portable&lt;/li&gt;
&lt;li&gt;behavior is consistent across CLI, editor, and SDK&lt;/li&gt;
&lt;li&gt;prompt changes don’t require backend redeploys&lt;/li&gt;
&lt;li&gt;agent logic can be reviewed like code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;agents folder becomes the product&lt;/strong&gt;.&lt;br&gt;
The SDK becomes an implementation detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this example actually demonstrates
&lt;/h2&gt;

&lt;p&gt;This post is not really about resumes.&lt;/p&gt;

&lt;p&gt;It demonstrates that Convo-Lang lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;treat LLM logic as first-class code&lt;/li&gt;
&lt;li&gt;build multi-agent systems without prompt chaos&lt;/li&gt;
&lt;li&gt;validate inputs and outputs explicitly&lt;/li&gt;
&lt;li&gt;make hallucinations visible instead of hidden&lt;/li&gt;
&lt;li&gt;scale reasoning without rewriting everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why Convo-Lang is worth using.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;Hallucinations are rarely a model problem.&lt;br&gt;
They are almost always an &lt;strong&gt;architecture problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Convo-Lang gives you the tools to fix that at the right level.&lt;/p&gt;




&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Convo-Lang core: &lt;a href="https://github.com/convo-lang/convo-lang" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convo-Lang Python SDK: &lt;a href="https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Resume agent example:
&lt;a href="https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://learn.convo-lang.ai/" rel="noopener noreferrer"&gt;https://learn.convo-lang.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>python</category>
    </item>
    <item>
      <title>Prompts are logic, not strings: Why I contributed to Convo-Lang</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Sun, 04 Jan 2026 13:36:56 +0000</pubDate>
      <link>https://dev.to/inozem/prompts-are-logic-not-strings-why-i-contributed-to-convo-lang-172d</link>
      <guid>https://dev.to/inozem/prompts-are-logic-not-strings-why-i-contributed-to-convo-lang-172d</guid>
      <description>&lt;p&gt;If you’ve built anything non-trivial with LLMs, you’ve probably written code 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="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Analyze this job description: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Analyze this candidate profile: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;candidate_profile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
Decide whether the candidate is a good fit.
Return JSON.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works.&lt;br&gt;
Until your project grows.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem: prompt spaghetti and technical debt
&lt;/h2&gt;

&lt;p&gt;Hardcoding prompts directly into application code feels convenient at first.&lt;br&gt;
But very quickly it turns into long-term technical debt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompts become unreadable f-string monsters&lt;/li&gt;
&lt;li&gt;Prompt changes require code changes and redeploys&lt;/li&gt;
&lt;li&gt;Prompt versions drift across files and branches&lt;/li&gt;
&lt;li&gt;Prompt engineers and copywriters are afraid to touch &lt;code&gt;.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Prompt logic, business logic, and orchestration logic get mixed together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, prompts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hard to test&lt;/li&gt;
&lt;li&gt;hard to reuse&lt;/li&gt;
&lt;li&gt;hard to reason about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We already solved this problem for SQL, HTML, configs, and templates.&lt;br&gt;
LLM prompts deserve the same treatment.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prompts are not strings — they are logic
&lt;/h2&gt;

&lt;p&gt;A modern LLM “prompt” is not just text.&lt;/p&gt;

&lt;p&gt;It contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structure&lt;/li&gt;
&lt;li&gt;contracts&lt;/li&gt;
&lt;li&gt;conditions&lt;/li&gt;
&lt;li&gt;branching&lt;/li&gt;
&lt;li&gt;deterministic steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating it as a Python string literal is the fastest way to lose control over your AI system.&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;Convo-Lang&lt;/strong&gt; comes in.&lt;/p&gt;


&lt;h2&gt;
  
  
  Convo-Lang as an AI-native DSL
&lt;/h2&gt;

&lt;p&gt;Convo-Lang is an open-source, AI-native DSL for building conversations and agent workflows.&lt;/p&gt;

&lt;p&gt;Instead of embedding prompts into code, you define them in &lt;code&gt;.convo&lt;/code&gt; files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit schemas&lt;/li&gt;
&lt;li&gt;role-based messages&lt;/li&gt;
&lt;li&gt;deterministic logic blocks&lt;/li&gt;
&lt;li&gt;structured outputs&lt;/li&gt;
&lt;li&gt;multi-agent workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your prompt becomes a &lt;strong&gt;first-class artifact&lt;/strong&gt;, not a string buried in code.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it works: Python as a thin runtime
&lt;/h2&gt;

&lt;p&gt;Here’s all the Python code required to run a single agent:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Running FitEvaluator agent...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;convo_fit_evaluator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Conversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_configs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;convo_fit_evaluator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_convo_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agents/fitEvaluator.convo&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_apply_decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convo_fit_evaluator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;variables&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;job_data&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_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match_data&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;Notice what’s missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no prompt text&lt;/li&gt;
&lt;li&gt;no formatting logic&lt;/li&gt;
&lt;li&gt;no hidden reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Python is just the runtime.&lt;/strong&gt;&lt;br&gt;
All intelligence lives in &lt;code&gt;.convo&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Typed contracts instead of free-form prompts
&lt;/h2&gt;

&lt;p&gt;The core idea that changed how I think about prompts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Agents should communicate through &lt;strong&gt;typed contracts&lt;/strong&gt;, not vague instructions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Example schema definitions used by an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;define
JobData = struct(
  title:string
  mustRequirements:array(string)
  niceToHaveRequirements:array(string)
)

RecommendationData = struct(
  recommendation: struct(
    decision: enum("apply","maybe apply","skip")
    confidence: number
    summary: string
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit input shapes&lt;/li&gt;
&lt;li&gt;explicit output contracts&lt;/li&gt;
&lt;li&gt;predictable agent behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent no longer “guesses” what to return.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deterministic logic lives next to the prompt
&lt;/h2&gt;

&lt;p&gt;Convo-Lang is not just a prompt format.&lt;br&gt;
It allows you to define &lt;strong&gt;explicit, deterministic logic&lt;/strong&gt; inside the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;do
jobData = new(JobData job_data)
matchData = new(MatchData match_data)

total = 100
reqCount = jobData.mustRequirements.length
niceCount = jobData.niceToHaveRequirements.length

reqPoints = div(total reqCount)
nicePoints = div(reqPoints 4)

reqGaps = matchData.gaps.mustRequirements.length
niceGaps = matchData.gaps.niceToHaveRequirements.length

confidence = add(
  mul(sub(reqCount reqGaps) reqPoints)
  mul(sub(niceCount niceGaps) nicePoints)
)

decision = "apply"
if (lt(confidence 70)) then (decision = "skip")
elif (lt(confidence 90)) then (decision = "maybe apply")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;business logic&lt;/strong&gt;, not prompt prose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Schema-enforced output with &lt;code&gt;@json&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@json RecommendationData
&amp;gt;user
Return recommendation for this candidate and job.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is not a suggestion.&lt;/strong&gt;&lt;br&gt;
It is schema‑enforced output validation.&lt;/p&gt;

&lt;p&gt;Malformed or invalid responses don’t silently pass through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-SDK portability by design
&lt;/h2&gt;

&lt;p&gt;Because the Convo-Lang core is implemented in TypeScript, it guarantees identical behavior across environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS Code preview&lt;/li&gt;
&lt;li&gt;CLI&lt;/li&gt;
&lt;li&gt;Python runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your prompt passes validation in the editor, it will behave the same way in your Python backend.&lt;/p&gt;

&lt;p&gt;Write once. Run anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: a smart bridge, not a rewrite
&lt;/h2&gt;

&lt;p&gt;The Python SDK does not reimplement the Convo-Lang engine.&lt;/p&gt;

&lt;p&gt;Instead, it acts as a high‑performance bridge to the Node.js core, which handles parsing, validation, and async I/O.&lt;/p&gt;

&lt;p&gt;This preserves full syntax and behavior parity across SDKs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separation of concerns — for real
&lt;/h2&gt;

&lt;p&gt;With this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.convo&lt;/code&gt; files own AI reasoning and decision logic&lt;/li&gt;
&lt;li&gt;Python only orchestrates execution&lt;/li&gt;
&lt;li&gt;Prompt engineers don’t touch backend code&lt;/li&gt;
&lt;li&gt;Developers don’t rewrite prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;agents/&lt;/code&gt; folder is the product.&lt;br&gt;
Python is just the runtime.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why I contributed to the Python SDK
&lt;/h2&gt;

&lt;p&gt;I believe AI workflows need standards.&lt;/p&gt;

&lt;p&gt;Prompts should be portable, testable, and explicit.&lt;/p&gt;

&lt;p&gt;That’s why I helped bring Convo-Lang to Python.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Resume generator example (Python):
&lt;a href="https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py/examples/02_patterns/resume_generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convo-Lang core:
&lt;a href="https://github.com/convo-lang/convo-lang" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Convo-Lang Python SDK:
&lt;a href="https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py" rel="noopener noreferrer"&gt;https://github.com/convo-lang/convo-lang/tree/main/packages/convo-lang-py&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation:
&lt;a href="https://learn.convo-lang.ai/" rel="noopener noreferrer"&gt;https://learn.convo-lang.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How I built a bulletproof CI/CD for my LLM Python library</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Wed, 24 Dec 2025 09:20:35 +0000</pubDate>
      <link>https://dev.to/inozem/how-i-built-a-cicd-pipeline-with-e2e-tests-via-testpypi-107k</link>
      <guid>https://dev.to/inozem/how-i-built-a-cicd-pipeline-with-e2e-tests-via-testpypi-107k</guid>
      <description>&lt;p&gt;When building an open-source library that integrates with multiple LLM providers (OpenAI, Anthropic, Google), reliability matters. Users expect upgrades to be safe and predictable.&lt;/p&gt;

&lt;p&gt;This post describes the CI/CD setup I use for &lt;strong&gt;llm-api-adapter&lt;/strong&gt;. The key idea is simple: &lt;strong&gt;test not only the code, but the actual published package&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Strategy: two pipelines, three stages
&lt;/h2&gt;

&lt;p&gt;I use a dual-pipeline setup aligned with GitHub Flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev pipeline&lt;/strong&gt; — runs on every push to &lt;code&gt;dev&lt;/code&gt;. Its job is early feedback and validating the distribution process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Main pipeline&lt;/strong&gt; — runs on &lt;code&gt;main&lt;/code&gt; and version tags. Its job is stable, repeatable releases to PyPI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most CI setups stop at unit or integration tests. This one goes further by validating the artifact installed from TestPyPI.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Dev pipeline: pre-flight validation
&lt;/h2&gt;

&lt;p&gt;The dev workflow is where most of the safety guarantees come from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage A: Unit &amp;amp; Integration tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Executed with &lt;code&gt;pytest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tests are separated via markers (&lt;code&gt;unit&lt;/code&gt;, &lt;code&gt;integration&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fast feedback on logic and provider integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage B: publish to TestPyPI
&lt;/h3&gt;

&lt;p&gt;After tests pass, the package is built and published to &lt;strong&gt;TestPyPI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This step catches issues that tests alone cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incorrect &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Missing files in the source distribution&lt;/li&gt;
&lt;li&gt;Broken dependency declarations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage C: E2E tests from TestPyPI
&lt;/h3&gt;

&lt;p&gt;This is the critical part of the pipeline.&lt;/p&gt;

&lt;p&gt;The job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Waits for TestPyPI to index the new release&lt;/li&gt;
&lt;li&gt;Installs the package &lt;strong&gt;from TestPyPI&lt;/strong&gt;, not from source&lt;/li&gt;
&lt;li&gt;Pulls dependencies from the real PyPI&lt;/li&gt;
&lt;li&gt;Runs real end-to-end tests using live API keys
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;pip install --index-url https://test.pypi.org/simple/ \&lt;/span&gt;
            &lt;span class="s"&gt;--extra-index-url https://pypi.org/simple \&lt;/span&gt;
            &lt;span class="s"&gt;llm-api-adapter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the CI environment matches what users will experience after &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Main pipeline: controlled release
&lt;/h2&gt;

&lt;p&gt;Once the package is validated in &lt;code&gt;dev&lt;/code&gt;, changes move to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What runs on &lt;code&gt;main&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full unit + integration test suite on every PR&lt;/li&gt;
&lt;li&gt;No publishing on pushes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What triggers a release
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A version tag (&lt;code&gt;vX.Y.Z&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Build and publish to &lt;strong&gt;PyPI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Credentials handled via GitHub Secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time a tag is pushed, the same artifact has already passed E2E tests via TestPyPI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this setup works
&lt;/h2&gt;

&lt;p&gt;Before publishing to PyPI, I know that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code behaves correctly (unit + integration tests)&lt;/li&gt;
&lt;li&gt;The package is installable from a registry (TestPyPI)&lt;/li&gt;
&lt;li&gt;External LLM providers respond as expected (E2E tests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, this approach prevents broken versions from ever being published to PyPI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If your library depends on external APIs, testing only the source code is not enough.&lt;/p&gt;

&lt;p&gt;Testing the &lt;strong&gt;published artifact&lt;/strong&gt; is what makes releases predictable and safe.&lt;/p&gt;

&lt;p&gt;The full setup is fully public and reproducible:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Repository:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Inozem/llm_api_adapter" rel="noopener noreferrer"&gt;https://github.com/Inozem/llm_api_adapter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;GitHub Actions workflows:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/Inozem/llm_api_adapter/tree/main/.github/workflows" rel="noopener noreferrer"&gt;https://github.com/Inozem/llm_api_adapter/tree/main/.github/workflows&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Question for you
&lt;/h2&gt;

&lt;p&gt;How do you usually set up CI for your open-source projects?&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Python LLM: reasoning is disabled by default in llm-api-adapter</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Sun, 14 Dec 2025 09:00:27 +0000</pubDate>
      <link>https://dev.to/inozem/python-llm-reasoning-is-disabled-by-default-in-llm-api-adapter-45h0</link>
      <guid>https://dev.to/inozem/python-llm-reasoning-is-disabled-by-default-in-llm-api-adapter-45h0</guid>
      <description>&lt;p&gt;Reasoning improves LLM output quality, but it is &lt;strong&gt;expensive&lt;/strong&gt; and often &lt;strong&gt;unnecessary&lt;/strong&gt;. Worse: most providers enable it implicitly or hide it behind non-obvious parameters.&lt;/p&gt;

&lt;p&gt;Result: developers pay for reasoning even when they don’t need it.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Reasoning is handled inconsistently across providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt;: often enabled implicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt;: controlled via &lt;code&gt;thinkingConfig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic (Claude)&lt;/strong&gt;: may enforce minimum reasoning tokens (e.g. 1024).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nano/Mini models&lt;/strong&gt;: sometimes impossible to disable reasoning entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden costs&lt;/li&gt;
&lt;li&gt;provider-specific conditionals&lt;/li&gt;
&lt;li&gt;easy-to-miss misconfiguration&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Approach: Off by Default
&lt;/h3&gt;

&lt;p&gt;Starting from &lt;strong&gt;llm_api_adapter v0.2.3&lt;/strong&gt;, reasoning is &lt;strong&gt;disabled by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If it is not explicitly enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no reasoning tokens are used&lt;/li&gt;
&lt;li&gt;no extra cost is incurred&lt;/li&gt;
&lt;li&gt;existing code keeps working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Costly features should be &lt;strong&gt;opt-in&lt;/strong&gt;, not opt-out.&lt;/p&gt;




&lt;h3&gt;
  
  
  Enabling Reasoning Explicitly
&lt;/h3&gt;

&lt;p&gt;When reasoning is actually required, it can be enabled via a single unified parameter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;String levels: &lt;code&gt;"none" | "low" | "medium" | "high"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Numeric values: &lt;code&gt;256&lt;/code&gt;, &lt;code&gt;512&lt;/code&gt;, &lt;code&gt;1024&lt;/code&gt;, &lt;code&gt;2048&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The adapter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maps the value to provider-specific fields&lt;/li&gt;
&lt;li&gt;applies correct formats per API&lt;/li&gt;
&lt;li&gt;respects provider minimums&lt;/li&gt;
&lt;li&gt;prevents invalid configurations&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Installation
&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;llm-api-adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Example
&lt;/h3&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;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&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="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 quantum computing simply.&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="c1"&gt;# Pick a provider (same interface)
&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&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="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;gpt-5.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;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="c1"&gt;# or
# adapter = UniversalLLMAPIAdapter(
#     organization="google",
#     model="gemini-2.5-pro",
#     api_key=google_api_key,
# )
&lt;/span&gt;
&lt;span class="c1"&gt;# or
# adapter = UniversalLLMAPIAdapter(
#     organization="anthropic",
#     model="claude-sonnet-4-5",
#     api_key=anthropic_api_key,
# )
&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;adapter&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reasoning_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# off by default, enabled explicitly
&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lower and predictable costs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No accidental reasoning usage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner application code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unified control across OpenAI, Claude, and Gemini&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Repository
&lt;/h3&gt;

&lt;p&gt;Source code and documentation: &lt;a href="https://github.com/Inozem/llm-api-adapter" rel="noopener noreferrer"&gt;https://github.com/Inozem/llm-api-adapter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>api</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Structured prompts: how YAML cut my LLM costs by 30%</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Wed, 05 Nov 2025 09:34:01 +0000</pubDate>
      <link>https://dev.to/inozem/structured-prompts-how-yaml-cut-my-llm-costs-by-30-3a56</link>
      <guid>https://dev.to/inozem/structured-prompts-how-yaml-cut-my-llm-costs-by-30-3a56</guid>
      <description>&lt;p&gt;&lt;strong&gt;Result Summary:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Original Prompt&lt;/th&gt;
&lt;th&gt;YAML Prompt&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;355&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;td&gt;−104 (−29.3%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;0.00001775 USD&lt;/td&gt;
&lt;td&gt;0.00001255 USD&lt;/td&gt;
&lt;td&gt;−29.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;I'll show how this works using a popular prompt taken from the internet, rewritten in YAML format to show whether structured phrasing can reduce token count without harming quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fewer tokens → lower cost per request.&lt;/li&gt;
&lt;li&gt;YAML forces clarity and structure, improving consistency of answers.&lt;/li&gt;
&lt;li&gt;Easier to maintain and version prompts in code.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Original Prompt (PROMPT_A)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are the &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Architect Guide,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures.
Your primary roles and methods of guidance include:

Basics of Project Architecture: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding.
Integration Insights: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration.
Exploration of Architectural Styles: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning.
Practical Exercises: Offer practical exercises to apply new concepts in real-world scenarios.
Analysis of Multi-layered Software Projects: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage.
Educational Insights: Focus on reviewing project readme files and source code for comprehensive understanding.
Use of Diagrams and Images: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions.
Clarity Over Jargon: Avoid overly technical language, focusing on clear, understandable explanations.
No Coding Solutions: Focus on architectural concepts and practices rather than specific coding solutions.
Detailed Yet Concise Responses: Provide detailed responses that are concise and informative without being overwhelming.
Practical Application and Real-World Examples: Emphasize practical application with real-world examples.
Clarification Requests: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice.
Professional and Approachable Tone: Maintain a professional yet approachable tone.
Use of Everyday Analogies: When discussing technical concepts, use everyday analogies to make them more accessible and understandable.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Optimized YAML Prompt (PROMPT_B)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;prompt_b = """&lt;/span&gt;
&lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Role: "Architect Guide"&lt;/span&gt;
  &lt;span class="s"&gt;Purpose: Help developers skilled in module-level coding grow into understanding and managing full project architectures.&lt;/span&gt;

&lt;span class="na"&gt;guidelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;- Teach project architecture fundamentals: modular communication, standardization, and structure.&lt;/span&gt;
  &lt;span class="s"&gt;- Explain module integration within larger systems using examples and case studies.&lt;/span&gt;
  &lt;span class="s"&gt;- Compare architectural styles, discuss suitability, and share learning resources.&lt;/span&gt;
  &lt;span class="s"&gt;- Provide practical exercises for real-world application.&lt;/span&gt;
  &lt;span class="s"&gt;- Analyze multi-layered software (frontend, backend, data storage) to illustrate architecture.&lt;/span&gt;
  &lt;span class="s"&gt;- Offer educational insights: review README files and source code for comprehension.&lt;/span&gt;
  &lt;span class="s"&gt;- Use diagrams and visuals to clarify system interactions.&lt;/span&gt;
  &lt;span class="s"&gt;- Prefer clarity over jargon; use plain, accessible language.&lt;/span&gt;
  &lt;span class="s"&gt;- Focus on architecture concepts — no coding solutions.&lt;/span&gt;
  &lt;span class="s"&gt;- Be detailed yet concise; avoid information overload.&lt;/span&gt;
  &lt;span class="s"&gt;- Include real-world examples for practical relevance.&lt;/span&gt;
  &lt;span class="s"&gt;- Ask clarifying questions about unclear project details.&lt;/span&gt;
  &lt;span class="s"&gt;- Maintain a professional, approachable tone.&lt;/span&gt;
  &lt;span class="s"&gt;- Use everyday analogies for complex concepts.&lt;/span&gt;

&lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Clear, didactic, structured.&lt;/span&gt;
  &lt;span class="s"&gt;Encourage understanding of architecture as a living system, not just code components.&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Observation:&lt;/strong&gt; The output quality didn’t just stay the same — it improved. ChatGPT understood the intent better, and responses became more focused.&lt;/p&gt;




&lt;h3&gt;
  
  
  Experiment Code Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Universal adapter
# using [llm-api-adapter](https://github.com/Inozem/llm_api_adapter)
# makes it easy to switch between different providers for testing
# can be installed easily via: pip install llm-api-adapter
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&lt;/span&gt;

&lt;span class="n"&gt;messages_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_a&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&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;Help me to create weather application.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;messages_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt_b&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&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;Help me to create weather application.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&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="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;gpt-5-nano&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;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="c1"&gt;# Runs
&lt;/span&gt;&lt;span class="n"&gt;resp_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&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="n"&gt;messages_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&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="n"&gt;messages_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Token savings
&lt;/span&gt;&lt;span class="n"&gt;tokens_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;
&lt;span class="n"&gt;tokens_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;
&lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens_a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tokens_b&lt;/span&gt;
&lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tokens_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tokens_a&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROMPT A tokens:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROMPT B tokens:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved tokens:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Relative saving: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Cost
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cost A:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_input&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cost B:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_input&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Prompt I used to generate the new YAML-formatted version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Optimize this prompt into YAML format
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Conclusion:&lt;/strong&gt; Structured prompts are not just cleaner — they’re cheaper. Try YAML structuring in your next LLM project. It’s simple, reproducible, and can cut your costs by ~30%.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>performance</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Unifying 3 LLM APIs in Python: OpenAI, Anthropic &amp; Google with one SDK</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Tue, 04 Nov 2025 12:34:19 +0000</pubDate>
      <link>https://dev.to/inozem/unifying-3-llm-apis-in-python-openai-anthropic-google-with-one-sdk-4l2</link>
      <guid>https://dev.to/inozem/unifying-3-llm-apis-in-python-openai-anthropic-google-with-one-sdk-4l2</guid>
      <description>&lt;p&gt;A year ago, I released the first version of &lt;strong&gt;LLM API Adapter&lt;/strong&gt; — a lightweight SDK that unified OpenAI, Anthropic, and Google APIs under one interface.  &lt;/p&gt;

&lt;p&gt;It got &lt;strong&gt;7 ⭐ on GitHub&lt;/strong&gt; and valuable feedback from early users.&lt;br&gt;&lt;br&gt;
That was enough motivation to take it to the next level.  &lt;/p&gt;


&lt;h2&gt;
  
  
  What changed in the new version
&lt;/h2&gt;

&lt;p&gt;The new version (&lt;strong&gt;v0.2.2&lt;/strong&gt;) is now:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SDK-free&lt;/strong&gt; — it talks directly to provider APIs, no external dependencies.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified&lt;/strong&gt; — one &lt;code&gt;chat()&lt;/code&gt; interface for all models (OpenAI, Anthropic, Google).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent&lt;/strong&gt; — automatic token and cost tracking.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilient&lt;/strong&gt; — consistent error taxonomy across providers (auth, rate, timeout, token limits).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested&lt;/strong&gt; — 98% unit test coverage.
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Example: chat with any LLM
&lt;/h2&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;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&lt;/span&gt;

&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&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="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;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapter&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="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;system&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;Be concise.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain how LLM adapters work.&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Switching models is as simple as changing two parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&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="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;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or
&lt;/span&gt;&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Token &amp;amp; cost tracking example
&lt;/h2&gt;

&lt;p&gt;Every response now includes full token and cost accounting — no manual math needed.&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;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&lt;/span&gt;

&lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;google_api_key&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;google&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;chat_params&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_output&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost_total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;512 tokens (0.00025 USD)
137 tokens (0.00010 USD)
649 tokens (0.00035 USD)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;Working with multiple LLMs used to mean rewriting the same code — again and again.&lt;br&gt;&lt;br&gt;
Each SDK had its own method names, parameter names, and error classes.  &lt;/p&gt;

&lt;p&gt;So I built a unified interface that abstracts those details.&lt;br&gt;&lt;br&gt;
One adapter — one consistent experience.  &lt;/p&gt;


&lt;h2&gt;
  
  
  Join the project
&lt;/h2&gt;

&lt;p&gt;You can try it now:&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;llm-api-adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs &amp;amp; examples: &lt;a href="https://github.com/Inozem/llm_api_adapter" rel="noopener noreferrer"&gt;github.com/Inozem/llm_api_adapter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like the idea — ⭐ star it or share feedback in Issues.  &lt;/p&gt;

</description>
      <category>python</category>
      <category>openai</category>
      <category>anthropic</category>
      <category>gemini</category>
    </item>
    <item>
      <title>LLM API Adapter SDK for Python</title>
      <dc:creator>Sergey Inozemtsev</dc:creator>
      <pubDate>Thu, 14 Nov 2024 10:49:57 +0000</pubDate>
      <link>https://dev.to/inozem/llm-api-adapter-sdk-for-python-2bck</link>
      <guid>https://dev.to/inozem/llm-api-adapter-sdk-for-python-2bck</guid>
      <description>&lt;p&gt;Here is my LLM API Adapter SDK for Python that allows you to easily switch between different LLM APIs.&lt;/p&gt;

&lt;p&gt;At the moment, it supports: OpenAI, Anthropic, and Google. And only the chat function (for now).&lt;/p&gt;

&lt;p&gt;It simplifies integration and debugging as it has standardized error classes across all supported LLMs.&lt;/p&gt;

&lt;p&gt;It also manages request parameters like temperature, max tokens, and other settings for better control.&lt;/p&gt;

&lt;p&gt;To use the adapter, you need to download the library and obtain API keys for the LLMs you want. In the code, I demonstrated how easy it is to use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.messages.chat_message&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llm_api_adapter.universal_adapter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UniversalLLMAPIAdapter&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="nc"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a friendly assistant who explains complex concepts &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in simple terms.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi! Can you explain how artificial intelligence works?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;AIMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sure! Artificial intelligence (AI) is a system that can perform &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks requiring human-like intelligence, such as recognizing images &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;or understanding language. It learns by analyzing large amounts of &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data, finding patterns, and making predictions.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How does AI learn?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;gpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&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="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;gpt-3.5-turbo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;openai_api_key&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gpt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_chat_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpt_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;claude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&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="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;claude-3-haiku-20240307&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;anthropic_api_key&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;claude_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_chat_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claude_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UniversalLLMAPIAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-1.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;google_api_key&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;google_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_chat_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have explained everything in more detail in the documentation: &lt;a href="https://github.com/Inozem/llm_api_adapter" rel="noopener noreferrer"&gt;https://github.com/Inozem/llm_api_adapter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the first stage, and it is just the beginning. I'd love to hear your thoughts, feedback, or ideas on where it could go next.&lt;/p&gt;

&lt;h1&gt;
  
  
  GenAi #Python #LLM #OpenAI #GPT #Anthropic #Claude #Google #Gemini
&lt;/h1&gt;

</description>
    </item>
  </channel>
</rss>
