diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 7d1ef4de71..78ee514828 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -246,6 +246,8 @@ 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, @@ -494,4 +496,6 @@ export const REGISTRY: Record = { "poixe-ai": poixeAiProvider, "naga-ai": nagaAiProvider, "chat-oripe": chatOripeProvider, + freeinference: freeinferenceProvider, + "free-ai": freeAiProvider, }; diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 935820b9ff..35b3be51d1 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -32,6 +32,8 @@ export const PROVIDER_ENDPOINTS = { "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/providers.ts b/src/shared/constants/providers.ts index 5df4623919..0a2656f16f 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -111,6 +111,8 @@ export const AGGREGATOR_PROVIDER_IDS = new Set([ "poixe-ai", "naga-ai", "chat-oripe", + "freeinference", + "free-ai", ]); export const ENTERPRISE_CLOUD_PROVIDER_IDS = new Set([ diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index ffb64f15c9..f772fa4dad 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -443,6 +443,36 @@ 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/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); +});