diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f027303f..152a003d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### ✨ New Features +- **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) - **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127) diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 7627f79958..e071ab4205 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -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 { hcnsecProvider } from "./registry/hcnsec/index.ts"; export const REGISTRY: Record = { aimlapi: aimlapiProvider, @@ -368,4 +369,5 @@ export const REGISTRY: Record = { x5lab: x5labProvider, kenari: kenariProvider, requesty: requestyProvider, + hcnsec: hcnsecProvider, }; diff --git a/open-sse/config/providers/registry/hcnsec/index.ts b/open-sse/config/providers/registry/hcnsec/index.ts new file mode 100644 index 0000000000..acce62c2bc --- /dev/null +++ b/open-sse/config/providers/registry/hcnsec/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const hcnsecProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "hcnsec", + alias: "hcnsec", + baseUrl: "https://api.hcnsec.cn/v1/chat/completions", + modelsUrl: "https://api.hcnsec.cn/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/src/shared/constants/providers/apikey/regional.ts b/src/shared/constants/providers/apikey/regional.ts index 7affe24500..d91175340d 100644 --- a/src/shared/constants/providers/apikey/regional.ts +++ b/src/shared/constants/providers/apikey/regional.ts @@ -355,4 +355,18 @@ export const APIKEY_PROVIDERS_REGIONAL = { deprecationReason: "api.inclusionai.tech no longer resolves (sweep 2026-06-19); the inference API appears discontinued.", }, + hcnsec: { + id: "hcnsec", + alias: "hcnsec", + name: "Huancheng Public API", + icon: "security", + color: "#0EA5E9", + textIcon: "HC", + website: "https://api.hcnsec.cn", + hasFree: true, + freeNote: + "Xinjiang Huancheng Cybersecurity public LLM API platform: free credits with daily check-ins.", + passthroughModels: true, + authHint: "Get API key at api.hcnsec.cn", + }, }; diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 95c12d3b22..526725e1e8 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -2018,6 +2018,29 @@ "stream": "https://api.haiper.ai/v1" } }, + "hcnsec": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://api.hcnsec.cn/v1/chat/completions", + "stream": "https://api.hcnsec.cn/v1/chat/completions" + } + }, "heroku": { "format": "openai", "headers": { diff --git a/tests/unit/hcnsec-provider.test.ts b/tests/unit/hcnsec-provider.test.ts new file mode 100644 index 0000000000..e7fbb30077 --- /dev/null +++ b/tests/unit/hcnsec-provider.test.ts @@ -0,0 +1,44 @@ +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 HCNSEC_CHAT_URL = "https://api.hcnsec.cn/v1/chat/completions"; +const HCNSEC_MODELS_URL = "https://api.hcnsec.cn/v1/models"; + +test("hcnsec is registered as an API-key regional provider", () => { + const entry = APIKEY_PROVIDERS.hcnsec; + assert.ok(entry, "APIKEY_PROVIDERS.hcnsec must be defined"); + assert.equal(entry.id, "hcnsec"); + assert.equal(entry.alias, "hcnsec"); + assert.equal(entry.name, "Huancheng Public API"); + assert.equal(entry.website, "https://api.hcnsec.cn"); + assert.equal(entry.passthroughModels, true); + assert.equal(entry.hasFree, true); +}); + +test("hcnsec registry entry uses OpenAI format with bearer API-key auth", () => { + const entry = providerRegistry.hcnsec; + assert.ok(entry, "providerRegistry.hcnsec must be defined"); + assert.equal(entry.id, "hcnsec"); + assert.equal(entry.alias, "hcnsec"); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, HCNSEC_CHAT_URL); + assert.equal(entry.modelsUrl, HCNSEC_MODELS_URL); + assert.equal(entry.passthroughModels, true); +}); + +test("hcnsec ships no static model seed — relies fully on passthrough + live catalog", () => { + assert.deepEqual(providerRegistry.hcnsec.models, []); +}); + +test("hcnsec accepts any model id via passthrough", () => { + assert.equal(isValidModel("hcnsec", "gpt-4o-mini"), true); + assert.equal(isValidModel("hcnsec", "deepseek-v3"), true); + assert.equal(isValidModel("hcnsec", "qwen-max"), true); +}); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 1c95453eda..a871ec1a0f 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -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/.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).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", () => {