DEV Community

gentleforge
gentleforge

Posted on

How I Cut Our AI API Bill by 95% — The Engineering Playbook

How I Cut Our AI API Bill by 95% — The Engineering Playbook

When our finance lead forwarded me the AWS bill for March, I almost choked on my coffee. We were a team of nine engineers shipping AI features, and somehow we'd burned through enough on inference to cover two salaries. The worst part? I hadn't even noticed because the charges were scattered across OpenAI, Anthropic, and a couple of side experiments. That's the moment I decided to actually treat LLM spending like a real infrastructure problem instead of a credit card swipe.

What follows is the playbook I wish I'd had on day one. These aren't theoretical tips — they're the exact moves I made across three products to get our run-rate down to roughly 5% of where it started, without shipping worse software.


The Harsh Truth About Model Defaults

Here's the dirty secret nobody tells you in the LLM hype cycle: most teams default to the most famous model for every single call. GPT-4o for everything. Claude Sonnet for everything. Then they wonder why their "simple AI feature" costs them a kidney.

The model selection decision is where I recovered the majority of my budget. When you look at it rationally, the gap between the flagship tier and the cheap-tier models is absurd for tasks that don't require frontier reasoning.

This is the matrix I landed on, and it still governs our routing today:

Task What I Used To Use What I Use Now Cost Cut
Simple chat GPT-4o ($10/M out) DeepSeek V4 Flash ($0.25/M) 97.5%
Classification GPT-4o-mini ($0.60/M) Qwen3-8B ($0.01/M) 98.3%
Code generation GPT-4o ($10/M) DeepSeek Coder ($0.25/M) 97.5%
Summarization GPT-4o ($10/M) Qwen3-32B ($0.28/M) 97.2%
Translation GPT-4o ($10/M) Qwen-MT-Turbo ($0.30/M) 97%

I want you to really sit with the classification row. Qwen3-8B at $0.01 per million output tokens. That's sixty times cheaper than GPT-4o-mini. For a binary sentiment classifier, the accuracy difference in my benchmarks was under 1.5 percentage points. The ROI math isn't even close.

The code for the basic version looks like this. I run this through Global API so I have a single billing surface and zero vendor lock-in:

from openai import OpenAI

client = OpenAI(
    api_key="sk-global-xxxxx",
    base_url="https://global-apis.com/v1"
)

MODEL_MAP = {
    "chat": "deepseek-v4-flash",
    "code": "deepseek-coder",
    "simple": "Qwen/Qwen3-8B",
    "reasoning": "deepseek-reasoner",
}

def pick_model(user_input: str) -> str:
    complexity = classify_complexity(user_input)
    return MODEL_MAP[complexity]

def chat(user_input: str) -> str:
    model = pick_model(user_input)
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": user_input}]
    )
    return resp.choices[0].message.content
Enter fullscreen mode Exit fullscreen mode

That classify_complexity function is itself a cheap-model call. Qwen3-8B looks at the input, decides if it's a "simple" or "reasoning" task, and we route accordingly. The classifier costs us fractions of a cent per request.


Tiered Routing: The Architecture That Saved My Quarter

Model selection is the foundation. Tiered routing is what made the system actually production-ready at scale.

The idea: don't ask the expensive model unless you've earned it. I built a three-tier ladder where most requests die on tier one, the harder ones climb to tier two, and only the genuinely hard ones hit the premium tier.

def smart_generate(prompt: str, max_budget: float = 0.50) -> str:
    cheap_resp = call_model("Qwen/Qwen3-8B", prompt)
    if quality_check(cheap_resp) >= 0.8:
        return cheap_resp

    # Tier 2 — Standard at $0.25/M
    mid_resp = call_model("deepseek-v4-flash", prompt)
    if quality_check(mid_resp) >= 0.9:
        return mid_resp

    # Tier 3 — Premium at $0.78 to $2.50/M
    return call_model("deepseek-reasoner", prompt)
Enter fullscreen mode Exit fullscreen mode

In my actual deployment, roughly 80% of requests resolved on tier one, about 15% escalated to tier two, and only 5% ever saw the reasoning model. The quality_check function runs a small evaluator — sometimes another cheap-model-as-judge, sometimes a heuristic for confidence — and gates the escalation.

I shipped this into our customer support chatbot in February. We went from $420/month to $28/month. Same product, same users, same answer quality (we ran blind A/B tests against human raters and the satisfaction scores were statistically indistinguishable). The CFO literally emailed me a thank-you.

The architecture decision here matters more than the numbers. Once you commit to tiered routing, you've built a system that can absorb new model releases without rewriting any application code. When DeepSeek dropped a new flash model last quarter, I swapped it into tier two and saved another 30% with a one-line config change. That's the lock-in escape hatch I want from any AI infrastructure.


Caching: The Layer Most Startups Skip

I'll be honest: I was skeptical about response caching at first. "How often do users actually ask the same question twice?" I thought. Then I instrumented it and shut up.

FAQ bots, documentation search, "what are your hours," "explain your refund policy" — these hit cache at absurd rates. We're seeing 50–80% cache hit rates on our support surfaces, which means literally half of those requests cost us nothing.

Here's the version I run in production. Nothing fancy, just a Redis layer in front of the model call:

import hashlib
import json
import time

def cached_chat(model: str, messages: list, ttl: int = 3600) -> dict:
    key = hashlib.md5(
        json.dumps({"model": model, "messages": messages}).encode()
    ).hexdigest()

    entry = cache.get(key)
    if entry and time.time() - entry["time"] < ttl:
        return entry["response"]

    response = client.chat.completions.create(
        model=model, messages=messages
    )
    cache[key] = {"response": response, "time": time.time()}
    return response
Enter fullscreen mode Exit fullscreen mode

The semantics matter. I normalize whitespace, lowercase the input, and strip out timestamps before hashing, otherwise nothing ever matches. For semantic caching — "did the user ask something similar even if not identical" — I embed the input with a cheap embedding model and cosine-search the last 24 hours of answers. That added another 15% cache hits on top of the exact-match layer.

A subtle but important point: caching also removes tail-latency variance. Your p99 gets dramatically better when a third of requests don't even leave your cache server. That's a UX win you don't get on a spreadsheet.


Prompt Compression: Where Token Math Gets Ugly

I used to think prompt engineering was about clever wording. Turns out prompt engineering is also prompt accounting. Every token in your system prompt is a token you pay for, on every request, forever.

I had a RAG feature where the system prompt had grown to about 2,000 tokens of instructions, examples, and context. At 10,000 requests a day, on DeepSeek V4 Flash at $0.25/M output, that input overhead alone was bleeding cash.

Here's the compression pattern I rolled out:

def compress_prompt(text: str, target_ratio: float = 0.5) -> str:
    if len(text) < 500:
        return text
    summary = call_model(
        "Qwen/Qwen3-8B",
        f"Summarize this in {int(len(text) * target_ratio)} chars: {text}"
    )
    return summary
Enter fullscreen mode Exit fullscreen mode

Compressing that 2,000-token system prompt down to 400 tokens saved $0.024 per request. Multiply by 10,000 daily requests: $240/day, which annualizes to $87,600/year. From a single prompt. I genuinely had to double-check the arithmetic.

The trick is that the compression itself uses a cheap model, so the meta-call costs almost nothing. You're trading a fraction of a cent of compute for permanent savings on every downstream call. At scale, this is one of the cleanest ROI plays in the whole playbook.


Batch Processing: Don't Make N Calls When 1 Will Do

This one burned me early. I had a pipeline that processed user feedback by calling the model once per comment. A user with 50 comments triggered 50 separate API calls. Each call paid full price on the input tokens because each one had to re-ship the system prompt.

The fix is the obvious one: batch.

# Before — N round trips, N system-prompt overheads
for comment in comments:
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=[{"role": "user", "content": comment}]
    )

# After — 1 round trip, 1 system-prompt overhead
batch_prompt = "\n".join(f"[{i}] {c}" for i, c in enumerate(comments))
response = client.chat.completions.create(
    model="deepseek-v4-flash",
    messages=[{
        "role": "system",
        "content": "Classify each comment. Return JSON list of {id, label}."
    }, {
        "role": "user",
        "content": batch_prompt
    }]
)
Enter fullscreen mode Exit fullscreen mode

The savings here are typically 10–20% on top of everything else, but the bigger gain is throughput. You go from N sequential round trips to one, and your wall-clock time drops proportionally. For any background processing job — bulk classification, nightly summarization, batch enrichment — this is non-negotiable.


Putting It All Together

Let me sketch what the production stack actually looks like, because no single strategy lives in isolation:

  1. Tiered router classifies each incoming request into a complexity bucket.
  2. Cache layer sits in front of the model. Exact-match first, semantic fallback second.
  3. Compressed prompts get loaded from a versioned prompt store, already trimmed.
  4. Cheap model handles the easy 80%, mid-tier handles the next 15%, premium handles the 5%.
  5. Batch aggregator collects async work and flushes every N seconds or M items.

The whole thing talks to models through a single unified endpoint. That's the architectural decision that ties this all together and keeps me out of vendor jail. I'm running everything through https://global-apis.com/v1 with the OpenAI Python SDK, which means I can swap models, switch providers, or A/B test a new vendor by changing a string in my config. No rewrites. No SDK migrations. No panic when one provider has a regional outage.

Here's the production-shaped version with all the layers wired up:


python
from openai import OpenAI
import hashlib, json, time

client = OpenAI(
    api_key="sk-global-xxxxx",
    base_url="https://global-apis.com/v1"
)

CACHE = {}

def cache_key(model, messages):
    return hashlib.md5(
        json.dumps({"model": model, "messages": messages}).encode()
    ).hexdigest()

def call_with_cache(model, messages, ttl=3600):
    k = cache_key(model, messages)
    hit = CACHE.get(k)
    if hit and time.time() - hit["t"] < ttl:
        return hit["r"]

    resp = client.chat.completions.create(model=model, messages=messages)
    CACHE[k] = {"r": resp, "t": time.time()}
    return resp

def production_generate(user_input: str) -> str:
    # Compress long user input
    compressed = compress_prompt(user_input)

    # Tier 1
    r1 = call_with_cache("Qwen/Qwen3-
Enter fullscreen mode Exit fullscreen mode

Top comments (0)