mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(providers): add SumoPod and X5Lab OpenAI-compatible providers (#5963)
* feat(providers): add SumoPod and X5Lab OpenAI-compatible providers Both are OpenAI-compatible BYOK aggregator gateways, wired via the default executor with bearer API-key auth. Neither ships a hardcoded model list — both use passthroughModels with an empty seed list and a live /v1/models fetcher, so the catalog always reflects what each gateway actually serves instead of speculative model IDs. - SumoPod: https://ai.sumopod.com/v1/chat/completions (sk- keys) - X5Lab: https://api.x5lab.dev/v1/chat/completions (x5- keys) Regression guard: tests/unit/sumopod-x5lab-provider.test.ts. Co-authored-by: Rigel Ramadhani Waloni <rigel8911@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/1288 * chore(changelog): restore release entries + add sumopod/x5lab bullet * test(golden): regenerate translate-path for sumopod + x5lab providers * test(providers): bump APIKEY count 164→166 for sumopod + x5lab --------- Co-authored-by: Rigel Ramadhani Waloni <rigel8911@gmail.com>
This commit is contained in:
committed by
GitHub
parent
7658a7da1e
commit
f984bef4ec
@@ -25,6 +25,7 @@
|
||||
- **feat(providers):** add b.ai as an OpenAI-compatible (API-key) provider. (thanks @DEYLNN)
|
||||
- **feat(providers):** add Nube.sh as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
|
||||
- **feat(providers):** add Charm Hyper as an OpenAI-compatible (API-key) provider. (thanks @whale9820)
|
||||
- **feat(providers):** add SumoPod and X5Lab as OpenAI-compatible (API-key) providers. (thanks @rigelra15)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
|
||||
@@ -178,6 +178,8 @@ import { grok_cliProvider } from "./registry/grok-cli/index.ts";
|
||||
import { codebuddy_cnProvider } from "./registry/codebuddy-cn/index.ts";
|
||||
import { pioneerProvider } from "./registry/pioneer/index.ts";
|
||||
import { zenmux_freeProvider } from "./registry/zenmux-free/index.ts";
|
||||
import { sumopodProvider } from "./registry/sumopod/index.ts";
|
||||
import { x5labProvider } from "./registry/x5lab/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -358,4 +360,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"codebuddy-cn": codebuddy_cnProvider,
|
||||
pioneer: pioneerProvider,
|
||||
"zenmux-free": zenmux_freeProvider,
|
||||
sumopod: sumopodProvider,
|
||||
x5lab: x5labProvider,
|
||||
};
|
||||
|
||||
15
open-sse/config/providers/registry/sumopod/index.ts
Normal file
15
open-sse/config/providers/registry/sumopod/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const sumopodProvider: RegistryEntry = {
|
||||
id: "sumopod",
|
||||
alias: "sumopod",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://ai.sumopod.com/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://ai.sumopod.com/v1/models",
|
||||
defaultContextLength: 128000,
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
15
open-sse/config/providers/registry/x5lab/index.ts
Normal file
15
open-sse/config/providers/registry/x5lab/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const x5labProvider: RegistryEntry = {
|
||||
id: "x5lab",
|
||||
alias: "x5lab",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.x5lab.dev/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://api.x5lab.dev/v1/models",
|
||||
defaultContextLength: 128000,
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -20,6 +20,8 @@ export const PROVIDER_ENDPOINTS = {
|
||||
openadapter: "https://api.openadapter.in/v1/chat/completions",
|
||||
dit: "https://api.dit.ai/v1/chat/completions",
|
||||
tokenrouter: "https://api.tokenrouter.com/v1/chat/completions",
|
||||
sumopod: "https://ai.sumopod.com/v1/chat/completions",
|
||||
x5lab: "https://api.x5lab.dev/v1/chat/completions",
|
||||
openai: "https://api.openai.com/v1/chat/completions",
|
||||
anthropic: "https://api.anthropic.com/v1/messages",
|
||||
gemini: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||
|
||||
@@ -602,4 +602,32 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
|
||||
apiHint:
|
||||
"TokenRouter exposes an OpenAI-compatible chat completions endpoint at https://api.tokenrouter.com/v1/chat/completions, plus a working /v1/models catalog. OmniRoute uses the OpenAI protocol.",
|
||||
},
|
||||
sumopod: {
|
||||
id: "sumopod",
|
||||
alias: "sumopod",
|
||||
name: "SumoPod",
|
||||
icon: "router",
|
||||
color: "#2563EB",
|
||||
textIcon: "SP",
|
||||
passthroughModels: true,
|
||||
website: "https://ai.sumopod.com",
|
||||
authHint:
|
||||
"Use your SumoPod API key (sk-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1.",
|
||||
apiHint:
|
||||
"SumoPod exposes an OpenAI-compatible chat completions endpoint at https://ai.sumopod.com/v1/chat/completions, plus a live /v1/models catalog. OmniRoute uses the OpenAI protocol and lists models via passthrough.",
|
||||
},
|
||||
x5lab: {
|
||||
id: "x5lab",
|
||||
alias: "x5lab",
|
||||
name: "X5Lab",
|
||||
icon: "router",
|
||||
color: "#7C3AED",
|
||||
textIcon: "X5",
|
||||
passthroughModels: true,
|
||||
website: "https://x5lab.dev",
|
||||
authHint:
|
||||
"Use your X5Lab API key (x5-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.x5lab.dev/v1.",
|
||||
apiHint:
|
||||
"X5Lab exposes an OpenAI-compatible chat completions endpoint at https://api.x5lab.dev/v1/chat/completions, plus a live /v1/models catalog. OmniRoute uses the OpenAI protocol and lists models via passthrough.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3778,6 +3778,29 @@
|
||||
"stream": "https://api.stepfun.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"sumopod": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://ai.sumopod.com/v1/chat/completions",
|
||||
"stream": "https://ai.sumopod.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"suno": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -4264,6 +4287,29 @@
|
||||
"stream": "https://server.self-serve.windsurf.com"
|
||||
}
|
||||
},
|
||||
"x5lab": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.x5lab.dev/v1/chat/completions",
|
||||
"stream": "https://api.x5lab.dev/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"xai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Characterization of the providers.ts catalog split (god-file decomposition): the host became a
|
||||
// barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is
|
||||
// merged from 6 semantic family files (apikey/<family>.ts). Locks: the public surface (every catalog
|
||||
// + helpers still exported), the spread-merge integrity (164 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (166 APIKEY entries, no loss/dup), and that
|
||||
// load-time Zod validation still runs. Pure-data move → behavior must be identical.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -31,12 +31,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 164 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 164);
|
||||
assert.equal(new Set(keys).size, 164, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 166);
|
||||
assert.equal(new Set(keys).size, 166, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 164.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 166.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -56,7 +56,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 164 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 164, "families must partition all 164 providers");
|
||||
assert.equal(famTotal, 166, "families must partition all 166 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
70
tests/unit/sumopod-x5lab-provider.test.ts
Normal file
70
tests/unit/sumopod-x5lab-provider.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
|
||||
const SUMOPOD_CHAT_URL = "https://ai.sumopod.com/v1/chat/completions";
|
||||
const SUMOPOD_MODELS_URL = "https://ai.sumopod.com/v1/models";
|
||||
|
||||
const X5LAB_CHAT_URL = "https://api.x5lab.dev/v1/chat/completions";
|
||||
const X5LAB_MODELS_URL = "https://api.x5lab.dev/v1/models";
|
||||
|
||||
test("SumoPod is registered as an OpenAI-compatible API-key gateway", () => {
|
||||
const entry = APIKEY_PROVIDERS.sumopod;
|
||||
assert.ok(entry, "APIKEY_PROVIDERS.sumopod must be defined");
|
||||
assert.equal(entry.id, "sumopod");
|
||||
assert.equal(entry.alias, "sumopod");
|
||||
assert.equal(entry.name, "SumoPod");
|
||||
assert.equal(entry.website, "https://ai.sumopod.com");
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("X5Lab is registered as an OpenAI-compatible API-key gateway", () => {
|
||||
const entry = APIKEY_PROVIDERS.x5lab;
|
||||
assert.ok(entry, "APIKEY_PROVIDERS.x5lab must be defined");
|
||||
assert.equal(entry.id, "x5lab");
|
||||
assert.equal(entry.alias, "x5lab");
|
||||
assert.equal(entry.name, "X5Lab");
|
||||
assert.equal(entry.website, "https://x5lab.dev");
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("SumoPod exposes the OpenAI-compatible chat completions endpoint", () => {
|
||||
assert.equal(PROVIDER_ENDPOINTS.sumopod, SUMOPOD_CHAT_URL);
|
||||
});
|
||||
|
||||
test("X5Lab exposes the OpenAI-compatible chat completions endpoint", () => {
|
||||
assert.equal(PROVIDER_ENDPOINTS.x5lab, X5LAB_CHAT_URL);
|
||||
});
|
||||
|
||||
test("SumoPod registry entry uses OpenAI format with bearer API-key auth and passthrough models", () => {
|
||||
const entry = providerRegistry.sumopod;
|
||||
assert.ok(entry, "providerRegistry.sumopod must be defined");
|
||||
assert.equal(entry.id, "sumopod");
|
||||
assert.equal(entry.alias, "sumopod");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, SUMOPOD_CHAT_URL);
|
||||
assert.equal(entry.modelsUrl, SUMOPOD_MODELS_URL);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, [], "SumoPod ships no speculative seeded models — passthrough only");
|
||||
});
|
||||
|
||||
test("X5Lab registry entry uses OpenAI format with bearer API-key auth and passthrough models", () => {
|
||||
const entry = providerRegistry.x5lab;
|
||||
assert.ok(entry, "providerRegistry.x5lab must be defined");
|
||||
assert.equal(entry.id, "x5lab");
|
||||
assert.equal(entry.alias, "x5lab");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, X5LAB_CHAT_URL);
|
||||
assert.equal(entry.modelsUrl, X5LAB_MODELS_URL);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, [], "X5Lab ships no speculative seeded models — passthrough only");
|
||||
});
|
||||
Reference in New Issue
Block a user