bake the model list into models.json with a regen script
25 unmodified lines26272829303132333435363031323334353637383940414243444546474849505152535436 unmodified lines25 unmodified lines`ALIBABA_BASE_URL` overrides the endpoint (other region or gateway).
## Model list
At startup the extension asks the endpoint for `/models` (entitlement) and models.devfor metadata (context window, output limit, reasoning, image support). Only models theaccount can actually call are registered. Metadata borrowed from another models.devprovider never claims image input. `models.json` is the offline snapshot used whenmodels.dev is unreachable; refresh it from `models.dev/api.json` (`alibaba-token-plan`).The entitlement list is cached in `~/.prime/agent/alibaba-entitlement.json`, so a slow orfailed `/models` call reuses the last known list instead of offering unusable models.`models.json` is the model list, committed and read at startup. Nothing about a model'scapabilities is looked up at runtime.
Regenerate it after a plan or catalog change:
```npx tsx regen.ts```
That asks the endpoint for `/models` (what the key is entitled to) and models.dev formetadata (context window, output limit, reasoning, image input), then writes the file.Non-chat entitlements (audio, image generation) are skipped. Ids the `alibaba-token-plan`entry has not caught up with are filled from another models.dev provider that lists thesame id; freezing that choice in a committed file is the point, because resolving it livemade `deepseek-v4.1-flash` gain and lose image input between runs depending on whichprovider models.dev happened to list first.
At startup the extension still asks `/models` for entitlement only, and hides models thekey cannot call. The list is cached in `~/.prime/agent/alibaba-entitlement.json`, so aslow or failed call reuses the last known list.
Thinking is mapped per family: `qwen` -> top-level `enable_thinking`, `deepseek` ->`thinking` + `reasoning_effort`, `glm` -> Z.ai format.
## Prompt caching36 unmodified lines2 unmodified lines345678910111213141 unmodified line1617181916171819202122232425262427262829303129303132323334353635373839404142434439454647484910 unmodified lines60616263586465606667626869707172737475767778798071728182838485867879808182838485868788899091929394959687888990919293949596979899100101102103104991001011021031051061071081091101061111121131141151161121171181151161171181191201211221231191201211221231241251261271311281291331341351361371381391401301311321421431441451461471481491501511331341351361371381541551391401411591421431611621631441451461471481495 unmodified lines2 unmodified lines * * `/login alibaba` stores a pasted API key in ~/.prime/agent/auth.json. * Fallbacks, in order: stored key, ALIBABA_API_KEY, ~/.alibaba.key. * ALIBABA_BASE_URL overrides the endpoint (e.g. another region). * * The model list is baked into models.json and regenerated by regen.ts, which * probes the live endpoint. Nothing here is fetched at startup except the * account's entitled ids, so capabilities never drift between runs. */
import type { Api } from "@earendil-works/pi-ai";import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";1 unmodified lineimport { homedir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";
const PROVIDER = "alibaba";const DEFAULT_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";const CATALOG_URL = "https://models.dev/api.json";const CATALOG_PROVIDER = "alibaba-token-plan";export const PROVIDER = "alibaba";export const DEFAULT_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";export const CATALOG = join(fileURLToPath(new URL(".", import.meta.url)), "models.json");const FETCH_TIMEOUT_MS = 10000;const KEY_FILE = join(homedir(), ".alibaba.key");const AUTH_FILE = join(homedir(), ".prime", "agent", "auth.json");const ENTITLEMENT_CACHE = join(homedir(), ".prime", "agent", "alibaba-entitlement.json");const SNAPSHOT = join(fileURLToPath(new URL(".", import.meta.url)), "models.json");
interface CatalogModel {export interface BakedModel { id: string; name: string; reasoning: boolean; tool_call: boolean; attachment: boolean; modalities: { input: string[]; output: string[] }; limit: { context: number; output: number }; input: ("text" | "image")[]; contextWindow: number; maxTokens: number;}
type Catalog = Record<string, CatalogModel>;export interface BakedCatalog { generatedAt: string; baseUrl: string; models: BakedModel[];}
type Family = "qwen" | "deepseek" | "zai" | "other";
const familyOf = (id: string): Family =>export const familyOf = (id: string): Family => id.startsWith("qwen") ? "qwen" : id.startsWith("deepseek") ? "deepseek" : id.startsWith("glm") ? "zai" : "other";
/** * Per-family request quirks, mirroring what Prime's own catalog applies to the same10 unmodified lines
/** DeepSeek V4 exposes only high/max, same as Prime's built-in DeepSeek models. */const DEEPSEEK_THINKING_LEVELS = { minimal: null, low: null, medium: null, high: "high", xhigh: "max", max: null };
const isChatModel = (m: CatalogModel): boolean => m.tool_call && m.modalities.output.includes("text");export const baseUrlFromEnv = (): string => (process.env.ALIBABA_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
const readKeyFile = (): string | undefined => {const readFile = (path: string): string | undefined => { try { return readFileSync(KEY_FILE, "utf-8").trim() || undefined; return readFileSync(path, "utf-8"); } catch { return undefined; }};
export const readKeyFile = (): string | undefined => readFile(KEY_FILE)?.trim() || undefined;
/** Key saved by `/login alibaba`, used only to list the account's entitled models. */const readStoredKey = (): string | undefined => { const raw = readFile(AUTH_FILE); if (!raw) return undefined; try { const entry = (JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as Record<string, { key?: string }>)[PROVIDER]; return entry?.key; return (JSON.parse(raw) as Record<string, { key?: string }>)[PROVIDER]?.key; } catch { return undefined; }};
/** Token-plan models from models.dev, plus every other model it knows, as a fallback pool. */async function loadCatalog(): Promise<{ own: Catalog; pool: Catalog }> { try { const response = await fetch(CATALOG_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); if (!response.ok) throw new Error(`models.dev ${response.status}`); const payload = (await response.json()) as Record<string, { models?: Catalog }>; const own = payload[CATALOG_PROVIDER]?.models; if (!own || Object.keys(own).length === 0) throw new Error(`models.dev has no ${CATALOG_PROVIDER} models`); const pool: Catalog = {}; for (const provider of Object.values(payload)) { for (const [id, model] of Object.entries(provider.models ?? {})) { if (!(id in pool)) pool[id] = model; } } return { own, pool: { ...pool, ...own } }; } catch { const own = (JSON.parse(readFileSync(SNAPSHOT, "utf-8")) as { models: Catalog }).models; return { own, pool: own }; }export const resolveKey = (): string | undefined => readStoredKey() ?? process.env.ALIBABA_API_KEY ?? readKeyFile();
export const loadCatalog = (): BakedCatalog => JSON.parse(readFileSync(CATALOG, "utf-8")) as BakedCatalog;
/** Model ids the account can actually call, straight from the endpoint. */export async function accountModelIds(baseUrl: string, apiKey: string): Promise<string[]> { const response = await fetch(`${baseUrl}/models`, { headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (!response.ok) throw new Error(`${baseUrl}/models ${response.status}`); const payload = (await response.json()) as { data?: Array<{ id: string }> }; const ids = (payload.data ?? []).map((m) => m.id); if (ids.length === 0) throw new Error("no entitled models"); return ids.sort();}
/** * Model ids the account is entitled to. A slow or failed lookup falls back to the last * known list, so a transient hiccup never silently offers models the key cannot call. */async function accountModelIds(baseUrl: string, apiKey: string | undefined): Promise<Set<string> | undefined> {/** Entitled ids, cached on disk: a slow or failed lookup reuses the last known list. */async function entitledIds(baseUrl: string, apiKey: string | undefined): Promise<Set<string> | undefined> { const cached = (): Set<string> | undefined => { const raw = readFile(ENTITLEMENT_CACHE); if (!raw) return undefined; try { const ids = JSON.parse(readFileSync(ENTITLEMENT_CACHE, "utf-8")) as string[]; const ids = JSON.parse(raw) as string[]; return ids.length > 0 ? new Set(ids) : undefined; } catch { return undefined; } };
if (!apiKey) return cached(); try { const response = await fetch(`${baseUrl}/models`, { headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (!response.ok) return cached(); const payload = (await response.json()) as { data?: Array<{ id: string }> }; const ids = (payload.data ?? []).map((m) => m.id); if (ids.length === 0) return cached(); writeFileSync(ENTITLEMENT_CACHE, `${JSON.stringify(ids.sort(), null, "\t")}\n`); const ids = await accountModelIds(baseUrl, apiKey); writeFileSync(ENTITLEMENT_CACHE, `${JSON.stringify(ids, null, "\t")}\n`); return new Set(ids); } catch { return cached(); }}
export default async function (pi: ExtensionAPI) { const baseUrl = (process.env.ALIBABA_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, ""); const baseUrl = baseUrlFromEnv(); const keyFileValue = readKeyFile(); const [{ own, pool }, entitled] = await Promise.all([ loadCatalog(), accountModelIds(baseUrl, readStoredKey() ?? process.env.ALIBABA_API_KEY ?? keyFileValue), ]); // The live endpoint is authoritative about entitlement; models.dev only supplies metadata. const catalog = entitled ? Object.fromEntries([...entitled].flatMap((id) => (pool[id] ? [[id, pool[id]] as const] : []))) : own; const catalog = loadCatalog(); const entitled = await entitledIds(baseUrl, resolveKey());
const models = Object.entries(catalog) .filter(([, m]) => isChatModel(m)) .map(([id, m]) => ({ id, name: m.name, reasoning: m.reasoning, input: (m.attachment && m.modalities.input.includes("image") ? ["text", "image"] : ["text"]) as ("text" | "image")[], const models = catalog.models .filter((m) => !entitled || entitled.has(m.id)) .map((m) => ({ ...m, // The token plan bills a flat subscription, so per-token cost is zero. cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: m.limit.context, maxTokens: m.limit.output, compat: { supportsDeveloperRole: false, maxTokensField: "max_tokens" as const, ...FAMILY_COMPAT[familyOf(id)], ...FAMILY_COMPAT[familyOf(m.id)], }, ...(m.reasoning && familyOf(id) === "deepseek" ? { thinkingLevelMap: DEEPSEEK_THINKING_LEVELS } : {}), })) .sort((a, b) => a.id.localeCompare(b.id)); ...(m.reasoning && familyOf(m.id) === "deepseek" ? { thinkingLevelMap: DEEPSEEK_THINKING_LEVELS } : {}), }));
pi.registerProvider(PROVIDER, { name: "Alibaba Model Studio (token plan)", baseUrl,5 unmodified lines12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313423456136137138139140141142143144145146147148149150151152715481561571581591601611621631641651669101116816917017112131417317417517617717817918018118215161841851861871881891901911921931941951961971981992001720218204205206207208209210211212213214192021216217218219222324221222223224225226227228229230231232233234235236237238239240241242243244245246247248249252627282512522532542552562572582592602612930313226326426526633343526826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829936300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357373593603613623633643653663673683693703713723733743837639378379380381382383384385404142387388389390434445392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426464274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385394754154254354454554654754854955055155255355455555655755848560495625635645655665675685695051525357157257357454555657657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961057611612613614615616617618619620621622623624625626627628629630631632586346356366376386396406416426436446456466476486495965160653654655656657658659660616263662663664665646566667668669670671672673674675676676867867968068168268368468568668768868969069169269369469569669698707007017027037047057067077172737470971071171275767771471571671771871972072172272378797257267277287297307317327337347357367377387397407417428074481746747748749750751752753754755756757758759760761828384857637647658687887677687697707717727737747757768990778779780781782783784785786787788789790791792793794795796917989280080180280380480580680780880981081181281381481581693949596818819820821822823824825826827828829830831832833834835836837838839840979884184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387499876100101{ "api": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", "id": "alibaba-token-plan", "models": { "MiniMax-M2.5": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Prior MiniMax coding model for agent workflows, office edits, and automation", "family": "minimax", "id": "MiniMax-M2.5", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-02-12", "limit": { "context": 196608, "input": 196601, "output": 32768 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "MiniMax-M2.5", "open_weights": true, "reasoning": true, "reasoning_options": [], "release_date": "2026-02-12", "temperature": true, "tool_call": true }, "deepseek-v3.2": { "attachment": false, "cost": { "input": 0, "output": 0 }, "description": "DeepSeek chat model for instruction following, coding, and analysis", "family": "deepseek", "id": "deepseek-v3.2", "knowledge": "2025-01", "last_updated": "2025-12-05", "limit": { "context": 131072, "output": 65536 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "DeepSeek V3.2", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" } ], "release_date": "2025-12-03", "structured_output": true, "temperature": true, "tool_call": true }, "deepseek-v4-flash": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Fast DeepSeek V4 lane for economical reasoning, coding, and long-context work", "family": "deepseek-flash", "id": "deepseek-v4-flash", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-05", "last_updated": "2026-04-24", "limit": { "context": 1000000, "output": 384000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "DeepSeek V4 Flash", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "high", "max" ] } ], "release_date": "2026-04-24", "structured_output": true, "temperature": true, "tool_call": true }, "deepseek-v4-flash-0731": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Official DeepSeek V4 Flash release with enhanced agentic capabilities and integrated DSpark speculative decoding", "family": "deepseek-flash", "generatedAt": "2026-09-15T00:18:25.774Z", "baseUrl": "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", "models": [ { "id": "deepseek-v4-flash-0731", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-05", "last_updated": "2026-07-31", "limit": { "context": 1000000, "output": 384000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "DeepSeek V4 Flash 0731", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "high", "max" ] } "input": [ "text" ], "release_date": "2026-07-31", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 384000 }, "deepseek-v4-pro": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Open MoE flagship with million-token context for coding and long agent runs", "family": "deepseek-thinking", { "id": "deepseek-v4-pro", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-05", "last_updated": "2026-04-24", "limit": { "context": 1000000, "output": 384000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "DeepSeek V4 Pro", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "high", "max" ] } "input": [ "text" ], "release_date": "2026-04-24", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 384000 }, "deepseek-v4-pro-0813": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "DeepSeek V4 Pro snapshot with million-token context and support for thinking and non-thinking modes", "family": "deepseek-thinking", "id": "deepseek-v4-pro-0813", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-08-22", "limit": { "context": 1000000, "output": 384000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "DeepSeek V4 Pro 0813", "open_weights": true, { "id": "deepseek-v4.1-flash", "name": "DeepSeek V4.1 Flash", "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "high", "max" ] } "input": [ "text", "image" ], "release_date": "2026-08-12", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1050000, "maxTokens": 384000 }, "glm-5": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "General GLM flagship for coding, analysis, and tool-heavy engineering workflows", "family": "glm", "id": "glm-5", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-02-12", "limit": { "context": 202752, "output": 16384 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM-5", "open_weights": false, "reasoning": true, "reasoning_options": [ { { "type": "toggle" } ], "release_date": "2026-02-12", "structured_output": true, "temperature": true, "tool_call": true }, "glm-5.1": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Strong GLM coding model for agentic engineering, terminals, and repository generation", "family": "glm", "id": "glm-5.1", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-04-07", "limit": { "context": 202752, "output": 128000 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM-5.1", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" } ], "release_date": "2026-04-07", "structured_output": true, "temperature": true, "tool_call": true }, "glm-5.2": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Open flagship GLM for long-horizon coding agents and million-token context work", "family": "glm", "id": "glm-5.2", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-06-13", "limit": { "context": 1000000, "output": 131072 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "GLM-5.2", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "effort", "values": [ "high", "max" ] } "input": [ "text" ], "release_date": "2026-06-13", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 131072 }, "kimi-k2.5": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Earlier Kimi frontier model for long-context agents, coding, and multimodal work", "family": "kimi-k2", "id": "kimi-k2.5", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-01", "last_updated": "2026-01", "limit": { "context": 262144, "output": 98304 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Kimi K2.5", "open_weights": true, "reasoning": true, "reasoning_options": [ { { "type": "toggle" }, { "type": "budget_tokens" } ], "release_date": "2026-01", "structured_output": true, "temperature": true, "tool_call": true }, "kimi-k2.6": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Multimodal Kimi workhorse for agent loops, coding tasks, and visual context", "family": "kimi-k2", "id": "kimi-k2.6", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-01", "last_updated": "2026-04-21", "limit": { "context": 262144, "output": 262144 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Kimi K2.6", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "budget_tokens" } ], "release_date": "2026-04-21", "structured_output": true, "temperature": true, "tool_call": true }, "kimi-k2.7-code": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Coding-focused Kimi model, stronger on long-horizon repo work with less overthinking", "family": "kimi-k2", "id": "kimi-k2.7-code", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-01", "last_updated": "2026-06-12", "limit": { "context": 262144, "output": 262144 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Kimi K2.7 Code", "open_weights": true, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "budget_tokens" } ], "release_date": "2026-06-12", "structured_output": true, "temperature": false, "tool_call": true }, "qwen3.6-flash": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Qwen vision-language model for visual reasoning, documents, and agent tasks", "family": "qwen3.6", "id": "qwen3.6-flash", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-04-27", "limit": { "context": 1000000, "output": 65536 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Qwen3.6 Flash", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "max": 131072, "type": "budget_tokens" } "input": [ "text", "image" ], "release_date": "2026-04-27", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 65536 }, "qwen3.6-plus": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Earlier Qwen multimodal workhorse for million-token agent and document tasks", "family": "qwen", "id": "qwen3.6-plus", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-04", "last_updated": "2026-04-02", "limit": { "context": 1000000, "output": 65536 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Qwen3.6 Plus", "open_weights": false, "reasoning": true, "reasoning_options": [ { { "type": "toggle" }, { "max": 131072, "type": "budget_tokens" } ], "release_date": "2026-04-02", "structured_output": true, "temperature": true, "tool_call": true }, "qwen3.7-max": { "attachment": false, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Qwen frontier model tuned for agent frameworks, coding assistants, and long tasks", "family": "qwen", "id": "qwen3.7-max", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-05-21", "limit": { "context": 1000000, "output": 131072 }, "modalities": { "input": [ "text" ], "output": [ "text" ] }, "name": "Qwen3.7 Max", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "max": 262144, "type": "budget_tokens" } "input": [ "text" ], "release_date": "2026-05-21", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 131072 }, "qwen3.7-plus": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Multimodal Qwen workhorse for long-context agents, visual inputs, and coding", "family": "qwen", { "id": "qwen3.7-plus", "interleaved": { "field": "reasoning_content" }, "knowledge": "2025-04", "last_updated": "2026-06-02", "limit": { "context": 1000000, "output": 65536 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Qwen3.7 Plus", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "max": 262144, "type": "budget_tokens" } "input": [ "text", "image" ], "release_date": "2026-06-02", "structured_output": true, "temperature": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 65536 }, "qwen3.8-flash": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Qwen vision-language model for visual reasoning, documents, and agent tasks", "family": "qwen", { "id": "qwen3.8-flash", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-08-26", "limit": { "context": 1000000, "output": 131072 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Qwen3.8 Flash", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "low", "medium", "xhigh" ] }, { "max": 262144, "type": "budget_tokens" } "input": [ "text", "image" ], "release_date": "2026-08-26", "structured_output": true, "tool_call": true "contextWindow": 1000000, "maxTokens": 131072 }, "qwen3.8-max": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "2.4-trillion-parameter MoE flagship for coding, professional work, multimodal understanding, and long-horizon agentic workflows", "family": "qwen", { "id": "qwen3.8-max", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-08-03", "limit": { "context": 1000000, "output": 131072 }, "modalities": { "input": [ "text", "image", "video", "pdf" ], "output": [ "text" ] }, "name": "Qwen3.8 Max", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "toggle" }, { "type": "effort", "values": [ "low", "medium", "xhigh" ] }, { "max": 262144, "min": 0, "type": "budget_tokens" } "input": [ "text", "image" ], "release_date": "2026-08-03", "structured_output": true, "temperature": true, "tool_call": true }, "qwen3.8-max-preview": { "attachment": true, "cost": { "cache_read": 0, "cache_write": 0, "input": 0, "output": 0 }, "description": "Preview Qwen flagship for million-token multimodal reasoning and long-horizon agentic workflows", "family": "qwen", "id": "qwen3.8-max-preview", "interleaved": { "field": "reasoning_content" }, "last_updated": "2026-07-19", "limit": { "context": 1000000, "output": 131072 "contextWindow": 1000000, "maxTokens": 131072 }, "modalities": { "input": [ "text", "image", "video" ], "output": [ "text" ] }, "name": "Qwen3.8 Max Preview", "open_weights": false, "reasoning": true, "reasoning_options": [ { "type": "effort", "values": [ "low", "medium", "xhigh" ] }, { "max": 262144, "min": 0, "type": "budget_tokens" } ], "release_date": "2026-07-19", "status": "deprecated", "structured_output": true, "temperature": true, "tool_call": true } } ]}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283/** * 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, set ALIBABA_API_KEY, or write ~/.alibaba.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> }>>), ]);
// The token-plan entry describes this deployment; other providers are a fallback for // ids it has not caught up with yet, preferred in order of how much they know. const own = payload[CATALOG_PROVIDER]?.models ?? {}; const elsewhere = new Map<string, DevModel[]>(); for (const [provider, entry] of Object.entries(payload)) { if (provider === CATALOG_PROVIDER) 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));
const catalog: BakedCatalog = { generatedAt: new Date().toISOString(), baseUrl, models }; 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();