How OmniRoute Handles Format Translation Between Different LLM Providers: A Complete Technical Guide
OmniRoute uses a registry-based plugin system in open-sse/translator/ to convert requests and responses between incompatible LLM provider formats, mapping fields bidirectionally while handling tool calls, streaming, and provider-specific quirks.
The diegosouzapw/OmniRoute project solves a fundamental problem in modern AI infrastructure: every major LLM provider uses a different API schema. OpenAI, Google Gemini, and Anthropic Claude each have distinct request shapes, parameter names, authentication patterns, and response formats. OmniRoute's translation layer abstracts these differences away through a centralized, extensible architecture that lets developers route requests to any supported provider without changing client code.
How the Translation Registry Works
At the heart of OmniRoute's format translation is a central registry that maps source-to-target format pairs to conversion functions. This design enables dynamic translator lookup at runtime.
The Registry Data Structure
In open-sse/translator/registry.ts, two Map objects store the available translators:
const requestRegistry = new Map<string, RequestTranslator>();
const responseRegistry = new Map<string, ResponseTranslator>();
export function register(
from: string,
to: string,
requestFn?: RequestTranslator,
responseFn?: ResponseTranslator,
) {
const key = `${from}:${to}`;
if (requestFn) requestRegistry.set(key, requestFn);
if (responseFn) responseRegistry.set(key, responseFn);
}
Registry keys follow the pattern "openai:gemini" or "openai:claude", constructed via a makeKey helper. Translators self-register at module load time, making the system automatically discoverable.
Format Constants
The FORMATS enum in open-sse/translator/formats.ts defines canonical names for each provider:
OPENAIGEMINICLAUDEANTIGRAVITY
These constants ensure type-safe references throughout the translation pipeline.
Request Translation: Converting Client Requests to Provider Format
When a client sends a request, OmniRoute performs three steps: lookup the appropriate translator, transform the payload, and dispatch to the executor.
Step 1: Translator Lookup
The request handler fetches translators using the registry:
import { getRequestTranslator, getResponseTranslator } from "./registry.ts";
const reqTranslator = getRequestTranslator(FORMATS.OPENAI, targetFormat);
const respTranslator = getResponseTranslator(targetFormat, FORMATS.OPENAI);
Step 2: OpenAI to Gemini Translation
The openai-to-gemini.ts module implements detailed field mapping from OpenAI's chat completion format to Gemini's generateContent API:
function openaiToGeminiBase(
model: string,
body: Record<string, unknown>,
stream: boolean,
toolNameOptions: GeminiToolNameOptions = {}
) {
const result: GeminiRequest = {
model,
contents: [],
generationConfig: {},
safetySettings: body.safetySettings || DEFAULT_SAFETY_SETTINGS,
};
// temperature → generationConfig.temperature
if (body.temperature !== undefined) {
result.generationConfig.temperature = body.temperature;
}
// max_tokens → generationConfig.maxOutputTokens (capped to model limits)
const maxOutputTokens = capMaxOutputTokens(
model,
(body.max_tokens ?? body.max_completion_tokens) as number | undefined
);
if (maxOutputTokens !== null) {
result.generationConfig.maxOutputTokens = maxOutputTokens;
}
// messages array transformed to Gemini contents with role mapping
// tool calls converted to function declarations with signature handling
// consecutive same-role messages merged (Gemini requirement)
return result;
}
This translator handles thinking budgets, tool call signatures, and safety settings that have no direct OpenAI equivalent.
Step 3: OpenAI to Claude Translation
The Claude translator in openai-to-claude.ts applies Anthropic-specific rules:
export function openaiToClaudeRequest(model, body, stream, credentials = null) {
const result: any = {
model,
max_tokens: adjustMaxTokens(body),
stream,
messages: []
};
// Temperature dropped when thinking is enabled (Claude constraint)
if (body.temperature !== undefined && !modelForcesThinking) {
result.temperature = body.temperature;
}
// System/developer messages extracted to Claude's top-level system array
// Tool names prefixed for OAuth-scoped calls
// Empty text blocks stripped (Claude rejects these)
// Minimal user turn injected when only system messages exist
return result;
}
Key differences from Gemini translation include system message handling (Claude uses a separate system parameter), tool name prefixing, and reasoning budget configuration.
Response Translation: Normalizing Provider Responses
After the upstream LLM returns a response, OmniRoute translates it back to the client's expected format.
Gemini to OpenAI Response Conversion
The response translator in open-sse/translator/response/openai-to-gemini.ts reconstructs OpenAI-compatible choices from Gemini's contents:
// Converts Gemini contents → OpenAI choices
// Handles streaming deltas for SSE responses
// Maps tool_responses back to function call results
// Preserves thought signatures in assistant messages
This ensures clients receive consistent response shapes regardless of which provider actually served the request.
Provider-Specific Edge Case Handling
OmniRoute embeds defensive logic for each provider's constraints:
- Tool name sanitization (
sanitizeGeminiToolName,sanitizeToolId) prevents 400 errors from invalid identifiers - Token budget capping (
capThinkingBudget,capMaxOutputTokens) enforces model-specific limits - Signature handling manages Gemini's
thoughtSignatureand Claude'sthinkingblocks without breaking requests - Consecutive role merging satisfies Gemini's requirement that adjacent messages have different roles
Adding New Providers to the Translation System
The pluggable architecture minimizes effort for new provider support. Implementers need only:
- Create request and response translator functions
- Register them with
register(from, to, requestFn, responseFn) - Add the new format to
FORMATSenum
Example registration from openai-to-gemini.ts:
register(
FORMATS.OPENAI,
FORMATS.GEMINI,
(model, body, stream = false, credentials = null) =>
openaiToGeminiRequest(model, body, stream, credentials, {
signaturelessToolCallMode: "context",
}),
null, // response translator registered separately
);
The rest of the pipeline automatically discovers and uses the new translator.
Core Translation Files Reference
| File | Purpose |
|---|---|
open-sse/translator/registry.ts |
Translator registration and lookup |
open-sse/translator/formats.ts |
Provider format constants |
open-sse/translator/request/openai-to-gemini.ts |
OpenAI → Gemini request conversion |
open-sse/translator/request/openai-to-claude.ts |
OpenAI → Claude request conversion |
open-sse/translator/response/openai-to-gemini.ts |
Gemini → OpenAI response conversion |
open-sse/translator/helpers/* |
Sanitization, capping, and utility functions |
Summary
- Registry-based architecture: OmniRoute uses
Mapstructures inregistry.tsto dynamically pair source and target format translators - Bidirectional conversion: Separate request and response translators handle each direction of format transformation
- Field-level mapping: Translators rewrite parameters like
temperature,max_tokens, andmessagesto provider-specific equivalents - Provider quirks handled: Tool names, thinking budgets, safety settings, and role constraints are normalized automatically
- Extensible design: New providers require only translator function implementation and
register()invocation
Frequently Asked Questions
How does OmniRoute know which translator to use for a given request?
The request handler inspects the target provider from the request metadata, then calls getRequestTranslator(FORMATS.OPENAI, targetFormat) to retrieve the appropriate conversion function. The registry key pattern "openai:gemini" enables O(1) lookup of registered translators.
Can OmniRoute translate between non-OpenAI formats directly?
The registry supports any source-to-target pair. While the current implementation primarily registers OpenAI-to-provider translators, the architecture supports arbitrary mappings. Direct Gemini-to-Claude translation would require registering a translator with register(FORMATS.GEMINI, FORMATS.CLAUDE, ...).
What happens if a provider adds new API parameters?
Translators are versioned with the codebase. New parameters require updates to the relevant translator file (e.g., openai-to-gemini.ts) to map or validate the field. The modular design isolates changes to the specific provider pair affected.
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 →