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
43 changes: 40 additions & 3 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export type RoutingMode = keyof typeof MODEL_TIERS;
* promise "still free when it returns".
*
* What that check cannot cover, and does not pretend to: a member the catalogue
* does not list at all. Eight of these are in that state on both gateways today,
* does not list at all. Twenty-three of these are in that state on both gateways today,
* and delisting is not death — gpt-oss-120b is the gateway's own free fallback
* and answers for itself while absent from GET /v1/models. The sweep names each
* one as UNVERIFIED rather than counting it as checked; the only way to settle
Expand Down Expand Up @@ -288,6 +288,32 @@ export const FREE_CHAT_MODELS: ReadonlySet<string> = new Set<string>([
"nvidia/nemotron-3.5-lightning", // itself on sol only; super-120b on Base
"nvidia/muse-glimmer-30b", // now listed on both; itself on sol only
"nvidia/gemma-4-31b", // delisted on both; redirected to super-120b (gateway MODEL_REDIRECTS)
// Retired/hidden NVIDIA ids the ACCOUNT rail still prices at $0
// (blockrun.ai/api/pricing, billingMode "free", 2026-10-20) and the wallet
// gateways redirect onto a $0 model (MODEL_REDIRECTS / fallbackModel). All
// fifteen kept here answered 200 with no payment header on BOTH chains on
// 2026-10-20, served by nemotron-3-super-120b, gpt-oss-20b, nano-omni or
// llama-3.2-11b-vision. Not routed — an explicit call just reserves $0
// instead of the $5/$30 default (verify:prices flagged each one).
"nvidia/nemotron-3-super-120b-a12b-free",
"nvidia/qwen3.5-397b-a17b",
"nvidia/nemotron-ultra-253b",
"nvidia/nemotron-super-49b",
"nvidia/deepseek-v3.2",
// NOT nvidia/deepseek-v4-pro or nvidia/deepseek-v4-flash: both are being
// removed from the gateways (blockrun#848 for -flash, live on blockrun.ai;
// -pro approved 2026-10-20) and then answer an unknown-model 400 — a $0
// reserve for an id that no longer exists would only hide that.
"nvidia/nemotron-3-nano-omni-nim",
"nvidia/mistral-large-3-675b",
"nvidia/qwen3-coder-480b",
"nvidia/devstral-2-123b",
"nvidia/glm-4.7",
"nvidia/llama-4-maverick",
"nvidia/qwen3-next-80b-a3b-instruct",
"nvidia/qwen3.5-122b-a10b",
"nvidia/seed-oss-36b",
"nvidia/mistral-small-4-119b",
]);

/**
Expand Down Expand Up @@ -589,19 +615,30 @@ export const GATEWAY_CHARS_PER_TOKEN_OBSERVED = 2.08;
* chat-anthropic.ts already normalised bare -> prefixed for the LEDGER; this is
* the same normalisation for the GATE, which is where the money decision is.
* Post-slash segments are unique across the catalog (pinned by a test), so the
* mapping is unambiguous.
* mapping is unambiguous — with one deliberate exception class: a $0 NVIDIA
* mirror whose bare name is also a PAID model's (nvidia/deepseek-v4-pro vs
* deepseek/deepseek-v4-pro was the case, until the mirror was retired on
* 2026-10-20; none remain). There the paid id must own the bare spelling:
* mapping it to the free mirror would reserve $0 for a paid call, the gate
* bypass this file exists to prevent. Free ids go in FIRST so any paid id
* with the same bare name overwrites them.
*/
const BARE_TO_PREFIXED: Record<string, string> = Object.fromEntries(
[
...FREE_CHAT_MODELS,
...Object.keys(CHAT_PRICE_PER_MTOKEN),
...Object.keys(LONG_CONTEXT_PRICE_PER_MTOKEN),
...Object.values(MODEL_TIERS).flat(),
...FREE_CHAT_MODELS,
]
.filter((id: string) => id.includes("/"))
.map((id: string) => [id.slice(id.indexOf("/") + 1), id]),
);

/** $0 ids whose bare spelling belongs to a paid model (see BARE_TO_PREFIXED). */
export const FREE_SHADOWED_BY_PAID: ReadonlySet<string> = new Set(
[...FREE_CHAT_MODELS].filter((id) => BARE_TO_PREFIXED[id.slice(id.indexOf("/") + 1)] !== id),
);

