How to Add a Custom Provider to the OmniRoute Registry with Executor and Translator
Register your provider in 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 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 and add your provider:
// 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 headeroauth: OAuth 2.0 flow with token refreshopenai-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/:
// 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 pathbuildHeaders(): 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:
// 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:
// 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:
// 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:
// 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:
// 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:
npm run test:all
Using Your Custom Provider
Once registered, your provider works through OmniRoute's standard HTTP API:
// 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:
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 |
Provider metadata and configuration |
open-sse/executors/myProvider.ts |
Custom executor implementation |
open-sse/executors/index.ts |
Executor factory for provider ID lookup |
open-sse/translator/myProviderTranslator.ts |
Request/response format translation |
open-sse/translator/index.ts |
Translator registry |
open-sse/config/providerRegistry.ts |
Runtime binding of executors and translators |
Summary
- Provider constants (
src/shared/constants/providers.ts) define the provider identity, auth type, and base URL - Executors (
open-sse/executors/) handle HTTP construction—overridebuildUrl,buildHeaders, andtransformRequestas needed - Translators (
open-sse/translator/) bridge between OmniRoute's OpenAI-compatible schema and provider-native formats - Provider registry (
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 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 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.
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 →