Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ _In development — bullets added per PR; finalized at release._

### 🔧 Bug Fixes

- **feat(providers):** add DGrid AI gateway provider — OpenAI-compatible gateway at `api.dgrid.ai/v1` (alias `dgrid`, API-key auth, passthrough models). Free router tier (10 RPM / 100 RPD); a $5 lifetime top-up raises limits to 20 RPM / 1,000 RPD. (thanks @dgridOP)

- **feat(providers):** add Pioneer AI (Fastino Labs) provider — OpenAI-compatible chat completions at `api.pioneer.ai/v1`. Registered with alias `pn`, `X-API-Key` auth, and a catalog of 10 open-tier serverless models (Qwen3, Llama 3.1/3.2, Gemma 3, SmolLM3). Free $75 credits, no credit card required. Gated enterprise models (Claude/GPT/Gemini) require prior fine-tuning on the Pioneer platform and are intentionally excluded from the catalog. (thanks @HikiNarou)

- **feat(sse): auto-promote successful combo model**: a new opt-in `comboAutoPromoteEnabled` setting reorders a combo's persisted model list so that, when a combo model responds successfully, it is moved to position #1 for future requests. (thanks @arssnndr)
Expand Down
2 changes: 2 additions & 0 deletions open-sse/config/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { baichuanProvider } from "./registry/baichuan/index.ts";
import { yiProvider } from "./registry/yi/index.ts";
import { deepseekProvider } from "./registry/deepseek/index.ts";
import { deepseek_webProvider } from "./registry/deepseek/web/index.ts";
import { dgridProvider } from "./registry/dgrid/index.ts";
import { kimi_coding_apikeyProvider } from "./registry/kimi/coding-apikey/index.ts";
import { kimi_codingProvider } from "./registry/kimi/coding/index.ts";
import { kimiProvider } from "./registry/kimi/index.ts";
Expand Down Expand Up @@ -208,6 +209,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
yi: yiProvider,
deepseek: deepseekProvider,
"deepseek-web": deepseek_webProvider,
dgrid: dgridProvider,
"kimi-coding-apikey": kimi_coding_apikeyProvider,
"kimi-coding": kimi_codingProvider,
kimi: kimiProvider,
Expand Down
15 changes: 15 additions & 0 deletions open-sse/config/providers/registry/dgrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { RegistryEntry } from "../../shared.ts";

export const dgridProvider: RegistryEntry = {
id: "dgrid",
alias: "dgrid",
format: "openai",
executor: "default",
baseUrl: "https://api.dgrid.ai/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
modelsUrl: "https://api.dgrid.ai/v1/models",
defaultContextLength: 128000,
models: [{ id: "dgridai/free", name: "DGrid Free Models Router" }],
passthroughModels: true,
};
3 changes: 3 additions & 0 deletions src/app/api/providers/[id]/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ const NAMED_OPENAI_STYLE_PROVIDERS = new Set([
// but was left out of the sweep, so it served a stale hardcoded seed (grok-3, grok-2-1212,
// claude-3.7-sonnet …). Live fetch keeps it fresh; seed stays as the offline fallback.
"api-airforce",
// DGrid is an OpenAI-compatible gateway whose default seed is the free auto-router;
// the full model catalog is discovered live from https://api.dgrid.ai/v1/models.
"dgrid",
]);

