Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8dc9cae24 | |||
| 964775cf77 | |||
| 7509cf1797 | |||
| 2738f81d1a | |||
| 89b834086a | |||
| 1ab2ff8163 | |||
| 9468676683 | |||
| 6cdd2f054b | |||
| b13abc9141 | |||
| e5ba264751 | |||
| a7811fb522 | |||
| 605fae75d9 | |||
| 26b05268ae | |||
| 1aee13d2e5 | |||
| 0a924e6bb2 | |||
| 9769b2b11d | |||
| 1b0db099cf | |||
| 16ed78587c | |||
| 0b88965165 | |||
| acc704ce39 | |||
| 51ad3b264e | |||
| 146b6c7084 | |||
| 0e3cbe3c64 | |||
| 604d4d66a4 | |||
| f5090028b8 | |||
| 4bad8faf29 | |||
| 0df2ccf586 | |||
| bafdc00b45 | |||
| 49840c013b | |||
| eccae0b54e | |||
| 4cca29405f | |||
| e40d9dd338 | |||
| 6a74991397 | |||
| 035999cb58 | |||
| cec56bf1bc | |||
| 85f0cdcb2f | |||
| ef80d4df4e | |||
| af0ef00109 | |||
| 5fdcea6b36 | |||
| 31e56480b4 | |||
| 92a621594e | |||
| d2db353ceb | |||
| 900ae509d2 | |||
| 9d60164243 | |||
| cd3e99025f | |||
| 1098981eb6 | |||
| 27a151cf53 | |||
| 41ff42ab7f | |||
| adf1cbdecd | |||
| 10ddc78ce0 | |||
| 7ae897e440 | |||
| 02e452c2e8 | |||
| f05b63fff5 | |||
| e277d60236 | |||
| b01277a737 | |||
| a5da5aa429 | |||
| 11e1d5623a | |||
| e789d7c1f2 | |||
| 899668ad49 | |||
| 9420048dfe | |||
| 8db6c27634 |
@@ -35,3 +35,4 @@ jobs:
|
||||
- run: bun sst deploy --stage=dev
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Sync Model Catalogs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 8 * * *"
|
||||
- cron: "17 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -13,16 +13,34 @@ permissions:
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
providers:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.providers.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: List sync providers
|
||||
id: providers
|
||||
run: echo "matrix=$(bun models:sync --list-providers)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
sync:
|
||||
needs: providers
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- group: aggregators
|
||||
title: "chore(sync): update aggregator model catalogs"
|
||||
branch: automation/sync-models-aggregators
|
||||
labels: automation,model-sync,sync-group:aggregators,provider:openrouter
|
||||
matrix: ${{ fromJSON(needs.providers.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -39,9 +57,16 @@ jobs:
|
||||
run: bun install
|
||||
|
||||
- name: Sync model catalogs
|
||||
run: bun models:sync ${{ matrix.group }}
|
||||
run: bun models:sync ${{ matrix.provider }}
|
||||
env:
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID }}
|
||||
CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN }}
|
||||
|
||||
- name: Validate models
|
||||
run: bun validate
|
||||
@@ -49,9 +74,9 @@ jobs:
|
||||
- name: Create pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH: ${{ matrix.branch }}
|
||||
LABELS: ${{ matrix.labels }}
|
||||
TITLE: ${{ matrix.title }}
|
||||
BRANCH: automation/sync-models-${{ matrix.provider }}
|
||||
LABELS: automation,model-sync,provider:${{ matrix.provider }}
|
||||
TITLE: "chore(sync): update ${{ matrix.name }} model catalog"
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain -- providers)" ]; then
|
||||
echo "No model catalog changes found."
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
.idea
|
||||
dist
|
||||
.DS_Store
|
||||
.sync/
|
||||
node_modules
|
||||
data/tokenspeed-monitor.sqlite
|
||||
data/tokenspeed-monitor.sqlite-shm
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"scripts": {
|
||||
"validate": "bun ./packages/core/script/validate.ts",
|
||||
"compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts",
|
||||
"cloudflare:sync": "bun ./packages/core/script/sync-models.ts cloudflare-workers-ai",
|
||||
"chutes:generate": "bun ./packages/core/script/generate-chutes.ts",
|
||||
"databricks:generate": "bun ./packages/core/script/generate-databricks.ts",
|
||||
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
|
||||
|
||||
@@ -1,421 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import path from "node:path";
|
||||
import { mkdir, readdir, rm } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
import { main } from "../src/sync/index.js";
|
||||
|
||||
import { AuthoredModel, AuthoredModelShape } from "../src/schema.js";
|
||||
import { google } from "./sync/google.js";
|
||||
import { openrouter } from "./sync/openrouter.js";
|
||||
|
||||
const ExistingModel = AuthoredModelShape.partial()
|
||||
.extend({
|
||||
extends: z
|
||||
.object({
|
||||
from: z.string(),
|
||||
omit: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ExistingModel = z.infer<typeof ExistingModel>;
|
||||
export type SyncedModel = Omit<z.infer<typeof AuthoredModelShape>, "id">;
|
||||
|
||||
export interface SyncProvider<SourceModel> {
|
||||
id: string;
|
||||
name: string;
|
||||
modelsDir: string;
|
||||
skipCreates?: boolean;
|
||||
sourceID?(model: SourceModel): string;
|
||||
skippedNotice?(ids: string[]): string[];
|
||||
fetchModels(): Promise<unknown>;
|
||||
parseModels(raw: unknown): SourceModel[];
|
||||
translateModel(
|
||||
model: SourceModel,
|
||||
context: { existing(id: string): ExistingModel | undefined },
|
||||
): { id: string; model: SyncedModel } | undefined;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "changed" | "unchanged";
|
||||
created: number;
|
||||
updated: number;
|
||||
deleted: number;
|
||||
unchanged: number;
|
||||
notices: string[];
|
||||
files: Array<{ status: "created" | "updated" | "deleted"; path: string }>;
|
||||
}
|
||||
|
||||
export const providers: {
|
||||
google: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
} = {
|
||||
google,
|
||||
openrouter,
|
||||
};
|
||||
|
||||
export const groups = {
|
||||
aggregators: ["openrouter"],
|
||||
direct: ["google"],
|
||||
} as const;
|
||||
|
||||
type ProviderID = keyof typeof providers;
|
||||
|
||||
interface SyncOptions {
|
||||
dryRun?: boolean;
|
||||
newOnly?: boolean;
|
||||
}
|
||||
|
||||
export async function syncProviderByID(id: ProviderID, options: SyncOptions = {}) {
|
||||
return syncProvider(providers[id], options);
|
||||
}
|
||||
|
||||
export async function syncProvider<SourceModel>(
|
||||
provider: SyncProvider<SourceModel>,
|
||||
options: SyncOptions = {},
|
||||
): Promise<SyncResult> {
|
||||
console.log(`\nSyncing ${provider.name}...`);
|
||||
|
||||
const existing = await readExisting(provider.modelsDir);
|
||||
const sourceModels = provider.parseModels(await provider.fetchModels());
|
||||
const desired = new Map<string, { model: z.infer<typeof AuthoredModel>; content: string }>();
|
||||
const skippedRemote: string[] = [];
|
||||
|
||||
for (const sourceModel of sourceModels) {
|
||||
const translated = provider.translateModel(sourceModel, {
|
||||
existing(id) {
|
||||
return existing.get(`${id}.toml`)?.toml;
|
||||
},
|
||||
});
|
||||
if (translated === undefined) {
|
||||
if (provider.skipCreates) skippedRemote.push(provider.sourceID?.(sourceModel) ?? "unknown");
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = `${translated.id}.toml`;
|
||||
if (provider.skipCreates && !existing.has(relativePath)) {
|
||||
skippedRemote.push(translated.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (desired.has(relativePath)) {
|
||||
throw new Error(`Duplicate synced model path: ${provider.id}/${relativePath}`);
|
||||
}
|
||||
|
||||
const parsed = AuthoredModel.safeParse({
|
||||
id: translated.id,
|
||||
...translated.model,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
parsed.error.cause = { provider: provider.id, path: relativePath };
|
||||
throw parsed.error;
|
||||
}
|
||||
|
||||
desired.set(relativePath, {
|
||||
model: parsed.data,
|
||||
content: formatToml(parsed.data),
|
||||
});
|
||||
}
|
||||
|
||||
const files: SyncResult["files"] = [];
|
||||
let unchanged = 0;
|
||||
|
||||
for (const [relativePath, file] of desired) {
|
||||
const filePath = path.join(provider.modelsDir, relativePath);
|
||||
const current = existing.get(relativePath);
|
||||
|
||||
if (current === undefined) {
|
||||
files.push({ status: "created", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would create ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await Bun.write(filePath, file.content);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sameModel(relativePath, current.toml, file.model)) {
|
||||
if (options.newOnly) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push({ status: "updated", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would update ${relativePath}`);
|
||||
} else {
|
||||
if (current.symlink) await rm(filePath, { force: true });
|
||||
await Bun.write(filePath, file.content);
|
||||
}
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const relativePath of existing.keys()) {
|
||||
if (desired.has(relativePath)) continue;
|
||||
if (options.newOnly) {
|
||||
console.log(`Skipping removal in new-only mode: ${relativePath}`);
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(provider.modelsDir, relativePath);
|
||||
files.push({ status: "deleted", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would remove ${relativePath}`);
|
||||
} else {
|
||||
await rm(filePath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const result = summarize(provider, files, unchanged, provider.skippedNotice?.(skippedRemote) ?? []);
|
||||
console.log(
|
||||
`${options.dryRun ? "Dry run: " : ""}${result.created} created, ${result.updated} updated, ${result.deleted} removed, ${result.unchanged} unchanged`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function syncTargets(target: string, options: SyncOptions = {}) {
|
||||
const ids = target in groups
|
||||
? groups[target as keyof typeof groups]
|
||||
: target in providers
|
||||
? [target as ProviderID]
|
||||
: undefined;
|
||||
|
||||
if (ids === undefined) {
|
||||
throw new Error(`Unknown sync target: ${target}`);
|
||||
}
|
||||
|
||||
const results: SyncResult[] = [];
|
||||
for (const id of ids) {
|
||||
results.push(await syncProviderByID(id as ProviderID, options));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function readExisting(modelsDir: string) {
|
||||
const existing = new Map<string, { text: string; toml: ExistingModel; symlink: boolean }>();
|
||||
|
||||
for (const { file, symlink } of await tomlFiles(modelsDir)) {
|
||||
const text = await Bun.file(path.join(modelsDir, file)).text();
|
||||
const parsed = ExistingModel.safeParse(Bun.TOML.parse(text));
|
||||
if (!parsed.success) {
|
||||
parsed.error.cause = { path: path.join(modelsDir, file) };
|
||||
throw parsed.error;
|
||||
}
|
||||
existing.set(file, { text, toml: parsed.data, symlink });
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function tomlFiles(root: string, dir = "") {
|
||||
const result: Array<{ file: string; symlink: boolean }> = [];
|
||||
|
||||
for (const entry of await readdir(path.join(root, dir), { withFileTypes: true })) {
|
||||
const file = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
result.push(...await tomlFiles(root, file));
|
||||
} else if (entry.name.endsWith(".toml") && (entry.isFile() || entry.isSymbolicLink())) {
|
||||
result.push({ file, symlink: entry.isSymbolicLink() });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function summarize(
|
||||
provider: { id: string; name: string },
|
||||
files: SyncResult["files"],
|
||||
unchanged: number,
|
||||
notices: string[],
|
||||
): SyncResult {
|
||||
return {
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
status: files.length > 0 ? "changed" : "unchanged",
|
||||
created: files.filter((file) => file.status === "created").length,
|
||||
updated: files.filter((file) => file.status === "updated").length,
|
||||
deleted: files.filter((file) => file.status === "deleted").length,
|
||||
unchanged,
|
||||
notices,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
function sameModel(
|
||||
relativePath: string,
|
||||
current: ExistingModel,
|
||||
desired: z.infer<typeof AuthoredModel>,
|
||||
) {
|
||||
const parsed = AuthoredModel.safeParse({
|
||||
id: relativePath.slice(0, -5),
|
||||
...current,
|
||||
});
|
||||
return parsed.success && stable(parsed.data) === stable(desired);
|
||||
}
|
||||
|
||||
function stable(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map(stable);
|
||||
const ordered = value.every((item) => item === null || typeof item !== "object")
|
||||
? items.sort()
|
||||
: items;
|
||||
return `[${ordered.join(",")}]`;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
return `{${Object.entries(value)
|
||||
.filter(([, item]) => item !== undefined)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
async function writeReport(target: string, results: SyncResult[]) {
|
||||
await mkdir(".sync", { recursive: true });
|
||||
|
||||
const lines = [
|
||||
`Updates model TOMLs for the \`${target}\` sync target.`,
|
||||
"",
|
||||
"| Provider | Status | Created | Updated | Deleted |",
|
||||
"| --- | --- | ---: | ---: | ---: |",
|
||||
];
|
||||
|
||||
for (const result of results) {
|
||||
lines.push(
|
||||
`| ${result.name} | ${result.status} | ${result.created} | ${result.updated} | ${result.deleted} |`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const result of results.filter((item) => item.files.length > 0)) {
|
||||
lines.push("", `<details><summary>${result.name} changed files</summary>`, "");
|
||||
for (const file of result.files) {
|
||||
lines.push(`- ${file.status}: \`${file.path}\``);
|
||||
}
|
||||
lines.push("", "</details>");
|
||||
}
|
||||
|
||||
const noticeResults = results.filter((item) => item.notices.length > 0);
|
||||
if (noticeResults.length > 0) {
|
||||
lines.push("", "## Notices");
|
||||
for (const result of noticeResults) {
|
||||
lines.push("", `### ${result.name}`);
|
||||
for (const notice of result.notices) {
|
||||
lines.push(`- ${notice}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("", "This PR was created automatically by the daily model sync workflow.");
|
||||
await Bun.write(".sync/model-sync-report.md", `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function quote(value: string) {
|
||||
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function formatInteger(n: number) {
|
||||
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "_");
|
||||
}
|
||||
|
||||
function formatNumber(n: number) {
|
||||
return Number.isInteger(n) ? formatInteger(n) : String(n);
|
||||
}
|
||||
|
||||
function formatToml(model: z.infer<typeof AuthoredModel>) {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`name = ${quote(model.name)}`);
|
||||
if (model.family !== undefined) lines.push(`family = ${quote(model.family)}`);
|
||||
lines.push(`release_date = ${quote(model.release_date)}`);
|
||||
lines.push(`last_updated = ${quote(model.last_updated)}`);
|
||||
lines.push(`attachment = ${model.attachment}`);
|
||||
lines.push(`reasoning = ${model.reasoning}`);
|
||||
if (model.temperature !== undefined) lines.push(`temperature = ${model.temperature}`);
|
||||
lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output !== undefined) {
|
||||
lines.push(`structured_output = ${model.structured_output}`);
|
||||
}
|
||||
if (model.knowledge !== undefined) lines.push(`knowledge = ${quote(model.knowledge)}`);
|
||||
lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
|
||||
|
||||
if (model.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
if (model.interleaved === true) {
|
||||
lines.push("interleaved = true");
|
||||
} else {
|
||||
lines.push("[interleaved]");
|
||||
lines.push(`field = ${quote(model.interleaved.field)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.cost !== undefined) {
|
||||
lines.push("", "[cost]");
|
||||
lines.push(`input = ${formatNumber(model.cost.input)}`);
|
||||
lines.push(`output = ${formatNumber(model.cost.output)}`);
|
||||
if (model.cost.reasoning !== undefined) {
|
||||
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
|
||||
}
|
||||
if (model.cost.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${formatNumber(model.cost.cache_read)}`);
|
||||
}
|
||||
if (model.cost.cache_write !== undefined) {
|
||||
lines.push(`cache_write = ${formatNumber(model.cost.cache_write)}`);
|
||||
}
|
||||
if (model.cost.input_audio !== undefined) {
|
||||
lines.push(`input_audio = ${formatNumber(model.cost.input_audio)}`);
|
||||
}
|
||||
if (model.cost.output_audio !== undefined) {
|
||||
lines.push(`output_audio = ${formatNumber(model.cost.output_audio)}`);
|
||||
}
|
||||
|
||||
for (const tier of model.cost.tiers ?? []) {
|
||||
lines.push("", "[[cost.tiers]]");
|
||||
lines.push(`tier = { size = ${formatInteger(tier.tier.size)} }`);
|
||||
lines.push(`input = ${formatNumber(tier.input)}`);
|
||||
lines.push(`output = ${formatNumber(tier.output)}`);
|
||||
if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`);
|
||||
if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
|
||||
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("", "[limit]");
|
||||
lines.push(`context = ${formatInteger(model.limit.context)}`);
|
||||
if (model.limit.input !== undefined) lines.push(`input = ${formatInteger(model.limit.input)}`);
|
||||
lines.push(`output = ${formatInteger(model.limit.output)}`);
|
||||
|
||||
lines.push("", "[modalities]");
|
||||
lines.push(`input = [${model.modalities.input.map(quote).join(", ")}]`);
|
||||
lines.push(`output = [${model.modalities.output.map(quote).join(", ")}]`);
|
||||
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export async function main(args = process.argv.slice(2)) {
|
||||
const target = args.find((arg) => !arg.startsWith("-")) ?? "aggregators";
|
||||
const results = await syncTargets(target, {
|
||||
dryRun: args.includes("--dry-run"),
|
||||
newOnly: args.includes("--new-only"),
|
||||
});
|
||||
|
||||
await writeReport(target, results);
|
||||
|
||||
console.log("\nSync summary");
|
||||
for (const result of results) {
|
||||
console.log(
|
||||
`${result.name}: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
||||
await main();
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ModelFamilyValues } from "../../src/family.js";
|
||||
import type { ExistingModel, SyncProvider } from "../sync-models.js";
|
||||
|
||||
const API_ENDPOINT = "https://openrouter.ai/api/v1/models";
|
||||
|
||||
const OpenRouterModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
created: z.number(),
|
||||
hugging_face_id: z.string().nullable(),
|
||||
knowledge_cutoff: z.string().nullable(),
|
||||
context_length: z.number(),
|
||||
architecture: z.object({
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
}),
|
||||
pricing: z.object({
|
||||
prompt: z.string(),
|
||||
completion: z.string(),
|
||||
internal_reasoning: z.string().optional(),
|
||||
input_cache_read: z.string().optional(),
|
||||
input_cache_write: z.string().optional(),
|
||||
}),
|
||||
top_provider: z.object({
|
||||
context_length: z.number().nullable(),
|
||||
max_completion_tokens: z.number().nullable(),
|
||||
}),
|
||||
supported_parameters: z.array(z.string()),
|
||||
});
|
||||
|
||||
const OpenRouterResponse = z.object({
|
||||
data: z.array(OpenRouterModel),
|
||||
}).passthrough();
|
||||
|
||||
type OpenRouterModel = z.infer<typeof OpenRouterModel>;
|
||||
|
||||
export const openrouter = {
|
||||
id: "openrouter",
|
||||
name: "OpenRouter",
|
||||
modelsDir: "providers/openrouter/models",
|
||||
async fetchModels() {
|
||||
const headers = process.env.OPENROUTER_API_KEY
|
||||
? { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` }
|
||||
: undefined;
|
||||
const response = await fetch(API_ENDPOINT, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenRouter request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return OpenRouterResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildModel(model, context.existing(model.id)),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<OpenRouterModel>;
|
||||
|
||||
function dateFromTimestamp(timestamp: number) {
|
||||
return new Date(timestamp * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function price(value: string | undefined) {
|
||||
if (value === undefined) return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0
|
||||
? Math.round(number * 1_000_000_000_000) / 1_000_000
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
function modalities(values: string[], fallback: Modality[]): Modality[] {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = values
|
||||
.map((value) => value.toLowerCase())
|
||||
.map((value) => value === "file" ? "pdf" : value)
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
return [...new Set(result.length > 0 ? result : fallback)];
|
||||
}
|
||||
|
||||
function inferFamily(model: OpenRouterModel, name: string) {
|
||||
const target = `${model.id} ${name}`.toLowerCase();
|
||||
return [...ModelFamilyValues]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((family) => {
|
||||
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (family === "o") {
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
|
||||
}
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
|
||||
});
|
||||
}
|
||||
|
||||
function buildModel(model: OpenRouterModel, existing: ExistingModel | undefined) {
|
||||
const params = new Set(model.supported_parameters);
|
||||
const name = model.name.replace(/^[^:]+:\s+/, "");
|
||||
const input = modalities(model.architecture.input_modalities, ["text"]);
|
||||
const output = modalities(model.architecture.output_modalities, ["text"]);
|
||||
const prompt = price(model.pricing.prompt);
|
||||
const completion = price(model.pricing.completion);
|
||||
const reasoning = params.has("reasoning") || params.has("include_reasoning");
|
||||
const context = model.top_provider.context_length ?? model.context_length;
|
||||
const family = inferFamily(model, name);
|
||||
|
||||
return {
|
||||
name,
|
||||
family: existing?.family === "o" && family !== "o"
|
||||
? family
|
||||
: (existing?.family ?? family),
|
||||
release_date: dateFromTimestamp(model.created),
|
||||
last_updated: dateFromTimestamp(model.created),
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning,
|
||||
temperature: params.has("temperature"),
|
||||
tool_call: params.has("tools") || params.has("tool_choice"),
|
||||
structured_output: params.has("structured_outputs"),
|
||||
knowledge: model.knowledge_cutoff?.slice(0, 10) ?? existing?.knowledge,
|
||||
open_weights: Boolean(model.hugging_face_id),
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost: prompt !== undefined && completion !== undefined
|
||||
? {
|
||||
input: prompt,
|
||||
output: completion,
|
||||
reasoning: reasoning ? price(model.pricing.internal_reasoning) : undefined,
|
||||
cache_read: price(model.pricing.input_cache_read),
|
||||
cache_write: price(model.pricing.input_cache_write),
|
||||
tiers: existing?.cost?.tiers,
|
||||
}
|
||||
: existing?.cost,
|
||||
limit: {
|
||||
context,
|
||||
input: existing?.limit?.input,
|
||||
output: model.top_provider.max_completion_tokens ?? existing?.limit?.output ?? context,
|
||||
},
|
||||
modalities: { input, output },
|
||||
};
|
||||
}
|
||||
@@ -82,6 +82,7 @@ export const ModelFamilyValues = [
|
||||
|
||||
// xAI Grok
|
||||
"grok",
|
||||
"grok-build",
|
||||
"grok-vision",
|
||||
"grok-beta",
|
||||
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
import path from "node:path";
|
||||
import { mkdir, readdir, rm } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuthoredModel, AuthoredModelShape } from "../schema.js";
|
||||
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
|
||||
import { fireworksAi } from "./providers/fireworks-ai.js";
|
||||
import { google } from "./providers/google.js";
|
||||
import { openrouter } from "./providers/openrouter.js";
|
||||
import { xai } from "./providers/xai.js";
|
||||
|
||||
const ExtendsConfig = z
|
||||
.object({
|
||||
from: z.string(),
|
||||
omit: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ExistingExtendsConfig = z
|
||||
.object({
|
||||
from: z.string(),
|
||||
omit: z.array(z.string()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const ExistingModel = AuthoredModelShape.partial()
|
||||
.extend({
|
||||
extends: ExistingExtendsConfig.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const SyncedExtendsModel = AuthoredModelShape.partial()
|
||||
.extend({
|
||||
id: z.string(),
|
||||
extends: ExtendsConfig,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const SyncedAuthoredModel = z.union([AuthoredModel, SyncedExtendsModel]);
|
||||
|
||||
export type ExistingModel = z.infer<typeof ExistingModel>;
|
||||
export type SyncedFullModel = Omit<z.infer<typeof AuthoredModelShape>, "id">;
|
||||
export type SyncedExtendsModel = Omit<z.infer<typeof SyncedExtendsModel>, "id">;
|
||||
export type SyncedModel = SyncedFullModel | SyncedExtendsModel;
|
||||
|
||||
export interface SyncProvider<SourceModel> {
|
||||
id: string;
|
||||
name: string;
|
||||
modelsDir: string;
|
||||
skipCreates?: boolean;
|
||||
deleteMissing?: boolean;
|
||||
sourceID?(model: SourceModel): string;
|
||||
skippedNotice?(ids: string[]): string[];
|
||||
fetchModels(): Promise<unknown>;
|
||||
parseModels(raw: unknown): SourceModel[];
|
||||
translateModel(
|
||||
model: SourceModel,
|
||||
context: { existing(id: string): ExistingModel | undefined },
|
||||
): { id: string; model: SyncedModel } | undefined;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "changed" | "unchanged";
|
||||
created: number;
|
||||
updated: number;
|
||||
deleted: number;
|
||||
unchanged: number;
|
||||
notices: string[];
|
||||
files: Array<{ status: "created" | "updated" | "deleted"; path: string }>;
|
||||
}
|
||||
|
||||
export const providers: {
|
||||
"cloudflare-workers-ai": SyncProvider<any>;
|
||||
"fireworks-ai": SyncProvider<any>;
|
||||
google: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
xai: SyncProvider<any>;
|
||||
} = {
|
||||
"cloudflare-workers-ai": cloudflareWorkersAi,
|
||||
"fireworks-ai": fireworksAi,
|
||||
google,
|
||||
openrouter,
|
||||
xai,
|
||||
};
|
||||
|
||||
export const groups = {
|
||||
aggregators: ["openrouter"],
|
||||
cloudflare: ["cloudflare-workers-ai"],
|
||||
direct: ["fireworks-ai", "google", "xai"],
|
||||
} as const;
|
||||
|
||||
type ProviderID = keyof typeof providers;
|
||||
|
||||
interface SyncOptions {
|
||||
dryRun?: boolean;
|
||||
newOnly?: boolean;
|
||||
}
|
||||
|
||||
export async function syncProviderByID(id: ProviderID, options: SyncOptions = {}) {
|
||||
return syncProvider(providers[id], options);
|
||||
}
|
||||
|
||||
export async function syncProvider<SourceModel>(
|
||||
provider: SyncProvider<SourceModel>,
|
||||
options: SyncOptions = {},
|
||||
): Promise<SyncResult> {
|
||||
console.log(`\nSyncing ${provider.name}...`);
|
||||
|
||||
const existing = await readExisting(provider.modelsDir);
|
||||
const sourceModels = provider.parseModels(await provider.fetchModels());
|
||||
const desired = new Map<string, { model: z.infer<typeof SyncedAuthoredModel>; content: string }>();
|
||||
const skippedRemote: string[] = [];
|
||||
|
||||
for (const sourceModel of sourceModels) {
|
||||
const translated = provider.translateModel(sourceModel, {
|
||||
existing(id) {
|
||||
return existing.get(`${id}.toml`)?.toml;
|
||||
},
|
||||
});
|
||||
if (translated === undefined) {
|
||||
if (provider.skipCreates) skippedRemote.push(provider.sourceID?.(sourceModel) ?? "unknown");
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativePath = `${translated.id}.toml`;
|
||||
if (provider.skipCreates && !existing.has(relativePath)) {
|
||||
skippedRemote.push(translated.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (desired.has(relativePath)) {
|
||||
throw new Error(`Duplicate synced model path: ${provider.id}/${relativePath}`);
|
||||
}
|
||||
|
||||
const parsed = SyncedAuthoredModel.safeParse({
|
||||
id: translated.id,
|
||||
...translated.model,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
parsed.error.cause = { provider: provider.id, path: relativePath };
|
||||
throw parsed.error;
|
||||
}
|
||||
|
||||
desired.set(relativePath, {
|
||||
model: parsed.data,
|
||||
content: formatToml(parsed.data),
|
||||
});
|
||||
}
|
||||
|
||||
const files: SyncResult["files"] = [];
|
||||
let unchanged = 0;
|
||||
|
||||
for (const [relativePath, file] of desired) {
|
||||
const filePath = path.join(provider.modelsDir, relativePath);
|
||||
const current = existing.get(relativePath);
|
||||
|
||||
if (current === undefined) {
|
||||
files.push({ status: "created", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would create ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await Bun.write(filePath, file.content);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sameModel(relativePath, current.toml, file.model)) {
|
||||
if (options.newOnly) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push({ status: "updated", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would update ${relativePath}`);
|
||||
} else {
|
||||
if (current.symlink) await rm(filePath, { force: true });
|
||||
await Bun.write(filePath, file.content);
|
||||
}
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const relativePath of existing.keys()) {
|
||||
if (desired.has(relativePath)) continue;
|
||||
if (provider.deleteMissing === false) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
if (options.newOnly) {
|
||||
console.log(`Skipping removal in new-only mode: ${relativePath}`);
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(provider.modelsDir, relativePath);
|
||||
files.push({ status: "deleted", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would remove ${relativePath}`);
|
||||
} else {
|
||||
await rm(filePath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const result = summarize(provider, files, unchanged, provider.skippedNotice?.(skippedRemote) ?? []);
|
||||
console.log(
|
||||
`${options.dryRun ? "Dry run: " : ""}${result.created} created, ${result.updated} updated, ${result.deleted} removed, ${result.unchanged} unchanged`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function syncTargets(target: string, options: SyncOptions = {}) {
|
||||
const ids = target in groups
|
||||
? groups[target as keyof typeof groups]
|
||||
: target in providers
|
||||
? [target as ProviderID]
|
||||
: undefined;
|
||||
|
||||
if (ids === undefined) {
|
||||
throw new Error(`Unknown sync target: ${target}`);
|
||||
}
|
||||
|
||||
const results: SyncResult[] = [];
|
||||
for (const id of ids) {
|
||||
results.push(await syncProviderByID(id as ProviderID, options));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function syncProviderMatrix() {
|
||||
return {
|
||||
include: Object.values(providers).map((provider) => ({
|
||||
provider: provider.id,
|
||||
name: provider.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function readExisting(modelsDir: string) {
|
||||
const existing = new Map<string, { text: string; toml: ExistingModel; symlink: boolean }>();
|
||||
|
||||
for (const { file, symlink } of await tomlFiles(modelsDir)) {
|
||||
const text = await Bun.file(path.join(modelsDir, file)).text();
|
||||
const parsed = ExistingModel.safeParse(Bun.TOML.parse(text));
|
||||
if (!parsed.success) {
|
||||
parsed.error.cause = { path: path.join(modelsDir, file) };
|
||||
throw parsed.error;
|
||||
}
|
||||
existing.set(file, { text, toml: parsed.data, symlink });
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function tomlFiles(root: string, dir = "") {
|
||||
const result: Array<{ file: string; symlink: boolean }> = [];
|
||||
|
||||
for (const entry of await readdir(path.join(root, dir), { withFileTypes: true })) {
|
||||
const file = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
result.push(...await tomlFiles(root, file));
|
||||
} else if (entry.name.endsWith(".toml") && (entry.isFile() || entry.isSymbolicLink())) {
|
||||
result.push({ file, symlink: entry.isSymbolicLink() });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function summarize(
|
||||
provider: { id: string; name: string },
|
||||
files: SyncResult["files"],
|
||||
unchanged: number,
|
||||
notices: string[],
|
||||
): SyncResult {
|
||||
return {
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
status: files.length > 0 ? "changed" : "unchanged",
|
||||
created: files.filter((file) => file.status === "created").length,
|
||||
updated: files.filter((file) => file.status === "updated").length,
|
||||
deleted: files.filter((file) => file.status === "deleted").length,
|
||||
unchanged,
|
||||
notices,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
function sameModel(
|
||||
relativePath: string,
|
||||
current: ExistingModel,
|
||||
desired: z.infer<typeof SyncedAuthoredModel>,
|
||||
) {
|
||||
const parsed = SyncedAuthoredModel.safeParse({
|
||||
id: relativePath.slice(0, -5),
|
||||
...current,
|
||||
});
|
||||
return parsed.success && stable(parsed.data) === stable(desired);
|
||||
}
|
||||
|
||||
function stable(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map(stable);
|
||||
const ordered = value.every((item) => item === null || typeof item !== "object")
|
||||
? items.sort()
|
||||
: items;
|
||||
return `[${ordered.join(",")}]`;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
return `{${Object.entries(value)
|
||||
.filter(([, item]) => item !== undefined)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
async function writeReport(target: string, results: SyncResult[]) {
|
||||
await mkdir(".sync", { recursive: true });
|
||||
|
||||
const lines = [
|
||||
`Updates model TOMLs for the \`${target}\` sync target.`,
|
||||
"",
|
||||
"| Provider | Status | Created | Updated | Deleted |",
|
||||
"| --- | --- | ---: | ---: | ---: |",
|
||||
];
|
||||
|
||||
for (const result of results) {
|
||||
lines.push(
|
||||
`| ${result.name} | ${result.status} | ${result.created} | ${result.updated} | ${result.deleted} |`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const result of results.filter((item) => item.files.length > 0)) {
|
||||
lines.push("", `<details><summary>${result.name} changed files</summary>`, "");
|
||||
for (const file of result.files) {
|
||||
lines.push(`- ${file.status}: \`${file.path}\``);
|
||||
}
|
||||
lines.push("", "</details>");
|
||||
}
|
||||
|
||||
const noticeResults = results.filter((item) => item.notices.length > 0);
|
||||
if (noticeResults.length > 0) {
|
||||
lines.push("", "## Notices");
|
||||
for (const result of noticeResults) {
|
||||
lines.push("", `### ${result.name}`);
|
||||
for (const notice of result.notices) {
|
||||
lines.push(`- ${notice}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("", "This PR was created automatically by the daily model sync workflow.");
|
||||
await Bun.write(".sync/model-sync-report.md", `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function quote(value: string) {
|
||||
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function formatInteger(n: number) {
|
||||
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "_");
|
||||
}
|
||||
|
||||
function formatNumber(n: number) {
|
||||
return Number.isInteger(n) ? formatInteger(n) : String(n);
|
||||
}
|
||||
|
||||
function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
const lines: string[] = [];
|
||||
const extendsLines: string[] = [];
|
||||
|
||||
if ("extends" in model) {
|
||||
extendsLines.push("[extends]");
|
||||
extendsLines.push(`from = ${quote(model.extends.from)}`);
|
||||
if (model.extends.omit !== undefined) {
|
||||
extendsLines.push(`omit = [${model.extends.omit.map(quote).join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`);
|
||||
if (model.family !== undefined) lines.push(`family = ${quote(model.family)}`);
|
||||
if (model.release_date !== undefined) lines.push(`release_date = ${quote(model.release_date)}`);
|
||||
if (model.last_updated !== undefined) lines.push(`last_updated = ${quote(model.last_updated)}`);
|
||||
if (model.attachment !== undefined) lines.push(`attachment = ${model.attachment}`);
|
||||
if (model.reasoning !== undefined) lines.push(`reasoning = ${model.reasoning}`);
|
||||
if (model.temperature !== undefined) lines.push(`temperature = ${model.temperature}`);
|
||||
if (model.tool_call !== undefined) lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output !== undefined) {
|
||||
lines.push(`structured_output = ${model.structured_output}`);
|
||||
}
|
||||
if (model.knowledge !== undefined) lines.push(`knowledge = ${quote(model.knowledge)}`);
|
||||
if (model.open_weights !== undefined) lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
|
||||
|
||||
if (extendsLines.length > 0) {
|
||||
if (lines.length > 0) lines.push("");
|
||||
lines.push(...extendsLines);
|
||||
}
|
||||
|
||||
if (model.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
if (model.interleaved === true) {
|
||||
lines.push("interleaved = true");
|
||||
} else {
|
||||
lines.push("[interleaved]");
|
||||
lines.push(`field = ${quote(model.interleaved.field)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.cost !== undefined) {
|
||||
lines.push("", "[cost]");
|
||||
lines.push(`input = ${formatNumber(model.cost.input)}`);
|
||||
lines.push(`output = ${formatNumber(model.cost.output)}`);
|
||||
if (model.cost.reasoning !== undefined) {
|
||||
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
|
||||
}
|
||||
if (model.cost.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${formatNumber(model.cost.cache_read)}`);
|
||||
}
|
||||
if (model.cost.cache_write !== undefined) {
|
||||
lines.push(`cache_write = ${formatNumber(model.cost.cache_write)}`);
|
||||
}
|
||||
if (model.cost.input_audio !== undefined) {
|
||||
lines.push(`input_audio = ${formatNumber(model.cost.input_audio)}`);
|
||||
}
|
||||
if (model.cost.output_audio !== undefined) {
|
||||
lines.push(`output_audio = ${formatNumber(model.cost.output_audio)}`);
|
||||
}
|
||||
|
||||
for (const tier of model.cost.tiers ?? []) {
|
||||
lines.push("", "[[cost.tiers]]");
|
||||
lines.push(`tier = { size = ${formatInteger(tier.tier.size)} }`);
|
||||
lines.push(`input = ${formatNumber(tier.input)}`);
|
||||
lines.push(`output = ${formatNumber(tier.output)}`);
|
||||
if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`);
|
||||
if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
|
||||
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.limit !== undefined) {
|
||||
lines.push("", "[limit]");
|
||||
if (model.limit.context !== undefined) lines.push(`context = ${formatInteger(model.limit.context)}`);
|
||||
if (model.limit.input !== undefined) lines.push(`input = ${formatInteger(model.limit.input)}`);
|
||||
if (model.limit.output !== undefined) lines.push(`output = ${formatInteger(model.limit.output)}`);
|
||||
}
|
||||
|
||||
if (model.modalities !== undefined) {
|
||||
lines.push("", "[modalities]");
|
||||
if (model.modalities.input !== undefined) {
|
||||
lines.push(`input = [${model.modalities.input.map(quote).join(", ")}]`);
|
||||
}
|
||||
if (model.modalities.output !== undefined) {
|
||||
lines.push(`output = [${model.modalities.output.map(quote).join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export async function main(args = process.argv.slice(2)) {
|
||||
if (args.includes("--list-providers")) {
|
||||
console.log(JSON.stringify(syncProviderMatrix()));
|
||||
return;
|
||||
}
|
||||
|
||||
const target = args.find((arg) => !arg.startsWith("-")) ?? "aggregators";
|
||||
const results = await syncTargets(target, {
|
||||
dryRun: args.includes("--dry-run"),
|
||||
newOnly: args.includes("--new-only"),
|
||||
});
|
||||
|
||||
await writeReport(target, results);
|
||||
|
||||
console.log("\nSync summary");
|
||||
for (const result of results) {
|
||||
console.log(
|
||||
`${result.name}: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
||||
@@ -0,0 +1,176 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider } from "../index.js";
|
||||
import {
|
||||
buildOpenRouterModel,
|
||||
OpenRouterModel,
|
||||
OpenRouterResponse,
|
||||
} from "./openrouter.js";
|
||||
|
||||
const API_BASE = "https://api.cloudflare.com/client/v4/accounts";
|
||||
|
||||
const CloudflareOpenRouterResponse = z.object({
|
||||
result: z.union([OpenRouterResponse, z.array(OpenRouterModel)]).optional(),
|
||||
result_info: z.object({
|
||||
page: z.number().optional(),
|
||||
total_pages: z.number().optional(),
|
||||
}).passthrough().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const CloudflareModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
created: z.number(),
|
||||
hugging_face_id: z.string().nullable().optional(),
|
||||
context_length: z.number(),
|
||||
max_output_length: z.number().nullable().optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
pricing: z.object({
|
||||
prompt: z.string(),
|
||||
completion: z.string(),
|
||||
internal_reasoning: z.string().optional(),
|
||||
input_cache_read: z.string().optional(),
|
||||
input_cache_write: z.string().optional(),
|
||||
}),
|
||||
supported_features: z.array(z.string()).optional(),
|
||||
supported_sampling_parameters: z.array(z.string()).optional(),
|
||||
}).passthrough();
|
||||
|
||||
const CloudflareResponse = z.object({
|
||||
data: z.array(CloudflareModel),
|
||||
}).passthrough();
|
||||
|
||||
type CloudflareModel = z.infer<typeof CloudflareModel>;
|
||||
|
||||
export const cloudflareWorkersAi = {
|
||||
id: "cloudflare-workers-ai",
|
||||
name: "Cloudflare Workers AI",
|
||||
modelsDir: "providers/cloudflare-workers-ai/models",
|
||||
async fetchModels() {
|
||||
const accountID = process.env.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID;
|
||||
const token = process.env.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN;
|
||||
if (accountID === undefined || token === undefined) {
|
||||
throw new Error(
|
||||
"Cloudflare Workers AI sync requires CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID and CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN",
|
||||
);
|
||||
}
|
||||
|
||||
const first = await fetchPage(accountID, token, 1);
|
||||
const models = parseCloudflareModels(first);
|
||||
const pageInfo = CloudflareOpenRouterResponse.safeParse(first).success
|
||||
? CloudflareOpenRouterResponse.parse(first).result_info
|
||||
: undefined;
|
||||
|
||||
for (let page = 2; page <= (pageInfo?.total_pages ?? 1); page++) {
|
||||
models.push(...parseCloudflareModels(await fetchPage(accountID, token, page)));
|
||||
}
|
||||
|
||||
return { data: models };
|
||||
},
|
||||
parseModels(raw) {
|
||||
return parseCloudflareModels(raw);
|
||||
},
|
||||
translateModel(model, context) {
|
||||
const normalized = normalizeModel(model);
|
||||
const id = normalized.id.replace(/^workers-ai\//, "");
|
||||
return {
|
||||
id,
|
||||
model: buildWorkersAiModel(normalized, context.existing(id)),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<CloudflareModel>;
|
||||
|
||||
function buildWorkersAiModel(model: z.infer<typeof OpenRouterModel>, existing: ExistingModel | undefined) {
|
||||
const synced = buildOpenRouterModel(model, existing);
|
||||
return {
|
||||
...synced,
|
||||
name: existing?.name ?? synced.name,
|
||||
release_date: existing?.release_date ?? synced.release_date,
|
||||
last_updated: existing?.last_updated ?? synced.last_updated,
|
||||
limit: {
|
||||
...synced.limit,
|
||||
output: existing?.limit?.output ?? synced.limit.output,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchPage(accountID: string, token: string, page: number) {
|
||||
const url = new URL(`${API_BASE}/${accountID}/ai/models/search`);
|
||||
url.searchParams.set("format", "openrouter");
|
||||
url.searchParams.set("per_page", "1000");
|
||||
url.searchParams.set("page", String(page));
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Cloudflare Workers AI models request failed: ${response.status} ${response.statusText}${await responseDetails(response)}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function parseCloudflareModels(raw: unknown) {
|
||||
const cloudflare = CloudflareResponse.safeParse(raw);
|
||||
if (cloudflare.success) return cloudflare.data.data;
|
||||
|
||||
const direct = OpenRouterResponse.safeParse(raw);
|
||||
if (direct.success) return direct.data.data;
|
||||
|
||||
const wrapped = CloudflareOpenRouterResponse.parse(raw);
|
||||
if (wrapped.result === undefined) {
|
||||
throw new Error("Cloudflare Workers AI response did not include model data");
|
||||
}
|
||||
return Array.isArray(wrapped.result) ? wrapped.result : wrapped.result.data;
|
||||
}
|
||||
|
||||
function normalizeModel(model: CloudflareModel) {
|
||||
if ("architecture" in model && "top_provider" in model && "supported_parameters" in model) {
|
||||
return OpenRouterModel.parse(model);
|
||||
}
|
||||
|
||||
return OpenRouterModel.parse({
|
||||
id: model.id.startsWith("@cf/") ? model.id : `@cf/${model.id.replace(/^@cf\//, "")}`,
|
||||
name: model.name,
|
||||
created: model.created,
|
||||
hugging_face_id: model.hugging_face_id ?? null,
|
||||
knowledge_cutoff: null,
|
||||
context_length: model.context_length,
|
||||
architecture: {
|
||||
input_modalities: model.input_modalities ?? ["text"],
|
||||
output_modalities: model.output_modalities ?? ["text"],
|
||||
},
|
||||
pricing: model.pricing,
|
||||
top_provider: {
|
||||
context_length: model.context_length,
|
||||
max_completion_tokens: model.max_output_length ?? null,
|
||||
},
|
||||
supported_parameters: [
|
||||
...model.supported_sampling_parameters ?? [],
|
||||
...model.supported_features ?? [],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async function responseDetails(response: Response) {
|
||||
const text = await response.text();
|
||||
if (text.length === 0) return "";
|
||||
|
||||
try {
|
||||
const body = z.object({
|
||||
errors: z.array(z.object({
|
||||
code: z.union([z.string(), z.number()]).optional(),
|
||||
message: z.string().optional(),
|
||||
}).passthrough()).optional(),
|
||||
}).passthrough().parse(JSON.parse(text));
|
||||
const details = body.errors
|
||||
?.map((error) => [error.code, error.message].filter(Boolean).join(": "))
|
||||
.filter((message) => message.length > 0)
|
||||
.join("; ");
|
||||
return details === undefined || details.length === 0 ? "" : ` (${details})`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { SyncProvider } from "../index.js";
|
||||
|
||||
const API_BASE = "https://api.fireworks.ai/v1";
|
||||
|
||||
const FireworksModel = z.object({
|
||||
name: z.string(),
|
||||
}).passthrough();
|
||||
|
||||
const FireworksResponse = z.object({
|
||||
models: z.array(FireworksModel).optional(),
|
||||
nextPageToken: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
type FireworksModel = z.infer<typeof FireworksModel>;
|
||||
|
||||
export const fireworksAi = {
|
||||
id: "fireworks-ai",
|
||||
name: "Fireworks AI",
|
||||
modelsDir: "providers/fireworks-ai/models",
|
||||
skipCreates: true,
|
||||
deleteMissing: false,
|
||||
sourceID(model) {
|
||||
return model.name;
|
||||
},
|
||||
skippedNotice(ids) {
|
||||
if (ids.length === 0) return [];
|
||||
return [
|
||||
`${ids.length} Fireworks AI models returned by the API were not created because the Models API does not provide pricing, output token limits, release dates, or complete capability metadata. Existing models are preserved unchanged.`,
|
||||
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
async fetchModels() {
|
||||
const key = process.env.FIREWORKS_API_KEY;
|
||||
if (key === undefined) {
|
||||
throw new Error("Fireworks AI sync requires FIREWORKS_API_KEY");
|
||||
}
|
||||
|
||||
const models: FireworksModel[] = [];
|
||||
let pageToken: string | undefined;
|
||||
|
||||
do {
|
||||
const url = new URL(`${API_BASE}/accounts/fireworks/models`);
|
||||
url.searchParams.set("pageSize", "200");
|
||||
url.searchParams.set("filter", "supports_serverless = true");
|
||||
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fireworks AI models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const page = FireworksResponse.parse(await response.json());
|
||||
models.push(...page.models ?? []);
|
||||
pageToken = page.nextPageToken || undefined;
|
||||
} while (pageToken !== undefined);
|
||||
|
||||
return { models };
|
||||
},
|
||||
parseModels(raw) {
|
||||
return FireworksResponse.parse(raw).models ?? [];
|
||||
},
|
||||
translateModel(model, context) {
|
||||
const existing = context.existing(model.name);
|
||||
if (existing === undefined) return undefined;
|
||||
|
||||
return {
|
||||
id: model.name,
|
||||
model: existing,
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<FireworksModel>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedModel } from "../sync-models.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
|
||||
|
||||
const API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import { z } from "zod";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { ModelFamilyValues } from "../../family.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
|
||||
const API_ENDPOINT = "https://openrouter.ai/api/v1/models";
|
||||
const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers");
|
||||
const modelFilesByProvider = new Map<string, Set<string>>();
|
||||
const canonicalTomlByModel = new Map<string, Record<string, unknown>>();
|
||||
|
||||
const CANONICAL_PROVIDER_PREFIXES = {
|
||||
anthropic: "anthropic",
|
||||
cohere: "cohere",
|
||||
deepseek: "deepseek",
|
||||
google: "google",
|
||||
meta: "llama",
|
||||
"meta-llama": "llama",
|
||||
minimax: "minimax",
|
||||
mistralai: "mistral",
|
||||
moonshotai: "moonshotai",
|
||||
openai: "openai",
|
||||
"x-ai": "xai",
|
||||
xai: "xai",
|
||||
xiaomi: "xiaomi",
|
||||
zai: "zai",
|
||||
"z-ai": "zai",
|
||||
} as const;
|
||||
|
||||
export const OpenRouterModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
created: z.number(),
|
||||
hugging_face_id: z.string().nullable(),
|
||||
knowledge_cutoff: z.string().nullable(),
|
||||
context_length: z.number(),
|
||||
architecture: z.object({
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
}),
|
||||
pricing: z.object({
|
||||
prompt: z.string(),
|
||||
completion: z.string(),
|
||||
internal_reasoning: z.string().optional(),
|
||||
input_cache_read: z.string().optional(),
|
||||
input_cache_write: z.string().optional(),
|
||||
}),
|
||||
top_provider: z.object({
|
||||
context_length: z.number().nullable(),
|
||||
max_completion_tokens: z.number().nullable(),
|
||||
}),
|
||||
supported_parameters: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const OpenRouterResponse = z.object({
|
||||
data: z.array(OpenRouterModel),
|
||||
}).passthrough();
|
||||
|
||||
export type OpenRouterModel = z.infer<typeof OpenRouterModel>;
|
||||
|
||||
export const openrouter = {
|
||||
id: "openrouter",
|
||||
name: "OpenRouter",
|
||||
modelsDir: "providers/openrouter/models",
|
||||
async fetchModels() {
|
||||
const headers = process.env.OPENROUTER_API_KEY
|
||||
? { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` }
|
||||
: undefined;
|
||||
const response = await fetch(API_ENDPOINT, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenRouter request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return OpenRouterResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildOpenRouterModel(model, context.existing(model.id)),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<OpenRouterModel>;
|
||||
|
||||
function dateFromTimestamp(timestamp: number) {
|
||||
return new Date(timestamp * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function price(value: string | undefined) {
|
||||
if (value === undefined) return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0
|
||||
? Math.round(number * 1_000_000_000_000) / 1_000_000
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
function modalities(values: string[], fallback: Modality[]): Modality[] {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = values
|
||||
.map((value) => value.toLowerCase())
|
||||
.map((value) => value === "file" ? "pdf" : value)
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
return [...new Set(result.length > 0 ? result : fallback)];
|
||||
}
|
||||
|
||||
function inferFamily(model: OpenRouterModel, name: string) {
|
||||
const target = `${model.id} ${name}`.toLowerCase();
|
||||
return [...ModelFamilyValues]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((family) => {
|
||||
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (family === "o") {
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
|
||||
}
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
|
||||
});
|
||||
}
|
||||
|
||||
export function buildOpenRouterModel(model: OpenRouterModel, existing: ExistingModel | undefined): SyncedModel {
|
||||
const params = new Set(model.supported_parameters);
|
||||
const name = model.name.replace(/^[^:]+:\s+/, "");
|
||||
const input = modalities(model.architecture.input_modalities, ["text"]);
|
||||
const output = modalities(model.architecture.output_modalities, ["text"]);
|
||||
const prompt = price(model.pricing.prompt);
|
||||
const completion = price(model.pricing.completion);
|
||||
const reasoning = params.has("reasoning") || params.has("include_reasoning");
|
||||
const context = model.top_provider.context_length ?? model.context_length;
|
||||
const family = inferFamily(model, name);
|
||||
const releaseDate = dateFromTimestamp(model.created);
|
||||
const familyValue = existing?.family === "o" && family !== "o"
|
||||
? family
|
||||
: (existing?.family ?? family);
|
||||
const attachment = input.some((value) => value !== "text");
|
||||
const toolCall = params.has("tools") || params.has("tool_choice");
|
||||
const structuredOutput = params.has("structured_outputs");
|
||||
const knowledge = model.knowledge_cutoff?.slice(0, 10) ?? existing?.knowledge;
|
||||
const openWeights = Boolean(model.hugging_face_id);
|
||||
const cost = prompt !== undefined && completion !== undefined
|
||||
? {
|
||||
input: prompt,
|
||||
output: completion,
|
||||
reasoning: reasoning ? price(model.pricing.internal_reasoning) : undefined,
|
||||
cache_read: price(model.pricing.input_cache_read),
|
||||
cache_write: price(model.pricing.input_cache_write),
|
||||
tiers: existing?.cost?.tiers,
|
||||
}
|
||||
: existing?.cost;
|
||||
const limit = {
|
||||
context,
|
||||
input: existing?.limit?.input,
|
||||
output: model.top_provider.max_completion_tokens ?? existing?.limit?.output ?? context,
|
||||
};
|
||||
const canonical = resolveCanonicalModel(model.id);
|
||||
|
||||
if (canonical !== undefined) {
|
||||
return {
|
||||
extends: {
|
||||
from: canonical.from,
|
||||
omit: canonicalOmit(canonical.provider, canonical.modelID, cost, limit),
|
||||
},
|
||||
...canonicalRuntimeOverrides(canonical.provider, canonical.modelID, {
|
||||
name: model.id.endsWith(":free") ? name : undefined,
|
||||
attachment,
|
||||
reasoning,
|
||||
}),
|
||||
temperature: params.has("temperature"),
|
||||
tool_call: toolCall,
|
||||
structured_output: structuredOutput,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
family: familyValue,
|
||||
release_date: releaseDate,
|
||||
last_updated: releaseDate,
|
||||
attachment,
|
||||
reasoning,
|
||||
temperature: params.has("temperature"),
|
||||
tool_call: toolCall,
|
||||
structured_output: structuredOutput,
|
||||
knowledge,
|
||||
open_weights: openWeights,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
} satisfies SyncedFullModel;
|
||||
}
|
||||
|
||||
function resolveCanonicalModel(openrouterID: string) {
|
||||
const [prefix, ...modelParts] = openrouterID.split("/");
|
||||
if (prefix === undefined || modelParts.length === 0) return undefined;
|
||||
if (openrouterID.startsWith("~/") || prefix.startsWith("~")) return undefined;
|
||||
|
||||
const provider = CANONICAL_PROVIDER_PREFIXES[prefix as keyof typeof CANONICAL_PROVIDER_PREFIXES];
|
||||
if (provider === undefined) return undefined;
|
||||
|
||||
const modelID = modelParts.join("/").replace(/:free$/, "");
|
||||
const candidates = canonicalCandidates(provider, modelID);
|
||||
const match = candidates.find((candidate) => {
|
||||
return canonicalModelExists(provider, candidate);
|
||||
});
|
||||
|
||||
return match === undefined
|
||||
? undefined
|
||||
: {
|
||||
from: `${provider}/${match}`,
|
||||
provider,
|
||||
modelID: match,
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalModelExists(provider: string, modelID: string) {
|
||||
let files = modelFilesByProvider.get(provider);
|
||||
if (files === undefined) {
|
||||
try {
|
||||
files = new Set(readdirSync(path.join(PROVIDERS_DIR, provider, "models")));
|
||||
} catch {
|
||||
files = new Set();
|
||||
}
|
||||
modelFilesByProvider.set(provider, files);
|
||||
}
|
||||
return files.has(`${modelID}.toml`);
|
||||
}
|
||||
|
||||
function canonicalOmit(
|
||||
provider: string,
|
||||
modelID: string,
|
||||
cost: SyncedFullModel["cost"],
|
||||
limit: SyncedFullModel["limit"],
|
||||
) {
|
||||
const toml = canonicalToml(provider, modelID);
|
||||
const omit = ["provider", "experimental"].filter((key) => toml[key] !== undefined);
|
||||
|
||||
const baseCost = toml.cost;
|
||||
if (baseCost !== undefined && baseCost !== null && typeof baseCost === "object" && !Array.isArray(baseCost)) {
|
||||
if (cost === undefined) {
|
||||
omit.push("cost");
|
||||
} else {
|
||||
for (const key of ["reasoning", "cache_read", "cache_write", "input_audio", "output_audio", "tiers"] as const) {
|
||||
if ((baseCost as Record<string, unknown>)[key] !== undefined && cost[key] === undefined) {
|
||||
omit.push(`cost.${key}`);
|
||||
}
|
||||
}
|
||||
if (hasLegacyContextOver200k(baseCost) && cost.tiers === undefined) {
|
||||
omit.push("cost.context_over_200k");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const baseLimit = toml.limit;
|
||||
if (
|
||||
baseLimit !== undefined &&
|
||||
baseLimit !== null &&
|
||||
typeof baseLimit === "object" &&
|
||||
!Array.isArray(baseLimit) &&
|
||||
(baseLimit as Record<string, unknown>).input !== undefined &&
|
||||
limit.input === undefined
|
||||
) {
|
||||
omit.push("limit.input");
|
||||
}
|
||||
|
||||
return omit.length > 0 ? omit : undefined;
|
||||
}
|
||||
|
||||
function hasLegacyContextOver200k(cost: object) {
|
||||
const tiers = (cost as { tiers?: unknown }).tiers;
|
||||
if (!Array.isArray(tiers) || tiers.length !== 1) return false;
|
||||
|
||||
const tier = tiers[0];
|
||||
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 size = (tierConfig as { size?: unknown }).size;
|
||||
return typeof size === "number" && size >= 200_000;
|
||||
}
|
||||
|
||||
function canonicalRuntimeOverrides(
|
||||
provider: string,
|
||||
modelID: string,
|
||||
values: Pick<SyncedFullModel, "name" | "attachment" | "reasoning">,
|
||||
) {
|
||||
const toml = canonicalToml(provider, modelID);
|
||||
return Object.fromEntries(
|
||||
Object.entries(values).filter(([key, value]) => value !== undefined && toml[key] !== value),
|
||||
);
|
||||
}
|
||||
|
||||
function canonicalToml(provider: string, modelID: string) {
|
||||
const key = `${provider}/${modelID}`;
|
||||
let toml = canonicalTomlByModel.get(key);
|
||||
if (toml === undefined) {
|
||||
const filePath = path.join(PROVIDERS_DIR, provider, "models", `${modelID}.toml`);
|
||||
toml = Bun.TOML.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
||||
canonicalTomlByModel.set(key, toml);
|
||||
}
|
||||
return toml;
|
||||
}
|
||||
|
||||
function canonicalCandidates(provider: string, modelID: string) {
|
||||
const candidates = [modelID];
|
||||
|
||||
if (provider === "anthropic") {
|
||||
candidates.push(modelID.replace(/(claude-(?:opus|sonnet|haiku)-\d+)\.(\d+)/, "$1-$2"));
|
||||
candidates.push(modelID.replace(/^claude-3\.5-/, "claude-3-5-"));
|
||||
}
|
||||
|
||||
if (provider === "llama") {
|
||||
candidates.push(modelID.replace(/^llama-(\d+)-(\d+)/, "llama-$1.$2"));
|
||||
candidates.push(modelID.replace(/^llama-(4)-(maverick|scout)$/, "llama-$1-$2-17b"));
|
||||
}
|
||||
|
||||
if (provider === "mistral") {
|
||||
candidates.push(modelID.replace(/-latest$/, ""));
|
||||
}
|
||||
|
||||
if (provider === "minimax") {
|
||||
candidates.push(modelID.replace(/^minimax-m/, "MiniMax-M"));
|
||||
}
|
||||
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
|
||||
|
||||
const API_BASE = "https://api.x.ai/v1";
|
||||
|
||||
const XAIModel = z.object({
|
||||
id: z.string(),
|
||||
canonical_id: z.string().optional(),
|
||||
created: z.number().int().nonnegative(),
|
||||
aliases: z.array(z.string()).optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
prompt_text_token_price: z.number().int().nonnegative().optional(),
|
||||
cached_prompt_text_token_price: z.number().int().nonnegative().optional(),
|
||||
completion_text_token_price: z.number().int().nonnegative().optional(),
|
||||
max_prompt_length: z.number().int().nonnegative().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const XAIModelList = z.object({
|
||||
models: z.array(XAIModel),
|
||||
}).passthrough();
|
||||
|
||||
const XAIResponse = z.object({
|
||||
models: z.array(XAIModel),
|
||||
});
|
||||
|
||||
const XAIAPIKey = z.object({
|
||||
acls: z.array(z.string()),
|
||||
}).passthrough();
|
||||
|
||||
type XAIModel = z.infer<typeof XAIModel>;
|
||||
|
||||
export const xai = {
|
||||
id: "xai",
|
||||
name: "xAI",
|
||||
modelsDir: "providers/xai/models",
|
||||
skipCreates: true,
|
||||
sourceID(model) {
|
||||
return model.id;
|
||||
},
|
||||
skippedNotice(ids) {
|
||||
if (ids.length === 0) return [];
|
||||
return [
|
||||
`${ids.length} xAI models returned by the API were not created because the Models API does not provide enough authoritative metadata for the catalog, especially output token limits and some feature/capability flags. Existing models are still updated from API-authoritative fields.`,
|
||||
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
async fetchModels() {
|
||||
const key = process.env.XAI_API_KEY;
|
||||
if (key === undefined) throw new Error("xAI sync requires XAI_API_KEY");
|
||||
await assertFullModelAccess(key);
|
||||
|
||||
const models = await Promise.all([
|
||||
fetchTypedModels(key, "language-models"),
|
||||
fetchTypedModels(key, "image-generation-models"),
|
||||
fetchTypedModels(key, "video-generation-models"),
|
||||
]);
|
||||
|
||||
return { models: models.flat() };
|
||||
},
|
||||
parseModels(raw) {
|
||||
const models = XAIResponse.parse(raw).models;
|
||||
const seen = new Set<string>();
|
||||
const expanded: XAIModel[] = [];
|
||||
|
||||
for (const model of models) {
|
||||
if (!seen.has(model.id)) {
|
||||
seen.add(model.id);
|
||||
expanded.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
for (const alias of model.aliases ?? []) {
|
||||
if (seen.has(alias)) continue;
|
||||
seen.add(alias);
|
||||
expanded.push({ ...model, id: alias, canonical_id: model.id });
|
||||
}
|
||||
}
|
||||
|
||||
return expanded;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
const existing = context.existing(model.id);
|
||||
if (existing === undefined) return undefined;
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildModel(model, existing),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<XAIModel>;
|
||||
|
||||
async function assertFullModelAccess(key: string) {
|
||||
const response = await fetch(`${API_BASE}/api-key`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`xAI API key metadata request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const apiKey = XAIAPIKey.parse(await response.json());
|
||||
if (!apiKey.acls.includes("api-key:model:*")) {
|
||||
throw new Error("xAI sync requires XAI_API_KEY to include api-key:model:* so the model list is not ACL-filtered");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTypedModels(key: string, endpoint: string) {
|
||||
const response = await fetch(`${API_BASE}/${endpoint}`, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`xAI ${endpoint} request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return XAIModelList.parse(await response.json()).models;
|
||||
}
|
||||
|
||||
function dateFromTimestamp(timestamp: number) {
|
||||
return new Date(timestamp * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
function modalities(values: string[] | undefined, fallback: Modality[]) {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = (values ?? [])
|
||||
.map((value) => value.toLowerCase())
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
if (result.includes("image")) result.push("pdf");
|
||||
return [...new Set(result.length > 0 ? result : fallback)];
|
||||
}
|
||||
|
||||
function tokenPrice(value: number | undefined) {
|
||||
if (value === undefined) return undefined;
|
||||
return value / 10_000;
|
||||
}
|
||||
|
||||
function preservedCostTiers(existing: ExistingModel) {
|
||||
// The xAI models API exposes base pricing only; long-context tiers are curated from xAI docs/console.
|
||||
return existing.cost?.tiers;
|
||||
}
|
||||
|
||||
function cost(model: XAIModel, existing: ExistingModel) {
|
||||
const input = tokenPrice(model.prompt_text_token_price);
|
||||
const output = tokenPrice(model.completion_text_token_price);
|
||||
if (input === undefined || output === undefined) return existing.cost;
|
||||
|
||||
return {
|
||||
input,
|
||||
output,
|
||||
reasoning: existing.cost?.reasoning,
|
||||
cache_read: tokenPrice(model.cached_prompt_text_token_price),
|
||||
cache_write: existing.cost?.cache_write,
|
||||
input_audio: existing.cost?.input_audio,
|
||||
output_audio: existing.cost?.output_audio,
|
||||
tiers: preservedCostTiers(existing),
|
||||
};
|
||||
}
|
||||
|
||||
function buildModel(model: XAIModel, existing: ExistingModel): SyncedModel {
|
||||
const name = existing.name;
|
||||
const attachment = existing.attachment;
|
||||
const reasoning = existing.reasoning;
|
||||
const toolCall = existing.tool_call;
|
||||
const openWeights = existing.open_weights;
|
||||
const limit = existing.limit;
|
||||
const releaseDate = existing.release_date;
|
||||
const lastUpdated = existing.last_updated;
|
||||
|
||||
if (
|
||||
name === undefined
|
||||
|| attachment === undefined
|
||||
|| reasoning === undefined
|
||||
|| toolCall === undefined
|
||||
|| openWeights === undefined
|
||||
|| limit === undefined
|
||||
|| (model.canonical_id !== undefined && releaseDate === undefined)
|
||||
|| (model.canonical_id !== undefined && lastUpdated === undefined)
|
||||
) {
|
||||
throw new Error(`xAI model ${model.id} has incomplete local TOML metadata required for sync`);
|
||||
}
|
||||
|
||||
const input = modalities(model.input_modalities, existing.modalities?.input ?? ["text"]);
|
||||
const output = modalities(model.output_modalities, existing.modalities?.output ?? ["text"]);
|
||||
const created = dateFromTimestamp(model.created);
|
||||
|
||||
return {
|
||||
name,
|
||||
family: existing.family,
|
||||
release_date: model.canonical_id === undefined ? created : releaseDate!,
|
||||
last_updated: model.canonical_id === undefined ? created : lastUpdated!,
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning,
|
||||
temperature: existing.temperature,
|
||||
tool_call: toolCall,
|
||||
structured_output: existing.structured_output,
|
||||
knowledge: existing.knowledge,
|
||||
open_weights: openWeights,
|
||||
status: existing.status,
|
||||
interleaved: existing.interleaved,
|
||||
cost: cost(model, existing),
|
||||
limit: {
|
||||
input: limit.input,
|
||||
context: model.max_prompt_length ?? limit.context,
|
||||
output: limit.output,
|
||||
},
|
||||
modalities: { input, output },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "Qwen3.7 Max"
|
||||
family = "qwen"
|
||||
release_date = "2026-05-21"
|
||||
last_updated = "2026-05-21"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 2.50
|
||||
output = 7.50
|
||||
cache_read = 0.50
|
||||
cache_write = 3.125
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Gemma Sea Lion V4 27B It"
|
||||
family = "gemma"
|
||||
release_date = "2025-09-23"
|
||||
last_updated = "2025-09-23"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.351
|
||||
output = 0.555
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
name = "Deepseek R1 Distill Qwen 32B"
|
||||
family = "deepseek"
|
||||
release_date = "2025-01-22"
|
||||
last_updated = "2025-01-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.497
|
||||
output = 4.881
|
||||
|
||||
[limit]
|
||||
context = 80_000
|
||||
output = 80_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Gemma 3 12B It"
|
||||
family = "gemma"
|
||||
release_date = "2025-03-18"
|
||||
last_updated = "2025-03-18"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.345
|
||||
output = 0.556
|
||||
|
||||
[limit]
|
||||
context = 80_000
|
||||
output = 80_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -6,15 +6,16 @@ attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.10
|
||||
output = 0.30
|
||||
input = 0.1
|
||||
output = 0.3
|
||||
|
||||
[limit]
|
||||
context = 256000
|
||||
output = 16384
|
||||
context = 256_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
name = "Trinity Large Preview"
|
||||
family = "trinity"
|
||||
release_date = "2026-01-27"
|
||||
last_updated = "2026-01-27"
|
||||
name = "Granite 4.0 H Micro"
|
||||
family = "granite"
|
||||
release_date = "2025-10-07"
|
||||
last_updated = "2025-10-07"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.15
|
||||
output = 0.45
|
||||
input = 0.017
|
||||
output = 0.112
|
||||
|
||||
[limit]
|
||||
context = 131_000
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 2 7B Chat fp16"
|
||||
family = "llama"
|
||||
release_date = "2023-11-07"
|
||||
last_updated = "2023-11-07"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.556
|
||||
output = 6.667
|
||||
|
||||
[limit]
|
||||
context = 4_096
|
||||
output = 4_096
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3 8B Instruct Awq"
|
||||
family = "llama"
|
||||
release_date = "2024-05-09"
|
||||
last_updated = "2024-05-09"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.123
|
||||
output = 0.266
|
||||
|
||||
[limit]
|
||||
context = 8_192
|
||||
output = 8_192
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3 8B Instruct"
|
||||
family = "llama"
|
||||
release_date = "2024-04-18"
|
||||
last_updated = "2024-04-18"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.282
|
||||
output = 0.827
|
||||
|
||||
[limit]
|
||||
context = 7_968
|
||||
output = 7_968
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.1 8B Instruct Awq"
|
||||
family = "llama"
|
||||
release_date = "2024-07-25"
|
||||
last_updated = "2024-07-25"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.123
|
||||
output = 0.266
|
||||
|
||||
[limit]
|
||||
context = 8_192
|
||||
output = 8_192
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.1 8B Instruct fp8"
|
||||
family = "llama"
|
||||
release_date = "2024-07-25"
|
||||
last_updated = "2024-07-25"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.152
|
||||
output = 0.287
|
||||
|
||||
[limit]
|
||||
context = 32_000
|
||||
output = 32_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.2 11B Vision Instruct"
|
||||
family = "llama"
|
||||
release_date = "2024-09-25"
|
||||
last_updated = "2024-09-25"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.0485
|
||||
output = 0.676
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.2 1B Instruct"
|
||||
family = "llama"
|
||||
release_date = "2024-09-25"
|
||||
last_updated = "2024-09-25"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.027
|
||||
output = 0.201
|
||||
|
||||
[limit]
|
||||
context = 60_000
|
||||
output = 60_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.2 3B Instruct"
|
||||
family = "llama"
|
||||
release_date = "2024-09-25"
|
||||
last_updated = "2024-09-25"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.0509
|
||||
output = 0.335
|
||||
|
||||
[limit]
|
||||
context = 80_000
|
||||
output = 80_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama 3.3 70B Instruct fp8 Fast"
|
||||
family = "llama"
|
||||
release_date = "2024-12-06"
|
||||
last_updated = "2024-12-06"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.293
|
||||
output = 2.253
|
||||
|
||||
[limit]
|
||||
context = 24_000
|
||||
output = 24_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
@@ -13,8 +14,8 @@ input = 0.27
|
||||
output = 0.85
|
||||
|
||||
[limit]
|
||||
context = 128000
|
||||
output = 16384
|
||||
context = 131_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Llama Guard 3 8B"
|
||||
family = "llama"
|
||||
release_date = "2025-01-22"
|
||||
last_updated = "2025-01-22"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.484
|
||||
output = 0.03
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Mistral 7B Instruct V0.1"
|
||||
family = "mistral"
|
||||
release_date = "2023-11-07"
|
||||
last_updated = "2023-11-07"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.11
|
||||
output = 0.19
|
||||
|
||||
[limit]
|
||||
context = 2_824
|
||||
output = 2_824
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
name = "Mistral Small 3.1 24B Instruct"
|
||||
family = "mistral-small"
|
||||
release_date = "2025-03-18"
|
||||
last_updated = "2025-03-18"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.351
|
||||
output = 0.555
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -4,9 +4,9 @@ release_date = "2026-04-20"
|
||||
last_updated = "2026-04-20"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
structured_output = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = true
|
||||
|
||||
@@ -15,11 +15,11 @@ field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.95
|
||||
output = 4.00
|
||||
output = 4
|
||||
cache_read = 0.16
|
||||
|
||||
[limit]
|
||||
context = 256_000
|
||||
context = 262_144
|
||||
output = 256_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -6,14 +6,15 @@ attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.50
|
||||
output = 1.50
|
||||
input = 0.5
|
||||
output = 1.5
|
||||
|
||||
[limit]
|
||||
context = 256_000
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
name = "GPT OSS 120B"
|
||||
family = "gpt-oss"
|
||||
release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
@@ -12,8 +14,8 @@ input = 0.35
|
||||
output = 0.75
|
||||
|
||||
[limit]
|
||||
context = 128000
|
||||
output = 16384
|
||||
context = 128_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
name = "GPT OSS 20B"
|
||||
family = "gpt-oss"
|
||||
release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
@@ -12,8 +14,8 @@ input = 0.2
|
||||
output = 0.3
|
||||
|
||||
[limit]
|
||||
context = 128000
|
||||
output = 16384
|
||||
context = 128_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Qwen2.5 Coder 32B Instruct"
|
||||
family = "qwen"
|
||||
release_date = "2025-02-27"
|
||||
last_updated = "2025-02-27"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.66
|
||||
output = 1
|
||||
|
||||
[limit]
|
||||
context = 32_768
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Qwen3 30B A3b fp8"
|
||||
family = "qwen"
|
||||
release_date = "2025-04-30"
|
||||
last_updated = "2025-04-30"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.0509
|
||||
output = 0.335
|
||||
|
||||
[limit]
|
||||
context = 32_768
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Qwq 32B"
|
||||
family = "qwen"
|
||||
release_date = "2025-03-05"
|
||||
last_updated = "2025-03-05"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.66
|
||||
output = 1
|
||||
|
||||
[limit]
|
||||
context = 24_000
|
||||
output = 24_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -6,12 +6,13 @@ attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.06
|
||||
output = 0.40
|
||||
input = 0.0605
|
||||
output = 0.4
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "DeepSeek V3.2"
|
||||
family = "deepseek"
|
||||
release_date = "2025-07-22"
|
||||
last_updated = "2025-07-22"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.28
|
||||
output = 0.38
|
||||
cache_read = 0.06
|
||||
|
||||
[limit]
|
||||
context = 163_840
|
||||
output = 163_840
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "deepseek/deepseek-v4-flash"
|
||||
|
||||
[cost]
|
||||
input = 0.12
|
||||
output = 0.21
|
||||
cache_read = 0.02
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 131_072
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "DeepSeek V4 Pro (Precision)"
|
||||
family = "deepseek-thinking"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
output = 2.50
|
||||
cache_read = 0.10
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "deepseek/deepseek-v4-pro"
|
||||
|
||||
[cost]
|
||||
input = 0.40
|
||||
output = 0.85
|
||||
cache_read = 0.003
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 131_072
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "google/gemma-4-31b-it"
|
||||
|
||||
[cost]
|
||||
input = 0.10
|
||||
output = 0.30
|
||||
cache_read = 0.02
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,10 @@
|
||||
[extends]
|
||||
from = "zhipuai/glm-4.7-flash"
|
||||
|
||||
[cost]
|
||||
input = 0.04
|
||||
output = 0.30
|
||||
cache_read = 0.008
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "zhipuai/glm-4.7"
|
||||
|
||||
[cost]
|
||||
input = 0.25
|
||||
output = 1.10
|
||||
cache_read = 0.05
|
||||
|
||||
[limit]
|
||||
context = 202_752
|
||||
output = 202_752
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "GLM 5.1 (Precision)"
|
||||
family = "glm"
|
||||
release_date = "2026-03-27"
|
||||
last_updated = "2026-03-27"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.75
|
||||
output = 2.90
|
||||
cache_read = 0.15
|
||||
|
||||
[limit]
|
||||
context = 202_752
|
||||
output = 202_752
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "zhipuai/glm-5.1"
|
||||
|
||||
[cost]
|
||||
input = 0.45
|
||||
output = 2.10
|
||||
cache_read = 0.09
|
||||
|
||||
[limit]
|
||||
context = 202_752
|
||||
output = 202_752
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "zhipuai/glm-5"
|
||||
|
||||
[cost]
|
||||
input = 0.48
|
||||
output = 1.90
|
||||
cache_read = 0.10
|
||||
|
||||
[limit]
|
||||
context = 202_752
|
||||
output = 202_752
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,21 @@
|
||||
name = "Experiment!: Greg"
|
||||
release_date = "2026-01-27"
|
||||
last_updated = "2026-01-27"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.10
|
||||
output = 0.20
|
||||
cache_read = 0.02
|
||||
|
||||
[limit]
|
||||
context = 229_376
|
||||
output = 229_376
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "Kimi K2.5 (Lightning)"
|
||||
family = "kimi-k2.5"
|
||||
release_date = "2026-02-06"
|
||||
last_updated = "2026-02-06"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 1.00
|
||||
output = 3.00
|
||||
cache_read = 0.20
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,10 @@
|
||||
[extends]
|
||||
from = "moonshotai/kimi-k2.5"
|
||||
|
||||
[cost]
|
||||
input = 0.35
|
||||
output = 1.70
|
||||
cache_read = 0.07
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "Kimi K2.6 (Precision)"
|
||||
family = "kimi-k2.6"
|
||||
release_date = "2026-04-21"
|
||||
last_updated = "2026-04-21"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.55
|
||||
output = 2.70
|
||||
cache_read = 0.11
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,10 @@
|
||||
[extends]
|
||||
from = "moonshotai/kimi-k2.6"
|
||||
|
||||
[cost]
|
||||
input = 0.50
|
||||
output = 1.99
|
||||
cache_read = 0.10
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,25 @@
|
||||
name = "MiMo-V2.5-Pro (Precision)"
|
||||
family = "mimo"
|
||||
release_date = "2026-04-22"
|
||||
last_updated = "2026-04-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.80
|
||||
output = 2.50
|
||||
cache_read = 0.16
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,10 @@
|
||||
[extends]
|
||||
from = "xiaomi/mimo-v2.5-pro"
|
||||
|
||||
[cost]
|
||||
input = 0.50
|
||||
output = 1.50
|
||||
cache_read = 0.10
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,12 @@
|
||||
reasoning = false
|
||||
|
||||
[extends]
|
||||
from = "minimax/MiniMax-M2.5"
|
||||
|
||||
[cost]
|
||||
input = 0.11
|
||||
output = 0.95
|
||||
cache_read = 0.02
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "alibaba/qwen3.5-397b-a17b"
|
||||
|
||||
[cost]
|
||||
input = 0.35
|
||||
output = 1.75
|
||||
cache_read = 0.07
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "Qwen3.5 9B"
|
||||
family = "qwen"
|
||||
release_date = "2026-03-13"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.04
|
||||
output = 0.15
|
||||
cache_read = 0.008
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video", "audio"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,14 @@
|
||||
[extends]
|
||||
from = "alibaba/qwen3.6-27b"
|
||||
|
||||
[cost]
|
||||
input = 0.20
|
||||
output = 1.50
|
||||
cache_read = 0.04
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
@@ -0,0 +1,5 @@
|
||||
name = "CrofAI"
|
||||
env = ["CROF_API_KEY"]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
api = "https://crof.ai/v1"
|
||||
doc = "https://crof.ai/docs"
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90.158706 90.190743" fill="currentColor">
|
||||
<g transform="matrix(0.32039341,0,0,0.32039341,-193.03703,-175.63967)">
|
||||
<polygon points="720.6,711.5 602.5,711.5 691.7,622.3 809.8,622.3" />
|
||||
<polygon points="720.6,711.5 809.8,622.3 809.8,711.5" />
|
||||
<polygon points="720.6,711.5 809.8,711.5 809.8,739.5 719.7,829.6 719.2,829.6 719.2,712.8" />
|
||||
<polygon points="602.5,711.5 720.6,711.5 602.5,829.6" />
|
||||
<path d="m 809.8,739.5 v 89.7 c -0.1,0.1 -0.3,0.3 -0.5,0.5 h -89.7 z" />
|
||||
<polygon points="691.7,622.3 602.5,622.3 676.5,548.2 765.7,548.2" />
|
||||
<polygon points="825.9,606.2 707.8,606.2 765.7,548.2 883.9,548.2" />
|
||||
<polygon points="602.5,622.3 691.7,622.3 602.5,711.5" />
|
||||
<polygon points="602.5,829.6 719.2,712.8 719.2,829.6" />
|
||||
<path d="m 809.8,622.3 74,-74 c 0,0 0,130.6 0,206.9 -23.2,23.2 -49.3,49.3 -74,74 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 889 B |
@@ -0,0 +1,15 @@
|
||||
name = "MiniMax M2.5"
|
||||
structured_output = true
|
||||
|
||||
[extends]
|
||||
from = "minimax/MiniMax-M2.5"
|
||||
|
||||
[cost]
|
||||
input = 0.24
|
||||
output = 0.9
|
||||
cache_read = 0.03
|
||||
cache_write = 0
|
||||
|
||||
[limit]
|
||||
context = 196608
|
||||
output = 196608
|
||||
@@ -0,0 +1,12 @@
|
||||
[extends]
|
||||
from = "moonshotai/kimi-k2.6"
|
||||
|
||||
[cost]
|
||||
input = 0.78
|
||||
output = 3.5
|
||||
cache_read = 0.2
|
||||
cache_write = 0
|
||||
|
||||
[modalities]
|
||||
input = [ "text", "image" ]
|
||||
output = [ "text" ]
|
||||
@@ -0,0 +1,15 @@
|
||||
name = "Llama 3.3 70B Instruct"
|
||||
structured_output = true
|
||||
|
||||
[extends]
|
||||
from = "llama/llama-3.3-70b-instruct"
|
||||
|
||||
[cost]
|
||||
input = 0.12
|
||||
output = 0.38
|
||||
cache_read = 0
|
||||
cache_write = 0
|
||||
|
||||
[limit]
|
||||
context = 131072
|
||||
output = 131072
|
||||
@@ -0,0 +1,14 @@
|
||||
name = "GLM 5.1"
|
||||
|
||||
[extends]
|
||||
from = "zai/glm-5.1"
|
||||
|
||||
[cost]
|
||||
input = 1.4
|
||||
output = 4.4
|
||||
cache_read = 0.26
|
||||
cache_write = 0
|
||||
|
||||
[limit]
|
||||
context = 202752
|
||||
output = 202752
|
||||
@@ -0,0 +1,5 @@
|
||||
name = "Inceptron"
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
env = ["INCEPTRON_API_KEY"]
|
||||
api = "https://api.inceptron.io/v1"
|
||||
doc = "https://docs.inceptron.io"
|
||||
@@ -0,0 +1,2 @@
|
||||
[extends]
|
||||
from = "google/gemini-3.5-flash"
|
||||
@@ -0,0 +1,2 @@
|
||||
[extends]
|
||||
from = "xai/grok-4.20-0309-non-reasoning"
|
||||
@@ -0,0 +1,2 @@
|
||||
[extends]
|
||||
from = "xai/grok-4.20-0309-reasoning"
|
||||
@@ -0,0 +1,2 @@
|
||||
[extends]
|
||||
from = "alibaba/qwen3.7-max"
|
||||
@@ -0,0 +1,28 @@
|
||||
name = "Gemini 3.5 Flash"
|
||||
family = "gemini-flash"
|
||||
release_date = "2026-05-19"
|
||||
last_updated = "2026-05-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.50
|
||||
output = 9.00
|
||||
cache_read = 0.15
|
||||
input_audio = 1.50
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video", "audio", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
[provider]
|
||||
npm = "@ai-sdk/google"
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "Grok Build 0.1"
|
||||
family = "grok-build"
|
||||
release_date = "2026-05-20"
|
||||
last_updated = "2026-05-20"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.00
|
||||
output = 2.00
|
||||
cache_read = 0.20
|
||||
|
||||
[limit]
|
||||
context = 256_000
|
||||
output = 256_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -8,6 +8,7 @@ temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = true
|
||||
status = "deprecated"
|
||||
|
||||
[cost]
|
||||
input = 0
|
||||
|
||||
@@ -8,6 +8,7 @@ temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = false
|
||||
status = "deprecated"
|
||||
|
||||
[cost]
|
||||
input = 0
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Claude Haiku 4.5"
|
||||
family = "claude-haiku"
|
||||
release_date = "2025-10-15"
|
||||
last_updated = "2025-10-15"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-02-28"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-haiku-4-5"
|
||||
|
||||
[cost]
|
||||
input = 1
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Claude Opus 4.1"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-opus-4-1"
|
||||
|
||||
[cost]
|
||||
input = 15
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Claude Opus 4.5"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-11-24"
|
||||
last_updated = "2025-11-24"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-05-30"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-opus-4-5"
|
||||
|
||||
[cost]
|
||||
input = 5
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Claude Opus 4.6"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-02-04"
|
||||
last_updated = "2026-02-04"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-05-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-opus-4-6"
|
||||
omit = ["experimental"]
|
||||
|
||||
[cost]
|
||||
input = 5
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Claude Opus 4.7"
|
||||
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
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-opus-4-7"
|
||||
omit = ["experimental"]
|
||||
|
||||
[cost]
|
||||
input = 5
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Claude Sonnet 4.5"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2025-09-29"
|
||||
last_updated = "2025-09-29"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-sonnet-4-5"
|
||||
|
||||
[cost]
|
||||
input = 3
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Claude Sonnet 4.6"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2026-02-17"
|
||||
last_updated = "2026-02-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-08-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "anthropic/claude-sonnet-4-6"
|
||||
|
||||
[cost]
|
||||
input = 3
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Command R (08-2024)"
|
||||
family = "command-r"
|
||||
release_date = "2024-08-30"
|
||||
last_updated = "2024-08-30"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2024-03-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "cohere/command-r-08-2024"
|
||||
|
||||
[cost]
|
||||
input = 0.15
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Command R+ (08-2024)"
|
||||
family = "command-r"
|
||||
release_date = "2024-08-30"
|
||||
last_updated = "2024-08-30"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2024-03-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "cohere/command-r-plus-08-2024"
|
||||
|
||||
[cost]
|
||||
input = 2.5
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Command R7B (12-2024)"
|
||||
family = "command-r"
|
||||
release_date = "2024-12-14"
|
||||
last_updated = "2024-12-14"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = true
|
||||
knowledge = "2024-08-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "cohere/command-r7b-12-2024"
|
||||
|
||||
[cost]
|
||||
input = 0.0375
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
name = "DeepSeek V3"
|
||||
family = "deepseek"
|
||||
release_date = "2024-12-26"
|
||||
last_updated = "2024-12-26"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2024-07-31"
|
||||
open_weights = true
|
||||
|
||||
[extends]
|
||||
from = "deepseek/deepseek-chat"
|
||||
omit = ["cost.cache_read"]
|
||||
|
||||
[cost]
|
||||
input = 0.32
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
name = "DeepSeek V4 Flash"
|
||||
family = "deepseek"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[extends]
|
||||
from = "deepseek/deepseek-v4-flash"
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
name = "DeepSeek V4 Flash (free)"
|
||||
family = "deepseek"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = true
|
||||
|
||||
[extends]
|
||||
from = "deepseek/deepseek-v4-flash"
|
||||
omit = ["cost.cache_read"]
|
||||
|
||||
[cost]
|
||||
input = 0
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
name = "DeepSeek V4 Pro"
|
||||
family = "deepseek"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[extends]
|
||||
from = "deepseek/deepseek-v4-pro"
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
@@ -17,7 +17,7 @@ cache_read = 0.025
|
||||
cache_write = 0.083333
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
context = 1_000_000
|
||||
output = 8_192
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Nano Banana (Gemini 2.5 Flash Image)"
|
||||
family = "gemini"
|
||||
release_date = "2025-10-07"
|
||||
last_updated = "2025-10-07"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-2.5-flash-image"
|
||||
|
||||
[cost]
|
||||
input = 0.3
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Gemini 2.5 Flash Lite"
|
||||
family = "gemini-flash-lite"
|
||||
release_date = "2025-07-22"
|
||||
last_updated = "2025-07-22"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-2.5-flash-lite"
|
||||
omit = ["cost.input_audio"]
|
||||
|
||||
[cost]
|
||||
input = 0.1
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Gemini 2.5 Flash"
|
||||
family = "gemini-flash"
|
||||
release_date = "2025-06-17"
|
||||
last_updated = "2025-06-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-2.5-flash"
|
||||
omit = ["cost.input_audio"]
|
||||
|
||||
[cost]
|
||||
input = 0.3
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Gemini 2.5 Pro"
|
||||
family = "gemini"
|
||||
release_date = "2025-06-17"
|
||||
last_updated = "2025-06-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01-31"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-2.5-pro"
|
||||
omit = ["cost.tiers", "cost.context_over_200k"]
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
name = "Gemini 3 Flash Preview"
|
||||
family = "gemini-flash"
|
||||
release_date = "2025-12-17"
|
||||
last_updated = "2025-12-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3-flash-preview"
|
||||
omit = ["cost.input_audio"]
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Nano Banana 2 (Gemini 3.1 Flash Image Preview)"
|
||||
family = "gemini-flash"
|
||||
release_date = "2026-02-26"
|
||||
last_updated = "2026-02-26"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = false
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3.1-flash-image-preview"
|
||||
|
||||
[cost]
|
||||
input = 0.5
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
name = "Gemini 3.1 Flash Lite Preview"
|
||||
family = "gemini-flash-lite"
|
||||
release_date = "2026-03-03"
|
||||
last_updated = "2026-03-03"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3.1-flash-lite-preview"
|
||||
omit = ["cost.input_audio"]
|
||||
|
||||
[cost]
|
||||
input = 0.25
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
name = "Gemini 3.1 Flash Lite"
|
||||
family = "gemini"
|
||||
release_date = "2026-05-07"
|
||||
last_updated = "2026-05-07"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3.1-flash-lite"
|
||||
omit = ["cost.input_audio"]
|
||||
|
||||
[cost]
|
||||
input = 0.25
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Gemini 3.1 Pro Preview Custom Tools"
|
||||
family = "gemini-pro"
|
||||
release_date = "2026-02-25"
|
||||
last_updated = "2026-02-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3.1-pro-preview-customtools"
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user