diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 2ff8afad06..57edc9efc1 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -499,4 +499,34 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "aihorde", modelId: "aphrodite/TheDrummer/Cydonia-24B-v4.3", displayName: "Cydonia 24B (AI Horde)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "aihorde-anon", tos: "ok" }, { provider: "aihorde", modelId: "aphrodite/TheDrummer/Skyfall-31B-v4.2", displayName: "Skyfall 31B (AI Horde)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "aihorde-anon", tos: "ok" }, { provider: "aihorde", modelId: "google/gemma-4-31b", displayName: "Gemma 4 31B (AI Horde)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "aihorde-anon", tos: "ok" }, + { provider: "ainative", modelId: "qwen3-235b-cerebras", displayName: "Qwen3 235B (Cerebras)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "qwen3-32b", displayName: "Qwen3 32B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "qwen3-14b", displayName: "Qwen3 14B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "qwen3-8b", displayName: "Qwen3 8B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "llama-4-maverick", displayName: "Llama 4 Maverick", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "llama3.1-8b-cerebras", displayName: "Llama 3.1 8B (Cerebras)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "deepseek-r1", displayName: "DeepSeek R1", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "nous-coder", displayName: "Nous Coder", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "ainative", modelId: "gemini-flash", displayName: "Gemini Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "ainative-free", tos: "caution" }, + { provider: "aion", modelId: "aion-labs/aion-3.0", displayName: "Aion 3.0", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "aion-free", tos: "ok" }, + { provider: "aion", modelId: "aion-labs/aion-3.0-mini", displayName: "Aion 3.0 Mini", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "aion-free", tos: "ok" }, + { provider: "aion", modelId: "aion-labs/aion-2.5", displayName: "Aion 2.5", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "aion-free", tos: "ok" }, + { provider: "aion", modelId: "aion-labs/aion-2.0", displayName: "Aion 2.0", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "aion-free", tos: "ok" }, + { provider: "aion", modelId: "aion-labs/aion-rp-llama-3.1-8b", displayName: "Aion RP Llama 3.1 8B", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "aion-free", tos: "ok" }, + { provider: "sealion", modelId: "aisingapore/Llama-SEA-LION-v3.5-70B-R", displayName: "Llama SEA-LION v3.5 70B R", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "sealion-free", tos: "ok" }, + { provider: "sealion", modelId: "aisingapore/Llama-SEA-LION-v3-70B-IT", displayName: "Llama SEA-LION v3 70B IT", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "sealion-free", tos: "ok" }, + { provider: "sealion", modelId: "aisingapore/Gemma-SEA-LION-v4-27B-IT", displayName: "Gemma SEA-LION v4 27B IT", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "sealion-free", tos: "ok" }, + { provider: "sealion", modelId: "aisingapore/Qwen-SEA-LION-v4.5-27B-IT", displayName: "Qwen SEA-LION v4.5 27B IT", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "sealion-free", tos: "ok" }, + { provider: "sealion", modelId: "aisingapore/Qwen-SEA-LION-v4-32B-IT", displayName: "Qwen SEA-LION v4 32B IT", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "sealion-free", tos: "ok" }, + { provider: "routeway", modelId: "llama-3.3-70b-instruct:free", displayName: "Llama 3.3 70B Instruct (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "nemotron-3-nano-30b-a3b:free", displayName: "Nemotron 3 Nano 30B (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "nemotron-nano-9b-v2:free", displayName: "Nemotron Nano 9B v2 (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "step-3.7-flash:free", displayName: "Step 3.7 Flash (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "step-3.5-flash:free", displayName: "Step 3.5 Flash (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "laguna-m.1:free", displayName: "Laguna M.1 (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "laguna-xs.2:free", displayName: "Laguna XS.2 (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "routeway", modelId: "llama-3.2-3b-instruct:free", displayName: "Llama 3.2 3B Instruct (free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "routeway-free", tos: "caution" }, + { provider: "nara", modelId: "tencent-hy3", displayName: "Tencent Hy3", monthlyTokens: 150000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "nara-free", tos: "caution" }, + { provider: "nara", modelId: "mistral-large", displayName: "Mistral Large", monthlyTokens: 150000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "nara-free", tos: "caution" }, + { provider: "nara", modelId: "mistral-medium-3-5", displayName: "Mistral Medium 3.5", monthlyTokens: 150000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "nara-free", tos: "caution" }, ]; diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 84b1493e0c..0aa3b960c3 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -127,6 +127,8 @@ import { deepinfraProvider } from "./registry/deepinfra/index.ts"; import { agyProvider } from "./registry/agy/index.ts"; import { agnesProvider } from "./registry/agnes/index.ts"; import { aihordeProvider } from "./registry/aihorde/index.ts"; +import { ainativeProvider } from "./registry/ainative/index.ts"; +import { aionProvider } from "./registry/aion/index.ts"; import { udioProvider } from "./registry/udio/index.ts"; import { longcatProvider } from "./registry/longcat/index.ts"; import { vertex_partnerProvider } from "./registry/vertex/partner/index.ts"; @@ -199,7 +201,10 @@ import { sumopodProvider } from "./registry/sumopod/index.ts"; import { x5labProvider } from "./registry/x5lab/index.ts"; import { kenariProvider } from "./registry/kenari/index.ts"; import { navyProvider } from "./registry/navy/index.ts"; +import { naraProvider } from "./registry/nara/index.ts"; import { requestyProvider } from "./registry/requesty/index.ts"; +import { sealionProvider } from "./registry/sealion/index.ts"; +import { routewayProvider } from "./registry/routeway/index.ts"; import { digitaloceanProvider } from "./registry/digitalocean/index.ts"; import { hcnsecProvider } from "./registry/hcnsec/index.ts"; @@ -329,6 +334,8 @@ export const REGISTRY: Record = { agy: agyProvider, agnes: agnesProvider, aihorde: aihordeProvider, + ainative: ainativeProvider, + aion: aionProvider, udio: udioProvider, longcat: longcatProvider, "vertex-partner": vertex_partnerProvider, @@ -403,7 +410,10 @@ export const REGISTRY: Record = { x5lab: x5labProvider, kenari: kenariProvider, navy: navyProvider, + nara: naraProvider, requesty: requestyProvider, + sealion: sealionProvider, + routeway: routewayProvider, digitalocean: digitaloceanProvider, hcnsec: hcnsecProvider, }; diff --git a/open-sse/config/providers/registry/ainative/index.ts b/open-sse/config/providers/registry/ainative/index.ts new file mode 100644 index 0000000000..564120f42a --- /dev/null +++ b/open-sse/config/providers/registry/ainative/index.ts @@ -0,0 +1,27 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * AINative Studio — OpenAI-compatible aggregator (api.ainative.studio). + * + * The /models catalog is public (84 models, + * verified 2026-07-20) so it is discovered via passthrough; the entries below + * are the free-tier ones and act as a fallback when discovery fails. + */ +export const ainativeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "ainative", + baseUrl: "https://api.ainative.studio/api/v1/chat/completions", + modelsUrl: "https://api.ainative.studio/api/v1/models", + passthroughModels: true, + models: [ + { id: "qwen3-235b-cerebras", name: "Qwen3 235B (Cerebras)", contextLength: 131072, toolCalling: true }, + { id: "qwen3-32b", name: "Qwen3 32B", contextLength: 131072, toolCalling: true }, + { id: "qwen3-14b", name: "Qwen3 14B", contextLength: 131072, toolCalling: true }, + { id: "qwen3-8b", name: "Qwen3 8B", contextLength: 131072, toolCalling: true }, + { id: "llama-4-maverick", name: "Llama 4 Maverick", contextLength: 131072, toolCalling: true }, + { id: "llama3.1-8b-cerebras", name: "Llama 3.1 8B (Cerebras)", contextLength: 131072, toolCalling: true }, + { id: "deepseek-r1", name: "DeepSeek R1", contextLength: 65536, supportsReasoning: true }, + { id: "nous-coder", name: "Nous Coder", contextLength: 131072, toolCalling: true }, + { id: "gemini-flash", name: "Gemini Flash", contextLength: 131072, toolCalling: true }, + ], +}); diff --git a/open-sse/config/providers/registry/aion/index.ts b/open-sse/config/providers/registry/aion/index.ts new file mode 100644 index 0000000000..697f1c4021 --- /dev/null +++ b/open-sse/config/providers/registry/aion/index.ts @@ -0,0 +1,23 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * Aion Labs — OpenAI-compatible aggregator (api.aionlabs.ai). + * + * Free key from aionlabs.ai (no card). The + * public /models catalog (5 models, verified 2026-07-20) carries context and + * pricing; free tier is 20k tokens/day. + */ +export const aionProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "aion", + baseUrl: "https://api.aionlabs.ai/v1/chat/completions", + modelsUrl: "https://api.aionlabs.ai/v1/models", + passthroughModels: true, + models: [ + { id: "aion-labs/aion-3.0", name: "Aion 3.0", contextLength: 131072 }, + { id: "aion-labs/aion-3.0-mini", name: "Aion 3.0 Mini", contextLength: 131072 }, + { id: "aion-labs/aion-2.5", name: "Aion 2.5", contextLength: 131072 }, + { id: "aion-labs/aion-2.0", name: "Aion 2.0", contextLength: 131072 }, + { id: "aion-labs/aion-rp-llama-3.1-8b", name: "Aion RP Llama 3.1 8B", contextLength: 32768 }, + ], +}); diff --git a/open-sse/config/providers/registry/nara/index.ts b/open-sse/config/providers/registry/nara/index.ts new file mode 100644 index 0000000000..e2f840d4e7 --- /dev/null +++ b/open-sse/config/providers/registry/nara/index.ts @@ -0,0 +1,19 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * NaraRouter — OpenAI-compatible aggregator (router.bynara.id). + * + * Free key issued via their Telegram channel. The free tier is a shared + * 5M-tokens/day pool; many models are gated behind + * credit/plan, so only the free-tier models are pinned. + */ +export const naraProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "nara", + baseUrl: "https://router.bynara.id/v1/chat/completions", + models: [ + { id: "tencent-hy3", name: "Tencent Hy3", contextLength: 1000000 }, + { id: "mistral-large", name: "Mistral Large", contextLength: 252000, toolCalling: true }, + { id: "mistral-medium-3-5", name: "Mistral Medium 3.5", contextLength: 256000, toolCalling: true, supportsVision: true }, + ], +}); diff --git a/open-sse/config/providers/registry/routeway/index.ts b/open-sse/config/providers/registry/routeway/index.ts new file mode 100644 index 0000000000..43c426e82e --- /dev/null +++ b/open-sse/config/providers/registry/routeway/index.ts @@ -0,0 +1,28 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * Routeway — OpenAI-compatible aggregator (api.routeway.ai). + * + * Cloudflare fronts the API and rejects + * non-browser User-Agents with error 1010, so a browser-style UA is pinned + * (verified needed 2026-07-20). The public /models catalog has 236 models with + * an `available`/pricing shape; free models carry the `:free` suffix. + */ +export const routewayProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "routeway", + baseUrl: "https://api.routeway.ai/v1/chat/completions", + modelsUrl: "https://api.routeway.ai/v1/models", + passthroughModels: true, + extraHeaders: { "User-Agent": "Mozilla/5.0 OmniRoute/1.0" }, + models: [ + { id: "llama-3.3-70b-instruct:free", name: "Llama 3.3 70B Instruct (free)", contextLength: 131072, toolCalling: true }, + { id: "nemotron-3-nano-30b-a3b:free", name: "Nemotron 3 Nano 30B (free)", contextLength: 256000, toolCalling: true }, + { id: "nemotron-nano-9b-v2:free", name: "Nemotron Nano 9B v2 (free)", contextLength: 128000, toolCalling: true }, + { id: "step-3.7-flash:free", name: "Step 3.7 Flash (free)", contextLength: 256000, toolCalling: true, supportsVision: true }, + { id: "step-3.5-flash:free", name: "Step 3.5 Flash (free)", contextLength: 65536, toolCalling: true }, + { id: "laguna-m.1:free", name: "Laguna M.1 (free)", contextLength: 131072, toolCalling: true }, + { id: "laguna-xs.2:free", name: "Laguna XS.2 (free)", contextLength: 131072, toolCalling: true }, + { id: "llama-3.2-3b-instruct:free", name: "Llama 3.2 3B Instruct (free)", contextLength: 16000, toolCalling: true }, + ], +}); diff --git a/open-sse/config/providers/registry/sealion/index.ts b/open-sse/config/providers/registry/sealion/index.ts new file mode 100644 index 0000000000..4987c3d9d6 --- /dev/null +++ b/open-sse/config/providers/registry/sealion/index.ts @@ -0,0 +1,21 @@ +import type { RegistryEntry } from "../../shared.ts"; +import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts"; + +/** + * SEA-LION (AI Singapore) — OpenAI-compatible first-party API (api.sea-lion.ai). + * + * Free key from sea-lion.ai (Google sign-in, no + * card); recurring free tier at 10 RPM. /models requires the key, so the free + * models are pinned rather than discovered. + */ +export const sealionProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({ + id: "sealion", + baseUrl: "https://api.sea-lion.ai/v1/chat/completions", + models: [ + { id: "aisingapore/Llama-SEA-LION-v3.5-70B-R", name: "Llama SEA-LION v3.5 70B R", contextLength: 131072 }, + { id: "aisingapore/Llama-SEA-LION-v3-70B-IT", name: "Llama SEA-LION v3 70B IT", contextLength: 131072 }, + { id: "aisingapore/Gemma-SEA-LION-v4-27B-IT", name: "Gemma SEA-LION v4 27B IT", contextLength: 131072 }, + { id: "aisingapore/Qwen-SEA-LION-v4.5-27B-IT", name: "Qwen SEA-LION v4.5 27B IT", contextLength: 32768 }, + { id: "aisingapore/Qwen-SEA-LION-v4-32B-IT", name: "Qwen SEA-LION v4 32B IT", contextLength: 32768 }, + ], +}); diff --git a/src/shared/constants/providers/apikey/gateways.ts b/src/shared/constants/providers/apikey/gateways.ts index 8c5e4c4c01..7824944098 100644 --- a/src/shared/constants/providers/apikey/gateways.ts +++ b/src/shared/constants/providers/apikey/gateways.ts @@ -732,4 +732,64 @@ export const APIKEY_PROVIDERS_GATEWAYS = { "OpenAI-compatible endpoint at https://api.navy/v1 with a live /v1/models catalog that exposes " + "per-model token_multiplier and premium flags. Upstream requires an explicit User-Agent header.", }, + ainative: { + id: "ainative", + alias: "ainative", + name: "AINative Studio", + icon: "hub", + color: "#7C3AED", + textIcon: "AN", + passthroughModels: true, + website: "https://ainative.studio", + hasFree: true, + freeNote: "Free tier ~10M tokens/month (claimed) across Qwen3, Llama 4, DeepSeek R1 and more.", + authHint: "Create a free API key at ainative.studio (no card), then paste it here as a Bearer token.", + apiHint: + "OpenAI-compatible endpoint at https://api.ainative.studio/api/v1 with a public /models catalog (84 models). OmniRoute lists models via passthrough.", + }, + aion: { + id: "aion", + alias: "aion", + name: "Aion Labs", + icon: "hub", + color: "#0EA5E9", + textIcon: "AI", + passthroughModels: true, + website: "https://www.aionlabs.ai", + hasFree: true, + freeNote: "Free tier ~20k tokens/day across the Aion reasoning models.", + authHint: "Create a free API key at aionlabs.ai (no card), then paste it here as a Bearer token.", + apiHint: + "OpenAI-compatible endpoint at https://api.aionlabs.ai/v1 with a public /models catalog carrying context and pricing.", + }, + routeway: { + id: "routeway", + alias: "routeway", + name: "Routeway", + icon: "hub", + color: "#F59E0B", + textIcon: "RW", + passthroughModels: true, + website: "https://routeway.ai", + hasFree: true, + freeNote: "Free models (:free suffix) at ~5 RPM / 200 RPD across Llama, Nemotron, Step and Laguna.", + authHint: "Create a free API key at routeway.ai, then paste it here as a Bearer token.", + apiHint: + "OpenAI-compatible endpoint at https://api.routeway.ai/v1 with a public /models catalog (236 models). Cloudflare fronts the API and requires a browser-style User-Agent.", + }, + nara: { + id: "nara", + alias: "nara", + name: "NaraRouter", + icon: "hub", + color: "#EC4899", + textIcon: "NA", + passthroughModels: true, + website: "https://bynara.id", + hasFree: true, + freeNote: "Free tier is a shared 5M tokens/day pool; some models are gated behind credit/plan.", + authHint: "Get a free API key via NaraRouter's Telegram channel, then paste it here as a Bearer token.", + apiHint: + "OpenAI-compatible endpoint at https://router.bynara.id/v1. Free-tier models are pinned; others need credit.", + }, }; diff --git a/src/shared/constants/providers/apikey/regional.ts b/src/shared/constants/providers/apikey/regional.ts index 96c972872a..87a82cfe1a 100644 --- a/src/shared/constants/providers/apikey/regional.ts +++ b/src/shared/constants/providers/apikey/regional.ts @@ -388,4 +388,16 @@ export const APIKEY_PROVIDERS_REGIONAL = { freeNote: "Permanently free API - no credit card required.", authHint: "Get API key at agnes-ai.com", }, + sealion: { + id: "sealion", + alias: "sealion", + name: "SEA-LION", + icon: "public", + color: "#0D9488", + textIcon: "SL", + website: "https://sea-lion.ai", + hasFree: true, + freeNote: "Permanently free at 10 RPM — AI Singapore's Southeast-Asian models (Llama/Qwen/Gemma SEA-LION).", + authHint: "Sign in at sea-lion.ai with Google (no card, no region wall), create an API key, then paste it here.", + }, }; diff --git a/tests/unit/free-providers-batch-2026-07.test.ts b/tests/unit/free-providers-batch-2026-07.test.ts new file mode 100644 index 0000000000..e58c6f5dd2 --- /dev/null +++ b/tests/unit/free-providers-batch-2026-07.test.ts @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + FREE_MODEL_BUDGETS, + computeFreeModelTotals, +} from "@omniroute/open-sse/config/freeModelCatalog.ts"; +import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts"; +import { AI_PROVIDERS } from "@/shared/constants/providers.ts"; + +/** + * Five OpenAI-compatible free-tier aggregators (ainative, aion, sealion, + * routeway, nara). The tests pin the decisions that are easy to break silently. + */ + +const NEW_PROVIDERS = ["ainative", "aion", "sealion", "routeway", "nara"] as const; +const byProvider = (id: string) => FREE_MODEL_BUDGETS.filter((m) => m.provider === id); + +test("each new provider is both routable and canonically registered", () => { + for (const id of NEW_PROVIDERS) { + assert.ok(REGISTRY[id], `${id} must exist in the execution REGISTRY`); + assert.ok(AI_PROVIDERS[id], `${id} must exist as a canonical provider`); + assert.equal(REGISTRY[id].format, "openai"); + assert.ok(byProvider(id).length > 0, `${id} must have free-catalog entries`); + } +}); + +test("routeway pins a browser User-Agent (Cloudflare rejects non-browser UAs with 1010)", () => { + const ua = REGISTRY.routeway.extraHeaders?.["User-Agent"] ?? ""; + assert.match(ua, /Mozilla/, "routeway must send a browser-style User-Agent"); +}); + +test("providers with no published token quota never inflate the headline", () => { + // ainative ("claimed"), aion (per-day request cap), sealion (RPM only), + // routeway (RPD): real free access, no verifiable monthly token figure. + for (const id of ["ainative", "aion", "sealion", "routeway"]) { + const rows = byProvider(id); + assert.ok( + rows.every((m) => m.monthlyTokens === 0 && m.creditTokens === 0), + `${id} has no published token quota — it must not carry invented numbers` + ); + assert.ok(rows.every((m) => m.freeType === "recurring-uncapped")); + } + const totals = computeFreeModelTotals(); + for (const id of ["ainative", "aion", "sealion", "routeway"]) { + assert.ok(totals.uncappedProviders.includes(id), `${id} must surface as uncapped`); + } +}); + +test("nara is a single shared 5M/day pool, counted once", () => { + const rows = byProvider("nara"); + assert.ok(rows.length >= 1); + // 5M tokens/day shared across all models => 150M/month, deduped by poolKey. + assert.ok(rows.every((m) => m.poolKey === "nara-free")); + assert.ok(rows.every((m) => m.monthlyTokens === 150_000_000)); + assert.ok(rows.every((m) => m.freeType === "recurring-daily")); +}); + +test("every catalog provider id still resolves to a canonical provider", () => { + const unknown = [...new Set(FREE_MODEL_BUDGETS.map((m) => m.provider))].filter( + (id) => !AI_PROVIDERS[id] + ); + assert.deepEqual(unknown, [], `dangling catalog providers: ${unknown.join(", ")}`); +});