@tduyng/pi-commandcode 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -15
- package/package.json +2 -2
- package/src/catalog.ts +46 -6
- package/src/index.ts +17 -5
- package/src/models.ts +74 -0
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# pi-commandcode
|
|
2
2
|
|
|
3
|
-
Command Code models in
|
|
3
|
+
Command Code models in pi through the documented Provider API, with pi's own prompt, tools, and approvals. Unofficial: not made or endorsed by Command Code.
|
|
4
|
+
|
|
5
|
+
## What it costs
|
|
6
|
+
|
|
7
|
+
No tools, no prompt block, no schema. It is a model provider, so it adds nothing to the context and nothing to the cold start.
|
|
8
|
+
|
|
9
|
+
Streaming, tool calls, and message conversion are pi's own adapters, so the transport adds no round trip of its own.
|
|
4
10
|
|
|
5
11
|
## Requirements
|
|
6
12
|
|
|
7
|
-
-
|
|
13
|
+
- An API key from a plan with API access: GOAT, Pro, Max, Team, or Provider. The Go and $1 plans have no API access.
|
|
8
14
|
- Pi 1.4 or newer.
|
|
9
15
|
|
|
10
16
|
## Use
|
|
@@ -14,7 +20,7 @@ export COMMAND_CODE_API_KEY="user_..."
|
|
|
14
20
|
pi --provider commandcode --model claude-opus-5
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
Inside
|
|
23
|
+
Inside pi, `/model` lists every `commandcode` model. The list is fetched from `https://api.commandcode.ai/provider/v1/models` at startup and again each time the selector opens.
|
|
18
24
|
|
|
19
25
|
## How it works
|
|
20
26
|
|
|
@@ -23,27 +29,41 @@ Inside Pi, `/model` lists every `commandcode` model. The list is fetched from `h
|
|
|
23
29
|
| `claude-*` | Anthropic Messages, `/provider/v1/messages` | `https://api.commandcode.ai/provider` |
|
|
24
30
|
| everything else | OpenAI Chat Completions, `/provider/v1/chat/completions` | `https://api.commandcode.ai/provider/v1` |
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
The key goes as a Bearer token for Chat Completions and as `x-api-key` for Messages, as the Provider API documents. `typesafe/*` decision models are left out because they run on a separate endpoint and never stream.
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
## Limits
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
- The catalog carries no image or price metadata, so every model is text-only and shows zero cost in pi.
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
| --- | --- |
|
|
34
|
-
| `COMMAND_CODE_API_KEY` | Provider API key. Required for requests. |
|
|
35
|
-
| `CMD_ZDR=1` | Sends `x-cmd-zdr: 1`, so requests go only to zero-data-retention upstreams. A model without one fails with HTTP 422. |
|
|
38
|
+
- Thinking is on for every model. Pi levels map to `reasoning_effort`, and `minimal` sends `low`. `off` is offered only for the DeepSeek ids that accept it, because a model that cannot stop thinking rejects `off` with HTTP 400.
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
- Claude 4.6 and newer use adaptive thinking, and the 4.5 models take a thinking budget.
|
|
41
|
+
|
|
42
|
+
- Every model gets a 32768 output limit, the smallest in the Command Code CLI catalog, because per-model limits are not in the Provider API.
|
|
38
43
|
|
|
39
|
-
- The catalog has no reasoning, image or price metadata, so every model is text-only, without reasoning, and shows zero cost in Pi.
|
|
40
|
-
- Every model gets a 32768 output limit, the smallest limit in the Command Code CLI catalog. Per-model limits are not in the Provider API.
|
|
41
44
|
- Chat Completions is used for every non-Claude model, as the docs say. A model that rejects that wire returns its own error.
|
|
42
|
-
|
|
45
|
+
|
|
46
|
+
- Messages requests carry pi's `?beta=true` query string, added by the Anthropic SDK.
|
|
47
|
+
|
|
43
48
|
- If no models appear, check the network and the key, then open `/model` again.
|
|
44
49
|
|
|
50
|
+
## Configure
|
|
51
|
+
|
|
52
|
+
| Variable | Meaning |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `COMMAND_CODE_API_KEY` | Provider API key, required for requests |
|
|
55
|
+
| `CMD_ZDR=1` | Sends `x-cmd-zdr: 1`, so requests go only to zero-data-retention upstreams. A model without one fails with HTTP 422 |
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pi install npm:@tduyng/pi-commandcode
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-commandcode`
|
|
64
|
+
|
|
45
65
|
## Tests
|
|
46
66
|
|
|
47
67
|
```sh
|
|
48
|
-
|
|
68
|
+
bun test extensions/pi-commandcode
|
|
49
69
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tduyng/pi-commandcode",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Command Code models in Pi through the documented Provider API, with Pi's own prompt and tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
package/src/catalog.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent'
|
|
5
|
+
import { Type, type Static } from 'typebox'
|
|
6
|
+
import { Check, Parse } from 'typebox/value'
|
|
3
7
|
|
|
4
8
|
import { COMMAND_CODE_ROOT } from './models.js'
|
|
5
9
|
|
|
@@ -11,22 +15,58 @@ export interface CatalogModel {
|
|
|
11
15
|
|
|
12
16
|
const DECISION_MODEL_PREFIX = 'typesafe/'
|
|
13
17
|
const CATALOG_URL = `${COMMAND_CODE_ROOT}/v1/models`
|
|
18
|
+
const CACHE_FILE = 'commandcode-models.json'
|
|
19
|
+
|
|
20
|
+
const ModelEntry = Type.Object({
|
|
21
|
+
id: Type.String({ minLength: 1 }),
|
|
22
|
+
name: Type.Optional(Type.String()),
|
|
23
|
+
context_length: Type.Number({ exclusiveMinimum: 0 })
|
|
24
|
+
})
|
|
14
25
|
|
|
15
26
|
const CatalogResponse = Type.Object({
|
|
16
|
-
data: Type.Array(
|
|
27
|
+
data: Type.Array(Type.Unknown())
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const CacheFile = Type.Object({
|
|
31
|
+
fetchedAt: Type.Number(),
|
|
32
|
+
models: Type.Array(
|
|
17
33
|
Type.Object({
|
|
18
|
-
id: Type.String(),
|
|
19
|
-
name: Type.
|
|
20
|
-
|
|
34
|
+
id: Type.String({ minLength: 1 }),
|
|
35
|
+
name: Type.String(),
|
|
36
|
+
contextWindow: Type.Number({ exclusiveMinimum: 1 })
|
|
21
37
|
})
|
|
22
38
|
)
|
|
23
39
|
})
|
|
24
40
|
|
|
41
|
+
function cachePath(): string {
|
|
42
|
+
return join(getAgentDir(), CACHE_FILE)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readCachedCatalog(): CatalogModel[] | undefined {
|
|
46
|
+
try {
|
|
47
|
+
const parsed: unknown = JSON.parse(readFileSync(cachePath(), 'utf8'))
|
|
48
|
+
if (!Check(CacheFile, parsed)) return undefined
|
|
49
|
+
return parsed.models
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeCachedCatalog(models: CatalogModel[]): void {
|
|
56
|
+
const target = cachePath()
|
|
57
|
+
const temporary = `${target}.${process.pid}.tmp`
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(temporary, `${JSON.stringify({ fetchedAt: Date.now(), models }, null, 2)}\n`)
|
|
60
|
+
renameSync(temporary, target)
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
export async function fetchCatalog(signal: AbortSignal): Promise<CatalogModel[]> {
|
|
26
65
|
const response = await fetch(CATALOG_URL, { headers: { accept: 'application/json' }, signal })
|
|
27
66
|
if (!response.ok) throw new Error(`Command Code model list failed with HTTP ${response.status}`)
|
|
28
67
|
const body = Parse(CatalogResponse, await response.json())
|
|
29
68
|
return body.data
|
|
69
|
+
.filter((entry): entry is Static<typeof ModelEntry> => Check(ModelEntry, entry))
|
|
30
70
|
.filter((model) => !model.id.startsWith(DECISION_MODEL_PREFIX))
|
|
31
71
|
.map((model) => ({ id: model.id, name: model.name ?? model.id, contextWindow: model.context_length }))
|
|
32
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import type { ExtensionAPI, ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
|
|
3
|
-
import { fetchCatalog } from './catalog.js'
|
|
3
|
+
import { fetchCatalog, readCachedCatalog, writeCachedCatalog } from './catalog.js'
|
|
4
4
|
import { COMMAND_CODE_ROOT, toProviderModels } from './models.js'
|
|
5
5
|
|
|
6
6
|
const PROVIDER_ID = 'commandcode'
|
|
7
7
|
const FETCH_TIMEOUT_MS = 10_000
|
|
8
8
|
|
|
9
|
-
function fetchModels(
|
|
10
|
-
|
|
9
|
+
async function fetchModels(
|
|
10
|
+
signal: AbortSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
11
|
+
): Promise<ProviderModelConfig[]> {
|
|
12
|
+
const catalog = await fetchCatalog(signal)
|
|
13
|
+
writeCachedCatalog(catalog)
|
|
14
|
+
return toProviderModels(catalog)
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export default async function piCommandCode(pi: ExtensionAPI): Promise<void> {
|
|
14
18
|
const zdr = process.env['CMD_ZDR'] === '1'
|
|
15
|
-
const
|
|
19
|
+
const cached = readCachedCatalog()
|
|
20
|
+
const models =
|
|
21
|
+
cached !== undefined ? toProviderModels(cached) : await fetchModels().catch((): ProviderModelConfig[] => [])
|
|
16
22
|
|
|
17
23
|
pi.registerProvider(PROVIDER_ID, {
|
|
18
24
|
name: 'Command Code',
|
|
@@ -20,6 +26,12 @@ export default async function piCommandCode(pi: ExtensionAPI): Promise<void> {
|
|
|
20
26
|
apiKey: '$COMMAND_CODE_API_KEY',
|
|
21
27
|
headers: zdr ? { 'x-cmd-zdr': '1' } : undefined,
|
|
22
28
|
models,
|
|
23
|
-
refreshModels:
|
|
29
|
+
refreshModels: async ({ signal, allowNetwork }) => {
|
|
30
|
+
if (!allowNetwork) {
|
|
31
|
+
const offline = readCachedCatalog()
|
|
32
|
+
return offline === undefined ? models : toProviderModels(offline)
|
|
33
|
+
}
|
|
34
|
+
return fetchModels(signal)
|
|
35
|
+
}
|
|
24
36
|
})
|
|
25
37
|
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ThinkingLevelMap } from '@earendil-works/pi-ai'
|
|
2
|
+
import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent'
|
|
3
|
+
|
|
4
|
+
import type { CatalogModel } from './catalog.js'
|
|
5
|
+
|
|
6
|
+
export const COMMAND_CODE_ROOT = 'https://api.commandcode.ai/provider'
|
|
7
|
+
|
|
8
|
+
const MAX_OUTPUT_TOKENS = 32768
|
|
9
|
+
const OPENAI_COMPAT = {
|
|
10
|
+
supportsStore: false,
|
|
11
|
+
supportsDeveloperRole: false,
|
|
12
|
+
supportsLongCacheRetention: false,
|
|
13
|
+
maxTokensField: 'max_tokens'
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
// The gateway accepts off, low, medium, high, xhigh and max. A model that cannot stop
|
|
17
|
+
// thinking rejects off with HTTP 400, so off is offered only for these ids.
|
|
18
|
+
const OFF_LEVEL_MODELS = new Set([
|
|
19
|
+
'deepseek/deepseek-v4-pro',
|
|
20
|
+
'deepseek/deepseek-v4-flash',
|
|
21
|
+
'deepseek/deepseek-v4-flash-vision-exp',
|
|
22
|
+
'deepseek/deepseek-v4.1-flash',
|
|
23
|
+
'deepseek/deepseek-v4.1-flash-fast'
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
function thinkingLevels(offAllowed: boolean): ThinkingLevelMap {
|
|
27
|
+
return {
|
|
28
|
+
off: offAllowed ? 'off' : null,
|
|
29
|
+
minimal: 'low',
|
|
30
|
+
low: 'low',
|
|
31
|
+
medium: 'medium',
|
|
32
|
+
high: 'high',
|
|
33
|
+
xhigh: 'xhigh',
|
|
34
|
+
max: 'max'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function chatFields(model: CatalogModel) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'chat' as const,
|
|
41
|
+
id: model.id,
|
|
42
|
+
name: model.name,
|
|
43
|
+
input: ['text' as const],
|
|
44
|
+
reasoning: true,
|
|
45
|
+
contextWindow: model.contextWindow,
|
|
46
|
+
maxTokens: MAX_OUTPUT_TOKENS,
|
|
47
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Claude 4.6 and newer reject the budget form and require adaptive thinking; only the 4.5
|
|
52
|
+
// models still take a thinking budget.
|
|
53
|
+
function anthropicModel(model: CatalogModel): ProviderModelConfig {
|
|
54
|
+
return {
|
|
55
|
+
...chatFields(model),
|
|
56
|
+
api: 'anthropic-messages',
|
|
57
|
+
baseUrl: COMMAND_CODE_ROOT,
|
|
58
|
+
compat: model.id.includes('-4-5') ? undefined : { forceAdaptiveThinking: true }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openAIModel(model: CatalogModel): ProviderModelConfig {
|
|
63
|
+
return {
|
|
64
|
+
...chatFields(model),
|
|
65
|
+
api: 'openai-completions',
|
|
66
|
+
baseUrl: `${COMMAND_CODE_ROOT}/v1`,
|
|
67
|
+
compat: OPENAI_COMPAT,
|
|
68
|
+
thinkingLevelMap: thinkingLevels(OFF_LEVEL_MODELS.has(model.id))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function toProviderModels(catalog: CatalogModel[]): ProviderModelConfig[] {
|
|
73
|
+
return catalog.map((model) => (model.id.startsWith('claude-') ? anthropicModel(model) : openAIModel(model)))
|
|
74
|
+
}
|