From 4057fe55a2aee84579c4c5c8c15d24358f957b60 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 23 Apr 2026 17:23:51 -0300 Subject: [PATCH] feat(providers): add new OpenAI-compatible gateway providers Register GLHF, CablyAI, TheB.AI, and FenayAI across the provider registry, managed provider catalog, and provider metadata so they can be configured like other API key-backed integrations. Extend the provider models route to fetch remote model catalogs for these gateways and add unit coverage for catalog resolution and managed provider handling. Increase unit test concurrency to keep the expanded test suite faster to run. --- open-sse/config/providerRegistry.ts | 60 +++++++++++++++++++ package.json | 6 +- src/app/api/providers/[id]/models/route.ts | 32 ++++++++++ src/shared/constants/providers.ts | 44 ++++++++++++++ tests/unit/provider-models-route.test.ts | 54 +++++++++++++++++ tests/unit/providers-page-utils.test.ts | 28 +++++++++ .../providers-route-managed-catalog.test.ts | 32 ++++++++++ 7 files changed, 253 insertions(+), 3 deletions(-) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 3d6c5caf5d..c56df4e72c 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -182,6 +182,14 @@ const CHAT_OPENAI_COMPAT_MODELS: Record = { "aisingapore/Qwen-SEA-LION-v4-32B-IT", "allenai/Olmo-3-32B-Think", ]), + glhf: buildModels([ + "hf:meta-llama/Llama-3.3-70B-Instruct", + "hf:Qwen/Qwen2.5-72B-Instruct", + "hf:deepseek-ai/DeepSeek-V3", + ]), + cablyai: buildModels(["gpt-4o", "gpt-4o-mini", "deepseek-chat"]), + thebai: buildModels(["gpt-4o", "claude-3.5-sonnet", "llama-3.3-70b"]), + fenayai: buildModels(["gpt-4o", "claude-3.5-sonnet", "deepseek-chat"]), moonshot: buildModels(["kimi-k2.5", "kimi-latest", "moonshot-v1-auto"]), "meta-llama": buildModels([ "Llama-3.3-70B-Instruct", @@ -1872,6 +1880,58 @@ export const REGISTRY: Record = { models: CHAT_OPENAI_COMPAT_MODELS.publicai, }, + glhf: { + id: "glhf", + alias: "glhf", + format: "openai", + executor: "default", + baseUrl: "https://glhf.chat/api/openai/v1/chat/completions", + modelsUrl: "https://glhf.chat/api/openai/v1/models", + authType: "apikey", + authHeader: "bearer", + models: CHAT_OPENAI_COMPAT_MODELS.glhf, + passthroughModels: true, + }, + + cablyai: { + id: "cablyai", + alias: "cablyai", + format: "openai", + executor: "default", + baseUrl: "https://cablyai.com/v1/chat/completions", + modelsUrl: "https://cablyai.com/v1/models", + authType: "apikey", + authHeader: "bearer", + models: CHAT_OPENAI_COMPAT_MODELS.cablyai, + passthroughModels: true, + }, + + thebai: { + id: "thebai", + alias: "thebai", + format: "openai", + executor: "default", + baseUrl: "https://api.theb.ai/v1/chat/completions", + modelsUrl: "https://api.theb.ai/v1/models", + authType: "apikey", + authHeader: "bearer", + models: CHAT_OPENAI_COMPAT_MODELS.thebai, + passthroughModels: true, + }, + + fenayai: { + id: "fenayai", + alias: "fenayai", + format: "openai", + executor: "default", + baseUrl: "https://fenayai.com/v1/chat/completions", + modelsUrl: "https://fenayai.com/v1/models", + authType: "apikey", + authHeader: "bearer", + models: CHAT_OPENAI_COMPAT_MODELS.fenayai, + passthroughModels: true, + }, + moonshot: { id: "moonshot", alias: "moonshot", diff --git a/package.json b/package.json index 81ff09242f..b96bec6cc6 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "electron:build:win": "npm run build && cd electron && npm run build:win", "electron:build:mac": "npm run build && cd electron && npm run build:mac", "electron:build:linux": "npm run build && cd electron && npm run build:linux", - "test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test --test-concurrency=1 tests/unit/*.test.ts", - "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test --test-concurrency=1 tests/unit/*.test.ts", + "test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test --test-concurrency=10 tests/unit/*.test.ts", + "test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test --test-concurrency=10 tests/unit/*.test.ts", "test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/plan3-p0.test.ts", "test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/fixes-p1.test.ts", "test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --test tests/unit/security-fase01.test.ts", @@ -89,7 +89,7 @@ "test:protocols:e2e": "node scripts/run-protocol-clients-tests.mjs", "test:vitest": "vitest run --config vitest.mcp.config.ts", "test:ecosystem": "node scripts/run-ecosystem-tests.mjs", - "test:coverage": "c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --import tsx/esm --test --test-concurrency=1 tests/unit/*.test.ts", + "test:coverage": "c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --import tsx/esm --test --test-concurrency=10 tests/unit/*.test.ts", "test:coverage:legacy": "c8 --output-dir coverage --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx/esm --test tests/unit/*.test.ts", "coverage:report": "c8 report --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov", "coverage:summary": "node scripts/test-report-summary.mjs --input coverage/coverage-summary.json --output coverage/coverage-report.md --threshold 60", diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 700902ab1f..b33350f69d 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -303,6 +303,38 @@ const PROVIDER_MODELS_CONFIG: Record = { authPrefix: "Bearer ", parseResponse: (data) => data.data || [], }, + glhf: { + url: "https://glhf.chat/api/openai/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || data.models || [], + }, + cablyai: { + url: "https://cablyai.com/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || data.models || [], + }, + thebai: { + url: "https://api.theb.ai/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || data.models || [], + }, + fenayai: { + url: "https://fenayai.com/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: (data) => data.data || data.models || [], + }, kimi: { url: "https://api.moonshot.ai/v1/models", method: "GET", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 97ddbb43b9..cd5ddb8d88 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -904,6 +904,50 @@ export const APIKEY_PROVIDERS = { website: "https://api.laozhang.ai", passthroughModels: true, }, + glhf: { + id: "glhf", + alias: "glhf", + name: "GLHF Chat", + icon: "hub", + color: "#10B981", + textIcon: "GH", + website: "https://glhf.chat", + authHint: "Bearer API key for the GLHF OpenAI-compatible gateway.", + passthroughModels: true, + }, + cablyai: { + id: "cablyai", + alias: "cablyai", + name: "CablyAI", + icon: "hub", + color: "#FF4081", + textIcon: "CA", + website: "https://cablyai.com", + authHint: "Bearer API key for the CablyAI OpenAI-compatible gateway.", + passthroughModels: true, + }, + thebai: { + id: "thebai", + alias: "thebai", + name: "TheB.AI", + icon: "hub", + color: "#3B82F6", + textIcon: "TB", + website: "https://theb.ai", + authHint: "Bearer API key for the TheB.AI OpenAI-compatible gateway.", + passthroughModels: true, + }, + fenayai: { + id: "fenayai", + alias: "fenayai", + name: "FenayAI", + icon: "hub", + color: "#FF9800", + textIcon: "FN", + website: "https://fenayai.com", + authHint: "Bearer API key for the FenayAI OpenAI-compatible gateway.", + passthroughModels: true, + }, "fal-ai": { id: "fal-ai", alias: "fal", diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index 0b8dd55896..c30e6b6496 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -272,6 +272,60 @@ test("provider models route prefers the remote OpenRouter /models API over stati assert.deepEqual(body.models, [{ id: "openai/gpt-4.1", name: "GPT-4.1 via OpenRouter" }]); }); +test("provider models route fetches remote catalogs for new OpenAI-compatible gateway providers", async () => { + const cases = [ + { + provider: "glhf", + apiKey: "glhf-key", + expectedUrl: "https://glhf.chat/api/openai/v1/models", + model: { id: "hf:Qwen/Qwen2.5-72B-Instruct", name: "Qwen 2.5 72B via GLHF" }, + }, + { + provider: "cablyai", + apiKey: "cably-key", + expectedUrl: "https://cablyai.com/v1/models", + model: { id: "gpt-4o", name: "GPT-4o via CablyAI" }, + }, + { + provider: "thebai", + apiKey: "theb-key", + expectedUrl: "https://api.theb.ai/v1/models", + model: { id: "gpt-4o", name: "GPT-4o via TheB.AI" }, + }, + { + provider: "fenayai", + apiKey: "fenay-key", + expectedUrl: "https://fenayai.com/v1/models", + model: { id: "deepseek-chat", name: "DeepSeek Chat via FenayAI" }, + }, + ]; + + for (const entry of cases) { + const connection = await seedConnection(entry.provider, { + apiKey: entry.apiKey, + }); + const seenUrls = []; + + globalThis.fetch = async (url, init = {}) => { + seenUrls.push(String(url)); + assert.equal(init.method, "GET"); + assert.equal(init.headers.Authorization, `Bearer ${entry.apiKey}`); + return Response.json({ + data: [entry.model], + }); + }; + + const response = await callRoute(connection.id, "?refresh=true"); + const body = (await response.json()) as any; + + assert.equal(response.status, 200); + assert.equal(body.provider, entry.provider); + assert.equal(body.source, "api"); + assert.deepEqual(seenUrls, [entry.expectedUrl]); + assert.deepEqual(body.models, [entry.model]); + } +}); + test("provider models route returns the local catalog for new built-in chat-openai-compat providers", async () => { const connection = await seedConnection("deepinfra", { apiKey: "deepinfra-key", diff --git a/tests/unit/providers-page-utils.test.ts b/tests/unit/providers-page-utils.test.ts index 1dc7a4f43b..8db1f1191f 100644 --- a/tests/unit/providers-page-utils.test.ts +++ b/tests/unit/providers-page-utils.test.ts @@ -220,6 +220,7 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre const searchProvider = providerPageUtils.resolveDashboardProviderInfo("brave-search"); const audioProvider = providerPageUtils.resolveDashboardProviderInfo("assemblyai"); const webCookieProvider = providerPageUtils.resolveDashboardProviderInfo("grok-web"); + const apiKeyProvider = providerPageUtils.resolveDashboardProviderInfo("glhf"); const perplexityWebProvider = providerPageUtils.resolveDashboardProviderInfo("perplexity-web"); const blackboxWebProvider = providerPageUtils.resolveDashboardProviderInfo("blackbox-web"); const museSparkWebProvider = providerPageUtils.resolveDashboardProviderInfo("muse-spark-web"); @@ -236,6 +237,9 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre assert.equal(audioProvider?.category, "audio"); assert.equal(audioProvider?.name, providers.AUDIO_ONLY_PROVIDERS.assemblyai.name); + assert.equal(apiKeyProvider?.category, "apikey"); + assert.equal(apiKeyProvider?.name, providers.APIKEY_PROVIDERS.glhf.name); + assert.equal(webCookieProvider?.category, "web-cookie"); assert.equal(webCookieProvider?.name, providers.WEB_COOKIE_PROVIDERS["grok-web"].name); @@ -257,6 +261,10 @@ test("static catalog entries resolve local, search, audio, web-cookie and upstre test("managed provider connection ids include supported static categories and exclude upstream proxy", () => { assert.equal(providerCatalog.isManagedProviderConnectionId("qoder"), true); + assert.equal(providerCatalog.isManagedProviderConnectionId("glhf"), true); + assert.equal(providerCatalog.isManagedProviderConnectionId("cablyai"), true); + assert.equal(providerCatalog.isManagedProviderConnectionId("thebai"), true); + assert.equal(providerCatalog.isManagedProviderConnectionId("fenayai"), true); assert.equal(providerCatalog.isManagedProviderConnectionId("sdwebui"), true); assert.equal(providerCatalog.isManagedProviderConnectionId("lm-studio"), true); assert.equal(providerCatalog.isManagedProviderConnectionId("vllm"), true); @@ -285,6 +293,10 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent assert.equal("blackbox-web" in providers.WEB_COOKIE_PROVIDERS, true); assert.equal("muse-spark-web" in providers.APIKEY_PROVIDERS, false); assert.equal("muse-spark-web" in providers.WEB_COOKIE_PROVIDERS, true); + assert.equal("glhf" in providers.APIKEY_PROVIDERS, true); + assert.equal("cablyai" in providers.APIKEY_PROVIDERS, true); + assert.equal("thebai" in providers.APIKEY_PROVIDERS, true); + assert.equal("fenayai" in providers.APIKEY_PROVIDERS, true); const apiKeyEntries = providerPageUtils.buildStaticProviderEntries("apikey", () => ({ total: 0, @@ -316,6 +328,22 @@ test("grok-web taxonomy stays web-cookie only and does not leak into api-key ent apiKeyEntries.some((entry) => entry.providerId === "grok-web"), false ); + assert.equal( + apiKeyEntries.some((entry) => entry.providerId === "glhf"), + true + ); + assert.equal( + apiKeyEntries.some((entry) => entry.providerId === "cablyai"), + true + ); + assert.equal( + apiKeyEntries.some((entry) => entry.providerId === "thebai"), + true + ); + assert.equal( + apiKeyEntries.some((entry) => entry.providerId === "fenayai"), + true + ); assert.equal( localEntries.some((entry) => entry.providerId === "sdwebui"), true diff --git a/tests/unit/providers-route-managed-catalog.test.ts b/tests/unit/providers-route-managed-catalog.test.ts index 2fd14f0488..48f81b6452 100644 --- a/tests/unit/providers-route-managed-catalog.test.ts +++ b/tests/unit/providers-route-managed-catalog.test.ts @@ -30,6 +30,38 @@ test.after(() => { test("providers route accepts managed local, audio, web-cookie and search providers", async () => { const cases = [ + { + provider: "glhf", + body: { + provider: "glhf", + apiKey: "glhf-key", + name: "GLHF Chat", + }, + }, + { + provider: "cablyai", + body: { + provider: "cablyai", + apiKey: "cably-key", + name: "CablyAI Primary", + }, + }, + { + provider: "thebai", + body: { + provider: "thebai", + apiKey: "theb-key", + name: "TheB.AI Primary", + }, + }, + { + provider: "fenayai", + body: { + provider: "fenayai", + apiKey: "fenay-key", + name: "FenayAI Primary", + }, + }, { provider: "sdwebui", body: {