pi-commandcode-provider 0.5.1 → 0.6.1
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/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +41 -13
- package/index.ts +106 -38
- package/package.json +13 -4
- package/scripts/live-e2e-profile.mjs +87 -0
- package/scripts/pi-authenticated.mjs +1 -0
- package/scripts/pi-isolated.mjs +1 -0
- package/src/api-key.ts +69 -0
- package/src/auth-server.ts +7 -0
- package/src/commandcode-catalog-overrides.ts +23 -0
- package/src/commandcode-catalog.ts +152 -0
- package/src/converters.ts +78 -17
- package/src/core.ts +117 -23
- package/src/models.ts +68 -97
- package/src/oauth.ts +84 -22
- package/src/pricing.ts +58 -53
- package/src/quota-command.ts +66 -0
- package/src/quota-format.ts +143 -0
- package/src/quota-types.ts +47 -0
- package/src/quota.ts +342 -0
- package/src/runtime.ts +49 -8
- package/src/transport.ts +140 -0
- package/src/types.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.6.1 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Expose selectable thinking levels (`minimal`, `low`, `medium`, `high`, `xhigh`) for `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor` through a manual catalog override, so `/thinking` and `Shift+Tab` no longer stay locked on `off` for these reasoning models.
|
|
8
|
+
- Start from the cached model catalog and refresh it in the background instead of blocking host startup on the catalog request; a first start without a cache still waits for the live catalog.
|
|
9
|
+
- Register the `commandcode-custom` API in the `@earendil-works/pi-ai/compat` registry so sibling extensions that stream with the active Command Code model no longer fail with `No API provider registered for api: commandcode-custom` on plain pi.
|
|
10
|
+
- Assert structural catalog invariants in the model tests so the daily catalog sync no longer fails on every upstream change.
|
|
11
|
+
- Display the monthly renewal date and remaining days in `/commandcode-quota`.
|
|
12
|
+
- Stop silently dropping `role: "developer"` messages (for example OMP advisor steering notes, reminders, and nudges). `/alpha/generate` only accepts `user`, `assistant`, and `tool` roles, so developer messages are now forwarded as `user` messages with identical content in the same chronological position instead of disappearing from the request.
|
|
13
|
+
- Add `Qwen/Qwen3.8-Flash` and `z-ai/glm-5.3-flash` with their verified reasoning efforts (`low, medium, xhigh` and `low, high, max`) and display pricing.
|
|
14
|
+
- Refresh static model capabilities from `command-code@1.40.1`, adding `claude-fable-5-1`, `deepseek/deepseek-v4-flash-fast`, and `tencent/hy4-preview` with their reasoning efforts, adding `moonshotai/Kimi-K3` efforts and the `z-ai/glm-5.3-flash` output limit, and dropping the retired `stealth/ox-alpha` and `minimax/minimax-m3-free`.
|
|
15
|
+
- Refresh display pricing for the current 62-model catalog, adding `claude-fable-5-1`, `deepseek/deepseek-v4-flash-fast`, and `tencent/hy4-preview`, removing the retired `stealth/ox-alpha`, `minimax/minimax-m3-free`, and `minimax/minimax-m2.7-free`, and ending the expired Claude Sonnet 5 introductory and Gemini 3.7 Flash promotional windows.
|
|
16
|
+
- Fix `npm run sync:commandcode-catalog` and `npm run check:commandcode-catalog` on Windows by spawning npm through the shell.
|
|
17
|
+
- Add a `refresh-model-catalog` agent skill with cross-platform helper scripts that snapshot the live model catalog and regenerate the pricing fixture from `MODEL_COSTS`.
|
|
18
|
+
|
|
19
|
+
### Contributors
|
|
20
|
+
|
|
21
|
+
- @warc0s — preserved developer messages on the `/alpha/generate` transport with OMP advisory coverage.
|
|
22
|
+
- @jagaliano — added the quota renewal date and diagnosed the failing daily catalog sync.
|
|
23
|
+
- @ThomasByr — added GLM 5.3 Flash and Qwen 3.8 Flash and contributed the `refresh-model-catalog` skill.
|
|
24
|
+
- @hjshin-ubob — proposed selectable thinking levels for the Muse Spark models.
|
|
25
|
+
- @Sokoshy — analyzed the `commandcode-custom` compat registry failure on plain pi.
|
|
26
|
+
- @CoderTCY — measured and proposed the cache-first catalog startup.
|
|
27
|
+
- @MertSoylu — reported the missing GLM 5.3 Flash effort levels.
|
|
28
|
+
|
|
29
|
+
## 0.6.0 - 2026-08-25
|
|
30
|
+
|
|
31
|
+
- Allow switching from a vision-capable model to a text-only model by omitting historical image tool results while preserving their text output; direct image prompts still fail clearly.
|
|
32
|
+
- Stream incremental tool-call arguments from the `/alpha/generate` transport instead of waiting for the final complete tool-call event.
|
|
33
|
+
- Add a daily GitHub Actions synchronization job that opens or updates a pull request for CLI version, image capability, reasoning, effort, and output-limit changes in the latest published Command Code catalog.
|
|
34
|
+
- Refresh static model capabilities from `command-code@1.32.2`, separating reasoning support from selectable effort levels and honoring model-specific output limits.
|
|
35
|
+
- Reject truncated, aborted, and network-failed generate streams instead of reporting partial responses as successful.
|
|
36
|
+
- Normalize malformed tool results and synthesize missing tool results so follow-up requests preserve valid tool-call history.
|
|
37
|
+
- Refresh display pricing for all 58 current models, including Gemini 3.7 Flash, Qwen 3.8 27B, Ox Alpha, Muse Spark 1.2, and Grok 4.6 long-context rates.
|
|
38
|
+
- Accept the official `COMMAND_CODE_API_KEY` and `CMD_ZDR` environment variables while retaining legacy aliases.
|
|
39
|
+
- Align generate request metadata with the CLI by forwarding stable session IDs, optional temperature, and the CLI user agent.
|
|
40
|
+
- Validate manually pasted API keys, use the CLI's two-minute browser timeout, and reject OAuth state mismatches without closing the callback server.
|
|
41
|
+
- Add `/commandcode-quota` with live credits, plan, usage totals, and rolling-limit diagnostics from Command Code's alpha usage endpoints.
|
|
42
|
+
- Add `zai-org/GLM-5.3` with its verified reasoning efforts and display pricing.
|
|
43
|
+
- Prefer Command Code's Provider API (`/provider/v1/chat/completions` and `/provider/v1/messages`) and automatically fall back to the existing `/alpha/generate` transport only when the Provider API returns `403 upgrade_required` for a Go-plan account.
|
|
44
|
+
- Remember the detected transport for the running process, re-detect it when credentials change, prevent stale in-flight requests from overwriting the new credential's transport, and never fall back for unrelated authentication, permission, rate-limit, network, or server failures.
|
|
45
|
+
- Use Pi's native OpenAI- and Anthropic-compatible providers for Provider API streaming, including adaptive thinking for current reasoning-capable Claude models, while preserving the existing hardened generate transport, dynamic model discovery, offline cache, refresh/status commands, pricing, and OAuth credentials.
|
|
46
|
+
- Let `/login` use browser authentication, an explicit API-key prompt, or a directly pasted API key.
|
|
47
|
+
- Add optional zero-data-retention headers through `CMD_ZDR=1` and the legacy `COMMANDCODE_ZDR=1` alias.
|
|
48
|
+
- Refresh GPT-5.6 Terra and Luna display prices after their temporary 50% promotion ended, and display the current DeepSeek V4 off-peak rates for its time-dependent pricing.
|
|
49
|
+
- Add isolated live E2E profiles for separate Go-, GOAT-, and Provider-plan credentials, covering transport selection, reasoning across turns, quota identity, aborts, tools, GOAT vision, Go image rejection, and packed-package validation.
|
|
50
|
+
- Fix extension load failure on newer pi hosts that reject registering a custom API under a built-in name (`openai-completions`); register under `commandcode-custom` instead and restore the real wire API before native compat dispatch.
|
|
51
|
+
|
|
52
|
+
### Contributors
|
|
53
|
+
|
|
54
|
+
- @jagaliano — added the live quota dashboard and hardened its integration.
|
|
55
|
+
- @omariqbalnaru — fixed custom API registration for Oh My Pi 17.4.0.
|
|
56
|
+
- @ThomasByr — added GLM-5.3 pricing and reasoning levels.
|
|
57
|
+
- @newCman1 — added DeepSeek V4 vision model support.
|
|
58
|
+
|
|
5
59
|
## 0.5.1 - 2026-08-11
|
|
6
60
|
|
|
7
61
|
- Add model-specific image input capabilities from the `command-code@1.15.1` catalog and forward user and tool-result images using the current Command Code wire format.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -42,6 +42,16 @@ npm run pi:authenticated
|
|
|
42
42
|
|
|
43
43
|
Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
|
|
44
44
|
|
|
45
|
+
Run the transport-specific live tests with separate credentials:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key npm run test:e2e:live:go
|
|
49
|
+
COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key npm run test:e2e:live:goat
|
|
50
|
+
COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:live:provider
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets.
|
|
54
|
+
|
|
45
55
|
Before opening a PR, run:
|
|
46
56
|
|
|
47
57
|
```sh
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
A custom provider for [pi](https://github.com/earendil-works/pi) that connects to the [Command Code](https://commandcode.ai) Provider API.
|
|
7
7
|
|
|
8
|
-
> **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account
|
|
8
|
+
> **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account, API key, and a plan with Provider API access. Command Code's terms, availability, and pricing apply.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ Start or reload pi, then authenticate:
|
|
|
19
19
|
/login
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Select **Use a subscription**, then **Command Code**.
|
|
22
|
+
Select **Use a subscription**, then **Command Code**. Choose browser login or paste an API key, then select a model with `/model`.
|
|
23
23
|
|
|
24
24
|
## Oh My Pi
|
|
25
25
|
|
|
@@ -33,9 +33,9 @@ Restart OMP or run `/reload`, then use `/login` and select **Use a subscription*
|
|
|
33
33
|
|
|
34
34
|
## Authentication
|
|
35
35
|
|
|
36
|
-
###
|
|
36
|
+
### Login dialog
|
|
37
37
|
|
|
38
|
-
Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**.
|
|
38
|
+
Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**. Press Enter for browser login, type `key` to open a paste prompt, or paste the API key directly. The selected credential is stored in the host's auth file.
|
|
39
39
|
|
|
40
40
|
<img width="1520" height="554" alt="Select Command Code in pi's login dialog" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" />
|
|
41
41
|
|
|
@@ -44,7 +44,7 @@ If automatic transfer from the browser fails, copy the API key shown by Command
|
|
|
44
44
|
### Environment variable
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
export
|
|
47
|
+
export COMMAND_CODE_API_KEY="user_..."
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
### Auth file
|
|
@@ -82,11 +82,11 @@ Supported examples:
|
|
|
82
82
|
|
|
83
83
|
Open `/model` and select one of the models provided by Command Code. Model availability changes over time and is refreshed from the Provider API when the extension loads.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Other extensions that stream with the active Command Code model, such as background agents or memory workers, use the same connection and the same credentials as the chat, so their requests count against your Command Code usage.
|
|
86
86
|
|
|
87
|
-
Reasoning
|
|
87
|
+
### Reasoning support
|
|
88
88
|
|
|
89
|
-
Reasoning
|
|
89
|
+
Reasoning capability and selectable effort levels follow the official CLI catalog independently. Models can therefore be marked as reasoning-capable even when Command Code chooses their depth automatically. Models with explicit effort support register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid levels. For a few reasoning models the CLI catalog ships no effort levels although the endpoint accepts `reasoning_effort`; `src/commandcode-catalog-overrides.ts` adds a manual level set for those (currently `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor`) on top of the generated catalog, and the tests fail once upstream publishes its own levels so the override gets removed. Pi's native OpenAI- and Anthropic-compatible providers translate the selected level for Provider API accounts; the existing Command Code generate transport sends the matching `reasoning_effort` for Go accounts.
|
|
90
90
|
|
|
91
91
|
List Command Code models from the terminal:
|
|
92
92
|
|
|
@@ -116,12 +116,17 @@ https://api.commandcode.ai/provider/v1/models
|
|
|
116
116
|
|
|
117
117
|
The last successful catalog is cached at `<agent-dir>/commandcode-models.json`. For pi this is `~/.pi/agent/commandcode-models.json` by default. Compatible hosts such as OMP use their own agent directory.
|
|
118
118
|
|
|
119
|
-
If the endpoint is temporarily unavailable, the
|
|
119
|
+
When a valid cache exists, the provider registers the cached catalog immediately and refreshes it from the endpoint in the background, so startup does not wait for the network. The refreshed catalog replaces the cached one as soon as it arrives; `/commandcode-status` reports `source: cache` until then. If the endpoint is temporarily unavailable, the cached catalog stays active. On a first start without a cache, the provider waits for the live catalog; if that fails offline, pi still loads, but Command Code models remain unavailable until the connection is restored and `/commandcode-refresh` succeeds.
|
|
120
120
|
|
|
121
121
|
While pi is running, use these provider commands without restarting:
|
|
122
122
|
|
|
123
123
|
- `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active.
|
|
124
124
|
- `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning.
|
|
125
|
+
- `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, available usage totals, the API key name, and the 5-hour and weekly usage windows.
|
|
126
|
+
|
|
127
|
+
The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
|
|
128
|
+
|
|
129
|
+
Set `CMD_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header. The legacy `COMMANDCODE_ZDR=1` alias remains supported.
|
|
125
130
|
|
|
126
131
|
The following environment variables are intended for tests, local mocks, and compatible API endpoints:
|
|
127
132
|
|
|
@@ -132,15 +137,15 @@ The following environment variables are intended for tests, local mocks, and com
|
|
|
132
137
|
|
|
133
138
|
## Image input
|
|
134
139
|
|
|
135
|
-
The provider advertises image input only for models marked with the `image` input modality in the official Command Code CLI model catalog. The capability snapshot currently follows `command-code@1.
|
|
140
|
+
The provider advertises image input only for models marked with the `image` input modality in the official Command Code CLI model catalog. The capability snapshot currently follows `command-code@1.40.1`; unknown models default to text-only until their upstream metadata is reviewed. A daily GitHub Actions job synchronizes the CLI version, image capabilities, reasoning flags, reasoning efforts, and model-specific output limits with the latest published CLI package and opens or updates a reviewable pull request when they change. Pricing remains manually reviewed because temporary promotions and long-context tiers require explicit review.
|
|
136
141
|
|
|
137
|
-
For vision-capable models, image blocks from user messages and tool results
|
|
142
|
+
For vision-capable models, Pi's native provider adapters forward image blocks from user messages and tool results using the documented OpenAI or Anthropic message schema. Unknown and text-only models remain marked text-only in Pi.
|
|
138
143
|
|
|
139
144
|
## Pricing display
|
|
140
145
|
|
|
141
|
-
The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs.
|
|
146
|
+
The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs. DeepSeek V4 uses time-dependent rates; pi displays the documented off-peak rate, which applies for 17 hours per day.
|
|
142
147
|
|
|
143
|
-
Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
|
|
148
|
+
Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
|
|
144
149
|
|
|
145
150
|
## Update and remove
|
|
146
151
|
|
|
@@ -181,6 +186,29 @@ npm run pi:authenticated
|
|
|
181
186
|
|
|
182
187
|
Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
|
|
183
188
|
|
|
189
|
+
### Live transport tests
|
|
190
|
+
|
|
191
|
+
Keep Go-, GOAT-, and optional Provider-plan test keys in separate secret-manager entries. Pass them through protected files so the keys do not enter shell history:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
|
|
195
|
+
npm run test:e2e:live:go
|
|
196
|
+
|
|
197
|
+
COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key \
|
|
198
|
+
npm run test:e2e:live:goat
|
|
199
|
+
|
|
200
|
+
COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key \
|
|
201
|
+
npm run test:e2e:live:provider
|
|
202
|
+
|
|
203
|
+
COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
|
|
204
|
+
COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key \
|
|
205
|
+
npm run test:e2e:live:all
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Each profile runs with an isolated Pi agent directory and asserts transport selection, reasoning across turns, quota plan identity, abort handling, tool calls, and the packed npm artifact. Go must select `generate` and reject unsupported images; GOAT must select `provider` and complete a live vision request. The profile-specific `*_API_KEY` environment variables are also supported for CI secrets, but key files are preferred for local use.
|
|
209
|
+
|
|
210
|
+
The Go profile defaults to DeepSeek V4 Flash; GOAT defaults to Grok 4.6 because its Provider API stream exposes reasoning consistently across consecutive turns. Override them with `COMMANDCODE_E2E_GO_MODEL`, `COMMANDCODE_E2E_GOAT_MODEL`, or `COMMANDCODE_E2E_PROVIDER_MODEL`. The GOAT vision phase defaults to GPT-5.6 Luna and can be overridden with `COMMANDCODE_E2E_GOAT_VISION_MODEL`. A successful live Anthropic `/provider/v1/messages` test requires a paid account whose plan includes the selected Claude model.
|
|
211
|
+
|
|
184
212
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
|
|
185
213
|
|
|
186
214
|
## License
|
package/index.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command Code provider for pi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* current pi host exposes `registerProvider(name, config)`, including OMP.
|
|
4
|
+
* Uses Command Code's documented Provider API:
|
|
5
|
+
* https://api.commandcode.ai/provider/v1
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
|
|
9
|
+
import {
|
|
10
|
+
registerApiProvider,
|
|
11
|
+
streamSimple as streamNativeProvider,
|
|
12
|
+
type ApiStreamSimpleFunction,
|
|
13
|
+
} from "@earendil-works/pi-ai/compat"
|
|
10
14
|
import {
|
|
11
15
|
getAgentDir,
|
|
12
16
|
type ExtensionAPI,
|
|
@@ -15,97 +19,161 @@ import {
|
|
|
15
19
|
} from "@earendil-works/pi-coding-agent"
|
|
16
20
|
import { join } from "node:path"
|
|
17
21
|
|
|
18
|
-
import {
|
|
22
|
+
import { getConfiguredApiKey } from "./src/api-key.ts"
|
|
23
|
+
import { createStreamCommandCode } from "./src/core.ts"
|
|
19
24
|
import { calculateCommandCodeCost } from "./src/cost.ts"
|
|
20
25
|
import {
|
|
26
|
+
apiForModelId,
|
|
27
|
+
baseUrlForModel,
|
|
21
28
|
DEFAULT_MODELS_URL,
|
|
29
|
+
DEFAULT_PROVIDER_API_BASE,
|
|
22
30
|
getModelsTimeoutMs,
|
|
23
31
|
inputModalitiesForModel,
|
|
32
|
+
loadCachedCommandCodeModels,
|
|
24
33
|
loadCommandCodeModels,
|
|
34
|
+
MODEL_EFFORTS,
|
|
25
35
|
thinkingMetadataForModel,
|
|
26
36
|
type CommandCodeModel,
|
|
27
37
|
} from "./src/models.ts"
|
|
28
38
|
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
|
|
39
|
+
import { normalizeCommandCodeMessage } from "./src/overflow.ts"
|
|
29
40
|
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
|
|
41
|
+
import { registerCommandCodeQuota } from "./src/quota-command.ts"
|
|
30
42
|
import { createCommandCodeRuntime } from "./src/runtime.ts"
|
|
31
|
-
import {
|
|
43
|
+
import { createCommandCodeTransportRouter } from "./src/transport.ts"
|
|
44
|
+
|
|
45
|
+
const COMMAND_CODE_API = "commandcode-custom"
|
|
46
|
+
const COMPAT_SOURCE_ID = "pi-commandcode-provider"
|
|
47
|
+
|
|
48
|
+
function commandCodeHeaders(): Record<string, string> | undefined {
|
|
49
|
+
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
|
|
50
|
+
return { "x-cmd-zdr": "1" }
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
32
54
|
|
|
33
55
|
function createProviderConfig(
|
|
34
56
|
models: readonly CommandCodeModel[],
|
|
35
57
|
apiBase: string,
|
|
36
58
|
streamCommandCode: ProviderConfig["streamSimple"],
|
|
37
59
|
): ProviderConfig {
|
|
60
|
+
const headers = commandCodeHeaders()
|
|
38
61
|
return {
|
|
39
62
|
name: "Command Code",
|
|
40
63
|
baseUrl: apiBase,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// request-time legacy-file fallback for older compatible hosts.
|
|
44
|
-
apiKey: "$COMMANDCODE_API_KEY",
|
|
45
|
-
authHeader: true,
|
|
46
|
-
api: "commandcode-custom",
|
|
64
|
+
apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
|
|
65
|
+
api: COMMAND_CODE_API,
|
|
47
66
|
streamSimple: streamCommandCode,
|
|
48
|
-
headers
|
|
49
|
-
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
50
|
-
"x-cli-environment": "production",
|
|
51
|
-
},
|
|
67
|
+
headers,
|
|
52
68
|
oauth: {
|
|
53
69
|
name: "Command Code",
|
|
54
70
|
login,
|
|
55
71
|
refreshToken,
|
|
56
72
|
getApiKey: getOAuthApiKey,
|
|
57
73
|
},
|
|
58
|
-
models: models.map(
|
|
74
|
+
models: models.map((model) => ({
|
|
75
|
+
id: model.id,
|
|
76
|
+
name: model.name,
|
|
77
|
+
api: COMMAND_CODE_API,
|
|
78
|
+
baseUrl: baseUrlForModel(apiBase, model.api),
|
|
79
|
+
reasoning: model.reasoning,
|
|
80
|
+
...(thinkingMetadataForModel(model.id) ?? {}),
|
|
81
|
+
input: [...inputModalitiesForModel(model.id)],
|
|
82
|
+
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
|
83
|
+
contextWindow: model.contextWindow,
|
|
84
|
+
maxTokens: model.maxTokens,
|
|
85
|
+
headers,
|
|
86
|
+
compat:
|
|
87
|
+
model.api === "openai-completions"
|
|
88
|
+
? {
|
|
89
|
+
supportsStore: false,
|
|
90
|
+
supportsDeveloperRole: false,
|
|
91
|
+
supportsReasoningEffort: MODEL_EFFORTS[model.id] !== undefined,
|
|
92
|
+
maxTokensField: "max_tokens",
|
|
93
|
+
}
|
|
94
|
+
: {
|
|
95
|
+
supportsEagerToolInputStreaming: false,
|
|
96
|
+
supportsLongCacheRetention: false,
|
|
97
|
+
supportsCacheControlOnTools: false,
|
|
98
|
+
supportsToolReferences: false,
|
|
99
|
+
...(model.reasoning ? { forceAdaptiveThinking: true } : {}),
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
59
102
|
}
|
|
60
103
|
}
|
|
61
104
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
name: string
|
|
65
|
-
reasoning: boolean
|
|
66
|
-
contextWindow: number
|
|
67
|
-
maxTokens: number
|
|
68
|
-
}) {
|
|
69
|
-
return {
|
|
70
|
-
id: model.id,
|
|
71
|
-
name: model.name,
|
|
72
|
-
reasoning: model.reasoning,
|
|
73
|
-
...(thinkingMetadataForModel(model.id) ?? {}),
|
|
74
|
-
input: inputModalitiesForModel(model.id),
|
|
75
|
-
cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
|
|
76
|
-
contextWindow: model.contextWindow,
|
|
77
|
-
maxTokens: model.maxTokens,
|
|
78
|
-
} as const
|
|
105
|
+
function legacyApiBase(providerApiBase: string): string {
|
|
106
|
+
return providerApiBase.replace(/\/provider\/v1\/?$/, "")
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
export default async function (pi: ExtensionAPI) {
|
|
82
|
-
const apiBase = process.env.COMMANDCODE_API_BASE ??
|
|
110
|
+
const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_PROVIDER_API_BASE
|
|
83
111
|
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
|
|
84
112
|
const modelsTimeoutMs = getModelsTimeoutMs()
|
|
85
113
|
const modelsCachePath =
|
|
86
114
|
process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
|
|
87
|
-
const
|
|
115
|
+
const streamGenerate = createStreamCommandCode({
|
|
88
116
|
createStream: () => new AssistantMessageEventStream(),
|
|
89
117
|
calculateCost: calculateCommandCodeCost,
|
|
90
|
-
apiBase,
|
|
118
|
+
apiBase: legacyApiBase(apiBase),
|
|
119
|
+
})
|
|
120
|
+
const transport = createCommandCodeTransportRouter({
|
|
121
|
+
createStream: () => new AssistantMessageEventStream(),
|
|
122
|
+
streamProvider: (model, context, options) =>
|
|
123
|
+
streamNativeProvider(
|
|
124
|
+
{ ...model, api: apiForModelId(model.id), compat: model.compatConfig ?? model.compat },
|
|
125
|
+
context,
|
|
126
|
+
options,
|
|
127
|
+
),
|
|
128
|
+
streamGenerate,
|
|
91
129
|
})
|
|
92
130
|
|
|
131
|
+
// pi dispatches the main chat through the registered provider, but sibling
|
|
132
|
+
// extensions that call `streamSimple` from `@earendil-works/pi-ai/compat`
|
|
133
|
+
// with a Command Code model resolve `model.api` through the compat
|
|
134
|
+
// api-registry, which knows nothing about extension providers. Register the
|
|
135
|
+
// custom api there so those calls reach the same transport. The registry
|
|
136
|
+
// resolves no credentials for extension providers, so fall back to the
|
|
137
|
+
// configured key when the caller passes none.
|
|
138
|
+
const compatStream: ApiStreamSimpleFunction = (model, context, options) =>
|
|
139
|
+
transport.stream(
|
|
140
|
+
model,
|
|
141
|
+
context,
|
|
142
|
+
options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
|
|
143
|
+
) as AssistantMessageEventStream
|
|
144
|
+
registerApiProvider(
|
|
145
|
+
{ api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream },
|
|
146
|
+
COMPAT_SOURCE_ID,
|
|
147
|
+
)
|
|
148
|
+
|
|
93
149
|
pi.on("message_end", async (event, ctx) => {
|
|
94
150
|
if (event.message.role !== "assistant") return
|
|
95
151
|
const normalized = normalizeCommandCodeMessage(event.message, ctx.model?.provider)
|
|
96
152
|
return normalized ? { message: normalized.message } : undefined
|
|
97
153
|
})
|
|
98
154
|
|
|
155
|
+
registerCommandCodeQuota(pi, {
|
|
156
|
+
apiBase: legacyApiBase(apiBase),
|
|
157
|
+
headers: commandCodeHeaders(),
|
|
158
|
+
})
|
|
159
|
+
|
|
99
160
|
const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
|
|
100
161
|
endpoint: modelsUrl,
|
|
101
162
|
cachePath: modelsCachePath,
|
|
102
|
-
loadModels: () =>
|
|
163
|
+
loadModels: (signal) =>
|
|
103
164
|
loadCommandCodeModels({
|
|
104
165
|
url: modelsUrl,
|
|
105
166
|
cachePath: modelsCachePath,
|
|
106
167
|
timeoutMs: modelsTimeoutMs,
|
|
168
|
+
signal,
|
|
107
169
|
}),
|
|
108
|
-
|
|
170
|
+
loadCachedModels: () => loadCachedCommandCodeModels(modelsCachePath),
|
|
171
|
+
createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
|
|
172
|
+
getTransport: transport.getTransport,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
pi.on("session_shutdown", () => {
|
|
176
|
+
runtime.dispose()
|
|
109
177
|
})
|
|
110
178
|
|
|
111
179
|
await runtime.initialize()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-commandcode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -29,14 +29,18 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
|
32
|
+
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
|
|
35
35
|
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
|
|
36
36
|
"pi:isolated": "node scripts/pi-isolated.mjs",
|
|
37
37
|
"pi:authenticated": "node scripts/pi-authenticated.mjs",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"check:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest",
|
|
39
|
+
"sync:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest --write",
|
|
40
|
+
"test:quota": "tsx tests/test-quota.ts && tsx tests/test-quota-command.ts",
|
|
41
|
+
"test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
|
|
42
|
+
"test:api-key": "tsx tests/test-api-key.ts",
|
|
43
|
+
"test:models": "tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts",
|
|
40
44
|
"test:runtime": "tsx tests/test-runtime.ts",
|
|
41
45
|
"test:pricing": "tsx tests/test-pricing.ts",
|
|
42
46
|
"test:oauth": "tsx tests/test-oauth.ts",
|
|
@@ -44,11 +48,16 @@
|
|
|
44
48
|
"test:overflow": "tsx tests/test-overflow.ts",
|
|
45
49
|
"test:stream": "tsx tests/test-stream.ts",
|
|
46
50
|
"test:retry": "tsx tests/test-retry.ts",
|
|
51
|
+
"test:transport": "tsx tests/test-transport.ts",
|
|
47
52
|
"test:pi-isolated": "node tests/test-pi-isolated.mjs",
|
|
48
53
|
"test:pi-authenticated": "node tests/test-pi-authenticated.mjs",
|
|
49
54
|
"test:pi-local": "node tests/test-pi-local.mjs",
|
|
50
55
|
"test:smoke": "node tests/test-smoke.mjs",
|
|
51
56
|
"test:e2e:live": "node tests/test-live-e2e.mjs",
|
|
57
|
+
"test:e2e:live:go": "node scripts/live-e2e-profile.mjs go",
|
|
58
|
+
"test:e2e:live:goat": "node scripts/live-e2e-profile.mjs goat",
|
|
59
|
+
"test:e2e:live:provider": "node scripts/live-e2e-profile.mjs provider",
|
|
60
|
+
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs go goat",
|
|
52
61
|
"test:cost": "tsx tests/test-cost.ts"
|
|
53
62
|
},
|
|
54
63
|
"pi": {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { readFile } from "node:fs/promises"
|
|
5
|
+
import { dirname, resolve } from "node:path"
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
|
|
8
|
+
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
9
|
+
const liveTest = resolve(projectDir, "tests", "test-live-e2e.mjs")
|
|
10
|
+
const profiles = process.argv.slice(2)
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
profiles.length === 0 ||
|
|
14
|
+
profiles.some((profile) => profile !== "go" && profile !== "goat" && profile !== "provider")
|
|
15
|
+
) {
|
|
16
|
+
console.error("Usage: node scripts/live-e2e-profile.mjs <go|goat|provider> [go|goat|provider]")
|
|
17
|
+
process.exit(2)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function credentialFor(profile) {
|
|
21
|
+
const prefix =
|
|
22
|
+
profile === "go"
|
|
23
|
+
? "COMMANDCODE_E2E_GO"
|
|
24
|
+
: profile === "goat"
|
|
25
|
+
? "COMMANDCODE_E2E_GOAT"
|
|
26
|
+
: "COMMANDCODE_E2E_PROVIDER"
|
|
27
|
+
const direct = process.env[`${prefix}_API_KEY`]?.trim()
|
|
28
|
+
const file = process.env[`${prefix}_API_KEY_FILE`]
|
|
29
|
+
|
|
30
|
+
if (direct && file)
|
|
31
|
+
throw new Error(`${prefix}_API_KEY and ${prefix}_API_KEY_FILE are mutually exclusive`)
|
|
32
|
+
if (direct) return direct
|
|
33
|
+
if (file) {
|
|
34
|
+
const credential = (await readFile(file, "utf-8")).trim()
|
|
35
|
+
if (credential) return credential
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error(`Set ${prefix}_API_KEY_FILE (recommended) or ${prefix}_API_KEY`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runProfile(profile, apiKey) {
|
|
42
|
+
const modelVariable =
|
|
43
|
+
profile === "go"
|
|
44
|
+
? "COMMANDCODE_E2E_GO_MODEL"
|
|
45
|
+
: profile === "goat"
|
|
46
|
+
? "COMMANDCODE_E2E_GOAT_MODEL"
|
|
47
|
+
: "COMMANDCODE_E2E_PROVIDER_MODEL"
|
|
48
|
+
const model =
|
|
49
|
+
process.env[modelVariable] ??
|
|
50
|
+
(profile === "goat" ? "xai/grok-4.6" : "deepseek/deepseek-v4-flash")
|
|
51
|
+
const env = {
|
|
52
|
+
...process.env,
|
|
53
|
+
COMMAND_CODE_API_KEY: apiKey,
|
|
54
|
+
COMMANDCODE_E2E_MODEL: model,
|
|
55
|
+
COMMANDCODE_E2E_PROFILE: profile,
|
|
56
|
+
}
|
|
57
|
+
delete env.COMMANDCODE_API_KEY
|
|
58
|
+
delete env.COMMANDCODE_E2E_GO_API_KEY
|
|
59
|
+
delete env.COMMANDCODE_E2E_GOAT_API_KEY
|
|
60
|
+
delete env.COMMANDCODE_E2E_PROVIDER_API_KEY
|
|
61
|
+
|
|
62
|
+
return new Promise((resolveRun, reject) => {
|
|
63
|
+
console.log(`[live-e2e:${profile}] model ${model}`)
|
|
64
|
+
const child = spawn(process.execPath, [liveTest], {
|
|
65
|
+
cwd: projectDir,
|
|
66
|
+
env,
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
})
|
|
69
|
+
child.on("error", reject)
|
|
70
|
+
child.on("close", (code, signal) => {
|
|
71
|
+
if (code === 0) {
|
|
72
|
+
resolveRun()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
reject(new Error(`[live-e2e:${profile}] failed (${signal ?? `exit ${code}`})`))
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
for (const profile of profiles) {
|
|
82
|
+
await runProfile(profile, await credentialFor(profile))
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
package/scripts/pi-isolated.mjs
CHANGED
package/src/api-key.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stringValue(value: unknown): string | undefined {
|
|
10
|
+
return typeof value === "string" ? value : undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultAuthPaths(home: string): string[] {
|
|
14
|
+
return [
|
|
15
|
+
join(home, ".commandcode", "auth.json"),
|
|
16
|
+
join(home, ".pi", "agent", "auth.json"),
|
|
17
|
+
join(home, ".omp", "agent", "auth.json"),
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function apiKeyFromCredential(value: unknown): string | undefined {
|
|
22
|
+
if (!isRecord(value)) return undefined
|
|
23
|
+
|
|
24
|
+
if (stringValue(value.type) === "oauth") return stringValue(value.access)
|
|
25
|
+
if (stringValue(value.type) === "api") return stringValue(value.key)
|
|
26
|
+
return stringValue(value.access) ?? stringValue(value.key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getConfiguredApiKey(
|
|
30
|
+
options: {
|
|
31
|
+
env?: NodeJS.ProcessEnv
|
|
32
|
+
authPaths?: readonly string[]
|
|
33
|
+
homeDir?: () => string
|
|
34
|
+
} = {},
|
|
35
|
+
): string | undefined {
|
|
36
|
+
const env = options.env ?? process.env
|
|
37
|
+
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
|
38
|
+
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
|
39
|
+
|
|
40
|
+
const home = options.homeDir?.() ?? homedir()
|
|
41
|
+
const authPaths = options.authPaths ?? defaultAuthPaths(home)
|
|
42
|
+
|
|
43
|
+
for (const authPath of authPaths) {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(authPath)) continue
|
|
46
|
+
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
|
47
|
+
if (!isRecord(parsed)) continue
|
|
48
|
+
|
|
49
|
+
const apiKey = stringValue(parsed.apiKey)
|
|
50
|
+
if (apiKey) return apiKey
|
|
51
|
+
|
|
52
|
+
const commandcode = stringValue(parsed.commandcode)
|
|
53
|
+
if (commandcode) return commandcode
|
|
54
|
+
|
|
55
|
+
const providerKey = apiKeyFromCredential(parsed.commandcode)
|
|
56
|
+
if (providerKey) return providerKey
|
|
57
|
+
|
|
58
|
+
const commandCode = stringValue(parsed["command-code"])
|
|
59
|
+
if (commandCode) return commandCode
|
|
60
|
+
|
|
61
|
+
const commandCodeKey = apiKeyFromCredential(parsed["command-code"])
|
|
62
|
+
if (commandCodeKey) return commandCodeKey
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore malformed or unreadable auth files.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined
|
|
69
|
+
}
|