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/src/auth-server.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface AuthServer {
|
|
|
28
28
|
export interface AuthServerOptions {
|
|
29
29
|
startPort?: number
|
|
30
30
|
portRange?: number
|
|
31
|
+
expectedState?: string
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function listenOnAvailablePort(
|
|
@@ -181,6 +182,12 @@ export async function startAuthServer(options: AuthServerOptions = {}): Promise<
|
|
|
181
182
|
return
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
if (options.expectedState !== undefined && state !== options.expectedState) {
|
|
186
|
+
res.writeHead(403)
|
|
187
|
+
res.end(JSON.stringify({ success: false, error: "Invalid state token" }))
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
184
191
|
res.writeHead(200)
|
|
185
192
|
res.end(JSON.stringify({ success: true }))
|
|
186
193
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const COMMAND_CODE_CLI_VERSION = "1.40.1"
|
|
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.40.1 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-fable-5-1": ["text", "image"],
|
|
13
|
+
"claude-haiku-4-5-20251001": ["text", "image"],
|
|
14
|
+
"claude-opus-4-7": ["text", "image"],
|
|
15
|
+
"claude-opus-4-8": ["text", "image"],
|
|
16
|
+
"claude-opus-5": ["text", "image"],
|
|
17
|
+
"claude-sonnet-4-6": ["text", "image"],
|
|
18
|
+
"claude-sonnet-5": ["text", "image"],
|
|
19
|
+
"deepseek/deepseek-v4-flash-vision-exp": ["text", "image"],
|
|
20
|
+
"google/gemini-3.1-flash-lite": ["text", "image"],
|
|
21
|
+
"google/gemini-3.5-flash": ["text", "image"],
|
|
22
|
+
"google/gemini-3.5-flash-lite": ["text", "image"],
|
|
23
|
+
"google/gemini-3.6-flash": ["text", "image"],
|
|
24
|
+
"google/gemini-3.7-flash": ["text", "image"],
|
|
25
|
+
"gpt-5.3-codex": ["text", "image"],
|
|
26
|
+
"gpt-5.4": ["text", "image"],
|
|
27
|
+
"gpt-5.4-mini": ["text", "image"],
|
|
28
|
+
"gpt-5.5": ["text", "image"],
|
|
29
|
+
"gpt-5.6-luna": ["text", "image"],
|
|
30
|
+
"gpt-5.6-sol": ["text", "image"],
|
|
31
|
+
"gpt-5.6-terra": ["text", "image"],
|
|
32
|
+
"meta/muse-spark-1.1": ["text", "image"],
|
|
33
|
+
"meta/muse-spark-1.2": ["text", "image"],
|
|
34
|
+
"meta/muse-spark-1.2-contributor": ["text", "image"],
|
|
35
|
+
"MiniMaxAI/MiniMax-M3": ["text", "image"],
|
|
36
|
+
"moonshotai/Kimi-K2.5": ["text", "image"],
|
|
37
|
+
"moonshotai/Kimi-K2.6": ["text", "image"],
|
|
38
|
+
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
|
39
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": ["text", "image"],
|
|
40
|
+
"moonshotai/Kimi-K3": ["text", "image"],
|
|
41
|
+
"Qwen/Qwen3.6-Plus": ["text", "image"],
|
|
42
|
+
"Qwen/Qwen3.7-Flash": ["text", "image"],
|
|
43
|
+
"Qwen/Qwen3.7-Plus": ["text", "image"],
|
|
44
|
+
"Qwen/Qwen3.8-27B": ["text", "image"],
|
|
45
|
+
"Qwen/Qwen3.8-Flash": ["text", "image"],
|
|
46
|
+
"Qwen/Qwen3.8-Max": ["text", "image"],
|
|
47
|
+
"sakana/fugu-ultra": ["text", "image"],
|
|
48
|
+
"stepfun/Step-3.7-Flash": ["text", "image"],
|
|
49
|
+
"thinkingmachines/inkling": ["text", "image"],
|
|
50
|
+
"thinkingmachines/inkling-small": ["text", "image"],
|
|
51
|
+
"xai/grok-4.5": ["text", "image"],
|
|
52
|
+
"xiaomi/mimo-v2.5": ["text", "image"],
|
|
53
|
+
"z-ai/glm-5.3-flash": ["text", "image"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MODEL_REASONING: Readonly<Record<string, true>> = {
|
|
57
|
+
"claude-fable-5": true,
|
|
58
|
+
"claude-fable-5-1": true,
|
|
59
|
+
"claude-opus-4-7": true,
|
|
60
|
+
"claude-opus-4-8": true,
|
|
61
|
+
"claude-opus-5": true,
|
|
62
|
+
"claude-sonnet-4-6": true,
|
|
63
|
+
"claude-sonnet-5": true,
|
|
64
|
+
"deepseek/deepseek-v4-flash": true,
|
|
65
|
+
"deepseek/deepseek-v4-flash-fast": true,
|
|
66
|
+
"deepseek/deepseek-v4-flash-vision-exp": true,
|
|
67
|
+
"deepseek/deepseek-v4-pro": true,
|
|
68
|
+
"google/gemini-3.1-flash-lite": true,
|
|
69
|
+
"google/gemini-3.5-flash": true,
|
|
70
|
+
"google/gemini-3.5-flash-lite": true,
|
|
71
|
+
"google/gemini-3.6-flash": true,
|
|
72
|
+
"google/gemini-3.7-flash": true,
|
|
73
|
+
"gpt-5.3-codex": true,
|
|
74
|
+
"gpt-5.4": true,
|
|
75
|
+
"gpt-5.4-mini": true,
|
|
76
|
+
"gpt-5.5": true,
|
|
77
|
+
"gpt-5.6-luna": true,
|
|
78
|
+
"gpt-5.6-sol": true,
|
|
79
|
+
"gpt-5.6-terra": true,
|
|
80
|
+
"meta/muse-spark-1.1": true,
|
|
81
|
+
"meta/muse-spark-1.2": true,
|
|
82
|
+
"meta/muse-spark-1.2-contributor": true,
|
|
83
|
+
"MiniMaxAI/MiniMax-M3": true,
|
|
84
|
+
"moonshotai/Kimi-K2.7-Code": true,
|
|
85
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": true,
|
|
86
|
+
"moonshotai/Kimi-K3": true,
|
|
87
|
+
"nvidia/nemotron-3-ultra-550b-a55b": true,
|
|
88
|
+
"poolside/laguna-s-2.1-free": true,
|
|
89
|
+
"Qwen/Qwen3.6-Max-Preview": true,
|
|
90
|
+
"Qwen/Qwen3.6-Plus": true,
|
|
91
|
+
"Qwen/Qwen3.7-Flash": true,
|
|
92
|
+
"Qwen/Qwen3.7-Max": true,
|
|
93
|
+
"Qwen/Qwen3.7-Plus": true,
|
|
94
|
+
"Qwen/Qwen3.8-27B": true,
|
|
95
|
+
"Qwen/Qwen3.8-Flash": true,
|
|
96
|
+
"Qwen/Qwen3.8-Max": true,
|
|
97
|
+
"sakana/fugu-ultra": true,
|
|
98
|
+
"stepfun/Step-3.5-Flash": true,
|
|
99
|
+
"stepfun/Step-3.7-Flash": true,
|
|
100
|
+
"tencent/hy3-paid": true,
|
|
101
|
+
"tencent/hy4-preview": true,
|
|
102
|
+
"thinkingmachines/inkling": true,
|
|
103
|
+
"thinkingmachines/inkling-small": true,
|
|
104
|
+
"xai/grok-4.5": true,
|
|
105
|
+
"xai/grok-4.6": true,
|
|
106
|
+
"z-ai/glm-5.3-flash": true,
|
|
107
|
+
"zai-org/GLM-5.2": true,
|
|
108
|
+
"zai-org/GLM-5.3": true,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const MODEL_EFFORTS: Readonly<Record<string, readonly CommandCodeReasoningEffort[]>> = {
|
|
112
|
+
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
113
|
+
"claude-fable-5-1": ["low", "medium", "high", "xhigh", "max"],
|
|
114
|
+
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
115
|
+
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
116
|
+
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
117
|
+
"claude-sonnet-4-6": ["low", "medium", "high", "xhigh", "max"],
|
|
118
|
+
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
119
|
+
"deepseek/deepseek-v4-flash": ["high", "max"],
|
|
120
|
+
"deepseek/deepseek-v4-flash-fast": ["low", "high", "max"],
|
|
121
|
+
"deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
|
|
122
|
+
"deepseek/deepseek-v4-pro": ["high", "max"],
|
|
123
|
+
"google/gemini-3.1-flash-lite": ["low", "medium", "high"],
|
|
124
|
+
"google/gemini-3.5-flash": ["low", "medium", "high"],
|
|
125
|
+
"google/gemini-3.5-flash-lite": ["low", "medium", "high"],
|
|
126
|
+
"google/gemini-3.6-flash": ["low", "medium", "high"],
|
|
127
|
+
"google/gemini-3.7-flash": ["low", "medium", "high"],
|
|
128
|
+
"gpt-5.3-codex": ["low", "medium", "high", "xhigh"],
|
|
129
|
+
"gpt-5.4": ["low", "medium", "high", "xhigh"],
|
|
130
|
+
"gpt-5.4-mini": ["low", "medium", "high"],
|
|
131
|
+
"gpt-5.5": ["low", "medium", "high", "xhigh"],
|
|
132
|
+
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
|
133
|
+
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
134
|
+
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
|
135
|
+
"moonshotai/Kimi-K3": ["low", "high", "max"],
|
|
136
|
+
"Qwen/Qwen3.8-27B": ["low", "medium", "xhigh"],
|
|
137
|
+
"Qwen/Qwen3.8-Flash": ["low", "medium", "xhigh"],
|
|
138
|
+
"Qwen/Qwen3.8-Max": ["low", "medium", "xhigh"],
|
|
139
|
+
"sakana/fugu-ultra": ["high", "xhigh"],
|
|
140
|
+
"tencent/hy4-preview": ["low", "medium", "high"],
|
|
141
|
+
"xai/grok-4.5": ["low", "medium", "high"],
|
|
142
|
+
"xai/grok-4.6": ["low", "medium", "high", "xhigh"],
|
|
143
|
+
"z-ai/glm-5.3-flash": ["low", "high", "max"],
|
|
144
|
+
"zai-org/GLM-5.2": ["high", "max"],
|
|
145
|
+
"zai-org/GLM-5.3": ["low", "high", "max"],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const MODEL_MAX_OUTPUT_TOKENS: Readonly<Record<string, number>> = {
|
|
149
|
+
"poolside/laguna-s-2.1-free": 32_768,
|
|
150
|
+
"Qwen/Qwen3.8-27B": 32_768,
|
|
151
|
+
"z-ai/glm-5.3-flash": 131_072,
|
|
152
|
+
}
|
package/src/converters.ts
CHANGED
|
@@ -66,9 +66,8 @@ function imageContentError(role: string): Error {
|
|
|
66
66
|
|
|
67
67
|
export function assertTextOnlyMessages(messages?: readonly MessageLike[]): void {
|
|
68
68
|
for (const message of messages ?? []) {
|
|
69
|
-
if (imageParts(message.content).length > 0) {
|
|
70
|
-
|
|
71
|
-
throw imageContentError(role)
|
|
69
|
+
if (message.role !== "toolResult" && imageParts(message.content).length > 0) {
|
|
70
|
+
throw imageContentError(`${message.role} messages`)
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
}
|
|
@@ -107,6 +106,7 @@ export function getApiKey(
|
|
|
107
106
|
} = {},
|
|
108
107
|
): string | undefined {
|
|
109
108
|
const env = options.env ?? process.env
|
|
109
|
+
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
|
|
110
110
|
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
|
111
111
|
|
|
112
112
|
const home = options.homeDir?.() ?? homedir()
|
|
@@ -138,7 +138,41 @@ export function getApiKey(
|
|
|
138
138
|
return undefined
|
|
139
139
|
}
|
|
140
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
|
+
|
|
141
165
|
export function textContent(message: { content?: unknown }): string {
|
|
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
|
+
}
|
|
175
|
+
|
|
142
176
|
return recordArray(message.content)
|
|
143
177
|
.filter((part) => part.type === "text")
|
|
144
178
|
.map((part) => stringValue(part.text) ?? "")
|
|
@@ -159,7 +193,12 @@ export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
|
|
159
193
|
}))
|
|
160
194
|
}
|
|
161
195
|
|
|
162
|
-
|
|
196
|
+
interface ToolCallState {
|
|
197
|
+
callIds: ReadonlySet<string>
|
|
198
|
+
resultIds: ReadonlySet<string>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function toolCallState(messages?: readonly MessageLike[]): ToolCallState {
|
|
163
202
|
const callIds = new Set<string>()
|
|
164
203
|
const resultIds = new Set<string>()
|
|
165
204
|
|
|
@@ -171,12 +210,12 @@ function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
|
|
171
210
|
if (id) callIds.add(id)
|
|
172
211
|
}
|
|
173
212
|
}
|
|
174
|
-
} else if (message.role === "toolResult") {
|
|
175
|
-
|
|
213
|
+
} else if (message.role === "toolResult" && message.toolCallId) {
|
|
214
|
+
resultIds.add(message.toolCallId)
|
|
176
215
|
}
|
|
177
216
|
}
|
|
178
217
|
|
|
179
|
-
return
|
|
218
|
+
return { callIds, resultIds }
|
|
180
219
|
}
|
|
181
220
|
|
|
182
221
|
export function messagesToCC(
|
|
@@ -187,33 +226,57 @@ export function messagesToCC(
|
|
|
187
226
|
if (!allowImages) assertTextOnlyMessages(messages)
|
|
188
227
|
|
|
189
228
|
const out: unknown[] = []
|
|
190
|
-
const
|
|
229
|
+
const { callIds, resultIds } = toolCallState(messages)
|
|
191
230
|
|
|
192
231
|
for (const message of messages ?? []) {
|
|
193
|
-
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.
|
|
194
238
|
out.push({
|
|
195
239
|
role: "user",
|
|
196
240
|
content: userContentToCommandCode(message.content, allowImages),
|
|
197
241
|
})
|
|
198
242
|
} else if (message.role === "assistant") {
|
|
199
243
|
const parts: unknown[] = []
|
|
244
|
+
const missingResults: unknown[] = []
|
|
200
245
|
for (const content of recordArray(message.content)) {
|
|
201
246
|
if (content.type === "text") {
|
|
202
247
|
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
|
203
248
|
} else if (content.type === "toolCall") {
|
|
204
249
|
const toolCallId = stringValue(content.id) ?? ""
|
|
205
|
-
|
|
250
|
+
const toolName = stringValue(content.name) ?? ""
|
|
251
|
+
if (!toolCallId) continue
|
|
206
252
|
parts.push({
|
|
207
253
|
type: "tool-call",
|
|
208
254
|
toolCallId,
|
|
209
|
-
toolName
|
|
255
|
+
toolName,
|
|
210
256
|
input: recordOrEmpty(content.arguments),
|
|
211
257
|
})
|
|
258
|
+
if (!resultIds.has(toolCallId)) {
|
|
259
|
+
missingResults.push({
|
|
260
|
+
type: "tool-result",
|
|
261
|
+
toolCallId,
|
|
262
|
+
toolName,
|
|
263
|
+
output: {
|
|
264
|
+
type: "error-text",
|
|
265
|
+
value: "No result — the tool call did not complete (interrupted or lost).",
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
}
|
|
212
269
|
}
|
|
213
270
|
}
|
|
214
271
|
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
|
272
|
+
if (missingResults.length > 0) out.push({ role: "tool", content: missingResults })
|
|
215
273
|
} else if (message.role === "toolResult") {
|
|
216
|
-
if (!message.toolCallId || !
|
|
274
|
+
if (!message.toolCallId || !callIds.has(message.toolCallId)) continue
|
|
275
|
+
const images = imageParts(message.content)
|
|
276
|
+
const text = textContent(message)
|
|
277
|
+
const outputText =
|
|
278
|
+
text ||
|
|
279
|
+
(images.length > 0 && !allowImages ? "[Image omitted: model does not support images]" : "")
|
|
217
280
|
out.push({
|
|
218
281
|
role: "tool",
|
|
219
282
|
content: [
|
|
@@ -222,15 +285,13 @@ export function messagesToCC(
|
|
|
222
285
|
toolCallId: message.toolCallId,
|
|
223
286
|
toolName: message.toolName,
|
|
224
287
|
output: message.isError
|
|
225
|
-
? { type: "error-text", value:
|
|
226
|
-
: { type: "text", value:
|
|
288
|
+
? { type: "error-text", value: outputText }
|
|
289
|
+
: { type: "text", value: outputText },
|
|
227
290
|
},
|
|
228
291
|
],
|
|
229
292
|
})
|
|
230
293
|
|
|
231
|
-
|
|
232
|
-
if (images.length > 0) {
|
|
233
|
-
if (!allowImages) throw imageContentError("tool results")
|
|
294
|
+
if (images.length > 0 && allowImages) {
|
|
234
295
|
out.push({
|
|
235
296
|
role: "user",
|
|
236
297
|
content: images.map(imageToCommandCode),
|
package/src/core.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
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"
|
|
11
12
|
import { modelSupportsImageInput } from "./models.ts"
|
|
12
13
|
import {
|
|
@@ -43,7 +44,7 @@ export * from "./overflow.ts"
|
|
|
43
44
|
export * from "./types.ts"
|
|
44
45
|
|
|
45
46
|
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
|
46
|
-
export
|
|
47
|
+
export { COMMAND_CODE_CLI_VERSION }
|
|
47
48
|
|
|
48
49
|
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
|
49
50
|
const DEFAULT_MAX_RETRIES = 0
|
|
@@ -147,6 +148,10 @@ function mappedReasoningEffort(model: ModelLike, options?: StreamOptions): strin
|
|
|
147
148
|
return typeof mapped === "string" && mapped !== "off" ? mapped : undefined
|
|
148
149
|
}
|
|
149
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
|
+
|
|
150
155
|
export function projectSlugFromPath(pathName: string): string {
|
|
151
156
|
const slug = pathName
|
|
152
157
|
.toLowerCase()
|
|
@@ -231,17 +236,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
231
236
|
const stream = deps.createStream()
|
|
232
237
|
|
|
233
238
|
async function run() {
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
])
|
|
239
246
|
const hostKey =
|
|
240
|
-
options?.apiKey &&
|
|
241
|
-
options.apiKey !== LEGACY_API_KEY_REF &&
|
|
242
|
-
options.apiKey !== OLD_API_KEY_REF
|
|
243
|
-
? options.apiKey
|
|
244
|
-
: undefined
|
|
247
|
+
options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
|
|
245
248
|
|
|
246
249
|
const apiKey =
|
|
247
250
|
hostKey ??
|
|
@@ -261,7 +264,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
261
264
|
usage: defaultUsage(),
|
|
262
265
|
stopReason: "error",
|
|
263
266
|
errorMessage:
|
|
264
|
-
"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",
|
|
265
268
|
timestamp: now(),
|
|
266
269
|
}
|
|
267
270
|
stream.push({ type: "error", reason: "error", error: msg })
|
|
@@ -285,6 +288,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
285
288
|
let textBlock: TextContent | undefined
|
|
286
289
|
let currentTextIdx = -1
|
|
287
290
|
let thinkingIdx = -1
|
|
291
|
+
const streamingToolCalls = new Map<
|
|
292
|
+
string,
|
|
293
|
+
{ contentIndex: number; toolCall: ToolCallContent; partialArgs: string }
|
|
294
|
+
>()
|
|
288
295
|
let finished = false
|
|
289
296
|
|
|
290
297
|
const abortUpstream = () => {
|
|
@@ -397,25 +404,81 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
397
404
|
break
|
|
398
405
|
}
|
|
399
406
|
|
|
400
|
-
case "tool-
|
|
407
|
+
case "tool-input-start": {
|
|
401
408
|
endTextBlock()
|
|
402
409
|
endThinking()
|
|
410
|
+
const id = stringValue(event.id)
|
|
411
|
+
if (!id || streamingToolCalls.has(id)) break
|
|
412
|
+
|
|
403
413
|
const toolCall: ToolCallContent = {
|
|
404
414
|
type: "toolCall",
|
|
405
|
-
id
|
|
415
|
+
id,
|
|
406
416
|
name: stringValue(event.toolName) ?? "",
|
|
407
|
-
arguments:
|
|
417
|
+
arguments: {},
|
|
408
418
|
}
|
|
409
419
|
output.content.push(toolCall)
|
|
410
|
-
const
|
|
420
|
+
const contentIndex = output.content.length - 1
|
|
421
|
+
streamingToolCalls.set(id, { contentIndex, toolCall, partialArgs: "" })
|
|
411
422
|
stream.push({
|
|
412
423
|
type: "toolcall_start",
|
|
413
|
-
contentIndex
|
|
424
|
+
contentIndex,
|
|
414
425
|
partial: output,
|
|
415
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
|
+
}
|
|
416
479
|
stream.push({
|
|
417
480
|
type: "toolcall_end",
|
|
418
|
-
contentIndex
|
|
481
|
+
contentIndex,
|
|
419
482
|
toolCall,
|
|
420
483
|
partial: output,
|
|
421
484
|
})
|
|
@@ -423,6 +486,15 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
423
486
|
}
|
|
424
487
|
|
|
425
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
|
+
}
|
|
426
498
|
const usage = commandCodeUsage(event)
|
|
427
499
|
if (usage) {
|
|
428
500
|
const details = commandCodeInputTokenDetails(usage)
|
|
@@ -446,6 +518,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
446
518
|
break
|
|
447
519
|
}
|
|
448
520
|
|
|
521
|
+
case "abort": {
|
|
522
|
+
throw abortError("Request aborted")
|
|
523
|
+
}
|
|
524
|
+
|
|
449
525
|
case "error": {
|
|
450
526
|
const message =
|
|
451
527
|
commandCodeErrorMessage(event.error) ??
|
|
@@ -463,7 +539,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
463
539
|
if (controller.signal.aborted) throw abortError("Aborted")
|
|
464
540
|
|
|
465
541
|
const workingDir = cwd()
|
|
466
|
-
const threadId =
|
|
542
|
+
const threadId = options?.sessionId
|
|
543
|
+
? isUuid(options.sessionId)
|
|
544
|
+
? options.sessionId
|
|
545
|
+
: undefined
|
|
546
|
+
: uuid()
|
|
467
547
|
const reasoningEffort = mappedReasoningEffort(model, options)
|
|
468
548
|
const timeoutMs = options?.timeoutMs
|
|
469
549
|
|
|
@@ -491,8 +571,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
491
571
|
tools: toolsToJson(context.tools),
|
|
492
572
|
system: systemPromptToText(context.systemPrompt),
|
|
493
573
|
max_tokens: generateMaxTokens(model, options),
|
|
494
|
-
temperature: 0.3,
|
|
495
574
|
stream: true,
|
|
575
|
+
...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
496
576
|
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
497
577
|
},
|
|
498
578
|
threadId,
|
|
@@ -522,7 +602,8 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
522
602
|
"x-cli-environment": "production",
|
|
523
603
|
"x-project-slug": projectSlugFromPath(workingDir),
|
|
524
604
|
"x-taste-learning": "true",
|
|
525
|
-
"x-
|
|
605
|
+
...(options?.sessionId ? { "x-session-id": options.sessionId } : {}),
|
|
606
|
+
"User-Agent": "cli",
|
|
526
607
|
...options?.headers,
|
|
527
608
|
}
|
|
528
609
|
const bodyStr = JSON.stringify(body)
|
|
@@ -635,6 +716,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
635
716
|
const { done, value } = await raceAbort(reader.read(), attemptController.signal)
|
|
636
717
|
if (done) {
|
|
637
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
|
+
}
|
|
638
724
|
break
|
|
639
725
|
}
|
|
640
726
|
if (controller.signal.aborted) throw abortError("Aborted")
|
|
@@ -658,7 +744,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
658
744
|
} catch {}
|
|
659
745
|
reader = undefined
|
|
660
746
|
|
|
661
|
-
if (
|
|
747
|
+
if (
|
|
748
|
+
controller.signal.aborted ||
|
|
749
|
+
(streamError instanceof Error && streamError.name === "AbortError")
|
|
750
|
+
) {
|
|
751
|
+
throw streamError
|
|
752
|
+
}
|
|
662
753
|
|
|
663
754
|
// Never retry after visible content was emitted (including timeout mid-stream).
|
|
664
755
|
const canRetry = output.content.length === 0 && attempt < maxRetries
|
|
@@ -695,7 +786,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|
|
695
786
|
}
|
|
696
787
|
}
|
|
697
788
|
} catch (error: unknown) {
|
|
698
|
-
const reason: ErrorReason =
|
|
789
|
+
const reason: ErrorReason =
|
|
790
|
+
controller.signal.aborted || (error instanceof Error && error.name === "AbortError")
|
|
791
|
+
? "aborted"
|
|
792
|
+
: "error"
|
|
699
793
|
output.stopReason = reason
|
|
700
794
|
output.errorMessage =
|
|
701
795
|
reason === "aborted"
|