@aexol/spectral 0.9.81 → 0.9.82

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.
Files changed (49) hide show
  1. package/dist/agent/index.js +2 -2
  2. package/dist/memory/tools/compact-context.d.ts.map +1 -1
  3. package/dist/memory/tools/compact-context.js +7 -4
  4. package/dist/sdk/agent-core/harness/agent-harness.js +1 -1
  5. package/dist/sdk/agent-core/harness/types.d.ts +2 -0
  6. package/dist/sdk/agent-core/harness/types.d.ts.map +1 -1
  7. package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
  8. package/dist/sdk/ai/providers/openai-completions.js +435 -92
  9. package/dist/sdk/ai/types.d.ts +4 -0
  10. package/dist/sdk/ai/types.d.ts.map +1 -1
  11. package/dist/sdk/coding-agent/core/agent-session.d.ts +1 -1
  12. package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
  13. package/dist/sdk/coding-agent/core/agent-session.js +1 -1
  14. package/dist/sdk/coding-agent/core/extensions/runner.d.ts +3 -1
  15. package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
  16. package/dist/sdk/coding-agent/core/extensions/runner.js +6 -2
  17. package/dist/sdk/coding-agent/core/extensions/types.d.ts +2 -0
  18. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  19. package/dist/sdk/coding-agent/core/sdk.d.ts +2 -2
  20. package/dist/sdk/coding-agent/core/sdk.js +2 -2
  21. package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
  22. package/dist/sdk/coding-agent/core/system-prompt.js +22 -4
  23. package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +27 -0
  24. package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -0
  25. package/dist/sdk/coding-agent/core/tools/apply-patch.js +353 -0
  26. package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +1 -6
  27. package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
  28. package/dist/sdk/coding-agent/core/tools/edit-diff.js +179 -100
  29. package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
  30. package/dist/sdk/coding-agent/core/tools/edit.js +9 -5
  31. package/dist/sdk/coding-agent/core/tools/index.d.ts +4 -1
  32. package/dist/sdk/coding-agent/core/tools/index.d.ts.map +1 -1
  33. package/dist/sdk/coding-agent/core/tools/index.js +11 -0
  34. package/dist/sdk/coding-agent/index.d.ts +1 -1
  35. package/dist/sdk/coding-agent/index.d.ts.map +1 -1
  36. package/dist/sdk/coding-agent/index.js +1 -1
  37. package/dist/server/agent-bridge.d.ts.map +1 -1
  38. package/dist/server/agent-bridge.js +274 -54
  39. package/dist/server/handlers/queue.d.ts +3 -11
  40. package/dist/server/handlers/queue.d.ts.map +1 -1
  41. package/dist/server/handlers/queue.js +48 -24
  42. package/dist/server/session-stream.d.ts.map +1 -1
  43. package/dist/server/session-stream.js +43 -9
  44. package/dist/server/storage.d.ts +2 -1
  45. package/dist/server/storage.d.ts.map +1 -1
  46. package/dist/server/storage.js +16 -8
  47. package/dist/server/wire.d.ts +11 -1
  48. package/dist/server/wire.d.ts.map +1 -1
  49. package/package.json +1 -1
@@ -5,7 +5,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
5
5
  import { headersToRecord } from "../utils/headers.js";
6
6
  import { parseStreamingJson } from "../utils/json-parse.js";
7
7
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
8
- import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
8
+ import { buildCopilotDynamicHeaders, hasCopilotVisionInput, } from "./github-copilot-headers.js";
9
9
  import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
10
10
  import { buildBaseOptions } from "./simple-options.js";
11
11
  import { transformMessages } from "./transform-messages.js";
