Back to the blog
post.md

How to design a multi-LLM architecture with Claude, Gemini, and OpenAI

How to create a multi-LLM architecture with an AI Gateway, adapters, routing, fallback, cost control, and observability without over-abstracting.

Full Stack AIMulti-LLMClaudeGeminiOpenAIArchitecture

Using multiple AI models should not mean spreading calls to different APIs across the whole system. A central layer with adapters, routing, fallback, and observability helps organize that complexity.

Context

As AI tools evolve, it becomes common to test different providers for different tasks. One model may be better for long analysis. Another may be cheaper for simple classification. Another may have better multimodal support.

The problem appears when that diversity becomes coupling: one controller calls OpenAI, one worker calls Claude, another service calls Gemini, and each place handles errors, format, cost, and logs differently.

Multi-LLM architecture with backend connected to an AI Gateway and adapters for OpenAI, Claude, Gemini, and local model.
The gateway centralizes decisions, while adapters preserve the differences of each provider.

The role of an AI Gateway

The name matters less than the responsibility. An AI Gateway is an application layer that centralizes how to call models, validate responses, measure cost, apply fallback, and record behavior.

  • Standardize inputs and outputs.
  • Select model by task, cost, latency, or context.
  • Run retries and fallback with limits.
  • Apply cache or usage limits when they make sense.
  • Record metrics for cost, error, duration, and quality.
  • Validate responses before returning them to the domain.
ai-gateway.types.tsts
1type AIProviderId = 'openai' | 'anthropic' | 'google' | 'local';23type AIRequest = {4  feature: 'summary' | 'classification' | 'code_review';5  input: unknown;6  constraints: {7    maxLatencyMs?: number;8    maxCostCents?: number;9    requiresJson: boolean;10    requiresVision?: boolean;11  };12  trace: {13    userId?: string;14    projectId?: string;15    requestId: string;16  };17};1819type AIResponse<T = unknown> = {20  provider: AIProviderId;21  model: string;22  output: T;23  usage?: {24    inputTokens: number;25    outputTokens: number;26    estimatedCostCents: number;27  };28  latencyMs: number;29};3031interface AIProvider {32  generate<T>(request: AIRequest): Promise<AIResponse<T>>;33}

Adapters reduce coupling

The idea behind adapters is simple: each provider may have its own SDK, response format, parameters, and errors, but the rest of the application talks to a common interface.

This does not mean pretending all models are the same. Some capabilities are provider-specific. The goal is to isolate operational differences without hiding important decisions.

model-registry.tsts
1const modelRegistry = {2  cheapClassifier: {3    provider: 'openai',4    model: 'small-classifier',5    maxCostCents: 1,6  },7  longContextSummary: {8    provider: 'anthropic',9    model: 'large-context-model',10    maxInputTokens: 180_000,11  },12  multimodalAnalysis: {13    provider: 'google',14    model: 'vision-capable-model',15    requiresVision: true,16  },17} as const;

Routing and fallback need judgment

Routing can consider task type, cost, latency, context size, multimodal needs, expected quality, availability, and project restrictions.

  • Simple classification can use a cheaper model.
  • A large document may require a larger context window.
  • Image analysis needs a multimodal model.
  • A critical task can use a primary model plus additional validation.
  • A temporary failure can trigger retry; unavailability can trigger fallback.

Fallback sounds simple, but it can quickly duplicate cost if used without limits. You need to define which errors justify retrying, when to switch models, and when to return a controlled error.

routing-policy.jsonjson
1{2  "summary": {3    "primary": "longContextSummary",4    "fallback": ["cheapClassifier"],5    "retry": {6      "maxAttempts": 2,7      "retryOn": ["timeout", "rate_limit", "provider_unavailable"]8    }9  },10  "classification": {11    "primary": "cheapClassifier",12    "fallback": [],13    "retry": {14      "maxAttempts": 1,15      "retryOn": ["provider_unavailable"]16    }17  },18  "critical_review": {19    "primary": "longContextSummary",20    "validator": "cheapClassifier",21    "requiresHumanReview": true22  }23}

Observability is not a detail

If the application uses multiple models, I would like to answer simple questions: which model is used the most, which feature costs more, which provider fails more, when fallback was triggered, and how many responses were invalid.

For that, each call should record provider, model, tokens when available, estimated cost, feature, origin, duration, status, error, and result evaluation.

When it is not worth it

Not every project needs a multi-LLM architecture. If there is a single simple feature, one model, and low risk, creating an overly abstract layer may be too much.

Conclusion

Multi-LLM architecture is not about using several providers because it is trendy. It is about creating a more controlled way to decide when, how, and why each model enters a feature.

Good abstraction solves a real problem. Premature abstraction only replaces simple mess with sophisticated mess.