Compare commits

..

47 Commits

Author SHA1 Message Date
Aiden Cline 458b7f4d1a use Venice context tier thresholds 2026-05-12 17:39:40 -05:00
Aiden Cline 8f9adc7567 fix generated tier change detection 2026-05-12 17:01:35 -05:00
Aiden Cline a671cc05d5 align cost tiers with model schema 2026-05-12 16:01:49 -05:00
Aiden Cline 2e015de42d preserve generated tier thresholds 2026-05-11 17:07:19 -05:00
Aiden Cline 151e9c9071 fix tiered cost generation 2026-05-11 16:43:32 -05:00
Aiden Cline b96b074a3b fix long-context cost tier omissions 2026-05-11 16:31:07 -05:00
Aiden Cline 2593e131a1 wip 2026-05-11 16:11:19 -05:00
Frank bc95b42ccd update zen models 2026-05-11 11:26:08 -04:00
Aiden Cline 6139fb8c69 Merge pull request #1598 from mugnimaestra/feat/chutes-generate-script
feat(chutes): add API-driven model generator script
2026-05-11 09:30:38 -05:00
Aiden Cline 3070758007 Merge pull request #1678 from 5kahoisaac/chore/nvidia-models
Sync NVIDIA endpoint model catalog
2026-05-11 09:29:54 -05:00
Frank 5525e83de4 update zen models 2026-05-11 10:00:09 -04:00
Frank 359fd879b8 update zen models 2026-05-10 03:54:03 -04:00
Frank 01b5a1a656 update zen models 2026-05-10 02:52:44 -04:00
Frank b1958be099 update zen models 2026-05-10 02:42:44 -04:00
Aiden Cline 08aa068523 Temporarily remove kiro provider and models 2026-05-10 01:19:47 -05:00
Aiden Cline f31ad0b02f Merge pull request #1738 from mattiacerutti/chore/remove-gh-copilot-deprecated
chore(copilot): mark deprecated models
2026-05-09 15:42:19 -05:00
Aiden Cline 585aa7fa1b Merge pull request #1741 from EriDeLee/dev
Update aihubmix models
2026-05-09 15:42:03 -05:00
Aiden Cline c42a327b3e Merge pull request #1745 from mads-digitial-solutions/patch-1
Update Google provider docs url from pricing page to models page
2026-05-09 15:41:51 -05:00
mads-digitial-solutions 92ebbfb5c4 Update provider.toml
Update Google provider docs URL from the pricing page to the models page
2026-05-09 19:49:19 +01:00
Aiden Cline 535fe8c971 Merge pull request #1744 from OpeOginni/fix/bedrock-model-ids
chore(bedrock): Getting rid of legacy Amazon Bedrock model offerings
2026-05-09 13:48:52 -05:00
OpeOginni a3b4bfc16c fix(bedrock): remove uneeded model configurations 2026-05-09 20:35:34 +02:00
OpeOginni d0fcd6f11f fix(bedrock): align models with current docs 2026-05-09 20:29:11 +02:00
OpeOginni e55cd54218 fix(bedrock): remove legacy model entries 2026-05-09 20:21:33 +02:00
OpeOginni 0d73b82b9f fix(bedrock): restore regional model IDs 2026-05-09 20:18:59 +02:00
Aiden Cline 83c7e2b63f Merge pull request #1742 from Adam8234/add-firepass-provider
feat: add Fireworks (Firepass) provider
2026-05-09 12:45:09 -05:00
Adam 83ae4cf813 feat: add Fireworks (Firepass) provider
Adds the Fireworks AI Firepass subscription provider.
- Provider uses a dedicated FIREPASS_API_KEY
- Uses @ai-sdk/openai-compatible SDK
- Includes Kimi K2.6 Turbo (accounts/fireworks/routers/kimi-k2p6-turbo)
- Zero per-token cost since covered by subscription
2026-05-09 12:22:13 -05:00
EriDeLee 77eae6eef7 Update aihubmix models 2026-05-09 20:53:57 +08:00
Aiden Cline 8cbf6ed10e Merge pull request #1736 from vercel/update-vercel-models-1778258030
Update Vercel models
2026-05-08 21:58:40 -05:00
Mattia Cerutti 06d87e4411 chore(copilot): remove deprecated models 2026-05-09 00:19:50 +02:00
Frank 2cb3832618 update zen models 2026-05-08 17:11:05 -04:00
github-actions[bot] df960d1a90 chore(vercel): update Vercel model definitions
Auto-generated by weekly workflow from Vercel AI Gateway API.

Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-08 16:33:52 +00:00
Aiden Cline 8f2f83ef61 Merge pull request #1735 from slpdy/dev
Create DeepSeek-V4-Pro.toml
2026-05-08 10:59:33 -05:00
Aiden Cline b133426465 Merge pull request #1734 from oskarkocol/chore/update-novita-deepseek-prices
chore: update novita deepseek-v4-pro prices
2026-05-08 10:59:25 -05:00
Aiden Cline dafff5a770 Merge pull request #1730 from smakosh/add-llmgateway-models
Add new LLM Gateway text models (gpt-5.5, grok-4-3, gemini-3.1-flash-lite, qwen3.6, MiMo v2)
2026-05-08 10:58:20 -05:00
smakosh dd894f077f Merge remote-tracking branch 'upstream/dev' into add-llmgateway-models
# Conflicts:
#	providers/google/models/gemini-3.1-flash-lite.toml
2026-05-08 17:46:52 +02:00
smakosh 91590874e7 Revert "fix(models): use canonical entries for qwen3.6-max-preview and grok-4.3"
This reverts commit 70ac6fccda.
2026-05-08 17:43:07 +02:00
Jj a436236146 Create DeepSeek-V4-Pro.toml
Added DeepSeek-v4-Pro model to Nebius provider
2026-05-08 11:03:16 +01:00
oskar 1415b4be97 chore: update novita deepseek prices 2026-05-08 14:21:27 +07:00
smakosh 70ac6fccda fix(models): use canonical entries for qwen3.6-max-preview and grok-4.3
Apply the canonical TOML provided by the LLM Gateway team for the
Qwen3.6 Max Preview and Grok 4.3 parent definitions, replacing the
upstream-merged variants whose dates and pricing did not match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:34:01 +02:00
smakosh dc3283417d Merge remote-tracking branch 'upstream/dev' into add-llmgateway-models
# Conflicts:
#	providers/alibaba/models/qwen3.6-max-preview.toml
#	providers/llmgateway/models/qwen3.6-max-preview.toml
2026-05-07 22:28:23 +02:00
smakosh 34fd6673e5 chore(llmgateway): add new text models from llmgateway catalog
Add gemini-3.1-flash-lite, grok-4-3, gpt-5.5, gpt-5.5-pro, qwen3.6
and MiMo v2 models that exist in llmgateway.io but were missing
from models.dev. Adds parent definitions for grok-4-3,
gemini-3.1-flash-lite, and qwen3.6-max-preview where they did not
already exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:20:52 +02:00
Isaac Ng c5fbcc2c9b 📦 CHORE: remove senera 2026-05-03 16:17:37 +08:00
Isaac Ng ab2eb51b4e chore(nvidia): align Nemotron endpoint slugs
Replace stale NVIDIA Nemotron entries with the live Build catalog slugs so the local provider catalog matches current free and partner endpoints.
2026-05-03 15:47:19 +08:00
Isaac Ng 8e19ec580c 📦 CHORE: sync latest nvidia model 2026-05-03 15:19:17 +08:00
Isaac Ng 3aecc94c46 chore(nvidia): sync endpoint model catalog
Update NVIDIA model TOMLs to match the live Build endpoint list by removing stale entries and adding missing ones.

This keeps the provider catalog aligned with the current free and partner endpoint inventory.
2026-05-03 14:47:30 +08:00
Muhammad Mugni Hadi dbe92646c3 chore(chutes): add header comments to generated TOML files
Each generated TOML now includes a comment noting which fields are
auto-managed vs manually overridable on re-run.
2026-04-26 06:52:21 +07:00
Muhammad Mugni Hadi 4717c67054 feat(chutes): add API-driven model generator script
Add generate-chutes.ts that fetches models from https://llm.chutes.ai/v1/models
and generates/updates TOML files, following the same pattern as generate-vercel.ts.

Supports --dry-run, --new-only, and --keep-orphans flags. Auto-deletes TOML files
for models no longer in the API (with empty directory cleanup).