@@ -39,8 +39,18 @@ function isToolCallBlock(block) {
39
39
  function isImageContentBlock(block) {
40
40
  return block.type === "image";
41
41
  }
42
+ const SAFE_REASONING_REPLAY_FIELDS = [
43
+ "reasoning_content",
44
+ "reasoning",
45
+ "reasoning_text",
46
+ "reasoning_summary",
47
+ "reasoning_summary_text",
48
+ "summary_text",
49
+ "summary",
50
+ ];
42
51
  function isOpenRouterModel(model) {
43
- return model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai");
52
+ return model.provider === "openrouter" ||
53
+ model.baseUrl.includes("openrouter.ai");
44
54
  }
45
55
  function isOpenRouterAnthropicModel(model) {
46
56
  return isOpenRouterModel(model) && model.id.startsWith("anthropic/");
@@ -52,11 +62,206 @@ function resolveCacheRetention(cacheRetention, model) {
52
62
  if (model && isOpenRouterAnthropicModel(model)) {
53
63
  return "long";
54
64
  }
55
- if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") {
65
+ if (typeof process !== "undefined" &&
66
+ process.env.PI_CACHE_RETENTION === "long") {
56
67
  return "long";
57
68
  }
58
69
  return "short";
59
70
  }
71
+ function asRecord(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value)
73
+ ? value
74
+ : undefined;
75
+ }
76
+ function collectReasoningText(value) {
77
+ if (typeof value === "string")
78
+ return value;
79
+ if (Array.isArray(value))
80
+ return value.map(collectReasoningText).join("");
81
+ const record = asRecord(value);
82
+ if (!record)
83
+ return "";
84
+ const direct = [
85
+ record.delta,
86
+ record.text,
87
+ record.value,
88
+ record.output_text,
89
+ record.reasoning_text,
90
+ record.reasoning_content,
91
+ record.reasoning_summary,
92
+ record.reasoning_summary_text,
93
+ record.summary_text,
94
+ ];
95
+ for (const candidate of direct) {
96
+ if (typeof candidate === "string" && candidate.length > 0)
97
+ return candidate;
98
+ }
99
+ return [record.summary, record.parts, record.content, record.annotations]
100
+ .map(collectReasoningText)
101
+ .join("");
102
+ }
103
+ function extractReasoningDelta(deltaFields) {
104
+ const reasoningFields = [
105
+ "reasoning_content",
106
+ "reasoning",
107
+ "reasoning_text",
108
+ "reasoning_summary",
109
+ "reasoning_summary_text",
110
+ "summary_text",
111
+ "summary",
112
+ ];
113
+ for (const field of reasoningFields) {
114
+ const delta = collectReasoningText(deltaFields[field]);
115
+ if (delta.length > 0)
116
+ return { field, delta };
117
+ }
118
+ const details = collectReasoningText(deltaFields.reasoning_details);
119
+ return details.length > 0
120
+ ? { field: "reasoning_details", delta: details }
121
+ : undefined;
122
+ }
123
+ function extractReasoningSignature(value) {
124
+ if (typeof value === "string" && value.length > 0)
125
+ return value;
126
+ const record = asRecord(value);
127
+ if (!record)
128
+ return undefined;
129
+ for (const candidate of [
130
+ record.id,
131
+ record.item_id,
132
+ record.data,
133
+ ]) {
134
+ if (typeof candidate === "string" && candidate.length > 0)
135
+ return candidate;
136
+ }
137
+ return undefined;
138
+ }
139
+ function extractReasoningMetadata(value) {
140
+ const record = asRecord(value);
141
+ if (!record)
142
+ return undefined;
143
+ const metadata = {};
144
+ for (const key of [
145
+ "id",
146
+ "item_id",
147
+ "output_index",
148
+ "summary_index",
149
+ "encrypted_content",
150
+ "type",
151
+ ]) {
152
+ if (record[key] !== undefined && record[key] !== null)
153
+ metadata[key] = record[key];
154
+ }
155
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
156
+ }
157
+ function stringMetadata(metadata, key) {
158
+ const value = metadata?.[key];
159
+ return typeof value === "string" && value.length > 0 ? value : undefined;
160
+ }
161
+ function numberMetadata(metadata, key) {
162
+ const value = metadata?.[key];
163
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
164
+ }
165
+ function hasEncryptedReasoningMetadata(block) {
166
+ return stringMetadata(block.thinkingMetadata, "encrypted_content") !== undefined;
167
+ }
168
+ function buildResponsesReasoningSidecar(metadata, includeOutputIndex) {
169
+ const sidecar = {};
170
+ const id = stringMetadata(metadata, "id");
171
+ if (id)
172
+ sidecar.id = id;
173
+ const itemId = stringMetadata(metadata, "item_id");
174
+ if (itemId)
175
+ sidecar.item_id = itemId;
176
+ if (includeOutputIndex) {
177
+ const outputIndex = numberMetadata(metadata, "output_index");
178
+ if (outputIndex !== undefined)
179
+ sidecar.output_index = outputIndex;
180
+ const summaryIndex = numberMetadata(metadata, "summary_index");
181
+ if (summaryIndex !== undefined)
182
+ sidecar.summary_index = summaryIndex;
183
+ }
184
+ const encryptedContent = stringMetadata(metadata, "encrypted_content");
185
+ if (encryptedContent)
186
+ sidecar.encrypted_content = encryptedContent;
187
+ return sidecar;
188
+ }
189
+ function isSafeReasoningReplayField(field) {
190
+ return SAFE_REASONING_REPLAY_FIELDS.includes(field);
191
+ }
192
+ function normalizeReasoningReplayField(field, model) {
193
+ const normalized = model.provider === "opencode-go" && field === "reasoning"
194
+ ? "reasoning_content"
195
+ : field;
196
+ return normalized && isSafeReasoningReplayField(normalized)
197
+ ? normalized
198
+ : "reasoning_content";
199
+ }
200
+ function setAssistantReasoningField(message, field, value) {
201
+ const messageWithSidecars = message;
202
+ messageWithSidecars[field] = value;
203
+ }
204
+ function setAssistantReasoningLifecycleSidecars(message, blocks) {
205
+ const encryptedBlock = blocks.find(hasEncryptedReasoningMetadata);
206
+ if (!encryptedBlock)
207
+ return;
208
+ const messageWithSidecars = message;
209
+ messageWithSidecars.reasoning_start = buildResponsesReasoningSidecar(encryptedBlock.thinkingMetadata, true);
210
+ messageWithSidecars.reasoning_content = blocks
211
+ .map((block) => block.thinking)
212
+ .join("\n");
213
+ messageWithSidecars.reasoning_end = buildResponsesReasoningSidecar(encryptedBlock.thinkingMetadata, false);
214
+ }
215
+ function assistantHasReasoningReplay(message) {
216
+ const messageWithSidecars = message;
217
+ return messageWithSidecars.reasoning_start !== undefined ||
218
+ messageWithSidecars.reasoning_end !== undefined ||
219
+ SAFE_REASONING_REPLAY_FIELDS.some((field) => typeof messageWithSidecars[field] === "string");
220
+ }
221
+ function mergeThinkingMetadata(block, metadata) {
222
+ if (!metadata)
223
+ return;
224
+ block.thinkingMetadata = { ...(block.thinkingMetadata ?? {}), ...metadata };
225
+ }
226
+ function splitInlineThinkingDelta(pending, inInlineThinking, delta) {
227
+ const parts = [];
228
+ let buffer = pending + delta;
229
+ let thinking = inInlineThinking;
230
+ const append = (type, value) => {
231
+ if (!value)
232
+ return;
233
+ const last = parts[parts.length - 1];
234
+ if (last?.type === type) {
235
+ last.delta += value;
236
+ return;
237
+ }
238
+ parts.push({ type, delta: value });
239
+ };
240
+ while (buffer.length > 0) {
241
+ const tag = thinking ? "</think>" : "<think>";
242
+ const idx = buffer.toLowerCase().indexOf(tag);
243
+ if (idx >= 0) {
244
+ append(thinking ? "thinking" : "text", buffer.slice(0, idx));
245
+ buffer = buffer.slice(idx + tag.length);
246
+ thinking = !thinking;
247
+ continue;
248
+ }
249
+ const maxTagLength = "</think>".length - 1;
250
+ let keep = 0;
251
+ const limit = Math.min(maxTagLength, buffer.length);
252
+ for (let size = limit; size > 0; size--) {
253
+ const suffix = buffer.slice(-size).toLowerCase();
254
+ if ("<think>".startsWith(suffix) || "</think>".startsWith(suffix)) {
255
+ keep = size;
256
+ break;
257
+ }
258
+ }
259
+ append(thinking ? "thinking" : "text", buffer.slice(0, buffer.length - keep));
260
+ buffer = buffer.slice(buffer.length - keep);
261
+ break;
262
+ }
263
+ return { parts, pending: buffer, inInlineThinking: thinking };
264
+ }
60
265
  export const streamOpenAICompletions = (model, context, options) => {
61
266
  const stream = new AssistantMessageEventStream();
62
267
  (async () => {
@@ -86,12 +291,17 @@ export const streamOpenAICompletions = (model, context, options) => {
86
291
  let params = buildParams(model, context, options, compat, cacheRetention);
87
292
  const nextParams = await options?.onPayload?.(params, model);
88
293
  if (nextParams !== undefined) {
89
- params = nextParams;
294
+ params =
295
+ nextParams;
90
296
  }
91
297
  const requestOptions = {
92
298
  ...(options?.signal ? { signal: options.signal } : {}),
93
- ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
94
- ...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
299
+ ...(options?.timeoutMs !== undefined
300
+ ? { timeout: options.timeoutMs }
301
+ : {}),
302
+ ...(options?.maxRetries !== undefined
303
+ ? { maxRetries: options.maxRetries }
304
+ : {}),
95
305
  };
96
306
  const { data: openaiStream, response } = await client.chat.completions
97
307
  .create(params, requestOptions)
@@ -101,6 +311,9 @@ export const streamOpenAICompletions = (model, context, options) => {
101
311
  let textBlock = null;
102
312
  let thinkingBlock = null;
103
313
  let hasFinishReason = false;
314
+ let sawStreamOutput = false;
315
+ let inlineThinkingPending = "";
316
+ let inInlineThinking = false;
104
317
  const toolCallBlocksByIndex = new Map();
105
318
  const toolCallBlocksById = new Map();
106
319
  const blocks = output.content;
@@ -124,6 +337,7 @@ export const streamOpenAICompletions = (model, context, options) => {
124
337
  contentIndex,
125
338
  content: block.thinking,
126
339
  partial: output,
340
+ metadata: block.thinkingMetadata,
127
341
  });
128
342
  }
129
343
  else if (block.type === "toolCall") {
@@ -144,25 +358,91 @@ export const streamOpenAICompletions = (model, context, options) => {
144
358
  if (!textBlock) {
145
359
  textBlock = { type: "text", text: "" };
146
360
  blocks.push(textBlock);
147
- stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output });
361
+ stream.push({
362
+ type: "text_start",
363
+ contentIndex: getContentIndex(textBlock),
364
+ partial: output,
365
+ });
148
366
  }
149
367
  return textBlock;
150
368
  };
151
- const ensureThinkingBlock = (thinkingSignature) => {
369
+ const ensureThinkingBlock = (thinkingSignature, metadata) => {
152
370
  if (!thinkingBlock) {
153
371
  thinkingBlock = {
154
372
  type: "thinking",
155
373
  thinking: "",
156
374
  thinkingSignature,
157
375
  };
376
+ mergeThinkingMetadata(thinkingBlock, metadata);
158
377
  blocks.push(thinkingBlock);
159
- stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output });
378
+ stream.push({
379
+ type: "thinking_start",
380
+ contentIndex: getContentIndex(thinkingBlock),
381
+ partial: output,
382
+ metadata: thinkingBlock.thinkingMetadata,
383
+ });
384
+ }
385
+ else {
386
+ if (!thinkingBlock.thinkingSignature && thinkingSignature) {
387
+ thinkingBlock.thinkingSignature = thinkingSignature;
388
+ }
389
+ mergeThinkingMetadata(thinkingBlock, metadata);
160
390
  }
161
391
  return thinkingBlock;
162
392
  };
393
+ const updateThinkingSignature = (value) => {
394
+ const signature = extractReasoningSignature(value);
395
+ const metadata = extractReasoningMetadata(value);
396
+ if (!signature && !metadata)
397
+ return;
398
+ const block = ensureThinkingBlock(signature ?? "reasoning_content", metadata);
399
+ if (signature)
400
+ block.thinkingSignature = signature;
401
+ };
402
+ const emitThinkingDelta = (field, delta, metadata) => {
403
+ const thinkingSignature = model.provider === "opencode-go" && field === "reasoning"
404
+ ? "reasoning_content"
405
+ : field;
406
+ const block = ensureThinkingBlock(thinkingSignature, metadata);
407
+ block.thinking += delta;
408
+ sawStreamOutput = true;
409
+ stream.push({
410
+ type: "thinking_delta",
411
+ contentIndex: getContentIndex(block),
412
+ delta,
413
+ partial: output,
414
+ metadata: block.thinkingMetadata,
415
+ });
416
+ };
417
+ const emitTextDelta = (delta) => {
418
+ const block = ensureTextBlock();
419
+ block.text += delta;
420
+ sawStreamOutput = true;
421
+ stream.push({
422
+ type: "text_delta",
423
+ contentIndex: getContentIndex(block),
424
+ delta,
425
+ partial: output,
426
+ });
427
+ };
428
+ const emitContentDelta = (delta) => {
429
+ const split = splitInlineThinkingDelta(inlineThinkingPending, inInlineThinking, delta);
430
+ inlineThinkingPending = split.pending;
431
+ inInlineThinking = split.inInlineThinking;
432
+ for (const part of split.parts) {
433
+ if (part.type === "thinking") {
434
+ emitThinkingDelta("inline_think", part.delta);
435
+ }
436
+ else {
437
+ emitTextDelta(part.delta);
438
+ }
439
+ }
440
+ };
163
441
  const ensureToolCallBlock = (toolCall) => {
164
442
  const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
165
- let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
443
+ let block = streamIndex !== undefined
444
+ ? toolCallBlocksByIndex.get(streamIndex)
445
+ : undefined;
166
446
  if (!block && toolCall.id) {
167
447
  block = toolCallBlocksById.get(toolCall.id);
168
448
  }
@@ -203,13 +483,17 @@ export const streamOpenAICompletions = (model, context, options) => {
203
483
  // OpenAI documents ChatCompletionChunk.id as the unique chat completion identifier,
204
484
  // and each chunk in a streamed completion carries the same id.
205
485
  output.responseId ||= chunk.id;
206
- if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== model.id) {
486
+ if (typeof chunk.model === "string" &&
487
+ chunk.model.length > 0 &&
488
+ chunk.model !== model.id) {
207
489
  output.responseModel ||= chunk.model;
208
490
  }
209
491
  if (chunk.usage) {
210
492
  output.usage = parseChunkUsage(chunk.usage, model);
211
493
  }
212
- const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : undefined;
494
+ const choice = Array.isArray(chunk.choices)
495
+ ? chunk.choices[0]
496
+ : undefined;
213
497
  if (!choice)
214
498
  continue;
215
499
  // Fallback: some providers (e.g., Moonshot) return usage
@@ -226,47 +510,24 @@ export const streamOpenAICompletions = (model, context, options) => {
226
510
  hasFinishReason = true;
227
511
  }
228
512
  if (choice.delta) {
513
+ const deltaFields = choice.delta;
514
+ const reasoningStart = deltaFields.reasoning_start;
515
+ if (reasoningStart) {
516
+ ensureThinkingBlock(extractReasoningSignature(reasoningStart) ?? "reasoning_content", extractReasoningMetadata(reasoningStart));
517
+ }
229
518
  if (choice.delta.content !== null &&
230
519
  choice.delta.content !== undefined &&
231
520
  choice.delta.content.length > 0) {
232
- const block = ensureTextBlock();
233
- block.text += choice.delta.content;
234
- stream.push({
235
- type: "text_delta",
236
- contentIndex: getContentIndex(block),
237
- delta: choice.delta.content,
238
- partial: output,
239
- });
521
+ emitContentDelta(choice.delta.content);
240
522
  }
241
- // Some endpoints return reasoning in reasoning_content (llama.cpp),
242
- // or reasoning (other openai compatible endpoints)
243
- // Use the first non-empty reasoning field to avoid duplication
244
- // (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
245
- const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
246
- const deltaFields = choice.delta;
247
- let foundReasoningField = null;
248
- for (const field of reasoningFields) {
249
- const value = deltaFields[field];
250
- if (typeof value === "string" && value.length > 0) {
251
- foundReasoningField = field;
252
- break;
253
- }
254
- }
255
- if (foundReasoningField) {
256
- const delta = deltaFields[foundReasoningField];
257
- if (typeof delta === "string" && delta.length > 0) {
258
- const thinkingSignature = model.provider === "opencode-go" && foundReasoningField === "reasoning"
259
- ? "reasoning_content"
260
- : foundReasoningField;
261
- const block = ensureThinkingBlock(thinkingSignature);
262
- block.thinking += delta;
263
- stream.push({
264
- type: "thinking_delta",
265
- contentIndex: getContentIndex(block),
266
- delta,
267
- partial: output,
268
- });
269
- }
523
+ // OpenAI-compatible providers do not agree on reasoning field names.
524
+ // Normalize chat deltas, Responses summary deltas, and reasoning_details
525
+ // into Spectral's single thinking stream, mirroring opencode's
526
+ // canonical `reasoning` message part.
527
+ const reasoningDelta = extractReasoningDelta(deltaFields);
528
+ if (reasoningDelta) {
529
+ emitThinkingDelta(reasoningDelta.field, reasoningDelta.delta, extractReasoningMetadata(deltaFields.reasoning_start) ??
530
+ extractReasoningMetadata(deltaFields.reasoning_end));
270
531
  }
271
532
  if (choice?.delta?.tool_calls) {
272
533
  for (const toolCall of choice.delta.tool_calls) {
@@ -281,9 +542,11 @@ export const streamOpenAICompletions = (model, context, options) => {
281
542
  let delta = "";
282
543
  if (toolCall.function?.arguments) {
283
544
  delta = toolCall.function.arguments;
284
- block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
545
+ block.partialArgs =
546
+ (block.partialArgs ?? "") + toolCall.function.arguments;
285
547
  block.arguments = parseStreamingJson(block.partialArgs);
286
548
  }
549
+ sawStreamOutput = true;
287
550
  stream.push({
288
551
  type: "toolcall_delta",
289
552
  contentIndex: getContentIndex(block),
@@ -292,10 +555,16 @@ export const streamOpenAICompletions = (model, context, options) => {
292
555
  });
293
556
  }
294
557
  }
558
+ const reasoningEnd = deltaFields.reasoning_end;
559
+ if (reasoningEnd) {
560
+ updateThinkingSignature(reasoningEnd);
561
+ }
295
562
  const reasoningDetails = choice.delta.reasoning_details;
296
563
  if (reasoningDetails && Array.isArray(reasoningDetails)) {
297
564
  for (const detail of reasoningDetails) {
298
- if (detail.type === "reasoning.encrypted" && detail.id && detail.data) {
565
+ if (detail.type === "reasoning.encrypted" &&
566
+ detail.id &&
567
+ detail.data) {
299
568
  const matchingToolCall = output.content.find((b) => b.type === "toolCall" && b.id === detail.id);
300
569
  if (matchingToolCall) {
301
570
  matchingToolCall.thoughtSignature = JSON.stringify(detail);
@@ -305,6 +574,15 @@ export const streamOpenAICompletions = (model, context, options) => {
305
574
  }
306
575
  }
307
576
  }
577
+ if (inlineThinkingPending) {
578
+ if (inInlineThinking) {
579
+ emitThinkingDelta("inline_think", inlineThinkingPending);
580
+ }
581
+ else {
582
+ emitTextDelta(inlineThinkingPending);
583
+ }
584
+ inlineThinkingPending = "";
585
+ }
308
586
  for (const block of blocks) {
309
587
  finishBlock(block);
310
588
  }
@@ -318,7 +596,12 @@ export const streamOpenAICompletions = (model, context, options) => {
318
596
  throw new Error(output.errorMessage || "Provider returned an error stop reason");
319
597
  }
320
598
  if (!hasFinishReason) {
321
- throw new Error("Stream ended without finish_reason");
599
+ if (!sawStreamOutput && blocks.length === 0) {
600
+ throw new Error("Stream ended without finish_reason");
601
+ }
602
+ output.stopReason = blocks.some((block) => block.type === "toolCall")
603
+ ? "toolUse"
604
+ : "stop";
322
605
  }
323
606
  stream.push({ type: "done", reason: output.stopReason, message: output });
324
607
  stream.end();
@@ -331,7 +614,8 @@ export const streamOpenAICompletions = (model, context, options) => {
331
614
  delete block.streamIndex;
332
615
  }
333
616
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
334
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
617
+ output.errorMessage =
618
+ error instanceof Error ? error.message : JSON.stringify(error);
335
619
  // Some providers via OpenRouter give additional information in this field.
336
620
  const rawMetadata = error?.error?.metadata?.raw;
337
621
  if (rawMetadata)
@@ -348,9 +632,12 @@ export const streamSimpleOpenAICompletions = (model, context, options) => {
348
632
  throw new Error(`No API key for provider: ${model.provider}`);
349
633
  }
350
634
  const base = buildBaseOptions(model, options, apiKey);
351
- const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
635
+ const clampedReasoning = options?.reasoning
636
+ ? clampThinkingLevel(model, options.reasoning)
637
+ : undefined;
352
638
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
353
- const toolChoice = options?.toolChoice;
639
+ const toolChoice = options
640
+ ?.toolChoice;
354
641
  return streamOpenAICompletions(model, context, {
355
642
  ...base,
356
643
  reasoningEffort,
@@ -455,17 +742,21 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
455
742
  else if (compat.thinkingFormat === "qwen" && model.reasoning) {
456
743
  params.enable_thinking = !!options?.reasoningEffort;
457
744
  }
458
- else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
745
+ else if (compat.thinkingFormat === "qwen-chat-template" &&
746
+ model.reasoning) {
459
747
  params.chat_template_kwargs = {
460
748
  enable_thinking: !!options?.reasoningEffort,
461
749
  preserve_thinking: true,
462
750
  };
463
751
  }
464
752
  else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
465
- params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
753
+ params.thinking = {
754
+ type: options?.reasoningEffort ? "enabled" : "disabled",
755
+ };
466
756
  if (options?.reasoningEffort) {
467
757
  params.reasoning_effort =
468
- model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
758
+ model.thinkingLevelMap?.[options.reasoningEffort] ??
759
+ options.reasoningEffort;
469
760
  }
470
761
  }
471
762
  else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
@@ -473,36 +764,49 @@ function buildParams(model, context, options, compat = getCompat(model), cacheRe
473
764
  const openRouterParams = params;
474
765
  if (options?.reasoningEffort) {
475
766
  openRouterParams.reasoning = {
476
- effort: model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort,
767
+ effort: model.thinkingLevelMap?.[options.reasoningEffort] ??
768
+ options.reasoningEffort,
477
769
  };
478
770
  }
479
771
  else if (model.thinkingLevelMap?.off !== null) {
480
- openRouterParams.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
772
+ openRouterParams.reasoning = {
773
+ effort: model.thinkingLevelMap?.off ?? "none",
774
+ };
481
775
  }
482
776
  }
483
777
  else if (compat.thinkingFormat === "together" && model.reasoning) {
484
778
  const togetherParams = params;
485
779
  togetherParams.reasoning = { enabled: !!options?.reasoningEffort };
486
780
  if (options?.reasoningEffort && compat.supportsReasoningEffort) {
487
- togetherParams.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
781
+ togetherParams.reasoning_effort =
782
+ model.thinkingLevelMap?.[options.reasoningEffort] ??
783
+ options.reasoningEffort;
488
784
  }
489
785
  }
490
- else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
786
+ else if (options?.reasoningEffort &&
787
+ model.reasoning &&
788
+ compat.supportsReasoningEffort) {
491
789
  // OpenAI-style reasoning_effort
492
- params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
790
+ params.reasoning_effort =
791
+ model.thinkingLevelMap?.[options.reasoningEffort] ??
792
+ options.reasoningEffort;
493
793
  }
494
- else if (!options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
794
+ else if (!options?.reasoningEffort &&
795
+ model.reasoning &&
796
+ compat.supportsReasoningEffort) {
495
797
  const offValue = model.thinkingLevelMap?.off;
496
798
  if (typeof offValue === "string") {
497
799
  params.reasoning_effort = offValue;
498
800
  }
499
801
  }
500
802
  // OpenRouter provider routing preferences
501
- if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
803
+ if (model.baseUrl.includes("openrouter.ai") &&
804
+ model.compat?.openRouterRouting) {
502
805
  params.provider = model.compat.openRouterRouting;
503
806
  }
504
807
  // Vercel AI Gateway provider routing preferences
505
- if (model.baseUrl.includes("ai-gateway.vercel.sh") && model.compat?.vercelGatewayRouting) {
808
+ if (model.baseUrl.includes("ai-gateway.vercel.sh") &&
809
+ model.compat?.vercelGatewayRouting) {
506
810
  const routing = model.compat.vercelGatewayRouting;
507
811
  if (routing.only || routing.order) {
508
812
  const gatewayOptions = {};
@@ -519,7 +823,9 @@ function getCompatCacheControl(compat, cacheRetention) {
519
823
  if (compat.cacheControlFormat !== "anthropic" || cacheRetention === "none") {
520
824
  return undefined;
521
825
  }
522
- const ttl = cacheRetention === "long" && compat.supportsLongCacheRetention ? "1h" : undefined;
826
+ const ttl = cacheRetention === "long" && compat.supportsLongCacheRetention
827
+ ? "1h"
828
+ : undefined;
523
829
  return { type: "ephemeral", ...(ttl ? { ttl } : {}) };
524
830
  }
525
831
  function applyAnthropicCacheControl(messages, tools, cacheControl) {
@@ -609,14 +915,19 @@ export function convertMessages(model, context, compat) {
609
915
  if (context.systemPrompt) {
610
916
  const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
611
917
  const role = useDeveloperRole ? "developer" : "system";
612
- params.push({ role: role, content: sanitizeSurrogates(context.systemPrompt) });
918
+ params.push({
919
+ role: role,
920
+ content: sanitizeSurrogates(context.systemPrompt),
921
+ });
613
922
  }
614
923
  let lastRole = null;
615
924
  for (let i = 0; i < transformedMessages.length; i++) {
616
925
  const msg = transformedMessages[i];
617
926
  // Some providers don't allow user messages directly after tool results
618
927
  // Insert a synthetic assistant message to bridge the gap
619
- if (compat.requiresAssistantAfterToolResult && lastRole === "toolResult" && msg.role === "user") {
928
+ if (compat.requiresAssistantAfterToolResult &&
929
+ lastRole === "toolResult" &&
930
+ msg.role === "user") {
620
931
  params.push({
621
932
  role: "assistant",
622
933
  content: "I have processed the tool results.",
@@ -667,7 +978,9 @@ export function convertMessages(model, context, compat) {
667
978
  type: "text",
668
979
  text: sanitizeSurrogates(block.text),
669
980
  }));
670
- const assistantText = assistantTextParts.map((part) => part.text).join("");
981
+ const assistantText = assistantTextParts
982
+ .map((part) => part.text)
983
+ .join("");
671
984
  const nonEmptyThinkingBlocks = msg.content
672
985
  .filter(isThinkingContentBlock)
673
986
  .filter((block) => block.thinking.trim().length > 0);
@@ -677,7 +990,10 @@ export function convertMessages(model, context, compat) {
677
990
  const thinkingText = nonEmptyThinkingBlocks
678
991
  .map((block) => sanitizeSurrogates(block.thinking))
679
992
  .join("\n\n");
680
- assistantMsg.content = [{ type: "text", text: thinkingText }, ...assistantTextParts];
993
+ assistantMsg.content = [
994
+ { type: "text", text: thinkingText },
995
+ ...assistantTextParts,
996
+ ];
681
997
  }
682
998
  else {
683
999
  // Always send assistant content as a plain string (OpenAI Chat Completions
@@ -688,13 +1004,14 @@ export function convertMessages(model, context, compat) {
688
1004
  if (assistantText.length > 0) {
689
1005
  assistantMsg.content = assistantText;
690
1006
  }
691
- // Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
692
- let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
693
- if (model.provider === "opencode-go" && signature === "reasoning") {
694
- signature = "reasoning_content";
1007
+ const reasoningText = nonEmptyThinkingBlocks
1008
+ .map((block) => block.thinking)
1009
+ .join("\n");
1010
+ if (nonEmptyThinkingBlocks.some(hasEncryptedReasoningMetadata)) {
1011
+ setAssistantReasoningLifecycleSidecars(assistantMsg, nonEmptyThinkingBlocks);
695
1012
  }
696
- if (signature && signature.length > 0) {
697
- assistantMsg[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
1013
+ else {
1014
+ setAssistantReasoningField(assistantMsg, normalizeReasoningReplayField(nonEmptyThinkingBlocks[0].thinkingSignature, model), reasoningText);
698
1015
  }
699
1016
  }
700
1017
  }
@@ -733,7 +1050,8 @@ export function convertMessages(model, context, compat) {
733
1050
  }
734
1051
  if (compat.requiresReasoningContentOnAssistantMessages &&
735
1052
  model.reasoning &&
736
- assistantMsg.reasoning_content === undefined) {
1053
+ assistantMsg.reasoning_content ===
1054
+ undefined) {
737
1055
  assistantMsg.reasoning_content = "";
738
1056
  }
739
1057
  // Skip assistant messages that have no content and no tool calls.
@@ -744,7 +1062,8 @@ export function convertMessages(model, context, compat) {
744
1062
  const hasContent = content !== null &&
745
1063
  content !== undefined &&
746
1064
  (typeof content === "string" ? content.length > 0 : content.length > 0);
747
- if (!hasContent && !assistantMsg.tool_calls) {
1065
+ if (!hasContent && !assistantMsg.tool_calls &&
1066
+ !assistantHasReasoningReplay(assistantMsg)) {
748
1067
  continue;
749
1068
  }
750
1069
  params.push(assistantMsg);
@@ -752,7 +1071,8 @@ export function convertMessages(model, context, compat) {
752
1071
  else if (msg.role === "toolResult") {
753
1072
  const imageBlocks = [];
754
1073
  let j = i;
755
- for (; j < transformedMessages.length && transformedMessages[j].role === "toolResult"; j++) {
1074
+ for (; j < transformedMessages.length &&
1075
+ transformedMessages[j].role === "toolResult"; j++) {
756
1076
  const toolMsg = transformedMessages[j];
757
1077
  // Extract text and image content
758
1078
  const textResult = toolMsg.content
@@ -828,7 +1148,9 @@ function convertTools(tools, compat) {
828
1148
  }
829
1149
  function parseChunkUsage(rawUsage, model) {
830
1150
  const promptTokens = rawUsage.prompt_tokens || 0;
831
- const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
1151
+ const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ??
1152
+ rawUsage.prompt_cache_hit_tokens ??
1153
+ 0;
832
1154
  const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
833
1155
  // Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
834
1156
  // tokens (hits). OpenAI does not document or emit cache_write_tokens, but
@@ -865,9 +1187,15 @@ function mapStopReason(reason) {
865
1187
  case "tool_calls":
866
1188
  return { stopReason: "toolUse" };
867
1189
  case "content_filter":
868
- return { stopReason: "error", errorMessage: "Provider finish_reason: content_filter" };
1190
+ return {
1191
+ stopReason: "error",
1192
+ errorMessage: "Provider finish_reason: content_filter",
1193
+ };
869
1194
  case "network_error":
870
- return { stopReason: "error", errorMessage: "Provider finish_reason: network_error" };
1195
+ return {
1196
+ stopReason: "error",
1197
+ errorMessage: "Provider finish_reason: network_error",
1198
+ };
871
1199
  default:
872
1200
  return {
873
1201
  stopReason: "error",
@@ -884,8 +1212,12 @@ function detectCompat(model) {
884
1212
  const provider = model.provider;
885
1213
  const baseUrl = model.baseUrl;
886
1214
  const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
887
- const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz");
888
- const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot.");
1215
+ const isTogether = provider === "together" ||
1216
+ baseUrl.includes("api.together.ai") ||
1217
+ baseUrl.includes("api.together.xyz");
1218
+ const isMoonshot = provider === "moonshotai" ||
1219
+ provider === "moonshotai-cn" ||
1220
+ baseUrl.includes("api.moonshot.");
889
1221
  // Cloudflare providers not supported (cloudflare.ts was removed)
890
1222
  const isCloudflareWorkersAI = false;
891
1223
  const isCloudflareAiGateway = false;
@@ -902,10 +1234,15 @@ function detectCompat(model) {
902
1234
  baseUrl.includes("opencode.ai") ||
903
1235
  isCloudflareWorkersAI ||
904
1236
  isCloudflareAiGateway;
905
- const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether;
1237
+ const useMaxTokens = baseUrl.includes("chutes.ai") ||
1238
+ isMoonshot ||
1239
+ isCloudflareAiGateway ||
1240
+ isTogether;
906
1241
  const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
907
1242
  const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
908
- const cacheControlFormat = isOpenRouterAnthropicModel(model) ? "anthropic" : undefined;
1243
+ const cacheControlFormat = isOpenRouterAnthropicModel(model)
1244
+ ? "anthropic"
1245
+ : undefined;
909
1246
  return {
910
1247
  supportsStore: !isNonStandard,
911
1248
  supportsDeveloperRole: !isNonStandard,
@@ -931,7 +1268,9 @@ function detectCompat(model) {
931
1268
  supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway,
932
1269
  cacheControlFormat,
933
1270
  sendSessionAffinityHeaders: isOpenRouterModel(model),
934
- supportsLongCacheRetention: !(isTogether || isCloudflareWorkersAI || isCloudflareAiGateway),
1271
+ supportsLongCacheRetention: !(isTogether ||
1272
+ isCloudflareWorkersAI ||
1273
+ isCloudflareAiGateway),
935
1274
  };
936
1275
  }
937
1276
  /**
@@ -946,10 +1285,12 @@ function getCompat(model) {
946
1285
  supportsStore: model.compat.supportsStore ?? detected.supportsStore,
947
1286
  supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
948
1287
  supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
949
- supportsUsageInStreaming: model.compat.supportsUsageInStreaming ?? detected.supportsUsageInStreaming,
1288
+ supportsUsageInStreaming: model.compat.supportsUsageInStreaming ??
1289
+ detected.supportsUsageInStreaming,
950
1290
  maxTokensField: model.compat.maxTokensField ?? detected.maxTokensField,
951
1291
  requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
952
- requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
1292
+ requiresAssistantAfterToolResult: model.compat.requiresAssistantAfterToolResult ??
1293
+ detected.requiresAssistantAfterToolResult,
953
1294
  requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
954
1295
  requiresReasoningContentOnAssistantMessages: model.compat.requiresReasoningContentOnAssistantMessages ??
955
1296
  detected.requiresReasoningContentOnAssistantMessages,
@@ -959,7 +1300,9 @@ function getCompat(model) {
959
1300
  zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream,
960
1301
  supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
961
1302
  cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat,
962
- sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ?? detected.sendSessionAffinityHeaders,
963
- supportsLongCacheRetention: model.compat.supportsLongCacheRetention ?? detected.supportsLongCacheRetention,
1303
+ sendSessionAffinityHeaders: model.compat.sendSessionAffinityHeaders ??
1304
+ detected.sendSessionAffinityHeaders,
1305
+ supportsLongCacheRetention: model.compat.supportsLongCacheRetention ??
1306
+ detected.supportsLongCacheRetention,
964
1307
  };
965
1308
  }