How Dexter Supports Multiple LLM Providers: OpenAI, Anthropic, and Google
Dexter supports multiple LLM providers through a provider registry, model factories, and prefix-based resolution that automatically routes requests to OpenAI, Anthropic, Google, or Ollama based on the model name.
The virattt/dexter repository implements a clean abstraction layer that lets you switch between LLM providers without changing your application code. By leveraging a centralized registry in src/providers.ts and factory methods in src/model/llm.ts, Dexter resolves the correct provider at runtime based on model name prefixes like claude-, gemini-, or gpt-.
The Three-Pillar Architecture
Dexter’s multi-provider support rests on three tightly-coupled components that handle declaration, instantiation, and routing.
Provider Registry
The registry in src/providers.ts declares every supported provider with metadata required for routing and configuration. Each entry includes the provider ID, display name, model prefix, environment variable for the API key, and a designated fast model for quick tasks.
// src/providers.ts
export interface ProviderDef {
id: string;
displayName: string;
modelPrefix: string;
envVar: string;
fastModel: string;
}
export const PROVIDERS: ProviderDef[] = [
{
id: 'anthropic',
displayName: 'Anthropic',
modelPrefix: 'claude-',
envVar: 'ANTHROPIC_API_KEY',
fastModel: 'claude-haiku-4-5'
},
{
id: 'google',
displayName: 'Google',
modelPrefix: 'gemini-',
envVar: 'GOOGLE_API_KEY',
fastModel: 'gemini-1.5-flash'
},
// ... OpenAI, Ollama entries
];
Model Factories
The factory pattern in src/model/llm.ts maps each provider ID to a LangChain chat class constructor. When getChatModel() receives a provider ID, it selects the appropriate factory, instantiates the class (e.g., ChatAnthropic, ChatGoogleGenerativeAI, ChatOpenAI), and injects the API key from the corresponding environment variable.
// src/model/llm.ts
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { ChatOpenAI } from '@langchain/openai';
import { ChatOllama } from '@langchain/community/chat_models/ollama';
const MODEL_FACTORIES: Record<string, (apiKey: string, model: string) => BaseChatModel> = {
anthropic: (key, model) => new ChatAnthropic({ apiKey: key, modelName: model }),
google: (key, model) => new ChatGoogleGenerativeAI({ apiKey: key, model: model }),
openai: (key, model) => new ChatOpenAI({ apiKey: key, modelName: model }),
ollama: (key, model) => new ChatOllama({ baseUrl: process.env.OLLAMA_BASE_URL, model: model }),
};
Resolution and Dispatch
The resolveProvider() function in src/providers.ts determines which provider to use by matching the model name against the modelPrefix of each registered provider. If no prefix matches, it defaults to OpenAI. The getChatModel() function in src/model/llm.ts then uses this provider ID to select the correct factory and return a configured BaseChatModel instance.
// src/providers.ts
export function resolveProvider(model: string): ProviderDef {
const provider = PROVIDERS.find(p => model.startsWith(p.modelPrefix));
return provider || defaultProvider; // defaultProvider is OpenAI
}
// src/model/llm.ts
export function getChatModel(model: string): BaseChatModel {
const provider = resolveProvider(model);
const apiKey = getApiKey(provider.envVar);
const factory = MODEL_FACTORIES[provider.id];
return factory(apiKey, model);
}
How Provider Selection Works
When you invoke a model through Dexter, the system follows a deterministic routing flow:
- Model name supplied – You pass a string like
"claude-3-5-sonnet-20240620","gemini-1.5-flash", or"gpt-4o". - Prefix matching –
resolveProvider()iterates through thePROVIDERSarray and selects the first entry wheremodel.startsWith(provider.modelPrefix)is true. - Fallback to OpenAI – If no prefix matches, Dexter uses the
defaultProvider(OpenAI) to maintain backward compatibility. - Factory instantiation –
getChatModel()retrieves the API key fromprocess.env[provider.envVar], selects the corresponding factory fromMODEL_FACTORIES, and instantiates the LangChain chat class. - Ready for inference – The returned
BaseChatModelis passed to the agent loop or direct invocation methods.
Provider-Specific Optimizations
Dexter handles unique features of each provider without exposing complexity to the caller.
Anthropic Prompt Caching
For Anthropic models, Dexter constructs explicit SystemMessage and HumanMessage objects with cache_control: {type: 'ephemeral'} to enable Anthropic’s prompt-caching optimization. This reduces token costs for repeated system prompts across multi-turn conversations.
// src/model/llm.ts
function buildAnthropicMessages(systemPrompt: string, userPrompt: string) {
return [
new SystemMessage({
content: systemPrompt,
additional_kwargs: { cache_control: { type: 'ephemeral' } }
}),
new HumanMessage({
content: userPrompt,
additional_kwargs: { cache_control: { type: 'ephemeral' } }
})
];
}
Fast-Model Shortcuts
Dexter can automatically downgrade to a cheaper, faster model for lightweight tasks by referencing the fastModel field in the provider definition. The getFastModel() utility returns the provider’s designated fast variant while keeping the same provider configuration.
// src/model/llm.ts
export function getFastModel(providerId: string, currentModel: string): string {
const provider = PROVIDERS.find(p => p.id === providerId);
return provider ? provider.fastModel : currentModel;
}
Code Examples
The following examples demonstrate how to invoke different providers using Dexter’s unified interface.
import { callLlm } from '@/model/llm';
// OpenAI (default behavior)
await callLlm('What is the current S&P 500 price?');
// Uses model "gpt-5.2" → OpenAI ChatOpenAI instance
// Anthropic – model name starts with "claude-"
await callLlm('Summarize Apple’s Q4 earnings.', {
model: 'claude-3-5-sonnet-20240620',
});
// resolveProvider() picks Anthropic, factory creates ChatAnthropic,
// buildAnthropicMessages adds cache_control for prompt caching
// Google Gemini
await callLlm('Explain the concept of discounted cash flow.', {
model: 'gemini-1.5-flash',
});
// resolveProvider() picks Google, factory creates ChatGoogleGenerativeAI
// Using a fast-model shortcut
import { getFastModel } from '@/model/llm';
const fast = getFastModel('anthropic', 'claude-3-5-sonnet-20240620');
// fast === 'claude-haiku-4-5'
await callLlm('Briefly list Apple’s top 5 products.', { model: fast });
Summary
- Provider Registry:
src/providers.tsdeclares all supported LLMs with routing prefixes, API key environment variables, and fast-model aliases. - Factory Pattern:
src/model/llm.tsmaps provider IDs to LangChain constructors (ChatOpenAI,ChatAnthropic,ChatGoogleGenerativeAI,ChatOllama). - Prefix Routing:
resolveProvider()selects the correct backend by matching model name prefixes (claude-,gemini-,gpt-), falling back to OpenAI by default. - Provider-Specific Logic: Dexter injects Anthropic-specific
cache_controlmetadata for prompt caching while using generic templates for OpenAI and Google. - Fast Model Switching:
getFastModel()allows runtime downgrades to cheaper provider-specific variants without changing invocation code.
Frequently Asked Questions
How does Dexter determine which LLM provider to use?
Dexter determines the provider by inspecting the model name string you provide. The resolveProvider() function in src/providers.ts iterates through the PROVIDERS array and matches the model name against each provider’s modelPrefix (e.g., claude- for Anthropic, gemini- for Google). The first match wins; if no prefix matches, Dexter defaults to OpenAI.
Can I use local models like Ollama with Dexter?
Yes. Dexter includes Ollama support through the same factory pattern used for cloud providers. The MODEL_FACTORIES record in src/model/llm.ts includes an ollama entry that instantiates ChatOllama from @langchain/community, reading the base URL from process.env.OLLAMA_BASE_URL. You invoke it by passing an Ollama model name, though the specific prefix logic depends on your local model naming convention.
What happens if I don’t specify a model name when calling Dexter?
If you omit the model parameter, Dexter uses its internal default model configuration, which typically points to an OpenAI model like gpt-5.2. Because OpenAI is designated as the defaultProvider in the provider registry, any unmatched or unspecified model falls back to the OpenAI factory, ensuring the system remains functional even without explicit configuration.
How does Dexter handle provider-specific features like Anthropic’s prompt caching?
Dexter handles provider-specific features through conditional logic in the callLlm function within src/model/llm.ts. When the resolved provider is Anthropic, Dexter constructs explicit SystemMessage and HumanMessage objects with cache_control: {type: 'ephemeral'} in the additional_kwargs. This enables Anthropic’s prompt-caching optimization. For other providers like OpenAI and Google, Dexter uses a generic ChatPromptTemplate that works with their standard APIs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →