feat(providers): add four OpenAI-compatible aggregator providers (dahl, atria, agnes, bai)
Adds Dahl Inference, Atria Dawn, Agnes AI, and B.AI following the existing registry pattern. Each uses DefaultExecutor with no custom translator needed.
This commit is contained in:
29
open-sse/providers/registry/agnes.js
Normal file
29
open-sse/providers/registry/agnes.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
id: "agnes",
|
||||
priority: 120,
|
||||
alias: "agnes",
|
||||
aliases: [
|
||||
"agnes-ai",
|
||||
],
|
||||
uiAlias: "agnes",
|
||||
display: {
|
||||
name: "Agnes AI",
|
||||
icon: "auto_awesome",
|
||||
color: "#7C3AED",
|
||||
textIcon: "AG",
|
||||
website: "https://agnes-ai.com",
|
||||
notice: {
|
||||
text: "OpenAI-compatible gateway from Agnes AI, offering free API credits on sign-up. Accepts a bearer token or an x-api-key header.",
|
||||
apiKeyUrl: "https://platform.agnes-ai.com",
|
||||
},
|
||||
},
|
||||
category: "freeTier",
|
||||
authType: "apikey",
|
||||
transport: {
|
||||
baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions",
|
||||
validateUrl: "https://apihub.agnes-ai.com/v1/models",
|
||||
},
|
||||
// No model ids could be verified without a key, so discovery is left to the
|
||||
// live endpoint and any id is accepted through passthroughModels.
|
||||
passthroughModels: true,
|
||||
};
|
||||
32
open-sse/providers/registry/atria.js
Normal file
32
open-sse/providers/registry/atria.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export default {
|
||||
id: "atria",
|
||||
priority: 120,
|
||||
alias: "atria",
|
||||
aliases: [
|
||||
"atria-asi",
|
||||
],
|
||||
uiAlias: "atria",
|
||||
display: {
|
||||
name: "Atria Dawn",
|
||||
icon: "flare",
|
||||
color: "#C2410C",
|
||||
textIcon: "AD",
|
||||
website: "https://atria-asi.ai",
|
||||
notice: {
|
||||
text: "OpenAI-compatible endpoint from Atria Dawn (AtomInnoLab). Currently a research preview offering a single text model, Atria-Dawn-Preview.",
|
||||
apiKeyUrl: "https://api.atria-asi.ai/dashboard",
|
||||
},
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
transport: {
|
||||
baseUrl: "https://api.atria-asi.ai/v1/chat/completions",
|
||||
validateUrl: "https://api.atria-asi.ai/v1/models",
|
||||
},
|
||||
// Docs pin the model field to one case-sensitive id. Text-only for now: the
|
||||
// service ships a hook that blocks image/PDF input, so no vision is claimed.
|
||||
models: [
|
||||
{ id: "Atria-Dawn-Preview", name: "Atria Dawn Preview" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
};
|
||||
30
open-sse/providers/registry/bai.js
Normal file
30
open-sse/providers/registry/bai.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
id: "bai",
|
||||
priority: 120,
|
||||
alias: "bai",
|
||||
aliases: [
|
||||
"b-ai",
|
||||
],
|
||||
uiAlias: "bai",
|
||||
display: {
|
||||
name: "B.AI",
|
||||
icon: "account_balance",
|
||||
color: "#0369A1",
|
||||
textIcon: "BA",
|
||||
website: "https://b.ai",
|
||||
notice: {
|
||||
text: "OpenAI-compatible gateway with one of the larger catalogues here. Accepts a bearer token or an x-api-key header. Model ids are fetched live from the provider.",
|
||||
apiKeyUrl: "https://b.ai",
|
||||
},
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
transport: {
|
||||
baseUrl: "https://api.b.ai/v1/chat/completions",
|
||||
validateUrl: "https://api.b.ai/v1/models",
|
||||
},
|
||||
// No ids hardcoded: the catalogue is large and rotates, so the live endpoint
|
||||
// is the source of truth and any id is accepted via passthroughModels.
|
||||
modelsFetcher: { url: "https://api.b.ai/v1/models", type: "openai" },
|
||||
passthroughModels: true,
|
||||
};
|
||||
35
open-sse/providers/registry/dahl.js
Normal file
35
open-sse/providers/registry/dahl.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "dahl",
|
||||
priority: 120,
|
||||
alias: "dahl",
|
||||
aliases: [
|
||||
"dahl-inference",
|
||||
],
|
||||
uiAlias: "dahl",
|
||||
display: {
|
||||
name: "Dahl Inference",
|
||||
icon: "hub",
|
||||
color: "#1E40AF",
|
||||
textIcon: "DH",
|
||||
website: "https://dahl.global",
|
||||
notice: {
|
||||
text: "OpenAI-compatible Gonka inference node. Small, fixed catalogue (GLM-5.3-Flash, DeepSeek-V4-Flash, MiniMax-M2.7) at a flat per-token rate.",
|
||||
apiKeyUrl: "https://dahl.global/dashboard",
|
||||
},
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
transport: {
|
||||
baseUrl: "https://inference.dahl.global/v1/chat/completions",
|
||||
validateUrl: "https://inference.dahl.global/v1/models",
|
||||
},
|
||||
// The live catalogue is public (no auth), so modelsFetcher works without a key
|
||||
// and the ids below are a convenience seed rather than an exhaustive list.
|
||||
models: [
|
||||
{ id: "zai-org/GLM-5.3-Flash", name: "GLM-5.3 Flash" },
|
||||
{ id: "deepseek-ai/DeepSeek-V4-Flash-0731", name: "DeepSeek V4 Flash 0731" },
|
||||
{ id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7" },
|
||||
],
|
||||
modelsFetcher: { url: "https://inference.dahl.global/v1/models", type: "openai" },
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -125,6 +125,10 @@ import p119 from "./selfhosted-embedding.js";
|
||||
import p120 from "./fish-audio.js";
|
||||
import p121 from "./alitp-intl.js";
|
||||
import p122 from "./xquik.js";
|
||||
import p126 from "./dahl.js";
|
||||
import p127 from "./atria.js";
|
||||
import p129 from "./agnes.js";
|
||||
import p130 from "./bai.js";
|
||||
export default [
|
||||
p0,
|
||||
p1,
|
||||
@@ -250,4 +254,8 @@ export default [
|
||||
p120,
|
||||
p121,
|
||||
p122,
|
||||
p126,
|
||||
p127,
|
||||
p129,
|
||||
p130,
|
||||
];
|
||||
|
||||
BIN
public/providers/agnes.png
Normal file
BIN
public/providers/agnes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/providers/atria.png
Normal file
BIN
public/providers/atria.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/providers/bai.png
Normal file
BIN
public/providers/bai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/providers/dahl.png
Normal file
BIN
public/providers/dahl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -302,6 +302,13 @@ const PROVIDER_MODELS_CONFIG = {
|
||||
nvidia: createOpenAIModelsConfig("https://integrate.api.nvidia.com/v1/models"),
|
||||
assemblyai: createOpenAIModelsConfig("https://api.assemblyai.com/v1/models"),
|
||||
"vercel-ai-gateway": createOpenAIModelsConfig("https://ai-gateway.vercel.sh/v1/models"),
|
||||
// OpenAI-compatible aggregators. All standard Bearer + OpenAI shape, so they
|
||||
// reuse createOpenAIModelsConfig; Kira and Dahl serve their catalogue
|
||||
// publicly, the rest need the connection API key.
|
||||
dahl: createOpenAIModelsConfig("https://inference.dahl.global/v1/models"),
|
||||
atria: createOpenAIModelsConfig("https://api.atria-asi.ai/v1/models"),
|
||||
agnes: createOpenAIModelsConfig("https://apihub.agnes-ai.com/v1/models"),
|
||||
bai: createOpenAIModelsConfig("https://api.b.ai/v1/models"),
|
||||
kimchi: {
|
||||
customResolver: async (connection) => {
|
||||
const result = await resolveKimchiModels({
|
||||
|
||||
@@ -696,6 +696,16 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
|
||||
const res = await fetchWithConnectionProxy("https://api.hyperbolic.xyz/v1/models", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy);
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
// OpenAI-compatible aggregators. All standard Bearer + OpenAI shape, so the
|
||||
// same /v1/models probe applies to each.
|
||||
case "dahl":
|
||||
case "atria":
|
||||
case "agnes":
|
||||
case "bai": {
|
||||
const cfg = PROVIDERS[connection.provider];
|
||||
const res = await fetchWithConnectionProxy(cfg.validateUrl, { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy);
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
}
|
||||
case "ollama": {
|
||||
const res = await fetch("https://ollama.com/api/tags", { headers: { Authorization: `Bearer ${connection.apiKey}` } });
|
||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"morphllm": "morph"
|
||||
},
|
||||
"idToAlias": {
|
||||
"agnes": "agnes",
|
||||
"alicode": "alicode",
|
||||
"alicode-intl": "alicode-intl",
|
||||
"alims-intl": "alims-intl",
|
||||
@@ -127,7 +128,9 @@
|
||||
"antigravity": "ag",
|
||||
"api-airforce": "af",
|
||||
"assemblyai": "assemblyai",
|
||||
"atria": "atria",
|
||||
"azure": "azure",
|
||||
"bai": "bai",
|
||||
"baidu": "qianfan",
|
||||
"bazaarlink": "bzl",
|
||||
"blackbox": "blackbox",
|
||||
@@ -145,6 +148,7 @@
|
||||
"cohere": "cohere",
|
||||
"commandcode": "commandcode",
|
||||
"cursor": "cu",
|
||||
"dahl": "dahl",
|
||||
"deepgram": "deepgram",
|
||||
"deepseek": "deepseek",
|
||||
"featherless": "featherless",
|
||||
@@ -212,6 +216,7 @@
|
||||
"alitp-intl",
|
||||
"anthropic",
|
||||
"assemblyai",
|
||||
"atria",
|
||||
"black-forest-labs",
|
||||
"blackbox",
|
||||
"bm",
|
||||
@@ -229,6 +234,7 @@
|
||||
"commandcode",
|
||||
"cu",
|
||||
"cx",
|
||||
"dahl",
|
||||
"deepgram",
|
||||
"deepseek",
|
||||
"edge-tts",
|
||||
|
||||
@@ -1085,5 +1085,25 @@
|
||||
"preserveCacheControl": true
|
||||
},
|
||||
"format": "openai"
|
||||
},
|
||||
"dahl": {
|
||||
"baseUrl": "https://inference.dahl.global/v1/chat/completions",
|
||||
"validateUrl": "https://inference.dahl.global/v1/models",
|
||||
"format": "openai"
|
||||
},
|
||||
"atria": {
|
||||
"baseUrl": "https://api.atria-asi.ai/v1/chat/completions",
|
||||
"validateUrl": "https://api.atria-asi.ai/v1/models",
|
||||
"format": "openai"
|
||||
},
|
||||
"agnes": {
|
||||
"baseUrl": "https://apihub.agnes-ai.com/v1/chat/completions",
|
||||
"validateUrl": "https://apihub.agnes-ai.com/v1/models",
|
||||
"format": "openai"
|
||||
},
|
||||
"bai": {
|
||||
"baseUrl": "https://api.b.ai/v1/chat/completions",
|
||||
"validateUrl": "https://api.b.ai/v1/models",
|
||||
"format": "openai"
|
||||
}
|
||||
}
|
||||
137
tests/unit/aggregator-providers-batch.test.js
Normal file
137
tests/unit/aggregator-providers-batch.test.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import REGISTRY from "../../open-sse/providers/registry/index.js";
|
||||
import { PROVIDERS } from "../../open-sse/providers/index.js";
|
||||
import { getExecutor } from "../../open-sse/executors/index.js";
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.js";
|
||||
|
||||
/**
|
||||
* OpenAI-compatible aggregator providers. Each was verified by probing the
|
||||
* live /v1/models endpoint: a 401 with a structured error body confirms a
|
||||
* real API behind the host, and Dahl/Kira answer 200 with no credentials.
|
||||
*/
|
||||
const BATCH = [
|
||||
{
|
||||
id: "dahl",
|
||||
category: "apikey",
|
||||
baseUrl: "https://inference.dahl.global/v1/chat/completions",
|
||||
modelsUrl: "https://inference.dahl.global/v1/models",
|
||||
aliases: ["dahl-inference"],
|
||||
},
|
||||
{
|
||||
id: "atria",
|
||||
category: "apikey",
|
||||
baseUrl: "https://api.atria-asi.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.atria-asi.ai/v1/models",
|
||||
aliases: ["atria-asi"],
|
||||
},
|
||||
{
|
||||
id: "agnes",
|
||||
category: "freeTier",
|
||||
baseUrl: "https://apihub.agnes-ai.com/v1/chat/completions",
|
||||
modelsUrl: "https://apihub.agnes-ai.com/v1/models",
|
||||
aliases: ["agnes-ai"],
|
||||
},
|
||||
{
|
||||
id: "bai",
|
||||
category: "apikey",
|
||||
baseUrl: "https://api.b.ai/v1/chat/completions",
|
||||
modelsUrl: "https://api.b.ai/v1/models",
|
||||
aliases: ["b-ai"],
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(BATCH)("$id provider", (p) => {
|
||||
const entry = REGISTRY.find((e) => e.id === p.id);
|
||||
|
||||
it("is registered with the expected category and base URL", () => {
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.category).toBe(p.category);
|
||||
expect(PROVIDERS[p.id].baseUrl).toBe(p.baseUrl);
|
||||
expect(PROVIDERS[p.id].format).toBe("openai");
|
||||
});
|
||||
|
||||
it("exposes its aliases and a UI display name", () => {
|
||||
for (const a of p.aliases) expect(entry.aliases).toContain(a);
|
||||
expect(entry.display?.name).toBeTruthy();
|
||||
expect(entry.display?.textIcon).toBeTruthy();
|
||||
});
|
||||
|
||||
it("routes through the shared DefaultExecutor", () => {
|
||||
expect(getExecutor(p.id)).toBeInstanceOf(DefaultExecutor);
|
||||
});
|
||||
|
||||
it("accepts arbitrary model ids via passthrough", () => {
|
||||
expect(entry.passthroughModels).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Atria Dawn specifics", () => {
|
||||
const entry = REGISTRY.find((e) => e.id === "atria");
|
||||
|
||||
it("is named after the service, not just the host", () => {
|
||||
expect(entry.display.name).toBe("Atria Dawn");
|
||||
});
|
||||
|
||||
it("pins the single documented preview model", () => {
|
||||
expect(entry.models.map((m) => m.id)).toEqual(["Atria-Dawn-Preview"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("provider icons", () => {
|
||||
// This file lives in tests/unit/, so resolve icons against the repo root.
|
||||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
it("ships a /public/providers/{id}.png for every provider in the batch", () => {
|
||||
// getProviderIconSrc() resolves /providers/{id}.png and falls back to the
|
||||
// textIcon tile when the file 404s, so a missing icon is silent in the UI.
|
||||
for (const p of BATCH.map((x) => x.id)) {
|
||||
expect(existsSync(join(REPO_ROOT, "public", "providers", `${p}.png`))).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticated model discovery", () => {
|
||||
const ROUTE = join(
|
||||
dirname(fileURLToPath(import.meta.url)), "..", "..",
|
||||
"src", "app", "api", "providers", "[id]", "models", "route.js"
|
||||
);
|
||||
const source = readFileSync(ROUTE, "utf8");
|
||||
|
||||
it("registers every batch provider in the /models resolver", () => {
|
||||
// Without an entry here the route answers
|
||||
// 400 "Provider X does not support models listing" and live discovery
|
||||
// silently fails once a key is saved.
|
||||
for (const p of BATCH.map((x) => x.id)) {
|
||||
expect(source).toContain(`${p}: createOpenAIModelsConfig(`);
|
||||
}
|
||||
});
|
||||
|
||||
it("points each entry at that provider's own /models URL", () => {
|
||||
for (const p of BATCH) {
|
||||
const line = source.split("\n").find((l) => l.trim().startsWith(`${p.id}: createOpenAIModelsConfig(`));
|
||||
expect(line, `no models entry for ${p.id}`).toBeTruthy();
|
||||
expect(line).toContain(p.modelsUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("batch invariants", () => {
|
||||
it("keeps every registry id unique", () => {
|
||||
const ids = REGISTRY.map((e) => e.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it("does not claim vision for any provider in the batch", () => {
|
||||
// These aggregators relay third-party models; none of them has documented
|
||||
// image input, so no registry entry may assert it.
|
||||
for (const p of BATCH) {
|
||||
const entry = REGISTRY.find((e) => e.id === p.id);
|
||||
expect(entry.serviceKinds ?? ["llm"]).toContain("llm");
|
||||
expect(entry.imageToTextConfig).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user