Compare commits

...

61 Commits

Author SHA1 Message Date
Aiden Cline c8dc9cae24 chore(sync): keep fireworks catalog entries curated 2026-05-21 18:32:00 -05:00
Aiden Cline 964775cf77 chore(sync): standardize fireworks api key env 2026-05-21 18:25:07 -05:00
Aiden Cline 7509cf1797 chore(sync): add fireworks ai provider 2026-05-21 18:18:35 -05:00
Aiden Cline 2738f81d1a Merge pull request #1828 from anomalyco/refactor/sync-core-layout
refactor: move sync implementation into core src
2026-05-21 18:10:09 -05:00
Aiden Cline 89b834086a refactor: move sync implementation into core src 2026-05-21 18:06:25 -05:00
Aiden Cline 1ab2ff8163 Merge pull request #1826 from smakosh/add-llmgateway-models
feat: add new LLM Gateway text models
2026-05-21 17:58:29 -05:00
Frank 9468676683 update zen models 2026-05-21 18:42:36 -04:00
Claude 6cdd2f054b Merge upstream/dev into add-llmgateway-models; resolve gemini-3.5-flash conflict
# Conflicts:
#	providers/google/models/gemini-3.5-flash.toml
2026-05-21 21:48:48 +00:00
Aiden Cline b13abc9141 Merge pull request #1827 from anomalyco/update-xai-pricing
fix xAI long-context pricing
2026-05-21 16:45:43 -05:00
Aiden Cline e5ba264751 fix xAI long-context pricing 2026-05-21 16:41:36 -05:00
smakosh a7811fb522 refactor: use extends for gemini and qwen models
Add canonical google/gemini-3.5-flash and alibaba/qwen3.7-max defs and
have the llmgateway entries extend them, per PR review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:38:57 +02:00
smakosh 605fae75d9 feat: add new LLM Gateway text models
Add Grok 4.20 (reasoning/non-reasoning), Gemini 3.5 Flash, and Qwen3.7 Max to the llmgateway provider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:04:47 +02:00
Aiden Cline 26b05268ae Merge pull request #1824 from anomalyco/fix/vercel-gemini-35-flash
Add new Vercel AI Gateway models
2026-05-21 15:30:35 -05:00
Aiden Cline 1aee13d2e5 Add new Vercel AI Gateway models 2026-05-21 13:21:25 -05:00
Aiden Cline 0a924e6bb2 Merge pull request #1822 from anomalyco/automation/sync-models-xai
chore(sync): update xAI model catalog
2026-05-21 13:05:20 -05:00
Aiden Cline 9769b2b11d Merge pull request #1823 from anomalyco/automation/sync-models-openrouter
chore(sync): update OpenRouter model catalog
2026-05-21 13:05:13 -05:00
github-actions[bot] 1b0db099cf chore(sync): update OpenRouter model catalog 2026-05-21 17:56:13 +00:00
github-actions[bot] 16ed78587c chore(sync): update xAI model catalog 2026-05-21 17:56:11 +00:00
Frank 0b88965165 update zen models 2026-05-21 13:41:55 -04:00
Aiden Cline acc704ce39 Merge pull request #1797 from arnavchachra/add-crof-provider
add crof.ai provider with 21 models
2026-05-21 11:43:02 -05:00
Aiden Cline 51ad3b264e Merge pull request #1821 from anomalyco/sync-provider-ci
chore: automate provider sync jobs
2026-05-21 11:23:49 -05:00
Aiden Cline 146b6c7084 Merge pull request #1819 from Inceptron-Software/add_inceptron_provider
Add Inceptron provider
2026-05-21 11:21:27 -05:00
Aiden Cline 0e3cbe3c64 chore: automate provider sync jobs 2026-05-21 11:21:12 -05:00
Aiden Cline 604d4d66a4 Merge pull request #1820 from Suat-B/codex/xpersona-image-input-20260521
Add image input modality to Xpersona model
2026-05-21 11:00:49 -05:00
SuatB f5090028b8 Add image input modality to Xpersona model 2026-05-21 09:46:28 -05:00
Frank 4bad8faf29 update zen models 2026-05-21 09:05:13 -04:00
Oskar Gustafsson 0df2ccf586 Add Inceptron provider 2026-05-21 09:24:43 +02:00
Aiden Cline bafdc00b45 Merge pull request #1812 from anomalyco/openrouter-extends-sync
Sync OpenRouter models with extends
2026-05-20 21:07:21 -05:00
Aiden Cline 49840c013b Merge pull request #1814 from neonn0d/feat/stepfun-ai
feat(stepfun-ai): add international StepFun platform
2026-05-20 20:58:37 -05:00
Aiden Cline eccae0b54e sync openrouter models with extends 2026-05-20 20:32:11 -05:00
Aiden Cline 4cca29405f Merge pull request #1817 from dpuyosa/dev
Venice: Remove Grok 4.1 Fast and add Grok Build 0.1
2026-05-20 20:26:45 -05:00
Aiden Cline e40d9dd338 Merge pull request #1818 from anomalyco/cloudflare-sync-env
chore(sync): isolate cloudflare credentials
2026-05-20 20:26:22 -05:00
Aiden Cline 6a74991397 chore(sync): isolate cloudflare credentials 2026-05-20 20:19:47 -05:00
dpuyosa 035999cb58 [venice] Replace Grok 4.1 Fast with Grok Build 0.1
- Remove deprecated grok-41-fast model entry
- Add grok-build-0-1 with 200K token tiered pricing
- Update context to 256K and output limit to 65,536
2026-05-21 02:36:19 +02:00
Frank cec56bf1bc update zen models 2026-05-20 19:43:25 -04:00
Aiden Cline 85f0cdcb2f Merge pull request #1816 from anomalyco/xai-sync
Add PDF input modality to Grok models
2026-05-20 18:12:47 -05:00
Aiden Cline ef80d4df4e Infer PDF modality for xAI image models 2026-05-20 18:12:11 -05:00
Aiden Cline af0ef00109 Update xAI Grok PDF modalities 2026-05-20 18:06:33 -05:00
Aiden Cline 5fdcea6b36 Merge pull request #1815 from anomalyco/cloudflare-ai-gateway
chore(sync): add cloudflare workers ai sync
2026-05-20 18:02:28 -05:00
Aiden Cline 31e56480b4 chore(sync): add cloudflare workers ai sync 2026-05-20 16:55:29 -05:00
Aiden Cline 92a621594e Merge pull request #1813 from anomalyco/sync-xai
chore(sync): add xai model sync
2026-05-20 16:02:38 -05:00
Aiden Cline d2db353ceb chore: ignore sync reports 2026-05-20 16:01:51 -05:00
Aiden Cline 900ae509d2 Merge pull request #1808 from ajussak/scaleway
Added Mistral Medium 3.5 128B from Scaleway
2026-05-20 15:58:00 -05:00
neo 9d60164243 feat(stepfun-ai): add international StepFun platform
StepFun runs two separate platforms with distinct accounts/keys:
platform.stepfun.com (China, already covered by providers/stepfun) and
platform.stepfun.ai (international). Keys are not interchangeable
across the two — .ai keys are rejected by api.stepfun.com as
invalid_api_key.

Stepfun's own opencode integration guide instructs users to point at
https://api.stepfun.ai/step_plan/v1. This adds providers/stepfun-ai
for that endpoint, symlinking the shared chat models. Follows the
moonshotai / moonshotai-cn pattern.
2026-05-20 20:43:47 +02:00
Adrien Jussak cd3e99025f Update Mistral Medium 3.5 128B model configuration to extend from mistral-medium-2604 and adjust context window size. 2026-05-20 20:43:45 +02:00
Aiden Cline 1098981eb6 chore(sync): add xai model sync 2026-05-20 13:23:02 -05:00
Aiden Cline 27a151cf53 Merge pull request #1811 from anomalyco/xai-grok-build-model
Add xAI Grok Build model
2026-05-20 13:05:06 -05:00
Aiden Cline 41ff42ab7f Merge pull request #1810 from fhennerkes/dev
poe: add Gemini-3.5-Flash model
2026-05-20 13:04:47 -05:00
Aiden Cline adf1cbdecd add xai grok build model 2026-05-20 13:04:13 -05:00
Frank 10ddc78ce0 update zen models 2026-05-20 14:02:01 -04:00
fhennerkes 7ae897e440 poe: add Gemini-3.5-Flash model
Add new Google model from Poe API (released 2026-05-19).
Uses extends format inheriting from google/gemini-3.5-flash with
Poe-specific overrides (name format, no temperature, markup pricing,
limited input modalities).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:52:15 -07:00
Aiden Cline 02e452c2e8 Merge pull request #1805 from anomalyco/sync-google
sync google models
2026-05-20 10:53:57 -05:00
Aiden Cline f05b63fff5 Merge pull request #1807 from anomalyco/automation/sync-models-aggregators
chore(sync): update aggregator model catalogs
2026-05-20 10:34:30 -05:00
Adrien Jussak e277d60236 Add Mistral Medium 3.5 128B to Scaleway 2026-05-20 16:14:37 +02:00
github-actions[bot] b01277a737 chore(sync): update aggregator model catalogs 2026-05-20 09:25:37 +00:00
Frank a5da5aa429 update zen models 2026-05-20 04:14:28 -04:00
Aiden Cline 11e1d5623a Merge pull request #1806 from Cahl-Dee/grid-model-updates-2026-05
Grid model updates 2026-05
2026-05-19 23:42:26 -05:00
Carl DiClementi e789d7c1f2 Merge branch 'anomalyco:dev' into grid-model-updates-2026-05 2026-05-19 16:06:23 -05:00
Cahl-Dee 899668ad49 added new code and agent models and updated existing text models 2026-05-19 16:04:58 -05:00
arnavchachra 9420048dfe fix crof model limits and reasoning flag to match Crof API 2026-05-18 21:51:19 +05:30
arnavchachra 8db6c27634 add crof provider with 21 models 2026-05-18 21:40:11 +05:30
223 changed files with 3114 additions and 1636 deletions
+1
View File
@@ -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 }}
+36 -11
View File
@@ -2,7 +2,7 @@ name: Sync Model Catalogs
on:
schedule:
- cron: "17 8 * * *"
- cron: "17 * * * *"
workflow_dispatch:
permissions:
@@ -13,16 +13,34 @@ permissions:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
providers:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.providers.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: dev
- name: Setup Bun
uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: List sync providers
id: providers
run: echo "matrix=$(bun models:sync --list-providers)" >> "$GITHUB_OUTPUT"
sync:
needs: providers
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- group: aggregators
title: "chore(sync): update aggregator model catalogs"
branch: automation/sync-models-aggregators
labels: automation,model-sync,sync-group:aggregators,provider:openrouter
matrix: ${{ fromJSON(needs.providers.outputs.matrix) }}
steps:
- name: Checkout code
@@ -39,9 +57,16 @@ jobs:
run: bun install
- name: Sync model catalogs
run: bun models:sync ${{ matrix.group }}
run: bun models:sync ${{ matrix.provider }}
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID }}
CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN }}
- name: Validate models
run: bun validate
@@ -49,9 +74,9 @@ jobs:
- name: Create pull request
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ matrix.branch }}
LABELS: ${{ matrix.labels }}
TITLE: ${{ matrix.title }}
BRANCH: automation/sync-models-${{ matrix.provider }}
LABELS: automation,model-sync,provider:${{ matrix.provider }}
TITLE: "chore(sync): update ${{ matrix.name }} model catalog"
run: |
if [ -z "$(git status --porcelain -- providers)" ]; then
echo "No model catalog changes found."
+1
View File
@@ -3,6 +3,7 @@
.idea
dist
.DS_Store
.sync/
node_modules
data/tokenspeed-monitor.sqlite
data/tokenspeed-monitor.sqlite-shm
+1
View File
@@ -17,6 +17,7 @@
"scripts": {
"validate": "bun ./packages/core/script/validate.ts",
"compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts",
"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",
+2 -418
View File
@@ -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();
-144
View File
@@ -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 },
};
}
+1
View File
@@ -82,6 +82,7 @@ export const ModelFamilyValues = [
// xAI Grok
"grok",
"grok-build",
"grok-vision",
"grok-beta",
+489
View File
@@ -0,0 +1,489 @@
import path from "node:path";
import { mkdir, readdir, rm } from "node:fs/promises";
import { z } from "zod";
import { AuthoredModel, AuthoredModelShape } from "../schema.js";
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
import { fireworksAi } from "./providers/fireworks-ai.js";
import { google } from "./providers/google.js";
import { openrouter } from "./providers/openrouter.js";
import { xai } from "./providers/xai.js";
const ExtendsConfig = z
.object({
from: z.string(),
omit: z.array(z.string()).optional(),
})
.strict();
const ExistingExtendsConfig = z
.object({
from: z.string(),
omit: z.array(z.string()).optional(),
})
.passthrough();
const ExistingModel = AuthoredModelShape.partial()
.extend({
extends: ExistingExtendsConfig.optional(),
})
.strict();
const SyncedExtendsModel = AuthoredModelShape.partial()
.extend({
id: z.string(),
extends: ExtendsConfig,
})
.strict();
const SyncedAuthoredModel = z.union([AuthoredModel, SyncedExtendsModel]);
export type ExistingModel = z.infer<typeof ExistingModel>;
export type SyncedFullModel = Omit<z.infer<typeof AuthoredModelShape>, "id">;
export type SyncedExtendsModel = Omit<z.infer<typeof SyncedExtendsModel>, "id">;
export type SyncedModel = SyncedFullModel | SyncedExtendsModel;
export interface SyncProvider<SourceModel> {
id: string;
name: string;
modelsDir: string;
skipCreates?: boolean;
deleteMissing?: boolean;
sourceID?(model: SourceModel): string;
skippedNotice?(ids: string[]): string[];
fetchModels(): Promise<unknown>;
parseModels(raw: unknown): SourceModel[];
translateModel(
model: SourceModel,
context: { existing(id: string): ExistingModel | undefined },
): { id: string; model: SyncedModel } | undefined;
}
export interface SyncResult {
id: string;
name: string;
status: "changed" | "unchanged";
created: number;
updated: number;
deleted: number;
unchanged: number;
notices: string[];
files: Array<{ status: "created" | "updated" | "deleted"; path: string }>;
}
export const providers: {
"cloudflare-workers-ai": SyncProvider<any>;
"fireworks-ai": SyncProvider<any>;
google: SyncProvider<any>;
openrouter: SyncProvider<any>;
xai: SyncProvider<any>;
} = {
"cloudflare-workers-ai": cloudflareWorkersAi,
"fireworks-ai": fireworksAi,
google,
openrouter,
xai,
};
export const groups = {
aggregators: ["openrouter"],
cloudflare: ["cloudflare-workers-ai"],
direct: ["fireworks-ai", "google", "xai"],
} as const;
type ProviderID = keyof typeof providers;
interface SyncOptions {
dryRun?: boolean;
newOnly?: boolean;
}
export async function syncProviderByID(id: ProviderID, options: SyncOptions = {}) {
return syncProvider(providers[id], options);
}
export async function syncProvider<SourceModel>(
provider: SyncProvider<SourceModel>,
options: SyncOptions = {},
): Promise<SyncResult> {
console.log(`\nSyncing ${provider.name}...`);
const existing = await readExisting(provider.modelsDir);
const sourceModels = provider.parseModels(await provider.fetchModels());
const desired = new Map<string, { model: z.infer<typeof SyncedAuthoredModel>; content: string }>();
const skippedRemote: string[] = [];
for (const sourceModel of sourceModels) {
const translated = provider.translateModel(sourceModel, {
existing(id) {
return existing.get(`${id}.toml`)?.toml;
},
});
if (translated === undefined) {
if (provider.skipCreates) skippedRemote.push(provider.sourceID?.(sourceModel) ?? "unknown");
continue;
}
const relativePath = `${translated.id}.toml`;
if (provider.skipCreates && !existing.has(relativePath)) {
skippedRemote.push(translated.id);
continue;
}
if (desired.has(relativePath)) {
throw new Error(`Duplicate synced model path: ${provider.id}/${relativePath}`);
}
const parsed = SyncedAuthoredModel.safeParse({
id: translated.id,
...translated.model,
});
if (!parsed.success) {
parsed.error.cause = { provider: provider.id, path: relativePath };
throw parsed.error;
}
desired.set(relativePath, {
model: parsed.data,
content: formatToml(parsed.data),
});
}
const files: SyncResult["files"] = [];
let unchanged = 0;
for (const [relativePath, file] of desired) {
const filePath = path.join(provider.modelsDir, relativePath);
const current = existing.get(relativePath);
if (current === undefined) {
files.push({ status: "created", path: filePath });
if (options.dryRun) {
console.log(`Would create ${relativePath}`);
} else {
await mkdir(path.dirname(filePath), { recursive: true });
await Bun.write(filePath, file.content);
}
continue;
}
if (!sameModel(relativePath, current.toml, file.model)) {
if (options.newOnly) {
unchanged++;
continue;
}
files.push({ status: "updated", path: filePath });
if (options.dryRun) {
console.log(`Would update ${relativePath}`);
} else {
if (current.symlink) await rm(filePath, { force: true });
await Bun.write(filePath, file.content);
}
} else {
unchanged++;
}
}
for (const relativePath of existing.keys()) {
if (desired.has(relativePath)) continue;
if (provider.deleteMissing === false) {
unchanged++;
continue;
}
if (options.newOnly) {
console.log(`Skipping removal in new-only mode: ${relativePath}`);
unchanged++;
continue;
}
const filePath = path.join(provider.modelsDir, relativePath);
files.push({ status: "deleted", path: filePath });
if (options.dryRun) {
console.log(`Would remove ${relativePath}`);
} else {
await rm(filePath, { force: true });
}
}
const result = summarize(provider, files, unchanged, provider.skippedNotice?.(skippedRemote) ?? []);
console.log(
`${options.dryRun ? "Dry run: " : ""}${result.created} created, ${result.updated} updated, ${result.deleted} removed, ${result.unchanged} unchanged`,
);
return result;
}
export async function syncTargets(target: string, options: SyncOptions = {}) {
const ids = target in groups
? groups[target as keyof typeof groups]
: target in providers
? [target as ProviderID]
: undefined;
if (ids === undefined) {
throw new Error(`Unknown sync target: ${target}`);
}
const results: SyncResult[] = [];
for (const id of ids) {
results.push(await syncProviderByID(id as ProviderID, options));
}
return results;
}
export function syncProviderMatrix() {
return {
include: Object.values(providers).map((provider) => ({
provider: provider.id,
name: provider.name,
})),
};
}
async function readExisting(modelsDir: string) {
const existing = new Map<string, { text: string; toml: ExistingModel; symlink: boolean }>();
for (const { file, symlink } of await tomlFiles(modelsDir)) {
const text = await Bun.file(path.join(modelsDir, file)).text();
const parsed = ExistingModel.safeParse(Bun.TOML.parse(text));
if (!parsed.success) {
parsed.error.cause = { path: path.join(modelsDir, file) };
throw parsed.error;
}
existing.set(file, { text, toml: parsed.data, symlink });
}
return existing;
}
async function tomlFiles(root: string, dir = "") {
const result: Array<{ file: string; symlink: boolean }> = [];
for (const entry of await readdir(path.join(root, dir), { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...await tomlFiles(root, file));
} else if (entry.name.endsWith(".toml") && (entry.isFile() || entry.isSymbolicLink())) {
result.push({ file, symlink: entry.isSymbolicLink() });
}
}
return result;
}
function summarize(
provider: { id: string; name: string },
files: SyncResult["files"],
unchanged: number,
notices: string[],
): SyncResult {
return {
id: provider.id,
name: provider.name,
status: files.length > 0 ? "changed" : "unchanged",
created: files.filter((file) => file.status === "created").length,
updated: files.filter((file) => file.status === "updated").length,
deleted: files.filter((file) => file.status === "deleted").length,
unchanged,
notices,
files,
};
}
function sameModel(
relativePath: string,
current: ExistingModel,
desired: z.infer<typeof SyncedAuthoredModel>,
) {
const parsed = SyncedAuthoredModel.safeParse({
id: relativePath.slice(0, -5),
...current,
});
return parsed.success && stable(parsed.data) === stable(desired);
}
function stable(value: unknown): string {
if (Array.isArray(value)) {
const items = value.map(stable);
const ordered = value.every((item) => item === null || typeof item !== "object")
? items.sort()
: items;
return `[${ordered.join(",")}]`;
}
if (value !== null && typeof value === "object") {
return `{${Object.entries(value)
.filter(([, item]) => item !== undefined)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
async function writeReport(target: string, results: SyncResult[]) {
await mkdir(".sync", { recursive: true });
const lines = [
`Updates model TOMLs for the \`${target}\` sync target.`,
"",
"| Provider | Status | Created | Updated | Deleted |",
"| --- | --- | ---: | ---: | ---: |",
];
for (const result of results) {
lines.push(
`| ${result.name} | ${result.status} | ${result.created} | ${result.updated} | ${result.deleted} |`,
);
}
for (const result of results.filter((item) => item.files.length > 0)) {
lines.push("", `<details><summary>${result.name} changed files</summary>`, "");
for (const file of result.files) {
lines.push(`- ${file.status}: \`${file.path}\``);
}
lines.push("", "</details>");
}
const noticeResults = results.filter((item) => item.notices.length > 0);
if (noticeResults.length > 0) {
lines.push("", "## Notices");
for (const result of noticeResults) {
lines.push("", `### ${result.name}`);
for (const notice of result.notices) {
lines.push(`- ${notice}`);
}
}
}
lines.push("", "This PR was created automatically by the daily model sync workflow.");
await Bun.write(".sync/model-sync-report.md", `${lines.join("\n")}\n`);
}
function quote(value: string) {
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
}
function formatInteger(n: number) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "_");
}
function formatNumber(n: number) {
return Number.isInteger(n) ? formatInteger(n) : String(n);
}
function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
const lines: string[] = [];
const extendsLines: string[] = [];
if ("extends" in model) {
extendsLines.push("[extends]");
extendsLines.push(`from = ${quote(model.extends.from)}`);
if (model.extends.omit !== undefined) {
extendsLines.push(`omit = [${model.extends.omit.map(quote).join(", ")}]`);
}
}
if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`);
if (model.family !== undefined) lines.push(`family = ${quote(model.family)}`);
if (model.release_date !== undefined) lines.push(`release_date = ${quote(model.release_date)}`);
if (model.last_updated !== undefined) lines.push(`last_updated = ${quote(model.last_updated)}`);
if (model.attachment !== undefined) lines.push(`attachment = ${model.attachment}`);
if (model.reasoning !== undefined) lines.push(`reasoning = ${model.reasoning}`);
if (model.temperature !== undefined) lines.push(`temperature = ${model.temperature}`);
if (model.tool_call !== undefined) lines.push(`tool_call = ${model.tool_call}`);
if (model.structured_output !== undefined) {
lines.push(`structured_output = ${model.structured_output}`);
}
if (model.knowledge !== undefined) lines.push(`knowledge = ${quote(model.knowledge)}`);
if (model.open_weights !== undefined) lines.push(`open_weights = ${model.open_weights}`);
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
if (extendsLines.length > 0) {
if (lines.length > 0) lines.push("");
lines.push(...extendsLines);
}
if (model.interleaved !== undefined) {
lines.push("");
if (model.interleaved === true) {
lines.push("interleaved = true");
} else {
lines.push("[interleaved]");
lines.push(`field = ${quote(model.interleaved.field)}`);
}
}
if (model.cost !== undefined) {
lines.push("", "[cost]");
lines.push(`input = ${formatNumber(model.cost.input)}`);
lines.push(`output = ${formatNumber(model.cost.output)}`);
if (model.cost.reasoning !== undefined) {
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
}
if (model.cost.cache_read !== undefined) {
lines.push(`cache_read = ${formatNumber(model.cost.cache_read)}`);
}
if (model.cost.cache_write !== undefined) {
lines.push(`cache_write = ${formatNumber(model.cost.cache_write)}`);
}
if (model.cost.input_audio !== undefined) {
lines.push(`input_audio = ${formatNumber(model.cost.input_audio)}`);
}
if (model.cost.output_audio !== undefined) {
lines.push(`output_audio = ${formatNumber(model.cost.output_audio)}`);
}
for (const tier of model.cost.tiers ?? []) {
lines.push("", "[[cost.tiers]]");
lines.push(`tier = { size = ${formatInteger(tier.tier.size)} }`);
lines.push(`input = ${formatNumber(tier.input)}`);
lines.push(`output = ${formatNumber(tier.output)}`);
if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`);
if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
}
}
if (model.limit !== undefined) {
lines.push("", "[limit]");
if (model.limit.context !== undefined) lines.push(`context = ${formatInteger(model.limit.context)}`);
if (model.limit.input !== undefined) lines.push(`input = ${formatInteger(model.limit.input)}`);
if (model.limit.output !== undefined) lines.push(`output = ${formatInteger(model.limit.output)}`);
}
if (model.modalities !== undefined) {
lines.push("", "[modalities]");
if (model.modalities.input !== undefined) {
lines.push(`input = [${model.modalities.input.map(quote).join(", ")}]`);
}
if (model.modalities.output !== undefined) {
lines.push(`output = [${model.modalities.output.map(quote).join(", ")}]`);
}
}
return `${lines.join("\n")}\n`;
}
export async function main(args = process.argv.slice(2)) {
if (args.includes("--list-providers")) {
console.log(JSON.stringify(syncProviderMatrix()));
return;
}
const target = args.find((arg) => !arg.startsWith("-")) ?? "aggregators";
const results = await syncTargets(target, {
dryRun: args.includes("--dry-run"),
newOnly: args.includes("--new-only"),
});
await writeReport(target, results);
console.log("\nSync summary");
for (const result of results) {
console.log(
`${result.name}: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted`,
);
}
}
if (import.meta.main) await main();
@@ -0,0 +1,176 @@
import { z } from "zod";
import type { ExistingModel, SyncProvider } from "../index.js";
import {
buildOpenRouterModel,
OpenRouterModel,
OpenRouterResponse,
} from "./openrouter.js";
const API_BASE = "https://api.cloudflare.com/client/v4/accounts";
const CloudflareOpenRouterResponse = z.object({
result: z.union([OpenRouterResponse, z.array(OpenRouterModel)]).optional(),
result_info: z.object({
page: z.number().optional(),
total_pages: z.number().optional(),
}).passthrough().optional(),
}).passthrough();
const CloudflareModel = z.object({
id: z.string(),
name: z.string(),
created: z.number(),
hugging_face_id: z.string().nullable().optional(),
context_length: z.number(),
max_output_length: z.number().nullable().optional(),
input_modalities: z.array(z.string()).optional(),
output_modalities: z.array(z.string()).optional(),
pricing: z.object({
prompt: z.string(),
completion: z.string(),
internal_reasoning: z.string().optional(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
}),
supported_features: z.array(z.string()).optional(),
supported_sampling_parameters: z.array(z.string()).optional(),
}).passthrough();
const CloudflareResponse = z.object({
data: z.array(CloudflareModel),
}).passthrough();
type CloudflareModel = z.infer<typeof CloudflareModel>;
export const cloudflareWorkersAi = {
id: "cloudflare-workers-ai",
name: "Cloudflare Workers AI",
modelsDir: "providers/cloudflare-workers-ai/models",
async fetchModels() {
const accountID = process.env.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID;
const token = process.env.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN;
if (accountID === undefined || token === undefined) {
throw new Error(
"Cloudflare Workers AI sync requires CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID and CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN",
);
}
const first = await fetchPage(accountID, token, 1);
const models = parseCloudflareModels(first);
const pageInfo = CloudflareOpenRouterResponse.safeParse(first).success
? CloudflareOpenRouterResponse.parse(first).result_info
: undefined;
for (let page = 2; page <= (pageInfo?.total_pages ?? 1); page++) {
models.push(...parseCloudflareModels(await fetchPage(accountID, token, page)));
}
return { data: models };
},
parseModels(raw) {
return parseCloudflareModels(raw);
},
translateModel(model, context) {
const normalized = normalizeModel(model);
const id = normalized.id.replace(/^workers-ai\//, "");
return {
id,
model: buildWorkersAiModel(normalized, context.existing(id)),
};
},
} satisfies SyncProvider<CloudflareModel>;
function buildWorkersAiModel(model: z.infer<typeof OpenRouterModel>, existing: ExistingModel | undefined) {
const synced = buildOpenRouterModel(model, existing);
return {
...synced,
name: existing?.name ?? synced.name,
release_date: existing?.release_date ?? synced.release_date,
last_updated: existing?.last_updated ?? synced.last_updated,
limit: {
...synced.limit,
output: existing?.limit?.output ?? synced.limit.output,
},
};
}
async function fetchPage(accountID: string, token: string, page: number) {
const url = new URL(`${API_BASE}/${accountID}/ai/models/search`);
url.searchParams.set("format", "openrouter");
url.searchParams.set("per_page", "1000");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(
`Cloudflare Workers AI models request failed: ${response.status} ${response.statusText}${await responseDetails(response)}`,
);
}
return response.json();
}
function parseCloudflareModels(raw: unknown) {
const cloudflare = CloudflareResponse.safeParse(raw);
if (cloudflare.success) return cloudflare.data.data;
const direct = OpenRouterResponse.safeParse(raw);
if (direct.success) return direct.data.data;
const wrapped = CloudflareOpenRouterResponse.parse(raw);
if (wrapped.result === undefined) {
throw new Error("Cloudflare Workers AI response did not include model data");
}
return Array.isArray(wrapped.result) ? wrapped.result : wrapped.result.data;
}
function normalizeModel(model: CloudflareModel) {
if ("architecture" in model && "top_provider" in model && "supported_parameters" in model) {
return OpenRouterModel.parse(model);
}
return OpenRouterModel.parse({
id: model.id.startsWith("@cf/") ? model.id : `@cf/${model.id.replace(/^@cf\//, "")}`,
name: model.name,
created: model.created,
hugging_face_id: model.hugging_face_id ?? null,
knowledge_cutoff: null,
context_length: model.context_length,
architecture: {
input_modalities: model.input_modalities ?? ["text"],
output_modalities: model.output_modalities ?? ["text"],
},
pricing: model.pricing,
top_provider: {
context_length: model.context_length,
max_completion_tokens: model.max_output_length ?? null,
},
supported_parameters: [
...model.supported_sampling_parameters ?? [],
...model.supported_features ?? [],
],
});
}
async function responseDetails(response: Response) {
const text = await response.text();
if (text.length === 0) return "";
try {
const body = z.object({
errors: z.array(z.object({
code: z.union([z.string(), z.number()]).optional(),
message: z.string().optional(),
}).passthrough()).optional(),
}).passthrough().parse(JSON.parse(text));
const details = body.errors
?.map((error) => [error.code, error.message].filter(Boolean).join(": "))
.filter((message) => message.length > 0)
.join("; ");
return details === undefined || details.length === 0 ? "" : ` (${details})`;
} catch {
return "";
}
}
@@ -0,0 +1,75 @@
import { z } from "zod";
import type { SyncProvider } from "../index.js";
const API_BASE = "https://api.fireworks.ai/v1";
const FireworksModel = z.object({
name: z.string(),
}).passthrough();
const FireworksResponse = z.object({
models: z.array(FireworksModel).optional(),
nextPageToken: z.string().optional(),
}).passthrough();
type FireworksModel = z.infer<typeof FireworksModel>;
export const fireworksAi = {
id: "fireworks-ai",
name: "Fireworks AI",
modelsDir: "providers/fireworks-ai/models",
skipCreates: true,
deleteMissing: false,
sourceID(model) {
return model.name;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} Fireworks AI models returned by the API were not created because the Models API does not provide pricing, output token limits, release dates, or complete capability metadata. Existing models are preserved unchanged.`,
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
async fetchModels() {
const key = process.env.FIREWORKS_API_KEY;
if (key === undefined) {
throw new Error("Fireworks AI sync requires FIREWORKS_API_KEY");
}
const models: FireworksModel[] = [];
let pageToken: string | undefined;
do {
const url = new URL(`${API_BASE}/accounts/fireworks/models`);
url.searchParams.set("pageSize", "200");
url.searchParams.set("filter", "supports_serverless = true");
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
const response = await fetch(url, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) {
throw new Error(`Fireworks AI models request failed: ${response.status} ${response.statusText}`);
}
const page = FireworksResponse.parse(await response.json());
models.push(...page.models ?? []);
pageToken = page.nextPageToken || undefined;
} while (pageToken !== undefined);
return { models };
},
parseModels(raw) {
return FireworksResponse.parse(raw).models ?? [];
},
translateModel(model, context) {
const existing = context.existing(model.name);
if (existing === undefined) return undefined;
return {
id: model.name,
model: existing,
};
},
} satisfies SyncProvider<FireworksModel>;
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { ExistingModel, SyncProvider, SyncedModel } from "../sync-models.js";
import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
const API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
@@ -0,0 +1,334 @@
import { z } from "zod";
import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { ModelFamilyValues } from "../../family.js";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
const API_ENDPOINT = "https://openrouter.ai/api/v1/models";
const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers");
const modelFilesByProvider = new Map<string, Set<string>>();
const canonicalTomlByModel = new Map<string, Record<string, unknown>>();
const CANONICAL_PROVIDER_PREFIXES = {
anthropic: "anthropic",
cohere: "cohere",
deepseek: "deepseek",
google: "google",
meta: "llama",
"meta-llama": "llama",
minimax: "minimax",
mistralai: "mistral",
moonshotai: "moonshotai",
openai: "openai",
"x-ai": "xai",
xai: "xai",
xiaomi: "xiaomi",
zai: "zai",
"z-ai": "zai",
} as const;
export const OpenRouterModel = z.object({
id: z.string(),
name: z.string(),
created: z.number(),
hugging_face_id: z.string().nullable(),
knowledge_cutoff: z.string().nullable(),
context_length: z.number(),
architecture: z.object({
input_modalities: z.array(z.string()),
output_modalities: z.array(z.string()),
}),
pricing: z.object({
prompt: z.string(),
completion: z.string(),
internal_reasoning: z.string().optional(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
}),
top_provider: z.object({
context_length: z.number().nullable(),
max_completion_tokens: z.number().nullable(),
}),
supported_parameters: z.array(z.string()),
});
export const OpenRouterResponse = z.object({
data: z.array(OpenRouterModel),
}).passthrough();
export type OpenRouterModel = z.infer<typeof OpenRouterModel>;
export const openrouter = {
id: "openrouter",
name: "OpenRouter",
modelsDir: "providers/openrouter/models",
async fetchModels() {
const headers = process.env.OPENROUTER_API_KEY
? { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` }
: undefined;
const response = await fetch(API_ENDPOINT, { headers });
if (!response.ok) {
throw new Error(`OpenRouter request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
return OpenRouterResponse.parse(raw).data;
},
translateModel(model, context) {
return {
id: model.id,
model: buildOpenRouterModel(model, context.existing(model.id)),
};
},
} satisfies SyncProvider<OpenRouterModel>;
function dateFromTimestamp(timestamp: number) {
return new Date(timestamp * 1000).toISOString().slice(0, 10);
}
function price(value: string | undefined) {
if (value === undefined) return undefined;
const number = Number(value);
return Number.isFinite(number) && number >= 0
? Math.round(number * 1_000_000_000_000) / 1_000_000
: undefined;
}
type Modality = "text" | "audio" | "image" | "video" | "pdf";
function modalities(values: string[], fallback: Modality[]): Modality[] {
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
const result = values
.map((value) => value.toLowerCase())
.map((value) => value === "file" ? "pdf" : value)
.filter((value): value is Modality => allowed.has(value as Modality));
return [...new Set(result.length > 0 ? result : fallback)];
}
function inferFamily(model: OpenRouterModel, name: string) {
const target = `${model.id} ${name}`.toLowerCase();
return [...ModelFamilyValues]
.sort((a, b) => b.length - a.length)
.find((family) => {
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (family === "o") {
return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
}
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
});
}
export function buildOpenRouterModel(model: OpenRouterModel, existing: ExistingModel | undefined): SyncedModel {
const params = new Set(model.supported_parameters);
const name = model.name.replace(/^[^:]+:\s+/, "");
const input = modalities(model.architecture.input_modalities, ["text"]);
const output = modalities(model.architecture.output_modalities, ["text"]);
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
const reasoning = params.has("reasoning") || params.has("include_reasoning");
const context = model.top_provider.context_length ?? model.context_length;
const family = inferFamily(model, name);
const releaseDate = dateFromTimestamp(model.created);
const familyValue = existing?.family === "o" && family !== "o"
? family
: (existing?.family ?? family);
const attachment = input.some((value) => value !== "text");
const toolCall = params.has("tools") || params.has("tool_choice");
const structuredOutput = params.has("structured_outputs");
const knowledge = model.knowledge_cutoff?.slice(0, 10) ?? existing?.knowledge;
const openWeights = Boolean(model.hugging_face_id);
const cost = prompt !== undefined && completion !== undefined
? {
input: prompt,
output: completion,
reasoning: reasoning ? price(model.pricing.internal_reasoning) : undefined,
cache_read: price(model.pricing.input_cache_read),
cache_write: price(model.pricing.input_cache_write),
tiers: existing?.cost?.tiers,
}
: existing?.cost;
const limit = {
context,
input: existing?.limit?.input,
output: model.top_provider.max_completion_tokens ?? existing?.limit?.output ?? context,
};
const canonical = resolveCanonicalModel(model.id);
if (canonical !== undefined) {
return {
extends: {
from: canonical.from,
omit: canonicalOmit(canonical.provider, canonical.modelID, cost, limit),
},
...canonicalRuntimeOverrides(canonical.provider, canonical.modelID, {
name: model.id.endsWith(":free") ? name : undefined,
attachment,
reasoning,
}),
temperature: params.has("temperature"),
tool_call: toolCall,
structured_output: structuredOutput,
status: existing?.status,
interleaved: existing?.interleaved,
cost,
limit,
modalities: { input, output },
};
}
return {
name,
family: familyValue,
release_date: releaseDate,
last_updated: releaseDate,
attachment,
reasoning,
temperature: params.has("temperature"),
tool_call: toolCall,
structured_output: structuredOutput,
knowledge,
open_weights: openWeights,
status: existing?.status,
interleaved: existing?.interleaved,
cost,
limit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
function resolveCanonicalModel(openrouterID: string) {
const [prefix, ...modelParts] = openrouterID.split("/");
if (prefix === undefined || modelParts.length === 0) return undefined;
if (openrouterID.startsWith("~/") || prefix.startsWith("~")) return undefined;
const provider = CANONICAL_PROVIDER_PREFIXES[prefix as keyof typeof CANONICAL_PROVIDER_PREFIXES];
if (provider === undefined) return undefined;
const modelID = modelParts.join("/").replace(/:free$/, "");
const candidates = canonicalCandidates(provider, modelID);
const match = candidates.find((candidate) => {
return canonicalModelExists(provider, candidate);
});
return match === undefined
? undefined
: {
from: `${provider}/${match}`,
provider,
modelID: match,
};
}
function canonicalModelExists(provider: string, modelID: string) {
let files = modelFilesByProvider.get(provider);
if (files === undefined) {
try {
files = new Set(readdirSync(path.join(PROVIDERS_DIR, provider, "models")));
} catch {
files = new Set();
}
modelFilesByProvider.set(provider, files);
}
return files.has(`${modelID}.toml`);
}
function canonicalOmit(
provider: string,
modelID: string,
cost: SyncedFullModel["cost"],
limit: SyncedFullModel["limit"],
) {
const toml = canonicalToml(provider, modelID);
const omit = ["provider", "experimental"].filter((key) => toml[key] !== undefined);
const baseCost = toml.cost;
if (baseCost !== undefined && baseCost !== null && typeof baseCost === "object" && !Array.isArray(baseCost)) {
if (cost === undefined) {
omit.push("cost");
} else {
for (const key of ["reasoning", "cache_read", "cache_write", "input_audio", "output_audio", "tiers"] as const) {
if ((baseCost as Record<string, unknown>)[key] !== undefined && cost[key] === undefined) {
omit.push(`cost.${key}`);
}
}
if (hasLegacyContextOver200k(baseCost) && cost.tiers === undefined) {
omit.push("cost.context_over_200k");
}
}
}
const baseLimit = toml.limit;
if (
baseLimit !== undefined &&
baseLimit !== null &&
typeof baseLimit === "object" &&
!Array.isArray(baseLimit) &&
(baseLimit as Record<string, unknown>).input !== undefined &&
limit.input === undefined
) {
omit.push("limit.input");
}
return omit.length > 0 ? omit : undefined;
}
function hasLegacyContextOver200k(cost: object) {
const tiers = (cost as { tiers?: unknown }).tiers;
if (!Array.isArray(tiers) || tiers.length !== 1) return false;
const tier = tiers[0];
if (tier === null || typeof tier !== "object" || Array.isArray(tier)) return false;
const tierConfig = (tier as { tier?: unknown }).tier;
if (tierConfig === null || typeof tierConfig !== "object" || Array.isArray(tierConfig)) return false;
const size = (tierConfig as { size?: unknown }).size;
return typeof size === "number" && size >= 200_000;
}
function canonicalRuntimeOverrides(
provider: string,
modelID: string,
values: Pick<SyncedFullModel, "name" | "attachment" | "reasoning">,
) {
const toml = canonicalToml(provider, modelID);
return Object.fromEntries(
Object.entries(values).filter(([key, value]) => value !== undefined && toml[key] !== value),
);
}
function canonicalToml(provider: string, modelID: string) {
const key = `${provider}/${modelID}`;
let toml = canonicalTomlByModel.get(key);
if (toml === undefined) {
const filePath = path.join(PROVIDERS_DIR, provider, "models", `${modelID}.toml`);
toml = Bun.TOML.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
canonicalTomlByModel.set(key, toml);
}
return toml;
}
function canonicalCandidates(provider: string, modelID: string) {
const candidates = [modelID];
if (provider === "anthropic") {
candidates.push(modelID.replace(/(claude-(?:opus|sonnet|haiku)-\d+)\.(\d+)/, "$1-$2"));
candidates.push(modelID.replace(/^claude-3\.5-/, "claude-3-5-"));
}
if (provider === "llama") {
candidates.push(modelID.replace(/^llama-(\d+)-(\d+)/, "llama-$1.$2"));
candidates.push(modelID.replace(/^llama-(4)-(maverick|scout)$/, "llama-$1-$2-17b"));
}
if (provider === "mistral") {
candidates.push(modelID.replace(/-latest$/, ""));
}
if (provider === "minimax") {
candidates.push(modelID.replace(/^minimax-m/, "MiniMax-M"));
}
return [...new Set(candidates)];
}
+211
View File
@@ -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 },
};
}
+23
View File
@@ -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"]
@@ -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"]
@@ -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"]
@@ -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
+22
View File
@@ -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"
+14
View File
@@ -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"
+10
View File
@@ -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"
+14
View File
@@ -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"]
+14
View File
@@ -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"
+14
View File
@@ -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"
+21
View File
@@ -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"]
+10
View File
@@ -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"]
+10
View File
@@ -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"]
+10
View File
@@ -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"
+12
View File
@@ -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"
+26
View File
@@ -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"]
+14
View File
@@ -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"
+5
View File
@@ -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"
+14
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
name = "Inceptron"
npm = "@ai-sdk/openai-compatible"
env = ["INCEPTRON_API_KEY"]
api = "https://api.inceptron.io/v1"
doc = "https://docs.inceptron.io"
@@ -0,0 +1,2 @@
[extends]
from = "google/gemini-3.5-flash"
@@ -0,0 +1,2 @@
[extends]
from = "xai/grok-4.20-0309-non-reasoning"
@@ -0,0 +1,2 @@
[extends]
from = "xai/grok-4.20-0309-reasoning"
@@ -0,0 +1,2 @@
[extends]
from = "alibaba/qwen3.7-max"
@@ -0,0 +1,28 @@
name = "Gemini 3.5 Flash"
family = "gemini-flash"
release_date = "2026-05-19"
last_updated = "2026-05-19"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01"
open_weights = false
[cost]
input = 1.50
output = 9.00
cache_read = 0.15
input_audio = 1.50
[limit]
context = 1_048_576
output = 65_536
[modalities]
input = ["text", "image", "video", "audio", "pdf"]
output = ["text"]
[provider]
npm = "@ai-sdk/google"
@@ -0,0 +1,23 @@
name = "Grok Build 0.1"
family = "grok-build"
release_date = "2026-05-20"
last_updated = "2026-05-20"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[cost]
input = 1.00
output = 2.00
cache_read = 0.20
[limit]
context = 256_000
output = 256_000
[modalities]
input = ["text", "image"]
output = ["text"]
@@ -8,6 +8,7 @@ temperature = true
tool_call = true
knowledge = "2025-01"
open_weights = true
status = "deprecated"
[cost]
input = 0
@@ -8,6 +8,7 @@ temperature = true
tool_call = true
knowledge = "2025-04"
open_weights = false
status = "deprecated"
[cost]
input = 0
@@ -1,14 +1,9 @@
name = "Claude Haiku 4.5"
family = "claude-haiku"
release_date = "2025-10-15"
last_updated = "2025-10-15"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-02-28"
open_weights = false
[extends]
from = "anthropic/claude-haiku-4-5"
[cost]
input = 1
@@ -1,14 +1,9 @@
name = "Claude Opus 4.1"
family = "claude-opus"
release_date = "2025-08-05"
last_updated = "2025-08-05"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "anthropic/claude-opus-4-1"
[cost]
input = 15
@@ -1,14 +1,9 @@
name = "Claude Opus 4.5"
family = "claude-opus"
release_date = "2025-11-24"
last_updated = "2025-11-24"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-05-30"
open_weights = false
[extends]
from = "anthropic/claude-opus-4-5"
[cost]
input = 5
@@ -1,14 +1,10 @@
name = "Claude Opus 4.6"
family = "claude-opus"
release_date = "2026-02-04"
last_updated = "2026-02-04"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-05-31"
open_weights = false
[extends]
from = "anthropic/claude-opus-4-6"
omit = ["experimental"]
[cost]
input = 5
@@ -1,14 +1,10 @@
name = "Claude Opus 4.7"
family = "claude-opus"
release_date = "2026-04-16"
last_updated = "2026-04-16"
attachment = true
reasoning = true
temperature = false
tool_call = true
structured_output = true
knowledge = "2026-01-31"
open_weights = false
[extends]
from = "anthropic/claude-opus-4-7"
omit = ["experimental"]
[cost]
input = 5
@@ -1,14 +1,9 @@
name = "Claude Sonnet 4.5"
family = "claude-sonnet"
release_date = "2025-09-29"
last_updated = "2025-09-29"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "anthropic/claude-sonnet-4-5"
[cost]
input = 3
@@ -1,14 +1,9 @@
name = "Claude Sonnet 4.6"
family = "claude-sonnet"
release_date = "2026-02-17"
last_updated = "2026-02-17"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-08-31"
open_weights = false
[extends]
from = "anthropic/claude-sonnet-4-6"
[cost]
input = 3
@@ -1,14 +1,9 @@
name = "Command R (08-2024)"
family = "command-r"
release_date = "2024-08-30"
last_updated = "2024-08-30"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
knowledge = "2024-03-31"
open_weights = false
[extends]
from = "cohere/command-r-08-2024"
[cost]
input = 0.15
@@ -1,14 +1,9 @@
name = "Command R+ (08-2024)"
family = "command-r"
release_date = "2024-08-30"
last_updated = "2024-08-30"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
knowledge = "2024-03-31"
open_weights = false
[extends]
from = "cohere/command-r-plus-08-2024"
[cost]
input = 2.5
@@ -1,14 +1,9 @@
name = "Command R7B (12-2024)"
family = "command-r"
release_date = "2024-12-14"
last_updated = "2024-12-14"
attachment = false
reasoning = false
temperature = true
tool_call = false
structured_output = true
knowledge = "2024-08-31"
open_weights = false
[extends]
from = "cohere/command-r7b-12-2024"
[cost]
input = 0.0375
@@ -1,14 +1,11 @@
name = "DeepSeek V3"
family = "deepseek"
release_date = "2024-12-26"
last_updated = "2024-12-26"
attachment = false
reasoning = false
temperature = true
tool_call = true
structured_output = true
knowledge = "2024-07-31"
open_weights = true
[extends]
from = "deepseek/deepseek-chat"
omit = ["cost.cache_read"]
[cost]
input = 0.32
@@ -1,13 +1,9 @@
name = "DeepSeek V4 Flash"
family = "deepseek"
release_date = "2026-04-24"
last_updated = "2026-04-24"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[extends]
from = "deepseek/deepseek-v4-flash"
[interleaved]
field = "reasoning_content"
@@ -1,13 +1,11 @@
name = "DeepSeek V4 Flash (free)"
family = "deepseek"
release_date = "2026-04-24"
last_updated = "2026-04-24"
attachment = false
reasoning = true
temperature = false
tool_call = true
structured_output = false
open_weights = true
[extends]
from = "deepseek/deepseek-v4-flash"
omit = ["cost.cache_read"]
[cost]
input = 0
@@ -1,13 +1,9 @@
name = "DeepSeek V4 Pro"
family = "deepseek"
release_date = "2026-04-24"
last_updated = "2026-04-24"
attachment = false
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = true
[extends]
from = "deepseek/deepseek-v4-pro"
[interleaved]
field = "reasoning_content"
@@ -17,7 +17,7 @@ cache_read = 0.025
cache_write = 0.083333
[limit]
context = 1_048_576
context = 1_000_000
output = 8_192
[modalities]
@@ -1,14 +1,10 @@
name = "Nano Banana (Gemini 2.5 Flash Image)"
family = "gemini"
release_date = "2025-10-07"
last_updated = "2025-10-07"
attachment = true
reasoning = false
temperature = true
tool_call = false
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "google/gemini-2.5-flash-image"
[cost]
input = 0.3
@@ -1,14 +1,10 @@
name = "Gemini 2.5 Flash Lite"
family = "gemini-flash-lite"
release_date = "2025-07-22"
last_updated = "2025-07-22"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "google/gemini-2.5-flash-lite"
omit = ["cost.input_audio"]
[cost]
input = 0.1
@@ -1,14 +1,10 @@
name = "Gemini 2.5 Flash"
family = "gemini-flash"
release_date = "2025-06-17"
last_updated = "2025-06-17"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "google/gemini-2.5-flash"
omit = ["cost.input_audio"]
[cost]
input = 0.3
@@ -1,14 +1,10 @@
name = "Gemini 2.5 Pro"
family = "gemini"
release_date = "2025-06-17"
last_updated = "2025-06-17"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01-31"
open_weights = false
[extends]
from = "google/gemini-2.5-pro"
omit = ["cost.tiers", "cost.context_over_200k"]
[cost]
input = 1.25
@@ -1,14 +1,10 @@
name = "Gemini 3 Flash Preview"
family = "gemini-flash"
release_date = "2025-12-17"
last_updated = "2025-12-17"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01"
open_weights = false
[extends]
from = "google/gemini-3-flash-preview"
omit = ["cost.input_audio"]
[interleaved]
field = "reasoning_details"
@@ -1,14 +1,9 @@
name = "Nano Banana 2 (Gemini 3.1 Flash Image Preview)"
family = "gemini-flash"
release_date = "2026-02-26"
last_updated = "2026-02-26"
attachment = true
reasoning = true
temperature = true
tool_call = false
structured_output = true
knowledge = "2025-01"
open_weights = false
[extends]
from = "google/gemini-3.1-flash-image-preview"
[cost]
input = 0.5
@@ -1,13 +1,10 @@
name = "Gemini 3.1 Flash Lite Preview"
family = "gemini-flash-lite"
release_date = "2026-03-03"
last_updated = "2026-03-03"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[extends]
from = "google/gemini-3.1-flash-lite-preview"
omit = ["cost.input_audio"]
[cost]
input = 0.25
@@ -1,13 +1,10 @@
name = "Gemini 3.1 Flash Lite"
family = "gemini"
release_date = "2026-05-07"
last_updated = "2026-05-07"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
open_weights = false
[extends]
from = "google/gemini-3.1-flash-lite"
omit = ["cost.input_audio"]
[cost]
input = 0.25
@@ -1,14 +1,9 @@
name = "Gemini 3.1 Pro Preview Custom Tools"
family = "gemini-pro"
release_date = "2026-02-25"
last_updated = "2026-02-25"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01"
open_weights = false
[extends]
from = "google/gemini-3.1-pro-preview-customtools"
[interleaved]
field = "reasoning_details"

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