From 7658a7da1e24fb37e907bab23e6d1855bb5f209c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:45:06 -0300 Subject: [PATCH] feat(providers): add Charm Hyper OpenAI-compatible provider (#5961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(providers): add Charm Hyper OpenAI-compatible provider Registers Charm Hyper (hyper.charm.land) as a new API-key gateway provider: OpenAI-compatible chat completions format, bearer auth, free tier (100 monthly Hypercredits). Models are resolved via passthrough (modelsUrl + live /v1/models import) instead of a hardcoded upstream model list, since the specific model catalog is not publicly documented. Co-authored-by: whale Inspired-by: https://github.com/decolua/9router/pull/2006 * test(golden): regenerate translate-path for charm-hyper provider * test(providers): bump APIKEY count 163→164 for charm-hyper --------- Co-authored-by: whale --- CHANGELOG.md | 1 + open-sse/config/providers/index.ts | 2 ++ .../providers/registry/charm-hyper/index.ts | 14 ++++++++ .../constants/providers/apikey/gateways.ts | 14 ++++++++ tests/snapshots/provider/translate-path.json | 23 +++++++++++++ tests/unit/charm-hyper-provider.test.ts | 33 +++++++++++++++++++ tests/unit/providers-constants-split.test.ts | 12 +++---- 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 open-sse/config/providers/registry/charm-hyper/index.ts create mode 100644 tests/unit/charm-hyper-provider.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c2580b5d..a7acb9d96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - **feat(providers):** add Qiniu as an OpenAI-compatible (API-key) provider. (thanks @JackChiang233) - **feat(providers):** add b.ai as an OpenAI-compatible (API-key) provider. (thanks @DEYLNN) - **feat(providers):** add Nube.sh as an OpenAI-compatible (API-key) provider. (thanks @whale9820) +- **feat(providers):** add Charm Hyper as an OpenAI-compatible (API-key) provider. (thanks @whale9820) ### 🔧 Bug Fixes diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 031c9c30a0..bf28d75830 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -51,6 +51,7 @@ import { groqProvider } from "./registry/groq/index.ts"; import { inference_netProvider } from "./registry/inference-net/index.ts"; import { llm7Provider } from "./registry/llm7/index.ts"; import { cerebrasProvider } from "./registry/cerebras/index.ts"; +import { charmHyperProvider } from "./registry/charm-hyper/index.ts"; import { nubeProvider } from "./registry/nube/index.ts"; import { clinepassProvider } from "./registry/clinepass/index.ts"; import { sparkdeskProvider } from "./registry/sparkdesk/index.ts"; @@ -228,6 +229,7 @@ export const REGISTRY: Record = { "inference-net": inference_netProvider, llm7: llm7Provider, cerebras: cerebrasProvider, + "charm-hyper": charmHyperProvider, nube: nubeProvider, clinepass: clinepassProvider, sparkdesk: sparkdeskProvider, diff --git a/open-sse/config/providers/registry/charm-hyper/index.ts b/open-sse/config/providers/registry/charm-hyper/index.ts new file mode 100644 index 0000000000..dfbf994891 --- /dev/null +++ b/open-sse/config/providers/registry/charm-hyper/index.ts @@ -0,0 +1,14 @@ +import type { RegistryEntry } from "../../shared.ts"; + +export const charmHyperProvider: RegistryEntry = { + id: "charm-hyper", + alias: "charm-hyper", + format: "openai", + executor: "default", + baseUrl: "https://hyper.charm.land/v1/chat/completions", + authType: "apikey", + authHeader: "bearer", + modelsUrl: "https://hyper.charm.land/v1/models", + models: [{ id: "hyper/auto", name: "Charm Hyper Auto" }], + passthroughModels: true, +}; diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 27ef167735..cad4f206cf 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -3,6 +3,20 @@ * Pure data; merged by apikey/index.ts via spread (god-file decomposition; semantic split). */ export const APIKEY_PROVIDERS_GATEWAYS = { + "charm-hyper": { + id: "charm-hyper", + alias: "charm-hyper", + name: "Charm Hyper", + icon: "router", + color: "#7C3AED", + textIcon: "CH", + passthroughModels: true, + website: "https://hyper.charm.land", + hasFree: true, + freeNote: "100 free monthly Hypercredits on signup", + apiHint: + "Create an API key at https://hyper.charm.land, then paste it here as a Bearer token.", + }, agentrouter: { id: "agentrouter", alias: "agentrouter", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 9a72ce7bd9..c619402d7c 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -611,6 +611,29 @@ "stream": "https://api.cerebras.ai/v1/chat/completions" } }, + "charm-hyper": { + "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://hyper.charm.land/v1/chat/completions", + "stream": "https://hyper.charm.land/v1/chat/completions" + } + }, "chatgpt-web": { "format": "openai", "headers": { diff --git a/tests/unit/charm-hyper-provider.test.ts b/tests/unit/charm-hyper-provider.test.ts new file mode 100644 index 0000000000..ffc25f2256 --- /dev/null +++ b/tests/unit/charm-hyper-provider.test.ts @@ -0,0 +1,33 @@ +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 CHARM_HYPER_CHAT_URL = "https://hyper.charm.land/v1/chat/completions"; +const CHARM_HYPER_MODELS_URL = "https://hyper.charm.land/v1/models"; + +test("Charm Hyper is registered as a free API-key provider", () => { + const entry = APIKEY_PROVIDERS["charm-hyper"]; + assert.ok(entry, "APIKEY_PROVIDERS['charm-hyper'] must be defined"); + assert.equal(entry.id, "charm-hyper"); + assert.equal(entry.alias, "charm-hyper"); + assert.equal(entry.name, "Charm Hyper"); + assert.equal(entry.website, "https://hyper.charm.land"); + assert.equal(entry.hasFree, true); + assert.equal(entry.passthroughModels, true); +}); + +test("Charm Hyper registry entry uses OpenAI format with bearer API-key auth", () => { + const entry = providerRegistry["charm-hyper"]; + assert.ok(entry, "providerRegistry['charm-hyper'] must be defined"); + assert.equal(entry.id, "charm-hyper"); + assert.equal(entry.alias, "charm-hyper"); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, CHARM_HYPER_CHAT_URL); + assert.equal(entry.modelsUrl, CHARM_HYPER_MODELS_URL); + assert.equal(entry.passthroughModels, true); +}); diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 076c246725..c9c6638d43 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 (163 APIKEY entries, no loss/dup), and that +// + helpers still exported), the spread-merge integrity (164 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 163 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 164 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 163); - assert.equal(new Set(keys).size, 163, "duplicate keys after spread-merge"); + assert.equal(keys.length, 164); + assert.equal(new Set(keys).size, 164, "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 163. + // strict partition (every provider in exactly one), so the sum must be exactly 164. 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 163 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 163, "families must partition all 163 providers"); + assert.equal(famTotal, 164, "families must partition all 164 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {