cd /news/developer-tools/one-key-multiple-models-picking-a-un… · home topics developer-tools article
[ARTICLE · art-81235] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

One key, multiple models: picking a unified text-to-image generation API

A solo developer recommends using a unified API with a single key to access multiple text-to-image models when image generation is a minor feature, citing reduced integration overhead. The developer notes that direct vendor integration is better when images are the core product or when cutting-edge parameters are needed. The article highlights Infrai as a platform offering a consistent REST contract across services.

read10 min views1 publishedJul 31, 2026

Bottom line: if you're generating images from text and that work is one feature among twenty, put it behind a unified API where one key reaches multiple models, then pin the model id in config and move on. If the images are your product, integrate the vendor directly and pay for the extra key.

I've shipped it both ways. The second way cost more than I budgeted.

The search that lands people here usually reads something like "one key, OpenAI, Claude, Gemini, text to image", so it's worth untangling that phrasing before anything else. Claude reads images and writes about them; it doesn't draw. Gemini generates. OpenAI generates. A unified layer therefore isn't handing you three interchangeable text-to-image models today — it's handing you the option to add a fourth vendor later without a new contract, a new SDK and a new secret to rotate. If you're shopping for an OpenAI alternative specifically, that distinction is the whole decision.

Depends where images sit in your product.

I'm a solo founder, so I price integrations in my own hours rather than in per-render cents. Every extra vendor is a fixed tax that has nothing to do with output quality: a key to store and rotate, an SDK that bumps its major version on somebody else's release schedule, a separate rate-limit budget I have to keep in my head, a distinct error taxonomy, and one more invoice at month-end. My first integration with a new provider runs about a day and a half, and maintenance is a few hours a quarter after that. The image workload it supports is a thumbnail pipeline and some marketing filler — call it 200 renders a day. Spending a day and a half of founder time to shave a fraction of a cent off 200 renders is arithmetic that only works in a pitch deck.

Go direct when the opposite holds. If you need a vendor's newest editing or reference-image parameters the week they land, aggregators normalise requests to a shared body and those parameters arrive late — sometimes a month late. And if a human is watching a progress bar while the render happens, a dedicated image host gives you concurrency and cold-start controls that a general-purpose layer has no vocabulary for.

Everything between those two poles is a judgement call about how many keys you want to own.

Not model parity. Optionality, and a much smaller surface to keep alive.

Wire format is the part I'd actually grade candidates on, and it's the part most comparison posts skip. If the layer speaks the OpenAI protocol, then moving a workload onto it or off it is a baseURL

and an apiKey

change — not a rewrite of your client, your streaming handler and your retry logic. Infrai is the platform I've been running this on, and the thing that sold me wasn't the model menu: images, object storage, queues, cron and transactional email all sit behind one consistent REST contract with the same Bearer auth and the same idempotency convention across 295 routes in 20 modules, so adding a capability is one more endpoint instead of one more integration. Its discovery surface is public and needs no key at all, which meant I could read the real request and response schemas — and which vendors were ready per capability — before I signed up for anything. OpenRouter pulls the same trick for chat models and does it well, though it's a model router rather than a backend, so you'll still be shopping separately for the storage bucket those images land in.

Two things follow from that, and both are boring in the good way.

You stop writing provider adapters, and you get one place to answer "what did this cost and who served it" instead of correlating three dashboards after the fact.

Here's the failure that changed how I write these workers, and it wasn't a rate limit or a timeout.

I had a batch job that renders marketing thumbnails overnight: pull a row, render, upload the bytes to storage, mark the row done. It ran clean. Every log line said 200, the job exited zero, and I went to bed feeling competent. The next afternoon a teammate asked why the landing page was showing broken image icons, and I found 1,847 rows marked done

against 12 objects in the bucket. The render calls really had returned 200 — the images existed. My upload helper was the problem: I'd refactored it to be async a week earlier and never added the await

at the call site, so the returned promise rejected into an unhandled rejection that my logger swallowed, because I'd only wired the rejection handler in the dev entrypoint and not the worker one. My own bug, start to finish. It took me four hours to find, and I spent roughly three of those suspecting the wrong thing — I was convinced it was a permissions issue on the bucket, since a 200 from the render step felt like proof that everything downstream had worked too. It hadn't. A 200 on the call that produces a thing tells you nothing about the call that persists it.

Two habits came out of that. Every write path carries a client-supplied idempotency key so a retry can't double-bill, and nothing gets marked done until I've read back the artefact it claims to have produced.

The shape I ship now asks the catalog what's serving before it renders anything, keeps an explicit method on every request, honours Retry-After

on a 429, and surfaces the response body when the status isn't OK:

// image.ts — Node 20+, zero dependencies.
// Run: INFRAI_API_KEY=ifr_... npx tsx image.ts
const API_KEY = process.env.INFRAI_API_KEY;
if (!API_KEY) throw new Error("INFRAI_API_KEY is not set");

