Fetch Command Code models at startup

This commit is contained in:
Patrick Wozniak
2026-05-26 21:51:41 +02:00
parent 960d0d1f23
commit 418f4c925b
6 changed files with 114 additions and 921 deletions
+13 -32
View File
@@ -6,17 +6,17 @@ A [pi](https://github.com/badlogic/pi-mono) custom provider that connects pi to
> **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own Command Code API key or subscription. > **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own Command Code API key or subscription.
> 💰 **Current offer:** Command Code offers [4× usage of DeepSeek V4](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) (Pro and Flash) at no extra cost. > 💰 **Current offers:** Command Code offers [4× usage of DeepSeek V4 Pro](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) and [2× usage of Qwen 3.7 Max](https://commandcode.ai/docs/resources/pricing-limits#qwen-3.7-max-2x-usage).
## Models ## Models
18 models across premium and open-source providers: Models are fetched live from Command Code's Provider API at startup, so new models like Qwen 3.7 Max show up without a package release.
| Category | Models | You can list the current Command Code models with:
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Anthropic** | Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5 | ```sh
| **OpenAI** | GPT-5.5, GPT-5.4, GPT-5.3 Codex, GPT-5.4 Mini | pi -e index.ts --list-models
| **Open-source** | DeepSeek V4, DeepSeek V4 Pro, DeepSeek V4 Flash, Kimi K2.6, Kimi K2.5, GLM-5.1, GLM-5, MiniMax M2.7, MiniMax M2.5, Qwen 3.6 Max, Qwen 3.6 Plus | ```
## Install ## Install
@@ -88,40 +88,21 @@ After installing and setting your API key, select a Command Code model in pi:
/model deepseek/deepseek-v4-flash /model deepseek/deepseek-v4-flash
``` ```
Any query will then use the Command Code API. You can list available models: Any query will then use the Command Code API. You can list available models within pi:
```sh
pi -e index.ts --list-models
```
Or within pi:
```txt ```txt
/models /models
``` ```
## Update models ## Model discovery
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: On startup, the provider fetches:
```sh ```txt
npm run extract-models https://api.commandcode.ai/provider/v1/models
``` ```
This runs `scripts/extract-models.ts`, which: For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
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 ## Publish
+15 -89
View File
@@ -9,106 +9,32 @@
* 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json` * 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json`
* as {"apiKey": "user_..."} or {"commandcode": "user_..."} * as {"apiKey": "user_..."} or {"commandcode": "user_..."}
* *
* Models are sourced from models.json, which is extracted from the command-code * Models are fetched from Command Code's Provider API at startup.
* npm package dist file. Run `npx tsx scripts/extract-models.ts` to refresh.
*/ */
import { readFileSync } from "node:fs"; import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"; import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"; const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
import { getApiKey, login, refreshToken } from "./src/oauth.ts"; const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE;
// ---------------------------------------------------------------------------
// Load model definitions from models.json
// ---------------------------------------------------------------------------
interface ModelsJson {
providers: Record<string, string>;
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<string, ModelsJson["pricing"][number]>();
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({ const streamCommandCode = createStreamCommandCode({
createStream: createAssistantMessageEventStream, createStream: createAssistantMessageEventStream,
calculateCost, calculateCost,
apiBase: API_BASE, apiBase: API_BASE,
}); })
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Extension entry point // Extension entry point
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export default function (pi: ExtensionAPI) { export default async function (pi: ExtensionAPI) {
const models = await fetchCommandCodeModels({ url: MODELS_URL })
pi.registerProvider("commandcode", { pi.registerProvider("commandcode", {
name: "Command Code", name: "Command Code",
baseUrl: API_BASE, baseUrl: API_BASE,
@@ -126,14 +52,14 @@ export default function (pi: ExtensionAPI) {
refreshToken, refreshToken,
getApiKey, getApiKey,
}, },
models: MODELS.map((model) => ({ models: models.map((model) => ({
id: model.id, id: model.id,
name: model.name, name: model.name,
reasoning: model.reasoning, reasoning: model.reasoning,
input: ["text"] as const, input: ["text"] as const,
cost: model.cost, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.contextWindow, contextWindow: model.contextWindow,
maxTokens: model.maxTokens, maxTokens: model.maxTokens,
})), })),
}); })
} }
-545
View File
@@ -1,545 +0,0 @@
{
"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
}
]
}
+1 -3
View File
@@ -21,7 +21,6 @@
"files": [ "files": [
"index.ts", "index.ts",
"src/", "src/",
"models.json",
"README.md", "README.md",
"LICENSE" "LICENSE"
], ],
@@ -35,8 +34,7 @@
"test:abort": "tsx tests/test-abort.ts", "test:abort": "tsx tests/test-abort.ts",
"test:stream": "tsx tests/test-stream.ts", "test:stream": "tsx tests/test-stream.ts",
"test:pi-local": "node tests/test-pi-local.mjs", "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": { "pi": {
"extensions": [ "extensions": [
-252
View File
@@ -1,252 +0,0 @@
#!/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<string, unknown> {
const fn = new Function(`return (${code})`);
return fn() as Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// 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<string, string> = {};
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<string, number> = {
"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<string, unknown>;
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<Record<string, unknown>>) {
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<string, unknown>;
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}`,
);
+85
View File
@@ -0,0 +1,85 @@
export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
interface ApiModel {
id: string
name: string
contextLength: number
}
export interface CommandCodeModel {
id: string
name: string
reasoning: boolean
contextWindow: number
maxTokens: number
}
interface FetchCommandCodeModelsOptions {
url?: string
fetchImpl?: typeof fetch
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function stringField(record: Record<string, unknown>, key: string): string {
const value = record[key]
if (typeof value !== "string") throw new Error(`Expected ${key} to be a string`)
return value
}
function numberField(record: Record<string, unknown>, key: string): number {
const value = record[key]
if (typeof value !== "number") throw new Error(`Expected ${key} to be a number`)
return value
}
function parseApiModel(value: unknown): ApiModel {
if (!isRecord(value)) throw new Error("Expected model entry to be an object")
return {
id: stringField(value, "id"),
name: stringField(value, "name"),
contextLength: numberField(value, "context_length"),
}
}
export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
if (!isRecord(value)) throw new Error("Expected models response to be an object")
if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
const data = value.data
if (!Array.isArray(data)) throw new Error("Expected models response data to be an array")
return data.map(parseApiModel).map((model) => ({
id: model.id,
name: `${model.name} (CC)`,
reasoning: true,
contextWindow: model.contextLength,
maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
}))
}
export async function fetchCommandCodeModels(
options: FetchCommandCodeModelsOptions = {},
): Promise<readonly CommandCodeModel[]> {
const url = options.url ?? DEFAULT_MODELS_URL
const fetchImpl = options.fetchImpl ?? fetch
const response = await fetchImpl(url, {
headers: {
accept: "application/json",
},
})
if (!response.ok) {
throw new Error(
`Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
)
}
const body: unknown = await response.json()
return commandCodeModelsFromApiResponse(body)
}