Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Any query will then use the Command Code API. You can list available models within pi:

```sh
pi -e index.ts --list-models
```txt
/models
```

Or within pi:
## Model discovery

On startup, the provider fetches:

```txt
/models
https://api.commandcode.ai/provider/v1/models
```

For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.

## Publish

```sh
Expand Down
148 changes: 8 additions & 140 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 85 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -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)
}
36 changes: 36 additions & 0 deletions tests/test-models.ts
Original file line number Diff line number Diff line change
@@ -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: [{}] }))
})
})
Loading