From 2cfd672f7996624966ca58014511e83c519a9619 Mon Sep 17 00:00:00 2001 From: Jeevan M Date: Thu, 6 Aug 2026 06:12:35 +0530 Subject: [PATCH] feat(providers): add UnoRouter provider (#8978) (#9009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log --- changelog.d/features/8978-unorouter.md | 1 + open-sse/config/providers/index.ts | 2 + .../providers/registry/unorouter/index.ts | 14 +++++++ public/providers/unorouter.svg | 1 + .../[id]/models/discovery/providerSets.ts | 1 + src/i18n/messages/en.json | 1 + src/shared/components/ProviderIcon.tsx | 1 + src/shared/constants/providers.ts | 1 + .../constants/providers/apikey/gateways.ts | 32 +++++++++----- tests/unit/unorouter-registry.test.ts | 42 +++++++++++++++++++ 10 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 changelog.d/features/8978-unorouter.md create mode 100644 open-sse/config/providers/registry/unorouter/index.ts create mode 100644 public/providers/unorouter.svg create mode 100644 tests/unit/unorouter-registry.test.ts diff --git a/changelog.d/features/8978-unorouter.md b/changelog.d/features/8978-unorouter.md new file mode 100644 index 0000000000..5a4e34839f --- /dev/null +++ b/changelog.d/features/8978-unorouter.md @@ -0,0 +1 @@ +- **feat(providers): add UnoRouter provider** — UnoRouter is an OpenAI-compatible routing gateway supporting hundreds of models. It is now registered as an API-key provider. ([#8978](https://github.com/diegosouzapw/OmniRoute/issues/8978)) diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index e586b42301..b12a6963f6 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -1,4 +1,5 @@ import type { RegistryEntry } from "./shared.ts"; +import { unorouterProvider } from "./registry/unorouter/index.ts"; import { aimlapiProvider } from "./registry/aimlapi/index.ts"; import { byteplusProvider } from "./registry/byteplus/index.ts"; @@ -444,4 +445,5 @@ export const REGISTRY: Record = { hcnsec: hcnsecProvider, promptql: promptqlProvider, hyperagent: hyperagentProvider, + unorouter: unorouterProvider, }; diff --git a/open-sse/config/providers/registry/unorouter/index.ts b/open-sse/config/providers/registry/unorouter/index.ts new file mode 100644 index 0000000000..771bde6a7c --- /dev/null +++ b/open-sse/config/providers/registry/unorouter/index.ts @@ -0,0 +1,14 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const unorouterProvider: RegistryEntry = { + id: "unorouter", + alias: "unorouter", + format: "openai", + executor: "default", + baseUrl: "https://api.unorouter.ai/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + defaultContextLength: 128000, + models: [{ id: "auto", name: "Auto (Best Available)" }], + passthroughModels: true, +}; diff --git a/public/providers/unorouter.svg b/public/providers/unorouter.svg new file mode 100644 index 0000000000..a9f5f22200 --- /dev/null +++ b/public/providers/unorouter.svg @@ -0,0 +1 @@ +UnoRouterU diff --git a/src/app/api/providers/[id]/models/discovery/providerSets.ts b/src/app/api/providers/[id]/models/discovery/providerSets.ts index 498c50dca2..212b18b9a3 100644 --- a/src/app/api/providers/[id]/models/discovery/providerSets.ts +++ b/src/app/api/providers/[id]/models/discovery/providerSets.ts @@ -42,6 +42,7 @@ export const NAMED_OPENAI_STYLE_PROVIDERS = new Set([ "ovhcloud", "sambanova", "orcarouter", + "unorouter", "uncloseai", "opencode-go", "baseten", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index c43d9a215e..a17e12b9d1 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -5769,6 +5769,7 @@ "onboardingProviderDescriptions": { "360ai": "Get API key at ai.360.cn", "agentrouter": "Get $200 free credits at https://agentrouter.org/register — no credit card required.", + "unorouter": "Create an API key at https://unorouter.ai, then paste it here as a Bearer token.", "agnes": "Get API key at agnes-ai.com", "aimlapi": "Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits.", "ai21": "$10 trial credits on signup (valid 3 months), no credit card required", diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index 099912f742..2112859d10 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -216,6 +216,7 @@ const KNOWN_SVGS = new Set([ "trae", "udio", "uncloseai", + "unorouter", "upstage", "v0", "veoaifree-web", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 23b4e03f8b..7fdca3033d 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -74,6 +74,7 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([ "getgoapi", "laozhang", "vercel-ai-gateway", + "unorouter", "agentrouter", "thebai", "fenayai", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 1f320ee394..e23cec3b72 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -45,6 +45,17 @@ export const APIKEY_PROVIDERS_GATEWAYS = { freeNote: "$200 free credits on signup - multi-model routing gateway", apiHint: "Get $200 free credits at https://agentrouter.org/register — no credit card required.", }, + unorouter: { + id: "unorouter", + alias: "unorouter", + name: "UnoRouter", + icon: "unorouter", + color: "#8B5CF6", + textIcon: "UR", + passthroughModels: true, + website: "https://unorouter.ai", + apiHint: "Create an API key at https://unorouter.ai, then paste it here as a Bearer token.", + }, "command-code": { id: "command-code", alias: "cmd", @@ -295,8 +306,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = { textIcon: "G4F", website: "https://g4f.space", hasFree: true, - freeNote: - "Free no-key reverse proxy to Groq (gpt4free project) — rate-limited to 5 req/min.", + freeNote: "Free no-key reverse proxy to Groq (gpt4free project) — rate-limited to 5 req/min.", passthroughModels: true, authHint: "No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits.", @@ -310,8 +320,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = { textIcon: "G4F", website: "https://g4f.space", hasFree: true, - freeNote: - "Free no-key reverse proxy to Gemini (gpt4free project) — rate-limited to 5 req/min.", + freeNote: "Free no-key reverse proxy to Gemini (gpt4free project) — rate-limited to 5 req/min.", passthroughModels: true, authHint: "No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits.", @@ -340,8 +349,7 @@ export const APIKEY_PROVIDERS_GATEWAYS = { textIcon: "G4F", website: "https://g4f.space", hasFree: true, - freeNote: - "Free no-key hosted Ollama gateway (gpt4free project) — rate-limited to 5 req/min.", + freeNote: "Free no-key hosted Ollama gateway (gpt4free project) — rate-limited to 5 req/min.", passthroughModels: true, authHint: "No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits.", @@ -759,7 +767,8 @@ export const APIKEY_PROVIDERS_GATEWAYS = { website: "https://ainative.studio", hasFree: true, freeNote: "Free tier ~10M tokens/month (claimed) across Qwen3, Llama 4, DeepSeek R1 and more.", - authHint: "Create a free API key at ainative.studio (no card), then paste it here as a Bearer token.", + authHint: + "Create a free API key at ainative.studio (no card), then paste it here as a Bearer token.", apiHint: "OpenAI-compatible endpoint at https://api.ainative.studio/api/v1 with a public /models catalog (84 models). OmniRoute lists models via passthrough.", }, @@ -774,7 +783,8 @@ export const APIKEY_PROVIDERS_GATEWAYS = { website: "https://www.aionlabs.ai", hasFree: true, freeNote: "Free tier ~20k tokens/day across the Aion reasoning models.", - authHint: "Create a free API key at aionlabs.ai (no card), then paste it here as a Bearer token.", + authHint: + "Create a free API key at aionlabs.ai (no card), then paste it here as a Bearer token.", apiHint: "OpenAI-compatible endpoint at https://api.aionlabs.ai/v1 with a public /models catalog carrying context and pricing.", }, @@ -788,7 +798,8 @@ export const APIKEY_PROVIDERS_GATEWAYS = { passthroughModels: true, website: "https://routeway.ai", hasFree: true, - freeNote: "Free models (:free suffix) at ~5 RPM / 200 RPD across Llama, Nemotron, Step and Laguna.", + freeNote: + "Free models (:free suffix) at ~5 RPM / 200 RPD across Llama, Nemotron, Step and Laguna.", authHint: "Create a free API key at routeway.ai, then paste it here as a Bearer token.", apiHint: "OpenAI-compatible endpoint at https://api.routeway.ai/v1 with a public /models catalog (236 models). Cloudflare fronts the API and requires a browser-style User-Agent.", @@ -804,7 +815,8 @@ export const APIKEY_PROVIDERS_GATEWAYS = { website: "https://bynara.id", hasFree: true, freeNote: "Free tier is a shared 5M tokens/day pool; some models are gated behind credit/plan.", - authHint: "Get a free API key via NaraRouter's Telegram channel, then paste it here as a Bearer token.", + authHint: + "Get a free API key via NaraRouter's Telegram channel, then paste it here as a Bearer token.", apiHint: "OpenAI-compatible endpoint at https://router.bynara.id/v1. Free-tier models are pinned; others need credit.", }, diff --git a/tests/unit/unorouter-registry.test.ts b/tests/unit/unorouter-registry.test.ts new file mode 100644 index 0000000000..3cfcd246a3 --- /dev/null +++ b/tests/unit/unorouter-registry.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts"); +const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); + +const UNOROUTER_CHAT_URL = "https://api.unorouter.ai/v1/chat/completions"; + +test("unorouter is registered as an API-key gateway provider", () => { + const entry = APIKEY_PROVIDERS.unorouter; + assert.ok(entry, "APIKEY_PROVIDERS.unorouter must be defined"); + assert.equal(entry.id, "unorouter"); + assert.equal(entry.alias, "unorouter"); + assert.equal(entry.name, "UnoRouter"); + assert.equal(entry.website, "https://unorouter.ai"); + assert.equal(entry.passthroughModels, true); +}); + +test("unorouter registry entry uses OpenAI format with bearer API-key auth", () => { + const entry = providerRegistry.unorouter; + assert.ok(entry, "providerRegistry.unorouter must be defined"); + assert.equal(entry.id, "unorouter"); + assert.equal(entry.alias, "unorouter"); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, UNOROUTER_CHAT_URL); + assert.equal(entry.passthroughModels, true); +}); + +test("unorouter ships with auto model", () => { + assert.deepEqual(providerRegistry.unorouter.models, [ + { id: "auto", name: "Auto (Best Available)" }, + ]); +}); + +test("unorouter accepts any model id via passthrough", () => { + assert.equal(isValidModel("unorouter", "openai/gpt-4o"), true); + assert.equal(isValidModel("unorouter", "anthropic/claude-3-5-sonnet"), true); +});