How to Configure Provider-Specific Executors and Custom Translators in OmniRoute
OmniRoute routes every LLM request through a provider-specific executor that handles URL construction and authentication, while a translator layer normalizes payloads to and from the OpenAI format, allowing you to configure custom headers, base URLs, and transformation logic via providerSpecificData and the translator registry.
OmniRoute is an open-source LLM routing layer that abstracts provider differences through a dual-architecture design. To configure provider-specific executors and custom translators in OmniRoute, you modify the executor chain for network-level customization and register translator functions for payload transformation. This guide examines the source code in diegosouzapw/OmniRoute to show you exactly how to override URLs, inject headers, and implement custom request/response mappings.
Understanding the Executor Architecture
The executor layer manages network-level concerns: building URLs, setting headers, handling retries, and managing authentication tokens. OmniRoute uses a hierarchy of executor classes, with the BaseExecutor in open-sse/executors/base.ts providing core logic and DefaultExecutor in open-sse/executors/default.ts handling OpenAI-compatible providers.
BaseExecutor and DefaultExecutor
The BaseExecutor class implements the generic execute() flow and provides helper methods like buildUrl() (lines 86-97) and buildHeaders(). The DefaultExecutor extends this for OpenAI-compatible providers, adding provider-specific URL normalization and custom header merging (lines 64-94 in open-sse/executors/default.ts).
Specialized executors exist for non-OpenAI providers—such as ClaudeWebExecutor, GeminiWebExecutor, and DeepSeekWebWithAutoRefreshExecutor—which override buildUrl() and buildHeaders() to inject provider-specific requirements like the Anthropic β-header or token refresh logic.
The executor registry in open-sse/executors/index.ts (lines 60-159) maintains a map of provider IDs to executor instances. The getExecutor() function (lines 65-69) returns a cached specialized executor or lazily creates a DefaultExecutor for unknown provider IDs.
URL Overrides via providerSpecificData
You can override URLs without modifying code by using the providerSpecificData field in your provider credentials. In open-sse/executors/base.ts (lines 98-101), the buildUrl method checks for openai-compatible- or anthropic-compatible- prefixes and extracts baseUrl and chatPath from credentials.providerSpecificData:
// open-sse/executors/base.ts – L98-L101
if (this.provider?.startsWith?.("openai-compatible-")) {
const psd = credentials?.providerSpecificData;
const baseUrl = typeof psd?.baseUrl === "string" ? psd.baseUrl : "https://api.openai.com/v1";
const normalized = baseUrl.replace(/\/$/, "");
const rawPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
const customPath = rawPath && sanitizePath(rawPath) ? rawPath : null;
if (customPath) return `${normalized}${customPath}`;
// ...
}
To configure this, store a JSON object in your connection's database row:
{
"baseUrl": "https://my-gateway.example.com/v1",
"chatPath": "/my-chat"
}
Custom Header Injection
The DefaultExecutor merges custom headers via the applyCustomHeaders function (lines 64-94 in open-sse/executors/default.ts). This filters out hop-by-hop headers and control characters, replacing any existing header with the same name (case-insensitive):
// open-sse/executors/default.ts – L64-L94 (applyCustomHeaders)
function applyCustomHeaders(headers: Record<string, string>, rawCustomHeaders: unknown): void {
// ...
for (const [k, v] of Object.entries(customHeaders)) {
if (typeof k !== "string" || typeof v !== "string") continue;
if (isForbiddenCustomHeaderName(k)) continue;
if (/[\r\n\0]/.test(k) || /[\r\n]/.test(v)) continue;
const lower = k.toLowerCase();
for (const existing of Object.keys(headers)) {
if (existing.toLowerCase() === lower) delete headers[existing];
}
headers[k] = v;
}
}
Add a customHeaders field to providerSpecificData or edit src/shared/constants/providers.ts to include defaults for a new provider.
Auth Header Configuration
For providers using non-standard authentication headers (e.g., x-api-key for Gemini), DefaultExecutor.buildHeaders consults the registry entry (lines 21-26 in open-sse/executors/default.ts):
const entry = getRegistryEntry(this.provider);
const authHeader = entry?.authHeader || "bearer";
// ...
if (authHeader === "x-api-key") {
headers["x-api-key"] = token;
} else if (authHeader === "x-goog-api-key") {
headers["x-goog-api-key"] = token;
} else {
headers["Authorization"] = `Bearer ${token}`;
}
Add "authHeader": "x-api-key" to the provider's entry in src/shared/constants/providers.ts to configure this behavior.
Working with the Translator Layer
While executors handle network transport, translators handle payload semantics. The translator layer converts between source formats (client API) and target formats (upstream provider), keeping the routing pipeline format-agnostic.
The Translator Registry
The registry in open-sse/translator/registry.ts holds request and response translator functions keyed by from:to format pairs. The register() function (lines 20-26) adds translator pairs, while getRequestTranslator and getResponseTranslator retrieve them.
The bootstrapTranslatorRegistry() function in open-sse/translator/bootstrap.ts installs built-in translators (OpenAI ↔ Claude, OpenAI ↔ Gemini, etc.) at startup. The core translation entry points are translateRequest and translateResponse in open-sse/translator/index.ts (request flow at lines 34-71 and 84-122), which handle thinking budgets, tool-call normalization, and cache control before dispatching through the registry.
Creating Custom Request Translators
To support a custom provider like my-foo that requires model name prefixes and field renaming, implement a request translator:
// src/lib/custom-translators.ts
import { register } from "./open-sse/translator/registry.ts";
function myFooRequestTranslator(
model: string,
body: Record<string, unknown>,
_stream?: boolean,
_cred?: Record<string, unknown> | null,
) {
// Prefix the model name
const prefixed = `foo-${model}`;
// Rename `messages` → `inputs`
const inputs = (body as any).messages ?? [];
return {
model: prefixed,
inputs,
// Preserve other top-level fields
...(body as Record<string, unknown>),
};
}
// Register the translator (source = "openai", target = "my-foo")
register("openai", "my-foo", myFooRequestTranslator);
Import this file once at application startup (e.g., from src/server/start.ts) to make the translator available to the system.
Creating Custom Response Translators
If your provider returns a non-standard format like { result: "...", usage: {...} }, add a response translator to convert it to OpenAI format:
import { register } from "./open-sse/translator/registry.ts";
function myFooResponseTranslator(
chunk: Record<string, unknown>,
_state: Record<string, unknown>,
) {
const result = chunk.result as string | undefined;
if (!result) return null;
return {
choices: [{ message: { content: result } }],
usage: chunk.usage,
};
}
// Register (target = "my-foo", source = "openai")
register("my-foo", "openai", undefined, myFooResponseTranslator);
The translateResponse routine in open-sse/translator/index.ts (lines 88-102) automatically routes through this function when targetFormat === "my-foo" and sourceFormat === "openai".
Direct vs Hub-and-Spoke Translation
OmniRoute supports two translation modes:
- Direct translation (
source → target): Used when the provider has a non-trivial mapping that would be lost if normalized to OpenAI first (e.g., Claude ↔ Gemini). The coretranslateRequestchecks for a direct path first (lines 84-106 inopen-sse/translator/index.ts). - Hub-and-spoke: Falls back to converting
source → openai → targetwhen no direct translator exists.
Direct translators are preferred for complex mappings to preserve provider-specific features.
Complete Implementation Example
This example adds a new provider my-foo with custom URL, headers, and bidirectional translation:
// 1️⃣ src/shared/constants/providers.ts – Add provider entry
export const PROVIDERS = {
// ... existing providers ...
"my-foo": {
id: "my-foo",
baseUrl: "https://api.my-foo.com/v1",
authHeader: "x-api-key",
customHeaders: JSON.stringify({ "X-Client-Version": "1.2.3" }),
},
};
// 2️⃣ src/lib/custom-translators.ts – Register translators
import { register } from "../open-sse/translator/registry.ts";
function myFooRequestTranslator(
model: string,
body: Record<string, unknown>,
) {
return {
model: `foo-${model}`,
inputs: (body as any).messages ?? [],
...body,
};
}
function myFooResponseTranslator(
chunk: Record<string, unknown>,
) {
if (!chunk.result) return null;
return {
choices: [{ message: { content: chunk.result } }],
usage: chunk.usage,
};
}
register("openai", "my-foo", myFooRequestTranslator);
register("my-foo", "openai", undefined, myFooResponseTranslator);
// 3️⃣ src/server/start.ts – Import at startup
import "./lib/custom-translators.ts";
With this configuration, clients can call the standard /api/v1/chat/completions endpoint with provider: "my-foo". The executor will use the overridden baseUrl and custom headers, while the translators handle the payload transformation automatically.
Summary
- Use
providerSpecificDatato override URLs (baseUrl,chatPath) and inject custom headers without touching core code, as implemented inBaseExecutor.buildUrlandDefaultExecutor.applyCustomHeaders. - Configure auth headers by setting the
authHeaderfield insrc/shared/constants/providers.tsto values like"x-api-key"or"bearer". - Register custom translators using the
register()API fromopen-sse/translator/registry.tsto handle format conversions between OpenAI and your provider's native format. - Import translator modules at application startup (e.g., in
src/server/start.ts) to ensurebootstrapTranslatorRegistryincludes your custom logic. - Extend executors by creating specialized classes for complex providers, or rely on
DefaultExecutorfor standard OpenAI-compatible services.
Frequently Asked Questions
How do I override the base URL for a specific provider connection?
Store a JSON object in the providerSpecificData column of your connections database table containing {"baseUrl": "https://custom.example.com/v1", "chatPath": "/chat"}. The BaseExecutor.buildUrl method in open-sse/executors/base.ts (lines 98-101) automatically detects these overrides when the provider ID starts with openai-compatible- or anthropic-compatible-, allowing per-connection URL customization without code changes.
What is the difference between an executor and a translator in OmniRoute?
Executors handle network transport concerns—building URLs, setting authentication headers, and managing retries—while translators handle payload semantics by converting between API formats. Executors live in open-sse/executors/ and manage the "how" of the request (HTTP), whereas translators live in open-sse/translator/ and manage the "what" of the request (JSON body structure). This separation allows you to change endpoint URLs without modifying payload logic, or vice versa.
How do I add support for a completely new LLM provider?
First, add the provider definition to src/shared/constants/providers.ts with baseUrl and authHeader settings. If the provider requires payload transformation, create a file in src/lib/ that imports register from open-sse/translator/registry.ts and implements request/response translators. Import this file in your application startup script. Finally, store connection credentials in the database with providerSpecificData for any per-instance overrides. The system will automatically use DefaultExecutor for unknown providers unless you create a specialized executor class.
Why does my custom translator need to handle both request and response directions?
OmniRoute normalizes all internal communication to an OpenAI-like format to keep the routing layer provider-agnostic. When a client sends a request, your request translator converts from OpenAI format to the provider's native format. When the provider responds, your response translator converts back from the provider's format to OpenAI format so the client receives a consistent response shape. Registering both directions ensures bidirectional compatibility and allows the hub-and-spoke fallback system to compose your translator with others if needed.
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 →