# How to Add a Custom Provider to the OmniRoute Registry with Executor and Translator

> Learn to add a custom provider to OmniRoute registry. Implement executors and translators to extend OmniRoute's unified API with your custom logic.

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

---

**Register your provider in [`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts), implement a custom executor in `open-sse/executors/`, optionally add a translator in `open-sse/translator/`, then wire both into [`open-sse/config/providerRegistry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/config/providerRegistry.ts) to make your provider available through OmniRoute's unified API.**

OmniRoute's provider architecture is built around three core components: **provider registration**, **executors**, and **translators**. Adding a custom provider means extending this system so your new LLM or API service becomes reachable through the same endpoint used by OpenAI, Anthropic, and other built-in providers. This guide walks through the complete implementation based on the OmniRoute source code.

## Register the Provider in Constants

Every provider starts with a definition in the central constants file. This entry defines the provider ID, authentication type, base URL, and model catalog.

Open [`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts) and add your provider:

```typescript
// src/shared/constants/providers.ts
export const PROVIDERS = {
  // Existing providers …
  MY_CUSTOM: {
    id: "my-custom",
    type: "apiKey",               // or "oauth" / "openai-compatible"
    baseUrl: "https://api.myprovider.com/v1",
    authHeader: "Authorization",  // header name used for the API key
    // Optional: default model list, rate‑limit config, etc.
  },
};

```

The **provider type** determines how OmniRoute handles authentication:

- **`apiKey`**: Standard API key passed in a header
- **`oauth`**: OAuth 2.0 flow with token refresh
- **`openai-compatible`**: Provider follows OpenAI's request/response format exactly

This constant is exported and consumed by the runtime's provider lookup logic throughout the codebase.

## Implement a Custom Executor

The **executor** handles HTTP request construction—building URLs, headers, and transforming request bodies for your provider's specific requirements.

Create a new executor in `open-sse/executors/`:

```typescript
// open-sse/executors/myProvider.ts
import { BaseExecutor } from "./base";

export class MyProviderExecutor extends BaseExecutor {
  // Override only what differs from the default OpenAI‑compatible flow.
  protected buildUrl(path: string): string {
    return `${this.provider.baseUrl}/${path}`;
  }

  protected buildHeaders(): Record<string, string> {
    const headers = super.buildHeaders();
    // My provider expects the API key in a custom header.
    headers["X‑My‑API‑Key"] = this.provider.apiKey;
    return headers;
  }

  // If the request body needs tweaking (e.g., nested `messages` field), do it here.
  protected transformRequest(body: any): any {
    // Example: rename `messages` → `chat` for this API.
    return { chat: body.messages, ...body };
  }
}

```

Key methods to override:

- **`buildUrl(path)`**: Constructs the full request URL from the base URL and API path
- **`buildHeaders()`**: Adds provider-specific headers (custom auth schemes, content types, etc.)
- **`transformRequest(body)`**: Modifies the request payload structure before sending

Register your executor in the factory so OmniRoute can instantiate it by provider ID:

```typescript
// open-sse/executors/index.ts
import { MyProviderExecutor } from "./myProvider";

export function getExecutor(providerId: string) {
  switch (providerId) {
    // … existing cases …
    case "my-custom":
      return new MyProviderExecutor(providerId);
    default:
      return new DefaultExecutor(providerId);
  }
}

```

If your provider is **OpenAI-compatible**, you can extend `DefaultExecutor` instead of `BaseExecutor` and override only the methods that differ.

## Add a Translator for Non-OpenAI Providers

When your provider uses a completely different request/response schema, implement a **translator** to convert between OmniRoute's internal OpenAI-compatible format and your provider's native format.

Create the translator module:

```typescript
// open-sse/translator/myProviderTranslator.ts
import { translateRequest, translateResponse } from "./generic";

export const myProviderTranslator = {
  // Convert OmniRoute's OpenAI‑style payload into the provider's schema.
  request: (input) => ({
    prompt: input.messages.map((m) => m.content).join("\n"),
    // …other fields…
  }),

  // Convert the provider's JSON reply back to OpenAI‑compatible shape.
  response: (output) => ({
    id: output.id,
    object: "chat.completion",
    choices: [{ message: { role: "assistant", content: output.reply } }],
    // …other fields…
  }),
};

```

Register it in the translator index:

```typescript
// open-sse/translator/index.ts
import { myProviderTranslator } from "./myProviderTranslator";

export const TRANSLATORS = {
  // …existing translators…
  "my-custom": myProviderTranslator,
};

```

Translators are optional—if your provider is OpenAI-compatible, the default pass-through translation suffices.

## Wire Everything into the Provider Registry

The **provider registry** ties your executor and translator together so the runtime can resolve provider IDs to the correct implementations.

Update [`open-sse/config/providerRegistry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/config/providerRegistry.ts):

