How OmniRoute Converts Between OpenAI, Anthropic, and Gemini API Formats
OmniRoute’s translator layer normalizes payloads through a hub-and-spoke engine that uses OpenAI’s JSON schema as the intermediate lingua-franca, executing direct source-to-target translations when available and falling back to a two-hop conversion otherwise.
OmniRoute is an open-source routing layer built on Next.js that lets users convert between OpenAI, Anthropic, and Gemini API formats without rewriting client code. Its translator layer sits between the Next.js API routes and the provider executors, reshaping request and response payloads so that a client can speak any supported LLM schema while the upstream provider receives its native format. This article breaks down exactly how the open-sse/translator module orchestrates these conversions, as implemented in diegosouzapw/OmniRoute.
Translator Registry and Bootstrapping
The translation pipeline starts with a global Translator Registry defined in open-sse/translator/registry.ts. This registry stores request and response translator functions for every supported source-to-target pair via three core functions: register(), getRequestTranslator(), and getResponseTranslator().
When the translator module loads, bootstrapTranslatorRegistry() runs immediately at the top of open-sse/translator/index.ts. It imports every per-format translator file, and each file self-registers by calling register(source, target, requestFn, responseFn). The registry therefore contains an entry for every conversion pair before any traffic hits the API.
Individual translator modules live in open-sse/translator/request/ and open-sse/translator/response/. Notable examples include openai-to-claude.ts, openai-to-gemini.ts, claude-to-openai.ts, and gemini-to-openai.ts.
Hub-and-Spoke Request and Response Translation
The central orchestration lives in open-sse/translator/index.ts. The engine implements translateRequest() and translateResponse(), which decide whether to use a direct translator or a two-step hub-and-spoke path.
The translateRequest Flow
The translateRequest() function (defined at lines L49-L73) accepts parameters for sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, and optional options. The execution follows a strict three-step sequence:
-
Normalization. The function applies thinking-budget adjustments, role normalization, tool-call ID handling, and other pre-processing (lines L70-L91).
-
Direct or Hub-and-Spoke Routing. If a direct translator exists and neither format is OpenAI, the engine invokes it via
getRequestTranslator(source, target). For all other cases, it performs a two-hop conversion: first from source → OpenAI (getRequestTranslator(source, FORMATS.OPENAI)), then from OpenAI → target (getRequestTranslator(FORMATS.OPENAI, target)). The intermediate OpenAI payload is logged viareqLogger?.logOpenAIRequestfor observability. -
Post-Processing. The pipeline strips OpenAI-only echo fields, injects reasoning cache placeholders, sanitizes tool schemas, and ensures final IDs are unique.
This design means a client can submit a Gemini-shaped payload and have it delivered to Anthropic’s Claude endpoint without either side knowing the difference.
The translateResponse Flow
The reverse path is implemented symmetrically in translateResponse() (see lines L87-L112). The function evaluates three conditions in order:
- If source equals target, the chunk returns unchanged.
- If a direct response translator exists for target → source, it is invoked via
getResponseTranslator(). - Otherwise, the engine routes the response through target → OpenAI → source using the same hub-and-spoke pattern (referenced at lines L11-L25).
The function also propagates an _openaiIntermediate field so that logging layers can capture the normalized representation.
Why OpenAI Is the Lingua-Franca
OpenAI’s JSON schema is the most widely supported interchange format across the LLM ecosystem, with many vendors exposing “OpenAI-compatible” endpoints. By normalizing everything through this pivot, OmniRoute only needs one translator per non-OpenAI format, which dramatically reduces code duplication. Direct converters—such as Claude → Gemini in open-sse/translator/request/claude-to-gemini.ts—are still registered and preferred for performance-critical pairs when both endpoints are non-OpenAI.
Normalizing Edge Cases Across Providers
Beyond simple field mapping, the translator layer accounts for provider quirks through helpers in open-sse/translator/helpers/.
Tool Calls and Schema Coercion
Tool-related payloads are normalized by ensureToolCallIds() and fixMissingToolResponses() inside modules such as toolCallHelper.ts. Placeholder tool results are injected when a provider expects them but the source format omits them, while schemas are coerced en route to match the target’s expected structure.
Role Mapping and Reasoning Cache
The engine converts developer-role messages to system-role unless preserveDeveloperRole is set. For providers like DeepSeek and Kimi that require reasoning payloads, the translator injects placeholders via reasoning helpers and can replay cached reasoning using lookupReasoning() and recordReplay(). Cache-control flags are propagated only when providerHonorsOpenAIFormatCacheControl() returns true (lines L122-L130).
Converting Requests and Responses: Code Examples
Claude to Gemini Direct Translation
When a client sends a Claude-compatible payload that must reach a Gemini model, OmniRoute can use a direct translator if both formats are non-OpenAI:
import { translateRequest, FORMATS } from '@/open-sse/translator';
const claudePayload = {
messages: [{ role: 'user', content: 'Hello' }],
// ...Claude-specific fields...
};
const geminiPayload = translateRequest(
FORMATS.CLAUDE,
FORMATS.GEMINI,
'gemini-1.5-flash',
claudePayload,
true,
null,
'google'
);
In this case, the registry locates the direct translator registered in open-sse/translator/request/claude-to-gemini.ts and executes it without an intermediate OpenAI hop.
OpenAI to Anthropic Request Conversion
Because OpenAI is the hub, translating an OpenAI-compatible request to Claude follows a truncated path:
import { translateRequest, FORMATS } from '@/open-sse/translator';
const openaiPayload = {
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Summarize this.' }],
};
const claudePayload = translateRequest(
FORMATS.OPENAI,
FORMATS.CLAUDE,
'claude-3-5-sonnet-20240620',
openaiPayload,
true,
null,
'anthropic'
);
The hub-and-spoke path effectively runs OpenAI → OpenAI (no change), then OpenAI → Claude via open-sse/translator/request/openai-to-claude.ts.
Gemini Response to OpenAI Format
import { translateResponse, FORMATS } from '@/open-sse/translator';
const geminiChunk = {
candidates: [{ content: { parts: [{ text: 'Answer' }] } }],
// ...Gemini-specific fields...
};
const openaiChunk = translateResponse(
FORMATS.GEMINI,
FORMATS.OPENAI,
geminiChunk,
{}
);
The engine calls gemini-to-openai.ts to extract Gemini candidates and reshape them into OpenAI choices. If a direct Gemini → OpenAI response translator is registered, it is used immediately; otherwise the hub-and-spoke routing applies.
Summary
- OmniRoute’s translator layer lives under
open-sse/translator/and bridges Next.js API routes with provider executors. registry.tsmaintains a global map of request and response translators bootstrapped at startup viabootstrapTranslatorRegistry().translateRequest()andtranslateResponse()inindex.tsprefer direct source-to-target translators, then fall back to a source → OpenAI → target hub-and-spoke pattern.- OpenAI’s JSON schema acts as the lingua-franca because it is the de-facto standard across the LLM industry.
- Edge cases—including tool-call IDs, role normalization, reasoning cache, and cache-control flags—are handled by helpers in
open-sse/translator/helpers/.
Frequently Asked Questions
What makes OpenAI the default hub format in OmniRoute?
OpenAI’s JSON schema is the most widely adopted request and response format among LLM providers, many of whom expose OpenAI-compatible endpoints. Using it as the pivot means OmniRoute only needs one translator per non-OpenAI format instead of an n² matrix of converters.
Does OmniRoute support direct translation between Claude and Gemini?
Yes. When a direct translator is registered for a source-target pair and neither side is OpenAI, OmniRoute invokes it immediately. For example, open-sse/translator/request/claude-to-gemini.ts handles Claude → Gemini directly without converting through OpenAI.
How does OmniRoute handle tool calls across different API formats?
The translator normalizes tool-call IDs via ensureToolCallIds(), injects missing tool responses through fixMissingToolResponses(), and coerces schemas before sending the payload to the target provider. These helpers reside in open-sse/translator/helpers/toolCallHelper.ts and related files.
Where does the translator registry get initialized?
The registry is initialized at module load time by bootstrapTranslatorRegistry(), which is called at lines L30-L31 in open-sse/translator/index.ts. Each translator file self-registers by importing the registry and calling register() with its source format, target format, and translation functions.
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 →