Preserves manually-set fields (family, knowledge, interleaved, status) when merging
with API data. Also syncs current models from the API.
2026-04-26 06:51:16 +07:00
334 changed files with 3003 additions and 3063 deletions
-3
View File
@@ -32,7 +32,6 @@
"packages/web": {
"name": "@models.dev/web",
"dependencies": {
"@tanstack/virtual-core": "^3.14.0",
"hono": "^4.8.0",
"models.dev": "workspace:*",
},
@@ -58,8 +57,6 @@
"@models.dev/web": ["@models.dev/web@workspace:packages/web"],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.8", "", {}, "sha512-JlJaRaS4hBTypxtFe8WhnwV8blf0R+3yehLk8XuyxUYNx6VXsKCjACSCvOYEFUiqlhlBWxtYCn/zRlOb8BzBQg=="],
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
+1
View File
@@ -17,6 +17,7 @@
"scripts": {
"validate": "bun ./packages/core/script/validate.ts",
"compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts",
"chutes:generate": "bun ./packages/core/script/generate-chutes.ts",
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
"venice:generate": "bun ./packages/core/script/generate-venice.ts",
"vercel:generate": "bun ./packages/core/script/generate-vercel.ts",
+589
View File
@@ -0,0 +1,589 @@
#!/usr/bin/env bun
/**
* Generates Chutes model TOML files from the Chutes LLM API.
*
* Flags:
* --dry-run: Preview changes without writing files
* --new-only: Only create new models, skip updating existing ones
* --keep-orphans: Don't delete TOML files for models no longer in the API
*/
import { z } from "zod";
import path from "node:path";
import { mkdir } from "node:fs/promises";
import { ModelFamilyValues } from "../src/family.js";
const API_ENDPOINT = "https://llm.chutes.ai/v1/models";
enum SkipZeroFields {
LimitContext = "limit.context",
LimitOutput = "limit.output",
}
const Pricing = z.object({
prompt: z.number().optional(),
completion: z.number().optional(),
input_cache_read: z.number().optional(),
}).passthrough();
const ChutesModel = z.object({
id: z.string(),
created: z.number(),
pricing: Pricing.optional(),
context_length: z.number().optional(),
max_output_length: z.number().optional(),
max_model_len: z.number().optional(),
input_modalities: z.array(z.string()).optional(),
output_modalities: z.array(z.string()).optional(),
supported_features: z.array(z.string()).optional(),
supported_sampling_parameters: z.array(z.string()).optional(),
quantization: z.string().optional(),
}).passthrough();
const ChutesResponse = z.object({
data: z.array(ChutesModel),
}).passthrough();
interface ExistingModel {
name?: string;
family?: string;
attachment?: boolean;
reasoning?: boolean;
tool_call?: boolean;
structured_output?: boolean;
temperature?: boolean;
knowledge?: string;
release_date?: string;
last_updated?: string;
open_weights?: boolean;
interleaved?: boolean | { field: string };
status?: string;
cost?: {
input?: number;
output?: number;
cache_read?: number;
};
limit?: {
context?: number;
output?: number;
};
modalities?: {
input?: string[];
output?: string[];
};
}
interface MergedModel {
name: string;
family?: string;
attachment: boolean;
reasoning: boolean;
tool_call: boolean;
structured_output?: boolean;
temperature: boolean;
knowledge?: string;
release_date: string;
last_updated: string;
open_weights: boolean;
interleaved?: boolean | { field: string };
status?: string;
cost?: {
input: number;
output: number;
cache_read?: number;
};
limit: {
context: number;
output: number;
};
modalities: {
input: string[];
output: string[];
};
}
interface Changes {
field: string;
oldValue: string;
newValue: string;
}
// ── Utility functions ────────────────────────────────────────────────
function timestampToDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toISOString().slice(0, 10);
}
function getTodayDate(): string {
return new Date().toISOString().slice(0, 10);
}
function formatNumber(n: number): string {
if (n >= 1000) {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_");
}
return n.toString();
}
/**
* Humanize a model ID into a readable name.
* Strips the org prefix and replaces hyphens with spaces.
* e.g. "Qwen/Qwen3-32B-TEE" → "Qwen3 32B TEE"
*/
function humanizeModelName(modelId: string): string {
const parts = modelId.split("/");
const modelPart = parts[parts.length - 1];
return modelPart.replace(/-/g, " ");
}
// ── Family inference (same approach as generate-vercel.ts) ───────────
function isSubstring(target: string, family: string): boolean {
return target.toLowerCase().includes(family.toLowerCase());
}
function matchesFamily(target: string, family: string): boolean {
const targetLower = target.toLowerCase();
const familyLower = family.toLowerCase();
let familyIdx = 0;
for (let i = 0; i < targetLower.length && familyIdx < familyLower.length; i++) {
if (targetLower[i] === familyLower[familyIdx]) {
familyIdx++;
}
}
return familyIdx === familyLower.length;
}
function inferFamily(modelId: string, modelName: string): string | undefined {
const sortedFamilies = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
// First pass: try exact substring matches
for (const family of sortedFamilies) {
if (isSubstring(modelId, family)) {
return family;
}
}
for (const family of sortedFamilies) {
if (isSubstring(modelName, family)) {
return family;
}
}
// Second pass: fall back to subsequence matching
for (const family of sortedFamilies) {
if (matchesFamily(modelId, family)) {
return family;
}
}
for (const family of sortedFamilies) {
if (matchesFamily(modelName, family)) {
return family;
}
}
return undefined;
}
// ── Load existing TOML ───────────────────────────────────────────────
async function loadExistingModel(filePath: string): Promise<ExistingModel | null> {
try {
const file = Bun.file(filePath);
if (!(await file.exists())) {
return null;
}
const toml = await import(filePath, { with: { type: "toml" } }).then(
(mod) => mod.default,
);
return toml as ExistingModel;
} catch (e) {
console.warn(`Warning: Failed to parse existing file ${filePath}:`, e);
return null;
}
}
// ── Merge API data with existing TOML ────────────────────────────────
function mergeModel(
apiModel: z.infer<typeof ChutesModel>,
existing: ExistingModel | null,
): MergedModel {
const features = new Set(apiModel.supported_features ?? []);
const samplingParams = new Set(apiModel.supported_sampling_parameters ?? []);
const inputMods = apiModel.input_modalities ?? ["text"];
const outputMods = apiModel.output_modalities ?? ["text"];
// Capabilities from API features
const hasAttachment = inputMods.some((m) =>
m === "image" || m === "video" || m === "pdf",
);
const hasReasoning = features.has("reasoning");
const hasToolCall = features.has("tools");
const hasStructuredOutput = features.has("structured_outputs");
const hasTemperature = samplingParams.size > 0
? samplingParams.has("temperature")
: true; // default true if no sampling params info
// Preserve existing values when available (manually specified)
const modelName = existing?.name ?? humanizeModelName(apiModel.id);
const family = existing?.family ?? inferFamily(apiModel.id, modelName);
const knowledge = existing?.knowledge;
const interleaved = existing?.interleaved;
const status = existing?.status;
// Release date: existing > API created timestamp > today
const releaseDate = existing?.release_date
?? timestampToDate(apiModel.created)
?? getTodayDate();
// Context limit: prefer context_length, fallback to max_model_len
const apiContext = apiModel.context_length ?? apiModel.max_model_len ?? 0;
const contextLimit = apiContext > 0
? apiContext
: (existing?.limit?.context ?? 0);
// Output limit: prefer max_output_length, fallback to existing
const apiOutput = apiModel.max_output_length ?? 0;
const outputLimit = apiOutput > 0
? apiOutput
: (existing?.limit?.output ?? 0);
const merged: MergedModel = {
name: modelName,
family,
attachment: hasAttachment,
reasoning: hasReasoning,
tool_call: hasToolCall,
temperature: hasTemperature,
release_date: releaseDate,
last_updated: getTodayDate(),
open_weights: true, // Chutes hosts open-weight models
...(hasStructuredOutput && { structured_output: hasStructuredOutput }),
...(knowledge && { knowledge }),
...(interleaved !== undefined && { interleaved }),
...(status && { status }),
limit: {
context: contextLimit,
output: outputLimit,
},
modalities: {
input: inputMods,
output: outputMods,
},
};
// Cost: API values are already in USD per 1M tokens — use directly
if (apiModel.pricing) {
const inputPrice = apiModel.pricing.prompt;
const outputPrice = apiModel.pricing.completion;
const cacheReadPrice = apiModel.pricing.input_cache_read;
if (inputPrice !== undefined && outputPrice !== undefined) {
merged.cost = {
input: inputPrice,
output: outputPrice,
...(cacheReadPrice !== undefined && { cache_read: cacheReadPrice }),
};
}
}
return merged;
}
// ── TOML formatting ──────────────────────────────────────────────────
function formatToml(model: MergedModel): string {
const lines: string[] = [];
lines.push(`# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.`);
lines.push(`# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status`);
lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`);
if (model.family) {
lines.push(`family = "${model.family}"`);
}
lines.push(`release_date = "${model.release_date}"`);
lines.push(`last_updated = "${model.last_updated}"`);
lines.push(`attachment = ${model.attachment}`);
lines.push(`reasoning = ${model.reasoning}`);
lines.push(`temperature = ${model.temperature}`);
lines.push(`tool_call = ${model.tool_call}`);
if (model.structured_output !== undefined) {
lines.push(`structured_output = ${model.structured_output}`);
}
lines.push(`open_weights = ${model.open_weights}`);
if (model.knowledge) {
lines.push(`knowledge = "${model.knowledge}"`);
}
if (model.status) {
lines.push(`status = "${model.status}"`);
}
if (model.cost) {
lines.push("");
lines.push(`[cost]`);
lines.push(`input = ${model.cost.input}`);
lines.push(`output = ${model.cost.output}`);
if (model.cost.cache_read !== undefined) {
lines.push(`cache_read = ${model.cost.cache_read}`);
}
}
lines.push("");
lines.push(`[limit]`);
lines.push(`context = ${formatNumber(model.limit.context)}`);
lines.push(`output = ${formatNumber(model.limit.output)}`);
lines.push("");
lines.push(`[modalities]`);
lines.push(`input = [${model.modalities.input.map((m) => `"${m}"`).join(", ")}]`);
lines.push(`output = [${model.modalities.output.map((m) => `"${m}"`).join(", ")}]`);
if (model.interleaved !== undefined) {
lines.push("");
if (model.interleaved === true) {
lines.push(`interleaved = true`);
} else if (typeof model.interleaved === "object") {
lines.push(`[interleaved]`);
lines.push(`field = "${model.interleaved.field}"`);
}
}
return lines.join("\n") + "\n";
}
// ── Change detection ─────────────────────────────────────────────────
function detectChanges(
existing: ExistingModel | null,
merged: MergedModel,
): Changes[] {
if (!existing) return [];
const changes: Changes[] = [];
const EPSILON = 0.001;
const shouldSkipZero = (field: string, oldVal: unknown, newVal: unknown): boolean => {
if (!Object.values(SkipZeroFields).includes(field as SkipZeroFields)) {
return false;
}
return (typeof oldVal === "number" && oldVal === 0) || (typeof newVal === "number" && newVal === 0);
};
const formatValue = (val: unknown): string => {
if (typeof val === "number") return formatNumber(val);
if (Array.isArray(val)) return `[${val.join(", ")}]`;
if (val === undefined) return "(none)";
return String(val);
};
const isMaterialPriceDiff = (oldPrice: unknown, newPrice: unknown): boolean => {
if (oldPrice === 0 && newPrice === undefined) return false;
if (oldPrice !== undefined && newPrice !== undefined) {
return Math.abs((oldPrice as number) - (newPrice as number)) > EPSILON;
}
return oldPrice !== newPrice;
};
const compare = (field: string, oldVal: unknown, newVal: unknown) => {
if (shouldSkipZero(field, oldVal, newVal)) return;
const isDiff = field.startsWith("cost.")
? isMaterialPriceDiff(oldVal, newVal)
: JSON.stringify(oldVal) !== JSON.stringify(newVal);
if (isDiff) {
changes.push({
field,
oldValue: formatValue(oldVal),
newValue: formatValue(newVal),
});
}
};
compare("name", existing.name, merged.name);
compare("family", existing.family, merged.family);
compare("attachment", existing.attachment, merged.attachment);
compare("reasoning", existing.reasoning, merged.reasoning);
compare("tool_call", existing.tool_call, merged.tool_call);
compare("structured_output", existing.structured_output, merged.structured_output);
compare("open_weights", existing.open_weights, merged.open_weights);
compare("release_date", existing.release_date, merged.release_date);
compare("cost.input", existing.cost?.input, merged.cost?.input);
compare("cost.output", existing.cost?.output, merged.cost?.output);
compare("cost.cache_read", existing.cost?.cache_read, merged.cost?.cache_read);
compare("limit.context", existing.limit?.context, merged.limit.context);
compare("limit.output", existing.limit?.output, merged.limit.output);
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
compare("modalities.output", existing.modalities?.output, merged.modalities.output);
return changes;
}
// ── Main ─────────────────────────────────────────────────────────────
async function main() {
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const newOnly = args.includes("--new-only");
const keepOrphans = args.includes("--keep-orphans");
const modelsDir = path.join(
import.meta.dirname,
"..",
"..",
"..",
"providers",
"chutes",
"models",
);
console.log(`${dryRun ? "[DRY RUN] " : ""}${newOnly ? "[NEW ONLY] " : ""}${keepOrphans ? "[KEEP ORPHANS] " : ""}Fetching Chutes models from API...`);
const res = await fetch(API_ENDPOINT);
if (!res.ok) {
console.error(`Failed to fetch API: ${res.status} ${res.statusText}`);
process.exit(1);
}
const json = await res.json();
const parsed = ChutesResponse.safeParse(json);
if (!parsed.success) {
console.error("Invalid API response:", parsed.error.errors);
process.exit(1);
}
const apiModels = parsed.data.data;
// Scan existing TOML files
const existingFiles = new Set<string>();
try {
for await (const file of new Bun.Glob("**/*.toml").scan({
cwd: modelsDir,
absolute: false,
})) {
existingFiles.add(file);
}
} catch {
}
console.log(`Found ${apiModels.length} models in API, ${existingFiles.size} existing files\n`);
const apiModelIds = new Set<string>();
let created = 0;
let updated = 0;
let unchanged = 0;
for (const apiModel of apiModels) {
const relativePath = `${apiModel.id}.toml`;
const filePath = path.join(modelsDir, relativePath);
const dirPath = path.dirname(filePath);
apiModelIds.add(relativePath);
const existing = await loadExistingModel(filePath);
const merged = mergeModel(apiModel, existing);
const tomlContent = formatToml(merged);
if (existing === null) {
created++;
if (dryRun) {
console.log(`[DRY RUN] Would create: ${relativePath}`);
console.log(` name = "${merged.name}"`);
if (merged.family) {
console.log(` family = "${merged.family}" (inferred)`);
}
console.log("");
} else {
await mkdir(dirPath, { recursive: true });
await Bun.write(filePath, tomlContent);
console.log(`Created: ${relativePath}`);
}
} else {
if (newOnly) {
unchanged++;
continue;
}
const changes = detectChanges(existing, merged);
const existingContent = await Bun.file(filePath).text();
const formatChanged = existingContent !== tomlContent;
if (changes.length > 0 || formatChanged) {
updated++;
if (dryRun) {
console.log(`[DRY RUN] Would update: ${relativePath}`);
} else {
await mkdir(dirPath, { recursive: true });
await Bun.write(filePath, tomlContent);
console.log(`Updated: ${relativePath}`);
}
for (const change of changes) {
console.log(` ${change.field}: ${change.oldValue}${change.newValue}`);
}
if (changes.length === 0 && formatChanged) {
console.log(` (format-only change)`);
}
console.log("");
} else {
unchanged++;
}
}
}
// Handle orphaned files (on disk but not in API)
const orphaned: string[] = [];
for (const file of existingFiles) {
if (!apiModelIds.has(file)) {
orphaned.push(file);
const orphanPath = path.join(modelsDir, file);
if (keepOrphans) {
console.log(`Orphaned (kept): ${file}`);
} else if (dryRun) {
console.log(`[DRY RUN] Would delete: ${file}`);
} else {
await Bun.file(orphanPath).delete();
console.log(`Deleted: ${file}`);
// Clean up empty parent directories
const parentDir = path.dirname(orphanPath);
try {
const remaining = [];
for await (const entry of new Bun.Glob("*").scan({ cwd: parentDir })) {
remaining.push(entry);
}
if (remaining.length === 0) {
const { rmdir } = await import("node:fs/promises");
await rmdir(parentDir);
console.log(` Removed empty directory: ${path.basename(parentDir)}/`);
}
} catch {
// Directory not empty or other error, ignore
}
}
}
}
console.log("");
if (dryRun) {
console.log(
`Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} would be deleted`,
);
} else if (keepOrphans) {
console.log(
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned (kept)`,
);
} else {
console.log(
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} deleted`,
);
}
}
await main();
+61 -15
View File
@@ -209,7 +209,18 @@ interface ExistingModel {
output?: number;
cache_read?: number;
cache_write?: number;
context_min?: number;
};
tiers?: Array<{
tier: {
type?: "context";
size: number;
};
input?: number;
output?: number;
cache_read?: number;
cache_write?: number;
}>;
};
limit?: {
context?: number;
@@ -262,6 +273,7 @@ interface MergedModel {
output: number;
cache_read?: number;
cache_write?: number;
context_min?: number;
};
};
limit: {
@@ -310,6 +322,35 @@ function inferFamily(modelId: string, modelName: string): string | undefined {
return undefined;
}
function getExistingLongContextCost(existing: ExistingModel | null) {
const tier = existing?.cost?.tiers?.find(
(tier) =>
(tier.tier.type === undefined || tier.tier.type === "context") &&
tier.tier.size >= 200_000,
);
if (tier) {
return {
...tier,
context_min: tier.tier.size,
};
}
return existing?.cost?.context_over_200k === undefined
? undefined
: {
...existing.cost.context_over_200k,
context_min: 200_000,
};
}
function getLongContextMin(cost: { context_min?: number }) {
return cost.context_min ?? 200_000;
}
function formatInlineNumber(n: number): string {
return n >= 1000 ? n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_") : n.toString();
}
// ---------------------------------------------------------------------------
// Merge API data with existing TOML
// ---------------------------------------------------------------------------
@@ -394,27 +435,30 @@ function mergeModel(
};
// Context-tiered pricing (>200k) from the static-content API
const existingLongContextCost = getExistingLongContextCost(existing);
if (pricing?.inputOver200k !== undefined && pricing?.outputOver200k !== undefined) {
merged.cost.context_over_200k = {
input: pricing.inputOver200k,
output: pricing.outputOver200k,
...(existing?.cost?.context_over_200k?.cache_read !== undefined && {
cache_read: existing.cost.context_over_200k.cache_read,
context_min: existingLongContextCost?.context_min ?? 200_000,
...(existingLongContextCost?.cache_read !== undefined && {
cache_read: existingLongContextCost.cache_read,
}),
...(existing?.cost?.context_over_200k?.cache_write !== undefined && {
cache_write: existing.cost.context_over_200k.cache_write,
...(existingLongContextCost?.cache_write !== undefined && {
cache_write: existingLongContextCost.cache_write,
}),
};
} else if (existing?.cost?.context_over_200k) {
// Preserve manually-entered context_over_200k if API has no data
} else if (existingLongContextCost) {
// Preserve manually-entered tiered pricing if API has no data
merged.cost.context_over_200k = {
input: existing.cost.context_over_200k.input ?? inputPrice,
output: existing.cost.context_over_200k.output ?? outputPrice,
...(existing.cost.context_over_200k.cache_read !== undefined && {
cache_read: existing.cost.context_over_200k.cache_read,
input: existingLongContextCost.input ?? inputPrice,
output: existingLongContextCost.output ?? outputPrice,
context_min: existingLongContextCost.context_min,
...(existingLongContextCost.cache_read !== undefined && {
cache_read: existingLongContextCost.cache_read,
}),
...(existing.cost.context_over_200k.cache_write !== undefined && {
cache_write: existing.cost.context_over_200k.cache_write,
...(existingLongContextCost.cache_write !== undefined && {
cache_write: existingLongContextCost.cache_write,
}),
};
}
@@ -463,7 +507,8 @@ function formatToml(model: MergedModel): string {
if (model.cost.context_over_200k) {
lines.push("");
lines.push(`[cost.context_over_200k]`);
lines.push(`[[cost.tiers]]`);
lines.push(`tier = { size = ${formatInlineNumber(getLongContextMin(model.cost.context_over_200k))} }`);
lines.push(`input = ${model.cost.context_over_200k.input}`);
lines.push(`output = ${model.cost.context_over_200k.output}`);
if (model.cost.context_over_200k.cache_read !== undefined)
@@ -524,8 +569,9 @@ function detectChanges(existing: ExistingModel | null, merged: MergedModel): Cha
compare("status", existing.status, merged.status);
compare("cost.input", existing.cost?.input, merged.cost?.input);
compare("cost.output", existing.cost?.output, merged.cost?.output);
compare("cost.context_over_200k.input", existing.cost?.context_over_200k?.input, merged.cost?.context_over_200k?.input);
compare("cost.context_over_200k.output", existing.cost?.context_over_200k?.output, merged.cost?.context_over_200k?.output);
const existingLongContextCost = getExistingLongContextCost(existing);
compare("cost.context_over_200k.input", existingLongContextCost?.input, merged.cost?.context_over_200k?.input);
compare("cost.context_over_200k.output", existingLongContextCost?.output, merged.cost?.context_over_200k?.output);
compare("limit.context", existing.limit?.context, merged.limit.context);
compare("limit.output", existing.limit?.output, merged.limit.output);
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
+44 -5
View File
@@ -162,7 +162,18 @@ interface ExistingModel {
output?: number;
cache_read?: number;
cache_write?: number;
context_min?: number;
};
tiers?: Array<{
tier: {
type?: "context";
size: number;
};
input?: number;
output?: number;
cache_read?: number;
cache_write?: number;
}>;
};
limit?: {
context?: number;
@@ -195,6 +206,30 @@ async function loadExistingModel(filePath: string): Promise<ExistingModel | null
}
}
function getExistingLongContextMin(existing: ExistingModel | null) {
return (
existing?.cost?.tiers?.find(
(tier) =>
(tier.tier.type === undefined || tier.tier.type === "context") &&
tier.tier.size >= 200_000,
)?.tier.size ?? 200_000
);
}
function getExistingLongContextCost(existing: ExistingModel | null) {
return (
existing?.cost?.tiers?.find(
(tier) =>
(tier.tier.type === undefined || tier.tier.type === "context") &&
tier.tier.size >= 200_000,
) ?? existing?.cost?.context_over_200k
);
}
function getLongContextMin(cost: { context_min?: number }) {
return cost.context_min ?? 200_000;
}
interface MergedModel {
name: string;
family?: string;
@@ -219,6 +254,7 @@ interface MergedModel {
output: number;
cache_read?: number;
cache_write?: number;
context_min?: number;
};
};
limit: {
@@ -292,6 +328,7 @@ function mergeModel(
merged.cost.context_over_200k = {
input: spec.pricing.extended.input.usd,
output: spec.pricing.extended.output.usd,
context_min: spec.pricing.extended.context_token_threshold,
...(spec.pricing.extended.cache_input && { cache_read: spec.pricing.extended.cache_input.usd }),
...(spec.pricing.extended.cache_write && { cache_write: spec.pricing.extended.cache_write.usd }),
};
@@ -366,7 +403,8 @@ function formatToml(model: MergedModel): string {
if (model.cost.context_over_200k) {
lines.push("");
lines.push(`[cost.context_over_200k]`);
lines.push(`[[cost.tiers]]`);
lines.push(`tier = { size = ${formatNumber(getLongContextMin(model.cost.context_over_200k))} }`);
lines.push(`input = ${model.cost.context_over_200k.input}`);
lines.push(`output = ${model.cost.context_over_200k.output}`);
if (model.cost.context_over_200k.cache_read !== undefined) {
@@ -438,10 +476,11 @@ function detectChanges(
compare("cost.output", existing.cost?.output, merged.cost?.output);
compare("cost.cache_read", existing.cost?.cache_read, merged.cost?.cache_read);
compare("cost.cache_write", existing.cost?.cache_write, merged.cost?.cache_write);
compare("cost.context_over_200k.input", existing.cost?.context_over_200k?.input, merged.cost?.context_over_200k?.input);
compare("cost.context_over_200k.output", existing.cost?.context_over_200k?.output, merged.cost?.context_over_200k?.output);
compare("cost.context_over_200k.cache_read", existing.cost?.context_over_200k?.cache_read, merged.cost?.context_over_200k?.cache_read);
compare("cost.context_over_200k.cache_write", existing.cost?.context_over_200k?.cache_write, merged.cost?.context_over_200k?.cache_write);
const existingLongContextCost = getExistingLongContextCost(existing);
compare("cost.context_over_200k.input", existingLongContextCost?.input, merged.cost?.context_over_200k?.input);
compare("cost.context_over_200k.output", existingLongContextCost?.output, merged.cost?.context_over_200k?.output);
compare("cost.context_over_200k.cache_read", existingLongContextCost?.cache_read, merged.cost?.context_over_200k?.cache_read);
compare("cost.context_over_200k.cache_write", existingLongContextCost?.cache_write, merged.cost?.context_over_200k?.cache_write);
compare("limit.context", existing.limit?.context, merged.limit.context);
compare("limit.output", existing.limit?.output, merged.limit.output);
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
+3
View File
@@ -55,6 +55,7 @@ export const ModelFamilyValues = [
"deepseek",
"deepseek-thinking",
"deepseek-flash",
"deepseek-flash-free",
"deepseek-flash-think",
// Microsoft Phi
@@ -289,12 +290,14 @@ export const ModelFamilyValues = [
"rnj",
// Tecent Hy
"hy3",
"hy3-free",
// Ling & Ring (InclusionAI)
"ling",
"ling-flash-free",
"ring",
"ring-1t-free",
// Kat Coder
"kat-coder",
+53 -5
View File
@@ -2,9 +2,9 @@ import path from "path";
import { mergeDeep } from "remeda";
import { z } from "zod";
import { Provider, Model } from "./schema.js";
import { Provider, Model, AuthoredModel, AuthoredModelShape } from "./schema.js";
const ExtendsModel = Model.sourceType()
const ExtendsModel = AuthoredModelShape
.partial()
.extend({
extends: z
@@ -71,12 +71,12 @@ export async function generate(directory: string) {
});
continue;
}
const model = Model.safeParse(toml);
const model = AuthoredModel.safeParse(toml);
if (!model.success) {
model.error.cause = { modelPath, toml };
throw model.error;
}
provider.data.models[modelID] = model.data;
provider.data.models[modelID] = normalizeModelCost(model.data);
}
result[providerID] = provider.data;
}
@@ -144,7 +144,7 @@ export async function generate(directory: string) {
}
}
const model = Model.safeParse(merged);
const model = Model.safeParse(normalizeCost(merged));
if (!model.success) {
model.error.cause = { modelPath: pendingModel.modelPath, toml: merged };
throw model.error;
@@ -155,3 +155,51 @@ export async function generate(directory: string) {
return result;
}
function normalizeModelCost(model: z.infer<typeof AuthoredModel>): Model {
return normalizeCost(model) as Model;
}
function normalizeCost(model: Record<string, unknown>) {
const cost = model.cost;
if (cost === undefined || cost === null || typeof cost !== "object" || Array.isArray(cost)) {
return model;
}
const tiers = (cost as { tiers?: unknown }).tiers;
if (!Array.isArray(tiers)) {
return model;
}
if (tiers.length !== 1) {
return model;
}
const contextOver200k = tiers.find((tier) => {
if (tier === null || typeof tier !== "object" || Array.isArray(tier)) return false;
const tierConfig = (tier as { tier?: unknown }).tier;
if (tierConfig === null || typeof tierConfig !== "object" || Array.isArray(tierConfig)) return false;
const type = (tierConfig as { type?: unknown }).type;
const size = (tierConfig as { size?: unknown }).size;
// context_over_200k is a legacy compatibility field. It intentionally
// includes higher thresholds; cost.tiers carries the exact threshold.
return (
(type === undefined || type === "context") &&
typeof size === "number" &&
size >= 200_000
);
});
if (contextOver200k === undefined) {
return model;
}
const { tier: _tier, ...legacyCost } = contextOver200k as Record<string, unknown>;
return {
...model,
cost: {
...(cost as Record<string, unknown>),
context_over_200k: legacyCost,
},
};
}
+151 -97
View File
@@ -21,110 +21,164 @@ const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
]),
);
const Cost = z.object({
input: z.number().min(0, "Input price cannot be negative"),
output: z.number().min(0, "Output price cannot be negative"),
reasoning: z.number().min(0, "Input price cannot be negative").optional(),
cache_read: z
.number()
.min(0, "Cache read price cannot be negative")
const Cost = z
.object({
input: z.number().min(0, "Input price cannot be negative"),
output: z.number().min(0, "Output price cannot be negative"),
reasoning: z
.number()
.min(0, "Reasoning price cannot be negative")
.optional(),
cache_read: z
.number()
.min(0, "Cache read price cannot be negative")
.optional(),
cache_write: z
.number()
.min(0, "Cache write price cannot be negative")
.optional(),
input_audio: z
.number()
.min(0, "Audio input price cannot be negative")
.optional(),
output_audio: z
.number()
.min(0, "Audio output price cannot be negative")
.optional(),
});
const CostTier = Cost.extend({
tier: z
.object({
type: z.literal("context").default("context"),
size: z.number().int().min(0, "Context tier size cannot be negative"),
})
.strict(),
}).strict();
const AuthoredCost = Cost.extend({
context_over_200k: z.never().optional(),
tiers: z.array(CostTier).optional(),
});
const OutputCost = Cost.extend({
context_over_200k: Cost.optional(),
tiers: z.array(CostTier).optional(),
});
const ModelBase = z.object({
id: z.string(),
name: z.string().min(1, "Model name cannot be empty"),
family: ModelFamily.optional(),
attachment: z.boolean(),
reasoning: z.boolean(),
tool_call: z.boolean(),
interleaved: z
.union([
z.literal(true),
z
.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
})
.strict(),
])
.optional(),
cache_write: z
.number()
.min(0, "Cache write price cannot be negative")
structured_output: z.boolean().optional(),
temperature: z.boolean().optional(),
knowledge: z
.string()
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
})
.optional(),
input_audio: z
.number()
.min(0, "Audio input price cannot be negative")
release_date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
}),
last_updated: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
}),
modalities: z.object({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
}),
open_weights: z.boolean(),
limit: z.object({
context: z.number().min(0, "Context window must be positive"),
input: z.number().min(0, "Input tokens must be positive").optional(),
output: z.number().min(0, "Output tokens must be positive"),
}),
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
experimental: z
.object({
modes: z
.record(
z.object({
cost: Cost.optional(),
provider: z
.object({
body: z.record(JsonValue).optional(),
headers: z.record(z.string()).optional(),
})
.optional(),
}),
)
.optional(),
})
.optional(),
output_audio: z
.number()
.min(0, "Audio output price cannot be negative")
provider: z
.object({
npm: z.string().optional(),
api: z.string().optional(),
shape: z.enum(["responses", "completions"]).optional(),
body: z.record(JsonValue).optional(),
headers: z.record(z.string()).optional(),
})
.optional(),
});
export const Model = z
function refineModel<T extends z.ZodTypeAny>(schema: T) {
return schema
.refine(
(data) => {
return !(data.reasoning === false && data.cost?.reasoning !== undefined);
},
{
message: "Cannot set cost.reasoning when reasoning is false",
path: ["cost", "reasoning"],
},
)
.refine(
(data) => {
const tiers = data.cost?.tiers;
if (tiers === undefined) return true;
const sizes = tiers.map((tier: { tier: { size: number } }) => tier.tier.size);
return new Set(sizes).size === sizes.length;
},
{
message: "Cost context tiers must not have duplicate sizes",
path: ["cost", "tiers"],
},
);
}
export const ModelShape = z
.object({
id: z.string(),
name: z.string().min(1, "Model name cannot be empty"),
family: ModelFamily.optional(),
attachment: z.boolean(),
reasoning: z.boolean(),
tool_call: z.boolean(),
interleaved: z
.union([
z.literal(true),
z
.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
})
.strict(),
])
.optional(),
structured_output: z.boolean().optional(),
temperature: z.boolean().optional(),
knowledge: z
.string()
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
})
.optional(),
release_date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
}),
last_updated: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
message: "Must be in YYYY-MM or YYYY-MM-DD format",
}),
modalities: z.object({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
}),
open_weights: z.boolean(),
cost: Cost.extend({
context_over_200k: Cost.optional(),
}).optional(),
limit: z.object({
context: z.number().min(0, "Context window must be positive"),
input: z.number().min(0, "Input tokens must be positive").optional(),
output: z.number().min(0, "Output tokens must be positive"),
}),
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
experimental: z
.object({
modes: z
.record(
z.object({
cost: Cost.optional(),
provider: z
.object({
body: z.record(JsonValue).optional(),
headers: z.record(z.string()).optional(),
})
.optional(),
}),
)
.optional(),
})
.optional(),
provider: z
.object({
npm: z.string().optional(),
api: z.string().optional(),
shape: z.enum(["responses", "completions"]).optional(),
body: z.record(JsonValue).optional(),
headers: z.record(z.string()).optional(),
})
.optional(),
...ModelBase.shape,
cost: OutputCost.optional(),
})
.strict()
.refine(
(data) => {
return !(data.reasoning === false && data.cost?.reasoning !== undefined);
},
{
message: "Cannot set cost.reasoning when reasoning is false",
path: ["cost", "reasoning"],
},
);
.strict();
export const AuthoredModelShape = z
.object({
...ModelBase.shape,
cost: AuthoredCost.optional(),
})
.strict();
export const Model = refineModel(ModelShape);
export const AuthoredModel = refineModel(AuthoredModelShape);
export type Model = z.infer<typeof Model>;
-1
View File
@@ -6,7 +6,6 @@
"build": "./script/build.ts"
},
"dependencies": {
"@tanstack/virtual-core": "^3.14.0",
"hono": "^4.8.0",
"models.dev": "workspace:*"
},
+8 -32
View File
@@ -41,8 +41,6 @@
html,
body {
height: 100%;
overflow: hidden;
font-family: 'Rubik', sans-serif;
line-height: 1.6;
color: var(--color-text);
@@ -207,17 +205,12 @@ header {
}
}
.table-viewport {
height: calc(100svh - var(--header-height));
margin-top: var(--header-height);
overflow: auto;
}
table {
border-collapse: separate;
border-spacing: 0;
font-size: 0.875rem;
width: 100%;
margin-top: var(--header-height);
}
thead,
@@ -225,7 +218,7 @@ tbody {}
table thead th {
position: sticky;
top: 0;
top: var(--header-height);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
font-size: 0.75rem;
@@ -271,7 +264,6 @@ td {
text-align: left;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
height: 48px;
}
tbody {
@@ -327,37 +319,21 @@ tbody {
gap: 0.375rem;
}
.provider-cell img,
.provider-cell svg {
.provider-cell span:first-child {
flex: 0 0 auto;
}
.provider-cell svg {
display: block;
width: 1rem;
height: 1rem;
color: var(--color-text-secondary);
}
.virtual-spacer td {
height: inherit;
padding: 0;
border-bottom: 0;
}
.empty-row td {
padding: 2rem 0.75rem;
color: var(--color-text-tertiary);
}
.empty-row div {
position: sticky;
left: 0;
width: calc(100vw - 1.5rem);
text-align: center;
}
.model-id-cell {
display: flex;
align-items: center;
justify-content: flex-start;
justify-content: space-between;
gap: 0.375rem;
}
@@ -570,4 +546,4 @@ dialog {
}
}
}
}
+94 -295
View File
@@ -1,53 +1,7 @@
import {
Virtualizer,
elementScroll,
observeElementOffset,
observeElementRect,
} from "@tanstack/virtual-core";
import {
type TableRow,
renderRow,
escapeHtml,
booleanText,
knowledgeText,
weightsText,
} from "./shared.js";
declare global {
interface Window {
__TABLE_DATA__: TableRow[];
}
}
interface VirtualizedRow extends TableRow {
key: string;
searchText: string;
sortValues: Array<string | number | undefined>;
}
type SortDirection = "asc" | "desc";
const ESTIMATED_ROW_HEIGHT = 48;
const VIRTUAL_OVERSCAN = 5;
const modal = document.getElementById("modal") as HTMLDialogElement;
const modalClose = document.getElementById("close")!;
const help = document.getElementById("help")!;
const search = document.getElementById("search")! as HTMLInputElement;
const viewport = document.getElementById("table-viewport") as HTMLElement;
const tbody = document.getElementById(
"models-table-body"
) as HTMLTableSectionElement;
const headers = Array.from(document.querySelectorAll("th.sortable"));
const columnCount = document.querySelectorAll("thead th").length;
let isLoaded = false;
let allRows: VirtualizedRow[] = [];
let visibleRows: VirtualizedRow[] = [];
let currentSort: { column: number; direction: SortDirection } = {
column: -1,
direction: "asc",
};
/////////////////////////
// URL State Management
@@ -77,7 +31,10 @@ function getColumnNameForURL(headerEl: Element): string {
}
function getColumnIndexByUrlName(name: string): number {
return headers.findIndex((header) => getColumnNameForURL(header) === name);
const headers = document.querySelectorAll("th.sortable");
return Array.from(headers).findIndex(
(header) => getColumnNameForURL(header) === name
);
}
/////////////////////////
@@ -106,181 +63,32 @@ modal.addEventListener("click", (e) => {
});
////////////////////
// Row Data
// Handle Sorting
////////////////////
function lockColumnWidths() {
const ths = document.querySelectorAll("#models-table thead th");
const widths = Array.from(ths).map((th) => th.getBoundingClientRect().width);
let currentSort = { column: -1, direction: "asc" };
const measurementRow = tbody.querySelector('tr[aria-hidden="true"]');
if (measurementRow) measurementRow.remove();
function sortTable(column: number, direction: "asc" | "desc") {
const header = document.querySelectorAll("th.sortable")[column];
const columnType = header.getAttribute("data-type");
if (!columnType) return;
const table = document.getElementById("models-table")!;
table.style.tableLayout = "fixed";
// update state
currentSort = { column, direction };
updateQueryParams({
sort: getColumnNameForURL(header),
order: direction,
});
const colgroup = document.createElement("colgroup");
for (const width of widths) {
const col = document.createElement("col");
col.style.width = `${width}px`;
colgroup.appendChild(col);
}
table.insertBefore(colgroup, table.firstChild);
}
function prepareRow(row: TableRow): VirtualizedRow {
const sortValues: VirtualizedRow["sortValues"] = [
row.providerName,
row.modelName,
row.family,
row.providerId,
row.modelId,
booleanText(row.toolCall),
booleanText(row.reasoning),
row.input.length,
row.output.length,
row.inputCost,
row.outputCost,
row.reasoningCost,
row.cacheReadCost,
row.cacheWriteCost,
row.audioInputCost,
row.audioOutputCost,
row.contextLimit,
row.inputLimit,
row.outputLimit,
row.structuredOutput === undefined
? undefined
: booleanText(row.structuredOutput),
booleanText(row.temperature),
weightsText(row.openWeights),
row.knowledge ? knowledgeText(row.knowledge) : undefined,
row.releaseDate,
row.lastUpdated,
];
const searchableValues = [
row.providerName,
row.modelName,
row.family ?? "",
row.providerId,
row.modelId,
row.releaseDate,
row.lastUpdated,
];
return {
...row,
key: `${row.providerId}/${row.modelId}`,
searchText: searchableValues.join(" ").toLowerCase(),
sortValues,
};
}
////////////////////
// Virtual Table
////////////////////
function getVirtualizerOptions(count: number) {
return {
count,
getScrollElement: () => viewport,
estimateSize: () => ESTIMATED_ROW_HEIGHT,
getItemKey: (index: number) => visibleRows[index]?.key ?? index,
initialRect: {
width: viewport.clientWidth || window.innerWidth,
height: viewport.clientHeight || window.innerHeight,
},
overscan: VIRTUAL_OVERSCAN,
observeElementRect,
observeElementOffset,
scrollToFn: elementScroll,
onChange: () => renderVirtualRows(),
};
}
const virtualizer = new Virtualizer<HTMLElement, HTMLTableRowElement>(
getVirtualizerOptions(0)
);
const cleanupVirtualizer = virtualizer._didMount();
virtualizer._willUpdate();
window.addEventListener("pagehide", () => cleanupVirtualizer());
function renderStatusRow(message: string) {
tbody.innerHTML = `<tr class="empty-row"><td colspan="${columnCount}"><div>${escapeHtml(
message
)}</div></td></tr>`;
}
function setVirtualizerCount(count: number, resetScroll: boolean) {
virtualizer.setOptions(getVirtualizerOptions(count));
virtualizer._willUpdate();
if (resetScroll) virtualizer.scrollToOffset(0);
renderVirtualRows();
}
function renderVirtualRows() {
if (!isLoaded) return;
if (visibleRows.length === 0) {
renderStatusRow("No models found");
return;
}
const virtualRows = virtualizer.getVirtualItems();
if (virtualRows.length === 0) return;
const firstRow = virtualRows[0]!;
const lastRow = virtualRows[virtualRows.length - 1]!;
const paddingTop = firstRow.start;
const paddingBottom = Math.max(virtualizer.getTotalSize() - lastRow.end, 0);
const html: string[] = [];
if (paddingTop > 0) html.push(renderSpacerRow(paddingTop));
for (const virtualRow of virtualRows) {
const row = visibleRows[virtualRow.index];
if (row) html.push(renderRow(row, virtualRow.index));
}
if (paddingBottom > 0) html.push(renderSpacerRow(paddingBottom));
tbody.innerHTML = html.join("");
tbody.querySelectorAll<HTMLTableRowElement>("tr[data-index]").forEach((row) =>
virtualizer.measureElement(row)
);
}
function renderSpacerRow(height: number) {
return `<tr class="virtual-spacer" style="height: ${height}px"><td colspan="${columnCount}"></td></tr>`;
}
function applyRows(resetScroll = true) {
if (!isLoaded) return;
visibleRows = getRowsForDisplay();
setVirtualizerCount(visibleRows.length, resetScroll);
}
////////////////////
// Sorting
////////////////////
function getRowsForDisplay() {
const terms = search.value
.toLowerCase()
.split(",")
.map((term) => term.trim())
.filter(Boolean);
const filteredRows =
terms.length === 0
? allRows
: allRows.filter((row) =>
terms.some((term) => row.searchText.includes(term))
);
if (currentSort.column === -1) return filteredRows;
const columnType = headers[currentSort.column]?.getAttribute("data-type");
if (!columnType) return filteredRows;
return [...filteredRows].sort((a, b) => {
const aValue = a.sortValues[currentSort.column];
const bValue = b.sortValues[currentSort.column];
// sort rows
const tbody = document.querySelector("table tbody")!;
const rows = Array.from(
tbody.querySelectorAll("tr")
) as HTMLTableRowElement[];
rows.sort((a, b) => {
const aValue = getCellValue(a.cells[column], columnType);
const bValue = getCellValue(b.cells[column], columnType);
// Handle undefined values - always sort to bottom
if (aValue === undefined && bValue === undefined) return 0;
if (aValue === undefined) return 1;
if (bValue === undefined) return -1;
@@ -288,48 +96,45 @@ function getRowsForDisplay() {
let comparison = 0;
if (columnType === "number" || columnType === "modalities") {
comparison = (aValue as number) - (bValue as number);
} else if (columnType === "boolean") {
comparison = (aValue as string).localeCompare(bValue as string);
} else {
comparison = String(aValue).localeCompare(String(bValue));
comparison = (aValue as string).localeCompare(bValue as string);
}
return currentSort.direction === "asc" ? comparison : -comparison;
return direction === "asc" ? comparison : -comparison;
});
}
rows.forEach((row) => tbody.appendChild(row));
function sortTable(
column: number,
direction: SortDirection,
updateURL = true
) {
const header = headers[column];
if (!header?.getAttribute("data-type")) return;
currentSort = { column, direction };
if (updateURL) {
updateQueryParams({
sort: getColumnNameForURL(header),
order: direction,
});
}
updateSortIndicators();
applyRows();
}
function updateSortIndicators() {
// update sort indicators
const headers = document.querySelectorAll("th.sortable");
headers.forEach((header, i) => {
const indicator = header.querySelector(".sort-indicator")!;
indicator.textContent =
i === currentSort.column
? currentSort.direction === "asc"
? "↑"
: ""
: "";
if (i === column) {
indicator.textContent = direction === "asc" ? "↑" : "↓";
} else {
indicator.textContent = "";
}
});
}
headers.forEach((header, column) => {
function getCellValue(
cell: HTMLTableCellElement,
type: string
): string | number | undefined {
if (type === "modalities")
return cell.querySelectorAll(".modality-icon").length;
const text = cell.textContent?.trim() || "";
if (text === "-") return;
if (type === "number") return parseFloat(text.replace(/[$,]/g, "")) || 0;
return text;
}
document.querySelectorAll("th.sortable").forEach((header) => {
header.addEventListener("click", () => {
const column = Array.from(header.parentElement!.children).indexOf(header);
const direction =
currentSort.column === column && currentSort.direction === "asc"
? "desc"
@@ -339,19 +144,34 @@ headers.forEach((header, column) => {
});
///////////////////
// Search
// Handle Search
///////////////////
function filterTable(value: string) {
const lowerCaseValues = value.toLowerCase().split(",").filter(str => str.trim() !== "");
const rows = document.querySelectorAll(
"table tbody tr"
) as NodeListOf<HTMLTableRowElement>;
rows.forEach((row) => {
const cellTexts = Array.from(row.cells).map((cell) =>
cell.textContent!.toLowerCase()
);
const isVisible = lowerCaseValues.length === 0 ||
lowerCaseValues.some((lowerCaseValue) => cellTexts.some((text) => text.includes(lowerCaseValue)));
row.style.display = isVisible ? "" : "none";
});
updateQueryParams({ search: value || null });
}
search.addEventListener("input", () => {
updateQueryParams({ search: search.value || null });
applyRows();
filterTable(search.value);
});
document.addEventListener("keydown", (e) => {
const key = e.key.toLowerCase();
if ((e.metaKey || e.ctrlKey) && (key === "k" || key === "f")) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
search.focus();
search.select();
}
});
@@ -365,7 +185,10 @@ search.addEventListener("keydown", (e) => {
///////////////////////////////////
// Handle Copy model ID function
///////////////////////////////////
async function copyModelId(button: HTMLButtonElement, modelId: string) {
(window as any).copyModelId = async (
button: HTMLButtonElement,
modelId: string
) => {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(modelId);
@@ -386,18 +209,7 @@ async function copyModelId(button: HTMLButtonElement, modelId: string) {
} catch (err) {
console.error("Failed to copy text: ", err);
}
}
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest<HTMLButtonElement>(
".copy-button[data-model-id]"
);
if (!button) return;
const modelId = button.dataset.modelId;
if (modelId) void copyModelId(button, modelId);
});
};
///////////////////////////////////
// Initialize State from URL
@@ -405,37 +217,24 @@ document.addEventListener("click", (event) => {
function initializeFromURL() {
const params = getQueryParams();
search.value = params.get("search") ?? "";
(() => {
const searchQuery = params.get("search");
if (!searchQuery) return;
search.value = searchQuery;
filterTable(searchQuery);
})();
(() => {
const columnName = params.get("sort");
if (!columnName) return;
currentSort = { column: -1, direction: "asc" };
const columnName = params.get("sort");
if (columnName) {
const columnIndex = getColumnIndexByUrlName(columnName);
if (columnIndex !== -1) {
currentSort = {
column: columnIndex,
direction: params.get("order") === "desc" ? "desc" : "asc",
};
}
}
if (columnIndex === -1) return;
updateSortIndicators();
applyRows(false);
const direction = (params.get("order") as "asc" | "desc") || "asc";
sortTable(columnIndex, direction);
})();
}
function loadRows() {
try {
allRows = window.__TABLE_DATA__.map(prepareRow);
lockColumnWidths();
isLoaded = true;
initializeFromURL();
} catch (error) {
console.error(error);
isLoaded = true;
visibleRows = [];
renderStatusRow("Failed to load model data");
}
}
loadRows();
document.addEventListener("DOMContentLoaded", initializeFromURL);
window.addEventListener("popstate", initializeFromURL);
+232 -55
View File
@@ -6,7 +6,6 @@ import { Fragment } from "hono/jsx";
import { renderToString } from "hono/jsx/dom/server";
import { existsSync } from "fs";
import path from "path";
import { type TableRow, renderRow, getLargestRow } from "./shared.js";
export const Providers = await generate(
path.join(import.meta.dir, "..", "..", "..", "providers")
@@ -39,6 +38,7 @@ const loadProviderSvg = async (providerId: string): Promise<string | null> => {
const file = Bun.file(providerLogoPath);
return await file.text();
}
//
// Fall back to default logo
if (existsSync(defaultLogoPath)) {
const file = Bun.file(defaultLogoPath);
@@ -62,47 +62,122 @@ for (const [providerId] of Object.entries(Providers)) {
}
}
export const INITIAL_ROW_COUNT = 50;
function renderProviderLogo(providerId: string) {
const svgContent = providerLogos.get(providerId) || "";
export const TableRows: TableRow[] = Object.entries(Providers)
.sort(([, providerA], [, providerB]) =>
providerA.name.localeCompare(providerB.name)
)
.flatMap(([providerId, provider]) =>
Object.entries(provider.models)
.filter(([, model]) => model.status !== "alpha")
.sort(([, modelA], [, modelB]) => modelA.name.localeCompare(modelB.name))
.map(([modelId, model]) => ({
providerId,
providerName: provider.name,
providerLogoSvg: providerLogos.get(providerId) || "",
modelId,
modelName: model.name,
family: model.family,
toolCall: model.tool_call,
reasoning: model.reasoning,
input: model.modalities.input,
output: model.modalities.output,
inputCost: model.cost?.input,
outputCost: model.cost?.output,
reasoningCost: model.cost?.reasoning,
cacheReadCost: model.cost?.cache_read,
cacheWriteCost: model.cost?.cache_write,
audioInputCost: model.cost?.input_audio,
audioOutputCost: model.cost?.output_audio,
contextLimit: model.limit.context,
inputLimit: model.limit.input,
outputLimit: model.limit.output,
structuredOutput: model.structured_output,
temperature: model.temperature ?? false,
openWeights: model.open_weights,
knowledge: model.knowledge,
releaseDate: model.release_date,
lastUpdated: model.last_updated,
}))
);
return <span dangerouslySetInnerHTML={{ __html: svgContent }} />;
}
const largestRow = getLargestRow(TableRows);
const getModalityIcon = (modality: string) => {
switch (modality) {
case "text":
return (
<span class="modality-icon" data-tooltip="Text">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="4,7 4,4 20,4 20,7"></polyline>
<line x1="9" y1="20" x2="15" y2="20"></line>
<line x1="12" y1="4" x2="12" y2="20"></line>
</svg>
</span>
);
case "image":
return (
<span class="modality-icon" data-tooltip="Image">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
<circle cx="9" cy="9" r="2"></circle>
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
</svg>
</span>
);
case "audio":
return (
<span class="modality-icon" data-tooltip="Audio">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="m19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>
</span>
);
case "video":
return (
<span class="modality-icon" data-tooltip="Video">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m22 8-6 4 6 4V8Z"></path>
<rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect>
</svg>
</span>
);
case "pdf":
return (
<span class="modality-icon" data-tooltip="PDF">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10,9 9,9 8,9"></polyline>
</svg>
</span>
);
default:
return null;
}
};
const renderCost = (cost?: number) => {
return cost === undefined ? "-" : `$${cost.toFixed(2)}`;
};
export const Rendered = renderToString(
<Fragment>
@@ -138,8 +213,7 @@ export const Rendered = renderToString(
<button id="help">How to use</button>
</div>
</header>
<div id="table-viewport" class="table-viewport">
<table id="models-table">
<table>
<thead>
<tr>
<th class="sortable" data-type="text">
@@ -268,12 +342,120 @@ export const Rendered = renderToString(
</th>
</tr>
</thead>
<tbody id="models-table-body" dangerouslySetInnerHTML={{
__html: TableRows.slice(0, INITIAL_ROW_COUNT).map((row, i) => renderRow(row, i)).join('')
+ renderRow(largestRow, -1).replace('<tr', '<tr style="visibility:hidden" aria-hidden="true"')
}} />
</table>
</div>
<tbody>
{Object.entries(Providers)
.sort(([, providerA], [, providerB]) =>
providerA.name.localeCompare(providerB.name)
)
.flatMap(([providerId, provider]) =>
Object.entries(provider.models)
.filter(([, model]) => model.status !== "alpha")
.sort(([, modelA], [, modelB]) =>
modelA.name.localeCompare(modelB.name)
)
.map(([modelId, model]) => (
<tr key={`${providerId}-${modelId}`}>
<td>
<div class="provider-cell">
{renderProviderLogo(providerId)}
<span>{provider.name}</span>
</div>
</td>
<td>{model.name}</td>
<td>{model.family ?? "-"}</td>
<td>{providerId}</td>
<td>
<div class="model-id-cell">
<span class="model-id-text">{modelId}</span>
<button
class="copy-button"
onclick={`copyModelId(this, '${modelId}')`}
>
<svg
class="copy-icon"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/>
<path d="m4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
<svg
class="check-icon"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="display: none;"
>
<polyline points="20,6 9,17 4,12" />
</svg>
</button>
</div>
</td>
<td>{model.tool_call ? "Yes" : "No"}</td>
<td>{model.reasoning ? "Yes" : "No"}</td>
<td>
<div class="modalities">
{model.modalities.input.map((modality) =>
getModalityIcon(modality)
)}
</div>
</td>
<td>
<div class="modalities">
{model.modalities.output.map((modality) =>
getModalityIcon(modality)
)}
</div>
</td>
<td>{renderCost(model.cost?.input)}</td>
<td>{renderCost(model.cost?.output)}</td>
<td>{renderCost(model.cost?.reasoning)}</td>
<td>{renderCost(model.cost?.cache_read)}</td>
<td>{renderCost(model.cost?.cache_write)}</td>
<td>{renderCost(model.cost?.input_audio)}</td>
<td>{renderCost(model.cost?.output_audio)}</td>
<td>{model.limit.context.toLocaleString()}</td>
<td>{model.limit.input?.toLocaleString() ?? "-"}</td>
<td>{model.limit.output.toLocaleString()}</td>
<td>
{model.structured_output === undefined
? "-"
: model.structured_output
? "Yes"
: "No"}
</td>
<td>{model.temperature ? "Yes" : "No"}</td>
<td>{model.open_weights ? "Open" : "Closed"}</td>
<td>
{model.knowledge ? model.knowledge.substring(0, 7) : "-"}
</td>
<td>{model.release_date}</td>
<td>{model.last_updated}</td>
</tr>
))
)}
</tbody>
</table>
<dialog id="modal">
<div class="header">
<h2>How to use</h2>
@@ -383,15 +565,10 @@ export const Rendered = renderToString(
>
Edit on GitHub
</a>
<a href="https://opencode.ai" target="_blank" rel="noopener noreferrer">
Created by OpenCode
<a href="https://sst.dev" target="_blank" rel="noopener noreferrer">
Created by SST
</a>
</div>
</dialog>
<script
dangerouslySetInnerHTML={{
__html: `window.__TABLE_DATA__ = ${JSON.stringify(TableRows)}`,
}}
></script>
</Fragment>
);
-176
View File
@@ -1,176 +0,0 @@
export interface TableRow {
providerId: string;
providerName: string;
providerLogoSvg: string;
modelId: string;
modelName: string;
family?: string;
toolCall: boolean;
reasoning: boolean;
input: string[];
output: string[];
inputCost?: number;
outputCost?: number;
reasoningCost?: number;
cacheReadCost?: number;
cacheWriteCost?: number;
audioInputCost?: number;
audioOutputCost?: number;
contextLimit: number;
inputLimit?: number;
outputLimit: number;
structuredOutput?: boolean;
temperature: boolean;
openWeights: boolean;
knowledge?: string;
releaseDate: string;
lastUpdated: string;
}
const MODALITY_ICONS: Record<string, string> = {
text: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4,7 4,4 20,4 20,7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg>`,
image: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg>`,
audio: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="m19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>`,
video: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect></svg>`,
pdf: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14,2 14,8 20,8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10,9 9,9 8,9"></polyline></svg>`,
};
export function escapeHtml(value: string | number) {
return String(value).replace(/[&<>'"]/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case "'":
return "&#39;";
case '"':
return "&quot;";
default:
return char;
}
});
}
export function booleanText(value: boolean) {
return value ? "Yes" : "No";
}
export function optionalBooleanText(value?: boolean) {
return value === undefined ? "-" : booleanText(value);
}
export function formatCost(cost?: number) {
return cost === undefined ? "-" : `$${cost.toFixed(2)}`;
}
export function formatNumber(value?: number) {
return value === undefined ? "-" : value.toLocaleString();
}
export function knowledgeText(value?: string) {
return value ? value.substring(0, 7) : "-";
}
export function weightsText(value: boolean) {
return value ? "Open" : "Closed";
}
export function renderModalityIcon(modality: string) {
const label =
modality === "pdf"
? "PDF"
: modality[0]!.toUpperCase() + modality.slice(1);
const icon = MODALITY_ICONS[modality];
if (!icon) return "";
return `<span class="modality-icon" data-tooltip="${label}">${icon}</span>`;
}
export function renderModalities(modalities: string[]) {
return `<div class="modalities">${modalities
.map(renderModalityIcon)
.join("")}</div>`;
}
export function renderCopyButton(modelId: string) {
const escapedModelId = escapeHtml(modelId);
return `<button type="button" class="copy-button" data-model-id="${escapedModelId}" aria-label="Copy model ID"><svg class="copy-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="m4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg><svg class="check-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><polyline points="20,6 9,17 4,12"></polyline></svg></button>`;
}
export function renderRow(row: TableRow, index: number) {
return `<tr data-index="${index}">
<td><div class="provider-cell">${row.providerLogoSvg}<span>${escapeHtml(
row.providerName
)}</span></div></td>
<td>${escapeHtml(row.modelName)}</td>
<td>${escapeHtml(row.family ?? "-")}</td>
<td>${escapeHtml(row.providerId)}</td>
<td><div class="model-id-cell"><span class="model-id-text">${escapeHtml(
row.modelId
)}</span>${renderCopyButton(row.modelId)}</div></td>
<td>${booleanText(row.toolCall)}</td>
<td>${booleanText(row.reasoning)}</td>
<td>${renderModalities(row.input)}</td>
<td>${renderModalities(row.output)}</td>
<td>${formatCost(row.inputCost)}</td>
<td>${formatCost(row.outputCost)}</td>
<td>${formatCost(row.reasoningCost)}</td>
<td>${formatCost(row.cacheReadCost)}</td>
<td>${formatCost(row.cacheWriteCost)}</td>
<td>${formatCost(row.audioInputCost)}</td>
<td>${formatCost(row.audioOutputCost)}</td>
<td>${formatNumber(row.contextLimit)}</td>
<td>${formatNumber(row.inputLimit)}</td>
<td>${formatNumber(row.outputLimit)}</td>
<td>${optionalBooleanText(row.structuredOutput)}</td>
<td>${booleanText(row.temperature)}</td>
<td>${weightsText(row.openWeights)}</td>
<td>${knowledgeText(row.knowledge)}</td>
<td>${escapeHtml(row.releaseDate)}</td>
<td>${escapeHtml(row.lastUpdated)}</td>
</tr>`;
}
export function getLargestRow(rows: TableRow[]): TableRow {
const worst: TableRow = {
providerId: "", providerName: "", providerLogoSvg: "", modelId: "", modelName: "",
toolCall: true, reasoning: true,
input: [], output: [],
contextLimit: 0, outputLimit: 0,
structuredOutput: true, temperature: true, openWeights: false,
releaseDate: "", lastUpdated: "",
};
for (const row of rows) {
if (row.providerName.length > worst.providerName.length) worst.providerName = row.providerName;
if (row.modelName.length > worst.modelName.length) worst.modelName = row.modelName;
if ((row.family ?? "").length > (worst.family ?? "").length) worst.family = row.family;
if (row.providerId.length > worst.providerId.length) worst.providerId = row.providerId;
if (row.modelId.length > worst.modelId.length) worst.modelId = row.modelId;
if ((row.knowledge ?? "").length > (worst.knowledge ?? "").length) worst.knowledge = row.knowledge;
if (row.releaseDate.length > worst.releaseDate.length) worst.releaseDate = row.releaseDate;
if (row.lastUpdated.length > worst.lastUpdated.length) worst.lastUpdated = row.lastUpdated;
if (row.input.length > worst.input.length) worst.input = row.input;
if (row.output.length > worst.output.length) worst.output = row.output;
const costWider = (a: number | undefined, b: number | undefined) =>
b !== undefined && (a === undefined || formatCost(b).length > formatCost(a).length);
if (costWider(worst.inputCost, row.inputCost)) worst.inputCost = row.inputCost;
if (costWider(worst.outputCost, row.outputCost)) worst.outputCost = row.outputCost;
if (costWider(worst.reasoningCost, row.reasoningCost)) worst.reasoningCost = row.reasoningCost;
if (costWider(worst.cacheReadCost, row.cacheReadCost)) worst.cacheReadCost = row.cacheReadCost;
if (costWider(worst.cacheWriteCost, row.cacheWriteCost)) worst.cacheWriteCost = row.cacheWriteCost;
if (costWider(worst.audioInputCost, row.audioInputCost)) worst.audioInputCost = row.audioInputCost;
if (costWider(worst.audioOutputCost, row.audioOutputCost)) worst.audioOutputCost = row.audioOutputCost;
const numWider = (a: number | undefined, b: number | undefined) =>
b !== undefined && (a === undefined || formatNumber(b).length > formatNumber(a).length);
if (numWider(worst.contextLimit as number | undefined, row.contextLimit)) worst.contextLimit = row.contextLimit;
if (numWider(worst.inputLimit, row.inputLimit)) worst.inputLimit = row.inputLimit;
if (numWider(worst.outputLimit as number | undefined, row.outputLimit)) worst.outputLimit = row.outputLimit;
}
return worst;
}
+2 -1
View File
@@ -15,7 +15,8 @@ output = 25.000
cache_read = 0.500
cache_write = 6.250
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 200_000 }
input = 10.000
output = 37.500
cache_read = 1.000
+2 -1
View File
@@ -16,7 +16,8 @@ output = 180.000
cache_read = 0
cache_write = 0
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 272_000 }
input = 60.000
output = 270.000
+2 -1
View File
@@ -16,7 +16,8 @@ output = 15.000
cache_read = 0.250
cache_write = 0
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 272_000 }
input = 5.000
output = 22.500
@@ -0,0 +1,25 @@
name = "Claude Opus 4.7 Thinking"
family = "claude-opus"
release_date = "2026-04-16"
last_updated = "2026-04-16"
attachment = true
reasoning = true
temperature = false
tool_call = true
structured_output = true
knowledge = "2026-01-31"
open_weights = false
[cost]
input = 5
output = 25
cache_read = 0.5
cache_write = 6.25
[limit]
context = 200_000
output = 32_000
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -15,7 +15,8 @@ output = 15.00
cache_read = 0.30
cache_write = 3.75
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 200_000 }
input = 6.00
output = 22.50
cache_read = 0.60
@@ -15,7 +15,8 @@ output = 15.00
cache_read = 0.30
cache_write = 3.75
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 200_000 }
input = 6.00
output = 22.50
cache_read = 0.60
@@ -1,24 +0,0 @@
name = "Coding-GLM-5-Free"
family = "glm"
release_date = "2026-02-11"
last_updated = "2026-02-11"
attachment = false
reasoning = true
temperature = true
tool_call = true
open_weights = false
[interleaved]
field = "reasoning_content"
[cost]
input = 0.0
output = 0.0
[limit]
context = 204800
output = 131072
[modalities]
input = ["text"]
output = ["text"]
@@ -1,25 +1,26 @@
name = "GLM 4.5 Air"
name = "Coding GLM 5.1 (free)"
family = "glm"
release_date = "2025-12-29"
last_updated = "2026-01-10"
release_date = "2026-04-11"
last_updated = "2026-04-11"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
open_weights = false
[interleaved]
field = "reasoning_content"
[cost]
input = 0.05
output = 0.22
input = 0
output = 0
cache_read = 0
[limit]
context = 131_072
output = 131_072
context = 204_800
output = 128_000
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,23 +1,21 @@
name = "MiniMax-M2.1"
name = "Coding MiniMax M2.7 Highspeed"
family = "minimax"
release_date = "2025-12-23"
last_updated = "2025-12-23"
release_date = "2026-03-18"
last_updated = "2026-03-18"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[interleaved]
field = "reasoning_details"
[cost]
input = 0.288
output = 1.152
input = 0.2
output = 0.2
[limit]
context = 204_800
output = 192_000
output = 13_100
[modalities]
input = ["text"]
@@ -1,20 +1,21 @@
name = "MiniMax-M2.1"
name = "Coding MiniMax M2.7"
family = "minimax"
release_date = "2025-12-23"
last_updated = "2025-12-23"
release_date = "2026-03-18"
last_updated = "2026-03-18"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.0
output = 0.0
input = 0.2
output = 0.2
[limit]
context = 204_800
output = 131_072
output = 13_100
[modalities]
input = ["text"]
@@ -1,4 +1,4 @@
name = "Gemini 3.1 Flash Lite Preview"
name = "Gemini 3.1 Flash Lite"
family = "gemini-flash-lite"
release_date = "2026-03-03"
last_updated = "2026-03-03"
@@ -0,0 +1,30 @@
name = "Gemini 3.1 Pro Preview Custom Tools"
family = "gemini-pro"
release_date = "2026-02-19"
last_updated = "2026-02-19"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01"
open_weights = false
[cost]
input = 2
output = 12
cache_read = 0.2
[[cost.tiers]]
tier = { size = 200_000 }
input = 4
output = 18
cache_read = 0.4
[limit]
context = 1_048_576
output = 65_536
[modalities]
input = ["text", "image", "audio", "video", "pdf"]
output = ["text"]
-25
View File
@@ -1,25 +0,0 @@
name = "GLM-5"
family = "glm"
release_date = "2026-02-11"
last_updated = "2026-02-11"
attachment = false
reasoning = true
temperature = true
tool_call = true
open_weights = true
[interleaved]
field = "reasoning_content"
[cost]
input = 0.88
output = 2.816
cache_read = 0.176
[limit]
context = 202_752
output = 0
[modalities]
input = ["text"]
output = ["text"]
@@ -0,0 +1,26 @@
name = "GLM 5 Vision Turbo"
family = "glm"
release_date = "2026-05-09"
last_updated = "2026-05-09"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[interleaved]
field = "reasoning_content"
[cost]
input = 0.7042
output = 3.09848
cache_read = 0.169008
[limit]
context = 200_000
output = 128_000
[modalities]
input = ["text", "image", "video"]
output = ["text"]
+29
View File
@@ -0,0 +1,29 @@
name = "Grok 4.3"
family = "grok"
release_date = "2026-05-01"
last_updated = "2026-05-01"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[cost]
input = 1.25
output = 2.5
cache_read = 0.2
[[cost.tiers]]
tier = { size = 200_000 }
input = 2.5
output = 5.0
cache_read = 0.4
[limit]
context = 1_000_000
output = 1_000_000
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,24 +0,0 @@
name = "Qwen3 Coder Plus"
family = "qwen"
release_date = "2025-07-23"
last_updated = "2025-07-23"
attachment = false
reasoning = false
temperature = true
tool_call = true
knowledge = "2025-04"
open_weights = true
[cost]
input = 0.137
output = 0.548
cache_read = 0.137
[limit]
context = 2_000_000
output = 64_000
input = 262_144
[modalities]
input = ["text"]
output = ["text"]
@@ -1,23 +0,0 @@
name = "Qwen3 Max"
family = "qwen"
release_date = "2025-09-23"
last_updated = "2025-09-23"
attachment = false
reasoning = false
temperature = true
tool_call = true
knowledge = "2025-04"
open_weights = false
[cost]
input = 0.34246
output = 1.36984
cache_read = 0.34246
[limit]
context = 252_000
output = 32_000
[modalities]
input = ["text"]
output = ["text"]
+5 -4
View File
@@ -1,11 +1,12 @@
name = "Qwen3.6 Plus"
name = "Qwen3.6 Flash"
family = "qwen"
release_date = "2026-04-02"
last_updated = "2026-04-02"
attachment = false
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-04"
open_weights = false
@@ -16,8 +17,8 @@ cache_read = 0.0169
cache_write = 0.21125
[limit]
context = 1_000_000
output = 65_536
context = 991_000
output = 64_000
[modalities]
input = ["text", "image", "video"]
@@ -0,0 +1,25 @@
name = "Qwen3.6 Max Preview"
family = "qwen"
release_date = "2026-05-09"
last_updated = "2026-05-09"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-04"
open_weights = false
[cost]
input = 1.268
output = 7.608
cache_read = 0.1268
cache_write = 1.585
[limit]
context = 240_000
output = 64_000
[modalities]
input = ["text"]
output = ["text"]
@@ -1,19 +1,20 @@
name = "Qwen3.5 Plus"
name = "Qwen3.6 Plus"
family = "qwen"
release_date = "2026-02-16"
last_updated = "2026-02-16"
attachment = false
release_date = "2026-05-09"
last_updated = "2026-05-09"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-04"
open_weights = false
[cost]
input = 0.1096
output = 0.6576
cache_read = 0.01096
cache_write = 0.137
input = 0.282
output = 1.692
cache_read = 0.0282
cache_write = 0.3525
[limit]
context = 991_000
+11 -4
View File
@@ -10,10 +10,17 @@ tool_call = true
open_weights = false
[cost]
input = 0.276
output = 1.651
cache_read = 0.028
cache_write = 0.344
input = 0.50
output = 3.00
cache_read = 0.05
cache_write = 0.625
[[cost.tiers]]
tier = { size = 256_000 }
input = 2.00
output = 6.00
cache_read = 0.20
cache_write = 2.50
[limit]
context = 1_000_000
+11 -4
View File
@@ -10,10 +10,17 @@ tool_call = true
open_weights = false
[cost]
input = 0.276
output = 1.651
cache_read = 0.028
cache_write = 0.344
input = 0.50
output = 3.00
cache_read = 0.05
cache_write = 0.625
[[cost.tiers]]
tier = { size = 256_000 }
input = 2.00
output = 6.00
cache_read = 0.20
cache_write = 2.50
[limit]
context = 1_000_000
@@ -1,22 +0,0 @@
name = "Nova Premier"
family = "nova"
release_date = "2024-12-03"
last_updated = "2024-12-03"
attachment = true
reasoning = true
temperature = true
knowledge = "2024-10"
tool_call = true
open_weights = false
[cost]
input = 2.50
output = 12.50
[limit]
context = 1_000_000
output = 16_384
[modalities]
input = ["text", "image", "video"]
output = ["text"]
@@ -1,2 +0,0 @@
[extends]
from = "anthropic/claude-3-5-haiku-20241022"
@@ -1,2 +0,0 @@
[extends]
from = "anthropic/claude-3-5-sonnet-20240620"
@@ -1,24 +0,0 @@
name = "Claude Sonnet 3.5 v2"
family = "claude-sonnet"
release_date = "2024-10-22"
last_updated = "2024-10-22"
attachment = true
reasoning = false
temperature = true
knowledge = "2024-04"
tool_call = true
open_weights = false
[cost]
input = 3.00
output = 15.00
cache_read = 0.30
cache_write = 3.75
[limit]
context = 200_000
output = 8_192
[modalities]
input = ["text", "image", "pdf"]
output = ["text"]
@@ -1,24 +0,0 @@
name = "Claude Sonnet 3.7"
family = "claude-sonnet"
release_date = "2025-02-19"
last_updated = "2025-02-19"
attachment = true
reasoning = false
temperature = true
knowledge = "2024-04"
tool_call = true
open_weights = false
[cost]
input = 3.00
output = 15.00
cache_read = 0.30
cache_write = 3.75
[limit]
context = 200_000
output = 8_192
[modalities]
input = ["text", "image", "pdf"]
output = ["text"]
@@ -1,22 +0,0 @@
name = "Claude Haiku 3"
family = "claude-haiku"
release_date = "2024-03-13"
last_updated = "2024-03-13"
attachment = true
reasoning = false
temperature = true
knowledge = "2024-02"
tool_call = true
open_weights = false
[cost]
input = 0.25
output = 1.25
[limit]
context = 200_000
output = 4_096
[modalities]
input = ["text", "image", "pdf"]
output = ["text"]
@@ -1,2 +0,0 @@
[extends]
from = "anthropic/claude-opus-4-20250514"
@@ -1,2 +0,0 @@
[extends]
from = "anthropic/claude-sonnet-4-20250514"
@@ -0,0 +1,4 @@
name = "Claude Haiku 4.5 (AU)"
[extends]
from = "anthropic/claude-haiku-4-5-20251001"
@@ -0,0 +1,4 @@
name = "Claude Sonnet 4.5 (AU)"
[extends]
from = "anthropic/claude-sonnet-4-5"
@@ -1,4 +0,0 @@
name = "Claude Sonnet 4 (EU)"
[extends]
from = "anthropic/claude-sonnet-4-20250514"
@@ -1,4 +0,0 @@
name = "Claude Sonnet 4 (Global)"
[extends]
from = "anthropic/claude-sonnet-4-20250514"
@@ -0,0 +1,4 @@
name = "Claude Opus 4.7 (JP)"
[extends]
from = "anthropic/claude-opus-4-7"
@@ -0,0 +1,4 @@
name = "Claude Sonnet 4.5 (JP)"
[extends]
from = "anthropic/claude-sonnet-4-5"
@@ -0,0 +1,4 @@
name = "Claude Sonnet 4.6 (JP)"
[extends]
from = "anthropic/claude-sonnet-4-6"
@@ -1,4 +0,0 @@
name = "Claude Opus 4 (US)"
[extends]
from = "anthropic/claude-opus-4-20250514"
@@ -1,4 +0,0 @@
name = "Claude Sonnet 4 (US)"
[extends]
from = "anthropic/claude-sonnet-4-20250514"
@@ -0,0 +1,4 @@
name = "DeepSeek-R1 (US)"
[extends]
from = "amazon-bedrock/deepseek.r1-v1:0"
@@ -0,0 +1,4 @@
name = "Llama 4 Maverick 17B Instruct (US)"
[extends]
from = "amazon-bedrock/meta.llama4-maverick-17b-instruct-v1:0"
@@ -0,0 +1,4 @@
name = "Llama 4 Scout 17B Instruct (US)"
[extends]
from = "amazon-bedrock/meta.llama4-scout-17b-instruct-v1:0"
@@ -15,7 +15,8 @@ output = 25.00
cache_read = 0.50
cache_write = 6.25
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 200_000 }
input = 10.00
output = 37.50
cache_read = 1.00
+2 -1
View File
@@ -15,7 +15,8 @@ output = 25.00
cache_read = 0.50
cache_write = 6.25
[cost.context_over_200k]
[[cost.tiers]]
tier = { size = 200_000 }
input = 10.00
output = 37.50
cache_read = 1.00
@@ -1,25 +0,0 @@
name = "MiniMax M2.1 TEE"
family = "minimax"
release_date = "2025-12-29"
last_updated = "2026-01-27"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.27
output = 1.12
[limit]
context = 196_608
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "MiniMax M2.5 TEE"
family = "minimax"
release_date = "2026-02-15"
last_updated = "2026-02-15"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -10,9 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.10
cache_read = 0.15
input = 0.15
output = 1.2
cache_read = 0.075
[limit]
context = 196_608
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepHermes 3 Mistral 24B Preview"
family = "nousresearch"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.02
output = 0.10
input = 0.0245
output = 0.0978
cache_read = 0.01225
[limit]
context = 32_768
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Hermes 4 14B"
family = "nousresearch"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.01
output = 0.05
input = 0.0136
output = 0.0543
cache_read = 0.0068
[limit]
context = 40_960
@@ -1,25 +0,0 @@
name = "Hermes 4 405B FP8 TEE"
family = "nousresearch"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.20
[limit]
context = 131_072
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,25 +0,0 @@
name = "Hermes 4 70B"
family = "nousresearch"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.11
output = 0.38
[limit]
context = 131_072
output = 131_072
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,22 +0,0 @@
name = "InternVL3 78B TEE"
family = "opengvlab"
release_date = "2025-01-06"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = true
open_weights = true
[cost]
input = 0.10
output = 0.39
[limit]
context = 32_768
output = 32_768
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen2.5 72B Instruct"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.13
output = 0.52
input = 0.2989
output = 1.1957
cache_read = 0.14945
[limit]
context = 32_768
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen2.5 Coder 32B Instruct"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.03
output = 0.11
input = 0.0272
output = 0.1087
cache_read = 0.0136
[limit]
context = 32_768
@@ -1,8 +1,10 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen2.5 VL 32B Instruct"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
last_updated = "2026-04-25"
attachment = true
reasoning = false
temperature = true
tool_call = false
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.05
output = 0.22
input = 0.0543
output = 0.2174
cache_read = 0.02715
[limit]
context = 16_384
@@ -1,22 +0,0 @@
name = "Qwen2.5 VL 72B Instruct TEE"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = true
open_weights = true
[cost]
input = 0.15
output = 0.60
[limit]
context = 32_768
output = 32_768
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 235B A22B Instruct 2507 TEE"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,9 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.08
output = 0.55
cache_read = 0.04
input = 0.1
output = 0.6
cache_read = 0.05
[limit]
context = 262_144
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 235B A22B Thinking 2507"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -11,7 +13,8 @@ open_weights = true
[cost]
input = 0.11
output = 0.60
output = 0.6
cache_read = 0.055
[limit]
context = 262_144
@@ -1,25 +0,0 @@
name = "Qwen3 235B A22B"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.20
[limit]
context = 40_960
output = 40_960
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,22 +0,0 @@
name = "Qwen3 30B A3B Instruct 2507"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.08
output = 0.33
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 30B A3B"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -12,6 +14,7 @@ open_weights = true
[cost]
input = 0.06
output = 0.22
cache_read = 0.03
[limit]
context = 40_960
@@ -1,7 +1,9 @@
name = "Qwen3 32B"
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 32B TEE"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
release_date = "2026-04-25"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -21,6 +23,3 @@ output = 40_960
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,23 +0,0 @@
name = "Qwen3 Coder 480B A35B Instruct FP8 TEE"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.22
output = 0.95
cache_read = 0.11
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text"]
output = ["text"]
@@ -0,0 +1,25 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 Coder Next TEE"
family = "qwen"
release_date = "2026-04-25"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.12
output = 0.75
cache_read = 0.06
[limit]
context = 262_144
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
@@ -1,22 +0,0 @@
name = "Qwen3 Coder Next"
family = "qwen"
release_date = "2026-02-05"
last_updated = "2026-02-05"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.07
output = 0.30
[limit]
context = 262_144
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3 Next 80B A3B Instruct"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.10
output = 0.80
input = 0.1
output = 0.8
cache_read = 0.05
[limit]
context = 262_144
@@ -1,22 +0,0 @@
name = "Qwen3 VL 235B A22B Instruct"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.20
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3.5 397B A17B TEE"
family = "qwen"
release_date = "2026-02-18"
last_updated = "2026-02-18"
last_updated = "2026-04-25"
attachment = true
reasoning = true
temperature = true
@@ -0,0 +1,25 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3.6 27B TEE"
family = "qwen"
release_date = "2026-04-25"
last_updated = "2026-04-25"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.195
output = 1.56
cache_read = 0.0975
[limit]
context = 262_144
output = 65_536
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,17 +1,18 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Qwen3Guard Gen 0.6B"
family = "qwen"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = false
open_weights = true
[cost]
input = 0.01
output = 0.01
output = 0.0109
cache_read = 0.005
[limit]
@@ -0,0 +1,25 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "MiMo V2 Flash TEE"
family = "mimo"
release_date = "2026-04-25"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.09
output = 0.29
cache_read = 0.045
[limit]
context = 262_144
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
@@ -1,22 +0,0 @@
name = "MiMo V2 Flash"
family = "mimo"
release_date = "2025-12-29"
last_updated = "2026-01-27"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = false
open_weights = true
[cost]
input = 0.09
output = 0.29
[limit]
context = 262_144
output = 32_000
[modalities]
input = ["text"]
output = ["text"]
@@ -1,23 +0,0 @@
name = "Mistral Small 3.1 24B Instruct 2503"
family = "chutesai"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.03
output = 0.11
cache_read = 0.015
[limit]
context = 131_072
output = 131_072
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,22 +0,0 @@
name = "Mistral Small 3.2 24B Instruct 2506"
family = "chutesai"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.06
output = 0.18
[limit]
context = 131_072
output = 131_072
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepSeek R1 0528 TEE"
family = "deepseek-thinking"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.40
output = 1.75
input = 0.45
output = 2.15
cache_read = 0.225
[limit]
context = 163_840
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepSeek R1 Distill Llama 70B"
family = "deepseek-thinking"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.03
output = 0.11
input = 0.0272
output = 0.1087
cache_read = 0.0136
[limit]
context = 131_072
@@ -1,25 +0,0 @@
name = "DeepSeek R1 TEE"
family = "deepseek-thinking"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = false
structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.20
[limit]
context = 163_840
output = 163_840
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepSeek V3 0324 TEE"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = false
temperature = true
@@ -10,9 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.19
output = 0.87
cache_read = 0.095
input = 0.25
output = 1
cache_read = 0.125
[limit]
context = 163_840
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepSeek V3.1 TEE"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -10,8 +12,9 @@ structured_output = true
open_weights = true
[cost]
input = 0.20
output = 0.80
input = 0.27
output = 1
cache_read = 0.135
[limit]
context = 163_840
@@ -1,25 +0,0 @@
name = "DeepSeek V3.1 Terminus TEE"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.23
output = 0.90
[limit]
context = 163_840
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,25 +0,0 @@
name = "DeepSeek V3.2 Speciale TEE"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = true
temperature = true
tool_call = false
structured_output = true
open_weights = true
[cost]
input = 0.27
output = 0.41
[limit]
context = 163_840
output = 65_536
[modalities]
input = ["text"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,7 +1,9 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "DeepSeek V3.2 TEE"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
last_updated = "2026-04-25"
attachment = false
reasoning = true
temperature = true
@@ -1,22 +0,0 @@
name = "DeepSeek V3"
family = "deepseek"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = true
open_weights = true
[cost]
input = 0.30
output = 1.20
[limit]
context = 163_840
output = 163_840
[modalities]
input = ["text"]
output = ["text"]
@@ -0,0 +1,25 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "gemma 4 31B turbo TEE"
family = "gemma"
release_date = "2026-04-25"
last_updated = "2026-04-25"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.13
output = 0.38
cache_read = 0.065
[limit]
context = 131_072
output = 65_536
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -1,22 +0,0 @@
name = "MiroThinker V1.5 235B"
release_date = "2026-01-10"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = false
open_weights = true
[cost]
input = 0.30
output = 1.20
cache_read = 0.15
[limit]
context = 262_144
output = 8_192
[modalities]
input = ["text"]
output = ["text"]
@@ -1,23 +0,0 @@
name = "Kimi K2 Instruct 0905"
family = "kimi"
release_date = "2025-12-29"
last_updated = "2026-01-10"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
open_weights = true
[cost]
input = 0.39
output = 1.90
cache_read = 0.195
[limit]
context = 262_144
output = 262_144
[modalities]
input = ["text"]
output = ["text"]
@@ -1,21 +1,21 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Kimi K2.5 TEE"
family = "kimi"
release_date = "2026-01-27"
last_updated = "2026-01-27"
attachment = false
last_updated = "2026-04-25"
attachment = true
reasoning = true
temperature = true
tool_call = true
knowledge = "2024-10"
open_weights = true
structured_output = true
[interleaved]
field = "reasoning_content"
open_weights = true
knowledge = "2024-10"
[cost]
input = 0.60
output = 3.00
input = 0.44
output = 2
cache_read = 0.22
[limit]
context = 262_144
@@ -24,3 +24,6 @@ output = 65_535
[modalities]
input = ["text", "image", "video"]
output = ["text"]
[interleaved]
field = "reasoning_content"
@@ -1,26 +1,29 @@
# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.
# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status
name = "Kimi K2.6 TEE"
family = "kimi"
release_date = "2026-04-20"
last_updated = "2026-04-23"
last_updated = "2026-04-25"
attachment = true
reasoning = true
temperature = true
tool_call = true
knowledge = "2025-12"
open_weights = true
structured_output = true
[interleaved]
field = "reasoning_content"
open_weights = true
knowledge = "2025-12"
[cost]
input = 0.44
output = 2.00
input = 0.95
output = 4
cache_read = 0.475
[limit]
context = 262_144
output = 262_144
output = 65_535
[modalities]
input = ["text", "image", "video"]
output = ["text"]
[interleaved]
field = "reasoning_content"

Some files were not shown because too many files have changed in this diff Show More