pi-commandcode-provider 0.5.0 → 0.5.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.1 - 2026-08-11
6
+
7
+ - 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.
8
+ - Update the Command Code client version header to `1.15.1`.
9
+
10
+ ### Contributors
11
+
12
+ - @DiyarD — reported missing vision support for GPT-5.6 Luna, Muse Spark 1.2, and other vision-capable models.
13
+
5
14
  ## 0.5.0 - 2026-08-07
6
15
 
7
16
  - Stop replaying completed assistant reasoning traces to Command Code while preserving visible text and completed tool calls in follow-up request history.
package/README.md CHANGED
@@ -132,9 +132,9 @@ The following environment variables are intended for tests, local mocks, and com
132
132
 
133
133
  ## Image input
134
134
 
135
- This provider currently advertises and accepts **text input only**. The extension uses Command Code's legacy `/alpha/generate` protocol, while the public Provider API documentation describes image parts for its documented `/provider/v1` endpoints. The legacy request path has no documented image-part contract, and the model catalog fixture exposes model IDs and context lengths but no image capability or limit fields.
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.15.1`; unknown models default to text-only until their upstream metadata is reviewed.
136
136
 
137
- To avoid silently dropping or changing image data, the provider rejects image content in user messages and tool results before making a network request. It does not claim image capability or define image-size/count limits. This limitation can be revisited when Command Code documents image parts and limits for the protocol used here.
137
+ For vision-capable models, image blocks from user messages and tool results are forwarded in Command Code's current data-URL wire format. Text-only models reject image content before making a network request instead of silently dropping it.
138
138
 
139
139
  ## Pricing display
140
140
 
package/index.ts CHANGED
@@ -15,16 +15,12 @@ import {
15
15
  } from "@earendil-works/pi-coding-agent"
16
16
  import { join } from "node:path"
17
17
 
18
- import {
19
- COMMAND_CODE_CLI_VERSION,
20
- COMMAND_CODE_INPUT_TYPES,
21
- createStreamCommandCode,
22
- DEFAULT_API_BASE,
23
- } from "./src/core.ts"
18
+ import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
24
19
  import { calculateCommandCodeCost } from "./src/cost.ts"
25
20
  import {
26
21
  DEFAULT_MODELS_URL,
27
22
  getModelsTimeoutMs,
23
+ inputModalitiesForModel,
28
24
  loadCommandCodeModels,
29
25
  thinkingMetadataForModel,
30
26
  type CommandCodeModel,
@@ -75,7 +71,7 @@ function createProviderModel(model: {
75
71
  name: model.name,
76
72
  reasoning: model.reasoning,
77
73
  ...(thinkingMetadataForModel(model.id) ?? {}),
78
- input: COMMAND_CODE_INPUT_TYPES,
74
+ input: inputModalitiesForModel(model.id),
79
75
  cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
80
76
  contextWindow: model.contextWindow,
81
77
  maxTokens: model.maxTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-commandcode-provider",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "pi custom provider for Command Code API (commandcode.ai)",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/converters.ts CHANGED
@@ -55,26 +55,50 @@ function apiKeyFromCredentialRecord(value: unknown): string | undefined {
55
55
  return stringValue(value.key) ?? stringValue(value.access)
56
56
  }
57
57
 
58
- function hasImageContent(value: unknown): boolean {
59
- if (isRecord(value)) return value.type === "image"
60
- return recordArray(value).some((part) => part.type === "image")
58
+ function imageParts(value: unknown): readonly Record<string, unknown>[] {
59
+ if (isRecord(value)) return value.type === "image" ? [value] : []
60
+ return recordArray(value).filter((part) => part.type === "image")
61
61
  }
62
62
 
63
63
  function imageContentError(role: string): Error {
64
- return new Error(
65
- `Command Code does not support image content in ${role}; refusing to send it to avoid lossy handling`,
66
- )
64
+ return new Error(`Selected Command Code model does not support image content in ${role}`)
67
65
  }
68
66
 
69
67
  export function assertTextOnlyMessages(messages?: readonly MessageLike[]): void {
70
68
  for (const message of messages ?? []) {
71
- if (hasImageContent(message.content)) {
69
+ if (imageParts(message.content).length > 0) {
72
70
  const role = message.role === "toolResult" ? "tool results" : `${message.role} messages`
73
71
  throw imageContentError(role)
74
72
  }
75
73
  }
76
74
  }
77
75
 
76
+ function imageToCommandCode(part: Record<string, unknown>): Record<string, string> {
77
+ const data = stringValue(part.data)
78
+ const mimeType = stringValue(part.mimeType)
79
+ if (!data || !mimeType)
80
+ throw new Error("Invalid image content: expected base64 data and mimeType")
81
+
82
+ return {
83
+ type: "image",
84
+ image: `data:${mimeType};base64,${data}`,
85
+ mimeType,
86
+ }
87
+ }
88
+
89
+ function userContentToCommandCode(content: unknown, allowImages: boolean): unknown {
90
+ if (typeof content === "string") return content
91
+
92
+ return recordArray(content).flatMap((part) => {
93
+ if (part.type === "text") return [{ type: "text", text: stringValue(part.text) ?? "" }]
94
+ if (part.type === "image") {
95
+ if (!allowImages) throw imageContentError("user messages")
96
+ return [imageToCommandCode(part)]
97
+ }
98
+ return []
99
+ })
100
+ }
101
+
78
102
  export function getApiKey(
79
103
  options: {
80
104
  env?: NodeJS.ProcessEnv
@@ -115,8 +139,6 @@ export function getApiKey(
115
139
  }
116
140
 
117
141
  export function textContent(message: { content?: unknown }): string {
118
- if (hasImageContent(message.content)) throw imageContentError("tool results")
119
-
120
142
  return recordArray(message.content)
121
143
  .filter((part) => part.type === "text")
122
144
  .map((part) => stringValue(part.text) ?? "")
@@ -157,8 +179,12 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
157
179
  return new Set([...callIds].filter((id) => resultIds.has(id)))
158
180
  }
159
181
 
160
- export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
161
- assertTextOnlyMessages(messages)
182
+ export function messagesToCC(
183
+ messages?: readonly MessageLike[],
184
+ options: { allowImages?: boolean } = {},
185
+ ): unknown[] {
186
+ const allowImages = options.allowImages ?? false
187
+ if (!allowImages) assertTextOnlyMessages(messages)
162
188
 
163
189
  const out: unknown[] = []
164
190
  const pairedToolCallIds = completeToolCallIds(messages)
@@ -167,7 +193,7 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
167
193
  if (message.role === "user") {
168
194
  out.push({
169
195
  role: "user",
170
- content: typeof message.content === "string" ? message.content : message.content,
196
+ content: userContentToCommandCode(message.content, allowImages),
171
197
  })
172
198
  } else if (message.role === "assistant") {
173
199
  const parts: unknown[] = []
@@ -201,6 +227,15 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
201
227
  },
202
228
  ],
203
229
  })
230
+
231
+ const images = imageParts(message.content)
232
+ if (images.length > 0) {
233
+ if (!allowImages) throw imageContentError("tool results")
234
+ out.push({
235
+ role: "user",
236
+ content: images.map(imageToCommandCode),
237
+ })
238
+ }
204
239
  }
205
240
  }
206
241
  return out
package/src/core.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { randomUUID } from "node:crypto"
9
9
 
10
10
  import { commandCodeErrorMessage, redactCommandCodeErrorText } from "./overflow.ts"
11
+ import { modelSupportsImageInput } from "./models.ts"
11
12
  import {
12
13
  getApiKey,
13
14
  getEnvironmentInfo,
@@ -42,13 +43,7 @@ export * from "./overflow.ts"
42
43
  export * from "./types.ts"
43
44
 
44
45
  export const DEFAULT_API_BASE = "https://api.commandcode.ai"
45
- export const COMMAND_CODE_CLI_VERSION = "0.29.0"
46
- /**
47
- * The legacy /alpha/generate request path used by this provider has no
48
- * documented image-part contract. Keep the advertised capability text-only
49
- * until Command Code documents and tests image handling for this endpoint.
50
- */
51
- export const COMMAND_CODE_INPUT_TYPES = ["text"] as const
46
+ export const COMMAND_CODE_CLI_VERSION = "1.15.1"
52
47
 
53
48
  const DEFAULT_GENERATE_MAX_TOKENS = 64_000
54
49
  const DEFAULT_MAX_RETRIES = 0
@@ -472,7 +467,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
472
467
  const reasoningEffort = mappedReasoningEffort(model, options)
473
468
  const timeoutMs = options?.timeoutMs
474
469
 
475
- assertTextOnlyMessages(context.messages)
470
+ const allowImages = modelSupportsImageInput(model.id)
471
+ if (!allowImages) assertTextOnlyMessages(context.messages)
476
472
 
477
473
  let body: unknown = {
478
474
  config: {
@@ -491,7 +487,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
491
487
  skills: null,
492
488
  params: {
493
489
  model: model.id,
494
- messages: messagesToCC(context.messages),
490
+ messages: messagesToCC(context.messages, { allowImages }),
495
491
  tools: toolsToJson(context.tools),
496
492
  system: systemPromptToText(context.systemPrompt),
497
493
  max_tokens: generateMaxTokens(model, options),
package/src/models.ts CHANGED
@@ -7,6 +7,63 @@ export const DEFAULT_MODELS_TIMEOUT_MS = 10_000
7
7
  const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
8
8
  const MODEL_CACHE_VERSION = 1
9
9
 
10
+ export type CommandCodeInputType = "text" | "image"
11
+
12
+ /**
13
+ * Model input modalities from the command-code@1.15.1 bundled catalog.
14
+ * Models omitted here remain text-only so newly discovered IDs never claim
15
+ * image support without upstream evidence.
16
+ */
17
+ export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
18
+ "MiniMaxAI/MiniMax-M3": ["text", "image"],
19
+ "Qwen/Qwen3.6-Plus": ["text", "image"],
20
+ "Qwen/Qwen3.7-Flash": ["text", "image"],
21
+ "Qwen/Qwen3.7-Plus": ["text", "image"],
22
+ "Qwen/Qwen3.8-Max": ["text", "image"],
23
+ "claude-fable-5": ["text", "image"],
24
+ "claude-haiku-4-5-20251001": ["text", "image"],
25
+ "claude-opus-4-7": ["text", "image"],
26
+ "claude-opus-4-8": ["text", "image"],
27
+ "claude-opus-5": ["text", "image"],
28
+ "claude-sonnet-4-6": ["text", "image"],
29
+ "claude-sonnet-5": ["text", "image"],
30
+ "google/gemini-3.1-flash-lite": ["text", "image"],
31
+ "google/gemini-3.5-flash": ["text", "image"],
32
+ "google/gemini-3.5-flash-lite": ["text", "image"],
33
+ "google/gemini-3.6-flash": ["text", "image"],
34
+ "gpt-5.3-codex": ["text", "image"],
35
+ "gpt-5.4": ["text", "image"],
36
+ "gpt-5.4-mini": ["text", "image"],
37
+ "gpt-5.5": ["text", "image"],
38
+ "gpt-5.6-luna": ["text", "image"],
39
+ "gpt-5.6-sol": ["text", "image"],
40
+ "gpt-5.6-terra": ["text", "image"],
41
+ "meta/muse-spark-1.1": ["text", "image"],
42
+ "meta/muse-spark-1.2": ["text", "image"],
43
+ "meta/muse-spark-1.2-contributor": ["text", "image"],
44
+ "moonshotai/Kimi-K2.5": ["text", "image"],
45
+ "moonshotai/Kimi-K2.6": ["text", "image"],
46
+ "moonshotai/Kimi-K2.7-Code": ["text", "image"],
47
+ "moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
48
+ "moonshotai/Kimi-K3": ["text", "image"],
49
+ "sakana/fugu-ultra": ["text", "image"],
50
+ "stepfun/Step-3.7-Flash": ["text", "image"],
51
+ "thinkingmachines/inkling": ["text", "image"],
52
+ "thinkingmachines/inkling-small": ["text", "image"],
53
+ "xai/grok-4.5": ["text", "image"],
54
+ "xiaomi/mimo-v2.5": ["text", "image"],
55
+ }
56
+
57
+ const TEXT_INPUT_ONLY = ["text"] as const
58
+
59
+ export function inputModalitiesForModel(modelId: string): readonly CommandCodeInputType[] {
60
+ return MODEL_INPUT_MODALITIES[modelId] ?? TEXT_INPUT_ONLY
61
+ }
62
+
63
+ export function modelSupportsImageInput(modelId: string): boolean {
64
+ return inputModalitiesForModel(modelId).includes("image")
65
+ }
66
+
10
67
  export type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
11
68
 
12
69
  type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
@@ -15,7 +72,7 @@ type CommandCodeReasoningEffort = Exclude<PiThinkingLevel, "off">
15
72
  * Per-model reasoning efforts supported by Command Code's generate endpoint.
16
73
  *
17
74
  * The Provider API does not expose reasoning metadata. This is an exact
18
- * snapshot of `reasoningEfforts` from the command-code@1.14.1 model catalog
75
+ * snapshot of `reasoningEfforts` from the command-code@1.15.1 model catalog
19
76
  * (`packages/shared/src/model-catalog.ts`, also published in the generated
20
77
  * `dist/bundled/command-code-knowledge/reference/models.md`). Models omitted
21
78
  * here let Command Code choose their reasoning depth, matching the CLI.