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.
This commit is contained in:
diegosouzapw
2026-04-23 17:23:51 -03:00
parent 968cbfeb15
commit 4057fe55a2
7 changed files with 253 additions and 3 deletions

View File

@@ -182,6 +182,14 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
"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<string, RegistryEntry> = {
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",

View File

@@ -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",

View File

@@ -303,6 +303,38 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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: {