# How to Configure Provider-Specific Executors and Custom Translators in OmniRoute

> Learn to configure OmniRoute provider-specific executors and custom translators for dynamic LLM request routing. Customize headers, base URLs, and transformation logic easily.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-07-05

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/executors/base.ts) providing core logic and `DefaultExecutor` in [`open-sse/executors/default.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`:

```typescript
// 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:

```json
{
  "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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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):

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/executors/default.ts)):

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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:

```typescript
// 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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:

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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 core `translateRequest` checks for a direct path first (lines 84-106 in [`open-sse/translator/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/translator/index.ts)).
- **Hub-and-spoke**: Falls back to converting `source → openai → target` when 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:

```typescript
// 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 `providerSpecificData`** to override URLs (`baseUrl`, `chatPath`) and inject custom headers without touching core code, as implemented in `BaseExecutor.buildUrl` and `DefaultExecutor.applyCustomHeaders`.
- **Configure auth headers** by setting the `authHeader` field in [`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts) to values like `"x-api-key"` or `"bearer"`.
- **Register custom translators** using the `register()` API from [`open-sse/translator/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/translator/registry.ts) to handle format conversions between OpenAI and your provider's native format.
- **Import translator modules** at application startup (e.g., in [`src/server/start.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/server/start.ts)) to ensure `bootstrapTranslatorRegistry` includes your custom logic.
- **Extend executors** by creating specialized classes for complex providers, or rely on `DefaultExecutor` for 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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.