How to Add a New AI Provider to OmniRoute: A Step-by-Step Integration Guide
Adding a new AI provider to OmniRoute requires registering the provider identifier in the constants file, configuring the central registry with endpoint details, wiring an executor into the factory, and exposing the provider in the dashboard UI.
OmniRoute is an open-source AI gateway that unifies multiple LLM providers behind a single routing layer. Extending the platform to support a new AI provider involves coordinated updates across the registry in open-sse/config/providerRegistry.ts, the executor layer in open-sse/executors/, and the constants definition in src/shared/constants/providers.ts. This guide documents the complete integration workflow based on the release/v3.8.50 branch.
Register the Provider Identifier
Start by adding the provider’s canonical name and metadata to the shared constants. This makes the provider ID searchable throughout the codebase and available for type checking.
In src/shared/constants/providers.ts, extend the PROVIDERS object with a new entry:
export const PROVIDERS = {
...DEFAULT_PROVIDERS,
"mynewai": {
id: "mynewai",
name: "MyNewAI",
category: "API_KEY",
// Optional: oauthConfig, defaultModel, etc.
},
};
The category field typically uses "API_KEY" for token-based authentication, though OAuth-based providers use different categories.
Configure the Provider Registry
The open-sse/config/providerRegistry.ts file serves as the single source of truth for routing, authentication schemes, and capability checks. Populate the REGISTRY object with the provider’s base URL, authorization header logic, and supported models:
REGISTRY["mynewai"] = {
baseUrl: "https://api.mynewai.com/v1",
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
models: ["gpt-4o", "gpt-4-mini"],
// Optional: specify custom transformers if the API deviates from OpenAI format
requestTransformer: "mynewaiRequest",
responseTransformer: "mynewaiResponse",
};
The authHeader function receives the user’s API key and must return the headers required for authentication. If the provider uses a non-standard payload format, define requestTransformer and responseTransformer keys here; the implementations are registered separately.
Implement the Executor Layer
Choose Between Default and Custom Executors
Most OpenAI-compatible providers operate correctly with the DefaultExecutor. However, if the new provider requires custom header handling, special error processing, or unique request shaping, create a subclass of BaseExecutor.
In open-sse/executors/base.ts, the BaseExecutor class defines the interface:
export abstract class BaseExecutor {
protected abstract buildHeaders(cfg: ProviderConfig): Record<string, string>;
abstract execute(request: Request): Promise<Response>;
}
Create a custom executor in a new file such as open-sse/executors/customProvider.ts:
import { BaseExecutor } from "./base";
export class MyNewAIExecutor extends BaseExecutor {
protected buildHeaders(cfg) {
const base = super.buildHeaders(cfg);
// MyNewAI requires an additional client identifier header
return { ...base, "X-MyNewAI-Client": cfg.clientId };
}
}
Wire the Executor into the Factory
Update open-sse/executors/index.ts to return the new executor instance when the provider ID matches:
import { MyNewAIExecutor } from "./customProvider";
export function getExecutor(providerId: string) {
switch (providerId) {
case "mynewai":
return new MyNewAIExecutor();
// Existing cases...
default:
return new DefaultExecutor();
}
}
The getExecutor factory is invoked by the routing layer to determine which execution strategy to apply for incoming requests.
Handle Request/Response Translation
If the provider deviates from the OpenAI chat completions format, implement translator functions that convert OmniRoute’s canonical request shape into the provider’s expected payload.
In open-sse/translator/index.ts, register transformers matching the keys defined in the registry:
export const requestTransformers = {
...DEFAULT_TRANSFORMERS,
mynewaiRequest: (body) => ({
prompt: body.messages.map((m) => m.content).join("\n"),
temperature: body.temperature,
max_tokens: body.max_tokens,
}),
};
export const responseTransformers = {
...DEFAULT_TRANSFORMERS,
mynewaiResponse: (raw) => ({
choices: raw.outputs.map((text, index) => ({
index,
message: { role: "assistant", content: text },
})),
}),
};
These transformers ensure that requests leaving OmniRoute conform to the provider’s API contract and that responses are normalized back to the OpenAI-compatible format expected by downstream clients.
Expose the Provider in the API
The REST route wrappers under src/app/api/v1/providers/[provider]/ automatically pick up registry entries. Typically, no code changes are required here unless you need special endpoint behavior such as OAuth login flows.
The completions endpoint at src/app/api/v1/providers/[provider]/chat/completions/route.ts dynamically handles the [provider] parameter by looking up the registry and invoking the appropriate executor:
// The route extracts providerId from params and delegates to the executor layer
const executor = getExecutor(providerId);
return executor.execute(request);
Update the Dashboard UI
Featured Providers List
To make the provider selectable in the "Add Provider" interface, update src/app/(dashboard)/dashboard/providers/featuredProviders.ts:
export const featuredProviders = [
...DEFAULT_FEATURED,
{
id: "mynewai",
name: "MyNewAI",
description: "Custom AI models with specialized reasoning capabilities",
logo: "/logos/mynewai.svg",
},
];
Provider Detail Page
Create a helper file for the provider’s configuration page. Copy an existing implementation from src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts and adjust the ID constant:
// src/app/(dashboard)/dashboard/providers/mynewai/helpers.ts
export const PROVIDER_ID = "mynewai";
export const requiredFields = ["apiKey", "baseUrl"];
Manage Model Catalogs
If the provider ships with unique model identifiers, ensure they appear in the global catalog. The catalog generator at src/app/api/v1/models/catalog.ts pulls data directly from the REGISTRY object defined in open-sse/config/providerRegistry.ts.
No additional code is required beyond the registry entry, though you should verify that the models array in the registry accurately reflects the provider’s available endpoints.
Database Considerations
Most providers store connection credentials in the existing provider_connections table, so migrations are rarely necessary. If your provider requires additional per-provider configuration tables, create a new migration in db/migrations/:
# Example migration file: db/migrations/0003_add_mynewai_config.sql
ALTER TABLE provider_connections ADD COLUMN client_id TEXT;
Run the migration script to bump the schema version before deploying.
Testing the Integration
Unit Tests
Add tests under tests/unit/providers/mynewai.test.ts that mock the provider’s HTTP endpoint and verify request shaping:
import { vi, describe, it, expect } from "vitest";
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ choices: [{ message: { content: "hello" } }] }),
}));
describe("MyNewAI Provider", () => {
it("should transform requests to flat prompt format", async () => {
// Assert that the executor calls fetch with the expected body structure
});
});
Integration Tests
Execute end-to-end tests by running the full route with the new provider selected:
npm run test:all
Verify that the CLI command omniroute providers list displays the new entry and that POST /api/v1/chat/completions routes correctly to the new provider.
Summary
- Register the provider in
src/shared/constants/providers.tsto establish the canonical ID and category. - Configure the registry in
open-sse/config/providerRegistry.tswith base URLs, auth headers, and model lists. - Implement executors by extending
BaseExecutorinopen-sse/executors/and wiring them into the factory atopen-sse/executors/index.ts. - Add translators in
open-sse/translator/index.tsfor non-OpenAI request/response formats. - Update the UI by modifying
featuredProviders.tsand creating provider-specific helpers. - Test thoroughly using unit mocks in
tests/unit/and integration tests against the live API routes.
Frequently Asked Questions
Do I need to create a custom executor for every new provider?
No. Use the DefaultExecutor for any provider that follows the standard OpenAI chat completions format. Create a custom executor extending BaseExecutor only when the provider requires unique header injection, error handling logic, or request preprocessing that cannot be handled by the default implementation.
How do I handle providers that don't use the OpenAI request/response format?
Implement request and response transformers in open-sse/translator/index.ts. Register these functions under requestTransformers and responseTransformers using keys that match the requestTransformer and responseTransformer values defined in your registry entry. These functions normalize the payload between OmniRoute’s internal format and the provider’s native API contract.
What database changes are required when adding a provider?
Typically none. OmniRoute uses a generic provider_connections table that stores encrypted credentials for all providers. Only create a new migration in db/migrations/ if your provider requires additional relational data beyond the standard apiKey, baseUrl, and metadata fields.
Where should I add unit tests for the new provider?
Place provider-specific unit tests in tests/unit/providers/[providerName].test.ts, following the pattern used for existing providers. Mock globalThis.fetch to simulate the provider’s API responses, and assert that the executor generates the correct request headers and body structure according to your registry configuration.
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 →