pi-commandcode-provider 0.5.1 → 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.
@@ -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
@@ -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,7 +226,7 @@ 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
232
  if (message.role === "user") {
@@ -197,23 +236,42 @@ export function messagesToCC(
197
236
  })
198
237
  } else if (message.role === "assistant") {
199
238
  const parts: unknown[] = []
239
+ const missingResults: unknown[] = []
200
240
  for (const content of recordArray(message.content)) {
201
241
  if (content.type === "text") {
202
242
  parts.push({ type: "text", text: stringValue(content.text) ?? "" })
203
243
  } else if (content.type === "toolCall") {
204
244
  const toolCallId = stringValue(content.id) ?? ""
205
- if (!pairedToolCallIds.has(toolCallId)) continue
245
+ const toolName = stringValue(content.name) ?? ""
246
+ if (!toolCallId) continue
206
247
  parts.push({
207
248
  type: "tool-call",
208
249
  toolCallId,
209
- toolName: stringValue(content.name) ?? "",
250
+ toolName,
210
251
  input: recordOrEmpty(content.arguments),
211
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
+ }
212
264
  }
213
265
  }
214
266
  if (parts.length > 0) out.push({ role: "assistant", content: parts })
267
+ if (missingResults.length > 0) out.push({ role: "tool", content: missingResults })
215
268
  } else if (message.role === "toolResult") {
216
- if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId)) continue
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]" : "")
217
275
  out.push({
218
276
  role: "tool",
219
277
  content: [
@@ -222,15 +280,13 @@ export function messagesToCC(
222
280
  toolCallId: message.toolCallId,
223
281
  toolName: message.toolName,
224
282
  output: message.isError
225
- ? { type: "error-text", value: textContent(message) }
226
- : { type: "text", value: textContent(message) },
283
+ ? { type: "error-text", value: outputText }
284
+ : { type: "text", value: outputText },
227
285
  },
228
286
  ],
229
287
  })
230
288
 
231
- const images = imageParts(message.content)
232
- if (images.length > 0) {
233
- if (!allowImages) throw imageContentError("tool results")
289
+ if (images.length > 0 && allowImages) {
234
290
  out.push({
235
291
  role: "user",
236
292
  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"