Short answer: put every model behind one API with one request shape, and make the model a string your code reads from config. With the LazySusan API, that's literal: GPT-6, Claude, Gemini, Grok, DeepSeek and Perplexity all take the same chat call, so going from anthropic/claude-sonnet-5-5 to openai/gpt-6.1-sol or google/gemini-3.8-flash is a one-string change. One key, one SDK, one prepaid balance, and one usage record that shows what each model cost you.
The rest of this post is the practical version: how to set it up, how to structure your code so switching stays cheap, and the handful of differences between models that no API can paper over.
Why multi-model code gets messy
Most teams don't plan to use several AI models. It just happens. Someone prototypes with GPT, someone else finds Claude writes better release notes, a third person needs Gemini's million-token context for a big document, and six months later the codebase has three SDKs in it.
Each one brings its own baggage:
- Different request formats. System prompts, tool definitions, image inputs and streaming events are shaped differently from lab to lab.
- Different errors. A rate limit, an overloaded model and an out-of-credit account all look different, so your retry logic forks three ways.
- Different keys and billing. Three dashboards, three invoices, three places to rotate a leaked key, and nobody can answer "what did AI cost us last month" without a spreadsheet.
- Different usage numbers. Token counts come back in different fields, so cost tracking turns into glue code.
None of this is hard on its own. It's the multiplication that hurts. Every new model costs you an integration, and every integration makes the next model switch less likely, which is exactly backwards when models improve every few months.
What "one API" should actually give you
An aggregation layer is only worth it if it removes the multiplication. That means:
- One request shape per category. Every chat model takes the same messages, tools and parameters. Every image model takes the same prompt, size and count. Every video model takes the same prompt, length and resolution.
- The model is just a string. Ids follow
provider/model, so they read clearly in config and logs. - One key and one balance for the whole team, with usage broken down by model and provider.
- The cost on every response, so you can compare models on real traffic, not guesses.
That's the design of the LazySusan API. It's our own API and our own SDKs for JavaScript/TypeScript and Python, and the full reference is in the API docs.
Setup in five minutes
Create an account in the developer console, buy a credit pack (from $25, no subscription), and create a key. The key is shown once, so put it straight into your environment as LAZYSUSAN_API_KEY.
Then install the SDK:
npm install lazysusan
# or
pip install lazysusanYour first call:
import LazySusan from "lazysusan";
const ls = new LazySusan(); // reads LAZYSUSAN_API_KEY
const reply = await ls.chat.create({
model: "anthropic/claude-sonnet-5-5",
messages: [{ role: "user", content: "Summarise this ticket in one line: ..." }],
});
console.log(reply.text);
console.log(reply.usage.costCredits, "credits");The same thing in Python:
from lazysusan import LazySusan
ls = LazySusan() # reads LAZYSUSAN_API_KEY
reply = ls.chat.create(
model="anthropic/claude-sonnet-5-5",
messages=[{"role": "user", "content": "Summarise this ticket in one line: ..."}],
)
print(reply.text, reply.usage.cost_credits)One credit is $0.001, so costCredits doubles as a price tag in tenths of a cent.
Make the model a config value, not a code change
The single most useful habit: never hard-code a model id deep in a function. Put it where you can change it without a deploy, and name it after the job, not the model.
// models.js: one place to decide which model does which job
export const MODELS = {
summarize: process.env.MODEL_SUMMARIZE ?? "google/gemini-3.8-flash",
draftReply: process.env.MODEL_DRAFT ?? "anthropic/claude-sonnet-5-5",
hardReasoning: process.env.MODEL_REASONING ?? "openai/gpt-6.1-sol",
};import { MODELS } from "./models.js";
export async function summarize(ls, text) {
const r = await ls.chat.create({
model: MODELS.summarize,
messages: [
{ role: "system", content: "Summarise in two sentences. Plain English." },
{ role: "user", content: text },
],
maxTokens: 400,
metadata: { feature: "summaries" },
});
return r.text;
}Now trying Claude Haiku 5.5 for summaries is an environment variable, and the metadata tag means the console can show you exactly what the summaries feature cost before and after. If you want a deeper take on which model to put in which slot, the routing cheat sheet goes through chat, image and video job by job.
Finding the right model id
You don't need to memorise ids. The models endpoint lists every model with its limits and price, and it doesn't need a key:
const chatModels = await ls.models.list({ type: "chat" });
for (const m of chatModels) console.log(m.id, "-", m.name);You'll see ids like openai/gpt-6-luna, anthropic/claude-opus-5-5, google/gemini-3.1-pro, xai/grok-4.7, deepseek/deepseek-v4-pro and perplexity/sonar-pro. The same list, with prices, is on the rate card.
Compare models on your own prompts
Benchmarks tell you how a model does on someone else's problem. The fastest way to choose is to run twenty or fifty of your real inputs through a few candidates and look at quality and cost side by side:
const candidates = [
"openai/gpt-6.1-sol",
"anthropic/claude-sonnet-5-5",
"google/gemini-3.8-flash",
"xai/grok-4.3",
];
for (const model of candidates) {
const r = await ls.chat.create({
model,
messages: [{ role: "user", content: samplePrompt }],
maxTokens: 600,
metadata: { experiment: "model-bakeoff" },
});
console.log(model, r.usage.costCredits, r.text.slice(0, 120));
}Tag experiments like this so they're easy to filter out of your production numbers later.
Fall back to another model when one is having a bad day
Providers have outages. With one request shape, a fallback is a loop rather than a second integration. The SDK already retries rate limits and briefly unavailable models (requests that were never charged), so your fallback only needs to handle the case where a model keeps failing:
import LazySusan, { LazySusanError } from "lazysusan";
const ls = new LazySusan();
const CHAIN = ["anthropic/claude-sonnet-5-5", "openai/gpt-6.1-sol", "google/gemini-3.8-flash"];
export async function chatWithFallback(messages) {
let lastError;
for (const model of CHAIN) {
try {
return await ls.chat.create({ model, messages, maxTokens: 1000 });
} catch (err) {
// 502/503: the model failed or is busy. You weren't charged, so try the next one.
if (err instanceof LazySusanError && (err.status === 502 || err.status === 503)) {
lastError = err;
continue;
}
throw err; // bad request, budget reached, bad key: switching models won't help
}
}
throw lastError;
}Notice what doesn't trigger a fallback: a 400 means the request itself is wrong, and a 402 means a balance or budget was reached. Sending the same request to a different model won't fix either.
What one API can't make identical
A shared request shape removes the plumbing differences. It doesn't make the models identical, and pretending otherwise is how fallbacks produce surprises. The ones worth knowing:
- Reasoning models think before they answer. GPT-6, Gemini 3.x, Grok, DeepSeek V4 Pro and Sonar Reasoning Pro spend part of
maxTokenson reasoning. Give them a few hundred tokens of headroom or you can get an empty answer with a finish reason oflength. - Some sampling knobs are ignored. OpenAI's GPT-6 models ignore
temperature,top_pand the penalties. Your call still works; the setting just doesn't do anything there. - JSON output isn't universal. If you ask for
responseFormaton a model that doesn't support it, you get a 400 and aren't charged. Test it on every model in your fallback chain. - Context windows differ. Claude, Gemini and Grok models take up to a million tokens of context; GPT-6 models take 400K; DeepSeek and Perplexity models are around 128K. A fallback from a long-context model needs to check the prompt fits.
- Not every model reads images. The DeepSeek and Perplexity models are text-only.
- Perplexity models search the web. Sonar models return
citations, and the search adds a per-request fee on top of tokens. - Prompts aren't perfectly portable. A prompt tuned for one model usually works on another, but tone and formatting drift. Keep a handful of golden examples per feature and re-check them when you switch.
The same idea for images and video
Chat is where most people start, but the same pattern covers generation. Images come from ls.images.generate with models like Nano Banana 2.1, GPT Image 2.5, FLUX 3, Midjourney V7 and Recraft V3; video comes from ls.videos.create with Veo 3.1, Runway Gen-4.5, Luma Ray 2, Hailuo 2.3 and others. Again, the model is a string:
const img = await ls.images.generate({
model: "bfl/flux-3", // or "google/nano-banana-2.1", "midjourney/v7", ...
prompt: "Product photo of a walnut lazy susan on a marble counter, soft daylight",
size: "1344x768",
});
console.log(img.images[0].url);There's a full walkthrough in image and video generation from one API.
What it costs
The API runs on prepaid credits, separate from LazySusan's app plans. Packs start at $25, credits don't expire, and you can turn on auto top-up so the balance refills from a saved card. Chat is priced per million tokens, images per image and video per clip, and every response includes what that request cost.
Two billing details matter when you're switching models a lot:
- You pay for what a request used. Each request reserves the most it could cost, then you're charged the actual amount and the rest is released straight away.
- Failures are free. If a provider errors, you aren't charged, which is what makes fallback chains safe to run.
A checklist before you ship
- Model ids live in config, named by job.
- Every call sets a sensible
maxTokens, with headroom for reasoning models. - Every call carries a
metadatatag for the feature it belongs to. - Your fallback chain only fires on 502 and 503, and every model in it has been tested with your prompts.
- Each service and environment has its own key (more on that in giving every developer their own API key).
Do that and the next time a better model ships, trying it is an afternoon, not a migration.
FAQ
Can I use GPT, Claude and Gemini with one API key? Yes. A LazySusan API key works with every chat, image and video model in the catalog, including OpenAI's GPT-6 models, Anthropic's Claude models and Google's Gemini models. You switch by changing the model id.
Do I need to rewrite my prompts when I switch models? Usually not, because the request shape is the same. It's still worth re-checking a few real examples, since tone and formatting vary between models, and reasoning models need more maxTokens headroom.
Which languages does the LazySusan SDK support? JavaScript/TypeScript (Node 18+, Bun, Deno and edge runtimes) and Python 3.9+. Any other language can call the HTTP API at https://www.lazysusan.ai/api/v1 with a bearer token.
Do I need a LazySusan subscription to use the API? No. The API runs on its own prepaid credits with no subscription or monthly minimum. App plans on the pricing page are separate.
What happens if a model is down? The request fails with a 502 or 503 and you aren't charged. The SDK retries briefly unavailable models automatically, and you can fall back to another model by catching those statuses.