Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,15 @@ 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 }}
|
||||
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 +73,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,481 @@
|
||||
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 { 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;
|
||||
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>;
|
||||
google: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
xai: SyncProvider<any>;
|
||||
} = {
|
||||
"cloudflare-workers-ai": cloudflareWorkersAi,
|
||||
google,
|
||||
openrouter,
|
||||
xai,
|
||||
};
|
||||
|
||||
export const groups = {
|
||||
aggregators: ["openrouter"],
|
||||
cloudflare: ["cloudflare-workers-ai"],
|
||||
direct: ["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 (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 "";
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
name = "Gemini 3.1 Pro Preview"
|
||||
family = "gemini-pro"
|
||||
release_date = "2026-02-19"
|
||||
last_updated = "2026-02-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[extends]
|
||||
from = "google/gemini-3.1-pro-preview"
|
||||
|
||||
[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