pi-commandcode-provider 0.6.0 → 0.6.2

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 CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.2 - 2026-09-02
6
+
7
+ - Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves.
8
+ - Run the Oh My Pi compatibility suite against a real `omp` binary in CI as a required check, and assert there that the extension loads against OMP's bundled pi packages.
9
+ - Pin the CI memory benchmark and Oh My Pi jobs to Bun 1.4.0, Node 22.23.2, and pi 0.84.4.
10
+
11
+ ### Contributors
12
+
13
+ - @AmeMizuki — reported the failing `omp plugin install` on Oh My Pi 18.1.2.
14
+
15
+ ## 0.6.1 - 2026-09-01
16
+
17
+ - 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.
18
+ - 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.
19
+ - 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.
20
+ - Assert structural catalog invariants in the model tests so the daily catalog sync no longer fails on every upstream change.
21
+ - Display the monthly renewal date and remaining days in `/commandcode-quota`.
22
+ - 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.
23
+ - 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.
24
+ - 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`.
25
+ - 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.
26
+ - Fix `npm run sync:commandcode-catalog` and `npm run check:commandcode-catalog` on Windows by spawning npm through the shell.
27
+ - 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`.
28
+
29
+ ### Contributors
30
+
31
+ - @warc0s — preserved developer messages on the `/alpha/generate` transport with OMP advisory coverage.
32
+ - @jagaliano — added the quota renewal date and diagnosed the failing daily catalog sync.
33
+ - @ThomasByr — added GLM 5.3 Flash and Qwen 3.8 Flash and contributed the `refresh-model-catalog` skill.
34
+ - @hjshin-ubob — proposed selectable thinking levels for the Muse Spark models.
35
+ - @Sokoshy — analyzed the `commandcode-custom` compat registry failure on plain pi.
36
+ - @CoderTCY — measured and proposed the cache-first catalog startup.
37
+ - @MertSoylu — reported the missing GLM 5.3 Flash effort levels.
38
+
5
39
  ## 0.6.0 - 2026-08-25
6
40
 
7
41
  - 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.
package/CONTRIBUTING.md CHANGED
@@ -52,6 +52,17 @@ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:liv
52
52
 
53
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
54
 
55
+ ### Oh My Pi compatibility
56
+
57
+ `tests/test-omp-compat.mjs` runs the extension inside a real `omp` binary against a mock Command Code API. It skips locally when `omp` is not on `PATH`; CI installs Oh My Pi and runs it as a required check with `OMP_COMPAT_REQUIRED=1`, so a change that only loads on pi fails CI instead of the next `omp plugin install`.
58
+
59
+ To run it locally, point `OMP_BIN` at an omp executable (Oh My Pi needs Bun ≥ 1.3.14):
60
+
61
+ ```sh
62
+ npm install -g @oh-my-pi/pi-coding-agent
63
+ OMP_BIN="$(npm prefix -g)/bin/omp" node tests/test-omp-compat.mjs
64
+ ```
65
+
55
66
  Before opening a PR, run:
56
67
 
57
68
  ```sh
package/README.md CHANGED
@@ -82,9 +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
+ 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
+
85
87
  ### Reasoning support
86
88
 
87
- 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 also register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid levels. 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.
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.
88
90
 
89
91
  List Command Code models from the terminal:
90
92
 
@@ -114,7 +116,7 @@ https://api.commandcode.ai/provider/v1/models
114
116
 
115
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.
116
118
 
117
- If the endpoint is temporarily unavailable, the provider uses the cached catalog. On a first offline start without a cache, pi still loads, but Command Code models remain unavailable until the connection is restored and `/commandcode-refresh` succeeds.
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.
118
120
 
119
121
  While pi is running, use these provider commands without restarting:
120
122
 
@@ -135,7 +137,7 @@ The following environment variables are intended for tests, local mocks, and com
135
137
 
136
138
  ## Image input
137
139
 
138
- 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.32.2`; 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.
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.
139
141
 
140
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.
141
143
 
@@ -205,7 +207,7 @@ COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key \
205
207
 
206
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.
207
209
 
