feat(providers): add DigitalOcean AI as an OpenAI-compatible provider (#6373)

add DigitalOcean AI OpenAI-compatible provider (port PR #2417). Resolved registry conflict with hcnsec #6410; count-guard 169→170. Tests green. Integrated into release/v3.8.46.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 19:27:51 -03:00
committed by GitHub
parent 437ca488b0
commit c6a80071f1
7 changed files with 102 additions and 6 deletions

View File

@@ -8,6 +8,7 @@
### ✨ New Features
- **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(providers):** add **Huancheng Public API** (`hcnsec`) as an OpenAI-compatible regional provider — Xinjiang Huancheng Cybersecurity's public LLM platform (base `https://api.hcnsec.cn/v1`, free credits via daily check-ins), wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/hcnsec/`, `src/shared/constants/providers/apikey/regional.ts`). Regression guard: `tests/unit/hcnsec-provider.test.ts`. (thanks @UnrealAryan)
- **feat(dashboard):** the web-session credential guide now shows an **"Open {host}" link** to the provider's sign-in site (derived from the provider `website` via `getProviderWebsiteHost`), so you can jump straight to the page where the cookie/session must be captured. Regression guard: `tests/unit/web-session-provider-link-6316.test.ts`. (thanks @jordansilly77-stack)
- **feat(cerebras):** add the **Gemma 4 31B** model (`gemma-4-31b`) to the Cerebras registry + pricing table. Regression guard extends `tests/unit/t28-model-catalog-updates.test.ts`. (thanks @backryun)

View File

@@ -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";
import { hcnsecProvider } from "./registry/hcnsec/index.ts";
export const REGISTRY: Record<string, RegistryEntry> = {
@@ -369,5 +370,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
x5lab: x5labProvider,
kenari: kenariProvider,
requesty: requestyProvider,
digitalocean: digitaloceanProvider,
hcnsec: hcnsecProvider,
};

View 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,
});

View File

@@ -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"],
},
};

View File

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

View 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);
});

View File

@@ -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 (169 APIKEY entries, no loss/dup), and that
// + helpers still exported), the spread-merge integrity (170 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 169 entries (no loss / no dup)", async () => {
test("APIKEY_PROVIDERS merges the 6 family files into 170 entries (no loss / no dup)", async () => {
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
assert.equal(keys.length, 169);
assert.equal(new Set(keys).size, 169, "duplicate keys after spread-merge");
assert.equal(keys.length, 170);
assert.equal(new Set(keys).size, 170, "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 169.
// strict partition (every provider in exactly one), so the sum must be exactly 170.
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 169 entries (no loss / no
seen.add(k);
}
}
assert.equal(famTotal, 169, "families must partition all 169 providers");
assert.equal(famTotal, 170, "families must partition all 170 providers");
});
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {