pi-commandcode-provider 0.5.0 → 0.6.0
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 +39 -0
- package/CONTRIBUTING.md +10 -0
- package/README.md +38 -12
- package/index.ts +73 -41
- 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.ts +141 -0
- package/src/converters.ts +114 -23
- package/src/core.ts +121 -31
- package/src/models.ts +60 -50
- package/src/oauth.ts +84 -22
- package/src/pricing.ts +53 -45
- package/src/quota-command.ts +66 -0
- package/src/quota-format.ts +111 -0
- package/src/quota-types.ts +47 -0
- package/src/quota.ts +335 -0
- package/src/runtime.ts +10 -5
- package/src/transport.ts +140 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export const COMMAND_CODE_CLI_VERSION = "1.32.2"
|
|
2
|
+
|
|
3
|
+
export type CommandCodeInputType = "text" | "image"
|
|
4
|
+
export type CommandCodeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generated from command-code@1.32.2 by `npm run sync:commandcode-catalog`.
|
|
8
|
+
* Do not edit manually.
|
|
9
|
+
*/
|
|
10
|
+
export const MODEL_INPUT_MODALITIES: Readonly<Record<string, readonly CommandCodeInputType[]>> = {
|
|
11
|
+
"claude-fable-5": ["text", "image"],
|
|
12
|
+
"claude-haiku-4-5-20251001": ["text", "image"],
|
|
13
|
+
"claude-opus-4-7": ["text", "image"],
|
|
14
|
+
"claude-opus-4-8": ["text", "image"],
|
|
15
|
+
"claude-opus-5": ["text", "image"],
|
|
16
|
+
"claude-sonnet-4-6": ["text", "image"],
|
|
17
|
+
"claude-sonnet-5": ["text", "image"],
|
|
18
|
+
"deepseek/deepseek-v4-flash-vision-exp": ["text", "image"],
|
|
19
|
+
"google/gemini-3.1-flash-lite": ["text", "image"],
|
|
20
|
+
"google/gemini-3.5-flash": ["text", "image"],
|
|
21
|
+
"google/gemini-3.5-flash-lite": ["text", "image"],
|
|
22
|
+
"google/gemini-3.6-flash": ["text", "image"],
|
|
23
|
+
"google/gemini-3.7-flash": ["text", "image"],
|
|
24
|
+
"gpt-5.3-codex": ["text", "image"],
|
|
25
|
+
"gpt-5.4": ["text", "image"],
|
|
26
|
+
"gpt-5.4-mini": ["text", "image"],
|
|
27
|
+
"gpt-5.5": ["text", "image"],
|
|
28
|
+
"gpt-5.6-luna": ["text", "image"],
|
|
29
|
+
"gpt-5.6-sol": ["text", "image"],
|
|
30
|
+
"gpt-5.6-terra": ["text", "image"],
|
|
31
|
+
"meta/muse-spark-1.1": ["text", "image"],
|
|
32
|
+
"meta/muse-spark-1.2": ["text", "image"],
|
|
33
|
+
"meta/muse-spark-1.2-contributor": ["text", "image"],
|
|
34
|
+
"MiniMaxAI/MiniMax-M3": ["text", "image"],
|
|
35
|
+
"moonshotai/Kimi-K2.5": ["text", "image"],
|
|
36
|
+
"moonshotai/Kimi-K2.6": ["text", "image"],
|
|
37
|
+
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
|
38
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
|
|
39
|
+
"moonshotai/Kimi-K3": ["text", "image"],
|
|
40
|
+
"Qwen/Qwen3.6-Plus": ["text", "image"],
|
|
41
|
+
"Qwen/Qwen3.7-Flash": ["text", "image"],
|
|
42
|
+
"Qwen/Qwen3.7-Plus": ["text", "image"],
|
|
43
|
+
"Qwen/Qwen3.8-27B": ["text", "image"],
|
|
44
|
+
"Qwen/Qwen3.8-Max": ["text", "image"],
|
|
45
|
+
"sakana/fugu-ultra": ["text", "image"],
|
|
46
|
+
"stealth/ox-alpha": ["text", "image"],
|
|
47
|
+
"stepfun/Step-3.7-Flash": ["text", "image"],
|
|
48
|
+
"thinkingmachines/inkling": ["text", "image"],
|
|
49
|
+
"thinkingmachines/inkling-small": ["text", "image"],
|
|
50
|
+
"xai/grok-4.5": ["text", "image"],
|
|
51
|
+
"xiaomi/mimo-v2.5": ["text", "image"],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const MODEL_REASONING: Readonly<Record<string, true>> = {
|
|
55
|
+
"claude-fable-5": true,
|
|
56
|
+
"claude-opus-4-7": true,
|
|
57
|
+
"claude-opus-4-8": true,
|
|
58
|
+
"claude-opus-5": true,
|
|
59
|
+
"claude-sonnet-4-6": true,
|
|
60
|
+
"claude-sonnet-5": true,
|
|
61
|
+
"deepseek/deepseek-v4-flash": true,
|
|
62
|
+
"deepseek/deepseek-v4-flash-vision-exp": true,
|
|
63
|
+
"deepseek/deepseek-v4-pro": true,
|
|
64
|
+
"google/gemini-3.1-flash-lite": true,
|
|
65
|
+
"google/gemini-3.5-flash": true,
|
|
66
|
+
"google/gemini-3.5-flash-lite": true,
|
|
67
|
+
"google/gemini-3.6-flash": true,
|
|
68
|
+
"google/gemini-3.7-flash": true,
|
|
69
|
+
"gpt-5.3-codex": true,
|
|
70
|
+
"gpt-5.4": true,
|
|
71
|
+
"gpt-5.4-mini": true,
|
|
72
|
+
"gpt-5.5": true,
|
|
73
|
+
"gpt-5.6-luna": true,
|
|
74
|
+
"gpt-5.6-sol": true,
|
|
75
|
+
"gpt-5.6-terra": true,
|
|
76
|
+
"meta/muse-spark-1.1": true,
|
|
77
|
+
"meta/muse-spark-1.2": true,
|
|
78
|
+
"meta/muse-spark-1.2-contributor": true,
|
|
79
|
+
"MiniMaxAI/MiniMax-M3": true,
|
|
80
|
+
"moonshotai/Kimi-K2.7-Code": true,
|
|
81
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": true,
|
|
82
|
+
"moonshotai/Kimi-K3": true,
|
|
83
|
+
"nvidia/nemotron-3-ultra-550b-a55b": true,
|
|
84
|
+
"poolside/laguna-s-2.1-free": true,
|
|
85
|
+
"Qwen/Qwen3.6-Max-Preview": true,
|
|
86
|
+
"Qwen/Qwen3.6-Plus": true,
|
|
87
|
+
"Qwen/Qwen3.7-Flash": true,
|
|
88
|
+
"Qwen/Qwen3.7-Max": true,
|
|
89
|
+
"Qwen/Qwen3.7-Plus": true,
|
|
90
|
+
"Qwen/Qwen3.8-27B": true,
|
|
91
|
+
"Qwen/Qwen3.8-Max": true,
|
|
92
|
+
"sakana/fugu-ultra": true,
|
|
93
|
+
"stealth/ox-alpha": true,
|
|
94
|
+
"stepfun/Step-3.5-Flash": true,
|
|
95
|
+
"stepfun/Step-3.7-Flash": true,
|
|
96
|
+
"tencent/hy3-paid": true,
|
|
97
|
+
"thinkingmachines/inkling": true,
|
|
98
|
+
"thinkingmachines/inkling-small": true,
|
|
99
|
+
"xai/grok-4.5": true,
|
|
100
|
+
"xai/grok-4.6": true,
|
|
101
|
+
"zai-org/GLM-5.2": true,
|
|
102
|
+
"zai-org/GLM-5.3": true,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
|
106
|
+
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
107
|
+
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
108
|
+
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
109
|
+
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
110
|
+
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
|
111
|
+
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
112
|
+
"deepseek/deepseek-v4-flash": ["high", "max"],
|
|
113
|
+
"deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
|
|
114
|
+
"deepseek/deepseek-v4-pro": ["high", "max"],
|
|
115
|
+
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
|
116
|
+
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
|
117
|
+
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
|
118
|
+
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
|
119
|
+
"google/gemini-3.7-flash": ["low", "medium", "high"],
|
|
120
|
+
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
|
121
|
+
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
|
122
|
+
"gpt-5.4-mini": ["low", "medium", "high"],
|
|
123
|
+
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
|
124
|
+
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
|
125
|
+
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
126
|
+
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
|
127
|
+
"Qwen/Qwen3.8-27B": ["low", "medium", "xhigh"],
|
|
128
|
+
"Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
|
|
129
|
+
"sakana/fugu-ultra": ["high", "xhigh"],
|
|
130
|
+
"stealth/ox-alpha": ["low", "high", "max"],
|
|
131
|
+
"xai/grok-4.5": ["low", "medium", "high"],
|
|
132
|
+
"xai/grok-4.6": ["low", "medium", "high", "xhigh"],
|
|
133
|
+
"zai-org/GLM-5.2": ["high", "max"],
|
|
134
|
+
"zai-org/GLM-5.3": ["low", "high", "max"],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
|
|
138
|
+
"poolside/laguna-s-2.1-free": 32_768,
|
|
139
|
+
"Qwen/Qwen3.8-27B": 32_768,
|
|
140
|
+
"stealth/ox-alpha": 131_072,
|
|
141
|
+
}
|
package/src/converters.ts
CHANGED
|
@@ -55,26 +55,49 @@ function apiKeyFromCredentialRecord(value: unknown): string | undefined {
|
|
|
55
55
|
return stringValue(value.key) ?? stringValue(value.access)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
if (isRecord(value)) return value.type === "image"
|
|
60
|
-
return recordArray(value).
|
|
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 (
|
|
72
|
-
|
|
73
|
-
throw imageContentError(role)
|
|
69
|
+
if (message.role !== "toolResult" && imageParts(message.content).length > 0) {
|
|
70
|
+
throw imageContentError(`${message.role} messages`)
|
|
74
71
|
}
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
|
|
75
|
+
function imageToCommandCode(part: Record<string, unknown>): Record<string, string> {
|
|
76
|
+
const data = stringValue(part.data)
|
|
77
|
+
const mimeType = stringValue(part.mimeType)
|
|
78
|
+
if (!data || !mimeType)
|
|
79
|
+
throw new Error("Invalid image content: expected base64 data and mimeType")
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
type: "image",
|
|
83
|
+
image: `data:${mimeType};base64,${data}`,
|
|
84
|
+
mimeType,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function userContentToCommandCode(content: unknown, allowImages: boolean): unknown {
|
|
89
|
+
if (typeof content === "string") return content
|
|
90
|
+
|
|
91
|
+
return recordArray(content).flatMap((part) => {
|
|
92
|
+
if (part.type === "text") return [{ type: "text", text: stringValue(part.text) ?? "" }]
|
|
93
|
+
if (part.type === "image") {
|
|
94
|
+
if (!allowImages) throw imageContentError("user messages")
|
|
95
|
+
return [imageToCommandCode(part)]
|
|
96
|
+
}
|
|
97
|
+
return []
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
export function getApiKey(
|
|
79
102
|
options: {
|
|
80
103
|
env?: NodeJS.ProcessEnv
|
|
@@ -83,6 +106,7 @@ export function getApiKey(
|
|
|
83
106
|
} = {},
|
|
84
107
|
): string | undefined {
|
|
85
108
|
const env = options.env ?? process.env
|
|
109
|
+
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
|
86
110
|
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
|
87
111
|
|
|
88
112
|
const home = options.homeDir?.() ?? homedir()
|
|
@@ -114,8 +138,40 @@ export function getApiKey(
|
|
|
114
138
|
return undefined
|
|
115
139
|
}
|
|
116
140
|
|
|
141
|
+
// Hosts such as OMP may pass a literal env-var name as the "resolved" registry
|
|
142
|
+
// key instead of the actual credential. Treat those as unresolved.
|
|
143
|
+
export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([
|
|
144
|
+
"$COMMAND_CODE_API_KEY",
|
|
145
|
+
"COMMAND_CODE_API_KEY",
|
|
146
|
+
"$COMMANDCODE_API_KEY",
|
|
147
|
+
"COMMANDCODE_API_KEY",
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Pick the real API key from a host registry value and/or the env/auth-file
|
|
152
|
+
* fallback, never returning a literal placeholder or an empty/whitespace value.
|
|
153
|
+
* Pure/testable.
|
|
154
|
+
*/
|
|
155
|
+
export function pickCommandCodeApiKey(
|
|
156
|
+
registryKey: string | undefined,
|
|
157
|
+
hostKey: string | undefined,
|
|
158
|
+
): string | undefined {
|
|
159
|
+
const trimmed = typeof registryKey === "string" ? registryKey.trim() : undefined
|
|
160
|
+
if (!trimmed) return hostKey
|
|
161
|
+
if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return hostKey
|
|
162
|
+
return trimmed
|
|
163
|
+
}
|
|
164
|
+
|
|
117
165
|
export function textContent(message: { content?: unknown }): string {
|
|
118
|
-
if (
|
|
166
|
+
if (typeof message.content === "string") return message.content
|
|
167
|
+
if (message.content === null || message.content === undefined) return ""
|
|
168
|
+
if (!Array.isArray(message.content)) {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.stringify(message.content) ?? String(message.content)
|
|
171
|
+
} catch {
|
|
172
|
+
return String(message.content)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
119
175
|
|
|
120
176
|
return recordArray(message.content)
|
|
121
177
|
.filter((part) => part.type === "text")
|
|
@@ -137,7 +193,12 @@ export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
|
|
137
193
|
}))
|
|
138
194
|
}
|
|
139
195
|
|
|
140
|
-
|
|
196
|
+
interface ToolCallState {
|
|
197
|
+
callIds: ReadonlySet<string>
|
|
198
|
+
resultIds: ReadonlySet<string>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function toolCallState(messages?: readonly MessageLike[]): ToolCallState {
|
|
141
202
|
const callIds = new Set<string>()
|
|
142
203
|
const resultIds = new Set<string>()
|
|
143
204
|
|
|
@@ -149,45 +210,68 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
|
|
149
210
|
if (id) callIds.add(id)
|
|
150
211
|
}
|
|
151
212
|
}
|
|
152
|
-
} else if (message.role === "toolResult") {
|
|
153
|
-
|
|
213
|
+
} else if (message.role === "toolResult" && message.toolCallId) {
|
|
214
|
+
resultIds.add(message.toolCallId)
|
|
154
215
|
}
|
|
155
216
|
}
|
|
156
217
|
|
|
157
|
-
return
|
|
218
|
+
return { callIds, resultIds }
|
|
158
219
|
}
|
|
159
220
|
|
|
160
|
-
export function messagesToCC(
|
|
161
|
-
|
|
221
|
+
export function messagesToCC(
|
|
222
|
+
messages?: readonly MessageLike[],
|
|
223
|
+
options: { allowImages?: boolean } = {},
|
|
224
|
+
): unknown[] {
|
|
225
|
+
const allowImages = options.allowImages ?? false
|
|
226
|
+
if (!allowImages) assertTextOnlyMessages(messages)
|
|
162
227
|
|
|
163
228
|
const out: unknown[] = []
|
|
164
|
-
const
|
|
229
|
+
const { callIds, resultIds } = toolCallState(messages)
|
|
165
230
|
|
|
166
231
|
for (const message of messages ?? []) {
|
|
167
232
|
if (message.role === "user") {
|
|
168
233
|
out.push({
|
|
169
234
|
role: "user",
|
|
170
|
-
content:
|
|
235
|
+
content: userContentToCommandCode(message.content, allowImages),
|
|
171
236
|
})
|
|
172
237
|
} else if (message.role === "assistant") {
|
|
173
238
|
const parts: unknown[] = []
|
|
239
|
+
const missingResults: unknown[] = []
|
|
174
240
|
for (const content of recordArray(message.content)) {
|
|
175
241
|
if (content.type === "text") {
|
|
176
242
|
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
|
177
243
|
} else if (content.type === "toolCall") {
|
|
178
244
|
const toolCallId = stringValue(content.id) ?? ""
|
|
179
|
-
|
|
245
|
+
const toolName = stringValue(content.name) ?? ""
|
|
246
|
+
if (!toolCallId) continue
|
|
180
247
|
parts.push({
|
|
181
248
|
type: "tool-call",
|
|
182
249
|
toolCallId,
|
|
183
|
-
toolName
|
|
250
|
+
toolName,
|
|
184
251
|
input: recordOrEmpty(content.arguments),
|
|
185
252
|
})
|
|
253
|
+
if (!resultIds.has(toolCallId)) {
|
|
254
|
+
missingResults.push({
|
|
255
|
+
type: "tool-result",
|
|
256
|
+
toolCallId,
|
|
257
|
+
toolName,
|
|
258
|
+
output: {
|
|
259
|
+
type: "error-text",
|
|
260
|
+
value: "No result — the tool call did not complete (interrupted or lost).",
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
}
|
|
186
264
|
}
|
|
187
265
|
}
|
|
188
266
|
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
|
267
|
+
if (missingResults.length > 0) out.push({ role: "tool", content: missingResults })
|
|
189
268
|
} else if (message.role === "toolResult") {
|
|
190
|
-
if (!message.toolCallId || !
|
|
269
|
+
if (!message.toolCallId || !callIds.has(message.toolCallId)) continue
|
|
270
|
+
const images = imageParts(message.content)
|
|
271
|
+
const text = textContent(message)
|
|
272
|
+
const outputText =
|
|
273
|
+
text ||
|
|
274
|
+
(images.length > 0 && !allowImages ? "[Image omitted: model does not support images]" : "")
|
|
191
275
|
out.push({
|
|
192
276
|
role: "tool",
|
|
193
277
|
content: [
|
|
@@ -196,11 +280,18 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
|
196
280
|
toolCallId: message.toolCallId,
|
|
197
281
|
toolName: message.toolName,
|
|
198
282
|
output: message.isError
|
|
199
|
-
? { type: "error-text", value:
|
|
200
|
-
: { type: "text", value:
|
|
283
|
+
? { type: "error-text", value: outputText }
|
|
284
|
+
: { type: "text", value: outputText },
|
|
201
285
|
},
|
|
202
286
|
],
|
|
203
287
|
})
|
|
288
|
+
|
|
289
|
+
if (images.length > 0 && allowImages) {
|
|
290
|
+
out.push({
|
|
291
|
+
role: "user",
|
|
292
|
+
content: images.map(imageToCommandCode),
|
|
293
|
+
})
|
|
294
|
+
}
|
|
204
295
|
}
|
|
205
296
|
}
|
|
206
297
|
return out
|
package/src/core.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from "node:crypto"
|
|
9
9
|
|
|
10
|
+
import { COMMAND_CODE_CLI_VERSION } from "./commandcode-catalog.ts"
|
|
10
11
|
import { commandCodeErrorMessage, redactCommandCodeErrorText } from "./overflow.ts"
|
|
12
|
+
import { modelSupportsImageInput } from "./models.ts"
|
|
11
13
|
import {
|
|
12
14
|
getApiKey,
|
|
13
15
|
getEnvironmentInfo,
|
|
@@ -42,13 +44,7 @@ export * from "./overflow.ts"
|
|
|
42
44
|
export * from "./types.ts"
|
|
43
45
|
|
|
44
46
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
45
|
-
export
|
|
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
|
|
47
|
+
export { COMMAND_CODE_CLI_VERSION }
|
|
52
48
|
|
|
53
49
|
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
54
50
|
const DEFAULT_MAX_RETRIES = 0
|
|
@@ -152,6 +148,10 @@ function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): strin
|
|
|
152
148
|
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
|
153
149
|
}
|
|
154
150
|
|
|
151
|
+
function isUuid(value: string): boolean {
|
|
152
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
155
|
export function projectSlugFromPath(pathName: string): string {
|
|
156
156
|
const slug = pathName
|
|
157
157
|
.toLowerCase()
|
|
@@ -236,17 +236,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
236
236
|
const stream = deps.createStream()
|
|
237
237
|
|
|
238
238
|
async function run() {
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
// Some hosts pass a literal env-var reference instead of resolving it.
|
|
240
|
+
const PLACEHOLDER_API_KEYS = new Set([
|
|
241
|
+
"$COMMAND_CODE_API_KEY",
|
|
242
|
+
"COMMAND_CODE_API_KEY",
|
|
243
|
+
"$COMMANDCODE_API_KEY",
|
|
244
|
+
"COMMANDCODE_API_KEY",
|
|
245
|
+
])
|
|
244
246
|
const hostKey =
|
|
245
|
-
options?.apiKey &&
|
|
246
|
-
options.apiKey !== LEGACY_API_KEY_REF &&
|
|
247
|
-
options.apiKey !== OLD_API_KEY_REF
|
|
248
|
-
? options.apiKey
|
|
249
|
-
: undefined
|
|
247
|
+
options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
|
|
250
248
|
|
|
251
249
|
const apiKey =
|
|
252
250
|
hostKey ??
|
|
@@ -266,7 +264,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
266
264
|
usage: defaultUsage(),
|
|
267
265
|
stopReason: "error",
|
|
268
266
|
errorMessage:
|
|
269
|
-
"No Command Code API key. Run /login and select Command Code, set
|
|
267
|
+
"No Command Code API key. Run /login and select Command Code, set COMMAND_CODE_API_KEY (or legacy COMMANDCODE_API_KEY), or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
|
|
270
268
|
timestamp: now(),
|
|
271
269
|
}
|
|
272
270
|
stream.push({ type: "error", reason: "error", error: msg })
|
|
@@ -290,6 +288,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
290
288
|
let textBlock: TextContent | undefined
|
|
291
289
|
let currentTextIdx = -1
|
|
292
290
|
let thinkingIdx = -1
|
|
291
|
+
const streamingToolCalls = new Map<
|
|
292
|
+
string,
|
|
293
|
+
{ contentIndex: number; toolCall: ToolCallContent; partialArgs: string }
|
|
294
|
+
>()
|
|
293
295
|
let finished = false
|
|
294
296
|
|
|
295
297
|
const abortUpstream = () => {
|
|
@@ -402,25 +404,81 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
402
404
|
break
|
|
403
405
|
}
|
|
404
406
|
|
|
405
|
-
case "tool-
|
|
407
|
+
case "tool-input-start": {
|
|
406
408
|
endTextBlock()
|
|
407
409
|
endThinking()
|
|
410
|
+
const id = stringValue(event.id)
|
|
411
|
+
if (!id || streamingToolCalls.has(id)) break
|
|
412
|
+
|
|
408
413
|
const toolCall: ToolCallContent = {
|
|
409
414
|
type: "toolCall",
|
|
410
|
-
id
|
|
415
|
+
id,
|
|
411
416
|
name: stringValue(event.toolName) ?? "",
|
|
412
|
-
arguments:
|
|
417
|
+
arguments: {},
|
|
413
418
|
}
|
|
414
419
|
output.content.push(toolCall)
|
|
415
|
-
const
|
|
420
|
+
const contentIndex = output.content.length - 1
|
|
421
|
+
streamingToolCalls.set(id, { contentIndex, toolCall, partialArgs: "" })
|
|
416
422
|
stream.push({
|
|
417
423
|
type: "toolcall_start",
|
|
418
|
-
contentIndex
|
|
424
|
+
contentIndex,
|
|
419
425
|
partial: output,
|
|
420
426
|
})
|
|
427
|
+
break
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case "tool-input-delta": {
|
|
431
|
+
const id = stringValue(event.id)
|
|
432
|
+
const delta = stringValue(event.delta)
|
|
433
|
+
if (!id || delta === undefined) break
|
|
434
|
+
const active = streamingToolCalls.get(id)
|
|
435
|
+
if (!active) break
|
|
436
|
+
|
|
437
|
+
active.partialArgs += delta
|
|
438
|
+
active.toolCall.arguments = recordOrEmpty(active.partialArgs)
|
|
439
|
+
stream.push({
|
|
440
|
+
type: "toolcall_delta",
|
|
441
|
+
contentIndex: active.contentIndex,
|
|
442
|
+
delta,
|
|
443
|
+
partial: output,
|
|
444
|
+
})
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case "tool-input-end": {
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
case "tool-call": {
|
|
453
|
+
endTextBlock()
|
|
454
|
+
endThinking()
|
|
455
|
+
const id = stringValue(event.toolCallId) ?? ""
|
|
456
|
+
const active = streamingToolCalls.get(id)
|
|
457
|
+
const toolCall: ToolCallContent = active?.toolCall ?? {
|
|
458
|
+
type: "toolCall",
|
|
459
|
+
id,
|
|
460
|
+
name: stringValue(event.toolName) ?? "",
|
|
461
|
+
arguments: {},
|
|
462
|
+
}
|
|
463
|
+
toolCall.name = stringValue(event.toolName) ?? toolCall.name
|
|
464
|
+
toolCall.arguments = recordOrEmpty(event.input ?? event.args ?? event.arguments)
|
|
465
|
+
|
|
466
|
+
let contentIndex: number
|
|
467
|
+
if (active) {
|
|
468
|
+
contentIndex = active.contentIndex
|
|
469
|
+
streamingToolCalls.delete(id)
|
|
470
|
+
} else {
|
|
471
|
+
output.content.push(toolCall)
|
|
472
|
+
contentIndex = output.content.length - 1
|
|
473
|
+
stream.push({
|
|
474
|
+
type: "toolcall_start",
|
|
475
|
+
contentIndex,
|
|
476
|
+
partial: output,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
421
479
|
stream.push({
|
|
422
480
|
type: "toolcall_end",
|
|
423
|
-
contentIndex
|
|
481
|
+
contentIndex,
|
|
424
482
|
toolCall,
|
|
425
483
|
partial: output,
|
|
426
484
|
})
|
|
@@ -428,6 +486,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
428
486
|
}
|
|
429
487
|
|
|
430
488
|
case "finish": {
|
|
489
|
+
const rawFinishReason = stringValue(event.rawFinishReason)
|
|
490
|
+
if (
|
|
491
|
+
rawFinishReason &&
|
|
492
|
+
/^(?:network|connection|upstream)[-_\s]?error$/i.test(rawFinishReason)
|
|
493
|
+
) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Provider finished with reason "${rawFinishReason}" — upstream connection failed mid-stream`,
|
|
496
|
+
)
|
|
497
|
+
}
|
|
431
498
|
const usage = commandCodeUsage(event)
|
|
432
499
|
if (usage) {
|
|
433
500
|
const details = commandCodeInputTokenDetails(usage)
|
|
@@ -451,6 +518,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
451
518
|
break
|
|
452
519
|
}
|
|
453
520
|
|
|
521
|
+
case "abort": {
|
|
522
|
+
throw abortError("Request aborted")
|
|
523
|
+
}
|
|
524
|
+
|
|
454
525
|
case "error": {
|
|
455
526
|
const message =
|
|
456
527
|
commandCodeErrorMessage(event.error) ??
|
|
@@ -468,11 +539,16 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
468
539
|
if (controller.signal.aborted) throw abortError("Aborted")
|
|
469
540
|
|
|
470
541
|
const workingDir = cwd()
|
|
471
|
-
const threadId =
|
|
542
|
+
const threadId = options?.sessionId
|
|
543
|
+
? isUuid(options.sessionId)
|
|
544
|
+
? options.sessionId
|
|
545
|
+
: undefined
|
|
546
|
+
: uuid()
|
|
472
547
|
const reasoningEffort = mappedReasoningEffort(model, options)
|
|
473
548
|
const timeoutMs = options?.timeoutMs
|
|
474
549
|
|
|
475
|
-
|
|
550
|
+
const allowImages = modelSupportsImageInput(model.id)
|
|
551
|
+
if (!allowImages) assertTextOnlyMessages(context.messages)
|
|
476
552
|
|
|
477
553
|
let body: unknown = {
|
|
478
554
|
config: {
|
|
@@ -491,12 +567,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
491
567
|
skills: null,
|
|
492
568
|
params: {
|
|
493
569
|
model: model.id,
|
|
494
|
-
messages: messagesToCC(context.messages),
|
|
570
|
+
messages: messagesToCC(context.messages, { allowImages }),
|
|
495
571
|
tools: toolsToJson(context.tools),
|
|
496
572
|
system: systemPromptToText(context.systemPrompt),
|
|
497
573
|
max_tokens: generateMaxTokens(model, options),
|
|
498
|
-
temperature: 0.3,
|
|
499
574
|
stream: true,
|
|
575
|
+
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
500
576
|
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
501
577
|
},
|
|
502
578
|
threadId,
|
|
@@ -526,7 +602,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
526
602
|
"x-cli-environment": "production",
|
|
527
603
|
"x-project-slug": projectSlugFromPath(workingDir),
|
|
528
604
|
"x-taste-learning": "true",
|
|
529
|
-
"x-
|
|
605
|
+
...(options?.sessionId ? { "x-session-id": options.sessionId } : {}),
|
|
606
|
+
"User-Agent": "cli",
|
|
530
607
|
...options?.headers,
|
|
531
608
|
}
|
|
532
609
|
const bodyStr = JSON.stringify(body)
|
|
@@ -639,6 +716,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
639
716
|
const { done, value } = await raceAbort(reader.read(), attemptController.signal)
|
|
640
717
|
if (done) {
|
|
641
718
|
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer))
|
|
719
|
+
if (!finished) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
"Stream ended unexpectedly before completion (no finish event) — response was truncated",
|
|
722
|
+
)
|
|
723
|
+
}
|
|
642
724
|
break
|
|
643
725
|
}
|
|
644
726
|
if (controller.signal.aborted) throw abortError("Aborted")
|
|
@@ -662,7 +744,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
662
744
|
} catch {}
|
|
663
745
|
reader = undefined
|
|
664
746
|
|
|
665
|
-
if (
|
|
747
|
+
if (
|
|
748
|
+
controller.signal.aborted ||
|
|
749
|
+
(streamError instanceof Error && streamError.name === "AbortError")
|
|
750
|
+
) {
|
|
751
|
+
throw streamError
|
|
752
|
+
}
|
|
666
753
|
|
|
667
754
|
// Never retry after visible content was emitted (including timeout mid-stream).
|
|
668
755
|
const canRetry = output.content.length === 0 && attempt < maxRetries
|
|
@@ -699,7 +786,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
699
786
|
}
|
|
700
787
|
}
|
|
701
788
|
} catch (error: unknown) {
|
|
702
|
-
const reason: ErrorReason =
|
|
789
|
+
const reason: ErrorReason =
|
|
790
|
+
controller.signal.aborted || (error instanceof Error && error.name === "AbortError")
|
|
791
|
+
? "aborted"
|
|
792
|
+
: "error"
|
|
703
793
|
output.stopReason = reason
|
|
704
794
|
output.errorMessage =
|
|
705
795
|
reason === "aborted"
|