208
- 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`. A successful live Anthropic `/provider/v1/messages` test requires a paid account whose plan includes the selected Claude model.
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.
209
211
 
210
212
  See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
211
213
 
package/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
9
+ import * as piAiCompat from "@earendil-works/pi-ai/compat"
9
10
  import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
10
11
  import {
11
12
  getAgentDir,
@@ -25,6 +26,7 @@ import {
25
26
  DEFAULT_PROVIDER_API_BASE,
26
27
  getModelsTimeoutMs,
27
28
  inputModalitiesForModel,
29
+ loadCachedCommandCodeModels,
28
30
  loadCommandCodeModels,
29
31
  MODEL_EFFORTS,
30
32
  thinkingMetadataForModel,
@@ -37,6 +39,27 @@ import { registerCommandCodeQuota } from "./src/quota-command.ts"
37
39
  import { createCommandCodeRuntime } from "./src/runtime.ts"
38
40
  import { createCommandCodeTransportRouter } from "./src/transport.ts"
39
41
 
42
+ const COMMAND_CODE_API = "commandcode-custom"
43
+ const COMPAT_SOURCE_ID = "pi-commandcode-provider"
44
+
45
+ type CompatStreamFunction = (
46
+ model: Parameters<typeof streamNativeProvider>[0],
47
+ context: Parameters<typeof streamNativeProvider>[1],
48
+ options?: Parameters<typeof streamNativeProvider>[2],
49
+ ) => AssistantMessageEventStream
50
+
51
+ /**
52
+ * pi's compat entrypoint exposes `registerApiProvider`; Oh My Pi maps
53
+ * `@earendil-works/pi-ai/compat` onto its own pi-ai, which lacks that export
54
+ * and registers custom APIs itself inside `registerProvider`. Resolve the
55
+ * function at runtime so the extension loads on both hosts.
56
+ */
57
+ function registerCompatApiProvider(stream: CompatStreamFunction): void {
58
+ const register = (piAiCompat as { registerApiProvider?: unknown }).registerApiProvider
59
+ if (typeof register !== "function") return
60
+ register({ api: COMMAND_CODE_API, stream, streamSimple: stream }, COMPAT_SOURCE_ID)
61
+ }
62
+
40
63
  function commandCodeHeaders(): Record<string, string> | undefined {
41
64
  if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
42
65
  return { "x-cmd-zdr": "1" }
@@ -54,7 +77,7 @@ function createProviderConfig(
54
77
  name: "Command Code",
55
78
  baseUrl: apiBase,
56
79
  apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
57
- api: "commandcode-custom",
80
+ api: COMMAND_CODE_API,
58
81
  streamSimple: streamCommandCode,
59
82
  headers,
60
83
  oauth: {
@@ -66,7 +89,7 @@ function createProviderConfig(
66
89
  models: models.map((model) => ({
67
90
  id: model.id,
68
91
  name: model.name,
69
- api: "commandcode-custom",
92
+ api: COMMAND_CODE_API,
70
93
  baseUrl: baseUrlForModel(apiBase, model.api),
71
94
  reasoning: model.reasoning,
72
95
  ...(thinkingMetadataForModel(model.id) ?? {}),
@@ -120,6 +143,21 @@ export default async function (pi: ExtensionAPI) {
120
143
  streamGenerate,
121
144
  })
122
145
 
146
+ // pi dispatches the main chat through the registered provider, but sibling
147
+ // extensions that call `streamSimple` from `@earendil-works/pi-ai/compat`
148
+ // with a Command Code model resolve `model.api` through the compat
149
+ // api-registry, which knows nothing about extension providers. Register the
150
+ // custom api there so those calls reach the same transport. The registry
151
+ // resolves no credentials for extension providers, so fall back to the
152
+ // configured key when the caller passes none.
153
+ const compatStream: CompatStreamFunction = (model, context, options) =>
154
+ transport.stream(
155
+ model,
156
+ context,
157
+ options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
158
+ ) as AssistantMessageEventStream
159
+ registerCompatApiProvider(compatStream)
160
+
123
161
  pi.on("message_end", async (event, ctx) => {
124
162
  if (event.message.role !== "assistant") return
125
163
  const normalized = normalizeCommandCodeMessage(event.message, ctx.model?.provider)
@@ -134,15 +172,21 @@ export default async function (pi: ExtensionAPI) {
134
172
  const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
135
173
  endpoint: modelsUrl,
136
174
  cachePath: modelsCachePath,
137
- loadModels: () =>
175
+ loadModels: (signal) =>
138
176
  loadCommandCodeModels({
139
177
  url: modelsUrl,
140
178
  cachePath: modelsCachePath,
141
179
  timeoutMs: modelsTimeoutMs,
180
+ signal,
142
181
  }),
182
+ loadCachedModels: () => loadCachedCommandCodeModels(modelsCachePath),
143
183
  createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
144
184
  getTransport: transport.getTransport,
145
185
  })
146
186
 
187
+ pi.on("session_shutdown", () => {
188
+ runtime.dispose()
189
+ })
190
+
147
191
  await runtime.initialize()
148
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-commandcode-provider",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "pi custom provider for Command Code API (commandcode.ai)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -0,0 +1,23 @@
1
+ import type { CommandCodeReasoningEffort } from "./commandcode-catalog.ts"
2
+
3
+ /**
4
+ * Manual reasoning-effort policy for models the official CLI marks as
5
+ * reasoning-capable without publishing selectable efforts.
6
+ *
7
+ * `src/commandcode-catalog.ts` is generated from the CLI package and must stay
8
+ * byte-identical to upstream so the daily drift check works. Entries here are
9
+ * merged over the generated catalog at load time and are not touched by
10
+ * `npm run sync:commandcode-catalog`.
11
+ *
12
+ * Add a model only when the effort parameter is known to be accepted by the
13
+ * Command Code endpoint; remove it once the CLI catalog ships its own efforts.
14
+ */
15
+ export const MODEL_EFFORT_OVERRIDES: Readonly<
16
+ Record<string, readonly CommandCodeReasoningEffort[]>
17
+ > = {
18
+ // Meta Muse Spark: the CLI ships no effort levels, but the endpoint accepts
19
+ // `reasoning_effort` for these models and other hosts expose the same set.
20
+ "meta/muse-spark-1.1": ["minimal", "low", "medium", "high", "xhigh"],
21
+ "meta/muse-spark-1.2": ["minimal", "low", "medium", "high", "xhigh"],
22
+ "meta/muse-spark-1.2-contributor": ["minimal", "low", "medium", "high", "xhigh"],
23
+ }
@@ -1,14 +1,15 @@
1
- export const COMMAND_CODE_CLI_VERSION = "1.32.2"
1
+ export const COMMAND_CODE_CLI_VERSION = "1.40.1"
2
2
 
3
3
  export type CommandCodeInputType = "text" | "image"
4
4
  export type CommandCodeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
5
5
 
6
6
  /**
7
- * Generated from command-code@1.32.2 by `npm run sync:commandcode-catalog`.
7
+ * Generated from command-code@1.40.1 by `npm run sync:commandcode-catalog`.
8
8
  * Do not edit manually.
9
9
  */
10
10
  export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
11
11
  "claude-fable-5": ["text", "image"],
12
+ "claude-fable-5-1": ["text", "image"],
12
13
  "claude-haiku-4-5-20251001": ["text", "image"],
13
14
  "claude-opus-4-7": ["text", "image"],
14
15
  "claude-opus-4-8": ["text", "image"],
@@ -41,24 +42,27 @@ export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCod
41
42
  "Qwen/Qwen3.7-Flash": ["text", "image"],
42
43
  "Qwen/Qwen3.7-Plus": ["text", "image"],
43
44
  "Qwen/Qwen3.8-27B": ["text", "image"],
45
+ "Qwen/Qwen3.8-Flash": ["text", "image"],
44
46
  "Qwen/Qwen3.8-Max": ["text", "image"],
45
47
  "sakana/fugu-ultra": ["text", "image"],
46
- "stealth/ox-alpha": ["text", "image"],
47
48
  "stepfun/Step-3.7-Flash": ["text", "image"],
48
49
  "thinkingmachines/inkling": ["text", "image"],
49
50
  "thinkingmachines/inkling-small": ["text", "image"],
50
51
  "xai/grok-4.5": ["text", "image"],
51
52
  "xiaomi/mimo-v2.5": ["text", "image"],
53
+ "z-ai/glm-5.3-flash": ["text", "image"],
52
54
  }
53
55
 
54
56
  export const MODEL_REASONING: Readonly<Record<string, true>> = {
55
57
  "claude-fable-5": true,
58
+ "claude-fable-5-1": true,
56
59
  "claude-opus-4-7": true,
57
60
  "claude-opus-4-8": true,
58
61
  "claude-opus-5": true,
59
62
  "claude-sonnet-4-6": true,
60
63
  "claude-sonnet-5": true,
61
64
  "deepseek/deepseek-v4-flash": true,
65
+ "deepseek/deepseek-v4-flash-fast": true,
62
66
  "deepseek/deepseek-v4-flash-vision-exp": true,
63
67
  "deepseek/deepseek-v4-pro": true,
64
68
  "google/gemini-3.1-flash-lite": true,
@@ -88,28 +92,32 @@ export const MODEL_REASONING: Readonly<Record<string, true>> = {
88
92
  "Qwen/Qwen3.7-Max": true,
89
93
  "Qwen/Qwen3.7-Plus": true,
90
94
  "Qwen/Qwen3.8-27B": true,
95
+ "Qwen/Qwen3.8-Flash": true,
91
96
  "Qwen/Qwen3.8-Max": true,
92
97
  "sakana/fugu-ultra": true,
93
- "stealth/ox-alpha": true,
94
98
  "stepfun/Step-3.5-Flash": true,
95
99
  "stepfun/Step-3.7-Flash": true,
96
100
  "tencent/hy3-paid": true,
101
+ "tencent/hy4-preview": true,
97
102
  "thinkingmachines/inkling": true,
98
103
  "thinkingmachines/inkling-small": true,
99
104
  "xai/grok-4.5": true,
100
105
  "xai/grok-4.6": true,
106
+ "z-ai/glm-5.3-flash": true,
101
107
  "zai-org/GLM-5.2": true,
102
108
  "zai-org/GLM-5.3": true,
103
109
  }
104
110
 
105
111
  export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
106
112
  "claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
113
+ "claude-fable-5-1": ["low", "medium", "high", "xhigh", "max"],
107
114
  "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
108
115
  "claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
109
116
  "claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
110
117
  "claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
111
118
  "claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
112
119
  "deepseek/deepseek-v4-flash": ["high", "max"],
120
+ "deepseek/deepseek-v4-flash-fast": ["low", "high", "max"],
113
121
  "deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
114
122
  "deepseek/deepseek-v4-pro": ["high", "max"],
115
123
  "google/gemini-3.1-flash-lite": ["low", "medium", "high"],
@@ -124,12 +132,15 @@ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasonin
124
132
  "gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
125
133
  "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
126
134
  "gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
135
+ "moonshotai/Kimi-K3": ["low", "high", "max"],
127
136
  "Qwen/Qwen3.8-27B": ["low", "medium", "xhigh"],
137
+ "Qwen/Qwen3.8-Flash": ["low", "medium", "xhigh"],
128
138
  "Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
129
139
  "sakana/fugu-ultra": ["high", "xhigh"],
130
- "stealth/ox-alpha": ["low", "high", "max"],
140
+ "tencent/hy4-preview": ["low", "medium", "high"],
131
141
  "xai/grok-4.5": ["low", "medium", "high"],
132
142
  "xai/grok-4.6": ["low", "medium", "high", "xhigh"],
143
+ "z-ai/glm-5.3-flash": ["low", "high", "max"],
133
144
  "zai-org/GLM-5.2": ["high", "max"],
134
145
  "zai-org/GLM-5.3": ["low", "high", "max"],
135
146
  }
@@ -137,5 +148,5 @@ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasonin
137
148
  export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
138
149
  "poolside/laguna-s-2.1-free": 32_768,
139
150
  "Qwen/Qwen3.8-27B": 32_768,
140
- "stealth/ox-alpha": 131_072,
151
+ "z-ai/glm-5.3-flash": 131_072,
141
152
  }
package/src/converters.ts CHANGED
@@ -229,7 +229,12 @@ export function messagesToCC(
229
229
  const { callIds, resultIds } = toolCallState(messages)
230
230
 
231
231
  for (const message of messages ?? []) {
232
- if (message.role === "user") {
232
+ if (message.role === "user" || message.role === "developer") {
233
+ // Hosts such as OMP steer the agent by injecting developer-role messages
234
+ // (advisor notes, reminders, nudges) mid-conversation. /alpha/generate
235
+ // only accepts user, assistant, and tool roles, so degrade the role to
236
+ // user instead of dropping the message. Content and chronological
237
+ // position are preserved; system-prompt hoisting would change semantics.
233
238
  out.push({
234
239
  role: "user",
235
240
  content: userContentToCommandCode(message.content, allowImages),
package/src/models.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
2
2
  import { dirname } from "node:path"
3
3
 
4
+ import { MODEL_EFFORT_OVERRIDES } from "./commandcode-catalog-overrides.ts"
4
5
  import {
5
- MODEL_EFFORTS,
6
+ MODEL_EFFORTS as CATALOG_MODEL_EFFORTS,
6
7
  MODEL_INPUT_MODALITIES,
7
8
  MODEL_MAX_OUTPUT_TOKENS,
8
9
  MODEL_REASONING,
@@ -10,7 +11,13 @@ import {
10
11
  type CommandCodeReasoningEffort,
11
12
  } from "./commandcode-catalog.ts"
12
13
 
13
- export { MODEL_EFFORTS, MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, MODEL_REASONING }
14
+ /** Upstream CLI efforts with the manual overrides merged over them. */
15
+ export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
16
+ ...CATALOG_MODEL_EFFORTS,
17
+ ...MODEL_EFFORT_OVERRIDES,
18
+ }
19
+
20
+ export { MODEL_INPUT_MODALITIES, MODEL_MAX_OUTPUT_TOKENS, MODEL_REASONING }
14
21
  export type { CommandCodeInputType }
15
22
 
16
23
  export const DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1"
@@ -332,6 +339,17 @@ async function readCommandCodeModelsCache(cachePath: string): Promise<readonly C
332
339
  return commandCodeModelsFromCache(parsed)
333
340
  }
334
341
 
342
+ /** Reads the cached catalog without touching the network; empty when missing or invalid. */
343
+ export async function loadCachedCommandCodeModels(
344
+ cachePath: string,
345
+ ): Promise<readonly CommandCodeModel[]> {
346
+ try {
347
+ return await readCommandCodeModelsCache(cachePath)
348
+ } catch {
349
+ return []
350
+ }
351
+ }
352
+
335
353
  async function writeCommandCodeModelsCache(
336
354
  cachePath: string,
337
355
  models: readonly CommandCodeModel[],
package/src/pricing.ts CHANGED
@@ -20,7 +20,7 @@ export interface TemporaryPricing {
20
20
  }
21
21
 
22
22
  export const PRICING_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits"
23
- export const PRICING_LAST_VERIFIED = "2026-08-25"
23
+ export const PRICING_LAST_VERIFIED = "2026-09-01"
24
24
 
25
25
  export const ZERO_MODEL_COST: CommandCodeModelCost = {
26
26
  input: 0,
@@ -40,10 +40,10 @@ export const ZERO_MODEL_COST: CommandCodeModelCost = {
40
40
  export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
41
41
  // Free models
42
42
  "poolside/laguna-s-2.1-free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
43
- "stealth/ox-alpha": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
44
43
 
45
44
  // Open and open-weight models
46
45
  "tencent/hy3-paid": { input: 0.14, output: 0.58, cacheRead: 0.035, cacheWrite: 0 },
46
+ "tencent/hy4-preview": { input: 0.834, output: 2.501, cacheRead: 0.042, cacheWrite: 0 },
47
47
  "moonshotai/Kimi-K3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
48
48
  "moonshotai/Kimi-K2.7-Code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
49
49
  "moonshotai/Kimi-K2.7-Code-Highspeed": {
@@ -54,6 +54,7 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
54
54
  },
55
55
  "moonshotai/Kimi-K2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
56
56
  "moonshotai/Kimi-K2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
57
+ "z-ai/glm-5.3-flash": { input: 0.15, output: 0.5, cacheRead: 0.03, cacheWrite: 0 },
57
58
  "zai-org/GLM-5.3": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
58
59
  "zai-org/GLM-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
59
60
  "zai-org/GLM-5.2-Fast": { input: 3, output: 10.25, cacheRead: 0.5, cacheWrite: 0 },
@@ -82,8 +83,15 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
82
83
  cacheRead: 0.007,
83
84
  cacheWrite: 0,
84
85
  },
86
+ "deepseek/deepseek-v4-flash-fast": {
87
+ input: 0.28,
88
+ output: 0.56,
89
+ cacheRead: 0.07,
90
+ cacheWrite: 0,
91
+ },
85
92
  "Qwen/Qwen3.8-Max": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
86
93
  "Qwen/Qwen3.8-27B": { input: 0.4, output: 3, cacheRead: 0.04, cacheWrite: 0 },
94
+ "Qwen/Qwen3.8-Flash": { input: 0.16, output: 0.47, cacheRead: 0.016, cacheWrite: 0 },
87
95
  "Qwen/Qwen3.7-Max": { input: 2.5, output: 7.5, cacheRead: 0.5, cacheWrite: 3.13 },
88
96
  "Qwen/Qwen3.7-Plus": {
89
97
  input: 0.4,
@@ -158,9 +166,9 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
158
166
  },
159
167
 
160
168
  // Anthropic
161
- // Introductory pricing through 2026-08-31.
162
169
  "claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
163
170
  "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
171
+ "claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
164
172
  "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
165
173
  "claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
166
174
  "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -183,10 +191,10 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
183
191
 
184
192
  // Google and xAI
185
193
  "google/gemini-3.7-flash": {
186
- input: 0.75,
187
- output: 3.75,
188
- cacheRead: 0.075,
189
- cacheWrite: 0.04167,
194
+ input: 1.5,
195
+ output: 7.5,
196
+ cacheRead: 0.15,
197
+ cacheWrite: 0.08334,
190
198
  },
191
199
  "google/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
192
200
  "google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
@@ -220,15 +228,4 @@ export const MODEL_COSTS: Readonly<Record<string, CommandCodeModelCost>> = {
220
228
  },
221
229
  }
222
230
 
223
- export const TEMPORARY_PRICING: readonly TemporaryPricing[] = [
224
- {
225
- models: ["claude-sonnet-5"],
226
- expiresOn: "2026-08-31",
227
- description: "introductory pricing",
228
- },
229
- {
230
- models: ["google/gemini-3.7-flash"],
231
- expiresOn: "2026-12-31",
232
- description: "50% promotional pricing",
233
- },
234
- ]
231
+ export const TEMPORARY_PRICING: readonly TemporaryPricing[] = []
@@ -49,10 +49,42 @@ function creditsDetail(credits: CommandCodeCredits | null): string | undefined {
49
49
  return `Sources: ${parts.join(" / ")}`
50
50
  }
51
51
 
52
- function subscriptionLine(subscription: CommandCodeSubscription): string {
52
+ function parsePeriodEnd(value: string): Date | null {
53
+ const trimmed = value.trim()
54
+ const timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed)
55
+ if (!Number.isFinite(timestamp) || timestamp < 0) return null
56
+ const milliseconds = timestamp >= 1e12 ? timestamp : timestamp * 1000
57
+ const date = new Date(milliseconds)
58
+ return Number.isNaN(date.getTime()) ? null : date
59
+ }
60
+
61
+ function subscriptionLine(
62
+ subscription: CommandCodeSubscription,
63
+ now: () => number = Date.now,
64
+ ): string {
53
65
  const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim()
54
66
  const status = subscription.status ? ` (${subscription.status})` : ""
55
- return `Plan: ${plan}${status}`
67
+ let renewal = ""
68
+ if (subscription.currentPeriodEnd) {
69
+ const end = parsePeriodEnd(subscription.currentPeriodEnd)
70
+ if (end) {
71
+ const diffMs = end.getTime() - now()
72
+ const days = Math.ceil(diffMs / 86_400_000)
73
+ const dateStr = end.toLocaleDateString("en-US", {
74
+ month: "short",
75
+ day: "numeric",
76
+ timeZone: "UTC",
77
+ })
78
+ if (days > 0) {
79
+ renewal = ` · renews ${dateStr} (${days}d)`
80
+ } else if (days === 0) {
81
+ renewal = ` · renews ${dateStr} (today)`
82
+ } else {
83
+ renewal = ` · renewed ${dateStr}`
84
+ }
85
+ }
86
+ }
87
+ return `Plan: ${plan}${status}${renewal}`
56
88
  }
57
89
 
58
90
  function formatTokens(tokens: number): string {
@@ -77,7 +109,7 @@ export function formatQuota(quota: CommandCodeQuota, now: () => number = Date.no
77
109
 
78
110
  const detail = creditsDetail(quota.credits)
79
111
  if (detail) lines.push(detail)
80
- if (quota.subscription) lines.push(subscriptionLine(quota.subscription))
112
+ if (quota.subscription) lines.push(subscriptionLine(quota.subscription, now))
81
113
 
82
114
  if (quota.summary) {
83
115
  lines.push("")
package/src/quota.ts CHANGED
@@ -43,6 +43,13 @@ function stringValue(value: unknown): string | undefined {
43
43
  return typeof value === "string" && value.length > 0 ? value : undefined
44
44
  }
45
45
 
46
+ function timestampValue(value: unknown): string | undefined {
47
+ const text = stringValue(value)
48
+ if (text) return text
49
+ const number = numberValue(value)
50
+ return number === undefined ? undefined : String(number)
51
+ }
52
+
46
53
  function errorMessage(error: unknown): string {
47
54
  return error instanceof Error ? error.message : String(error)
48
55
  }
@@ -100,8 +107,8 @@ function parseSubscription(value: unknown): CommandCodeSubscription | null {
100
107
  const data = value.data
101
108
  const planId = stringValue(data.planId)
102
109
  const status = stringValue(data.status)
103
- const currentPeriodStart = stringValue(data.currentPeriodStart)
104
- const currentPeriodEnd = stringValue(data.currentPeriodEnd)
110
+ const currentPeriodStart = timestampValue(data.currentPeriodStart)
111
+ const currentPeriodEnd = timestampValue(data.currentPeriodEnd)
105
112
  if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null
106
113
  return {
107
114
  planId: planId ?? null,
package/src/runtime.ts CHANGED
@@ -26,7 +26,9 @@ export interface CommandCodeRuntimeApi<
26
26
  export interface CommandCodeRuntimeOptions<TProviderConfig> {
27
27
  endpoint: string
28
28
  cachePath: string
29
- loadModels: () => Promise<LoadCommandCodeModelsResult>
29
+ loadModels: (signal: AbortSignal) => Promise<LoadCommandCodeModelsResult>
30
+ /** Cached catalog only; resolves to an empty list when no valid cache exists. */
31
+ loadCachedModels: () => Promise<readonly CommandCodeModel[]>
30
32
  createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
31
33
  getTransport?: () => "unknown" | "provider" | "generate"
32
34
  now?: () => number
@@ -108,6 +110,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
108
110
  private status: CommandCodeRuntimeStatus
109
111
  private providerRegistered = false
110
112
  private refreshPromise: Promise<CommandCodeRefreshResult> | undefined
113
+ private readonly shutdown = new AbortController()
111
114
 
112
115
  constructor(
113
116
  private readonly pi: CommandCodeRuntimeApi<TProviderConfig, TContext>,
@@ -133,9 +136,34 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
133
136
  }
134
137
  }
135
138
 
139
+ /**
140
+ * Registers the cached catalog immediately and refreshes it in the
141
+ * background so host startup does not wait for the network. Without a
142
+ * valid cache the live refresh is awaited so models are available at once.
143
+ */
136
144
  async initialize(): Promise<void> {
137
145
  this.registerCommands()
138
- await this.refresh()
146
+
147
+ const cached = await this.options.loadCachedModels()
148
+ if (cached.length === 0) {
149
+ await this.refresh()
150
+ return
151
+ }
152
+
153
+ this.pi.registerProvider("commandcode", this.options.createProviderConfig(cached))
154
+ this.providerRegistered = true
155
+ this.status = {
156
+ ...this.status,
157
+ source: "cache",
158
+ modelCount: cached.length,
159
+ lastSuccess: this.now(),
160
+ }
161
+ void this.refresh()
162
+ }
163
+
164
+ /** Aborts any background refresh so a stopping host does not wait for the network. */
165
+ dispose(): void {
166
+ this.shutdown.abort(new Error("Command Code provider shut down"))
139
167
  }
140
168
 
141
169
  refresh(): Promise<CommandCodeRefreshResult> {
@@ -156,7 +184,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
156
184
  }
157
185
 
158
186
  try {
159
- const loaded = await this.options.loadModels()
187
+ const loaded = await this.options.loadModels(this.shutdown.signal)
160
188
  const warning = loaded.warning ? redactDiagnosticText(loaded.warning) : undefined
161
189
 
162
190
  const shouldRegister =
@@ -217,6 +245,14 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
217
245
  warning: preservedWarning,
218
246
  }
219
247
  } catch (error) {
248
+ if (this.shutdown.signal.aborted) {
249
+ this.status = { ...this.status, refreshing: false }
250
+ return {
251
+ refreshed: false,
252
+ source: this.status.source,
253
+ modelCount: this.status.modelCount,
254
+ }
255
+ }
220
256
  const warning = redactDiagnosticText(
221
257
  `Could not refresh the Command Code model catalog: ${errorMessage(error)}`,
222
258
  )