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.
@@ -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
- const role = message.role === "toolResult" ? "tool results" : `${message.role} messages`
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
- function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
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
- if (message.toolCallId) resultIds.add(message.toolCallId)
213
+ } else if (message.role === "toolResult" && message.toolCallId) {
214
+ resultIds.add(message.toolCallId)
176
215
  }
177
216
  }
178
217
 
179
- return new Set([...callIds].filter((id) => resultIds.has(id)))
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 pairedToolCallIds = completeToolCallIds(messages)
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
- if (!pairedToolCallIds.has(toolCallId)) continue
250
+ const toolName = stringValue(content.name) ?? ""
251
+ if (!toolCallId) continue
206
252
  parts.push({
207
253
  type: "tool-call",
208
254
  toolCallId,
209
- toolName: stringValue(content.name) ?? "",
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 || !pairedToolCallIds.has(message.toolCallId)) continue
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: textContent(message) }
226
- : { type: "text", value: textContent(message) },
288
+ ? { type: "error-text", value: outputText }
289
+ : { type: "text", value: outputText },
227
290
  },
228
291
  ],
229
292
  })
230
293
 
231
- const images = imageParts(message.content)
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 const COMMAND_CODE_CLI_VERSION = "1.15.1"
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
- // OMP may pass the legacy env-var name "COMMANDCODE_API_KEY" (old pi)
235
- // or "$COMMANDCODE_API_KEY" (new pi) as the apiKey value instead of
236
- // resolving it. Filter out these specific strings.
237
- const LEGACY_API_KEY_REF = "$COMMANDCODE_API_KEY"
238
- const OLD_API_KEY_REF = "COMMANDCODE_API_KEY"
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 the COMMANDCODE_API_KEY env var, or configure ~/.commandcode/auth.json, ~/.pi/agent/auth.json or ~/.omp/agent/auth.json",
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-call": {
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: stringValue(event.toolCallId) ?? "",
415
+ id,
406
416
  name: stringValue(event.toolName) ?? "",
407
- arguments: recordOrEmpty(event.input ?? event.args ?? event.arguments),
417
+ arguments: {},
408
418
  }
409
419
  output.content.push(toolCall)
410
- const idx = output.content.length - 1
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: idx,
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: idx,
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 = uuid()
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-co-flag": "false",
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 (controller.signal.aborted) throw streamError
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 = controller.signal.aborted ? "aborted" : "error"
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"