mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
feat(providers): add DigitalOcean AI as an OpenAI-compatible provider
DigitalOcean AI serverless inference (base https://inference.do-ai.run/v1) registered as an API-key OpenAI-compatible provider with full model passthrough. Wired through the shared OpenAI-compatible registry (inference-hosts family). No new deps, no schema change. Co-authored-by: newnol <tantai13102005@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/2417
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe)
|
||||
- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma)
|
||||
- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127)
|
||||
- **feat(providers):** add **DigitalOcean** AI (serverless inference) as an OpenAI-compatible API-key provider — base `https://inference.do-ai.run/v1`, wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/digitalocean/`, `src/shared/constants/providers/apikey/inference-hosts.ts`). Regression guard: `tests/unit/digitalocean-provider.test.ts`. (thanks @newnol)
|
||||
- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -183,6 +183,7 @@ import { sumopodProvider } from "./registry/sumopod/index.ts";
|
||||
import { x5labProvider } from "./registry/x5lab/index.ts";
|
||||
import { kenariProvider } from "./registry/kenari/index.ts";
|
||||
import { requestyProvider } from "./registry/requesty/index.ts";
|
||||
import { digitaloceanProvider } from "./registry/digitalocean/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -368,4 +369,5 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
x5lab: x5labProvider,
|
||||
kenari: kenariProvider,
|
||||
requesty: requestyProvider,
|
||||
digitalocean: digitaloceanProvider,
|
||||
};
|
||||
|
||||
11
open-sse/config/providers/registry/digitalocean/index.ts
Normal file
11
open-sse/config/providers/registry/digitalocean/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const digitaloceanProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "digitalocean",
|
||||
alias: "digitalocean",
|
||||
baseUrl: "https://inference.do-ai.run/v1/chat/completions",
|
||||
modelsUrl: "https://inference.do-ai.run/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
});
|
||||
@@ -309,4 +309,20 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
},
|
||||
serviceKinds: ["llm"],
|
||||
},
|
||||
digitalocean: {
|
||||
id: "digitalocean",
|
||||
alias: "digitalocean",
|
||||
name: "DigitalOcean",
|
||||
icon: "cloud",
|
||||
color: "#0060FF",
|
||||
textIcon: "DO",
|
||||
passthroughModels: true,
|
||||
website: "https://docs.digitalocean.com/products/ai-platform/",
|
||||
hasFree: false,
|
||||
notice: {
|
||||
text: "Use a DigitalOcean Personal Access Token (dop_v1_...) or a Model Access Key from the Inference console. OAuth tokens (doo_v1_...) may not have the required scopes.",
|
||||
apiKeyUrl: "https://cloud.digitalocean.com/account/api/tokens",
|
||||
},
|
||||
serviceKinds: ["llm"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1324,6 +1324,29 @@
|
||||
"stream": "https://api.dify.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"digitalocean": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://inference.do-ai.run/v1/chat/completions",
|
||||
"stream": "https://inference.do-ai.run/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"dit": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
43
tests/unit/digitalocean-provider.test.ts
Normal file
43
tests/unit/digitalocean-provider.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
|
||||
const DO_CHAT_URL = "https://inference.do-ai.run/v1/chat/completions";
|
||||
const DO_MODELS_URL = "https://inference.do-ai.run/v1/models";
|
||||
|
||||
test("digitalocean is registered as an API-key inference-host provider", () => {
|
||||
const entry = APIKEY_PROVIDERS.digitalocean;
|
||||
assert.ok(entry, "APIKEY_PROVIDERS.digitalocean must be defined");
|
||||
assert.equal(entry.id, "digitalocean");
|
||||
assert.equal(entry.alias, "digitalocean");
|
||||
assert.equal(entry.name, "DigitalOcean");
|
||||
assert.equal(entry.website, "https://docs.digitalocean.com/products/ai-platform/");
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("digitalocean registry entry uses OpenAI format with bearer API-key auth", () => {
|
||||
const entry = providerRegistry.digitalocean;
|
||||
assert.ok(entry, "providerRegistry.digitalocean must be defined");
|
||||
assert.equal(entry.id, "digitalocean");
|
||||
assert.equal(entry.alias, "digitalocean");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, DO_CHAT_URL);
|
||||
assert.equal(entry.modelsUrl, DO_MODELS_URL);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("digitalocean ships no static model seed — relies fully on passthrough + live catalog", () => {
|
||||
assert.deepEqual(providerRegistry.digitalocean.models, []);
|
||||
});
|
||||
|
||||
test("digitalocean accepts any model id via passthrough (serverless inference catalog)", () => {
|
||||
assert.equal(isValidModel("digitalocean", "openai-gpt-oss-120b"), true);
|
||||
assert.equal(isValidModel("digitalocean", "llama3.3-70b-instruct"), true);
|
||||
assert.equal(isValidModel("digitalocean", "anthropic-claude-3.5-sonnet"), true);
|
||||
});
|
||||
@@ -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/<family>.ts). Locks: the public surface (every catalog
|
||||
// + helpers still exported), the spread-merge integrity (168 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (169 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 168 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 169 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 168);
|
||||
assert.equal(new Set(keys).size, 168, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 169);
|
||||
assert.equal(new Set(keys).size, 169, "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 168.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 169.
|
||||
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 168 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 168, "families must partition all 168 providers");
|
||||
assert.equal(famTotal, 169, "families must partition all 169 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
Reference in New Issue
Block a user