export function canonicalChatModel(model: string): string {
const id = model.trim();
if (Object.hasOwn(CHAT_PRICE_PER_MTOKEN, id)) return id;
Expand Down
52 changes: 48 additions & 4 deletions test/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { estimateChatCost, freeTierTruncationNote } from "../src/tools/chat.js";
import { handleAnthropicNative, anthropicCallCost } from "../src/tools/chat-anthropic.js";
import { MODEL_TIERS, CHAT_PRICE_PER_MTOKEN, DEFAULT_CHAT_PRICE, FREE_CHAT_MODELS } from "../src/utils/constants.js";
import { MODEL_TIERS, CHAT_PRICE_PER_MTOKEN, DEFAULT_CHAT_PRICE, FREE_CHAT_MODELS, FREE_SHADOWED_BY_PAID } from "../src/utils/constants.js";
import { withTxFee } from "../src/utils/tx-fee.js";
import type { BudgetState } from "../src/types.js";

Expand Down Expand Up @@ -203,8 +203,14 @@ test("every MODEL_TIERS.free entry is in FREE_CHAT_MODELS, and every member rese
}
for (const m of FREE_CHAT_MODELS) {
assert.equal(estimateChatCost(1024, undefined, m, undefined, 600 * 1024), 0, m);
// The bare spelling is a real, chargeable-or-free id too (see BARE_TO_PREFIXED).
assert.equal(estimateChatCost(1024, undefined, m.slice(m.indexOf("/") + 1), undefined, 600 * 1024), 0, `bare ${m}`);
// The bare spelling is a real, chargeable-or-free id too (see BARE_TO_PREFIXED)
// — unless a PAID model owns it, in which case it must reserve that model.
const bare = m.slice(m.indexOf("/") + 1);
if (FREE_SHADOWED_BY_PAID.has(m)) {
assert.ok(estimateChatCost(1024, undefined, bare, undefined, 600 * 1024) > 0, `bare ${m} belongs to a paid model`);
} else {
assert.equal(estimateChatCost(1024, undefined, bare, undefined, 600 * 1024), 0, `bare ${m}`);
}
}
});

Expand Down Expand Up @@ -366,8 +372,46 @@ test("every catalog id has a unique vendor-less segment — the mapping cannot b
if (!byBare.has(bare)) byBare.set(bare, new Set());
byBare.get(bare)!.add(id);
}
const clashes = [...byBare.entries()].filter(([, set]) => set.size > 1);
// The one tolerated clash: a $0 NVIDIA mirror sharing a PAID model's name.
// The paid id owns the bare spelling (FREE_SHADOWED_BY_PAID), so the clash
// can only over-reserve, never reserve $0 for a paid call.
const clashes = [...byBare.entries()].filter(([, set]) => {
if (set.size < 2) return false;
const paid = [...set].filter((id) => !FREE_CHAT_MODELS.has(id));
const free = [...set].filter((id) => FREE_CHAT_MODELS.has(id));
return !(paid.length === 1 && free.every((id) => FREE_SHADOWED_BY_PAID.has(id)));
});
assert.deepEqual(clashes, [], `two vendors ship the same model name: ${JSON.stringify(clashes)}`);
// nvidia/deepseek-v4-pro was the one case until it was retired (2026-10-20).
assert.deepEqual([...FREE_SHADOWED_BY_PAID], []);
});

// The account sheet (blockrun.ai/api/pricing) prices these hidden NVIDIA ids
// at $0 and both wallet gateways answer them unpaid (2026-10-20); before this
// an explicit call reserved the $5/$30 default and an exhausted budget refused it.
test("account-sheet $0 NVIDIA ids reserve nothing, and are not routed", () => {
const ids = [
"nvidia/nemotron-3-super-120b-a12b-free", "nvidia/qwen3.5-397b-a17b", "nvidia/nemotron-ultra-253b",
"nvidia/nemotron-super-49b", "nvidia/deepseek-v3.2",
"nvidia/nemotron-3-nano-omni-nim", "nvidia/mistral-large-3-675b", "nvidia/qwen3-coder-480b",
"nvidia/devstral-2-123b", "nvidia/glm-4.7", "nvidia/llama-4-maverick", "nvidia/qwen3-next-80b-a3b-instruct",
"nvidia/qwen3.5-122b-a10b", "nvidia/seed-oss-36b", "nvidia/mistral-small-4-119b",
];
for (const id of ids) {
assert.equal(estimateChatCost(1024, undefined, id, undefined, 100_000), 0, id);
assert.ok(!(MODEL_TIERS.free as readonly string[]).includes(id), `${id} must not join the routing order`);
}
// Retired from the gateways (unknown-model 400 since 2026-10-20): not free.
for (const gone of ["nvidia/deepseek-v4-pro", "nvidia/deepseek-v4-flash", "deepseek-v4-flash"]) {
assert.ok(!FREE_CHAT_MODELS.has(gone), gone);
assert.ok(estimateChatCost(1024, undefined, gone, undefined, 100_000) > 0, gone);
}
// The bare name of the paid DeepSeek still reserves the paid rate.
assert.equal(
estimateChatCost(1024, undefined, "deepseek-v4-pro", undefined, 100_000),
estimateChatCost(1024, undefined, "deepseek/deepseek-v4-pro", undefined, 100_000),
);
assert.ok(estimateChatCost(1024, undefined, "deepseek-v4-pro", undefined, 100_000) > 0);
});

// ── Two flagship ids landed above the default, and nothing noticed ──
Expand Down
Loading