Prompt
How do I centralize API calls across OpenAI, Anthropic, and Google models?
Latest observation
A good way to centralize API calls across OpenAI, Anthropic, and Google is to build a single provider-agnostic wrapper with:
- a common request/response schema
- provider adapters that translate that schema into each vendor’s API
- a routing layer to choose the model/provider
- shared handling for auth, retries, streaming, logging, and errors
1) Define one internal interface
Keep your app talking to one function, not three SDKs.
Example normalized request:
type ChatMessage = {
role: "system" | "user" | "assistant";
content: string;
};
type LLMRequest = {
provider?: "openai" | "anthropic" | "google";
model: string;
messages: ChatMessage[];
temperature?: number;
maxTokens?: number;
stream?: boolean;
};
And a normalized response:
type LLMResponse = {
text: string;
raw?: unknown;
usage?: {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
};
};
2) Write adapter functions per provider
Each adapter:
- maps your normalized request to the vendor’s request format
- calls the vendor SDK/API
- converts the response back into your normalized response
OpenAI adapter
async function callOpenAI(req: LLMRequest): Promise<LLMResponse> {
// map messages, temperature, maxTokens, etc.
// use OpenAI SDK
return {
text: "response text",
raw: {},
};
}
Anthropic adapter
async function callAnthropic(req: LLMRequest): Promise<LLMResponse> {
// Anthropic uses a slightly different message format and system prompt handling
return {
text: "response text",
raw: {},
};
}
Google adapter
async function callGoogle(req: LLMRequest): Promise<LLMResponse> {
// Google Gemini APIs have their own content structure
return {
text: "response text",
raw: {},
};
}
3) Add a central dispatcher
async function callLLM(req: LLMRequest): Promise<LLMResponse> {
switch (req.provider) {
case "openai":
return callOpenAI(req);
case "anthropic":
return callAnthropic(req);
case "google":
return callGoogle(req);
default:
throw new Error(`Unsupported provider: ${req.provider}`);
}
}
4) Normalize model names and capabilities
Create a config map so your app doesn’t need vendor-specific strings everywhere.
const MODELS = {
"gpt-4.1": { provider: "openai" },
"claude-3.5-sonnet": { provider: "anthropic" },
"gemini-2.0-flash": { provider: "google" },
};
You can also store:
- context window
- supports streaming
- supports images
- tool/function calling support
- pricing info
5) Centralize cross-cutting concerns
Put these in the wrapper, not per call site:
- Authentication: env vars / secret manager
- Retries: exponential backoff for 429/5xx
- Timeouts
- Rate limiting
- Observability: logs, traces, token usage
- Fallbacks: if one provider fails, route to another
- Caching: for repeated prompts
- Safety / moderation: if needed
6) Handle streaming separately but consistently
Streaming APIs differ a lot, so create one abstraction like:
async function* streamLLM(req: LLMRequest): AsyncGenerator<string> {
// provider-specific stream parsing
}
Then your UI only consumes text chunks, regardless of provider.
7) Consider using a unified library if you don’t want to maintain adapters
If you want faster setup, look at libraries/frameworks that already normalize providers:
- LiteLLM
- LangChain
- Vercel AI SDK (for some use cases)
- OpenRouter as a single API gateway to multiple models
These can reduce adapter work, but you’ll trade off some control.
Recommended architecture
App code
→ callLLM()
→ ProviderRouter
→ OpenAIAdapter | AnthropicAdapter | GoogleAdapter
→ vendor SDK/API
Practical tip
Start by centralizing only:
- chat/completions
- error handling
- model selection
Then expand to:
- tools/function calling
- streaming
- embeddings
- vision
- structured output
If you want, I can give you:
- a TypeScript implementation
- a Python implementation
- a production-ready architecture with retries, streaming, and fallbacks