Merge pull request #4 from ninehills/main
Update Command Code model discovery. Closes #3.
This commit is contained in:
@@ -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.
|
||||
|
||||
> 💰 **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
|
||||
|
||||
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 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Anthropic** | Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5 |
|
||||
| **OpenAI** | GPT-5.5, GPT-5.4, GPT-5.3 Codex, GPT-5.4 Mini |
|
||||
| **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 |
|
||||
You can list the current Command Code models with:
|
||||
|
||||
```sh
|
||||
pi -e index.ts --list-models
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
@@ -88,18 +88,22 @@ After installing and setting your API key, select a Command Code model in pi:
|
||||
/model deepseek/deepseek-v4-flash
|
||||
```
|
||||
|
||||
Any query will then use the Command Code API. You can list available models:
|
||||
|
||||
```sh
|
||||
pi -e index.ts --list-models
|
||||
```
|
||||
|
||||
Or within pi:
|
||||
Any query will then use the Command Code API. You can list available models within pi:
|
||||
|
||||
```txt
|
||||
/models
|
||||
```
|
||||
|
||||
## Model discovery
|
||||
|
||||
On startup, the provider fetches:
|
||||
|
||||
```txt
|
||||
https://api.commandcode.ai/provider/v1/models
|
||||
```
|
||||
|
||||
For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
|
||||
|
||||
## Publish
|
||||
|
||||
```sh
|
||||
|
||||
@@ -9,152 +9,18 @@
|
||||
* 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 fetched from Command Code's Provider API at startup.
|
||||
*/
|
||||
|
||||
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
||||
|
||||
import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
||||
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
||||
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||
|
||||
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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,
|
||||
},
|
||||
]
|
||||
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
||||
|
||||
const streamCommandCode = createStreamCommandCode({
|
||||
createStream: createAssistantMessageEventStream,
|
||||
@@ -166,7 +32,9 @@ const streamCommandCode = createStreamCommandCode({
|
||||
// Extension entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
export default async function (pi: ExtensionAPI) {
|
||||
const models = await fetchCommandCodeModels({ url: MODELS_URL })
|
||||
|
||||
pi.registerProvider("commandcode", {
|
||||
name: "Command Code",
|
||||
baseUrl: API_BASE,
|
||||
@@ -184,11 +52,11 @@ export default function (pi: ExtensionAPI) {
|
||||
refreshToken,
|
||||
getApiKey,
|
||||
},
|
||||
models: MODELS.map((model) => ({
|
||||
models: models.map((model) => ({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
reasoning: model.reasoning,
|
||||
input: ["text"],
|
||||
input: ["text"] as const,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: model.contextWindow,
|
||||
maxTokens: model.maxTokens,
|
||||
|
||||
+2
-1
@@ -25,11 +25,12 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
|
||||
"test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
||||
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
||||
"test:unit": "tsx tests/test-pure-functions.ts",
|
||||
"test:models": "tsx tests/test-models.ts",
|
||||
"test:oauth": "tsx tests/test-oauth.ts",
|
||||
"test:abort": "tsx tests/test-abort.ts",
|
||||
"test:stream": "tsx tests/test-stream.ts",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
|
||||
import { commandCodeModelsFromApiResponse } from "../src/models.ts"
|
||||
|
||||
describe("commandCodeModelsFromApiResponse()", () => {
|
||||
it("converts the Provider API model list to pi models", () => {
|
||||
const models = commandCodeModelsFromApiResponse({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "Qwen/Qwen3.7-Max",
|
||||
object: "model",
|
||||
created: 1779824324,
|
||||
owned_by: "command-code",
|
||||
name: "Qwen 3.7 Max",
|
||||
context_length: 1_000_000,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
assert.deepEqual(models, [
|
||||
{
|
||||
id: "Qwen/Qwen3.7-Max",
|
||||
name: "Qwen 3.7 Max (CC)",
|
||||
reasoning: true,
|
||||
contextWindow: 1_000_000,
|
||||
maxTokens: 65_536,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("rejects unexpected API shapes", () => {
|
||||
assert.throws(() => commandCodeModelsFromApiResponse({ object: "list", data: [{}] }))
|
||||
})
|
||||
})
|
||||
+57
-3
@@ -56,10 +56,40 @@ if (piCheck.error) {
|
||||
}
|
||||
|
||||
let requestCount = 0
|
||||
let modelListRequestCount = 0
|
||||
let lastRequestBody
|
||||
let lastRequestHeaders = {}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
if (req.method === "GET" && req.url === "/provider/v1/models") {
|
||||
modelListRequestCount += 1
|
||||
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" })
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: TEST_MODEL,
|
||||
object: "model",
|
||||
created: 1779824324,
|
||||
owned_by: "command-code",
|
||||
name: "DeepSeek V4 Flash",
|
||||
context_length: 1_000_000,
|
||||
},
|
||||
{
|
||||
id: "Qwen/Qwen3.7-Max",
|
||||
object: "model",
|
||||
created: 1779824324,
|
||||
owned_by: "command-code",
|
||||
name: "Qwen 3.7 Max",
|
||||
context_length: 1_000_000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method !== "POST" || req.url !== "/alpha/generate") {
|
||||
res.writeHead(404)
|
||||
res.end("Not found")
|
||||
@@ -114,6 +144,7 @@ let tempHome
|
||||
const env = {
|
||||
...process.env,
|
||||
COMMANDCODE_API_BASE: apiBase,
|
||||
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
|
||||
}
|
||||
|
||||
if (hasLivePiAuth()) {
|
||||
@@ -161,7 +192,17 @@ function runPi(args, timeoutMs = 30_000) {
|
||||
async function runRpcQuery(timeoutMs = 30_000) {
|
||||
const child = spawn(
|
||||
PI_BIN,
|
||||
["--mode", "rpc", "-e", EXT_PATH, "--provider", "commandcode", "--model", TEST_MODEL],
|
||||
[
|
||||
"--no-extensions",
|
||||
"--mode",
|
||||
"rpc",
|
||||
"-e",
|
||||
EXT_PATH,
|
||||
"--provider",
|
||||
"commandcode",
|
||||
"--model",
|
||||
TEST_MODEL,
|
||||
],
|
||||
{
|
||||
cwd: PROJECT_DIR,
|
||||
env,
|
||||
@@ -250,15 +291,28 @@ async function runRpcQuery(timeoutMs = 30_000) {
|
||||
|
||||
try {
|
||||
console.log("[pi-local] list models through real extension")
|
||||
const list = await runPi(["-e", EXT_PATH, "--list-models"], 20_000)
|
||||
modelListRequestCount = 0
|
||||
const list = await runPi(["--no-extensions", "-e", EXT_PATH, "--list-models"], 20_000)
|
||||
assert.equal(list.code, 0, list.stderr)
|
||||
assert.match(list.stdout, /commandcode/)
|
||||
assert.match(list.stdout, /deepseek\/deepseek-v4-flash/)
|
||||
assert.match(list.stdout, /Qwen\/Qwen3\.7-Max/)
|
||||
assert.equal(modelListRequestCount, 1)
|
||||
|
||||
console.log("[pi-local] print mode through real extension and mock API")
|
||||
requestCount = 0
|
||||
const print = await runPi(
|
||||
["-e", EXT_PATH, "-p", "say mock token", "--provider", "commandcode", "--model", TEST_MODEL],
|
||||
[
|
||||
"--no-extensions",
|
||||
"-e",
|
||||
EXT_PATH,
|
||||
"-p",
|
||||
"say mock token",
|
||||
"--provider",
|
||||
"commandcode",
|
||||
"--model",
|
||||
TEST_MODEL,
|
||||
],
|
||||
30_000,
|
||||
)
|
||||
assert.equal(print.code, 0, print.stderr)
|
||||
|
||||
Reference in New Issue
Block a user