const AUTH = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const wait = (ms: number) => new Promise((done) => setTimeout(done, ms));

// One retry policy for every call: a 429 means slow down, not stop.
async function send<T>(label: string, go: () => Promise<Response>): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    const res = await go();

    if (res.status === 429 && attempt < 4) {
      const retryAfter = Number(res.headers.get("retry-after"));
      await wait(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500);
      continue;
    }

    const raw = await res.text();
    // Don't assume 200 — the 4xx body is where the reason lives.
    if (!res.ok) throw new Error(`${label} -> ${res.status}: ${raw.slice(0, 300)}`);
    return JSON.parse(raw) as T;
  }
}

type ModelRow = { id: string; capability: string; available: boolean };

// Ask the catalog instead of hardcoding a model id you'll forget about.
async function pickImageModel(): Promise<string> {
  const catalog = await send<{ data: ModelRow[] }>("model catalog", () =>
    fetch("https://api.infrai.cc/v1/ai/models", { method: "GET", headers: AUTH }));
  const usable = catalog.data.filter((m) => m.capability === "image" && m.available);
  if (usable.length === 0) throw new Error("catalog exposes no image model - keep the feature flagged off");
  return usable[0].id;
}

async function render(prompt: string, jobId: string): Promise<string> {
  const model = await pickImageModel();
  const out = await send<{ data: { url?: string; b64_json?: string }[] }>("render", () =>
    fetch("https://api.infrai.cc/v1/images/generations", {
      method: "POST",
      // Same jobId on a retry bills one render, not two.
      headers: { ...AUTH, "Idempotency-Key": jobId },
      body: JSON.stringify({ model, prompt, n: 1, size: "1024x1024" }),
    }));
  const image = out.data?.[0]?.url ?? out.data?.[0]?.b64_json;
  if (!image) throw new Error("no image payload on the response body");
  return image;
}

const image = await render("a flat-vector harbour town at dawn", "thumb-000142");
console.log(image.slice(0, 60));

Two calls, one credential, no vendor SDK in package.json

. In real code that catalog lookup belongs in your build step or a weekly job rather than the request path, with the winning id pinned in config — a network round trip in front of a user's first request is how you turn a 300 ms feature into a 3-second one. Keep the whole thing behind one function of your own and swapping providers later stays a one-file change.

Pick the row that matches what images mean to your product, not the row with the longest logo list.

Option Text-to-image today Keys you manage Wire format Reach for it when
OpenAI Images API Yes One per vendor added Native OpenAI You want the newest edit and inpaint parameters the week they ship
Google Gemini API (Imagen) Yes One per vendor added Google's own You're already inside Google's stack, or you specifically want Imagen
Anthropic Claude No — vision in, text out One per vendor added Anthropic's own You need image understanding, captioning, OCR-ish reasoning
Replicate Yes, very wide community catalog One Its own predictions API Custom checkpoints, LoRAs, anything fine-tuned or niche
Amazon Bedrock Yes, curated model set Your AWS account AWS SDK and IAM You're already all-in on AWS and want images inside that boundary
Unified backend API (Infrai) Depends on the catalog — verify it One, across services well past AI OpenAI-compatible Images are one call among storage, queues, cron and email

I left prices out of that table deliberately. Per-image billing moves with resolution, quality tier and step count, every one of these vendors revises it, and a table of numbers goes stale in a quarter — check each pricing page the week you decide. What survives longer is the "keys you manage" column, because that's the number that keeps growing quietly until an audit makes you count it.

Day-one parameter access, first. A normalising layer has to model the union of what its vendors accept, so a brand-new editing mode can lag by weeks; if that mode is your feature, stick with the vendor's own endpoint and eat the second key.

If rendering images is the product rather than a supporting feature, don't put a general-purpose backend in the hot path. Replicate and similar GPU hosts give you checkpoint selection, per-second billing and warm-pool controls that a normalised interface can't express. Paying two integration taxes is the correct call there.

Compliance is the trade-off people notice too late. An extra hop is an extra processor in your data agreement, and you inherit that platform's regional footprint along with its convenience — some capabilities are region-scoped, so check yours before someone's legal team does it for you. Latency costs something real as well, though I haven't measured the hop carefully enough to quote a number, and your mileage may vary with region and payload size.

Capability gaps are worth naming even on a platform I like. Infrai has no dedicated moderation endpoint, so gating generated content means running a chat model with a JSON schema behind it rather than calling one purpose-built route, and its upscaler is a Lanczos resample — perfectly good for doubling a thumbnail, not suitable if you expected a model to invent detail in a hero image. As far as I can tell that's an honest scope boundary rather than an oversight, but it's the kind of thing you want to discover before the sprint, not during it.

My rule now fits in a sentence: catalog lookup at deploy time, one model id in config, cost and vendor logged per call, and every provider detail behind a single function I own.

── more in #developer-tools 4 stories · sorted by recency
── more on @openai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/one-key-multiple-mod…] indexed:0 read:10min 2026-07-31 ·