feat(ai): publish generated model catalogs to R2 (#6720)

This commit is contained in:
Armin Ronacher
2026-07-16 11:43:47 +02:00
committed by GitHub
parent 5220aba619
commit 2be9efa19c
6 changed files with 548 additions and 67 deletions
+137 -67
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { readdirSync, rmSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import {
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
@@ -22,6 +22,40 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageRoot = join(__dirname, "..");
function readGeneratorOptions(args: string[]): {
strict: boolean;
jsonOnly: boolean;
jsonOutputDir: string | undefined;
} {
let strict = false;
let jsonOnly = false;
let jsonOutputDir: string | undefined;
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--strict") {
strict = true;
continue;
}
if (arg === "--json-only") {
jsonOnly = true;
continue;
}
if (arg === "--json-output") {
const value = args[++index];
if (!value) throw new Error("--json-output requires a directory");
jsonOutputDir = resolve(value);
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (jsonOnly && !jsonOutputDir) throw new Error("--json-only requires --json-output");
return { strict, jsonOnly, jsonOutputDir };
}
const generatorOptions = readGeneratorOptions(process.argv.slice(2));
interface ModelsDevModel {
id: string;
name: string;
@@ -688,6 +722,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
try {
console.log("Fetching models from NVIDIA NIM API...");
const response = await fetch(`${NVIDIA_BASE_URL}/models`);
if (!response.ok) throw new Error(`NVIDIA NIM API returned ${response.status}`);
const data = (await response.json()) as { data?: NvidiaNimModelListItem[] };
const modelIds = new Map<string, string>();
@@ -700,6 +735,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
return modelIds;
} catch (error) {
console.error("Failed to fetch NVIDIA NIM models:", error);
if (generatorOptions.strict) throw error;
return new Map();
}
}
@@ -708,6 +744,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
try {
console.log("Fetching models from OpenRouter API...");
const response = await fetch("https://openrouter.ai/api/v1/models");
if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -760,6 +797,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to fetch OpenRouter models:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -768,6 +806,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
try {
console.log("Fetching models from Vercel AI Gateway API...");
const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`);
if (!response.ok) throw new Error(`Vercel AI Gateway API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -818,6 +857,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to fetch Vercel AI Gateway models:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -826,6 +866,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
try {
console.log("Fetching models from models.dev API...");
const response = await fetch("https://models.dev/api.json");
if (!response.ok) throw new Error(`models.dev API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -1703,6 +1744,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to load models.dev data:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -2236,85 +2278,110 @@ async function generateModels() {
}
}
// Generate TypeScript files: one catalog per provider plus an aggregator
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
const sortedProviderIds = Object.keys(providers).sort();
if (!generatorOptions.jsonOnly) {
// Generate TypeScript files: one catalog per provider plus an aggregator
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
`;
const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
const catalogConstName = (providerId: string) =>
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
function emitModel(model: Model<any>, indent: string): string {
let output = `${indent}"${model.id}": {\n`;
output += `${indent}\tid: "${model.id}",\n`;
output += `${indent}\tname: "${model.name}",\n`;
output += `${indent}\tapi: "${model.api}",\n`;
output += `${indent}\tprovider: "${model.provider}",\n`;
if (model.baseUrl !== undefined) {
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
function emitModel(model: Model<any>, indent: string): string {
let output = `${indent}"${model.id}": {\n`;
output += `${indent}\tid: "${model.id}",\n`;
output += `${indent}\tname: "${model.name}",\n`;
output += `${indent}\tapi: "${model.api}",\n`;
output += `${indent}\tprovider: "${model.provider}",\n`;
if (model.baseUrl !== undefined) {
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
}
if (model.headers) {
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
}
if (model.compat) {
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
}
output += `${indent}\treasoning: ${model.reasoning},\n`;
if (model.thinkingLevelMap) {
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
}
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
output += `${indent}\tcost: {\n`;
output += `${indent}\t\tinput: ${model.cost.input},\n`;
output += `${indent}\t\toutput: ${model.cost.output},\n`;
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
if (model.cost.tiers) {
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
}
output += `${indent}\t},\n`;
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
output += `${indent}} satisfies Model<"${model.api}">,\n`;
return output;
}
if (model.headers) {
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
}
if (model.compat) {
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
}
output += `${indent}\treasoning: ${model.reasoning},\n`;
if (model.thinkingLevelMap) {
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
}
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
output += `${indent}\tcost: {\n`;
output += `${indent}\t\tinput: ${model.cost.input},\n`;
output += `${indent}\t\toutput: ${model.cost.output},\n`;
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
if (model.cost.tiers) {
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
}
output += `${indent}\t},\n`;
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
output += `${indent}} satisfies Model<"${model.api}">,\n`;
return output;
}
const sortedProviderIds = Object.keys(providers).sort();
const providersDir = join(packageRoot, "src/providers");
const providersDir = join(packageRoot, "src/providers");
// Remove stale per-provider catalogs
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts")) {
rmSync(join(providersDir, entry));
// Remove stale per-provider catalogs
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts")) {
rmSync(join(providersDir, entry));
}
}
}
// Per-provider catalogs (sorted for deterministic output)
for (const providerId of sortedProviderIds) {
const models = providers[providerId];
// Per-provider catalogs (sorted for deterministic output)
for (const providerId of sortedProviderIds) {
const models = providers[providerId];
let output = generatedHeader;
output += `import type { Model } from "../types.ts";\n\n`;
output += `export const ${catalogConstName(providerId)} = {\n`;
const sortedModelIds = Object.keys(models).sort();
for (const modelId of sortedModelIds) {
output += emitModel(models[modelId], "\t");
}
output += `} as const;\n`;
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
}
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
// Aggregator
let output = generatedHeader;
output += `import type { Model } from "../types.ts";\n\n`;
output += `export const ${catalogConstName(providerId)} = {\n`;
const sortedModelIds = Object.keys(models).sort();
for (const modelId of sortedModelIds) {
output += emitModel(models[modelId], "\t");
for (const providerId of sortedProviderIds) {
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
}
output += `\nexport const MODELS = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
}
output += `} as const;\n`;
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
console.log("Generated src/models.generated.ts");
}
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
// Aggregator
let output = generatedHeader;
for (const providerId of sortedProviderIds) {
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
if (generatorOptions.jsonOutputDir) {
const jsonProviders: Record<string, Record<string, Model<any>>> = {};
for (const providerId of sortedProviderIds) {
jsonProviders[providerId] = {};
for (const modelId of Object.keys(providers[providerId]).sort()) {
jsonProviders[providerId][modelId] = providers[providerId][modelId];
}
}
const providerOutputDir = join(generatorOptions.jsonOutputDir, "providers");
rmSync(generatorOptions.jsonOutputDir, { recursive: true, force: true });
mkdirSync(providerOutputDir, { recursive: true });
const writeJson = (path: string, value: unknown) => writeFileSync(path, `${JSON.stringify(value)}\n`);
writeJson(join(generatorOptions.jsonOutputDir, "models.json"), jsonProviders);
writeJson(join(generatorOptions.jsonOutputDir, "providers.json"), sortedProviderIds);
for (const providerId of sortedProviderIds) {
writeJson(join(providerOutputDir, `${providerId}.json`), jsonProviders[providerId]);
}
console.log(`Generated JSON model catalog under ${generatorOptions.jsonOutputDir}`);
}
output += `\nexport const MODELS = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
}
output += `} as const;\n`;
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
console.log("Generated src/models.generated.ts");
// Print statistics
const totalModels = allModels.length;
@@ -2330,4 +2397,7 @@ async function generateModels() {
}
// Run the generator
generateModels().catch(console.error);
generateModels().catch((error) => {
console.error(error);
process.exitCode = 1;
});