diff --git a/README.md b/README.md index 74d8b0e0bac..b64f3dc543f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ -> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **442 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`). +> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **444 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`). -OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.10B in the first month with signup credits, from 34 documented recurring pool keys covering 442 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. +OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.10B in the first month with signup credits, from 34 documented recurring pool keys covering 444 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers. > Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**. > @@ -648,7 +648,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) -> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **442 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md). +> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **444 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
@@ -1307,7 +1307,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi Resilience GuideCircuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing Auto-Combo Engine16-factor scoring, mode packs, self-healing Proxy Guide3-level proxy system, 1proxy marketplace, registry CRUD - Free TiersConsolidated directory: 34 documented recurring pools / 442 cataloged free-tier entries + Free TiersConsolidated directory: 34 documented recurring pools / 444 cataloged free-tier entries Features GalleryVisual dashboard tour with screenshots Codebase DocumentationBeginner-friendly codebase walkthrough diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg index 19e0ca5f16b..f706e4824f5 100644 --- a/docs/diagrams/free-tier-budget.svg +++ b/docs/diagrams/free-tier-budget.svg @@ -1,4 +1,4 @@ - + Pool-deduplicated chart of the 16 recurring free-token pools with positive published budgets (plus Groq's five per-model caps as one segment), plus signup credits and uncapped providers shown separately. @@ -64,7 +64,7 @@ ~1.47B FREE TOKENS / MONTH · STEADY up to ~2.10B in your first month — signup credits - documented free tiers · 34 recurring pools · 442 catalog entries · one endpoint + documented free tiers · 34 recurring pools · 444 catalog entries · one endpoint diff --git a/docs/getting-started/FREE-TIERS-GUIDE.md b/docs/getting-started/FREE-TIERS-GUIDE.md index 804707209d7..f93250470ab 100644 --- a/docs/getting-started/FREE-TIERS-GUIDE.md +++ b/docs/getting-started/FREE-TIERS-GUIDE.md @@ -1,6 +1,6 @@ # Free Tiers Guide: Understand and Combine Free AI Access -> **TL;DR**: OmniRoute registers 352 provider IDs, with **152 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **34 recurring pool keys / 442 entries** (435 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies. +> **TL;DR**: OmniRoute registers 352 provider IDs, with **152 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **34 recurring pool keys / 444 entries** (437 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies. --- @@ -159,13 +159,13 @@ provider's quota or access policy. The live, pool-deduplicated catalog currently reports: -| Metric | Current audited value | Interpretation | -| ---------------------------------------------------- | -----------------------------------------------: | ----------------------------------------------------------------------------------------- | -| Recurring quantified grant | **~1.47B tokens/month** | Shared pools counted once; excludes uncapped providers from the sum | -| First month with signup grants | **~2.10B tokens** | Recurring total plus one-time and recurring credits | -| Audited free-model inventory | **34 recurring pool keys / 442 catalog entries** | 435 active + 7 discontinued; distinct from the 352-provider catalog | -| Recurring/keyless free-forever providers represented | **52** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types | -| Provider catalog entries marked `hasFree` | **152 / 352** | Broader provider metadata; not all have a quantifiable recurring quota | +| Metric | Current audited value | Interpretation | +| ---------------------------------------------------- | -----------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------- | +| Recurring quantified grant | **~1.47B tokens/month** | Shared pools counted once; excludes uncapped providers from the sum | +| First month with signup grants | **~2.10B tokens** | Recurring total plus one-time and recurring credits | +| Audited free-model inventory | **34 recurring pool keys / 444 catalog entries** | 437 active + 7 discontinued; distinct from the 352-provider catalog | +| Recurring/keyless free-forever providers represented | **52** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types, eligibility-gated rows excluded | +| Provider catalog entries marked `hasFree` | **152 / 352** | Broader provider metadata; not all have a quantifiable recurring quota | These values are computed from `open-sse/config/freeModelCatalog.ts`; see the [Free Tiers Reference](../reference/FREE_TIERS.md) for pool deduplication, ToS flags, diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md index 394142ccd28..963bd3e421f 100644 --- a/docs/reference/FREE_TIERS.md +++ b/docs/reference/FREE_TIERS.md @@ -1,7 +1,7 @@ --- title: "Free Tiers & Free-Token Budget" version: 3.8.50 -lastUpdated: 2026-09-02 +lastUpdated: 2026-09-03 --- # Free Tiers & Free-Token Budget @@ -19,6 +19,7 @@ lastUpdated: 2026-09-02 | **+ first month with signup credits** | **~2.10B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. | | **+ permanently free, no published cap** | _un-quantifiable_ | `siliconflow`, `glm-cn` (GLM-4-Flash), `tencent`, `baidu`, `kilo-gateway`, `opencode-zen`, `gemini`, `ollama-cloud` — real recurring access, rate/concurrency-limited, **no token cap to count**. Listed, never summed (counting them at `RPM×24/7` is the inflation we reject). | | **+ deposit-unlock boost** | **+~24M** | A one-time **$10** OpenRouter top-up raises its free pool from 50 → 1000 req/day. Reported separately so it never inflates the steady number. | +| **+ behind a regional identity check** | **+~6M** | `modelscope` (Alibaba Cloud binding + mainland-China real-name verification). Real recurring quota, exposed as `gatedRecurringTokens` / `gatedProviders` and on the dashboard. Never summed into the headline: +~6M behind regional identity verification. | | Theoretical ceiling (all rate limits, 24/7) | ~10B | Sum of every provider rate limit extrapolated to non-stop use. **Not a guarantee** — do not headline this. | **Honest headline:** _OmniRoute aggregates **~1.47B documented free tokens per month** (up to ~2.10B in your first month with signup credits) across 34 free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (15–95% token savings) stretches that further._ @@ -78,10 +79,23 @@ purpose. - Daily token cap → `monthly = daily × 30`. Only RPD documented → `RPD × ~800 output tokens × 30`. Only RPM/TPM (no daily cap) → **uncapped** (see below). - **Permanently free, but no published token cap** (`siliconflow`, `glm-cn`, `tencent`, `baidu`, `kilo-gateway`, `opencode-zen`, `gemini`, `ollama-cloud`): these are real recurring free access, rate/concurrency-limited. We classify them `recurring-uncapped` and **never sum them** — multiplying `RPM × 24/7 × 30d` would produce a fantasy ceiling (the inflation we reject). They are listed so you know they exist. - **Deposit-unlock boost:** a one-time small top-up that permanently raises a free quota (OpenRouter: $10 → 1000 req/day ≈ +24M/mo). Reported as a separate figure, kept out of the steady headline. +- **Eligibility-gated quotas** (`eligibilityGate: "regional-identity"`): a real recurring quota that only opens after a region-bound identity check (mainland-China real-name verification today). Counted with the same pool-dedupe rule into a separate figure (`gatedRecurringTokens`), never into the steady headline. The regime (`freeType`) is unchanged, so routing is unchanged. - **Evidence classes.** The rule: a number in the catalog cites its source in an `// evidence:` comment next to the entry — `public-page` (a provider page anyone can read), `api-public` (an unauthenticated endpoint of the provider, e.g. NaraRouter's public plans endpoint at router.bynara.id), or `console-verified por ` (the figure is only visible inside an account console; the comment records who saw it and when, and the public page that says the cap exists). The state today: the five blocks re-audited on 2026-09-02 carry it (`gemini`, `groq`, `mistral`, `ollama-cloud`, `nara`); entries that predate the 2026-09-02 re-audit inherit the earlier research until they are touched; any **new or changed** number without an evidence comment is a bug. Today only `mistral` is console-verified. --- +## Why our number is smaller than other aggregators' + +Most "free tokens per month" figures in this space are sums of per-model labels. Ours is not, on purpose: + +- **Each shared pool is counted once.** Mistral's free plan is one 1B/month allowance per organization; listing it under five models does not make it 5B. Summed per model, our own catalog would read **~7.4B** (recomputed on 2026-09-03; this figure is not CI-gated — re-measure it whenever the catalog changes) — the headline says **~1.47B** because that is what one account of each provider can actually spend. +- **Daily caps are converted, rates are not.** A documented tokens/day cap becomes `× 30`; a documented requests/day cap becomes `RPD × ~800 tokens × 30`; a provider that only publishes requests-per-minute has **no** monthly figure and is listed as _uncapped_, never summed. Multiplying a rate limit by 24/7 is the inflation we refuse. +- **Quotas behind a regional identity check are shown apart** (`+~6M behind regional identity verification`), because most readers cannot use them. +- **Signup credits are first-month only** and reported as a second figure, never blended into the steady number. +- **The figure is enforced by CI.** `npm run check:docs-counts` recomputes the totals from the catalog and fails the build when this file, the README or the budget card drift from them. + +--- + ## ToS attention table > **ToS flag is advisory, not a routing gate.** Providers marked `tos` are still included in routing and combo/fallback by default; the flag only surfaces on `/dashboard/free-tiers` and `/api/free-tier/summary`. The `excludeTosAvoid` query parameter affects the summary view only, not global routing. The verdict lives in `open-sse/config/freeTierCatalog.ts` (informational, not read by routing engines). diff --git a/docs/screenshots/free-tier-budget-card.svg b/docs/screenshots/free-tier-budget-card.svg index 90ab6b02794..15e11774a90 100644 --- a/docs/screenshots/free-tier-budget-card.svg +++ b/docs/screenshots/free-tier-budget-card.svg @@ -1,94 +1,104 @@ - - - -OmniRoute · /dashboard/free-tiers · preview mockup + + + +OmniRoute · /dashboard/free-tiers · preview mockup Monthly free-token budget -20 free pools · 446 models · one endpoint +21 free pools · 444 models · one endpoint Steady / month -~1.48B +~1.47B First month (+ signup credits) ~2.10B ToS-flagged (you decide) 13 providers - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid. Mistral Large 3 1.00B -GPT-4o mini 150M +Agnes 2.0 Flash 210M -Tencent Hy3 150M +GPT-4o mini 150M -Gemini 2.5 Flash 60M +Llama 3.3 70B 30M -Llama 3.3 70B 30M +Grok-3 24M -Grok-3 24M +GPT-4o 7M -DeepSeek V4 Pro 20M +GPT-OSS 120B 6M -GPT-4o 7M +GPT-OSS 20B 6M -MiniMax-M2.7 6M +GPT-OSS Safeguard 20B 6M -Arcee Trinity Large Prev 5M +Qwen3.6 27B 6M -NavyAI free pool 5M +Qwen3.8 27B 6M -Auto Free 4M +MiniMax-M2.7 6M -Auto 1M +Arcee Trinity Large Prev 5M -Command A Reasoning 800K +NavyAI free pool 5M -ERNIE 4.5 VL 424B A47B B 500K +Auto Free 4M -morph-v3-large 400K +Auto 1M -Llama 3.1 8B 200K +Command A Reasoning 800K -Claude Sonnet 4.5 25K - -+ First month: one-time signup credits (~626M) - -vertex 300M - -agentrouter 200M - -predibase 25M - -together 25M - -glm-cn 20M - -doubao 15M - -ai21 10M - -longcat 10M +ERNIE 4.5 VL 424B A47B B 500K + +morph-v3-large 400K + +Llama 3.1 8B 200K + +Claude Sonnet 4.5 25K + ++ First month: one-time signup credits (~626M) -deepseek 5M - -Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide. -+ 12 permanently-free, no-cap providers (e.g. baidu, glm-cn, opencode-zen) · OpenRouter $10 → +24M/mo. +vertex 300M + +agentrouter 200M + +predibase 25M + +together 25M + +glm-cn 20M + +doubao 15M + +ai21 10M + +longcat 10M + +deepseek 5M + +Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide. ++ 15 permanently-free, no-cap providers (e.g. agnes, ainative, aion) · OpenRouter $10 → +24M/mo. ++ ~6M behind regional identity verification (modelscope) — real quota, never in the headline. diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 08b54e455df..c6b8b133a34 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -287,6 +287,14 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "mistral", modelId: "mistral-small-latest", displayName: "Mistral Small 4", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, { provider: "mistral", modelId: "devstral-latest", displayName: "Devstral 2", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, { provider: "mistral", modelId: "codestral-latest", displayName: "Codestral", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" }, + // evidence: public-page https://modelscope.cn/docs/model-service/API-Inference/limits and + // https://modelscope.cn/docs/magicube/intro (2026-09-02) — API-Inference is free; calls are paid with + // 魔粒: "注册并登录 200 魔粒/日" + "绑定阿里云账号 50 魔粒/日", 1 魔粒 per call on "主流" models ⇒ ~250 calls/day + // ⇒ 250 × 800 × 30 = 6M/month, one balance per account (single pool). + // eligibilityGate: "账号注册后需绑定阿里云账号,并且通过实名认证后才可使用" (Alibaba Cloud binding + mainland + // real-name verification). The docs also call the product "非商业化,非盈利" — hence tos: caution. + { provider: "modelscope", modelId: "Qwen/Qwen3.5-397B-A17B", displayName: "Qwen3.5 397B A17B (ModelScope)", monthlyTokens: 6000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "modelscope-free", tos: "caution", eligibilityGate: "regional-identity" }, + { provider: "modelscope", modelId: "deepseek-ai/DeepSeek-V4-Pro", displayName: "DeepSeek V4 Pro (ModelScope)", monthlyTokens: 6000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "modelscope-free", tos: "caution", eligibilityGate: "regional-identity" }, { provider: "monsterapi", modelId: "llama-3-8b-fuse", displayName: "Llama 3 8B Fuse", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "monsterapi", tos: "ambiguous" }, { provider: "morph", modelId: "morph-v3-large", displayName: "morph-v3-large", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "ok" }, { provider: "morph", modelId: "morph-v3-fast", displayName: "morph-v3-fast", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "ok" }, diff --git a/open-sse/config/freeModelCatalog.ts b/open-sse/config/freeModelCatalog.ts index 041e3934307..aa32a36464d 100644 --- a/open-sse/config/freeModelCatalog.ts +++ b/open-sse/config/freeModelCatalog.ts @@ -11,6 +11,13 @@ export type FreeModelFreeType = | "keyless" | "discontinued"; +/** + * A real, recurring quota that only opens after an identity check tied to a + * region (e.g. 实名认证 with a mainland-China ID). One member today; extend the + * union when a second kind of gate is catalogued. + */ +export type FreeEligibilityGate = "regional-identity"; + export interface FreeModelBudget { provider: string; modelId: string; @@ -40,14 +47,28 @@ export interface FreeModelBudget { * `open-sse/services/autoCombo/strictZeroCostFilter.ts`. */ hardStopGuaranteed?: boolean; + /** + * Set when the quota is real and recurring but only reachable after a + * region-bound identity verification. Affects COUNTING only: the row + * leaves the steady headline and lands in `gatedRecurringTokens`. + * Routing, `isFreeModel` and STRICT_ZERO_COST read `freeType` alone. + * Put the gate's source in a comment next to the entry. + */ + eligibilityGate?: FreeEligibilityGate; } export interface FreeModelTotals { /** Pool-deduped recurring tokens/month — the headline "steady" number. */ steadyRecurringTokens: number; - /** Steady + recurring credit grants (e.g. monthly $-credit plans). */ + /** + * Steady + recurring credit grants (e.g. monthly $-credit plans). + * Eligibility-gated rows contribute nothing, exactly like the steady headline. + */ steadyWithRecurringCreditsTokens: number; - /** Steady + recurring + one-time signup credits — first-month only. */ + /** + * Steady + recurring + one-time signup credits — first-month only. + * Eligibility-gated rows contribute nothing, exactly like the steady headline. + */ firstMonthRealisticTokens: number; /** * Extra recurring tokens/month unlocked by a one-time small deposit @@ -59,8 +80,16 @@ export interface FreeModelTotals { * Providers that are permanently free but publish NO token cap * (rate/concurrency-limited). Real access, but un-quantifiable — listed, * never summed into the headline (avoids the rate-limit×24/7 inflation). + * Eligibility-gated rows are excluded: the list reads as "open to anyone". */ uncappedProviders: string[]; + /** + * Pool-deduped tokens/month behind an eligibility gate (same rule as the + * headline). Never summed into `steadyRecurringTokens`. + */ + gatedRecurringTokens: number; + /** Providers (sorted) contributing to `gatedRecurringTokens`. */ + gatedProviders: string[]; modelCount: number; poolCount: number; perModel: FreeModelBudget[]; @@ -224,41 +253,60 @@ export function computeFreeModelTotals( (m) => !(opts.excludeTosAvoid && m.tos === "avoid") && m.enabled !== false ); + const isGated = (m: FreeModelBudget) => m.eligibilityGate !== undefined; + const steadyRecurringTokens = dedupedSum( models, (m) => m.monthlyTokens, - (m) => STEADY_MONTHLY.has(m.freeType) + (m) => STEADY_MONTHLY.has(m.freeType) && !isGated(m) ); + const gatedRecurringTokens = dedupedSum( + models, + (m) => m.monthlyTokens, + (m) => STEADY_MONTHLY.has(m.freeType) && isGated(m) + ); + const gatedProviders = [ + ...new Set( + models.filter((m) => STEADY_MONTHLY.has(m.freeType) && isGated(m)).map((m) => m.provider) + ), + ].sort(); const recurringCredits = dedupedSum( models, (m) => m.creditTokens, - (m) => RECURRING_CREDIT.has(m.freeType) + (m) => RECURRING_CREDIT.has(m.freeType) && !isGated(m) ); const oneTimeCredits = dedupedSum( models, (m) => m.creditTokens, - (m) => ONE_TIME_CREDIT.has(m.freeType) + (m) => ONE_TIME_CREDIT.has(m.freeType) && !isGated(m) ); const steadyWithRecurringCreditsTokens = steadyRecurringTokens + recurringCredits; const firstMonthRealisticTokens = steadyWithRecurringCreditsTokens + oneTimeCredits; const poolCount = new Set( - models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey) + models + .filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey && !isGated(m)) + .map((m) => m.poolKey) ).size; // Deposit-unlock boost: sum the FREE_TIER_BOOSTS whose pool still has a live // recurring model in the (optionally ToS-filtered) set. const livePools = new Set( - models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey) + models + .filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey && !isGated(m)) + .map((m) => m.poolKey) ); const boostMonthlyTokens = Object.entries(FREE_TIER_BOOSTS) .filter(([pool]) => livePools.has(pool)) .reduce((s, [, b]) => s + b.boostMonthlyTokens, 0); // Permanently-free-but-uncapped providers (real access, no published cap). + // Gated rows are excluded: the list is read as "anyone can use this, forever". const uncappedProviders = [ - ...new Set(models.filter((m) => UNCAPPED.has(m.freeType)).map((m) => m.provider)), + ...new Set( + models.filter((m) => UNCAPPED.has(m.freeType) && !isGated(m)).map((m) => m.provider) + ), ].sort(); return { @@ -267,6 +315,8 @@ export function computeFreeModelTotals( firstMonthRealisticTokens, boostMonthlyTokens, uncappedProviders, + gatedRecurringTokens, + gatedProviders, modelCount: models.length, poolCount, perModel: models.slice().sort((a, b) => b.monthlyTokens - a.monthlyTokens), diff --git a/scripts/check/check-docs-counts-sync.mjs b/scripts/check/check-docs-counts-sync.mjs index 29a42fca2a1..4e74d304647 100644 --- a/scripts/check/check-docs-counts-sync.mjs +++ b/scripts/check/check-docs-counts-sync.mjs @@ -218,12 +218,16 @@ function readCodeFacts() { "const t=computeFreeModelTotals();const cli=Object.values(CLI_TOOLS);", "const by=(c)=>cli.filter(x=>x.category===c).length;", // "Free forever" = every provider whose free access renews or needs no key at all. - // one-time-initial (signup credits) and discontinued pools are excluded on purpose. + // one-time-initial (signup credits) and discontinued pools are excluded on purpose, + // and so is every eligibility-gated row: a provider nobody can sign up for without + // clearing a gate is not "free forever" for the reader of the headline. "const FOREVER=new Set(['recurring-monthly','recurring-daily','recurring-uncapped',", "'recurring-credit','keyless']);", - "const ff=new Set();for(const m of t.perModel)if(FOREVER.has(m.freeType))ff.add(m.provider);", + "const ff=new Set();for(const m of t.perModel)", + "if(FOREVER.has(m.freeType)&&!m.eligibilityGate)ff.add(m.provider);", 'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,entries:t.perModel.length,', - "freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,", + "freeFirst:t.firstMonthRealisticTokens,freeGated:t.gatedRecurringTokens,", + "freePools:t.poolCount,engines:ENGINE_IDS.length,", "cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),", "mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size,freeForever:ff.size,", "modePacks:Object.keys(MODE_PACKS),", @@ -271,6 +275,20 @@ export function extractHeadlineClaims(content) { return claims; } +// The eligibility-gated figure ("+~6M behind regional identity verification") is validated +// with its own anchor so it can neither drift nor be silently dropped once it exists. +const GATED_ANCHOR = /^\s*behind regional identity verification/i; + +export function extractGatedClaims(content) { + const claims = []; + for (const m of content.matchAll(/\+?~?(\d+(?:\.\d+)?)([BM])\b/g)) { + const after = content.slice(m.index + m[0].length, m.index + m[0].length + 60); + if (!GATED_ANCHOR.test(after)) continue; + claims.push({ tokens: Number(m[1]) * (m[2] === "B" ? 1e9 : 1e6), unit: m[2], text: m[0] }); + } + return claims; +} + export function checkFreeTierHeadline(content, totals) { const claims = extractHeadlineClaims(content); if (!claims.length) return { ok: true, detail: "no aggregate free-tier headline in this file" }; @@ -279,14 +297,31 @@ export function checkFreeTierHeadline(content, totals) { const stale = claims.filter( (c) => Math.abs(c.value - steady) >= 0.05 && Math.abs(c.value - first) >= 0.05 ); - if (!stale.length) - return { ok: true, detail: `${claims.length} headline claim(s) match the live catalog` }; - return { - ok: false, - detail: + const problems = []; + if (stale.length) { + problems.push( `stale headline ${[...new Set(stale.map((c) => c.text))].join(", ")} — live catalog ` + - `computes ~${steady.toFixed(2)}B steady / ~${first.toFixed(2)}B first month`, - }; + `computes ~${steady.toFixed(2)}B steady / ~${first.toFixed(2)}B first month` + ); + } + if (totals.g != null && totals.g > 0) { + const gated = extractGatedClaims(content); + const tol = (c) => (c.unit === "B" ? 0.05e9 : 0.5e6); + const gatedStale = gated.filter((c) => Math.abs(c.tokens - totals.g) >= tol(c)); + if (!gated.length) { + problems.push( + `missing gated figure — live catalog computes ${Math.round(totals.g / 1e6)}M behind regional identity verification` + ); + } else if (gatedStale.length) { + problems.push( + `stale gated figure ${[...new Set(gatedStale.map((c) => c.text))].join(", ")} — live catalog ` + + `computes ${Math.round(totals.g / 1e6)}M behind regional identity verification` + ); + } + } + if (!problems.length) + return { ok: true, detail: `${claims.length} headline claim(s) match the live catalog` }; + return { ok: false, detail: problems.join("; ") }; } // PURE: docs prose that names the product version ("OmniRoute v3.8.50 ·", @@ -599,12 +634,12 @@ export function buildChecks() { }, { label: "Free-tier headline (live catalog)", - actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools`, + actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools / ${Math.round(f.freeGated / 1e6)}M gated`, docKey: "free-tier headline", strict: true, files: ["README.md", "docs/reference/FREE_TIERS.md"], validate: (content) => - checkFreeTierHeadline(content, { s: f.freeSteady, m: f.freeFirst }), + checkFreeTierHeadline(content, { s: f.freeSteady, m: f.freeFirst, g: f.freeGated }), }, claim( f.engines, diff --git a/scripts/research/gen-budget-card-svg.mjs b/scripts/research/gen-budget-card-svg.mjs index f253ded074c..f2c01b082ca 100644 --- a/scripts/research/gen-budget-card-svg.mjs +++ b/scripts/research/gen-budget-card-svg.mjs @@ -1,56 +1,81 @@ -// Generates docs/screenshots/free-tier-budget-card.svg from the per-model catalog. -// Run: node scripts/research/gen-budget-card-svg.mjs +#!/usr/bin/env node +// Generates the free-tier budget card from the per-model catalog, through the +// same function the docs gate and the dashboard use — never by parsing the data +// file with a regex (that silently skipped every row carrying an extra field). +// Run from the repo root: +// node --import tsx/esm scripts/research/gen-budget-card-svg.mjs [--out path.svg] import fs from "node:fs"; +import { computeFreeModelTotals } from "../../open-sse/config/freeModelCatalog.ts"; -const txt = fs.readFileSync("open-sse/config/freeModelCatalog.data.ts", "utf8"); -const recs = [ - ...txt.matchAll( - /\{ provider: "([^"]+)", modelId: "([^"]+)", displayName: "([^"]+)", monthlyTokens: (\d+), creditTokens: (\d+), freeType: "([^"]+)", poolKey: (null|"[^"]+"), tos: "([^"]+)" \}/g - ), -].map((m) => ({ - provider: m[1], - modelId: m[2], - displayName: m[3], - monthlyTokens: +m[4], - creditTokens: +m[5], - freeType: m[6], - poolKey: m[7] === "null" ? null : m[7].slice(1, -1), - tos: m[8], -})); +const outIdx = process.argv.indexOf("--out"); +if (outIdx >= 0 && !process.argv[outIdx + 1]) throw new Error("--out requires a path"); +const OUT = outIdx >= 0 ? process.argv[outIdx + 1] : "docs/screenshots/free-tier-budget-card.svg"; +const t = computeFreeModelTotals(); +const STEADY_TYPES = new Set(["recurring-daily", "recurring-monthly", "keyless"]); const fmt = (n) => - n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : n >= 1e6 ? Math.round(n / 1e6) + "M" : Math.round(n / 1e3) + "K"; + n >= 1e9 + ? (n / 1e9).toFixed(2) + "B" + : n >= 1e6 + ? Math.round(n / 1e6) + "M" + : Math.round(n / 1e3) + "K"; +// One bar segment per steady pool (largest member), gated rows excluded like the headline. const poolMap = new Map(); -for (const r of recs) { - if (!["recurring-daily", "recurring-monthly", "keyless"].includes(r.freeType)) continue; +for (const r of t.perModel) { + if (!STEADY_TYPES.has(r.freeType) || r.eligibilityGate) continue; const k = r.poolKey || `${r.provider}:${r.modelId}`; const cur = poolMap.get(k); if (!cur || r.monthlyTokens > cur.monthlyTokens) poolMap.set(k, r); } -const pools = [...poolMap.values()].filter((r) => r.monthlyTokens > 0).sort((a, b) => b.monthlyTokens - a.monthlyTokens); -const steady = pools.reduce((s, r) => s + r.monthlyTokens, 0); +const pools = [...poolMap.values()] + .filter((r) => r.monthlyTokens > 0) + .sort((a, b) => b.monthlyTokens - a.monthlyTokens); +const steady = t.steadyRecurringTokens; +const firstMonth = t.firstMonthRealisticTokens; +const gated = t.gatedRecurringTokens; const otMap = new Map(); -for (const r of recs) { - if (r.freeType !== "one-time-initial" || r.creditTokens <= 0) continue; +for (const r of t.perModel) { + // Gated rows are excluded here too — they are absent from firstMonthRealisticTokens. + if (r.freeType !== "one-time-initial" || r.creditTokens <= 0 || r.eligibilityGate) continue; const k = r.poolKey || r.provider; otMap.set(k, { provider: r.provider, v: Math.max(otMap.get(k)?.v || 0, r.creditTokens) }); } const oneTime = [...otMap.values()].sort((a, b) => b.v - a.v); const oneTimeSum = oneTime.reduce((s, r) => s + r.v, 0); -const firstMonth = steady + oneTimeSum; -const avoidProviders = [...new Set(recs.filter((r) => r.tos === "avoid").map((r) => r.provider))].length; -const uncappedProviders = [...new Set(recs.filter((r) => r.freeType === "recurring-uncapped").map((r) => r.provider))]; +const avoidProviders = new Set(t.perModel.filter((r) => r.tos === "avoid").map((r) => r.provider)) + .size; +const uncappedProviders = t.uncappedProviders; const GRID = pools.slice(0, 28); const STRIP = oneTime.slice(0, 9); -const PAL = ["#6c5ce7","#00b894","#0984e3","#e17055","#fdcb6e","#e84393","#00cec9","#d63031","#a29bfe","#55efc4","#74b9ff","#ffeaa7","#fab1a0","#81ecec"]; +const PAL = [ + "#6c5ce7", + "#00b894", + "#0984e3", + "#e17055", + "#fdcb6e", + "#e84393", + "#00cec9", + "#d63031", + "#a29bfe", + "#55efc4", + "#74b9ff", + "#ffeaa7", + "#fab1a0", + "#81ecec", +]; const color = (i) => PAL[i % PAL.length]; -const cleanName = (r) => (r.displayName || r.provider).replace(/\s*\(.*$/, "").replace(/ —.*$/, "").slice(0, 24); +const cleanName = (r) => + (r.displayName || r.provider) + .replace(/\s*\(.*$/, "") + .replace(/ —.*$/, "") + .slice(0, 24); -// bar segments (min width so every pool shows) -const BAR_X = 32, BAR_W = 836, MIN = 7; +const BAR_X = 32, + BAR_W = 836, + MIN = 7; const extra = BAR_W - MIN * GRID.length; let bx = BAR_X; const segs = GRID.map((r, i) => { @@ -60,11 +85,13 @@ const segs = GRID.map((r, i) => { return s; }); -const B = []; // body elements -// title -B.push(`Monthly free-token budget`); -B.push(`${pools.length} free pools · ${recs.length} models · one endpoint`); -// stats +const B = []; +B.push( + `Monthly free-token budget` +); +B.push( + `${pools.length} free pools · ${t.modelCount} models · one endpoint` +); const stat = (sx, label, val, vc) => { B.push(`${label}`); B.push(`${val}`); @@ -72,50 +99,90 @@ const stat = (sx, label, val, vc) => { stat(32, "Steady / month", `~${fmt(steady)}`, "#e6edf3"); stat(330, "First month (+ signup credits)", `~${fmt(firstMonth)}`, "#3fb950"); stat(700, "ToS-flagged (you decide)", `${avoidProviders} providers`, "#d29922"); -// bar -B.push(``); -B.push(``); -for (const s of segs) B.push(``); +B.push( + `` +); +B.push( + `` +); +for (const s of segs) + B.push( + `` + ); B.push(``); -B.push(`Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid.`); -// model grid 4 cols -const COLS = 4, COLW = 213, GX = 32, GY = 200, RH = 30; +B.push( + `Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid.` +); +const COLS = 4, + COLW = 213, + GX = 32, + GY = 200, + RH = 30; GRID.forEach((r, i) => { - const col = i % COLS, row = (i / COLS) | 0; - const cx = GX + col * COLW, cy = GY + row * RH; + const col = i % COLS, + row = (i / COLS) | 0; + const cx = GX + col * COLW, + cy = GY + row * RH; B.push(``); - B.push(`${cleanName(r)} ${fmt(r.monthlyTokens)}`); + B.push( + `${cleanName(r)} ${fmt(r.monthlyTokens)}` + ); }); let y = GY + Math.ceil(GRID.length / COLS) * RH + 6; -// first-month strip (wrapping) B.push(``); y += 26; -B.push(`+ First month: one-time signup credits (~${fmt(oneTimeSum)})`); +B.push( + `+ First month: one-time signup credits (~${fmt(oneTimeSum)})` +); y += 24; let sxp = 32; for (const r of STRIP) { const label = `${r.provider} ${fmt(r.v)}`; const w = 16 + label.length * 6.7; - if (sxp + w > 862) { sxp = 32; y += 30; } - B.push(``); - B.push(`${label}`); + if (sxp + w > 862) { + sxp = 32; + y += 30; + } + B.push( + `` + ); + B.push( + `${label}` + ); sxp += w + 8; } y += 26; -// ToS note (softened) -B.push(``); -B.push(`Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.`); -B.push(`+ ${uncappedProviders.length} permanently-free, no-cap providers (e.g. ${uncappedProviders.slice(0, 3).join(", ")}) · OpenRouter $10 → +24M/mo.`); -y += 34; -const H = y + 24; // card content bottom +const noteH = gated > 0 ? 48 : 34; +B.push( + `` +); +B.push( + `Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.` +); +B.push( + `+ ${uncappedProviders.length} permanently-free, no-cap providers (e.g. ${uncappedProviders.slice(0, 3).join(", ")}) · OpenRouter $10 → +${fmt(t.boostMonthlyTokens)}/mo.` +); +if (gated > 0) { + B.push( + `+ ~${fmt(gated)} behind regional identity verification (${t.gatedProviders.join(", ")}) — real quota, never in the headline.` + ); +} +y += noteH; +const H = y + 24; const CANVAS = H + 16; const out = []; -out.push(``); +out.push( + `` +); out.push(``); out.push(``); -out.push(`OmniRoute · /dashboard/free-tiers · preview mockup`); +out.push( + `OmniRoute · /dashboard/free-tiers · preview mockup` +); out.push(...B); out.push(``); -fs.writeFileSync("docs/screenshots/free-tier-budget-card.svg", out.join("\n") + "\n"); -console.log(`SVG: ${GRID.length} models, ${STRIP.length} first-month chips, canvas ${CANVAS}px. steady=${fmt(steady)} firstMonth=${fmt(firstMonth)} oneTime=${fmt(oneTimeSum)}`); +fs.writeFileSync(OUT, out.join("\n") + "\n"); +console.log( + `SVG → ${OUT}: ${GRID.length} pools, ${STRIP.length} first-month chips, canvas ${CANVAS}px. steady=${fmt(steady)} firstMonth=${fmt(firstMonth)} gated=${fmt(gated)} oneTime=${fmt(oneTimeSum)}` +); diff --git a/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx b/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx index 4fd8dff8ede..4e5ecaadca3 100644 --- a/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx @@ -33,6 +33,10 @@ export interface FreeBudgetData { boostMonthlyTokens?: number; /** Providers that are permanently free but publish no token cap (rate/concurrency-limited). */ uncappedProviders?: string[]; + /** Pool-deduped tokens/mo behind a regional identity check — real quota, never in the headline. */ + gatedRecurringTokens?: number; + /** Providers behind that check. */ + gatedProviders?: string[]; headline?: string; /** ISO timestamp of the last catalog update. Absent/null → freshness is not shown. */ catalogUpdatedAt?: string | null; @@ -88,6 +92,7 @@ interface FreeBudgetLabels { segmentHint: string; boost: (tokens: string) => string; uncapped: string; + gated: (tokens: string) => string; tosRestricted: (count: number) => string; provider: string; model: string; @@ -111,6 +116,8 @@ const DEFAULT_LABELS: FreeBudgetLabels = { `Unlock ~${tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)`, uncapped: "Permanently free, no published cap (rate-limited) — real access, not counted in the headline:", + gated: (tokens) => + `~${tokens}/mo more behind a regional identity check — real quota, not counted in the headline:`, tosRestricted: (count) => `${count} model${count === 1 ? "" : "s"} flagged as ToS-restricted — you decide`, provider: "Provider", @@ -339,6 +346,8 @@ export function FreeBudgetView({ perModel, boostMonthlyTokens = 0, uncappedProviders = [], + gatedRecurringTokens = 0, + gatedProviders = [], catalogUpdatedAt, noCredentialProviders = [], } = data; @@ -429,9 +438,7 @@ export function FreeBudgetView({ lock_open - - {labels.noApiKey} - + {labels.noApiKey} ({keylessModels.length}个模型 · {keylessProviders.length}个提供者) @@ -475,6 +482,23 @@ export function FreeBudgetView({
)} + {gatedRecurringTokens > 0 && ( +
+ + {labels.gated(fmt(gatedRecurringTokens))} + +
+ {gatedProviders.map((p) => ( + + {p} + + ))} +
+
+ )} {/* ToS-restricted callout */} {avoidModels.length > 0 && ( @@ -669,6 +693,7 @@ export default function FreeBudgetCard() { segmentHint: t("segmentHint"), boost: (tokens) => t("boost", { tokens }), uncapped: t("uncapped"), + gated: (tokens) => t("gated", { tokens }), tosRestricted: (count) => t("tosRestricted", { count }), provider: t("provider"), model: t("model"), diff --git a/src/app/api/free-tier/summary/route.ts b/src/app/api/free-tier/summary/route.ts index 52c38f86736..f391634f53a 100644 --- a/src/app/api/free-tier/summary/route.ts +++ b/src/app/api/free-tier/summary/route.ts @@ -46,6 +46,7 @@ function toBudgetEntry(entry: MergedEntry): FreeModelBudget & { enabled?: boolea poolKey: entry.poolKey, tos: entry.tos, trainsOnPrompts: entry.trainsOnPrompts, + eligibilityGate: entry.eligibilityGate, hardStopGuaranteed: HARD_STOP_BY_KEY.get(`${entry.provider}:${entry.modelId}`), enabled: entry.enabled, }; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index f470c997b5d..de809e4c4d7 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -13133,6 +13133,7 @@ "segmentHint": "كل شريحة = مجمع مجاني واحد · مجمع مفرود من التكرار، عدّ صادق (بدون حدود قصوى مضخمة لمعدل الطلبات).", "boost": "افتح حوالي {tokens} إضافية/شهرياً بشحن رصيد OpenRouter لمرة واحدة بقيمة 10$ (50 ← 1000 طلب/يوم)", "uncapped": "مجاني بشكل دائم، بدون حد أقصى معلن (محدود بمعدل الطلبات) — وصول حقيقي، لا يُحتسب في العنوان الرئيسي:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# نموذج تم وضع علامة عليه كمقيد بشروط الخدمة} other {# نماذج تم وضع علامة عليها كمقيدة بشروط الخدمة}} — القرار لك", "provider": "المزود", "model": "النموذج", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 10cd89ab9d7..3718faf7215 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -13133,6 +13133,7 @@ "segmentHint": "Hər seqment = bir pulsuz hovuz · hovuz üzrə təkrarlanmayan, dürüst sayım (şişirdilmiş sorğu limiti tavanları olmadan).", "boost": "Birdəfəlik $10 OpenRouter balans artımı ilə ayda ~{tokens} daha çox əldə edin (50 → 1000 sorğu/gün)", "uncapped": "Həmişəlik pulsuz, dərc edilmiş limit yoxdur (sorğu sayı məhdudlaşdırılıb) — real giriş, əsas göstəricidə sayılmır:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# model}} ToS ilə məhdudlaşdırılmış kimi qeyd edilib — qərar sizindir", "provider": "Provayder", "model": "Model", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index a46d545e1c7..1368427ffed 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -13133,6 +13133,7 @@ "segmentHint": "Всеки сегмент = един безплатен пул · дедупликиран пул, честно отчитане (без изкуствено завишени тавани на лимитите за скорост).", "boost": "Отключете още ~{tokens}/месец с еднократно допълване от $10 в OpenRouter (50 → 1000 заявки/ден)", "uncapped": "Постоянно безплатно, без публикуван лимит (с ограничение на скоростта) — реален достъп, който не се отчита в заглавието:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# модел е маркиран като ограничен от Условията за ползване — вие решавате} other {# модела са маркирани като ограничени от Условията за ползване — вие решавате}}", "provider": "Доставчик", "model": "Модел", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 20f15cafa2d..110fb37e5b0 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -13133,6 +13133,7 @@ "segmentHint": "প্রতিটি সেগমেন্ট = একটি ফ্রি পুল · পুল-ডিডুপ্লিকেটেড, সঠিক গণনা (কোনো অতিরঞ্জিত রেট-লিমিট সিলিং নেই)।", "boost": "এককালীন $10 OpenRouter টপ-আপের মাধ্যমে প্রতি মাসে আরও ~{tokens} আনলক করুন (50 → 1000 req/day)", "uncapped": "স্থায়ীভাবে বিনামূল্যে, কোনো প্রকাশিত সীমা নেই (রেট-সীমিত) — প্রকৃত অ্যাক্সেস, হেডলাইনে গণনা করা হয়নি:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {#টি মডেল} other {#টি মডেল}} ToS-সীমাবদ্ধ হিসেবে চিহ্নিত — সিদ্ধান্ত আপনার", "provider": "প্রোভাইডার", "model": "মডেল", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 427e6d2ef6e..d82e76ce115 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -13133,6 +13133,7 @@ "segmentHint": "Každý segment = jeden bezplatný pool · deduplikovaný pool, poctivé počítání (žádné uměle navýšené stropy limitů).", "boost": "Odemkněte o ~{tokens} více/měs. jednorázovým dobitím $10 na OpenRouteru (50 → 1000 požadavků/den)", "uncapped": "Trvale zdarma, bez zveřejněného limitu (omezená rychlost) — reálný přístup, nepočítá se do hlavního přehledu:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model označený jako omezený ToS} few {# modely označené jako omezené ToS} other {# modelů označených jako omezené ToS}} — rozhodnutí je na vás", "provider": "Poskytovatel", "model": "Model", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index fa3c227a467..830f5861216 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -13133,6 +13133,7 @@ "segmentHint": "Hvert segment = én gratis pulje · pulje-dedupliceret, ærlig optælling (ingen oppustede hastighedsgrænselofter).", "boost": "Lås op for ~{tokens} mere/md. med en engangsoptankning på $10 hos OpenRouter (50 → 1000 anm./dag)", "uncapped": "Permanent gratis, intet offentliggjort loft (hastighedsbegrænset) — reel adgang, ikke talt med i overskriften:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# modeller}} markeret som ToS-begrænset — du bestemmer", "provider": "Udbyder", "model": "Model", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 16e1370d916..ce9cb745a80 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -13140,6 +13140,7 @@ "segmentHint": "Jedes Segment = ein kostenloser Pool · Pool-dedupliziert, ehrliche Zählung (keine künstlich erhöhten Rate-Limit-Obergrenzen).", "boost": "Schalten Sie ~{tokens} mehr/Monat mit einer einmaligen $10 OpenRouter-Aufladung frei (50 → 1000 Anfr./Tag)", "uncapped": "Dauerhaft kostenlos, kein veröffentlichtes Limit (ratenbegrenzt) — echter Zugang, nicht in der Überschrift gezählt:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# Modell} other {# Modelle}} als ToS-eingeschränkt markiert — Sie entscheiden", "provider": "Anbieter", "model": "Modell", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 163a1fe5818..1216c8d20c6 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -13143,6 +13143,7 @@ "segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).", "boost": "Unlock ~{tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)", "uncapped": "Permanently free, no published cap (rate-limited) — real access, not counted in the headline:", + "gated": "~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide", "provider": "Provider", "model": "Model", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 73dd31e3b84..64e6da527eb 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -13133,6 +13133,7 @@ "segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).", "boost": "Unlock ~{tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)", "uncapped": "Permanently free, no published cap (rate-limited) — real access, not counted in the headline:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide", "provider": "Provider", "model": "Model", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index f6507ed49e5..b963180f7e3 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -13133,6 +13133,7 @@ "segmentHint": "هر بخش = یک استخر رایگان · حذف تکرار استخر، شمارش واقعی (بدون سقف‌های محدودیت نرخ کاذب).", "boost": "با یک‌بار شارژ ۱۰ دلاری OpenRouter، حدود ~{tokens} بیشتر در ماه آزاد کنید (۵۰ → ۱۰۰۰ درخواست/روز)", "uncapped": "دائماً رایگان، بدون سقف اعلام‌شده (دارای محدودیت نرخ) — دسترسی واقعی، بدون احتساب در عنوان اصلی:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# مدل} other {# مدل}} دارای محدودیت ToS علامت‌گذاری شده‌اند — تصمیم با شماست", "provider": "ارائه‌دهنده", "model": "مدل", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 253e58687f7..1457083c5f0 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -13133,6 +13133,7 @@ "segmentHint": "Jokainen segmentti = yksi ilmainen pooli · poolikohtaisesti duplikaatit poistettu, rehellinen laskenta (ei paisutettuja nopeusrajoituskattoja).", "boost": "Avaa ~{tokens} lisää/kk kertaluonteisella 10 dollarin OpenRouter-latauksella (50 → 1000 pyyntöä/päivä)", "uncapped": "Pysyvästi ilmainen, ei julkaistua ylärajaa (nopeusrajoitettu) — todellinen käyttöoikeus, ei lasketa pääotsikkoon:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# malli} other {# mallia}} merkitty käyttöehtojen vastaiseksi — sinä päätät", "provider": "Tarjoaja", "model": "Malli", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 9870e03616e..57cdb707247 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -13133,6 +13133,7 @@ "segmentHint": "Chaque segment = un pool gratuit · dédoublonné par pool, décompte honnête (sans plafonds de limite de débit gonflés).", "boost": "Débloquez ~{tokens} de plus/mois avec une recharge unique de 10 $ sur OpenRouter (50 → 1000 req/jour)", "uncapped": "Gratuit en permanence, sans plafond publié (limité en débit) — accès réel, non comptabilisé dans le total :", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modèle} other {# modèles}} signalés comme restreints par les CGU — à vous de décider", "provider": "Fournisseur", "model": "Modèle", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index e6e20f58c22..789d383b95e 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -13133,6 +13133,7 @@ "segmentHint": "દરેક સેગમેન્ટ = એક મફત પૂલ · પૂલ-ડિડુપ્લિકેટ, પ્રમાણિક ગણતરી (કોઈ ફૂલેલી રેટ-મર્યાદા સીમાઓ નહીં).", "boost": "એક વખતના $10 OpenRouter ટોપ-અપ સાથે દર મહિને ~{tokens} વધુ અનલૉક કરો (50 → 1000 req/day)", "uncapped": "કાયમી ધોરણે મફત, કોઈ પ્રકાશિત મર્યાદા નથી (રેટ-મર્યાદિત) — વાસ્તવિક ઍક્સેસ, હેડલાઇનમાં ગણવામાં આવતી નથી:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# મોડેલ} other {# મોડેલો}} ToS-પ્રતિબંધિત તરીકે ચિહ્નિત થયેલ છે — તમે નક્કી કરો", "provider": "પ્રદાતા", "model": "મોડેલ", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 654be87cc91..ee0139be8f5 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -13133,6 +13133,7 @@ "segmentHint": "כל מקטע = מאגר חינמי אחד · מניעת כפילויות במאגר, ספירה הוגנת (ללא תקרות מגבלת קצב מנופחות).", "boost": "פתחו עוד כ-{tokens}/חודש עם טעינה חד-פעמית של $10 ב-OpenRouter ‏(50 → 1000 בקשות/יום)", "uncapped": "חינם לצמיתות, ללא מגבלה מפורסמת (מוגבל בקצב) — גישה אמיתית, לא נספר בכותרת הראשית:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {מודל #} other {# מודלים}} מסומנים כחסומים לפי תנאי השימוש — ההחלטה בידיך", "provider": "ספק", "model": "מודל", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index f02e4c2e5ce..f465e8d15aa 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -13133,6 +13133,7 @@ "segmentHint": "प्रत्येक सेगमेंट = एक मुफ़्त पूल · पूल-डीडुप्लिकेटेड, सटीक गणना (कोई बढ़ी हुई रेट-लिमिट सीमा नहीं)।", "boost": "एकमुश्त $10 OpenRouter टॉप-अप के साथ ~{tokens} अधिक/माह अनलॉक करें (50 → 1000 req/day)", "uncapped": "स्थायी रूप से मुफ़्त, कोई प्रकाशित सीमा नहीं (रेट-लिमिटेड) — वास्तविक एक्सेस, हेडलाइन में नहीं गिना गया:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# मॉडल} other {# मॉडल}} ToS-प्रतिबंधित के रूप में चिह्नित — आप तय करें", "provider": "प्रदाता", "model": "मॉडल", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 7f30948405a..6fc11601336 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -13133,6 +13133,7 @@ "segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).", "boost": "Oldjon fel további ~{tokens}/hó-t egy egyszeri 10 dolláros OpenRouter feltöltéssel (50 → 1000 req/day)", "uncapped": "Tartósan ingyenes, nincs közzétett korlát (sebességkorlátozott) — valós hozzáférés, nem számít bele a főcímbe:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modell} other {# modell}} ÁSZF-korlátozottként megjelölve — Ön dönt", "provider": "Provider", "model": "Model", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index d92469fca93..8f38d5aa407 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -13133,6 +13133,7 @@ "segmentHint": "Setiap segmen = satu pool gratis · deduplikasi pool, penghitungan jujur (tanpa batas rate-limit yang digelembungkan).", "boost": "Buka ~{tokens} tambahan/bln dengan top-up OpenRouter $10 satu kali (50 → 1000 req/hari)", "uncapped": "Gratis permanen, tanpa batas yang dipublikasikan (dibatasi rate-limit) — akses nyata, tidak dihitung dalam tajuk utama:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# model}} ditandai sebagai dibatasi ToS — Anda yang menentukan", "provider": "Penyedia", "model": "Model", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 0b1d244f02a..5620185626c 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -13133,6 +13133,7 @@ "segmentHint": "Ogni segmento = un pool gratuito · pool deduplicato, conteggio onesto (nessun limite massimo di rate-limit gonfiato).", "boost": "Sblocca ~{tokens} in più al mese con una ricarica una tantum di $10 su OpenRouter (50 → 1000 rich/giorno)", "uncapped": "Permanentemente gratuito, nessun limite pubblicato (soggetto a rate-limit) — accesso reale, non conteggiato nel titolo:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modello contrassegnato come limitato dai ToS} other {# modelli contrassegnati come limitati dai ToS}} — decidi tu", "provider": "Provider", "model": "Modello", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 61f4d19d6e4..7689fa86e3d 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -13133,6 +13133,7 @@ "segmentHint": "各セグメント = 1つの無料プール · プール重複排除、誠実なカウント(誇張されたレート制限上限なし)。", "boost": "1回限りの$10のOpenRouterチャージで、月あたりさらに約{tokens}をアンロック(50 → 1000 リクエスト/日)", "uncapped": "恒久的に無料、公開された上限なし(レート制限あり) — 見出しにはカウントされない、実際のアクセス:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# 個のモデル} other {# 個のモデル}}が利用規約制限としてフラグ立てされています — ご自身で判断してください", "provider": "プロバイダー", "model": "モデル", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index c4b71001dfc..44815927d0f 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -13133,6 +13133,7 @@ "segmentHint": "각 세그먼트 = 하나의 무료 풀 · 풀 중복 제거, 정직한 집계 (부풀려진 요율 제한 한도 없음).", "boost": "일회성 $10 OpenRouter 충전으로 월 약 {tokens}개 추가 잠금 해제 (일일 50 → 1000회 요청)", "uncapped": "영구 무료, 공개된 제한 없음 (요율 제한됨) — 헤드라인에 집계되지 않는 실제 액세스:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {#개 모델} other {#개 모델}}이 이용약관(ToS) 제한으로 표시됨 — 귀하가 결정하세요", "provider": "제공업체", "model": "모델", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index eabefd54647..cd1a59da140 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -13133,6 +13133,7 @@ "segmentHint": "प्रत्येक विभाग = एक मोफत पूल · पूल-डिड्युप केलेले, प्रामाणिक मोजणी (कोणतीही फुगवलेली रेट-लिमिट कमाल मर्यादा नाही).", "boost": "एकवेळच्या $10 OpenRouter टॉप-अपसह आणखी ~{tokens}/महिना अनलॉक करा (50 → 1000 req/day)", "uncapped": "कायमस्वरूपी मोफत, कोणतीही प्रकाशित मर्यादा नाही (रेट-लिमिटेड) — खरा ॲक्सेस, हेडलाइनमध्ये मोजला जात नाही:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# मॉडेल} other {# मॉडेल्स}} ToS-प्रतिबंधित म्हणून चिन्हांकित — तुम्ही ठरवा", "provider": "प्रदाता", "model": "मॉडेल", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 98a8e047783..fa0afa7169e 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -13133,6 +13133,7 @@ "segmentHint": "Setiap segmen = satu kolam percuma · kolam dinyahduplikasi, pengiraan jujur (tiada siling had kadar yang melambung).", "boost": "Nyahkunci ~{tokens} lagi/bln dengan tambah nilai OpenRouter $10 sekali sahaja (50 → 1000 perm/hari)", "uncapped": "Percuma selama-lamanya, tiada had diterbitkan (had kadar dikenakan) — akses sebenar, tidak dikira dalam tajuk utama:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# model}} ditandakan sebagai disekat ToS — anda tentukan", "provider": "Penyedia", "model": "Model", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index dc251e05db0..23ae71bb77c 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -13133,6 +13133,7 @@ "segmentHint": "Elk segment = één gratis pool · pool-ontdubbeld, eerlijke telling (geen opgeblazen rate-limit-plafonds).", "boost": "Ontgrendel ~{tokens} extra/mnd met een eenmalige OpenRouter-opwaardering van $10 (50 → 1000 req/dag)", "uncapped": "Permanent gratis, geen gepubliceerde limiet (rate-limited) — echte toegang, niet meegeteld in de kop:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# modellen}} gemarkeerd als ToS-beperkt — jij bepaalt", "provider": "Provider", "model": "Model", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 0051f48f055..535874aeabd 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -13133,6 +13133,7 @@ "segmentHint": "Hvert segment = én gratis pool · pool-deduplisert, ærlig telling (ingen oppblåste tak for hastighetsbegrensning).", "boost": "Lås opp ~{tokens} mer/mnd med en engangs $10 OpenRouter-påfylling (50 → 1000 forespørsler/dag)", "uncapped": "Permanent gratis, ingen publisert grense (hastighetsbegrenset) — reell tilgang, ikke talt med i overskriften:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modell} other {# modeller}} flagget som ToS-begrenset — du bestemmer", "provider": "Leverandør", "model": "Modell", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 6263e50e01c..fdba4316592 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -13133,6 +13133,7 @@ "segmentHint": "Bawat segment = isang libreng pool · pool-deduped, tapat na pagbibilang (walang pinalobong mga ceiling ng rate-limit).", "boost": "I-unlock ang ~{tokens} pa/buwan gamit ang isang beses na $10 OpenRouter top-up (50 → 1000 req/araw)", "uncapped": "Permanenteng libre, walang nai-publish na limitasyon (rate-limited) — totoong access, hindi binibilang sa headline:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modelo} other {# na modelo}} ang na-flag bilang ToS-restricted — ikaw ang magpasya", "provider": "Provider", "model": "Modelo", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 29fc91d100c..a7ae116765e 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -13133,6 +13133,7 @@ "segmentHint": "Każdy segment = jedna darmowa pula · deduplikacja puli, rzetelne zliczanie (bez zawyżonych limitów zapytań).", "boost": "Odblokuj ~{tokens} więcej/mies. dzięki jednorazowemu doładowaniu OpenRouter za 10 $ (50 → 1000 żądań/dzień)", "uncapped": "Trwale bezpłatne, brak opublikowanego limitu (z ograniczeniem zapytań) — rzeczywisty dostęp, nieuwzględniony w nagłówku:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} few {# modele} many {# modeli} other {# modeli}} oznaczono jako ograniczone przez ToS — Ty decydujesz", "provider": "Dostawca", "model": "Model", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 15b6f6df86a..e2823d77b02 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -13144,6 +13144,7 @@ "segmentHint": "Cada segmento = um pool gratuito · deduplicado por pool, contagem honesta (sem tetos de limite de taxa inflados).", "boost": "Desbloqueie ~{tokens} a mais/mês com uma recarga única de $10 na OpenRouter (50 → 1000 solicitações/dia)", "uncapped": "Permanentemente gratuito, sem teto publicado (limitado por taxa) — acesso real, não contabilizado no total principal:", + "gated": "~{tokens}/mês a mais atrás de verificação de identidade regional (ex.: real-name da China continental) — quota real, fora do headline:", "tosRestricted": "{count, plural, one {# modelo} other {# modelos}} sinalizado(s) como restrito(s) pelos Termos de Serviço — você decide", "provider": "Provedor", "model": "Modelo", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 3ecb98b3539..0b1173571fe 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -13133,6 +13133,7 @@ "segmentHint": "Cada segmento = um pool gratuito · deduplicado por pool, contagem honesta (sem limites de taxa inflacionados).", "boost": "Desbloqueie mais ~{tokens}/mês com um carregamento único de $10 no OpenRouter (50 → 1000 ped/dia)", "uncapped": "Permanentemente gratuito, sem limite publicado (com limite de taxa) — acesso real, não contabilizado no destaque:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modelo assinalado} other {# modelos assinalados}} com restrições de ToS — você decide", "provider": "Fornecedor", "model": "Modelo", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 124b1a610d2..f2a34e7a8a8 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -13133,6 +13133,7 @@ "segmentHint": "Fiecare segment = un pool gratuit · pool deduplicat, contorizare corectă (fără plafoane de limită de rată umflate).", "boost": "Deblochează încă ~{tokens}/lună cu o reîncărcare unică de 10 $ pe OpenRouter (50 → 1000 cereri/zi)", "uncapped": "Permanent gratuit, fără limită publicată (limitat ca rată) — acces real, necontorizat în titlu:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model marcat ca restricționat prin ToS — tu decizi} few {# modele marcate ca restricționate prin ToS — tu decizi} other {# de modele marcate ca restricționate prin ToS — tu decizi}}", "provider": "Furnizor", "model": "Model", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 7b65d8eafa2..9df81a13249 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -13133,6 +13133,7 @@ "segmentHint": "Каждый сегмент = один бесплатный пул · дедупликация пулов, честный подсчет (без завышенных лимитов частоты запросов).", "boost": "Разблокируйте еще ~{tokens}/мес. с помощью разового пополнения OpenRouter на $10 (50 → 1000 запр./день)", "uncapped": "Навсегда бесплатно, без опубликованного лимита (с ограничением частоты) — реальный доступ, не учитывается в заголовке:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# модель помечена как ограниченная Условиями использования} few {# модели помечены как ограниченные Условиями использования} many {# моделей помечено как ограниченные Условиями использования} other {# моделей помечено как ограниченные Условиями использования}} — решать вам", "provider": "Провайдер", "model": "Модель", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 2f8c8143577..a581afc285e 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -13133,6 +13133,7 @@ "segmentHint": "Každý segment = jeden bezplatný pool · deduplikované podľa poolov, poctivé počítanie (žiadne nafúknuté stropy limitov).", "boost": "Odomknite o ~{tokens} viac/mes. jednorazovým dobitím 10 $ na OpenRouter (50 → 1000 požiadaviek/deň)", "uncapped": "Trvalo zadarmo, bez zverejneného limitu (obmedzená rýchlosť) — skutočný prístup, nezapočítaný v hlavnom prehľade:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model označený ako obmedzený podmienkami ToS} few {# modely označené ako obmedzené podmienkami ToS} other {# modelov označených ako obmedzené podmienkami ToS}} — rozhodnutie je na vás", "provider": "Poskytovateľ", "model": "Model", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 73a2cea1ddf..2008856f6bd 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -13133,6 +13133,7 @@ "segmentHint": "Varje segment = en gratispool · pool-deduplicerad, ärlig räkning (inga uppblåsta hastighetsbegränsningstak).", "boost": "Lås upp ~{tokens} fler/mån med en engångspåfyllning på $10 hos OpenRouter (50 → 1000 förfrågn./dag)", "uncapped": "Permanent gratis, inget publicerat tak (hastighetsbegränsad) — verklig åtkomst, räknas inte i rubriken:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# modell} other {# modeller}} flaggad som ToS-begränsad — du bestämmer", "provider": "Leverantör", "model": "Modell", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index d1974ce9454..a004d4c7aac 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -13133,6 +13133,7 @@ "segmentHint": "Kila sehemu = pool moja ya bure · pool isiyo na marudio, hesabu ya uaminifu (hakuna dari zilizoongezwa za kikomo cha kasi).", "boost": "Fungua takriban ~{tokens} zaidi/mwezi kwa kuongeza salio la mara moja la $10 la OpenRouter (maombi 50 → 1000/siku)", "uncapped": "Bure kabisa, hakuna kikomo kilichochapishwa (kasi imedhibitiwa) — ufikiaji halisi, haujahesabiwa kwenye kichwa cha habari:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# mfano} other {# mifano}} imewekewa alama kama iliyozuiliwa na ToS — unaamua", "provider": "Mtoa huduma", "model": "Mfano", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 1d12b14de61..a2f0fb70ee3 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -13133,6 +13133,7 @@ "segmentHint": "ஒவ்வொரு பகுதியும் = ஒரு இலவச பூல் · பூல்-நகல் நீக்கப்பட்டது, நேர்மையான எண்ணிக்கை (அதிகரித்த விகித-வரம்பு உச்சவரம்புகள் இல்லை).", "boost": "ஒரு முறை $10 OpenRouter டாப்-அப் மூலம் மாதத்திற்கு மேலும் ~{tokens} ஐ அன்லாக் செய்யவும் (50 → 1000 கோரிக்கைகள்/நாள்)", "uncapped": "நிரந்தரமாக இலவசம், வெளியிடப்பட்ட வரம்பு இல்லை (விகிதம் வரையறுக்கப்பட்டது) — உண்மையான அணுகல், தலைப்புச் செய்தியில் கணக்கிடப்படவில்லை:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# மாடல்} other {# மாடல்கள்}} சேவை விதிமுறைகள் (ToS) கட்டுப்படுத்தப்பட்டதாகக் குறிக்கப்பட்டுள்ளது — நீங்களே முடிவு செய்யுங்கள்", "provider": "வழங்குநர்", "model": "மாடல்", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index dc5998cee4f..2c1da63d641 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -13133,6 +13133,7 @@ "segmentHint": "ప్రతి విభాగం = ఒక ఉచిత పూల్ · పూల్-డూప్లికేట్ తీసివేసినది, నిజాయితీ గల లెక్కింపు (ఎక్కువ చేసి చూపిన రేట్-పరిమితి గరిష్టాలు లేవు).", "boost": "ఒకేసారి $10 OpenRouter టాప్-అప్‌తో నెలకు మరో ~{tokens} అన్‌లాక్ చేయండి (రోజుకు 50 → 1000 అభ్యర్థనలు)", "uncapped": "శాశ్వతంగా ఉచితం, ప్రచురించిన పరిమితి లేదు (రేట్-పరిమితం చేయబడింది) — నిజమైన యాక్సెస్, హెడ్‌లైన్‌లో లెక్కించబడదు:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# మోడల్} other {# మోడల్స్}} ToS-పరిమితం చేయబడినట్లుగా ఫ్లాగ్ చేయబడ్డాయి — మీరే నిర్ణయించుకోండి", "provider": "ప్రొవైడర్", "model": "మోడల్", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 13b002b810d..227b47ae918 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -13133,6 +13133,7 @@ "segmentHint": "แต่ละส่วน = หนึ่งพูลฟรี · ลดข้อมูลซ้ำในพูล, นับตามจริง (ไม่มีการเพิ่มเพดานจำกัดอัตราการใช้งานเกินจริง)", "boost": "ปลดล็อกเพิ่มอีกประมาณ ~{tokens}/เดือน ด้วยการเติมเงิน OpenRouter $10 ครั้งเดียว (50 → 1000 คำขอ/วัน)", "uncapped": "ฟรีถาวร ไม่มีขีดจำกัดที่เผยแพร่ (จำกัดอัตราการใช้งาน) — เข้าถึงได้จริง ไม่นับรวมในหัวข้อหลัก:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# โมเดล} other {# โมเดล}} ถูกทำเครื่องหมายว่าจำกัดตามข้อกำหนดการให้บริการ — คุณเป็นผู้ตัดสินใจ", "provider": "ผู้ให้บริการ", "model": "โมเดล", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 3b90a96aacd..52aef99d5d6 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -13133,6 +13133,7 @@ "segmentHint": "Her segment = bir ücretsiz havuz · havuzdan tekilleştirilmiş, dürüst sayım (şişirilmiş istek sınırı tavanları yok).", "boost": "Tek seferlik 10 $'lık OpenRouter bakiye yüklemesi ile ayda yaklaşık ~{tokens} daha fazlasının kilidini açın (50 → 1000 istek/gün)", "uncapped": "Kalıcı olarak ücretsiz, yayınlanmış bir sınır yok (istek sınırlı) — gerçek erişim, başlıkta sayılmaz:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# model}} Hizmet Şartları kısıtlamalı olarak işaretlendi — karar sizin", "provider": "Sağlayıcı", "model": "Model", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index a84e6b9b77a..b1bd9160ba5 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -13133,6 +13133,7 @@ "segmentHint": "Кожен сегмент = один безкоштовний пул · дедуплікований пул, чесний підрахунок (без завищених лімітів частоти запитів).", "boost": "Розблокуйте ще ~{tokens}/міс за допомогою одноразового поповнення OpenRouter на $10 (50 → 1000 зап./день)", "uncapped": "Постійно безкоштовно, без опублікованого ліміту (з обмеженням частоти) — реальний доступ, не враховується в заголовку:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# модель позначена як обмежена ToS — рішення за вами} few {# моделі позначені як обмежені ToS — рішення за вами} many {# моделей позначено як обмежені ToS — рішення за вами} other {# моделі позначено як обмежені ToS — рішення за вами}}", "provider": "Провайдер", "model": "Модель", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index fa38b24d3e1..460b225f997 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -13133,6 +13133,7 @@ "segmentHint": "ہر حصہ = ایک مفت پول · پول ڈی ڈپلیکیٹڈ، ایماندارانہ گنتی (بغیر کسی بڑھی ہوئی ریٹ لمٹ کی حد کے)۔", "boost": "ایک بار $10 OpenRouter ٹاپ اپ کے ساتھ مزید ~{tokens}/ماہ ان لاک کریں (50 → 1000 req/day)", "uncapped": "مستقل طور پر مفت، کوئی شائع شدہ حد نہیں (ریٹ لمیٹڈ) — حقیقی رسائی، ہیڈ لائن میں شمار نہیں:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# ماڈل} other {# ماڈلز}} ToS-محدود کے طور پر نشان زد — آپ فیصلہ کریں", "provider": "فراہم کنندہ", "model": "ماڈل", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index cb5dc787d49..685944363a9 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -13144,6 +13144,7 @@ "segmentHint": "Mỗi đoạn là một nhóm miễn phí · đã khử trùng lặp theo nhóm, đếm đúng thực tế (không thổi phồng trần giới hạn tốc độ).", "boost": "Mở khóa thêm khoảng {tokens}/tháng bằng một lần nạp $10 vào OpenRouter (50 → 1000 yêu cầu/ngày)", "uncapped": "Miễn phí vĩnh viễn, không công bố giới hạn (bị giới hạn tốc độ) — quyền truy cập thực, không tính vào tổng nổi bật:", + "gated": "~{tokens}/tháng nữa nằm sau bước xác minh danh tính theo khu vực (ví dụ: xác thực tên thật ở Trung Quốc đại lục) — hạn mức thật, không tính vào con số chính:", "tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide", "provider": "Nhà cung cấp", "model": "Mô hình", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 14413d9130a..d8ae9370b76 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -13133,6 +13133,7 @@ "segmentHint": "每个分段 = 一个免费池 · 池去重,真实统计(无虚高的速率限制上限)。", "boost": "一次性充值 $10 OpenRouter 即可每月多解锁约 ~{tokens}(50 → 1000 次请求/天)", "uncapped": "永久免费,无公开上限(受速率限制)— 实际可用,未计入总览:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# 个模型} other {# 个模型}}被标记为受 ToS 限制 — 由您决定", "provider": "提供者", "model": "模型", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 77932434c9d..c3e316d55a9 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -13133,6 +13133,7 @@ "segmentHint": "每個區段 = 一個免費池 · 池間去重、誠實計數(無膨脹的速率限制上限)。", "boost": "一次性 $10 OpenRouter 充值即可解鎖約 {tokens}/月(50 → 1000 請求/天)", "uncapped": "永久免費,無公佈上限(有速率限制)——真實存取,不計入標題數字:", + "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:", "tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide", "provider": "提供者", "model": "模型", diff --git a/src/lib/radar/applyFeed.ts b/src/lib/radar/applyFeed.ts index 6bfcf3e846e..a041cd9de3a 100644 --- a/src/lib/radar/applyFeed.ts +++ b/src/lib/radar/applyFeed.ts @@ -42,6 +42,8 @@ export interface MergedEntry { poolKey: string | null; tos: "ok" | "caution" | "ambiguous" | "avoid" | "unknown"; trainsOnPrompts?: boolean; + /** Set when the quota only opens after a region-bound identity check; counted apart. */ + eligibilityGate?: "regional-identity"; /** Whether the entry is enabled for use. Defaults to true. */ enabled?: boolean; /** @@ -113,6 +115,8 @@ export interface FeedModel { metadataEvidenceUrls?: string[]; trainsOnPrompts: boolean | null; tosRisk: MergedEntry["tos"]; + /** Absent = the feed does not know; null = explicitly no gate. */ + eligibilityGate?: "regional-identity" | null; setup: { keyUrl: string | null; steps: RadarLocalizedText[]; @@ -299,6 +303,11 @@ function mergeOne( if (!overriddenKeys.has("trainsOnPrompts")) { result.trainsOnPrompts = feed.trainsOnPrompts ?? undefined; } + // Absent means "this feed predates the field": keep whatever the baseline says. + // An explicit null is the feed clearing the gate. + if (!overriddenKeys.has("eligibilityGate") && feed.eligibilityGate !== undefined) { + result.eligibilityGate = feed.eligibilityGate ?? undefined; + } if (!overriddenKeys.has("creditTokens")) { // Feed doesn't have creditTokens; keep baseline } @@ -329,6 +338,7 @@ function mergeOne( if (overrides.poolKey !== undefined) result.poolKey = overrides.poolKey; if (overrides.tos !== undefined) result.tos = overrides.tos; if (overrides.trainsOnPrompts !== undefined) result.trainsOnPrompts = overrides.trainsOnPrompts; + if (overrides.eligibilityGate !== undefined) result.eligibilityGate = overrides.eligibilityGate; if (overrides.enabled !== undefined) result.enabled = overrides.enabled; if (overrides.contextWindow !== undefined) result.contextWindow = overrides.contextWindow; if (overrides.capabilities !== undefined) result.capabilities = overrides.capabilities; @@ -368,6 +378,7 @@ function feedModelToMerged( creditTokens: overrides?.creditTokens ?? 0, freeType: overrides?.freeType ?? feed.freeType, poolKey: overrides?.poolKey ?? feedBudgetToPoolKey(feed.budget), + eligibilityGate: overrides?.eligibilityGate ?? feed.eligibilityGate ?? undefined, tos: overrides?.tos ?? feed.tosRisk, trainsOnPrompts: overrides?.trainsOnPrompts ?? feed.trainsOnPrompts ?? undefined, enabled: feed.enabled ? (overrides?.enabled ?? true) : false, diff --git a/src/lib/radar/feedSchema.ts b/src/lib/radar/feedSchema.ts index e8895f6cc7d..7c2e42ccb9b 100644 --- a/src/lib/radar/feedSchema.ts +++ b/src/lib/radar/feedSchema.ts @@ -182,6 +182,8 @@ const ModelV1Schema = z.object({ capabilities: CapabilitiesV1Schema, trainsOnPrompts: z.boolean().nullable(), tosRisk: TosRiskEnum, + /** Real quota, but behind a regional identity verification (counted apart on the client). */ + eligibilityGate: z.enum(["regional-identity"]).nullable().optional(), setup: SetupSchema, enabled: z.boolean(), }); diff --git a/src/lib/radar/index.ts b/src/lib/radar/index.ts index af0c80d3c9f..d8f2d88cf51 100644 --- a/src/lib/radar/index.ts +++ b/src/lib/radar/index.ts @@ -86,6 +86,7 @@ export function baselineToMergedEntries(budgets: typeof FREE_MODEL_BUDGETS): Mer poolKey: b.poolKey ?? null, tos: b.tos, trainsOnPrompts: b.trainsOnPrompts, + eligibilityGate: b.eligibilityGate, enabled: true, origin: "baseline" as const, })); diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts index 2cea8b52dba..7bafffec8c6 100644 --- a/tests/unit/check-docs-counts-sync.test.ts +++ b/tests/unit/check-docs-counts-sync.test.ts @@ -106,16 +106,20 @@ test("the gate exits 0 against the current (synced) repo state", () => { // down to 1.37B, because no gate watched that number. import { checkFreeTierHeadline, + extractGatedClaims, extractHeadlineClaims, } from "../../scripts/check/check-docs-counts-sync.mjs"; const checkHeadline = checkFreeTierHeadline as ( content: string, - totals: { s: number; m: number; p: number } + totals: { s: number; m: number; p: number; g?: number } ) => { ok: boolean; detail: string }; const extractClaims = extractHeadlineClaims as ( content: string ) => { value: number; text: string }[]; +const extractGated = extractGatedClaims as ( + content: string +) => { tokens: number; unit: "B" | "M"; text: string }[]; const TOTALS = { s: 1_371_725_000, m: 1_998_225_000, p: 39 }; @@ -147,6 +151,45 @@ test("free-tier gate passes when a file carries no headline at all", () => { assert.equal(checkHeadline("no figures here", TOTALS).ok, true); }); +// --- Eligibility-gated bucket ("+~6M behind regional identity verification") -- +// The gated figure sits next to the headline and is validated with its own anchor, +// so it can neither drift nor be silently dropped once the catalog reports one. +const TOTALS_G = { s: 1_503_225_000, m: 2_129_725_000, p: 35, g: 6_000_000 }; + +test("free-tier gate validates the gated figure that sits next to the headline", () => { + const ok = + "~1.5B free tokens per month … +~6M behind regional identity verification (ModelScope)"; + assert.equal(checkHeadline(ok, TOTALS_G).ok, true); + const stale = "~1.5B free tokens per month … +~60M behind regional identity verification"; + assert.equal(checkHeadline(stale, TOTALS_G).ok, false); + assert.match(checkHeadline(stale, TOTALS_G).detail, /gated/); +}); + +test("free-tier gate rejects a file that carries the headline but omits the gated line", () => { + assert.equal(checkHeadline("~1.5B free tokens per month", TOTALS_G).ok, false); + // a file with no headline at all is still fine (per-provider tables, changelogs) + assert.equal(checkHeadline("no figures here", TOTALS_G).ok, true); + // and nothing changes for callers that pass no gated total + assert.equal( + checkHeadline("~1.5B free tokens per month", { s: TOTALS_G.s, m: TOTALS_G.m, p: 35 }).ok, + true + ); +}); + +test("gated claims are read in M or B and need the anchor phrase", () => { + assert.deepEqual(extractGated("~6M of unrelated text"), []); + assert.deepEqual(extractGated("+~6M behind regional identity verification"), [ + { tokens: 6_000_000, unit: "M", text: "+~6M" }, + ]); + assert.equal( + checkHeadline("~1.5B free tokens per month · ~1.2B behind regional identity verification", { + ...TOTALS_G, + g: 1_230_000_000, + }).ok, + true + ); +}); + // --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) -------- // Extends the same drift guard to the counts that silently drifted in v3.8.49: // 11→12 engines, 94→109 MCP tools, 30→33 scopes, 26→33 CLI tools. diff --git a/tests/unit/free-catalog-no-confidence-field.test.ts b/tests/unit/free-catalog-no-confidence-field.test.ts index bb546b16685..39be8d86356 100644 --- a/tests/unit/free-catalog-no-confidence-field.test.ts +++ b/tests/unit/free-catalog-no-confidence-field.test.ts @@ -20,6 +20,9 @@ const here = path.dirname(fileURLToPath(import.meta.url)); const CATALOG_ENTRY_KEYS = [ "creditTokens", "displayName", + // Who may claim a quota (e.g. a regional identity check), not how much the row can + // be trusted — the totals split it into its own gated bucket instead of rating it. + "eligibilityGate", "freeType", "hardStopGuaranteed", "modelId", diff --git a/tests/unit/free-model-catalog-gated-bucket.test.ts b/tests/unit/free-model-catalog-gated-bucket.test.ts new file mode 100644 index 00000000000..73cbd0fea57 --- /dev/null +++ b/tests/unit/free-model-catalog-gated-bucket.test.ts @@ -0,0 +1,165 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + computeFreeModelTotals, + FREE_MODEL_BUDGETS, + type FreeModelBudget, +} from "@omniroute/open-sse/config/freeModelCatalog.ts"; + +/** + * `eligibilityGate` changes COUNTING only: a gated row leaves the steady + * headline (and the pool count) and lands in `gatedRecurringTokens`, + * pool-deduped exactly like the headline. The regime stays what it is. + */ +const base = { creditTokens: 0, tos: "caution" as const }; +const entries: Array = [ + { + provider: "a", + modelId: "a1", + displayName: "A1", + monthlyTokens: 100, + freeType: "recurring-daily", + poolKey: "a-pool", + ...base, + }, + { + provider: "a", + modelId: "a2", + displayName: "A2", + monthlyTokens: 100, + freeType: "recurring-daily", + poolKey: "a-pool", + ...base, + }, + { + provider: "g", + modelId: "g1", + displayName: "G1", + monthlyTokens: 60, + freeType: "recurring-daily", + poolKey: "g-pool", + eligibilityGate: "regional-identity", + ...base, + }, + { + provider: "g", + modelId: "g2", + displayName: "G2", + monthlyTokens: 60, + freeType: "recurring-daily", + poolKey: "g-pool", + eligibilityGate: "regional-identity", + ...base, + }, + { + provider: "h", + modelId: "h1", + displayName: "H1", + monthlyTokens: 7, + freeType: "recurring-monthly", + poolKey: null, + eligibilityGate: "regional-identity", + ...base, + }, + { + provider: "u", + modelId: "u1", + displayName: "U1", + monthlyTokens: 0, + freeType: "recurring-uncapped", + poolKey: "u-pool", + eligibilityGate: "regional-identity", + ...base, + }, +]; + +test("gated entries leave the steady headline and the pool count", () => { + const t = computeFreeModelTotals({ entries }); + assert.equal(t.steadyRecurringTokens, 100); + assert.equal(t.poolCount, 1); + assert.equal(t.firstMonthRealisticTokens, 100); +}); + +test("gated entries are summed apart, pool-deduped, with their providers listed", () => { + const t = computeFreeModelTotals({ entries }); + assert.equal(t.gatedRecurringTokens, 67); // g-pool once (60) + h1 (7); the uncapped u1 adds 0 + assert.deepEqual(t.gatedProviders, ["g", "h"]); +}); + +test("a disabled gated entry contributes nothing", () => { + const t = computeFreeModelTotals({ + entries: entries.map((e) => (e.provider === "h" ? { ...e, enabled: false } : e)), + }); + assert.equal(t.gatedRecurringTokens, 60); + assert.deepEqual(t.gatedProviders, ["g"]); +}); + +test("excludeTosAvoid applies to gated entries too", () => { + const t = computeFreeModelTotals({ + excludeTosAvoid: true, + entries: entries.map((e) => (e.provider === "g" ? { ...e, tos: "avoid" as const } : e)), + }); + assert.equal(t.gatedRecurringTokens, 7); + assert.deepEqual(t.gatedProviders, ["h"]); +}); + +test("a gated signup credit never enters the first-month figure", () => { + const baseline = computeFreeModelTotals({ entries }); + const withGatedCredit = computeFreeModelTotals({ + entries: [ + ...entries, + { + provider: "c", + modelId: "c1", + displayName: "C1", + monthlyTokens: 0, + freeType: "one-time-initial", + poolKey: null, + creditTokens: 1000, + tos: "caution", + eligibilityGate: "regional-identity", + }, + ], + }); + assert.equal( + withGatedCredit.firstMonthRealisticTokens, + baseline.firstMonthRealisticTokens, + "a gated one-time credit must not inflate the first-month headline" + ); + assert.equal( + withGatedCredit.steadyWithRecurringCreditsTokens, + baseline.steadyWithRecurringCreditsTokens + ); +}); + +test("a gated uncapped provider is not advertised as permanently free", () => { + const t = computeFreeModelTotals({ entries }); + // `u` is the gated recurring-uncapped row in the fixture above. + assert.ok(!t.uncappedProviders.includes("u"), "gated rows must stay out of uncappedProviders"); + assert.deepEqual(t.uncappedProviders, []); +}); + +test("the shipped catalog exposes the two new fields", () => { + const t = computeFreeModelTotals(); + assert.equal(typeof t.gatedRecurringTokens, "number"); + assert.ok(Array.isArray(t.gatedProviders)); +}); + +/** + * Invariant: the gated bucket only ever accounts for STEADY tokens. A gated row + * carrying credits would silently drop them from every figure (credits are filtered + * out of the credit sums, and `gatedRecurringTokens` only sums `monthlyTokens`), so + * such a row must not exist in the shipped catalog without the totals growing a + * matching gated-credit figure first. + */ +test("shipped gated rows carry no credit tokens", () => { + for (const m of FREE_MODEL_BUDGETS) { + if (!m.eligibilityGate) continue; + assert.equal( + m.creditTokens, + 0, + `${m.provider}/${m.modelId} is eligibility-gated but declares creditTokens=${m.creditTokens}` + ); + } +}); diff --git a/tests/unit/free-tier-reaudit-2026-09.test.ts b/tests/unit/free-tier-reaudit-2026-09.test.ts index f2276163490..32323bf893b 100644 --- a/tests/unit/free-tier-reaudit-2026-09.test.ts +++ b/tests/unit/free-tier-reaudit-2026-09.test.ts @@ -115,3 +115,37 @@ test("legacy provider-level catalog agrees with the per-model catalog for the re assert.equal(FREE_TIER_BUDGETS.groq, 30_000_000); assert.equal(FREE_TIER_BUDGETS.nara, 210_000_000); }); + +test("modelscope: 250 魔粒/day → 6M/month, one pool, behind mainland real-name verification", () => { + const m = rows("modelscope"); + assert.ok(m.length >= 1); + for (const r of m) { + assert.equal(r.monthlyTokens, 6_000_000, r.modelId); + assert.equal(r.poolKey, "modelscope-free"); + assert.equal(r.freeType, "recurring-daily"); + assert.equal(r.eligibilityGate, "regional-identity"); + assert.equal(r.tos, "caution"); + } + const t = computeFreeModelTotals(); + assert.equal(t.gatedRecurringTokens, 6_000_000); + assert.deepEqual(t.gatedProviders, ["modelscope"]); + assert.ok(!t.uncappedProviders.includes("modelscope")); +}); + +test("a shared pool never mixes gated and ungated entries", () => { + const byPool = new Map>(); + for (const r of FREE_MODEL_BUDGETS) { + if (!r.poolKey) continue; + const gate = r.eligibilityGate ?? "none"; + const seen = byPool.get(r.poolKey) ?? new Set(); + seen.add(gate); + byPool.set(r.poolKey, seen); + } + for (const [poolKey, gates] of byPool) { + assert.equal( + gates.size, + 1, + `pool ${poolKey} mixes eligibility gates: ${[...gates].join(", ")}` + ); + } +}); diff --git a/tests/unit/free-tier-summary-radar-overlay.test.ts b/tests/unit/free-tier-summary-radar-overlay.test.ts index 59090b1c3ca..b63bdab68d3 100644 --- a/tests/unit/free-tier-summary-radar-overlay.test.ts +++ b/tests/unit/free-tier-summary-radar-overlay.test.ts @@ -54,6 +54,7 @@ const STALE_GEN_AT = "2026-01-02T12:00:00.000Z"; const FETCHED_AT = `${FREE_CATALOG_CURATED_AT}T18:00:00.000Z`; const OVERLAY_TOKENS = 1_234_567; const DISABLED_TOKENS = 9_999_999; +const GATED_TOKENS = 4_242_000; async function authCookieHeader(): Promise { const secret = new TextEncoder().encode(process.env.JWT_SECRET); @@ -115,6 +116,34 @@ function feedPayload(tier: "community" | "live") { }; } +function seedGatedCache() { + const payload = feedPayload("community"); + payload.models.push({ + provider: "test-radar", + modelId: "overlay-gated-model", + displayName: "Overlay Gated Model", + familyId: null, + freeType: "recurring-daily", + budget: { kind: "shared_pool", poolId: "overlay-gated", tokensPerMonth: GATED_TOKENS }, + limits: { rpm: null, rpd: null, tpm: null, tpd: null }, + contextWindow: null, + capabilities: { tools: false, vision: false, thinking: false }, + trainsOnPrompts: null, + tosRisk: "ok", + setup: null, + enabled: true, + eligibilityGate: "regional-identity", + } as (typeof payload.models)[number]); + radarDb.setRadarCache({ + version: "2026.08.25.1", + generatedAt: GEN_AT, + tier: "community", + payload: JSON.stringify(payload), + signature: "test-signature-not-verified-on-read", + fetchedAt: FETCHED_AT, + }); +} + function resetState() { core.resetDbInstance(); try { @@ -364,3 +393,25 @@ test("stale overlay => a locally disabled model stays out of the totals", async "a locally disabled model must not be counted when the feed is withheld" ); }); + +// --- eligibility-gated entries from the feed stay out of the headline -------- + +test("a gated overlay entry lands in gatedRecurringTokens, never in the steady headline", async () => { + resetState(); + setFeatureFlagOverride("RADAR_ENABLED", "true"); + seedGatedCache(); + + const body = await getBody(false); + + assert.equal(body.catalogSource, "radar-overlay"); + assert.equal( + body.gatedRecurringTokens, + (computeFreeModelTotals().gatedRecurringTokens as number) + GATED_TOKENS, + "the feed-only gated pool joins the gated bucket on top of the baseline's own" + ); + assert.equal( + body.steadyRecurringTokens, + (computeFreeModelTotals().steadyRecurringTokens as number) + OVERLAY_TOKENS, + "the gated pool must not move the steady headline" + ); +}); diff --git a/tests/unit/gen-budget-card-svg.test.ts b/tests/unit/gen-budget-card-svg.test.ts new file mode 100644 index 00000000000..a976ad3b6d4 --- /dev/null +++ b/tests/unit/gen-budget-card-svg.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { computeFreeModelTotals } from "@omniroute/open-sse/config/freeModelCatalog.ts"; + +const fmt = (n: number) => (n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : Math.round(n / 1e6) + "M"); + +test("the budget card prints the catalog's own totals (no regex-parsed subset)", () => { + const out = path.join(mkdtempSync(path.join(os.tmpdir(), "budget-card-")), "card.svg"); + execFileSync( + process.execPath, + ["--import", "tsx/esm", "scripts/research/gen-budget-card-svg.mjs", "--out", out], + { stdio: "pipe" } + ); + const svg = readFileSync(out, "utf8"); + const t = computeFreeModelTotals(); + assert.ok(svg.includes(`~${fmt(t.steadyRecurringTokens)}`), "steady figure"); + assert.ok(svg.includes(`~${fmt(t.firstMonthRealisticTokens)}`), "first-month figure"); + assert.ok(svg.includes(`${t.uncappedProviders.length} permanently-free`), "uncapped count"); + if (t.gatedRecurringTokens > 0) { + assert.ok(svg.includes("behind regional identity verification"), "gated line"); + } + + // The committed card is a generated artifact: it must be exactly what the + // generator produces today, or the docs ship a stale picture of the totals. + const committed = readFileSync( + path.join(import.meta.dirname, "../../docs/screenshots/free-tier-budget-card.svg"), + "utf8" + ); + assert.equal( + svg, + committed, + "docs/screenshots/free-tier-budget-card.svg is stale — regenerate with " + + "`node --import tsx/esm scripts/research/gen-budget-card-svg.mjs`" + ); +}); + +test("--out without a path fails loudly instead of writing to undefined", () => { + assert.throws(() => + execFileSync( + process.execPath, + ["--import", "tsx/esm", "scripts/research/gen-budget-card-svg.mjs", "--out"], + { stdio: "pipe" } + ) + ); +}); diff --git a/tests/unit/radar-feed-eligibility-gate.test.ts b/tests/unit/radar-feed-eligibility-gate.test.ts new file mode 100644 index 00000000000..e93158079f4 --- /dev/null +++ b/tests/unit/radar-feed-eligibility-gate.test.ts @@ -0,0 +1,128 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { RadarFeedSchema } from "../../src/lib/radar/feedSchema.ts"; +import { applyFeed, type FeedModel, type MergedEntry } from "../../src/lib/radar/applyFeed.ts"; +import { baselineToMergedEntries } from "../../src/lib/radar/index.ts"; + +const fixture = JSON.parse( + readFileSync(new URL("../fixtures/radar-feed-canonical.json", import.meta.url), "utf8") +) as { models: Array> }; + +test("feed schema: eligibilityGate is optional, nullable and closed to unknown values", () => { + const absent = RadarFeedSchema.parse(structuredClone(fixture)); + assert.equal(absent.models[0]!.eligibilityGate, undefined); + const gated = structuredClone(fixture); + gated.models[0]!.eligibilityGate = "regional-identity"; + assert.equal(RadarFeedSchema.parse(gated).models[0]!.eligibilityGate, "regional-identity"); + const nulled = structuredClone(fixture); + nulled.models[0]!.eligibilityGate = null; + assert.equal(RadarFeedSchema.parse(nulled).models[0]!.eligibilityGate, null); + const bad = structuredClone(fixture); + bad.models[0]!.eligibilityGate = "vip"; + assert.throws(() => RadarFeedSchema.parse(bad)); +}); + +function feedModel(overrides: Partial = {}): FeedModel { + return { + provider: "modelscope", + modelId: "Qwen/Qwen3.5-397B-A17B", + displayName: "Qwen3.5 397B A17B (ModelScope)", + familyId: null, + freeType: "recurring-daily", + budget: { kind: "shared_pool", poolId: "modelscope-free", tokensPerMonth: 6_000_000 }, + limits: { rpm: null, rpd: null, tpm: null, tpd: null }, + contextWindow: null, + capabilities: { tools: null, vision: null, thinking: null }, + trainsOnPrompts: null, + tosRisk: "caution", + setup: null, + enabled: true, + ...overrides, + }; +} +const KEY = "modelscope:Qwen/Qwen3.5-397B-A17B"; +const noLocal = () => ({ + localOverrides: new Map>(), + tombstones: new Set(), +}); +const gatedBaseline = () => + baselineToMergedEntries([ + { + provider: "modelscope", + modelId: "Qwen/Qwen3.5-397B-A17B", + displayName: "Qwen3.5 397B A17B (ModelScope)", + monthlyTokens: 6_000_000, + creditTokens: 0, + freeType: "recurring-daily", + poolKey: "modelscope-free", + tos: "caution", + eligibilityGate: "regional-identity", + }, + ]); + +test("baseline entries keep their gate through baselineToMergedEntries", () => { + assert.equal(gatedBaseline()[0]!.eligibilityGate, "regional-identity"); +}); + +test("a feed-only entry carries its gate into the merged catalog", () => { + const [m] = applyFeed({ + baseline: [], + feed: [feedModel({ eligibilityGate: "regional-identity" })], + ...noLocal(), + }); + assert.equal(m!.eligibilityGate, "regional-identity"); + assert.equal(m!.origin, "radar"); +}); + +test("a feed that does not know the field preserves the baseline gate; an explicit null clears it", () => { + const kept = applyFeed({ baseline: gatedBaseline(), feed: [feedModel()], ...noLocal() }); + assert.equal(kept[0]!.eligibilityGate, "regional-identity"); + const cleared = applyFeed({ + baseline: gatedBaseline(), + feed: [feedModel({ eligibilityGate: null })], + ...noLocal(), + }); + assert.equal(cleared[0]!.eligibilityGate, undefined); +}); + +test("a local override on the gate wins over the feed", () => { + const localOverrides = new Map>([ + [KEY, { eligibilityGate: "regional-identity" }], + ]); + const [m] = applyFeed({ + baseline: [], + feed: [feedModel({ eligibilityGate: null })], + localOverrides, + tombstones: new Set(), + }); + assert.equal(m!.eligibilityGate, "regional-identity"); + assert.equal(m!.origin, "local"); +}); + +test("a local override sets the gate even when the feed clears it on a baseline entry", () => { + const ungatedBaseline = baselineToMergedEntries([ + { + provider: "modelscope", + modelId: "Qwen/Qwen3.5-397B-A17B", + displayName: "Qwen3.5 397B A17B (ModelScope)", + monthlyTokens: 6_000_000, + creditTokens: 0, + freeType: "recurring-daily", + poolKey: "modelscope-free", + tos: "caution", + }, + ]); + assert.equal(ungatedBaseline[0]!.eligibilityGate, undefined); + const [m] = applyFeed({ + baseline: ungatedBaseline, + feed: [feedModel({ eligibilityGate: null })], + localOverrides: new Map>([ + [KEY, { eligibilityGate: "regional-identity" }], + ]), + tombstones: new Set(), + }); + assert.equal(m!.eligibilityGate, "regional-identity"); + assert.equal(m!.origin, "local"); +}); diff --git a/tests/unit/ui/free-budget-card-gated.test.tsx b/tests/unit/ui/free-budget-card-gated.test.tsx new file mode 100644 index 00000000000..55be1c908e2 --- /dev/null +++ b/tests/unit/ui/free-budget-card-gated.test.tsx @@ -0,0 +1,77 @@ +// @vitest-environment jsdom +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +const { default: FreeBudgetCard } = + await import("../../../src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard"); + +const summary = { + steadyRecurringTokens: 1_503_225_000, + steadyWithRecurringCreditsTokens: 1_504_225_000, + firstMonthRealisticTokens: 2_129_725_000, + usedThisMonth: 0, + remaining: 1_503_225_000, + modelCount: 453, + poolCount: 35, + perModel: [], + boostMonthlyTokens: 24_000_000, + uncappedProviders: ["gemini"], + gatedRecurringTokens: 6_000_000, + gatedProviders: ["modelscope"], + catalogUpdatedAt: null, + noCredentialProviders: [], +}; + +describe("FreeBudgetCard — eligibility-gated line", () => { + beforeAll(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + }); + + afterEach(() => { + document.body.innerHTML = ""; + vi.unstubAllGlobals(); + }); + + it("renders the gated callout with its providers when gatedRecurringTokens > 0", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(JSON.stringify(summary), { status: 200 })) + ); + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + await act(async () => { + root.render(); + }); + await act(async () => {}); + expect(el.textContent).toContain("gated"); + expect(el.textContent).toContain("modelscope"); + }); + + it("hides the callout when the gated total is zero", async () => { + vi.stubGlobal( + "fetch", + vi.fn( + async () => + new Response( + JSON.stringify({ ...summary, gatedRecurringTokens: 0, gatedProviders: [] }), + { status: 200 } + ) + ) + ); + const el = document.createElement("div"); + document.body.appendChild(el); + await act(async () => { + createRoot(el).render(); + }); + await act(async () => {}); + expect(el.textContent).not.toContain("gated"); + }); +});