From 9eac8c2efe90f9c2c71676b00a3451fa7503fd74 Mon Sep 17 00:00:00 2001 From: jimmyzhuu Date: Sat, 25 Apr 2026 18:47:42 +0800 Subject: [PATCH] feat(provider): add Baidu Qianfan chat provider (#1582) Integrated into release/v3.7.0 --- README.md | 10 +++ docs/ENVIRONMENT.md | 1 + docs/USER_GUIDE.md | 10 +++ open-sse/config/providerRegistry.ts | 21 ++++++ public/providers/qianfan.svg | 5 ++ src/shared/components/ProviderIcon.tsx | 1 + src/shared/constants/providers.ts | 12 ++++ .../unit/chat-openai-compat-providers.test.ts | 1 + tests/unit/qianfan-provider.test.ts | 69 +++++++++++++++++++ 9 files changed, 130 insertions(+) create mode 100644 public/providers/qianfan.svg create mode 100644 tests/unit/qianfan-provider.test.ts diff --git a/README.md b/README.md index 4fa135fb9a..cd4b658283 100644 --- a/README.md +++ b/README.md @@ -1770,6 +1770,16 @@ Models: **Dashboard behavior:** OpenRouter models are managed from **Available Models**. Manual add, import, and auto-sync all update the same list. +### Baidu Qianfan / ERNIE + +1. Sign up: [Baidu AI Cloud Qianfan](https://cloud.baidu.com/product/wenxinworkshop) +2. Create a Qianfan API key +3. Dashboard → Add Provider → Baidu Qianfan + +**Use:** `qianfan/ernie-4.5-turbo-128k`, `qianfan/ernie-x1-turbo-32k`, or any Qianfan OpenAI-compatible model ID. + +**Dashboard behavior:** Qianfan is registered as an OpenAI-compatible API key provider. Built-in ERNIE models are available immediately, and passthrough model IDs are accepted for newer Qianfan deployments. +
diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 9a9150f339..c6e20dfd73 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -404,6 +404,7 @@ Recognized pattern: `{PROVIDER_ID}_API_KEY` | `COHERE_API_KEY` | Cohere | | `NVIDIA_API_KEY` | NVIDIA NIM | | `NEBIUS_API_KEY` | Nebius (embeddings) | +| `QIANFAN_API_KEY` | Baidu Qianfan | > [!TIP] > Keys set via the Dashboard are stored encrypted in SQLite and take precedence over environment variables. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index b1f180b531..4b076e1d00 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -36,6 +36,7 @@ Complete guide for configuring providers, creating combos, integrating CLI tools | | Cerebras | Pay per use | None | Wafer-scale speed | | | Cohere | Pay per use | None | Command R+ RAG | | | NVIDIA NIM | Pay per use | None | Enterprise models | +| | Baidu Qianfan | Pay per use | None | ERNIE models | | **💰 CHEAP** | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | | | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | | | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | @@ -191,6 +192,13 @@ Models: **Use:** `kimi/kimi-latest` — **Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost! +#### Baidu Qianfan / ERNIE + +1. Sign up: [Baidu AI Cloud Qianfan](https://cloud.baidu.com/product/wenxinworkshop) +2. Create a Qianfan API key → Dashboard → Add API Key: Provider: `qianfan` + +**Use:** `qianfan/ernie-4.5-turbo-128k`, `qianfan/ernie-x1-turbo-32k`, or another Qianfan OpenAI-compatible model ID. + ### 🆓 FREE Providers #### Qoder (8 FREE models) @@ -590,6 +598,8 @@ For the full environment variable reference, see the [README](../README.md). **NVIDIA NIM (`nvidia/`)**: `nvidia/nvidia/llama-3.3-70b-instruct` +**Baidu Qianfan (`qianfan/`)**: `qianfan/ernie-4.5-turbo-128k`, `qianfan/ernie-x1-turbo-32k` +
--- diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 657afa57e9..ab960e7790 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -864,6 +864,27 @@ export const REGISTRY: Record = { models: [{ id: "auto", name: "Auto (Best Available)" }], }, + qianfan: { + id: "qianfan", + alias: "qianfan", + format: "openai", + executor: "default", + baseUrl: "https://qianfan.baidubce.com/v2/chat/completions", + modelsUrl: "https://qianfan.baidubce.com/v2/models", + authType: "apikey", + authHeader: "bearer", + defaultContextLength: 128000, + models: [ + { id: "ernie-4.5-turbo-128k", name: "ERNIE 4.5 Turbo 128K" }, + { id: "ernie-4.5-turbo-32k", name: "ERNIE 4.5 Turbo 32K", contextLength: 32000 }, + { id: "ernie-4.5-turbo-8k", name: "ERNIE 4.5 Turbo 8K", contextLength: 8000 }, + { id: "ernie-x1-turbo-32k", name: "ERNIE X1 Turbo 32K", contextLength: 32000 }, + { id: "ernie-x1-turbo-8k", name: "ERNIE X1 Turbo 8K", contextLength: 8000 }, + { id: "ernie-4.5-8k-preview", name: "ERNIE 4.5 8K Preview", contextLength: 8000 }, + ], + passthroughModels: true, + }, + glm: { id: "glm", alias: "glm", diff --git a/public/providers/qianfan.svg b/public/providers/qianfan.svg new file mode 100644 index 0000000000..19ade38113 --- /dev/null +++ b/public/providers/qianfan.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/shared/components/ProviderIcon.tsx b/src/shared/components/ProviderIcon.tsx index 1c070fb56c..af59c278da 100644 --- a/src/shared/components/ProviderIcon.tsx +++ b/src/shared/components/ProviderIcon.tsx @@ -150,6 +150,7 @@ const KNOWN_SVGS = new Set([ "opencode", "playht", "puter", + "qianfan", "scaleway", "sdwebui", "synthetic", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 64169da340..084dfb23dd 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -151,6 +151,18 @@ export const APIKEY_PROVIDERS = { passthroughModels: true, website: "https://openrouter.ai", }, + qianfan: { + id: "qianfan", + alias: "qianfan", + name: "Baidu Qianfan", + icon: "cloud", + color: "#2468F2", + textIcon: "BD", + website: "https://cloud.baidu.com/product/wenxinworkshop", + passthroughModels: true, + apiHint: + "Use a Qianfan API key from Baidu AI Cloud. The default endpoint is OpenAI-compatible v2.", + }, glm: { id: "glm", alias: "glm", diff --git a/tests/unit/chat-openai-compat-providers.test.ts b/tests/unit/chat-openai-compat-providers.test.ts index 7e2c96c206..baf02ca8f6 100644 --- a/tests/unit/chat-openai-compat-providers.test.ts +++ b/tests/unit/chat-openai-compat-providers.test.ts @@ -8,6 +8,7 @@ import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers.ts"; const CHAT_OPENAI_COMPAT_PROVIDER_IDS = [ "deepinfra", "vercel-ai-gateway", + "qianfan", "lambda-ai", "sambanova", "nscale", diff --git a/tests/unit/qianfan-provider.test.ts b/tests/unit/qianfan-provider.test.ts new file mode 100644 index 0000000000..d0eede12fc --- /dev/null +++ b/tests/unit/qianfan-provider.test.ts @@ -0,0 +1,69 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +import { REGISTRY, getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; +import { PROVIDERS } from "../../open-sse/config/constants.ts"; +import { getModelsByProviderId, isValidModel } from "../../src/shared/constants/models.ts"; +import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers.ts"; +import { validateBody, createProviderSchema } from "../../src/shared/validation/schemas.ts"; + +test("qianfan registers Baidu ERNIE as an OpenAI-compatible API key provider", () => { + const registryEntry = getRegistryEntry("qianfan"); + + assert.ok(registryEntry, "qianfan should be in the provider registry"); + assert.equal(registryEntry, REGISTRY.qianfan); + assert.equal(registryEntry.id, "qianfan"); + assert.equal(registryEntry.alias, "qianfan"); + assert.equal(registryEntry.format, "openai"); + assert.equal(registryEntry.executor, "default"); + assert.equal(registryEntry.authType, "apikey"); + assert.equal(registryEntry.authHeader, "bearer"); + assert.equal(registryEntry.baseUrl, "https://qianfan.baidubce.com/v2/chat/completions"); + assert.equal(registryEntry.defaultContextLength, 128000); + assert.equal(registryEntry.passthroughModels, true); + + assert.ok(APIKEY_PROVIDERS.qianfan, "qianfan should be visible in API key providers"); + assert.equal(APIKEY_PROVIDERS.qianfan.name, "Baidu Qianfan"); + assert.equal(APIKEY_PROVIDERS.qianfan.website, "https://cloud.baidu.com/product/wenxinworkshop"); + assert.equal(APIKEY_PROVIDERS.qianfan.passthroughModels, true); + + assert.equal(PROVIDERS.qianfan.baseUrl, registryEntry.baseUrl); + assert.equal(PROVIDERS.qianfan.format, "openai"); + assert.equal("modelsUrl" in PROVIDERS.qianfan, false); + assert.equal("passthroughModels" in PROVIDERS.qianfan, false); +}); + +test("qianfan exposes ERNIE chat models in the local model catalog", () => { + const models = getModelsByProviderId("qianfan"); + const modelIds = models.map((model) => model.id); + + assert.ok(modelIds.includes("ernie-4.5-turbo-128k")); + assert.ok(modelIds.includes("ernie-x1-turbo-32k")); + assert.ok(modelIds.includes("ernie-4.5-8k-preview")); + assert.ok(models.every((model) => typeof model.name === "string" && model.name.length > 0)); +}); + +test("qianfan accepts known and passthrough model IDs", () => { + assert.equal(isValidModel("qianfan", "ernie-4.5-turbo-128k"), true); + assert.equal(isValidModel("qianfan", "future-qianfan-openai-compatible-model"), true); +}); + +test("qianfan provider creation schema accepts API-key connections", () => { + const validation = validateBody(createProviderSchema, { + provider: "qianfan", + apiKey: "bce-v3/test-key", + name: "Baidu Qianfan", + }); + + assert.equal(validation.success, true); + if (validation.success) { + assert.equal(validation.data.provider, "qianfan"); + assert.equal(validation.data.apiKey, "bce-v3/test-key"); + } +}); + +test("qianfan has a static provider icon asset", () => { + assert.equal(existsSync(join(process.cwd(), "public/providers/qianfan.svg")), true); +});