From f984bef4ecb4cd1982bf0c7965194aea7ea0a5ae Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:46:30 -0300 Subject: [PATCH] feat(providers): add SumoPod and X5Lab OpenAI-compatible providers (#5963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 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 --- CHANGELOG.md | 1 + open-sse/config/providers/index.ts | 4 ++ .../providers/registry/sumopod/index.ts | 15 ++++ .../config/providers/registry/x5lab/index.ts | 15 ++++ src/shared/constants/config.ts | 2 + .../constants/providers/apikey/gateways.ts | 28 ++++++++ tests/snapshots/provider/translate-path.json | 46 ++++++++++++ tests/unit/providers-constants-split.test.ts | 12 ++-- tests/unit/sumopod-x5lab-provider.test.ts | 70 +++++++++++++++++++ 9 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 open-sse/config/providers/registry/sumopod/index.ts create mode 100644 open-sse/config/providers/registry/x5lab/index.ts create mode 100644 tests/unit/sumopod-x5lab-provider.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a7acb9d96a..9f826b19c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index bf28d75830..9239a5b7d2 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 = { aimlapi: aimlapiProvider, @@ -358,4 +360,6 @@ export const REGISTRY: Record = { "codebuddy-cn": codebuddy_cnProvider, pioneer: pioneerProvider, "zenmux-free": zenmux_freeProvider, + sumopod: sumopodProvider, + x5lab: x5labProvider, }; diff --git a/open-sse/config/providers/registry/sumopod/index.ts b/open-sse/config/providers/registry/sumopod/index.ts new file mode 100644 index 0000000000..1a5414e879 --- /dev/null +++ b/open-sse/config/providers/registry/sumopod/index.ts @@ -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, +}; diff --git a/open-sse/config/providers/registry/x5lab/index.ts b/open-sse/config/providers/registry/x5lab/index.ts new file mode 100644 index 0000000000..ac5112a3a1 --- /dev/null +++ b/open-sse/config/providers/registry/x5lab/index.ts @@ -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, +}; diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 6271c744bc..edbd57987b 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -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", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index cad4f206cf..4b0e2e31af 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -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 . 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 . 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.", + }, }; diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index c619402d7c..434627e149 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -3778,6 +3778,29 @@ "stream": "https://api.stepfun.com/v1/chat/completions" } }, + "sumopod": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "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 ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "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": { diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index c9c6638d43..7e187dbbb2 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -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/.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).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", () => { diff --git a/tests/unit/sumopod-x5lab-provider.test.ts b/tests/unit/sumopod-x5lab-provider.test.ts new file mode 100644 index 0000000000..e539726273 --- /dev/null +++ b/tests/unit/sumopod-x5lab-provider.test.ts @@ -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"); +});