```typescript
// open-sse/config/providerRegistry.ts
import { MyProviderExecutor } from "../executors/myProvider";
import { myProviderTranslator } from "../translator/myProviderTranslator";

export const providerRegistry = {
  // …other entries…
  "my-custom": {
    executor: MyProviderExecutor,
    translator: myProviderTranslator,
    // Optional: default model list, auth scheme, etc.
  },
};

```

This registry is the single source of truth that OmniRoute consults when routing requests to your custom provider.

## Test Your Implementation

Verify your provider integration with unit tests covering the executor and translator contracts:

```typescript
// tests/unit/customProvider.test.ts
import { getExecutor } from "../../open-sse/executors";
import { providerRegistry } from "../../open-sse/config/providerRegistry";

test("MyCustom executor builds correct request", async () => {
  const exec = getExecutor("my-custom");
  const mockBody = { messages: [{ role: "user", content: "Hi" }] };
  const url = exec.buildUrl("chat/completions");
  const headers = exec.buildHeaders();
  const transformed = exec.transformRequest(mockBody);
  expect(url).toBe("https://api.myprovider.com/v1/chat/completions");
  expect(headers["X-My-API-Key"]).toBeDefined();
  expect(transformed.chat).toBeDefined();
});

```

Run the full test suite to ensure no regressions:

```bash
npm run test:all

```

## Using Your Custom Provider

Once registered, your provider works through OmniRoute's standard HTTP API:

```typescript
// Example: issuing a request to the newly added provider
import fetch from "node-fetch";

const response = await fetch(
  "http://localhost:3000/api/v1/chat/completions",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-omniroute-provider": "my-custom",
      "Authorization": "Bearer sk‑my-secret-key",
    },
    body: JSON.stringify({
      model: "my-model",
      messages: [{ role: "user", content: "Explain the theory of relativity." }],
    }),
  }
);
const data = await response.json();
console.log(data);

```

For internal tooling, use the executor directly:

```typescript
import { getExecutor } from "./open-sse/executors";

const exec = getExecutor("my-custom");
const result = await exec.execute({
  path: "chat/completions",
  body: { messages: [{ role: "user", content: "Hello!" }] },
});

```

## Key Files Reference

| File | Purpose |
|------|---------|
| [`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts) | Provider metadata and configuration |
| [`open-sse/executors/myProvider.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/executors/myProvider.ts) | Custom executor implementation |
| [`open-sse/executors/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/executors/index.ts) | Executor factory for provider ID lookup |
| [`open-sse/translator/myProviderTranslator.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/translator/myProviderTranslator.ts) | Request/response format translation |
| [`open-sse/translator/index.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/translator/index.ts) | Translator registry |
| [`open-sse/config/providerRegistry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/config/providerRegistry.ts) | Runtime binding of executors and translators |

## Summary

- **Provider constants** ([`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts)) define the provider identity, auth type, and base URL
- **Executors** (`open-sse/executors/`) handle HTTP construction—override `buildUrl`, `buildHeaders`, and `transformRequest` as needed
- **Translators** (`open-sse/translator/`) bridge between OmniRoute's OpenAI-compatible schema and provider-native formats
- **Provider registry** ([`open-sse/config/providerRegistry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/config/providerRegistry.ts)) wires executor and translator together for runtime resolution
- Test coverage in `tests/unit/` validates request building, response handling, and error mapping before deployment

## Frequently Asked Questions

### Do I need a translator if my provider is OpenAI-compatible?

No. If your provider follows the OpenAI request/response format exactly, omit the translator and use `DefaultExecutor` or extend it with minimal overrides. The translator is only required when the provider uses a different schema—such as Anthropic's `prompt` field instead of `messages`, or Google's `contents` array structure.

### Can I support multiple authentication methods for the same provider?

Yes. Define separate provider entries in [`src/shared/constants/providers.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers.ts) for each auth variant, each with a unique ID like `"myprovider-apikey"` and `"myprovider-oauth"`. Implement corresponding executors that handle each auth flow, or use conditional logic within a single executor based on configuration passed at initialization.

### How do I handle streaming responses from my custom provider?

Override the `executeStream` method in your executor class. The base executor in [`open-sse/executors/base.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/open-sse/executors/base.ts) provides a default implementation for Server-Sent Events (SSE). If your provider uses a different streaming format—such as JSON Lines or WebSockets—implement the transform logic in `executeStream` to convert native chunks into OmniRoute's standard SSE format before yielding to the client.