Repository navigation
feat(free-tiers): honest free-token budget catalog, API, headline + research-refreshed docs #3257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e9d60c9
3853f20
d21c39f
7a831e3
e2274b3
6136b87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /** | ||
| * Free-tier monthly token budget catalog. | ||
| * | ||
| * Hand-seeded from the 2026-06-05 per-provider research snapshot. Each value is | ||
| * the UPPER-BOUND of a provider's DOCUMENTED recurring monthly free tokens | ||
| * (explicit daily/monthly token cap, or documented RPD × ~800 tokens × 30). | ||
| * | ||
| * Deliberately EXCLUDED (rate-limit-only, no published token cap — theoretical, | ||
| * not granted): tencent, siliconflow, nvidia, baidu, publicai, sparkdesk. | ||
| * One-time signup credits and discontinued tiers are excluded (do not recur). | ||
| */ | ||
| export type TosVerdict = "ok" | "caution" | "ambiguous" | "avoid" | "unknown"; | ||
|
|
||
| export const FREE_TIER_BUDGETS: Record<string, number> = { | ||
| mistral: 1_000_000_000, | ||
| longcat: 150_000_000, | ||
| "cloudflare-ai": 122_000_000, | ||
| gemini: 60_000_000, | ||
| doubao: 60_000_000, | ||
| cerebras: 30_000_000, | ||
| "api-airforce": 24_000_000, | ||
| "ollama-cloud": 20_000_000, | ||
| "github-models": 18_000_000, | ||
| groq: 15_000_000, | ||
| inclusionai: 15_000_000, | ||
| bluesminds: 7_200_000, | ||
| sambanova: 6_000_000, | ||
| "arcee-ai": 4_800_000, | ||
| llm7: 4_300_000, | ||
| bazaarlink: 3_600_000, | ||
| openrouter: 1_200_000, | ||
| cohere: 800_000, | ||
| huggingchat: 500_000, | ||
| morph: 400_000, | ||
| huggingface: 200_000, | ||
| kiro: 25_000, | ||
| }; | ||
|
|
||
| /** | ||
| * Providers whose terms PROHIBIT routing through a self-hosted proxy or forbid | ||
| * non-personal use. Source: ToS attention table in docs/reference/FREE_TIERS.md. | ||
| */ | ||
| export const FREE_TIER_TOS: Record<string, TosVerdict> = { | ||
| opencode: "avoid", | ||
| "duckduckgo-web": "avoid", | ||
| "gemini-cli": "avoid", | ||
| agy: "avoid", | ||
| kiro: "avoid", | ||
| "amazon-q": "avoid", | ||
| "muse-spark-web": "avoid", | ||
| "t3-web": "avoid", | ||
| "qwen-web": "avoid", | ||
| modal: "avoid", | ||
| nlpcloud: "avoid", | ||
| blackbox: "avoid", | ||
| completions: "avoid", | ||
| fireworks: "avoid", | ||
| "featherless-ai": "avoid", | ||
| friendliai: "avoid", | ||
| ai21: "avoid", | ||
| iflytek: "avoid", | ||
| coze: "avoid", | ||
| }; | ||
|
|
||
| export interface FreeTierTotals { | ||
| documentedMonthlyTokens: number; | ||
| providerCount: number; | ||
| byProvider: Array<{ id: string; monthlyTokens: number; tos: TosVerdict }>; | ||
| headline: string; | ||
| } | ||
|
|
||
| function billions(n: number): string { | ||
| return n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : Math.round(n / 1e6) + "M"; | ||
| } | ||
|
|
||
| /** | ||
| * Sum the documented free-tier budgets. `excludeTosAvoid` drops providers whose | ||
| * terms prohibit proxy use (not usable headroom). | ||
| */ | ||
| export function computeFreeTierTotals( | ||
| opts: { excludeTosAvoid?: boolean } = {} | ||
| ): FreeTierTotals { | ||
| const byProvider = Object.entries(FREE_TIER_BUDGETS) | ||
| .map(([id, monthlyTokens]) => ({ | ||
| id, | ||
| monthlyTokens, | ||
| tos: (FREE_TIER_TOS[id] ?? "caution") as TosVerdict, | ||
|
Check warning on line 87 in open-sse/config/freeTierCatalog.ts
|
||
| })) | ||
| .filter((p) => !(opts.excludeTosAvoid && p.tos === "avoid")) | ||
| .sort((a, b) => b.monthlyTokens - a.monthlyTokens); | ||
|
|
||
| const documentedMonthlyTokens = byProvider.reduce((s, p) => s + p.monthlyTokens, 0); | ||
| return { | ||
| documentedMonthlyTokens, | ||
| providerCount: byProvider.length, | ||
| byProvider, | ||
| headline: `over ${billions(documentedMonthlyTokens)} documented free tokens/month across ${byProvider.length}+ providers`, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||
| import { computeFreeTierTotals } from "@omniroute/open-sse/config/freeTierCatalog.ts"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Omitting the
Suggested change
|
||||||
|
|
||||||
| const CORS = { | ||||||
| "Access-Control-Allow-Origin": "*", | ||||||
| "Access-Control-Allow-Methods": "GET, OPTIONS", | ||||||
| "Access-Control-Allow-Headers": "Content-Type, Authorization", | ||||||
| }; | ||||||
|
|
||||||
| export function OPTIONS(): Response { | ||||||
| return new Response(null, { status: 204, headers: CORS }); | ||||||
| } | ||||||
|
|
||||||
| export function GET(req: Request): Response { | ||||||
| const url = new URL(req.url); | ||||||
| const excludeTosAvoid = url.searchParams.get("excludeTosAvoid") === "1"; | ||||||
| const totals = computeFreeTierTotals({ excludeTosAvoid }); | ||||||
| return new Response(JSON.stringify(totals), { | ||||||
| status: 200, | ||||||
| headers: { "Content-Type": "application/json", ...CORS }, | ||||||
| }); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,19 @@ | ||||||
| import test from "node:test"; | ||||||
| import assert from "node:assert/strict"; | ||||||
| import { getProviderById } from "../../src/shared/constants/providers.ts"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Omitting the
Suggested change
|
||||||
|
|
||||||
| const note = (id: string): string => getProviderById(id)?.freeNote ?? ""; | ||||||
|
|
||||||
| test("kiro freeNote reflects the current 50-credit/month reality + ToS warning", () => { | ||||||
| const n = note("kiro"); | ||||||
| assert.match(n, /50 credits\/month/i); | ||||||
| assert.match(n, /ToS|proxy/i); | ||||||
| }); | ||||||
|
|
||||||
| test("longcat freeNote reflects the post-2026-05-29 5M tokens/day reality", () => { | ||||||
| assert.match(note("longcat"), /5M tokens\/day|LongCat-2\.0/i); | ||||||
| }); | ||||||
|
|
||||||
| test("cerebras freeNote reflects the tightened 30K TPM", () => { | ||||||
| assert.match(note("cerebras"), /30K TPM|1M tokens\/day/i); | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||||||||||||||||||
| import test from "node:test"; | ||||||||||||||||||||||
| import assert from "node:assert/strict"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| FREE_TIER_BUDGETS, | ||||||||||||||||||||||
| FREE_TIER_TOS, | ||||||||||||||||||||||
| computeFreeTierTotals, | ||||||||||||||||||||||
| } from "../../open-sse/config/freeTierCatalog.ts"; | ||||||||||||||||||||||
|
Comment on lines
+3
to
+7
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Omitting the
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("FREE_TIER_BUDGETS holds positive integer monthly-token budgets", () => { | ||||||||||||||||||||||
| assert.ok(Object.keys(FREE_TIER_BUDGETS).length >= 20); | ||||||||||||||||||||||
| for (const [id, tokens] of Object.entries(FREE_TIER_BUDGETS)) { | ||||||||||||||||||||||
| assert.ok(Number.isInteger(tokens) && tokens > 0, `${id} must be a positive integer`); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| assert.equal(FREE_TIER_BUDGETS.mistral, 1_000_000_000); | ||||||||||||||||||||||
| assert.equal(FREE_TIER_BUDGETS.longcat, 150_000_000); | ||||||||||||||||||||||
| assert.equal(FREE_TIER_BUDGETS["cloudflare-ai"], 122_000_000); | ||||||||||||||||||||||
| assert.equal(FREE_TIER_BUDGETS.cerebras, 30_000_000); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("FREE_TIER_TOS marks proxy-prohibited providers as avoid", () => { | ||||||||||||||||||||||
| for (const id of ["kiro", "gemini-cli", "amazon-q", "blackbox", "fireworks"]) { | ||||||||||||||||||||||
| assert.equal(FREE_TIER_TOS[id], "avoid", `${id} must be flagged avoid`); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("computeFreeTierTotals sums the documented budgets", () => { | ||||||||||||||||||||||
| const t = computeFreeTierTotals(); | ||||||||||||||||||||||
| assert.equal(t.providerCount, 22); | ||||||||||||||||||||||
| assert.ok(t.documentedMonthlyTokens >= 1_500_000_000); | ||||||||||||||||||||||
| assert.ok(t.documentedMonthlyTokens <= 1_600_000_000); | ||||||||||||||||||||||
| assert.equal(typeof t.headline, "string"); | ||||||||||||||||||||||
| assert.match(t.headline, /1\.5/); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("computeFreeTierTotals can exclude ToS-avoid providers", () => { | ||||||||||||||||||||||
| const all = computeFreeTierTotals(); | ||||||||||||||||||||||
| const clean = computeFreeTierTotals({ excludeTosAvoid: true }); | ||||||||||||||||||||||
| assert.equal(all.documentedMonthlyTokens - clean.documentedMonthlyTokens, 25_000); | ||||||||||||||||||||||
| assert.equal(clean.providerCount, 21); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { mkdtempSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
|
|
||
| process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-freetier-route-")); | ||
|
|
||
| const { GET } = await import("../../src/app/api/free-tier/summary/route.ts"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| test("GET /api/free-tier/summary returns the documented total and breakdown", async () => { | ||
| const res = await GET(new Request("http://localhost/api/free-tier/summary")); | ||
| assert.equal(res.status, 200); | ||
| const body = await res.json(); | ||
| assert.ok(body.documentedMonthlyTokens >= 1_500_000_000); | ||
| assert.equal(body.providerCount, 22); | ||
| assert.ok(Array.isArray(body.byProvider)); | ||
| assert.match(body.headline, /free tokens\/month/); | ||
| assert.ok(!JSON.stringify(body).includes("at /")); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||
| import test from "node:test"; | ||||||||||
| import assert from "node:assert/strict"; | ||||||||||
| import { OMNIROUTE_RESPONSE_HEADERS } from "../../src/shared/constants/headers.ts"; | ||||||||||
| import { buildOmniRouteResponseMetaHeaders } from "../../src/domain/omnirouteResponseMeta.ts"; | ||||||||||
|
Comment on lines
+3
to
+4
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Omitting the
Suggested change
|
||||||||||
|
|
||||||||||
| test("headers constant exposes the fallback-attempts key", () => { | ||||||||||
| assert.equal( | ||||||||||
| OMNIROUTE_RESPONSE_HEADERS.fallbackAttempts, | ||||||||||
| "X-OmniRoute-Fallback-Attempts" | ||||||||||
| ); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test("buildOmniRouteResponseMetaHeaders emits the fallback-attempts count when > 0", () => { | ||||||||||
| const h = buildOmniRouteResponseMetaHeaders({ model: "gpt", provider: "openai", fallbackAttempts: 2 }); | ||||||||||
| assert.equal(h["X-OmniRoute-Fallback-Attempts"], "2"); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test("buildOmniRouteResponseMetaHeaders omits the header when 0 / absent", () => { | ||||||||||
| const none = buildOmniRouteResponseMetaHeaders({ model: "gpt" }); | ||||||||||
| assert.equal(none["X-OmniRoute-Fallback-Attempts"], undefined); | ||||||||||
| const zero = buildOmniRouteResponseMetaHeaders({ model: "gpt", fallbackAttempts: 0 }); | ||||||||||
| assert.equal(zero["X-OmniRoute-Fallback-Attempts"], undefined); | ||||||||||
| }); | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
billionshelper function rounds any token count below1,000,000to0M(viaMath.round(n / 1e6)). For smaller token budgets (such askirowith25,000tokens ormorphwith400,000tokens), this results in an incorrect or misleading'0M'display if they are ever formatted individually or if the aggregated total falls below500,000. Improving the helper to handle thousands (K) and fractional millions/billions dynamically ensures robust and accurate formatting across all ranges.