function isNamedOpenAIStyleProvider(provider: string): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/shared/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const API_ENDPOINTS = {
export const PROVIDER_ENDPOINTS = {
agentrouter: "https://agentrouter.org/v1/chat/completions",
openrouter: "https://openrouter.ai/api/v1/chat/completions",
dgrid: "https://api.dgrid.ai/v1/chat/completions",
glm: "https://api.z.ai/api/anthropic/v1/messages",
glmt: "https://api.z.ai/api/anthropic/v1/messages",
"bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages",
Expand Down
17 changes: 17 additions & 0 deletions src/shared/constants/providers/apikey/gateways.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ export const APIKEY_PROVIDERS_GATEWAYS = {
hasFree: true,
freeNote: "Free models at $0/token with :free suffix - 20 RPM / 200 RPD",
},
dgrid: {
id: "dgrid",
alias: "dgrid",
name: "DGrid",
icon: "router",
color: "#65A30D",
textIcon: "DG",
passthroughModels: true,
website: "https://dgrid.ai",
hasFree: true,
freeNote:
"DGrid Free Models Router: 10 requests/minute and 100 requests/day. " +
"A $5 lifetime top-up unlocks up to 20 requests/minute and 1,000 requests/day.",
apiHint:
"Create a DGrid API key at https://dgrid.ai, then use https://api.dgrid.ai/v1 " +
"as the OpenAI-compatible base URL.",
},
orcarouter: {
id: "orcarouter",
alias: "orcarouter",
Expand Down
162 changes: 162 additions & 0 deletions tests/unit/dgrid-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

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 { isValidModel } = await import("../../src/shared/constants/models.ts");

const DGRID_CHAT_URL = "https://api.dgrid.ai/v1/chat/completions";
const DGRID_MODELS_URL = "https://api.dgrid.ai/v1/models";
const DGRID_FREE_ROUTER = "dgridai/free";

test("DGrid is registered as a free API-key provider", () => {
const entry = APIKEY_PROVIDERS.dgrid;
assert.ok(entry, "APIKEY_PROVIDERS.dgrid must be defined");
assert.equal(entry.id, "dgrid");
assert.equal(entry.alias, "dgrid");
assert.equal(entry.name, "DGrid");
assert.equal(entry.website, "https://dgrid.ai");
assert.equal(entry.hasFree, true);
assert.equal(entry.passthroughModels, true);
assert.match(entry.freeNote, /10 requests\/minute/);
assert.match(entry.freeNote, /100 requests\/day/);
assert.match(entry.freeNote, /\$5 lifetime top-up/);
});

test("DGrid exposes the OpenAI-compatible chat completions endpoint", () => {
assert.equal(PROVIDER_ENDPOINTS.dgrid, DGRID_CHAT_URL);
});

test("DGrid registry entry uses OpenAI format with bearer API-key auth", () => {
const entry = providerRegistry.dgrid;
assert.ok(entry, "providerRegistry.dgrid must be defined");
assert.equal(entry.id, "dgrid");
assert.equal(entry.alias, "dgrid");
assert.equal(entry.format, "openai");
assert.equal(entry.executor, "default");
assert.equal(entry.authType, "apikey");
assert.equal(entry.authHeader, "bearer");
assert.equal(entry.baseUrl, DGRID_CHAT_URL);
assert.equal(entry.modelsUrl, DGRID_MODELS_URL);
assert.equal(entry.passthroughModels, true);
});

test("DGrid ships the Free Models Router as the seeded default model", () => {
const models = providerRegistry.dgrid.models;
const ids = models.map((model: { id: string }) => model.id);
assert.ok(ids.includes(DGRID_FREE_ROUTER), "seed list must include dgridai/free");
assert.equal(new Set(ids).size, ids.length, "model ids must be unique");
assert.equal(
models.find((model: { id: string }) => model.id === DGRID_FREE_ROUTER)?.name,
"DGrid Free Models Router"
);
});

test("DGrid accepts the free router and the 200+ live catalog via passthrough models", () => {
assert.equal(isValidModel("dgrid", DGRID_FREE_ROUTER), true);
assert.equal(isValidModel("dgrid", "openai/gpt-4o"), true);
assert.equal(isValidModel("dgrid", "anthropic/claude-sonnet-4"), true);
});

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-dgrid-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");

async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}

test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});

interface ModelsBody {
provider: string;
connectionId: string;
models: Array<{ id: string }>;
source?: string;
}

test("DGrid import fetches the live /v1/models catalog", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "dgrid",
authType: "apikey",
name: "dgrid-live",
apiKey: "dgrid-key",
});

let fetched = false;
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (String(url) === DGRID_MODELS_URL) {
fetched = true;
return Response.json({
object: "list",
data: [
{ id: DGRID_FREE_ROUTER },
{ id: "openai/gpt-4o" },
{ id: "anthropic/claude-sonnet-4" },
],
});
}
return new Response("not found", { status: 404 });
};

try {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 200);
const body = (await response.json()) as ModelsBody;
assert.equal(body.provider, "dgrid");
assert.equal(body.source, "api", "should serve the live upstream catalog");
assert.ok(fetched, `should have probed ${DGRID_MODELS_URL}`);
const ids = body.models.map((model) => model.id);
assert.ok(ids.includes(DGRID_FREE_ROUTER), `free router missing: ${ids.join(",")}`);
assert.ok(ids.includes("openai/gpt-4o"), `live catalog model missing: ${ids.join(",")}`);
} finally {
globalThis.fetch = originalFetch;
}
});

test("DGrid import falls back to the seeded free router when live fetch fails", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "dgrid",
authType: "apikey",
name: "dgrid-fallback",
apiKey: "dgrid-key-2",
});

const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("bad gateway", { status: 502 });

try {
const response = await modelsRoute.GET(
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
{ params: { id: connection.id } }
);
assert.equal(response.status, 200);
const body = (await response.json()) as ModelsBody;
assert.equal(body.provider, "dgrid");
assert.equal(body.source, "local_catalog", "import must not break when upstream is down");
assert.deepEqual(
body.models.map((model) => model.id),
[DGRID_FREE_ROUTER]
);
} finally {
globalThis.fetch = originalFetch;
}
});