mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(providers): add ModelScope OpenAI-compatible provider (#5965)
* feat(providers): add ModelScope OpenAI-compatible provider Ports ModelScope (Alibaba 魔搭) as a new API-key, OpenAI-compatible provider — upstream 9router PR #1764. The upstream PR hardcoded `https://api-inference.modelscope.ai/...` (`.ai` TLD); verified against ModelScope's own API-Inference docs and third-party integration guides that the real production domain is `api-inference.modelscope.cn` (`.cn` TLD) and shipped that instead. Also drops the PR's static 5-model snapshot in favor of `passthroughModels: true` with an empty seed list + `modelsUrl`, since ModelScope's open-model catalog moves fast. Updates the providers-constants-split characterization test's hardcoded APIKEY_PROVIDERS count (159 -> 160) to match the new entry. Co-authored-by: Umar Javed <114807145+tn5052@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/1764 * chore(changelog): restore release entries + add modelscope bullet * test(golden): regenerate translate-path for modelscope provider --------- Co-authored-by: Umar Javed <114807145+tn5052@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
196375b8a9
commit
7aaf109c0c
@@ -20,6 +20,7 @@
|
||||
- **feat(oauth):** import a ChatGPT/Codex connection from a raw access token (no refresh token required). (thanks @ryanngit)
|
||||
- **feat(providers):** add NVIDIA NIM image generation (FLUX models). (thanks @eng2007)
|
||||
- **feat(providers):** add Augment (Auggie CLI) as a local no-auth provider. (thanks @chamdanilukman)
|
||||
- **feat(providers):** add ModelScope as an OpenAI-compatible (API-key) provider. (thanks @tn5052)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ import { leonardoProvider } from "./registry/leonardo/index.ts";
|
||||
import { grok_webProvider } from "./registry/grok-web/index.ts";
|
||||
import { kieProvider } from "./registry/kie/index.ts";
|
||||
import { monsterapiProvider } from "./registry/monsterapi/index.ts";
|
||||
import { modelscopeProvider } from "./registry/modelscope/index.ts";
|
||||
import { sensenovaProvider } from "./registry/sensenova/index.ts";
|
||||
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
|
||||
import { lambda_aiProvider } from "./registry/lambda-ai/index.ts";
|
||||
@@ -252,6 +253,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"grok-web": grok_webProvider,
|
||||
kie: kieProvider,
|
||||
monsterapi: monsterapiProvider,
|
||||
modelscope: modelscopeProvider,
|
||||
sensenova: sensenovaProvider,
|
||||
hyperbolic: hyperbolicProvider,
|
||||
"lambda-ai": lambda_aiProvider,
|
||||
|
||||
24
open-sse/config/providers/registry/modelscope/index.ts
Normal file
24
open-sse/config/providers/registry/modelscope/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// ModelScope (Alibaba 魔搭) — OpenAI-compatible API-Inference, ported from upstream
|
||||
// 9router PR #1764 (@tn5052). The upstream PR hardcoded `https://api-inference.modelscope.ai/...`
|
||||
// (`.ai` TLD) and a static 5-model list. Both were dropped here after verification:
|
||||
//
|
||||
// - baseUrl: ModelScope's own API-Inference docs (modelscope.cn/docs/model-service/API-Inference)
|
||||
// and third-party integration guides (e.g. Alibaba Cloud Model Studio compatibility docs)
|
||||
// consistently confirm the production domain is `api-inference.modelscope.cn` — the `.cn` TLD,
|
||||
// not `.ai`. Using the unverified `.ai` domain would have shipped a broken provider.
|
||||
// - models: passthrough + empty static seed instead of copying the PR's 5-model snapshot, since
|
||||
// ModelScope hosts a large and fast-moving open-model catalog — `modelsUrl` keeps the list live.
|
||||
export const modelscopeProvider: RegistryEntry = {
|
||||
id: "modelscope",
|
||||
alias: "ms",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
modelsUrl: "https://api-inference.modelscope.cn/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
passthroughModels: true,
|
||||
models: [],
|
||||
};
|
||||
@@ -265,6 +265,21 @@ export const APIKEY_PROVIDERS_INFERENCE = {
|
||||
passthroughModels: true,
|
||||
authHint: "Get API key at monsterapi.ai",
|
||||
},
|
||||
modelscope: {
|
||||
id: "modelscope",
|
||||
alias: "ms",
|
||||
name: "ModelScope",
|
||||
icon: "cloud",
|
||||
color: "#FF6A00",
|
||||
textIcon: "MS",
|
||||
website: "https://modelscope.cn",
|
||||
hasFree: true,
|
||||
// #1764 (upstream 9router): OpenAI-compatible API-Inference. Base URL verified
|
||||
// live against ModelScope's own docs — the upstream PR used the `.ai` TLD, but
|
||||
// the confirmed production domain is `api-inference.modelscope.cn` (see registry
|
||||
// entry + test guard).
|
||||
freeNote: "Free tier via ModelScope API-Inference — Alibaba account required.",
|
||||
},
|
||||
byteplus: {
|
||||
id: "byteplus",
|
||||
alias: "bpm",
|
||||
|
||||
@@ -2709,6 +2709,29 @@
|
||||
"stream": "https://api.modal.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"modelscope": {
|
||||
"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://api-inference.modelscope.cn/v1/chat/completions",
|
||||
"stream": "https://api-inference.modelscope.cn/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"monsterapi": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
65
tests/unit/modelscope-provider.test.ts
Normal file
65
tests/unit/modelscope-provider.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { modelscopeProvider } from "../../open-sse/config/providers/registry/modelscope/index.ts";
|
||||
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
|
||||
import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers/apikey/index.ts";
|
||||
import { AI_PROVIDERS, getProviderById } from "../../src/shared/constants/providers.ts";
|
||||
|
||||
// Upstream 9router PR #1764 (@tn5052) ports ModelScope (Alibaba 魔搭) as an OpenAI-compatible
|
||||
// BYOK free-tier provider. The upstream PR hardcoded `https://api-inference.modelscope.ai/...`
|
||||
// (`.ai` TLD) — ModelScope's own docs confirm the real production domain is
|
||||
// `api-inference.modelscope.cn` (`.cn` TLD). This suite locks in the verified domain and
|
||||
// guards against a regression back to the unverified `.ai` domain.
|
||||
|
||||
test("providers-shape: modelscope is registered in APIKEY_PROVIDERS metadata", () => {
|
||||
assert.ok("modelscope" in APIKEY_PROVIDERS, "modelscope missing from APIKEY_PROVIDERS");
|
||||
const meta = (APIKEY_PROVIDERS as Record<string, Record<string, unknown>>).modelscope;
|
||||
assert.equal(meta.id, "modelscope");
|
||||
assert.equal(meta.alias, "ms");
|
||||
assert.equal(meta.name, "ModelScope");
|
||||
assert.equal(meta.website, "https://modelscope.cn");
|
||||
assert.equal(meta.hasFree, true);
|
||||
});
|
||||
|
||||
test("providers-shape: modelscope is a canonical AI_PROVIDERS entry resolvable by id", () => {
|
||||
assert.ok("modelscope" in AI_PROVIDERS, "modelscope missing from AI_PROVIDERS");
|
||||
const provider = getProviderById("modelscope");
|
||||
assert.ok(provider, "getProviderById('modelscope') returned nothing");
|
||||
assert.equal(provider?.id, "modelscope");
|
||||
});
|
||||
|
||||
test("registry resolution: modelscope resolves in the provider REGISTRY as OpenAI-compatible", () => {
|
||||
assert.ok("modelscope" in REGISTRY, "modelscope missing from REGISTRY");
|
||||
const entry = REGISTRY.modelscope;
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry, modelscopeProvider);
|
||||
});
|
||||
|
||||
test("registry resolution: modelscope uses passthrough models with an empty static seed list", () => {
|
||||
assert.equal(modelscopeProvider.passthroughModels, true);
|
||||
assert.deepEqual(modelscopeProvider.models, []);
|
||||
assert.equal(
|
||||
modelscopeProvider.modelsUrl,
|
||||
"https://api-inference.modelscope.cn/v1/models",
|
||||
"modelsUrl must point at the verified .cn domain"
|
||||
);
|
||||
});
|
||||
|
||||
test("baseUrl guard: modelscope targets the verified api-inference.modelscope.cn domain (not .ai)", () => {
|
||||
assert.equal(
|
||||
modelscopeProvider.baseUrl,
|
||||
"https://api-inference.modelscope.cn/v1/chat/completions"
|
||||
);
|
||||
assert.ok(
|
||||
modelscopeProvider.baseUrl.includes("api-inference.modelscope.cn"),
|
||||
`baseUrl must use the verified .cn domain, got: ${modelscopeProvider.baseUrl}`
|
||||
);
|
||||
assert.ok(
|
||||
!modelscopeProvider.baseUrl.includes("modelscope.ai"),
|
||||
"baseUrl must not regress to the upstream PR's unverified .ai domain"
|
||||
);
|
||||
});
|
||||
@@ -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 (159 APIKEY entries, no loss/dup), and that
|
||||
// + helpers still exported), the spread-merge integrity (160 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 159 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 160 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 159);
|
||||
assert.equal(new Set(keys).size, 159, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 160);
|
||||
assert.equal(new Set(keys).size, 160, "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 159.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 160.
|
||||
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 159 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 159, "families must partition all 159 providers");
|
||||
assert.equal(famTotal, 160, "families must partition all 160 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
Reference in New Issue
Block a user