Compare commits

..

57 Commits

Author SHA1 Message Date
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
222 changed files with 3018 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 }}
+35 -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,15 @@ jobs:
run: bun install
- name: Sync model catalogs
run: bun models:sync ${{ matrix.group }}
run: bun models:sync ${{ matrix.provider }}
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID }}
CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN }}
- name: Validate models
run: bun validate
@@ -49,9 +73,9 @@ jobs:
- name: Create pull request
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ matrix.branch }}
LABELS: ${{ matrix.labels }}
TITLE: ${{ matrix.title }}
BRANCH: automation/sync-models-${{ matrix.provider }}
LABELS: automation,model-sync,provider:${{ matrix.provider }}
TITLE: "chore(sync): update ${{ matrix.name }} model catalog"
run: |
if [ -z "$(git status --porcelain -- providers)" ]; then
echo "No model catalog changes found."
+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",
+481
View File
@@ -0,0 +1,481 @@
import path from "node:path";
import { mkdir, readdir, rm } from "node:fs/promises";
import { z } from "zod";
import { AuthoredModel, AuthoredModelShape } from "../schema.js";
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
import { google } from "./providers/google.js";
import { openrouter } from "./providers/openrouter.js";
import { xai } from "./providers/xai.js";
const ExtendsConfig = z
.object({
from: z.string(),
omit: z.array(z.string()).optional(),
})
.strict();
const ExistingExtendsConfig = z
.object({
from: z.string(),
omit: z.array(z.string()).optional(),
})
.passthrough();
const ExistingModel = AuthoredModelShape.partial()
.extend({
extends: ExistingExtendsConfig.optional(),
})
.strict();
const SyncedExtendsModel = AuthoredModelShape.partial()
.extend({
id: z.string(),
extends: ExtendsConfig,
})
.strict();
const SyncedAuthoredModel = z.union([AuthoredModel, SyncedExtendsModel]);
export type ExistingModel = z.infer<typeof ExistingModel>;
export type SyncedFullModel = Omit<z.infer<typeof AuthoredModelShape>, "id">;
export type SyncedExtendsModel = Omit<z.infer<typeof SyncedExtendsModel>, "id">;
export type SyncedModel = SyncedFullModel | SyncedExtendsModel;
export interface SyncProvider<SourceModel> {
id: string;
name: string;
modelsDir: string;
skipCreates?: boolean;
sourceID?(model: SourceModel): string;
skippedNotice?(ids: string[]): string[];
fetchModels(): Promise<unknown>;
parseModels(raw: unknown): SourceModel[];
translateModel(
model: SourceModel,
context: { existing(id: string): ExistingModel | undefined },
): { id: string; model: SyncedModel } | undefined;
}
export interface SyncResult {
id: string;
name: string;
status: "changed" | "unchanged";
created: number;
updated: number;
deleted: number;
unchanged: number;
notices: string[];
files: Array<{ status: "created" | "updated" | "deleted"; path: string }>;
}
export const providers: {
"cloudflare-workers-ai": SyncProvider<any>;
google: SyncProvider<any>;
openrouter: SyncProvider<any>;
xai: SyncProvider<any>;
} = {
"cloudflare-workers-ai": cloudflareWorkersAi,
google,
openrouter,
xai,
};
export const groups = {
aggregators: ["openrouter"],
cloudflare: ["cloudflare-workers-ai"],
direct: ["google", "xai"],
} as const;
type ProviderID = keyof typeof providers;
interface SyncOptions {
dryRun?: boolean;
newOnly?: boolean;
}
export async function syncProviderByID(id: ProviderID, options: SyncOptions = {}) {
return syncProvider(providers[id], options);
}
export async function syncProvider<SourceModel>(
provider: SyncProvider<SourceModel>,
options: SyncOptions = {},
): Promise<SyncResult> {
console.log(`\nSyncing ${provider.name}...`);
const existing = await readExisting(provider.modelsDir);
const sourceModels = provider.parseModels(await provider.fetchModels());
const desired = new Map<string, { model: z.infer<typeof SyncedAuthoredModel>; content: string }>();
const skippedRemote: string[] = [];
for (const sourceModel of sourceModels) {
const translated = provider.translateModel(sourceModel, {
existing(id) {
return existing.get(`${id}.toml`)?.toml;
},
});
if (translated === undefined) {
if (provider.skipCreates) skippedRemote.push(provider.sourceID?.(sourceModel) ?? "unknown");
continue;
}
const relativePath = `${translated.id}.toml`;
if (provider.skipCreates && !existing.has(relativePath)) {
skippedRemote.push(translated.id);
continue;
}
if (desired.has(relativePath)) {
throw new Error(`Duplicate synced model path: ${provider.id}/${relativePath}`);
}
const parsed = SyncedAuthoredModel.safeParse({
id: translated.id,
...translated.model,
});
if (!parsed.success) {
parsed.error.cause = { provider: provider.id, path: relativePath };
throw parsed.error;
}
desired.set(relativePath, {
model: parsed.data,
content: formatToml(parsed.data),
});
}
const files: SyncResult["files"] = [];
let unchanged = 0;
for (const [relativePath, file] of desired) {
const filePath = path.join(provider.modelsDir, relativePath);
const current = existing.get(relativePath);
if (current === undefined) {
files.push({ status: "created", path: filePath });
if (options.dryRun) {
console.log(`Would create ${relativePath}`);
} else {
await mkdir(path.dirname(filePath), { recursive: true });
await Bun.write(filePath, file.content);
}
continue;
}
if (!sameModel(relativePath, current.toml, file.model)) {
if (options.newOnly) {
unchanged++;
continue;
}
files.push({ status: "updated", path: filePath });
if (options.dryRun) {
console.log(`Would update ${relativePath}`);
} else {
if (current.symlink) await rm(filePath, { force: true });
await Bun.write(filePath, file.content);
}
} else {
unchanged++;
}
}
for (const relativePath of existing.keys()) {
if (desired.has(relativePath)) continue;
if (options.newOnly) {
console.log(`Skipping removal in new-only mode: ${relativePath}`);
unchanged++;
continue;
}
const filePath = path.join(provider.modelsDir, relativePath);
files.push({ status: "deleted", path: filePath });
if (options.dryRun) {
console.log(`Would remove ${relativePath}`);
} else {
await rm(filePath, { force: true });
}
}
const result = summarize(provider, files, unchanged, provider.skippedNotice?.(skippedRemote) ?? []);
console.log(
`${options.dryRun ? "Dry run: " : ""}${result.created} created, ${result.updated} updated, ${result.deleted} removed, ${result.unchanged} unchanged`,
);
return result;
}
export async function syncTargets(target: string, options: SyncOptions = {}) {
const ids = target in groups
? groups[target as keyof typeof groups]
: target in providers
? [target as ProviderID]
: undefined;
if (ids === undefined) {
throw new Error(`Unknown sync target: ${target}`);
}
const results: SyncResult[] = [];
for (const id of ids) {
results.push(await syncProviderByID(id as ProviderID, options));
}
return results;
}
export function syncProviderMatrix() {
return {
include: Object.values(providers).map((provider) => ({
provider: provider.id,
name: provider.name,
})),
};
}
async function readExisting(modelsDir: string) {
const existing = new Map<string, { text: string; toml: ExistingModel; symlink: boolean }>();
for (const { file, symlink } of await tomlFiles(modelsDir)) {
const text = await Bun.file(path.join(modelsDir, file)).text();
const parsed = ExistingModel.safeParse(Bun.TOML.parse(text));
if (!parsed.success) {
parsed.error.cause = { path: path.join(modelsDir, file) };
throw parsed.error;
}
existing.set(file, { text, toml: parsed.data, symlink });
}
return existing;
}
async function tomlFiles(root: string, dir = "") {
const result: Array<{ file: string; symlink: boolean }> = [];
for (const entry of await readdir(path.join(root, dir), { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...await tomlFiles(root, file));
} else if (entry.name.endsWith(".toml") && (entry.isFile() || entry.isSymbolicLink())) {
result.push({ file, symlink: entry.isSymbolicLink() });
}
}
return result;
}
function summarize(
provider: { id: string; name: string },
files: SyncResult["files"],
unchanged: number,
notices: string[],
): SyncResult {
return {
id: provider.id,
name: provider.name,
status: files.length > 0 ? "changed" : "unchanged",
created: files.filter((file) => file.status === "created").length,
updated: files.filter((file) => file.status === "updated").length,
deleted: files.filter((file) => file.status === "deleted").length,
unchanged,
notices,
files,
};
}
function sameModel(
relativePath: string,
current: ExistingModel,
desired: z.infer<typeof SyncedAuthoredModel>,
) {
const parsed = SyncedAuthoredModel.safeParse({
id: relativePath.slice(0, -5),
...current,
});
return parsed.success && stable(parsed.data) === stable(desired);
}
function stable(value: unknown): string {
if (Array.isArray(value)) {
const items = value.map(stable);
const ordered = value.every((item) => item === null || typeof item !== "object")
? items.sort()
: items;
return `[${ordered.join(",")}]`;
}
if (value !== null && typeof value === "object") {
return `{${Object.entries(value)
.filter(([, item]) => item !== undefined)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
async function writeReport(target: string, results: SyncResult[]) {
await mkdir(".sync", { recursive: true });
const lines = [
`Updates model TOMLs for the \`${target}\` sync target.`,
"",
"| Provider | Status | Created | Updated | Deleted |",
"| --- | --- | ---: | ---: | ---: |",
];
for (const result of results) {
lines.push(
`| ${result.name} | ${result.status} | ${result.created} | ${result.updated} | ${result.deleted} |`,
);
}
for (const result of results.filter((item) => item.files.length > 0)) {
lines.push("", `<details><summary>${result.name} changed files</summary>`, "");
for (const file of result.files) {
lines.push(`- ${file.status}: \`${file.path}\``);
}
lines.push("", "</details>");
}
const noticeResults = results.filter((item) => item.notices.length > 0);
if (noticeResults.length > 0) {
lines.push("", "## Notices");
for (const result of noticeResults) {
lines.push("", `### ${result.name}`);
for (const notice of result.notices) {
lines.push(`- ${notice}`);
}
}
}
lines.push("", "This PR was created automatically by the daily model sync workflow.");
await Bun.write(".sync/model-sync-report.md", `${lines.join("\n")}\n`);
}
function quote(value: string) {
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
}
function formatInteger(n: number) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "_");
}
function formatNumber(n: number) {
return Number.isInteger(n) ? formatInteger(n) : String(n);
}
function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
const lines: string[] = [];
const extendsLines: string[] = [];
if ("extends" in model) {
extendsLines.push("[extends]");
extendsLines.push(`from = ${quote(model.extends.from)}`);
if (model.extends.omit !== undefined) {
extendsLines.push(`omit = [${model.extends.omit.map(quote).join(", ")}]`);
}
}
if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`);
if (model.family !== undefined) lines.push(`family = ${quote(model.family)}`);
if (model.release_date !== undefined) lines.push(`release_date = ${quote(model.release_date)}`);
if (model.last_updated !== undefined) lines.push(`last_updated = ${quote(model.last_updated)}`);
if (model.attachment !== undefined) lines.push(`attachment = ${model.attachment}`);
if (model.reasoning !== undefined) lines.push(`reasoning = ${model.reasoning}`);
if (model.temperature !== undefined) lines.push(`temperature = ${model.temperature}`);
if (model.tool_call !== undefined) lines.push(`tool_call = ${model.tool_call}`);
if (model.structured_output !== undefined) {
lines.push(`structured_output = ${model.structured_output}`);
}
if (model.knowledge !== undefined) lines.push(`knowledge = ${quote(model.knowledge)}`);
if (model.open_weights !== undefined) lines.push(`open_weights = ${model.open_weights}`);
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
if (extendsLines.length > 0) {
if (lines.length > 0) lines.push("");
lines.push(...extendsLines);
}
if (model.interleaved !== undefined) {
lines.push("");
if (model.interleaved === true) {
lines.push("interleaved = true");
} else {
lines.push("[interleaved]");
lines.push(`field = ${quote(model.interleaved.field)}`);
}
}
if (model.cost !== undefined) {
lines.push("", "[cost]");
lines.push(`input = ${formatNumber(model.cost.input)}`);
lines.push(`output = ${formatNumber(model.cost.output)}`);
if (model.cost.reasoning !== undefined) {
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
}
if (model.cost.cache_read !== undefined) {
lines.push(`cache_read = ${formatNumber(model.cost.cache_read)}`);
}
if (model.cost.cache_write !== undefined) {
lines.push(`cache_write = ${formatNumber(model.cost.cache_write)}`);
}
if (model.cost.input_audio !== undefined) {
lines.push(`input_audio = ${formatNumber(model.cost.input_audio)}`);
}
if (model.cost.output_audio !== undefined) {
lines.push(`output_audio = ${formatNumber(model.cost.output_audio)}`);
}
for (const tier of model.cost.tiers ?? []) {
lines.push("", "[[cost.tiers]]");
lines.push(`tier = { size = ${formatInteger(tier.tier.size)} }`);
lines.push(`input = ${formatNumber(tier.input)}`);
lines.push(`output = ${formatNumber(tier.output)}`);
if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`);
if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
}
}
if (model.limit !== undefined) {
lines.push("", "[limit]");
if (model.limit.context !== undefined) lines.push(`context = ${formatInteger(model.limit.context)}`);
if (model.limit.input !== undefined) lines.push(`input = ${formatInteger(model.limit.input)}`);
if (model.limit.output !== undefined) lines.push(`output = ${formatInteger(model.limit.output)}`);
}
if (model.modalities !== undefined) {
lines.push("", "[modalities]");
if (model.modalities.input !== undefined) {
lines.push(`input = [${model.modalities.input.map(quote).join(", ")}]`);
}
if (model.modalities.output !== undefined) {
lines.push(`output = [${model.modalities.output.map(quote).join(", ")}]`);
}
}
return `${lines.join("\n")}\n`;
}
export async function main(args = process.argv.slice(2)) {
if (args.includes("--list-providers")) {
console.log(JSON.stringify(syncProviderMatrix()));
return;
}
const target = args.find((arg) => !arg.startsWith("-")) ?? "aggregators";
const results = await syncTargets(target, {
dryRun: args.includes("--dry-run"),
newOnly: args.includes("--new-only"),
});
await writeReport(target, results);
console.log("\nSync summary");
for (const result of results) {
console.log(
`${result.name}: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted`,
);
}
}
if (import.meta.main) await main();
@@ -0,0 +1,176 @@
import { z } from "zod";
import type { ExistingModel, SyncProvider } from "../index.js";
import {
buildOpenRouterModel,
OpenRouterModel,
OpenRouterResponse,
} from "./openrouter.js";
const API_BASE = "https://api.cloudflare.com/client/v4/accounts";
const CloudflareOpenRouterResponse = z.object({
result: z.union([OpenRouterResponse, z.array(OpenRouterModel)]).optional(),
result_info: z.object({
page: z.number().optional(),
total_pages: z.number().optional(),
}).passthrough().optional(),
}).passthrough();
const CloudflareModel = z.object({
id: z.string(),
name: z.string(),
created: z.number(),
hugging_face_id: z.string().nullable().optional(),
context_length: z.number(),
max_output_length: z.number().nullable().optional(),
input_modalities: z.array(z.string()).optional(),
output_modalities: z.array(z.string()).optional(),
pricing: z.object({
prompt: z.string(),
completion: z.string(),
internal_reasoning: z.string().optional(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
}),
supported_features: z.array(z.string()).optional(),
supported_sampling_parameters: z.array(z.string()).optional(),
}).passthrough();
const CloudflareResponse = z.object({
data: z.array(CloudflareModel),
}).passthrough();
type CloudflareModel = z.infer<typeof CloudflareModel>;
export const cloudflareWorkersAi = {
id: "cloudflare-workers-ai",
name: "Cloudflare Workers AI",
modelsDir: "providers/cloudflare-workers-ai/models",
async fetchModels() {
const accountID = process.env.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID;
const token = process.env.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN;
if (accountID === undefined || token === undefined) {
throw new Error(
"Cloudflare Workers AI sync requires CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID and CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN",
);
}
const first = await fetchPage(accountID, token, 1);
const models = parseCloudflareModels(first);
const pageInfo = CloudflareOpenRouterResponse.safeParse(first).success
? CloudflareOpenRouterResponse.parse(first).result_info
: undefined;
for (let page = 2; page <= (pageInfo?.total_pages ?? 1); page++) {
models.push(...parseCloudflareModels(await fetchPage(accountID, token, page)));
}
return { data: models };
},
parseModels(raw) {
return parseCloudflareModels(raw);
},
translateModel(model, context) {
const normalized = normalizeModel(model);
const id = normalized.id.replace(/^workers-ai\//, "");
return {
id,
model: buildWorkersAiModel(normalized, context.existing(id)),
};
},
} satisfies SyncProvider<CloudflareModel>;
function buildWorkersAiModel(model: z.infer<typeof OpenRouterModel>, existing: ExistingModel | undefined) {
const synced = buildOpenRouterModel(model, existing);
return {
...synced,
name: existing?.name ?? synced.name,
release_date: existing?.release_date ?? synced.release_date,
last_updated: existing?.last_updated ?? synced.last_updated,
limit: {
...synced.limit,
output: existing?.limit?.output ?? synced.limit.output,
},
};
}
async function fetchPage(accountID: string, token: string, page: number) {
const url = new URL(`${API_BASE}/${accountID}/ai/models/search`);
url.searchParams.set("format", "openrouter");
url.searchParams.set("per_page", "1000");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(
`Cloudflare Workers AI models request failed: ${response.status} ${response.statusText}${await responseDetails(response)}`,
);
}
return response.json();
}
function parseCloudflareModels(raw: unknown) {
const cloudflare = CloudflareResponse.safeParse(raw);
if (cloudflare.success) return cloudflare.data.data;
const direct = OpenRouterResponse.safeParse(raw);
if (direct.success) return direct.data.data;
const wrapped = CloudflareOpenRouterResponse.parse(raw);
if (wrapped.result === undefined) {
throw new Error("Cloudflare Workers AI response did not include model data");
}
return Array.isArray(wrapped.result) ? wrapped.result : wrapped.result.data;
}
function normalizeModel(model: CloudflareModel) {
if ("architecture" in model && "top_provider" in model && "supported_parameters" in model) {
return OpenRouterModel.parse(model);
}
return OpenRouterModel.parse({
id: model.id.startsWith("@cf/") ? model.id : `@cf/${model.id.replace(/^@cf\//, "")}`,
name: model.name,
created: model.created,
hugging_face_id: model.hugging_face_id ?? null,
knowledge_cutoff: null,
context_length: model.context_length,
architecture: {
input_modalities: model.input_modalities ?? ["text"],
output_modalities: model.output_modalities ?? ["text"],
},
pricing: model.pricing,
top_provider: {
context_length: model.context_length,
max_completion_tokens: model.max_output_length ?? null,
},
supported_parameters: [
...model.supported_sampling_parameters ?? [],
...model.supported_features ?? [],
],
});
}
async function responseDetails(response: Response) {
const text = await response.text();
if (text.length === 0) return "";
try {
const body = z.object({
errors: z.array(z.object({
code: z.union([z.string(), z.number()]).optional(),
message: z.string().optional(),
}).passthrough()).optional(),
}).passthrough().parse(JSON.parse(text));
const details = body.errors
?.map((error) => [error.code, error.message].filter(Boolean).join(": "))
.filter((message) => message.length > 0)
.join("; ");
return details === undefined || details.length === 0 ? "" : ` (${details})`;
} catch {
return "";
}
}
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { ExistingModel, SyncProvider, SyncedModel } from "../sync-models.js";
import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
const API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
@@ -0,0 +1,334 @@
import { z } from "zod";
import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { ModelFamilyValues } from "../../family.js";
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
const API_ENDPOINT = "https://openrouter.ai/api/v1/models";
const PROVIDERS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "providers");
const modelFilesByProvider = new Map<string, Set<string>>();
const canonicalTomlByModel = new Map<string, Record<string, unknown>>();
const CANONICAL_PROVIDER_PREFIXES = {
anthropic: "anthropic",
cohere: "cohere",
deepseek: "deepseek",
google: "google",
meta: "llama",
"meta-llama": "llama",
minimax: "minimax",
mistralai: "mistral",
moonshotai: "moonshotai",
openai: "openai",
"x-ai": "xai",
xai: "xai",
xiaomi: "xiaomi",
zai: "zai",
"z-ai": "zai",
} as const;
export const OpenRouterModel = z.object({
id: z.string(),
name: z.string(),
created: z.number(),
hugging_face_id: z.string().nullable(),
knowledge_cutoff: z.string().nullable(),
context_length: z.number(),
architecture: z.object({
input_modalities: z.array(z.string()),
output_modalities: z.array(z.string()),
}),
pricing: z.object({
prompt: z.string(),
completion: z.string(),
internal_reasoning: z.string().optional(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
}),
top_provider: z.object({
context_length: z.number().nullable(),
max_completion_tokens: z.number().nullable(),
}),
supported_parameters: z.array(z.string()),
});
export const OpenRouterResponse = z.object({
data: z.array(OpenRouterModel),
}).passthrough();
export type OpenRouterModel = z.infer<typeof OpenRouterModel>;
export const openrouter = {
id: "openrouter",
name: "OpenRouter",
modelsDir: "providers/openrouter/models",
async fetchModels() {
const headers = process.env.OPENROUTER_API_KEY
? { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` }
: undefined;
const response = await fetch(API_ENDPOINT, { headers });
if (!response.ok) {
throw new Error(`OpenRouter request failed: ${response.status} ${response.statusText}`);
}
return response.json();
},
parseModels(raw) {
return OpenRouterResponse.parse(raw).data;
},
translateModel(model, context) {
return {
id: model.id,
model: buildOpenRouterModel(model, context.existing(model.id)),
};
},
} satisfies SyncProvider<OpenRouterModel>;
function dateFromTimestamp(timestamp: number) {
return new Date(timestamp * 1000).toISOString().slice(0, 10);
}
function price(value: string | undefined) {
if (value === undefined) return undefined;
const number = Number(value);
return Number.isFinite(number) && number >= 0
? Math.round(number * 1_000_000_000_000) / 1_000_000
: undefined;
}
type Modality = "text" | "audio" | "image" | "video" | "pdf";
function modalities(values: string[], fallback: Modality[]): Modality[] {
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
const result = values
.map((value) => value.toLowerCase())
.map((value) => value === "file" ? "pdf" : value)
.filter((value): value is Modality => allowed.has(value as Modality));
return [...new Set(result.length > 0 ? result : fallback)];
}
function inferFamily(model: OpenRouterModel, name: string) {
const target = `${model.id} ${name}`.toLowerCase();
return [...ModelFamilyValues]
.sort((a, b) => b.length - a.length)
.find((family) => {
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (family === "o") {
return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
}
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
});
}
export function buildOpenRouterModel(model: OpenRouterModel, existing: ExistingModel | undefined): SyncedModel {
const params = new Set(model.supported_parameters);
const name = model.name.replace(/^[^:]+:\s+/, "");
const input = modalities(model.architecture.input_modalities, ["text"]);
const output = modalities(model.architecture.output_modalities, ["text"]);
const prompt = price(model.pricing.prompt);
const completion = price(model.pricing.completion);
const reasoning = params.has("reasoning") || params.has("include_reasoning");
const context = model.top_provider.context_length ?? model.context_length;
const family = inferFamily(model, name);
const releaseDate = dateFromTimestamp(model.created);
const familyValue = existing?.family === "o" && family !== "o"
? family
: (existing?.family ?? family);
const attachment = input.some((value) => value !== "text");
const toolCall = params.has("tools") || params.has("tool_choice");
const structuredOutput = params.has("structured_outputs");
const knowledge = model.knowledge_cutoff?.slice(0, 10) ?? existing?.knowledge;
const openWeights = Boolean(model.hugging_face_id);
const cost = prompt !== undefined && completion !== undefined
? {
input: prompt,
output: completion,
reasoning: reasoning ? price(model.pricing.internal_reasoning) : undefined,
cache_read: price(model.pricing.input_cache_read),
cache_write: price(model.pricing.input_cache_write),
tiers: existing?.cost?.tiers,
}
: existing?.cost;
const limit = {
context,
input: existing?.limit?.input,
output: model.top_provider.max_completion_tokens ?? existing?.limit?.output ?? context,
};
const canonical = resolveCanonicalModel(model.id);
if (canonical !== undefined) {
return {
extends: {
from: canonical.from,
omit: canonicalOmit(canonical.provider, canonical.modelID, cost, limit),
},
...canonicalRuntimeOverrides(canonical.provider, canonical.modelID, {
name: model.id.endsWith(":free") ? name : undefined,
attachment,
reasoning,
}),
temperature: params.has("temperature"),
tool_call: toolCall,
structured_output: structuredOutput,
status: existing?.status,
interleaved: existing?.interleaved,
cost,
limit,
modalities: { input, output },
};
}
return {
name,
family: familyValue,
release_date: releaseDate,
last_updated: releaseDate,
attachment,
reasoning,
temperature: params.has("temperature"),
tool_call: toolCall,
structured_output: structuredOutput,
knowledge,
open_weights: openWeights,
status: existing?.status,
interleaved: existing?.interleaved,
cost,
limit,
modalities: { input, output },
} satisfies SyncedFullModel;
}
function resolveCanonicalModel(openrouterID: string) {
const [prefix, ...modelParts] = openrouterID.split("/");
if (prefix === undefined || modelParts.length === 0) return undefined;
if (openrouterID.startsWith("~/") || prefix.startsWith("~")) return undefined;
const provider = CANONICAL_PROVIDER_PREFIXES[prefix as keyof typeof CANONICAL_PROVIDER_PREFIXES];
if (provider === undefined) return undefined;
const modelID = modelParts.join("/").replace(/:free$/, "");
const candidates = canonicalCandidates(provider, modelID);
const match = candidates.find((candidate) => {
return canonicalModelExists(provider, candidate);
});
return match === undefined
? undefined
: {
from: `${provider}/${match}`,
provider,
modelID: match,
};
}
function canonicalModelExists(provider: string, modelID: string) {
let files = modelFilesByProvider.get(provider);
if (files === undefined) {
try {
files = new Set(readdirSync(path.join(PROVIDERS_DIR, provider, "models")));
} catch {
files = new Set();
}
modelFilesByProvider.set(provider, files);
}
return files.has(`${modelID}.toml`);
}
function canonicalOmit(
provider: string,
modelID: string,
cost: SyncedFullModel["cost"],
limit: SyncedFullModel["limit"],
) {
const toml = canonicalToml(provider, modelID);
const omit = ["provider", "experimental"].filter((key) => toml[key] !== undefined);
const baseCost = toml.cost;
if (baseCost !== undefined && baseCost !== null && typeof baseCost === "object" && !Array.isArray(baseCost)) {
if (cost === undefined) {
omit.push("cost");
} else {
for (const key of ["reasoning", "cache_read", "cache_write", "input_audio", "output_audio", "tiers"] as const) {
if ((baseCost as Record<string, unknown>)[key] !== undefined && cost[key] === undefined) {
omit.push(`cost.${key}`);
}
}
if (hasLegacyContextOver200k(baseCost) && cost.tiers === undefined) {
omit.push("cost.context_over_200k");
}
}
}
const baseLimit = toml.limit;
if (
baseLimit !== undefined &&
baseLimit !== null &&
typeof baseLimit === "object" &&
!Array.isArray(baseLimit) &&
(baseLimit as Record<string, unknown>).input !== undefined &&
limit.input === undefined
) {
omit.push("limit.input");
}
return omit.length > 0 ? omit : undefined;
}
function hasLegacyContextOver200k(cost: object) {
const tiers = (cost as { tiers?: unknown }).tiers;
if (!Array.isArray(tiers) || tiers.length !== 1) return false;
const tier = tiers[0];
if (tier === null || typeof tier !== "object" || Array.isArray(tier)) return false;
const tierConfig = (tier as { tier?: unknown }).tier;
if (tierConfig === null || typeof tierConfig !== "object" || Array.isArray(tierConfig)) return false;
const size = (tierConfig as { size?: unknown }).size;
return typeof size === "number" && size >= 200_000;
}
function canonicalRuntimeOverrides(
provider: string,
modelID: string,
values: Pick<SyncedFullModel, "name" | "attachment" | "reasoning">,
) {
const toml = canonicalToml(provider, modelID);
return Object.fromEntries(
Object.entries(values).filter(([key, value]) => value !== undefined && toml[key] !== value),
);
}
function canonicalToml(provider: string, modelID: string) {
const key = `${provider}/${modelID}`;
let toml = canonicalTomlByModel.get(key);
if (toml === undefined) {
const filePath = path.join(PROVIDERS_DIR, provider, "models", `${modelID}.toml`);
toml = Bun.TOML.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
canonicalTomlByModel.set(key, toml);
}
return toml;
}
function canonicalCandidates(provider: string, modelID: string) {
const candidates = [modelID];
if (provider === "anthropic") {
candidates.push(modelID.replace(/(claude-(?:opus|sonnet|haiku)-\d+)\.(\d+)/, "$1-$2"));
candidates.push(modelID.replace(/^claude-3\.5-/, "claude-3-5-"));
}
if (provider === "llama") {
candidates.push(modelID.replace(/^llama-(\d+)-(\d+)/, "llama-$1.$2"));
candidates.push(modelID.replace(/^llama-(4)-(maverick|scout)$/, "llama-$1-$2-17b"));
}
if (provider === "mistral") {
candidates.push(modelID.replace(/-latest$/, ""));
}
if (provider === "minimax") {
candidates.push(modelID.replace(/^minimax-m/, "MiniMax-M"));
}
return [...new Set(candidates)];
}
+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"
@@ -1,14 +1,9 @@
name = "Gemini 3.1 Pro Preview"
family = "gemini-pro"
release_date = "2026-02-19"
last_updated = "2026-02-19"
attachment = true
reasoning = true
temperature = true
tool_call = true
structured_output = true
knowledge = "2025-01"
open_weights = false
[extends]
from = "google/gemini-3.1-pro-preview"
[interleaved]
field = "reasoning_details"

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