Orchestrating Multiple AI Models:
Architecture Decisions

Tradeoffs between OpenAI, Anthropic, and Gemini — and how to design a multi-model routing system that actually scales in production without becoming a maintenance nightmare.

Sayan Dutta
Sayan Dutta
Full-Stack Developer & AI Enthusiast · IIT Patna

Builder of the AI Integration Hub — a unified layer that orchestrates OpenAI, Anthropic, and Gemini behind a single API. This is the real architecture and the hard-won tradeoffs behind it.


When I first started integrating large language models into projects, I made the same mistake everyone makes: I picked one provider, wired it in directly, and shipped. Fast. Felt good. Then two months later I wanted to switch models for a specific task, and I realised my entire codebase was coupled to one vendor's SDK, response format, and error contract.

That's when I started building the AI Integration Hub — a routing layer that sits between my applications and every AI provider I use. This post is the technical breakdown of why it exists, how it's structured, and what I learned about the real tradeoffs between OpenAI, Anthropic, and Google Gemini that you won't find in any marketing page.

3Providers unified
1API interface
200+Req/day in testing

The Problem With Direct Integration

Every AI provider has its own SDK, its own request shape, its own error codes, its own rate-limiting behaviour, and its own way of streaming tokens. None of them agree on anything. Here's a simplified illustration of the same "summarise this text" task across three providers:

javascript — three APIs, three different contracts
// OpenAI
const r1 = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: text }]
});
const out1 = r1.choices[0].message.content;

// Anthropic
const r2 = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: text }]
});
const out2 = r2.content[0].text;

// Gemini
const r3 = await model.generateContent(text);
const out3 = r3.response.text();

Three different method names. Three different response shapes. Three different error types to catch. And that's before you add streaming, retry logic, or token counting. Every time you add a new provider or upgrade a model version, you touch application code. That's the problem the Hub solves.

The Architecture

The Hub is a Node.js service that exposes a single unified endpoint. All provider complexity lives inside it — the application never knows which model handled its request.

clients
Blix.ai (beta)
Pathology App
Any project
AI Integration Hub — POST /complete
internal layers
Redis Cache
Task Router
Retry Handler
providers
OpenAI GPT-4o
Anthropic Claude
Google Gemini

The request flow from any client is always the same: hit POST /complete with a task type, a prompt, and optional parameters. The Hub handles everything else — which provider to call, how to format the request, how to parse the response, and what to do when something fails.

The Routing Logic

The most important decision in a multi-model system isn't which models to support — it's how you decide which model handles a given request. Get this wrong and you're paying GPT-4o prices for tasks that a smaller model handles just as well, or routing safety-critical prompts to a model that hallucinates more freely.

Here's the decision logic I settled on after testing all three providers extensively:

1
Check the cache first. Identical prompts with identical parameters return the cached response. LLM calls are expensive and slow — don't repeat them.
2
Read the task type. code_review, summarise, creative, factual_qa, safety_check — each maps to a preferred provider based on benchmarked performance.
3
Check provider health. If the preferred provider returned a 429 or 5xx in the last 60 seconds, route to the fallback. Never queue — fail fast to the next option.
4
Normalise the response. Whatever comes back — OpenAI's choices[0].message.content, Anthropic's content[0].text, Gemini's response.text() — the Hub returns the same shape every time.
5
Log and cache. Write response metadata to Redis with a TTL. Log token usage, latency, and provider used for cost monitoring.
javascript — unified response shape
// What every client receives, regardless of provider
{
  text:       "The model's response here",
  provider:   "anthropic",           // which provider served it
  model:      "claude-sonnet-4-6",   // exact model used
  tokens: {
    input:    142,
    output:   389,
    total:    531
  },
  latencyMs:  1240,
  cached:     false
}

The Real Tradeoffs: Provider by Provider

Every AI provider's marketing copy says the same things — fast, accurate, safe, cheap. Here's what I actually found after routing thousands of requests through all three:

Dimension OpenAI GPT-4o Anthropic Claude Google Gemini
Code generation ✓ Excellent ✓ Excellent ~ Good
Long context (100k+) ~ Good ✓ Best-in-class ✓ Excellent
Following instructions ✓ Strong ✓ Very strong ~ Inconsistent
Refusing harmful prompts ~ Moderate ✓ Most cautious ~ Moderate
API reliability ✓ High ✓ High ~ Occasional issues
Cost per 1M tokens ~ Mid ~ Mid ✓ Lowest
SDK ergonomics ✓ Best ✓ Very good ✗ Fragmented
Streaming support ✓ Solid ✓ Solid ~ Workable
ℹ️
My default routing rules from this data: code review and instruction-following tasks → Anthropic. Creative generation and general Q&A → OpenAI. High-volume, cost-sensitive summarisation → Gemini. Long document analysis → Anthropic or Gemini depending on context window needs.

The Caching Layer: More Impactful Than I Expected

I added Redis caching as an afterthought. It turned out to be the single highest-impact performance optimisation in the whole system.

The cache key is a hash of the normalised prompt + task type + model family (not exact model — so a model version bump doesn't invalidate everything). TTL varies by task type: factual Q&A caches for 24 hours, creative tasks for 1 hour, anything with a timestamp in the prompt doesn't cache at all.

javascript — cache key generation
function buildCacheKey(req) {
  const normalised = req.prompt
    .trim()
    .toLowerCase()
    .replace(/\s+/g, ' ');          // collapse whitespace

  const payload = JSON.stringify({
    p: normalised,
    t: req.task,
    f: modelFamily(req.task)      // "openai" | "anthropic" | "gemini"
  });

  return `hub:` + crypto
    .createHash('sha256')
    .update(payload)
    .digest('hex')
    .slice(0, 32);
}

const TTL = {
  factual_qa:   86400,   // 24h
  summarise:    3600,    // 1h
  code_review:  1800,    // 30min
  creative:     900,     // 15min
  safety_check: 0        // never cache
};

In practice, cache hit rates during Blix.ai beta testing — where classmates repeatedly ask similar CS theory questions — are around 38–45%. That's more than a third of all requests served from cache with sub-5ms latency and zero API cost.

Retry Logic and Graceful Degradation

Rate limit errors (429) and transient server errors (500, 503) are a fact of life with any AI provider. The Hub handles them with a three-tier strategy:

🚨
What I got wrong initially: I was retrying on the same provider with exponential backoff. This is a bad pattern for LLM APIs — if you're rate-limited, the provider doesn't care how elegantly your backoff is configured. Fail fast to a different provider instead. Your users get a response; the rate limit window resets in the background.

What the Hub Does NOT Do

Scope creep is the enemy of reliable infrastructure. The Hub deliberately does not handle these things, despite being tempting to add:

"Make it work, make it right, make it fast — in that order." The Hub started as 80 lines of routing glue. The discipline of not adding features until they were clearly needed kept it maintainable as it grew.

Cost Monitoring in Practice

Every response logs { provider, model, inputTokens, outputTokens, latencyMs, taskType, cached } to a lightweight append-only log. A small cron job aggregates this daily into a cost summary. For the first month of running the Blix.ai beta through the Hub, the breakdown looked roughly like:

Without the Hub, every single request would have gone to one provider at full cost. With routing and caching, real-world spend during testing was roughly 55% lower than a naive single-provider setup would have cost.

💡
Practical tip: Log everything from day one, even if you don't have a dashboard yet. Token usage patterns tell you which tasks are expensive, which prompts are bloated, and where semantic caching would have the most impact. You can't optimise what you haven't measured.

Is a Multi-Model Architecture Right for You?

Honestly — not always. If you're building a quick prototype or a side project that calls one provider for one type of task, a routing layer adds complexity you don't need. Direct integration is fine.

You should consider building something like the Hub when:

The Hub took about three weeks to build properly. It's saved me far more than that in maintenance time and API costs since. The code is on my GitHub — feel free to fork it as a starting point.

Sayan Dutta
Sayan Dutta
Full-Stack Developer & AI Enthusiast · IIT Patna
GitHub Get in touch