diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 228d9e93ef..fc5d42e675 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -233,6 +233,32 @@ import { hyperagentProvider } from "./registry/hyperagent/index.ts"; import { muse_codeProvider } from "./registry/muse-code/index.ts"; import { naga_acProvider } from "./registry/naga-ac/index.ts"; import { chatanywhereProvider } from "./registry/chatanywhere/index.ts"; +import { zyloApiProvider } from "./registry/zylo-api/index.ts"; +import { poolsideProvider } from "./registry/poolside/index.ts"; +import { fastrouterProvider } from "./registry/fastrouter/index.ts"; +import { anyapiProvider } from "./registry/anyapi/index.ts"; +import { electronhubProvider } from "./registry/electronhub/index.ts"; +import { llmgatewayProvider } from "./registry/llmgateway/index.ts"; +import { llmKiwiProvider } from "./registry/llm-kiwi/index.ts"; +import { literouterProvider } from "./registry/literouter/index.ts"; +import { mnnAiProvider } from "./registry/mnn-ai/index.ts"; +import { meganovaAiProvider } from "./registry/meganova-ai/index.ts"; +import { mixlayerProvider } from "./registry/mixlayer/index.ts"; +import { spekaProvider } from "./registry/speka/index.ts"; +import { tokenreplyProvider } from "./registry/tokenreply/index.ts"; +import { yoloAutoProvider } from "./registry/yolo-auto/index.ts"; +import { dxntProvider } from "./registry/dxnt/index.ts"; +import { cloudcodeOneProvider } from "./registry/cloudcode-one/index.ts"; +import { ofoxaiProvider } from "./registry/ofoxai/index.ts"; +import { zerolimitaiProvider } from "./registry/zerolimitai/index.ts"; +import { helyxaiProvider } from "./registry/helyxai/index.ts"; +import { aurikoProvider } from "./registry/auriko/index.ts"; +import { poixeAiProvider } from "./registry/poixe-ai/index.ts"; +import { nagaAiProvider } from "./registry/naga-ai/index.ts"; +import { chatOripeProvider } from "./registry/chat-oripe/index.ts"; +import { freeinferenceProvider } from "./registry/freeinference/index.ts"; +import { freeAiProvider } from "./registry/free-ai/index.ts"; + export const REGISTRY: Record = { aimlapi: aimlapiProvider, @@ -465,7 +491,33 @@ export const REGISTRY: Record = { promptql: promptqlProvider, hyperagent: hyperagentProvider, "muse-code": muse_codeProvider, + "zylo-api": zyloApiProvider, unorouter: unorouterProvider, "naga-ac": naga_acProvider, chatanywhere: chatanywhereProvider, + poolside: poolsideProvider, + fastrouter: fastrouterProvider, + anyapi: anyapiProvider, + electronhub: electronhubProvider, + llmgateway: llmgatewayProvider, + "llm-kiwi": llmKiwiProvider, + literouter: literouterProvider, + "mnn-ai": mnnAiProvider, + "meganova-ai": meganovaAiProvider, + mixlayer: mixlayerProvider, + speka: spekaProvider, + tokenreply: tokenreplyProvider, + "yolo-auto": yoloAutoProvider, + dxnt: dxntProvider, + "cloudcode-one": cloudcodeOneProvider, + ofoxai: ofoxaiProvider, + zerolimitai: zerolimitaiProvider, + helyxai: helyxaiProvider, + auriko: aurikoProvider, + "poixe-ai": poixeAiProvider, + "naga-ai": nagaAiProvider, + "chat-oripe": chatOripeProvider, + freeinference: freeinferenceProvider, + "free-ai": freeAiProvider, + }; diff --git a/open-sse/config/providers/registry/auriko/index.ts b/open-sse/config/providers/registry/auriko/index.ts new file mode 100644 index 0000000000..8e9319c9c5 --- /dev/null +++ b/open-sse/config/providers/registry/auriko/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const aurikoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "auriko", + alias: "auriko", + baseUrl: "https://api.auriko.ai/v1/chat/completions", + modelsUrl: "https://api.auriko.ai/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/chat-oripe/index.ts b/open-sse/config/providers/registry/chat-oripe/index.ts new file mode 100644 index 0000000000..4aac54edeb --- /dev/null +++ b/open-sse/config/providers/registry/chat-oripe/index.ts @@ -0,0 +1,12 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +// The upstream brand and hostname are ambiguous, so avoid unverified quota claims. +export const chatOripeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "chat-oripe", + alias: "chat-oripe", + baseUrl: "https://api.oriper.com/v1/chat/completions", + modelsUrl: "https://api.oriper.com/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/chatanywhere/index.ts b/open-sse/config/providers/registry/chatanywhere/index.ts index 137cef8d05..6c99250b20 100644 --- a/open-sse/config/providers/registry/chatanywhere/index.ts +++ b/open-sse/config/providers/registry/chatanywhere/index.ts @@ -1,17 +1,12 @@ import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; -// ChatAnywhere (api.chatanywhere.tech) — OpenAI-compatible gateway from the -// chatanywhere/GPT_API_free project (~38.7k GitHub stars). Requires GitHub-account- -// gated API key. Free tier is for personal non-commercial use only. -export const chatanywhereProvider: RegistryEntry = { +// International endpoint; audited free access is limited to non-commercial use. +export const chatanywhereProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ id: "chatanywhere", - alias: "chtany", - format: "openai", - executor: "default", - baseUrl: "https://api.chatanywhere.tech/v1/chat/completions", - modelsUrl: "https://api.chatanywhere.tech/v1/models", - authType: "apikey", - authHeader: "bearer", - passthroughModels: true, + alias: "chatanywhere", + baseUrl: "https://api.chatanywhere.org/v1/chat/completions", + modelsUrl: "https://api.chatanywhere.org/v1/models", models: [], -}; \ No newline at end of file + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/cloudcode-one/index.ts b/open-sse/config/providers/registry/cloudcode-one/index.ts new file mode 100644 index 0000000000..e9f8bc211d --- /dev/null +++ b/open-sse/config/providers/registry/cloudcode-one/index.ts @@ -0,0 +1,20 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * CloudCode.ONE - OpenAI-compatible API with published free model aliases. + * + * The Anthropic-compatible endpoint is documented separately; this registry + * covers the OpenAI-compatible API surface audited for this migration. + */ +export const cloudcodeOneProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "cloudcode-one", + alias: "cloudcode-one", + baseUrl: "https://api.cloudcode.one/v1/chat/completions", + modelsUrl: "https://api.cloudcode.one/v1/models", + models: [ + { id: "glm-4.7-flash", name: "GLM 4.7 Flash" }, + { id: "glm-4.6v-flash", name: "GLM 4.6V Flash" }, + ], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/dxnt/index.ts b/open-sse/config/providers/registry/dxnt/index.ts new file mode 100644 index 0000000000..2a70c339c3 --- /dev/null +++ b/open-sse/config/providers/registry/dxnt/index.ts @@ -0,0 +1,17 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * DXNT - OpenAI-compatible API with a free account quota. + * + * Models are discovered from the provider's authenticated catalog rather than + * copied into a static list, so account-specific availability remains intact. + */ +export const dxntProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "dxnt", + alias: "dxnt", + baseUrl: "https://www.dxnt.com/v1/chat/completions", + modelsUrl: "https://www.dxnt.com/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/free-ai/index.ts b/open-sse/config/providers/registry/free-ai/index.ts new file mode 100644 index 0000000000..a055dab8db --- /dev/null +++ b/open-sse/config/providers/registry/free-ai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const freeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "free-ai", + alias: "free-ai", + baseUrl: "https://api.free.ai/v1/chat/", + modelsUrl: "https://api.free.ai/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/freeinference/index.ts b/open-sse/config/providers/registry/freeinference/index.ts new file mode 100644 index 0000000000..25c02dbaad --- /dev/null +++ b/open-sse/config/providers/registry/freeinference/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const freeinferenceProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "freeinference", + alias: "freeinference", + baseUrl: "https://freeinference.org/v1/chat/completions", + modelsUrl: "https://freeinference.org/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/helyxai/index.ts b/open-sse/config/providers/registry/helyxai/index.ts new file mode 100644 index 0000000000..e0b3fd1ea0 --- /dev/null +++ b/open-sse/config/providers/registry/helyxai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const helyxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "helyxai", + alias: "helyxai", + baseUrl: "https://helyxai.space/v1/chat/completions", + modelsUrl: "https://helyxai.space/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/literouter/index.ts b/open-sse/config/providers/registry/literouter/index.ts new file mode 100644 index 0000000000..fd13e540eb --- /dev/null +++ b/open-sse/config/providers/registry/literouter/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const literouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "literouter", + alias: "literouter", + baseUrl: "https://api.literouter.com/v1/chat/completions", + modelsUrl: "https://api.literouter.com/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/meganova-ai/index.ts b/open-sse/config/providers/registry/meganova-ai/index.ts new file mode 100644 index 0000000000..e5c61d01df --- /dev/null +++ b/open-sse/config/providers/registry/meganova-ai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const meganovaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "meganova-ai", + alias: "meganova-ai", + baseUrl: "https://api.meganova.ai/v1/chat/completions", + modelsUrl: "https://api.meganova.ai/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/mixlayer/index.ts b/open-sse/config/providers/registry/mixlayer/index.ts new file mode 100644 index 0000000000..63f4ab7715 --- /dev/null +++ b/open-sse/config/providers/registry/mixlayer/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const mixlayerProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "mixlayer", + alias: "mixlayer", + baseUrl: "https://models.mixlayer.ai/v1/chat/completions", + modelsUrl: "https://models.mixlayer.ai/v1/models", + models: [{ id: "qwen/qwen3.5-4b-free", name: "Qwen 3.5 4B (free)" }], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/mnn-ai/index.ts b/open-sse/config/providers/registry/mnn-ai/index.ts new file mode 100644 index 0000000000..f1d7e2317f --- /dev/null +++ b/open-sse/config/providers/registry/mnn-ai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const mnnAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "mnn-ai", + alias: "mnn-ai", + baseUrl: "https://api.mnnai.ru/v1/chat/completions", + modelsUrl: "https://api.mnnai.ru/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/naga-ai/index.ts b/open-sse/config/providers/registry/naga-ai/index.ts new file mode 100644 index 0000000000..3918bd5585 --- /dev/null +++ b/open-sse/config/providers/registry/naga-ai/index.ts @@ -0,0 +1,12 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +// Free access terms may permit data collection or training use; discover models dynamically. +export const nagaAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "naga-ai", + alias: "naga-ai", + baseUrl: "https://api.naga.ac/v1/chat/completions", + modelsUrl: "https://api.naga.ac/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/ofoxai/index.ts b/open-sse/config/providers/registry/ofoxai/index.ts new file mode 100644 index 0000000000..ada7b5eef2 --- /dev/null +++ b/open-sse/config/providers/registry/ofoxai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const ofoxaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "ofoxai", + alias: "ofoxai", + baseUrl: "https://api.ofox.ai/v1/chat/completions", + modelsUrl: "https://api.ofox.ai/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/poixe-ai/index.ts b/open-sse/config/providers/registry/poixe-ai/index.ts new file mode 100644 index 0000000000..7507e8580a --- /dev/null +++ b/open-sse/config/providers/registry/poixe-ai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const poixeAiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "poixe-ai", + alias: "poixe-ai", + baseUrl: "https://api.poixe.com/v1/chat/completions", + modelsUrl: "https://api.poixe.com/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/speka/index.ts b/open-sse/config/providers/registry/speka/index.ts new file mode 100644 index 0000000000..04f1f17c7e --- /dev/null +++ b/open-sse/config/providers/registry/speka/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const spekaProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "speka", + alias: "speka", + baseUrl: "https://speka.me/v1/chat/completions", + modelsUrl: "https://speka.me/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/tokenreply/index.ts b/open-sse/config/providers/registry/tokenreply/index.ts new file mode 100644 index 0000000000..835225c471 --- /dev/null +++ b/open-sse/config/providers/registry/tokenreply/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const tokenreplyProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "tokenreply", + alias: "tokenreply", + baseUrl: "https://api.tokenreply.com/v1/chat/completions", + modelsUrl: "https://api.tokenreply.com/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/unorouter/index.ts b/open-sse/config/providers/registry/unorouter/index.ts index 771bde6a7c..a84c38387d 100644 --- a/open-sse/config/providers/registry/unorouter/index.ts +++ b/open-sse/config/providers/registry/unorouter/index.ts @@ -1,14 +1,11 @@ import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; -export const unorouterProvider: RegistryEntry = { +export const unorouterProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ id: "unorouter", alias: "unorouter", - format: "openai", - executor: "default", - baseUrl: "https://api.unorouter.ai/v1/chat/completions", - authType: "apikey", - authHeader: "bearer", - defaultContextLength: 128000, - models: [{ id: "auto", name: "Auto (Best Available)" }], + baseUrl: "https://api.unorouter.com/v1/chat/completions", + modelsUrl: "https://api.unorouter.com/v1/models", + models: [], passthroughModels: true, -}; +}); diff --git a/open-sse/config/providers/registry/yolo-auto/index.ts b/open-sse/config/providers/registry/yolo-auto/index.ts new file mode 100644 index 0000000000..6b90dc1a75 --- /dev/null +++ b/open-sse/config/providers/registry/yolo-auto/index.ts @@ -0,0 +1,17 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * Yolo-Auto - OpenAI-compatible API with a request-limited free tier. + * + * The catalog is kept intentionally small: the documented free-tier model is + * seeded while passthrough discovery allows the service to publish updates. + */ +export const yoloAutoProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "yolo-auto", + alias: "yolo-auto", + baseUrl: "https://yolo-auto.com/v1/chat/completions", + modelsUrl: "https://yolo-auto.com/v1/models", + models: [{ id: "qwen3.6-35b-a3b", name: "Qwen 3.6 35B A3B" }], + passthroughModels: true, +}); diff --git a/open-sse/config/providers/registry/zerolimitai/index.ts b/open-sse/config/providers/registry/zerolimitai/index.ts new file mode 100644 index 0000000000..5ad779d6d7 --- /dev/null +++ b/open-sse/config/providers/registry/zerolimitai/index.ts @@ -0,0 +1,11 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +export const zerolimitaiProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "zerolimitai", + alias: "zerolimitai", + baseUrl: "https://www.zerolimitai.com/api/v1/chat/completions", + modelsUrl: "https://www.zerolimitai.com/api/v1/models", + models: [], + passthroughModels: true, +}); diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index d80b94f4ef..35b3be51d1 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -7,6 +7,33 @@ export const PROVIDER_ENDPOINTS = { dgrid: "https://api.dgrid.ai/v1/chat/completions", bai: "https://api.b.ai/v1/chat/completions", qiniu: "https://api.qnaigc.com/v1/chat/completions", + "zylo-api": "https://api.zyloai.net/v1/chat/completions", + unorouter: "https://api.unorouter.com/v1/chat/completions", + poolside: "https://inference.poolside.ai/v1/chat/completions", + fastrouter: "https://api.fastrouter.ai/api/v1/chat/completions", + anyapi: "https://api.anyapi.ai/v1/chat/completions", + electronhub: "https://api.electronhub.ai/v1/chat/completions", + llmgateway: "https://api.llmgateway.io/v1/chat/completions", + "llm-kiwi": "https://api.llm.kiwi/v1/chat/completions", + literouter: "https://api.literouter.com/v1/chat/completions", + "mnn-ai": "https://api.mnnai.ru/v1/chat/completions", + "meganova-ai": "https://api.meganova.ai/v1/chat/completions", + mixlayer: "https://models.mixlayer.ai/v1/chat/completions", + speka: "https://speka.me/v1/chat/completions", + tokenreply: "https://api.tokenreply.com/v1/chat/completions", + "yolo-auto": "https://yolo-auto.com/v1/chat/completions", + dxnt: "https://www.dxnt.com/v1/chat/completions", + "cloudcode-one": "https://api.cloudcode.one/v1/chat/completions", + ofoxai: "https://api.ofox.ai/v1/chat/completions", + zerolimitai: "https://www.zerolimitai.com/api/v1/chat/completions", + chatanywhere: "https://api.chatanywhere.org/v1/chat/completions", + helyxai: "https://helyxai.space/v1/chat/completions", + auriko: "https://api.auriko.ai/v1/chat/completions", + "poixe-ai": "https://api.poixe.com/v1/chat/completions", + "naga-ai": "https://api.naga.ac/v1/chat/completions", + "chat-oripe": "https://api.oriper.com/v1/chat/completions", + freeinference: "https://freeinference.org/v1/chat/completions", + "free-ai": "https://api.free.ai/v1/chat/", glm: "https://api.z.ai/api/anthropic/v1/messages", glmt: "https://api.z.ai/api/anthropic/v1/messages", "bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages", diff --git a/src/shared/constants/models.ts b/src/shared/constants/models.ts index 7595280708..f78d64858f 100644 --- a/src/shared/constants/models.ts +++ b/src/shared/constants/models.ts @@ -21,7 +21,10 @@ import { PROVIDER_MODELS as MODELS } from "@omniroute/open-sse/config/providerMo const PASSTHROUGH_PROVIDERS = new Set( Object.entries(AI_PROVIDERS) .filter(([, p]) => (p as any).passthroughModels) - .map(([key]) => key) + .flatMap(([key, provider]) => { + const alias = (provider as { alias?: unknown }).alias; + return typeof alias === "string" && alias.length > 0 ? [key, alias] : [key]; + }) ); // Wrap isValidModel with passthrough providers diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 9d6b15b824..a20078ee1a 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -96,7 +96,32 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([ "g4f-nvidia", "naga-ac", "chatanywhere", -]); + "zylo-api", + "fastrouter", + "anyapi", + "electronhub", + "llmgateway", + "llm-kiwi", + "literouter", + "mnn-ai", + "meganova-ai", + "mixlayer", + "speka", + "tokenreply", + "yolo-auto", + "dxnt", + "cloudcode-one", + "ofoxai", + "zerolimitai", + "helyxai", + "auriko", + "poixe-ai", + "naga-ai", + "chat-oripe", + "freeinference", + "free-ai", + +]);; export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([ "azure-openai", diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 32bfc1feb8..f55d2f5620 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -53,6 +53,9 @@ export const APIKEY_PROVIDERS_GATEWAYS = { color: "#8B5CF6", textIcon: "UR", passthroughModels: true, + hasFree: true, + freeNote: + "Models with the :free suffix do not debit balance; limit is 1 request/minute per free model per user.", website: "https://unorouter.ai", apiHint: "Create an API key at https://unorouter.ai, then paste it here as a Bearer token.", }, @@ -439,6 +442,37 @@ export const APIKEY_PROVIDERS_GATEWAYS = { apiHint: "Use https://api.oriper.com/v1 only after confirming the provider's current documentation, terms and key issuance. No quota is guaranteed by this catalog.", }, + freeinference: { + id: "freeinference", + alias: "freeinference", + name: "FreeInference", + icon: "science", + color: "#8B5CF6", + textIcon: "FI", + passthroughModels: true, + website: "https://freeinference.org", + hasFree: true, + freeNote: + "Free research access without a card; non-Harvard applicants require manual approval and no numeric quota is publicly guaranteed.", + apiHint: + "Apply for a FreeInference key, then use https://freeinference.org/v1 as the OpenAI-compatible base URL. Terms allow prompt/response logging and possible publication of anonymized research data; never send sensitive or production data.", + }, + "free-ai": { + id: "free-ai", + alias: "free-ai", + name: "Free.ai", + icon: "hub", + color: "#16A34A", + textIcon: "FA", + passthroughModels: true, + website: "https://free.ai", + hasFree: true, + freeNote: + "30,000 tokens/day cover self-hosted models after email verification. Usage beyond the pool can bill at raw cost, and premium external models are paid.", + apiHint: + "Create an sk-free- key, then use the nonstandard but OpenAI-shaped https://api.free.ai/v1/chat/ endpoint. Select a self-hosted zero-price model to stay within the free pool.", + }, + dgrid: { id: "dgrid", alias: "dgrid", diff --git a/src/shared/constants/providers/apikey/inference-hosts.ts b/src/shared/constants/providers/apikey/inference-hosts.ts index 926260f5e7..678fbc7c6a 100644 --- a/src/shared/constants/providers/apikey/inference-hosts.ts +++ b/src/shared/constants/providers/apikey/inference-hosts.ts @@ -45,6 +45,21 @@ export const APIKEY_PROVIDERS_INFERENCE = { hasFree: true, freeNote: "Free plan: 3-day trial with open-source models — no credit card required", }, + poolside: { + id: "poolside", + alias: "poolside", + name: "Poolside", + icon: "memory", + color: "#111827", + textIcon: "PS", + passthroughModels: true, + website: "https://poolside.ai", + hasFree: true, + freeNote: + "Laguna S 2.1 and XS 2.1 are free during Preview; no public numeric quota is published.", + apiHint: + "Create a free developer API key, then use https://inference.poolside.ai/v1 as the OpenAI-compatible base URL.", + }, fireworks: { id: "fireworks", alias: "fireworks", diff --git a/tests/unit/free-tier-providers-phase3-integration.test.ts b/tests/unit/free-tier-providers-phase3-integration.test.ts new file mode 100644 index 0000000000..89c3141d8b --- /dev/null +++ b/tests/unit/free-tier-providers-phase3-integration.test.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts"); +const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); +const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts"); + +const providers = [ + ["zylo-api", "zylo", "https://api.zyloai.net/v1/chat/completions"], + ["unorouter", "unorouter", "https://api.unorouter.com/v1/chat/completions"], + ["poolside", "poolside", "https://inference.poolside.ai/v1/chat/completions"], + ["fastrouter", "fastrouter", "https://api.fastrouter.ai/api/v1/chat/completions"], + ["anyapi", "anyapi", "https://api.anyapi.ai/v1/chat/completions"], + ["electronhub", "electronhub", "https://api.electronhub.ai/v1/chat/completions"], + ["llmgateway", "llmgateway", "https://api.llmgateway.io/v1/chat/completions"], + ["llm-kiwi", "llmkiwi", "https://api.llm.kiwi/v1/chat/completions"], +] as const; + +for (const [id, alias, endpoint] of providers) { + test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + const registry = REGISTRY[id]; + const metadata = APIKEY_PROVIDERS[id]; + + assert.ok(registry); + assert.ok(metadata); + assert.equal(registry.id, id); + assert.equal(registry.alias, alias); + assert.equal(registry.baseUrl, endpoint); + assert.equal(PROVIDER_ENDPOINTS[id], endpoint); + assert.equal(metadata.id, id); + assert.equal(metadata.alias, alias); + assert.equal(metadata.hasFree, true); + assert.equal(metadata.passthroughModels, true); + assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); + assert.ok(getExecutor(id) instanceof DefaultExecutor); + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + assert.equal(isValidModel(alias, "future/live-catalog-model"), true); + }); +} + +test("gateway providers are classified as aggregators while direct Poolside is not", () => { + const gatewayIds = providers.map(([id]) => id).filter((id) => id !== "poolside"); + for (const id of gatewayIds) assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); + assert.equal(AGGREGATOR_PROVIDER_IDS.has("poolside"), false); +}); + +test("LLM.Kiwi statically exposes only the confirmed Free plan models", () => { + assert.deepEqual(REGISTRY["llm-kiwi"].models, [ + { id: "auto", name: "Auto" }, + { id: "hrLLM", name: "hrLLM" }, + ]); +}); diff --git a/tests/unit/free-tier-providers-wave2-a.test.ts b/tests/unit/free-tier-providers-wave2-a.test.ts new file mode 100644 index 0000000000..b646b51bdf --- /dev/null +++ b/tests/unit/free-tier-providers-wave2-a.test.ts @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { literouterProvider } from "../../open-sse/config/providers/registry/literouter/index.ts"; +import { meganovaAiProvider } from "../../open-sse/config/providers/registry/meganova-ai/index.ts"; +import { mnnAiProvider } from "../../open-sse/config/providers/registry/mnn-ai/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; +}> = [ + { + entry: literouterProvider, + id: "literouter", + chatUrl: "https://api.literouter.com/v1/chat/completions", + modelsUrl: "https://api.literouter.com/v1/models", + }, + { + entry: mnnAiProvider, + id: "mnn-ai", + chatUrl: "https://api.mnnai.ru/v1/chat/completions", + modelsUrl: "https://api.mnnai.ru/v1/models", + }, + { + entry: meganovaAiProvider, + id: "meganova-ai", + chatUrl: "https://api.meganova.ai/v1/chat/completions", + modelsUrl: "https://api.meganova.ai/v1/models", + }, +]; + +for (const { entry, id, chatUrl, modelsUrl } of providers) { + test(`${id} uses an OpenAI-compatible Bearer registry entry`, () => { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + }); + + test(`${id} leaves model discovery to the live upstream catalog`, () => { + assert.deepEqual(entry.models, []); + }); +} diff --git a/tests/unit/free-tier-providers-wave2-b.test.ts b/tests/unit/free-tier-providers-wave2-b.test.ts new file mode 100644 index 0000000000..3fec49f943 --- /dev/null +++ b/tests/unit/free-tier-providers-wave2-b.test.ts @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { mixlayerProvider } from "../../open-sse/config/providers/registry/mixlayer/index.ts"; +import { spekaProvider } from "../../open-sse/config/providers/registry/speka/index.ts"; +import { tokenreplyProvider } from "../../open-sse/config/providers/registry/tokenreply/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; +}> = [ + { + entry: mixlayerProvider, + id: "mixlayer", + chatUrl: "https://models.mixlayer.ai/v1/chat/completions", + modelsUrl: "https://models.mixlayer.ai/v1/models", + }, + { + entry: spekaProvider, + id: "speka", + chatUrl: "https://speka.me/v1/chat/completions", + modelsUrl: "https://speka.me/v1/models", + }, + { + entry: tokenreplyProvider, + id: "tokenreply", + chatUrl: "https://api.tokenreply.com/v1/chat/completions", + modelsUrl: "https://api.tokenreply.com/v1/models", + }, +]; + +test("Wave 2-B providers expose OpenAI-compatible Bearer registries", () => { + for (const { entry, id, chatUrl, modelsUrl } of providers) { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + assert.ok(Array.isArray(entry.models)); + } +}); + +test("Mixlayer seeds only its documented free model", () => { + assert.deepEqual( + mixlayerProvider.models.map((model) => model.id), + ["qwen/qwen3.5-4b-free"] + ); +}); + +test("Speka and TokenReply rely on live model catalogs without invented models", () => { + assert.deepEqual(spekaProvider.models, []); + assert.deepEqual(tokenreplyProvider.models, []); +}); diff --git a/tests/unit/free-tier-providers-wave2-c.test.ts b/tests/unit/free-tier-providers-wave2-c.test.ts new file mode 100644 index 0000000000..75b42242ea --- /dev/null +++ b/tests/unit/free-tier-providers-wave2-c.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { cloudcodeOneProvider } from "../../open-sse/config/providers/registry/cloudcode-one/index.ts"; +import { dxntProvider } from "../../open-sse/config/providers/registry/dxnt/index.ts"; +import { yoloAutoProvider } from "../../open-sse/config/providers/registry/yolo-auto/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; + modelIds: string[]; +}> = [ + { + entry: yoloAutoProvider, + id: "yolo-auto", + chatUrl: "https://yolo-auto.com/v1/chat/completions", + modelsUrl: "https://yolo-auto.com/v1/models", + modelIds: ["qwen3.6-35b-a3b"], + }, + { + entry: dxntProvider, + id: "dxnt", + chatUrl: "https://www.dxnt.com/v1/chat/completions", + modelsUrl: "https://www.dxnt.com/v1/models", + modelIds: [], + }, + { + entry: cloudcodeOneProvider, + id: "cloudcode-one", + chatUrl: "https://api.cloudcode.one/v1/chat/completions", + modelsUrl: "https://api.cloudcode.one/v1/models", + modelIds: ["glm-4.7-flash", "glm-4.6v-flash"], + }, +]; + +for (const { entry, id, chatUrl, modelsUrl, modelIds } of providers) { + test(`${id} uses the standard OpenAI-compatible API-key registry shape`, () => { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + }); + + test(`${id} seeds only the audited model identifiers`, () => { + assert.deepEqual( + (entry.models ?? []).map((model) => model.id), + modelIds + ); + }); +} diff --git a/tests/unit/free-tier-providers-wave2-integration.test.ts b/tests/unit/free-tier-providers-wave2-integration.test.ts new file mode 100644 index 0000000000..83128bd3c0 --- /dev/null +++ b/tests/unit/free-tier-providers-wave2-integration.test.ts @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts"); +const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); +const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts"); + +const providers = [ + ["literouter", "https://api.literouter.com/v1/chat/completions", []], + ["mnn-ai", "https://api.mnnai.ru/v1/chat/completions", []], + ["meganova-ai", "https://api.meganova.ai/v1/chat/completions", []], + ["mixlayer", "https://models.mixlayer.ai/v1/chat/completions", ["qwen/qwen3.5-4b-free"]], + ["speka", "https://speka.me/v1/chat/completions", []], + ["tokenreply", "https://api.tokenreply.com/v1/chat/completions", []], + ["yolo-auto", "https://yolo-auto.com/v1/chat/completions", ["qwen3.6-35b-a3b"]], + ["dxnt", "https://www.dxnt.com/v1/chat/completions", []], + [ + "cloudcode-one", + "https://api.cloudcode.one/v1/chat/completions", + ["glm-4.7-flash", "glm-4.6v-flash"], + ], +] as const; + +for (const [id, endpoint, modelIds] of providers) { + test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + const registry = REGISTRY[id]; + const metadata = APIKEY_PROVIDERS[id]; + + assert.ok(registry); + assert.ok(metadata); + assert.equal(registry.id, id); + assert.equal(registry.alias, id); + assert.equal(registry.baseUrl, endpoint); + assert.equal(PROVIDER_ENDPOINTS[id], endpoint); + assert.equal(metadata.id, id); + assert.equal(metadata.alias, id); + assert.equal(metadata.hasFree, true); + assert.equal(metadata.passthroughModels, true); + assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); + assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); + assert.ok(getExecutor(id) instanceof DefaultExecutor); + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + assert.deepEqual( + registry.models.map((model) => model.id), + modelIds + ); + }); +} + +test("risk-sensitive metadata preserves the audited quota qualifications", () => { + assert.match(APIKEY_PROVIDERS["mnn-ai"].apiHint ?? "", /jurisdiction.*privacy/i); + assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /per-model quotas/i); + assert.match(APIKEY_PROVIDERS["meganova-ai"].freeNote ?? "", /paid overage/i); + assert.match(APIKEY_PROVIDERS.tokenreply.freeNote ?? "", /no fixed global free quota/i); + assert.match(APIKEY_PROVIDERS["yolo-auto"].freeNote ?? "", /no numeric daily quota/i); + assert.match(APIKEY_PROVIDERS["cloudcode-one"].freeNote ?? "", /credit or a coupon/i); +}); diff --git a/tests/unit/free-tier-providers-wave3-a.test.ts b/tests/unit/free-tier-providers-wave3-a.test.ts new file mode 100644 index 0000000000..be610977fc --- /dev/null +++ b/tests/unit/free-tier-providers-wave3-a.test.ts @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { chatanywhereProvider } from "../../open-sse/config/providers/registry/chatanywhere/index.ts"; +import { ofoxaiProvider } from "../../open-sse/config/providers/registry/ofoxai/index.ts"; +import { zerolimitaiProvider } from "../../open-sse/config/providers/registry/zerolimitai/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; +}> = [ + { + entry: ofoxaiProvider, + id: "ofoxai", + chatUrl: "https://api.ofox.ai/v1/chat/completions", + modelsUrl: "https://api.ofox.ai/v1/models", + }, + { + entry: zerolimitaiProvider, + id: "zerolimitai", + chatUrl: "https://www.zerolimitai.com/api/v1/chat/completions", + modelsUrl: "https://www.zerolimitai.com/api/v1/models", + }, + { + entry: chatanywhereProvider, + id: "chatanywhere", + chatUrl: "https://api.chatanywhere.org/v1/chat/completions", + modelsUrl: "https://api.chatanywhere.org/v1/models", + }, +]; + +test("Wave 3-A providers expose OpenAI-compatible Bearer registries", () => { + for (const { entry, id, chatUrl, modelsUrl } of providers) { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + assert.deepEqual(entry.models, []); + } +}); diff --git a/tests/unit/free-tier-providers-wave3-b.test.ts b/tests/unit/free-tier-providers-wave3-b.test.ts new file mode 100644 index 0000000000..541c28472a --- /dev/null +++ b/tests/unit/free-tier-providers-wave3-b.test.ts @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { aurikoProvider } from "../../open-sse/config/providers/registry/auriko/index.ts"; +import { helyxaiProvider } from "../../open-sse/config/providers/registry/helyxai/index.ts"; +import { poixeAiProvider } from "../../open-sse/config/providers/registry/poixe-ai/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; +}> = [ + { + entry: helyxaiProvider, + id: "helyxai", + chatUrl: "https://helyxai.space/v1/chat/completions", + modelsUrl: "https://helyxai.space/v1/models", + }, + { + entry: aurikoProvider, + id: "auriko", + chatUrl: "https://api.auriko.ai/v1/chat/completions", + modelsUrl: "https://api.auriko.ai/v1/models", + }, + { + entry: poixeAiProvider, + id: "poixe-ai", + chatUrl: "https://api.poixe.com/v1/chat/completions", + modelsUrl: "https://api.poixe.com/v1/models", + }, +]; + +test("Wave 3-B providers expose OpenAI-compatible Bearer registries", () => { + for (const { entry, id, chatUrl, modelsUrl } of providers) { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + } +}); + +test("Wave 3-B providers rely on live catalogs without invented models", () => { + for (const { entry } of providers) { + assert.deepEqual(entry.models, []); + } +}); diff --git a/tests/unit/free-tier-providers-wave3-c.test.ts b/tests/unit/free-tier-providers-wave3-c.test.ts new file mode 100644 index 0000000000..05442fbe45 --- /dev/null +++ b/tests/unit/free-tier-providers-wave3-c.test.ts @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { chatOripeProvider } from "../../open-sse/config/providers/registry/chat-oripe/index.ts"; +import { nagaAiProvider } from "../../open-sse/config/providers/registry/naga-ai/index.ts"; +import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts"; + +const providers: Array<{ + entry: RegistryEntry; + id: string; + chatUrl: string; + modelsUrl: string; +}> = [ + { + entry: nagaAiProvider, + id: "naga-ai", + chatUrl: "https://api.naga.ac/v1/chat/completions", + modelsUrl: "https://api.naga.ac/v1/models", + }, + { + entry: chatOripeProvider, + id: "chat-oripe", + chatUrl: "https://api.oriper.com/v1/chat/completions", + modelsUrl: "https://api.oriper.com/v1/models", + }, +]; + +test("Wave 3-C providers expose OpenAI-compatible Bearer registries", () => { + for (const { entry, id, chatUrl, modelsUrl } of providers) { + assert.equal(entry.id, id); + assert.equal(entry.alias, id); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + assert.equal(entry.authType, "apikey"); + assert.equal(entry.authHeader, "bearer"); + assert.equal(entry.baseUrl, chatUrl); + assert.equal(entry.modelsUrl, modelsUrl); + assert.equal(entry.passthroughModels, true); + assert.deepEqual(entry.models, []); + } +}); diff --git a/tests/unit/free-tier-providers-wave3-integration.test.ts b/tests/unit/free-tier-providers-wave3-integration.test.ts new file mode 100644 index 0000000000..ec8ae79cf3 --- /dev/null +++ b/tests/unit/free-tier-providers-wave3-integration.test.ts @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { DefaultExecutor, getExecutor } = await import("../../open-sse/executors/index.ts"); +const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); +const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts"); + +const providers = [ + ["ofoxai", "https://api.ofox.ai/v1/chat/completions"], + ["zerolimitai", "https://www.zerolimitai.com/api/v1/chat/completions"], + ["chatanywhere", "https://api.chatanywhere.org/v1/chat/completions"], + ["helyxai", "https://helyxai.space/v1/chat/completions"], + ["auriko", "https://api.auriko.ai/v1/chat/completions"], + ["poixe-ai", "https://api.poixe.com/v1/chat/completions"], + ["naga-ai", "https://api.naga.ac/v1/chat/completions"], + ["chat-oripe", "https://api.oriper.com/v1/chat/completions"], +] as const; + +for (const [id, endpoint] of providers) { + test(`${id} is wired through registry, metadata, endpoint and default executor`, () => { + const registry = REGISTRY[id]; + const metadata = APIKEY_PROVIDERS[id]; + + assert.ok(registry); + assert.ok(metadata); + assert.equal(registry.id, id); + assert.equal(registry.alias, id); + assert.equal(registry.baseUrl, endpoint); + assert.equal(PROVIDER_ENDPOINTS[id], endpoint); + assert.equal(metadata.id, id); + assert.equal(metadata.alias, id); + assert.equal(metadata.hasFree, true); + assert.equal(metadata.passthroughModels, true); + assert.ok(typeof metadata.freeNote === "string" && metadata.freeNote.length > 0); + assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); + assert.ok(getExecutor(id) instanceof DefaultExecutor); + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + assert.deepEqual(registry.models, []); + }); +} + +test("Wave 3 metadata preserves the audited legal, quota and privacy warnings", () => { + assert.match(APIKEY_PROVIDERS.chatanywhere.freeNote ?? "", /commercial traffic/i); + assert.match(APIKEY_PROVIDERS.zerolimitai.freeNote ?? "", /3 and 7 days/i); + assert.match(APIKEY_PROVIDERS.helyxai.freeNote ?? "", /100,000 tokens\/day/i); + assert.match(APIKEY_PROVIDERS.auriko.freeNote ?? "", /not a free-token pool/i); + assert.match(APIKEY_PROVIDERS["poixe-ai"].freeNote ?? "", /2 RPM\/5 RPD/i); + assert.match(APIKEY_PROVIDERS["naga-ai"].freeNote ?? "", /training/i); + assert.match(APIKEY_PROVIDERS["chat-oripe"].freeNote ?? "", /unconfirmed/i); +}); diff --git a/tests/unit/free-tier-providers-wave4-a.test.ts b/tests/unit/free-tier-providers-wave4-a.test.ts new file mode 100644 index 0000000000..277b9b2778 --- /dev/null +++ b/tests/unit/free-tier-providers-wave4-a.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { freeinferenceProvider } from "../../open-sse/config/providers/registry/freeinference/index.ts"; +import { + DefaultExecutor, + getExecutor, + hasSpecializedExecutor, +} from "../../open-sse/executors/index.ts"; + +test("FreeInference exposes an OpenAI-compatible Bearer registry", () => { + assert.equal(freeinferenceProvider.id, "freeinference"); + assert.equal(freeinferenceProvider.alias, "freeinference"); + assert.equal(freeinferenceProvider.format, "openai"); + assert.equal(freeinferenceProvider.executor, "default"); + assert.equal(freeinferenceProvider.authType, "apikey"); + assert.equal(freeinferenceProvider.authHeader, "bearer"); + assert.equal(freeinferenceProvider.baseUrl, "https://freeinference.org/v1/chat/completions"); + assert.equal(freeinferenceProvider.modelsUrl, "https://freeinference.org/v1/models"); + assert.deepEqual(freeinferenceProvider.models, []); + assert.equal(freeinferenceProvider.passthroughModels, true); +}); + +test("FreeInference uses DefaultExecutor without specialized behavior", () => { + assert.equal(hasSpecializedExecutor("freeinference"), false); + assert.ok(getExecutor("freeinference") instanceof DefaultExecutor); +}); diff --git a/tests/unit/free-tier-providers-wave4-b.test.ts b/tests/unit/free-tier-providers-wave4-b.test.ts new file mode 100644 index 0000000000..a547e150a0 --- /dev/null +++ b/tests/unit/free-tier-providers-wave4-b.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { freeAiProvider } from "../../open-sse/config/providers/registry/free-ai/index.ts"; +import { + DefaultExecutor, + getExecutor, + hasSpecializedExecutor, +} from "../../open-sse/executors/index.ts"; + +test("Free.ai exposes its exact OpenAI-compatible endpoint and live catalog", () => { + assert.equal(freeAiProvider.id, "free-ai"); + assert.equal(freeAiProvider.alias, "free-ai"); + assert.equal(freeAiProvider.format, "openai"); + assert.equal(freeAiProvider.executor, "default"); + assert.equal(freeAiProvider.authType, "apikey"); + assert.equal(freeAiProvider.authHeader, "bearer"); + assert.equal(freeAiProvider.baseUrl, "https://api.free.ai/v1/chat/"); + assert.equal(freeAiProvider.modelsUrl, "https://api.free.ai/v1/models"); + assert.deepEqual(freeAiProvider.models, []); + assert.equal(freeAiProvider.passthroughModels, true); +}); + +test("Free.ai uses DefaultExecutor without a specialized executor", () => { + assert.ok(getExecutor("free-ai") instanceof DefaultExecutor); + assert.equal(hasSpecializedExecutor("free-ai"), false); +}); diff --git a/tests/unit/free-tier-providers-wave4-integration.test.ts b/tests/unit/free-tier-providers-wave4-integration.test.ts new file mode 100644 index 0000000000..743e188271 --- /dev/null +++ b/tests/unit/free-tier-providers-wave4-integration.test.ts @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { DefaultExecutor, getExecutor, hasSpecializedExecutor } = + await import("../../open-sse/executors/index.ts"); +const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts"); +const { isValidModel } = await import("../../src/shared/constants/models.ts"); +const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts"); +const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts"); + +const providers = [ + ["freeinference", "https://freeinference.org/v1/chat/completions"], + ["free-ai", "https://api.free.ai/v1/chat/"], +] as const; + +for (const [id, endpoint] of providers) { + test(`${id} is fully wired without a specialized executor`, () => { + const registry = REGISTRY[id]; + const metadata = APIKEY_PROVIDERS[id]; + + assert.ok(registry); + assert.ok(metadata); + assert.equal(registry.id, id); + assert.equal(registry.alias, id); + assert.equal(registry.baseUrl, endpoint); + assert.equal(PROVIDER_ENDPOINTS[id], endpoint); + assert.equal(metadata.id, id); + assert.equal(metadata.alias, id); + assert.equal(metadata.hasFree, true); + assert.equal(metadata.passthroughModels, true); + assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true); + assert.equal(hasSpecializedExecutor(id), false); + const executor = getExecutor(id); + assert.ok(executor instanceof DefaultExecutor); + assert.equal(executor.buildUrl("live-model", false), endpoint); + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + assert.deepEqual(registry.models, []); + }); +} + +test("Wave 4 model discovery accepts both public catalog response envelopes", () => { + const freeInferenceDiscovery = deriveConfigFromRegistryModelsUrl("freeinference"); + const freeAiDiscovery = deriveConfigFromRegistryModelsUrl("free-ai"); + + assert.ok(freeInferenceDiscovery); + assert.ok(freeAiDiscovery); + assert.deepEqual(freeInferenceDiscovery.parseResponse({ data: [{ id: "glm-5.1" }] }), [ + { id: "glm-5.1" }, + ]); + assert.deepEqual(freeAiDiscovery.parseResponse({ models: [{ id: "qwen7b" }] }), [ + { id: "qwen7b" }, + ]); +}); + +test("Wave 4 metadata preserves approval, logging and overage warnings", () => { + assert.match(APIKEY_PROVIDERS.freeinference.freeNote ?? "", /manual approval/i); + assert.match(APIKEY_PROVIDERS.freeinference.apiHint ?? "", /logging/i); + assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /30,000 tokens\/day/i); + assert.match(APIKEY_PROVIDERS["free-ai"].freeNote ?? "", /premium external models are paid/i); + assert.match(APIKEY_PROVIDERS["free-ai"].apiHint ?? "", /\/v1\/chat\//i); +}); diff --git a/tests/unit/passthrough-provider-aliases.test.ts b/tests/unit/passthrough-provider-aliases.test.ts new file mode 100644 index 0000000000..7ea409663e --- /dev/null +++ b/tests/unit/passthrough-provider-aliases.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +const { isValidModel } = await import("../../src/shared/constants/models.ts"); + +test("passthrough providers accept live model ids through both provider id and alias", () => { + for (const [id, alias] of [ + ["zylo-api", "zylo"], + ["llm-kiwi", "llmkiwi"], + ["freetheai", "fta"], + ] as const) { + assert.equal(isValidModel(id, "future/live-catalog-model"), true); + assert.equal(isValidModel(alias, "future/live-catalog-model"), true); + } +});