Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ai-ration",
"version": "0.1.0",
"version": "0.2.0",
"description": "Stay on free LLM tiers: a multi-vendor fallback chain, rate-limit classification that tells the three kinds of 429 apart, and fair-share rationing of a shared daily pool across users.",
"license": "MIT",
"author": "Mao Nakamoto",
Expand Down
45 changes: 45 additions & 0 deletions src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,51 @@ export function freeChain(prefix = "AI"): Provider[] {
];
}

/** What a model id tells us about who pays. */
export type CostVerdict = "free" | "paid" | "unknown";

/**
* Does this model id cost money?
*
* Exists because the same mistake was found in THREE separate apps on one day,
* each a fallback that silently began spending when the free tier ran dry:
*
* anthropic/claude-sonnet-5 a premium model as the fallback
* google/gemini-2.0-flash-001 the paid twin of a `:free` id
* meta-llama/llama-3.3-70b-instruct reads free; bills at 1e-7/token,
* and its `:free` sibling has been
* retired from the catalogue
*
* The decidable rule is narrow and stated as such. A routed id (`vendor/model`,
* the OpenRouter shape) is FREE only with the `:free` suffix, and PAID without
* it — that suffix is the entire difference between free routing and a per-call
* charge for the same weights. A bare id (`llama-3.1-8b-instant`) says nothing:
* whether it costs depends on the account's tier at that vendor, which no string
* can answer, so it returns "unknown" rather than guessing.
*
* Guessing "free" there would be the dangerous direction — it is what let three
* of these through code review.
*/
export function modelCost(id: string): CostVerdict {
const model = id.trim();
if (!model) return "unknown";
// OpenRouter's auto-router across the free catalogue.
if (model === "openrouter/free") return "free";
if (!model.includes("/")) return "unknown";
return model.endsWith(":free") ? "free" : "paid";
}

/**
* Assert every model in a chain is free, for apps that must never bill.
*
* Returns the offending ids rather than throwing: the caller knows whether a
* paid link is a bug or a deliberate, opted-in upgrade, and a library that
* throws on the second case forces people to route around it.
*/
export function paidModelsIn(chain: Provider[]): string[] {
return chain.flatMap((p) => p.models).filter((m) => modelCost(m) === "paid");
}

/**
* The day's total budget: every provider we hold a key for.
*
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ export {
type Provider,
type Env,
type Link,
type CostVerdict,
providerModels,
withEnvPrefix,
freeChain,
modelCost,
paidModelsIn,
dayCapacityTokens,
usableChain,
chainFrom,
Expand Down
37 changes: 37 additions & 0 deletions test/cost.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* `modelCost` exists because one mistake appeared in THREE apps on one day:
* a fallback that silently began spending the moment the free tier ran dry.
* These cases are the real ids that were found in production config.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';

import { modelCost, paidModelsIn, freeChain } from 'ai-ration';

test('the three ids that were actually billing are all caught', () => {
assert.equal(modelCost('anthropic/claude-sonnet-5'), 'paid');
assert.equal(modelCost('google/gemini-2.0-flash-001'), 'paid');
// Reads free, bills at 1e-7/token, and its `:free` sibling has been retired.
assert.equal(modelCost('meta-llama/llama-3.3-70b-instruct'), 'paid');
});

test('the `:free` suffix is the whole difference', () => {
assert.equal(modelCost('openai/gpt-oss-20b:free'), 'free');
assert.equal(modelCost('openai/gpt-oss-20b'), 'paid');
assert.equal(modelCost('openrouter/free'), 'free', 'the free auto-router');
});

test('a bare vendor id is UNKNOWN, never assumed free', () => {
// Whether `llama-3.1-8b-instant` costs depends on the account tier at Groq —
// no string can answer that. Guessing "free" is the direction that let three
// of these through review, so it must not be the default.
assert.equal(modelCost('llama-3.1-8b-instant'), 'unknown');
assert.equal(modelCost('llama-3.3-70b-versatile'), 'unknown');
assert.equal(modelCost(''), 'unknown');
assert.equal(modelCost(' '), 'unknown');
});

test('the shipped free chain contains no paid model', () => {
// The package would have no standing to flag anyone else's chain otherwise.
assert.deepEqual(paidModelsIn(freeChain('TEST')), []);
});