From 960d0d1f2388d039c8e8cd8f610723c2425ca92b Mon Sep 17 00:00:00 2001 From: Tao Yang Date: Mon, 25 May 2026 16:37:08 +0800 Subject: [PATCH] feat: extract models from command-code dist, load via models.json - Add scripts/extract-models.ts to parse command-code npm dist file - Generate models.json (21 models, 15 pricing entries) with contextWindow and maxOutputTokens pre-filled; no nulls or hardcoded fallbacks in index.ts - Rewrite index.ts to load model list and costs from models.json - Cap gateway model maxOutputTokens at 65536 (API limit for Baseten/Vercel) - Add 'Update models' section to README documenting the generation flow - Add npm run extract-models script --- README.md | 23 ++ index.ts | 226 ++++++---------- models.json | 545 ++++++++++++++++++++++++++++++++++++++ package.json | 4 +- scripts/extract-models.ts | 252 ++++++++++++++++++ 5 files changed, 907 insertions(+), 143 deletions(-) create mode 100644 models.json create mode 100644 scripts/extract-models.ts diff --git a/README.md b/README.md index 9321b3a..442bd0b 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,29 @@ Or within pi: /models ``` +## Update models + +The model list (`models.json`) is extracted from the [command-code](https://www.npmjs.com/package/command-code) npm package's dist file. When Command Code releases a new version with updated models, regenerate it: + +```sh +npm run extract-models +``` + +This runs `scripts/extract-models.ts`, which: + +1. Downloads the latest `command-code` tarball from npm (`npm pack command-code`) +2. Parses the minified `dist/index.mjs` to extract provider definitions, model metadata, and pricing +3. Fills in `contextWindow` and `maxOutputTokens` with sensible defaults where the CLI omits them +4. Writes the result to `models.json` + +To use a specific version or local dist file: + +```sh +npx tsx scripts/extract-models.ts /path/to/command-code/dist/index.mjs +``` + +`models.json` is committed to the repo and included in the npm package. + ## Publish ```sh diff --git a/index.ts b/index.ts index c56fdef..24a6b03 100644 --- a/index.ts +++ b/index.ts @@ -9,158 +9,100 @@ * 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json` * as {"apiKey": "user_..."} or {"commandcode": "user_..."} * - * Models: deepseek-v4-pro, deepseek-v4-flash, claude-sonnet-4-6, claude-opus-4-7, etc. + * Models are sourced from models.json, which is extracted from the command-code + * npm package dist file. Run `npx tsx scripts/extract-models.ts` to refresh. */ -import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai" -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent" +import { readFileSync } from "node:fs"; -import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts" -import { getApiKey, login, refreshToken } from "./src/oauth.ts" +import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"; +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE +import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"; +import { getApiKey, login, refreshToken } from "./src/oauth.ts"; + +const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE; // --------------------------------------------------------------------------- -// Model definitions +// Load model definitions from models.json // --------------------------------------------------------------------------- -const MODELS = [ - // Premium (Anthropic) - { - id: "claude-opus-4-7", - name: "Claude Opus 4.7 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 32_000, - }, - { - id: "claude-opus-4-6", - name: "Claude Opus 4.6 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 32_000, - }, - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 16_384, - }, - { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 8_192, - }, - // Premium (OpenAI) - { - id: "gpt-5.5", - name: "GPT-5.5 (CC)", - reasoning: true, - contextWindow: 256_000, - maxTokens: 128_000, - }, - { - id: "gpt-5.4", - name: "GPT-5.4 (CC)", - reasoning: true, - contextWindow: 256_000, - maxTokens: 128_000, - }, - { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex (CC)", - reasoning: true, - contextWindow: 256_000, - maxTokens: 128_000, - }, - { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini (CC)", - reasoning: false, - contextWindow: 256_000, - maxTokens: 128_000, - }, - // Open-source - { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro (CC)", - reasoning: true, - contextWindow: 1_000_000, - maxTokens: 384_000, - }, - { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash (CC)", - reasoning: true, - contextWindow: 1_000_000, - maxTokens: 384_000, - }, - { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6 (CC)", - reasoning: true, - contextWindow: 262_144, - maxTokens: 131_072, - }, - { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5 (CC)", - reasoning: true, - contextWindow: 262_144, - maxTokens: 131_072, - }, - { - id: "zai-org/GLM-5.1", - name: "GLM-5.1 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 131_072, - }, - { - id: "zai-org/GLM-5", - name: "GLM-5 (CC)", - reasoning: true, - contextWindow: 200_000, - maxTokens: 131_072, - }, - { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax M2.7 (CC)", - reasoning: true, - contextWindow: 1_048_576, - maxTokens: 131_072, - }, - { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5 (CC)", - reasoning: true, - contextWindow: 1_048_576, - maxTokens: 131_072, - }, - { - id: "Qwen/Qwen3.6-Max-Preview", - name: "Qwen 3.6 Max (CC)", - reasoning: true, - contextWindow: 1_000_000, - maxTokens: 131_072, - }, - { - id: "Qwen/Qwen3.6-Plus", - name: "Qwen 3.6 Plus (CC)", - reasoning: true, - contextWindow: 1_000_000, - maxTokens: 131_072, - }, -] +interface ModelsJson { + providers: Record; + models: Array<{ + key: string; + id: string; + provider: string; + spec: string; + label: string; + name: string; + description: string; + reasoning: boolean; + reasoningEfforts: string[] | null; + contextWindow: number; + maxOutputTokens: number; + vendorLabel: string | null; + }>; + pricing: Array<{ + provider: string; + id: string; + category: string; + promptCost: number; + completionCost: number; + cacheWrite5mCost: number; + cacheWrite1hCost: number; + cacheHitCost: number; + }>; +} + +const modelsJson: ModelsJson = JSON.parse( + readFileSync(new URL("./models.json", import.meta.url), "utf8"), +); + +// --------------------------------------------------------------------------- +// Build cost lookup (model id -> pricing) +// --------------------------------------------------------------------------- + +const costByModelId = new Map(); +for (const p of modelsJson.pricing) { + // Pricing id is like "anthropic:claude-sonnet-4-6" + const colonIdx = p.id.indexOf(":"); + if (colonIdx > 0) { + costByModelId.set(p.id.substring(colonIdx + 1), p); + } + costByModelId.set(p.id, p); +} + +// --------------------------------------------------------------------------- +// Build pi model list (all defaults come from models.json) +// --------------------------------------------------------------------------- + +const MODELS = modelsJson.models.map((m) => { + const cost = costByModelId.get(m.id); + return { + id: m.id, + name: `${m.name} (CC)`, + reasoning: m.reasoning, + contextWindow: m.contextWindow, + maxTokens: m.maxOutputTokens, + cost: { + input: cost?.promptCost ?? 0, + output: cost?.completionCost ?? 0, + cacheRead: cost?.cacheHitCost ?? 0, + cacheWrite: Math.max(cost?.cacheWrite5mCost ?? 0, cost?.cacheWrite1hCost ?? 0), + }, + }; +}); + +// --------------------------------------------------------------------------- +// Stream factory +// --------------------------------------------------------------------------- const streamCommandCode = createStreamCommandCode({ createStream: createAssistantMessageEventStream, calculateCost, apiBase: API_BASE, -}) +}); // --------------------------------------------------------------------------- // Extension entry point @@ -188,10 +130,10 @@ export default function (pi: ExtensionAPI) { id: model.id, name: model.name, reasoning: model.reasoning, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + input: ["text"] as const, + cost: model.cost, contextWindow: model.contextWindow, maxTokens: model.maxTokens, })), - }) + }); } diff --git a/models.json b/models.json new file mode 100644 index 0000000..21bab17 --- /dev/null +++ b/models.json @@ -0,0 +1,545 @@ +{ + "providers": { + "ANTHROPIC": "anthropic", + "OPENAI": "openai", + "BASETEN": "baseten", + "VERCEL_AI_GATEWAY": "vercel-ai-gateway", + "CLOUDFLARE_AI_GATEWAY": "cloudflare-ai-gateway", + "OPENROUTER": "openrouter" + }, + "providerGroups": [ + { + "id": "command-code", + "label": "Command Code", + "shortLabel": "cmd", + "description": "recommended", + "providers": [ + "anthropic", + "openai", + "baseten", + "vercel-ai-gateway" + ] + }, + { + "id": "anthropic", + "label": "Anthropic", + "shortLabel": "anth", + "description": "Claude Pro/Max", + "providers": [ + "anthropic" + ] + }, + { + "id": "github-copilot", + "label": "GitHub Copilot", + "shortLabel": "copilot", + "description": "Copilot subscription", + "providers": [ + "anthropic", + "openai" + ] + }, + { + "id": "codex", + "label": "ChatGPT (Codex)", + "shortLabel": "codex", + "description": "ChatGPT Pro/Plus subscription", + "providers": [ + "openai" + ] + } + ], + "models": [ + { + "key": "SONNET_4_6", + "id": "claude-sonnet-4-6", + "provider": "anthropic", + "spec": "chatComplete", + "label": "Claude Sonnet 4.6", + "name": "Claude Sonnet 4.6", + "description": "best combo of speed & intelligence (recommended)", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "contextWindow": 1000000, + "maxOutputTokens": 64000, + "vendorLabel": null + }, + { + "key": "OPUS_4_7", + "id": "claude-opus-4-7", + "provider": "anthropic", + "spec": "chatComplete", + "label": "Claude Opus 4.7", + "name": "Claude Opus 4.7", + "description": "most intelligent for agents and coding", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "contextWindow": 1000000, + "maxOutputTokens": 64000, + "vendorLabel": null + }, + { + "key": "HAIKU_4_5", + "id": "claude-haiku-4-5-20251001", + "provider": "anthropic", + "spec": "chatComplete", + "label": "Claude Haiku 4.5", + "name": "Claude Haiku 4.5", + "description": "fastest & most compact, great for quick tasks", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 200000, + "maxOutputTokens": 64000, + "vendorLabel": null + }, + { + "key": "GPT_5_5", + "id": "gpt-5.5", + "provider": "openai", + "spec": "responses", + "label": "GPT-5.5", + "name": "GPT-5.5", + "description": "latest frontier model for general complex work", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high", + "xhigh" + ], + "contextWindow": 256000, + "maxOutputTokens": 128000, + "vendorLabel": null + }, + { + "key": "GPT_5_4", + "id": "gpt-5.4", + "provider": "openai", + "spec": "responses", + "label": "GPT-5.4", + "name": "GPT-5.4", + "description": "frontier model for general complex work", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high", + "xhigh" + ], + "contextWindow": 400000, + "maxOutputTokens": 128000, + "vendorLabel": null + }, + { + "key": "GPT_5_3_CODEX", + "id": "gpt-5.3-codex", + "provider": "openai", + "spec": "responses", + "label": "GPT-5.3 Codex", + "name": "GPT-5.3 Codex", + "description": "frontier coding model", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high", + "xhigh" + ], + "contextWindow": 400000, + "maxOutputTokens": 128000, + "vendorLabel": null + }, + { + "key": "GPT_5_4_MINI", + "id": "gpt-5.4-mini", + "provider": "openai", + "spec": "responses", + "label": "GPT-5.4 Mini", + "name": "GPT-5.4 Mini", + "description": "fast, cost-effective model for everyday tasks", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high" + ], + "contextWindow": 400000, + "maxOutputTokens": 128000, + "vendorLabel": null + }, + { + "key": "KIMI_K2_6", + "id": "moonshotai/Kimi-K2.6", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Kimi K2.6", + "name": "Kimi K2.6", + "description": "long-horizon coding with vision", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 256000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "KIMI_K2_5", + "id": "moonshotai/Kimi-K2.5", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Kimi K2.5", + "name": "Kimi K2.5", + "description": "multimodal frontend coding", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 256000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "GLM_5_1", + "id": "zai-org/GLM-5.1", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "GLM-5.1", + "name": "GLM-5.1", + "description": "long-horizon autonomous coding agent", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 200000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "GLM_5", + "id": "zai-org/GLM-5", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "GLM-5", + "name": "GLM-5", + "description": "multi-mode thinking & long-range planning", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 200000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "MINIMAX_M2_7", + "id": "MiniMaxAI/MiniMax-M2.7", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "MiniMax M2.7", + "name": "MiniMax M2.7", + "description": "end-to-end software engineering agent", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "MINIMAX_M2_5", + "id": "MiniMaxAI/MiniMax-M2.5", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "MiniMax M2.5", + "name": "MiniMax M2.5", + "description": "cross-platform full-stack agentic dev", + "reasoning": false, + "reasoningEfforts": null, + "contextWindow": 200000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "DEEPSEEK_V4_PRO", + "id": "deepseek/deepseek-v4-pro", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "DeepSeek V4 Pro", + "name": "DeepSeek V4 Pro", + "description": "hybrid-attention long-context reasoning", + "reasoning": true, + "reasoningEfforts": [ + "high", + "max" + ], + "contextWindow": 1000000, + "maxOutputTokens": 384000, + "vendorLabel": null + }, + { + "key": "DEEPSEEK_V4_FLASH", + "id": "deepseek/deepseek-v4-flash", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "DeepSeek V4 Flash", + "name": "DeepSeek V4 Flash", + "description": "fast hybrid-attention reasoning", + "reasoning": true, + "reasoningEfforts": [ + "high", + "max" + ], + "contextWindow": 1000000, + "maxOutputTokens": 384000, + "vendorLabel": null + }, + { + "key": "QWEN_3_6_MAX_PREVIEW", + "id": "Qwen/Qwen3.6-Max-Preview", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Qwen 3.6 Max Preview", + "name": "Qwen 3.6 Max Preview", + "description": "vibe coding & efficient agent execution", + "reasoning": true, + "reasoningEfforts": null, + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "QWEN_3_6_PLUS", + "id": "Qwen/Qwen3.6-Plus", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Qwen 3.6 Plus", + "name": "Qwen 3.6 Plus", + "description": "agentic coding & reasoning", + "reasoning": true, + "reasoningEfforts": null, + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "QWEN_3_7_MAX", + "id": "Qwen/Qwen3.7-Max", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Qwen 3.7 Max", + "name": "Qwen 3.7 Max", + "description": "frontier coding & long-horizon agent execution", + "reasoning": true, + "reasoningEfforts": null, + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "STEP_3_5_FLASH", + "id": "stepfun/Step-3.5-Flash", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Step 3.5 Flash", + "name": "Step 3.5 Flash", + "description": "fast sparse-MoE agentic reasoning", + "reasoning": true, + "reasoningEfforts": null, + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": null + }, + { + "key": "GEMINI_3_5_FLASH", + "id": "google/gemini-3.5-flash", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Gemini 3.5 Flash", + "name": "Gemini 3.5 Flash", + "description": "Pro-level coding proficiency, parallel agentic execution", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high" + ], + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": "Google" + }, + { + "key": "GEMINI_3_1_FLASH_LITE", + "id": "google/gemini-3.1-flash-lite", + "provider": "vercel-ai-gateway", + "spec": "chatComplete", + "label": "Gemini 3.1 Flash Lite", + "name": "Gemini 3.1 Flash Lite", + "description": "high-volume workhorse model with implicit caching", + "reasoning": true, + "reasoningEfforts": [ + "low", + "medium", + "high" + ], + "contextWindow": 1000000, + "maxOutputTokens": 65536, + "vendorLabel": "Google" + } + ], + "pricing": [ + { + "provider": "Anthropic", + "id": "anthropic:claude-sonnet-4-20250514", + "category": "premium", + "promptCost": 3, + "completionCost": 15, + "cacheWrite5mCost": 3.75, + "cacheWrite1hCost": 6, + "cacheHitCost": 0.3 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-sonnet-4-5-20250929", + "category": "premium", + "promptCost": 3, + "completionCost": 15, + "cacheWrite5mCost": 3.75, + "cacheWrite1hCost": 6, + "cacheHitCost": 0.3 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-opus-4-5-20251101", + "category": "premium", + "promptCost": 5, + "completionCost": 25, + "cacheWrite5mCost": 6.25, + "cacheWrite1hCost": 10, + "cacheHitCost": 0.5 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-sonnet-4-6", + "category": "premium", + "promptCost": 3, + "completionCost": 15, + "cacheWrite5mCost": 3.75, + "cacheWrite1hCost": 6, + "cacheHitCost": 0.3 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-opus-4-7", + "category": "premium", + "promptCost": 5, + "completionCost": 25, + "cacheWrite5mCost": 6.25, + "cacheWrite1hCost": 10, + "cacheHitCost": 0.5 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-opus-4-6", + "category": "premium", + "promptCost": 5, + "completionCost": 25, + "cacheWrite5mCost": 6.25, + "cacheWrite1hCost": 10, + "cacheHitCost": 0.5 + }, + { + "provider": "Anthropic", + "id": "anthropic:claude-haiku-4-5-20251001", + "category": "premium", + "promptCost": 1, + "completionCost": 5, + "cacheWrite5mCost": 1.25, + "cacheWrite1hCost": 2, + "cacheHitCost": 0.1 + }, + { + "provider": "OpenAI", + "id": "openai:gpt-5.5", + "category": "premium", + "promptCost": 5, + "completionCost": 30, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0.5 + }, + { + "provider": "OpenAI", + "id": "openai:gpt-5.4", + "category": "premium", + "promptCost": 2.5, + "completionCost": 15, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0.25 + }, + { + "provider": "OpenAI", + "id": "openai:gpt-5.3-codex", + "category": "premium", + "promptCost": 2, + "completionCost": 8, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0.5 + }, + { + "provider": "OpenAI", + "id": "openai:gpt-5.4-mini", + "category": "premium", + "promptCost": 0.75, + "completionCost": 4.5, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0.075 + }, + { + "provider": "Baseten", + "id": "baseten:zai-org/GLM-5", + "category": "opensource", + "promptCost": 0.95, + "completionCost": 3.15, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0 + }, + { + "provider": "Baseten", + "id": "baseten:moonshotai/Kimi-K2.5", + "category": "opensource", + "promptCost": 0.6, + "completionCost": 3, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0 + }, + { + "provider": "Baseten", + "id": "baseten:moonshotai/Kimi-K2.6", + "category": "opensource", + "promptCost": 0.95, + "completionCost": 4, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0.16 + }, + { + "provider": "Baseten", + "id": "baseten:MiniMaxAI/MiniMax-M2.5", + "category": "opensource", + "promptCost": 0.5, + "completionCost": 2, + "cacheWrite5mCost": 0, + "cacheWrite1hCost": 0, + "cacheHitCost": 0 + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 2757d5b..2fc4fee 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "files": [ "index.ts", "src/", + "models.json", "README.md", "LICENSE" ], @@ -34,7 +35,8 @@ "test:abort": "tsx tests/test-abort.ts", "test:stream": "tsx tests/test-stream.ts", "test:pi-local": "node tests/test-pi-local.mjs", - "test:smoke": "node tests/test-smoke.mjs" + "test:smoke": "node tests/test-smoke.mjs", + "extract-models": "tsx scripts/extract-models.ts" }, "pi": { "extensions": [ diff --git a/scripts/extract-models.ts b/scripts/extract-models.ts new file mode 100644 index 0000000..d9dca06 --- /dev/null +++ b/scripts/extract-models.ts @@ -0,0 +1,252 @@ +#!/usr/bin/env -S npx tsx + +/** + * Extract model & provider definitions from the command-code npm package dist file. + * + * Usage: + * npx tsx scripts/extract-models.ts [path-to-dist/index.mjs] + * npx tsx scripts/extract-models.ts (downloads latest from npm) + * + * Output: models.json + * { + * providers: { ... }, // provider key -> value map + * providerGroups: { ... }, // provider-group key -> { id, label, providers[] } + * models: [ ... ], // flattened model definitions + * pricing: [ ... ] // pricing entries (per 1M tokens, USD) + * } + */ + +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Evaluate a JS object literal string safely via Function constructor. */ +function parseObjectLiteral(code: string): Record { + const fn = new Function(`return (${code})`); + return fn() as Record; +} + +// --------------------------------------------------------------------------- +// Step 1: get the dist file +// --------------------------------------------------------------------------- + +function ensureDist(srcPath?: string): string { + if (srcPath) { + if (!existsSync(srcPath)) throw new Error(`File not found: ${srcPath}`); + return srcPath; + } + + // Download latest from npm + const tmpDir = join(process.cwd(), ".extract-tmp"); + mkdirSync(tmpDir, { recursive: true }); + const tgz = execSync(`npm pack command-code --pack-destination "${tmpDir}"`, { + encoding: "utf8", + }).trim(); + const tgzPath = join(tmpDir, tgz); + execSync(`tar xzf "${tgzPath}" -C "${tmpDir}"`, { encoding: "utf8" }); + return join(tmpDir, "package", "dist", "index.mjs"); +} + +// --------------------------------------------------------------------------- +// Step 2: extract +// --------------------------------------------------------------------------- + +interface ModelDef { + key: string; + id: string; + provider: string; + spec: string; + label: string; + name: string; + description: string; + reasoning: boolean; + reasoningEfforts: string[] | null; + contextWindow: number; + maxOutputTokens: number; + vendorLabel: string | null; +} + +interface PricingEntry { + provider: string; + id: string; + category: string; + promptCost: number; + completionCost: number; + cacheWrite5mCost: number; + cacheWrite1hCost: number; + cacheHitCost: number; +} + +interface ProviderGroup { + id: string; + label: string; + shortLabel: string; + description: string; + providers: string[]; +} + +function extract(code: string) { + // --- Wt: provider constants --- + const wtMatch = code.match(/Wt=\{([^}]+)\}/); + if (!wtMatch) throw new Error("Cannot find Wt"); + const wtRaw = "{" + wtMatch[1] + "}"; + const wt: Record = {}; + for (const m of wtRaw.matchAll(/(\w+):"(\w[-\w]*)"/g)) { + wt[m[1]] = m[2]; + } + console.log("Providers:", wt); + + // --- an: model definitions --- + // an={...}).SONNET + const anIdx = code.indexOf("an={"); + if (anIdx < 0) throw new Error("Cannot find an"); + const anEndIdx = code.indexOf("}).SONNET", anIdx); + if (anEndIdx < 0) throw new Error("Cannot find end of an"); + let anCode = code.substring(anIdx + 1, anEndIdx + 2); // "an={...})" + anCode = anCode.replace(/^\(an=/, "").replace(/\)$/, ""); + + // Replace minified JS idioms + anCode = anCode.replace(/\bQt\b/g, JSON.stringify("vercel-ai-gateway")); + anCode = anCode.replace(/\bon\b/g, JSON.stringify("chatComplete")); + anCode = anCode.replace(/\bsn\b/g, JSON.stringify("responses")); + anCode = anCode.replace(/Wt\.([A-Z_]+)/g, (_, key: string) => + JSON.stringify(wt[key]), + ); + anCode = anCode.replace(/!0/g, "true"); + anCode = anCode.replace(/!1/g, "false"); + + const an = parseObjectLiteral(anCode); + + // --- Yt: pricing (provider -> model array) --- + const ytIdx = code.indexOf("Yt={["); + if (ytIdx < 0) throw new Error("Cannot find Yt"); + // Find the matching closing brace for Yt + let depth = 1; + let ytEndIdx = ytIdx + 4; + while (depth > 0 && ytEndIdx < code.length) { + if (code[ytEndIdx] === "{") depth++; + else if (code[ytEndIdx] === "}") depth--; + ytEndIdx++; + } + let ytCode = code.substring(ytIdx + 3, ytEndIdx); // "{ ... }" + ytCode = ytCode.replace(/Wt\.([A-Z_]+)/g, (_, key: string) => + JSON.stringify(wt[key]), + ); + ytCode = ytCode.replace(/!0/g, "true"); + ytCode = ytCode.replace(/!1/g, "false"); + const yt = parseObjectLiteral(ytCode); + + // --- pn: provider groups --- + const pnIdx = code.indexOf('pn={"command-code"'); + if (pnIdx < 0) throw new Error("Cannot find pn"); + const pnEndIdx = code.indexOf(",__name(buildModelGroups", pnIdx); + if (pnEndIdx < 0) throw new Error("Cannot find end of pn"); + let pnCode = code.substring(pnIdx + 3, pnEndIdx); + pnCode = pnCode.replace(/Wt\.([A-Z_]+)/g, (_, key: string) => + JSON.stringify(wt[key]), + ); + pnCode = pnCode.replace(/!0/g, "true"); + pnCode = pnCode.replace(/!1/g, "false"); + pnCode = pnCode.replace(/,\s*$/, ""); + const pn = parseObjectLiteral(pnCode); + + // --- Defaults for fields the CLI doesn't provide per-model --- + // maxOutputTokens: use the minimum across all providers for each model. + // Anthropic direct: 64k OpenAI direct: 128k + // DeepSeek (gateway): 384k (known to work) + // Other gateway models (Baseten/Vercel/Cloudflare/OpenRouter): 65536 + const CONTEXT_WINDOW_FALLBACKS: Record = { + "gpt-5.5": 256_000, + "zai-org/GLM-5.1": 200_000, + "MiniMaxAI/MiniMax-M2.7": 1_048_576, + "Qwen/Qwen3.6-Max-Preview": 1_000_000, + "Qwen/Qwen3.6-Plus": 1_000_000, + }; + const DEFAULT_CONTEXT_WINDOW = 200_000; + + function maxOutputTokensForModel(id: string, provider: string): number { + if (provider === "anthropic") return 64_000; + if (provider === "openai") return 128_000; + if (id.startsWith("deepseek/")) return 384_000; + // Gateway models — lowest common denominator across Baseten/Vercel/Cloudflare + return 65_536; + } + + // --- Build output --- + const models: ModelDef[] = []; + for (const [key, obj] of Object.entries(an)) { + const m = obj as Record; + const id = m.id as string; + const provider = m.provider as string; + models.push({ + key, + id, + provider, + spec: (m.spec as string) || "chatComplete", + label: m.label as string, + name: m.name as string, + description: m.description as string, + reasoning: !!( + m.reasoning ?? + ((m.reasoningEfforts as string[])?.length ?? 0) > 0 + ), + reasoningEfforts: (m.reasoningEfforts as string[]) || null, + contextWindow: + (typeof m.contextWindow === "number" ? m.contextWindow : null) ?? + CONTEXT_WINDOW_FALLBACKS[id] ?? + DEFAULT_CONTEXT_WINDOW, + maxOutputTokens: + maxOutputTokensForModel(id, provider), + vendorLabel: (m.vendorLabel as string) || null, + }); + } + + const pricing: PricingEntry[] = []; + for (const [_provider, entries] of Object.entries(yt)) { + for (const entry of entries as Array>) { + pricing.push({ + provider: entry.provider as string, + id: entry.id as string, + category: entry.category as string, + promptCost: entry.promptCost as number, + completionCost: entry.completionCost as number, + cacheWrite5mCost: (entry.cacheWrite5mCost as number) || 0, + cacheWrite1hCost: (entry.cacheWrite1hCost as number) || 0, + cacheHitCost: (entry.cacheHitCost as number) || 0, + }); + } + } + + const providerGroups: ProviderGroup[] = []; + for (const [key, obj] of Object.entries(pn)) { + const g = obj as Record; + providerGroups.push({ + id: g.id as string, + label: g.label as string, + shortLabel: (g.shortLabel as string) || "", + description: (g.description as string) || "", + providers: (g.supportedModelProviders as string[]) || [], + }); + } + + return { providers: wt, providerGroups, models, pricing }; +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +const distPath = ensureDist(process.argv[2]); +console.log("Reading:", distPath); +const code = readFileSync(distPath, "utf8"); +const result = extract(code); + +const outPath = join(process.cwd(), "models.json"); +writeFileSync(outPath, JSON.stringify(result, null, 2), "utf8"); +console.log( + `Wrote ${result.models.length} models + ${result.pricing.length} pricing entries to ${outPath}`, +);