Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 458b7f4d1a | |||
| 8f9adc7567 | |||
| a671cc05d5 | |||
| 2e015de42d | |||
| 151e9c9071 | |||
| b96b074a3b | |||
| 2593e131a1 | |||
| bc95b42ccd | |||
| 6139fb8c69 | |||
| 3070758007 | |||
| 5525e83de4 | |||
| 359fd879b8 | |||
| 01b5a1a656 | |||
| b1958be099 | |||
| 08aa068523 | |||
| f31ad0b02f | |||
| 585aa7fa1b | |||
| c42a327b3e | |||
| 92ebbfb5c4 | |||
| 535fe8c971 | |||
| a3b4bfc16c | |||
| d0fcd6f11f | |||
| e55cd54218 | |||
| 0d73b82b9f | |||
| 83c7e2b63f | |||
| 83ae4cf813 | |||
| 77eae6eef7 | |||
| 8cbf6ed10e | |||
| 06d87e4411 | |||
| 2cb3832618 | |||
| df960d1a90 | |||
| 8f2f83ef61 | |||
| b133426465 | |||
| dafff5a770 | |||
| dd894f077f | |||
| 91590874e7 | |||
| a436236146 | |||
| 1415b4be97 | |||
| 70ac6fccda | |||
| dc3283417d | |||
| 34fd6673e5 | |||
| c5fbcc2c9b | |||
| ab2eb51b4e | |||
| 8e19ec580c | |||
| 3aecc94c46 | |||
| dbe92646c3 | |||
| 4717c67054 |
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"build": "./script/build.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "^3.14.0",
|
||||
"hono": "^4.8.0",
|
||||
"models.dev": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 "&";
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "'":
|
||||
return "'";
|
||||
case '"':
|
||||
return """;
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
+12
-11
@@ -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"
|
||||
+7
-9
@@ -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"]
|
||||
+7
-6
@@ -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
-1
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
+9
-8
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-6
@@ -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
Reference in New Issue
Block a user