How to Add a New AI Provider with a Custom Executor in OmniRoute
Adding a new AI provider to OmniRoute requires registering the provider in the constants registry, implementing a custom executor class that extends the base abstraction, and exporting it through the executor factory—optionally adding a request translator if the provider deviates from the OpenAI request schema.
OmniRoute is an open-source AI gateway that normalizes requests across multiple LLM providers through a modular executor architecture. To add a new AI provider with a custom executor in OmniRoute, you will wire the provider into four specific layers: the provider registry (src/shared/constants/providers.ts), the execution engine (open-sse/executors/), the translation layer (open-sse/translator/), and optionally the OAuth configuration (src/lib/oauth/).
Step 1 – Register the Provider in the Constants File
Every provider must be declared in src/shared/constants/providers.ts using Zod validation. This central registry makes the provider discoverable by the combo routing engine, the provider catalog UI, and the type-safe executor factory.
Add your provider ID to the Zod enum and define its metadata in the providerMeta object:
export const PROVIDERS = z.enum([
// …existing providers…
"myprovider", // <-- new entry
]);
export type ProviderId = z.infer<typeof PROVIDERS>;
export const providerMeta = {
myprovider: {
displayName: "MyProvider",
authType: "apiKey", // or "oauth"
baseUrl: "https://api.myprovider.com",
// optional: default model list, capabilities, etc.
},
};
This registration step is mandatory—without it, the factory cannot resolve the executor and the UI will not display the provider.
Step 2 – Implement the Custom Executor
If your provider follows the standard OpenAI-compatible request format, you can reuse the default executor. For providers requiring non-standard headers, URL structures, or payloads, create a new executor class in open-sse/executors/ that extends BaseExecutor from open-sse/executors/base.ts.
Override these three abstract methods to customize the HTTP lifecycle:
buildUrl(request)– Returns the fully qualified endpoint (e.g.,https://api.myprovider.com/v1/chat/completions).buildHeaders(request)– Returns the authentication and content-type headers (e.g.,Authorization: Bearer <TOKEN>).transformRequest(body)– Converts the normalized OmniRoute request body into the provider’s proprietary wire format.
Example implementation for a hypothetical provider:
import { BaseExecutor } from "./base";
import type { ExecutorRequest } from "./types";
export class MyProviderExecutor extends BaseExecutor {
protected buildUrl(_: ExecutorRequest): string {
return "https://api.myprovider.com/v1/chat/completions";
}
protected buildHeaders(request: ExecutorRequest): Record<string, string> {
return {
Authorization: `Bearer ${request.apiKey}`,
"Content-Type": "application/json",
};
}
protected transformRequest(body: any): any {
// MyProvider expects a field called `prompt` instead of `messages`
return {
model: body.model,
prompt: body.messages.map((m: any) => m.content).join("\n"),
max_tokens: body.max_tokens,
};
}
}
Step 3 – Register the Executor in the Factory
After implementing the class, export it from open-sse/executors/index.ts so the factory can instantiate it when the router requests your provider ID.
Update the getExecutor function to return your new class:
import { MyProviderExecutor } from "./myprovider";
export function getExecutor(providerId: string): BaseExecutor {
switch (providerId) {
case "myprovider":
return new MyProviderExecutor();
// …existing cases…
default:
return new DefaultExecutor(); // fallback
}
}
This factory pattern ensures that OmniRoute’s routing logic remains decoupled from provider-specific implementation details.
Step 4 – Add a Request Translator (If Needed)
Most OmniRoute providers consume the OpenAI request schema, but some (e.g., Anthropic, Gemini) require field translation. If your provider uses a divergent JSON shape, add a translator module under open-sse/translator/ implementing translateRequest and translateResponse.
Wire the translator into open-sse/translator/index.ts so the dispatcher invokes it automatically:
import { translateMyProviderRequest } from "./myprovider";
export function translateRequest(
body: any,
sourceFormat: string,
targetFormat: string,
) {
if (targetFormat === "myprovider") {
return translateMyProviderRequest(body, targetFormat);
}
// existing translators …
}
This layer sits between the router and the executor, normalizing payloads without polluting the executor’s HTTP logic.
Step 5 – Configure OAuth (Optional)
For providers that use OAuth rather than static API keys, add the client credentials to src/lib/oauth/constants/oauth.ts. This file defines client IDs, secrets, and authorization URLs for OAuth-based flows.
Ensure the OAuth flow logic in src/lib/oauth/ is updated to handle your provider’s token exchange endpoints. Skip this step if your provider uses authType: "apiKey" as defined in Step 1.
Testing Your Implementation
Once the provider is registered, the executor is implemented, and the factory is wired, run the validation suite to verify integration:
npm run check
This command verifies that:
- The new provider ID passes Zod validation in the provider catalog.
- The executor factory correctly resolves the class for your provider ID.
- The combo routing engine can route requests to the new executor without type errors.
Summary
- Register the provider in
src/shared/constants/providers.tsusing Zod enums and metadata objects. - Extend
BaseExecutorinopen-sse/executors/to overridebuildUrl,buildHeaders, andtransformRequestfor custom HTTP logic. - Export the executor from
open-sse/executors/index.tsvia the factory functiongetExecutor. - Translate non-standard request schemas by adding modules under
open-sse/translator/and wiring them into the dispatcher. - Configure OAuth settings in
src/lib/oauth/constants/oauth.tsonly if the provider does not use API keys.
Frequently Asked Questions
Do I need to create a custom executor for every new provider?
No. If the provider follows the OpenAI-compatible request shape (same JSON schema, authentication header format, and endpoint structure), you can use the default executor located at open-sse/executors/default.ts. Only implement a custom executor when the provider requires non-standard headers, URL patterns, or request body transformations.
How do I handle authentication for OAuth-based providers?
Set authType: "oauth" in the provider metadata within src/shared/constants/providers.ts, then add the client ID and secret definitions to src/lib/oauth/constants/oauth.ts. The OmniRoute OAuth flow will handle token exchange and refresh automatically; your executor will receive the resolved bearer token via the request object passed to buildHeaders.
What if my provider returns a non-standard response format?
Implement a response translator in open-sse/translator/ that normalizes the provider’s output to the OpenAI-compatible response schema. Import this translator into open-sse/translator/index.ts and call it within the translateResponse function when the targetFormat matches your provider ID. This keeps the executor focused on HTTP transport while the translator handles schema mapping.
Can I test the executor without deploying the full application?
Yes. The executor class can be unit tested in isolation by instantiating it directly and mocking the ExecutorRequest object. Verify that buildUrl returns the correct endpoint, buildHeaders injects the API key, and transformRequest produces the expected wire format. Run npm run check to ensure the factory registration and type safety constraints are satisfied before deploying to the routing layer.
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 →