DEV Community

Alejandro Garcia
Alejandro Garcia

Posted on

I Ran Out of AI API Credits Mid-Build. Swapping Providers Took 20 Minutes.

It happened on a Tuesday night, two weeks into building the AI service for Materia AI — a bilingual menu-description generator I was building off real problems I'd seen as a waiter. I was testing description generation for the tenth item in a row when the request just... died. Not a timeout. Not a 500. A very polite "you're out of credits" from the Anthropic API.

I didn't have a backup plan. I had a backend that called anthropic.messages.create() and, if I'm honest, I assumed I'd deal with "what if this provider goes away" later. Later showed up faster than I expected.

Here's the part I didn't expect: swapping to Google's Gemini API took about 20 minutes. Not because I'm fast — because of a boring decision I'd made weeks earlier that I almost didn't bother making.

The decision that saved me

When I started building the backend, I split it into three layers instead of writing AI calls directly inside my route handlers: routers (HTTP layer), services (business logic — including the actual AI call), and schemas (Pydantic models defining what goes in and out).

# schemas.py
class DescriptionRequest(BaseModel):
    item_name: str
    ingredients: list[str]
    language: str = "en"

class DescriptionResponse(BaseModel):
    description: str
    language: str

# services/ai_service.py
async def generate_description(request: DescriptionRequest) -> DescriptionResponse:
    prompt = build_prompt(request)
    raw_text = await ai_client.complete(prompt)  # <- the only line that touches the provider
    return DescriptionResponse(description=raw_text, language=request.language)
Enter fullscreen mode Exit fullscreen mode

Notice ai_client.complete(). My router never imports Anthropic or Gemini. It imports generate_description. The provider is an implementation detail hidden behind one function call.

What the swap actually looked like

# ai_client.py — before
from anthropic import Anthropic
client = Anthropic(api_key=settings.ANTHROPIC_KEY)

async def complete(prompt: str) -> str:
    resp = client.messages.create(model="claude-...", messages=[{"role": "user", "content": prompt}], max_tokens=300)
    return resp.content[0].text

# ai_client.py — after
import google.generativeai as genai
genai.configure(api_key=settings.GEMINI_KEY)
model = genai.GenerativeModel("gemini-1.5-flash")

async def complete(prompt: str) -> str:
    resp = await model.generate_content_async(prompt)
    return resp.text
Enter fullscreen mode Exit fullscreen mode

One file changed. Zero routes touched. Zero schemas touched. My error-handling middleware, my validation, my endpoint tests — none of it cared which company wrote the model I was calling.

The lesson

I didn't build this separation because I was anticipating a credit shortage — I did it because "separate concerns" is what every FastAPI tutorial tells you to do, and I almost skipped it to move faster. Turns out the boring architectural decision is the one that saves you when the thing you didn't plan for happens: a price hike, a rate limit, a deprecated model, or — in my case — just running out of free credits at 11pm.

If your AI calls are sprinkled across your route handlers right now, this is your sign to pull them into one place before you need to.

This exact pattern — routers, services, and schemas kept separate, with the AI call isolated behind one function — is what I packaged into the Materia-AI Starter Kit, a FastAPI backend template you can grab if you want the fuller structure to start from instead of building it from scratch.

I'm currently open to freelance and full-time backend/AI-integration work — you can see more of what I've built at github.com/GerAle30.

Top comments (0)