/**
* Regenerate models.json: the account's entitled models, with metadata from models.dev.
*
* Run: npx tsx regen.ts
*
* Capabilities are frozen into the file on purpose. Looking them up at startup made
* image support flip between runs, because ids missing from the `alibaba-token-plan`
* entry were filled from whichever other provider models.dev happened to list first.
*/
import { writeFileSync } from "node:fs";
import { accountModelIds, type BakedCatalog, type BakedModel, baseUrlFromEnv, CATALOG, resolveKey } from "./index.js";
const CATALOG_URL = "https://models.dev/api.json";
const CATALOG_PROVIDER = "alibaba-token-plan";
interface DevModel {
name?: string;
reasoning?: boolean;
tool_call?: boolean;
attachment?: boolean;
modalities?: { input?: string[]; output?: string[] };
limit?: { context?: number; output?: number };
}
const isChatModel = (m: DevModel): boolean => m.tool_call === true && (m.modalities?.output ?? []).includes("text");
const toBaked = (id: string, m: DevModel): BakedModel => ({
id,
name: m.name ?? id,
reasoning: m.reasoning === true,
input: (m.modalities?.input ?? []).includes("image") ? ["text", "image"] : ["text"],
contextWindow: m.limit?.context ?? 128000,
maxTokens: m.limit?.output ?? 8192,
});
async function main(): Promise<void> {
const baseUrl = baseUrlFromEnv();
const key = resolveKey();
if (!key) throw new Error("no API key: run /login alibaba or set ALIBABA_API_KEY");
const [entitled, payload] = await Promise.all([
accountModelIds(baseUrl, key),
fetch(CATALOG_URL).then((r) => r.json() as Promise<Record<string, { models?: Record<string, DevModel> }>>),
]);
// other alibaba catalogs describe the same models, an unrelated one just shares the id:
// `auto` is a route here and model-oracle-ai's model there
const own = payload[CATALOG_PROVIDER]?.models ?? {};
const elsewhere = new Map<string, DevModel[]>();
for (const [provider, entry] of Object.entries(payload)) {
if (provider === CATALOG_PROVIDER || !provider.startsWith("alibaba")) continue;
for (const [id, model] of Object.entries(entry.models ?? {})) {
elsewhere.set(id, [...(elsewhere.get(id) ?? []), model]);
}
}
const best = (id: string): DevModel | undefined =>
own[id] ??
(elsewhere.get(id) ?? [])
.filter(isChatModel)
.sort((a, b) => (b.limit?.context ?? 0) - (a.limit?.context ?? 0))[0];
const models: BakedModel[] = [];
const skipped: string[] = [];
for (const id of entitled) {
const model = best(id);
if (!model || !isChatModel(model)) {
skipped.push(id);
continue;
}
models.push(toBaked(id, model));
}
models.sort((a, b) => a.id.localeCompare(b.id));
skipped.sort();
const catalog: BakedCatalog = { generatedAt: new Date().toISOString(), baseUrl, models, skipped };
writeFileSync(CATALOG, `${JSON.stringify(catalog, null, "\t")}\n`);
for (const m of models) {
console.log(`${m.id}\t${m.contextWindow}\t${m.maxTokens}\t${m.reasoning ? "thinking" : "-"}\t${m.input.join("+")}`);
}
if (skipped.length > 0) console.log(`skipped (not a chat model or unknown to models.dev): ${skipped.join(", ")}`);
}
await main();