@oh-my-pi/pi-ai 18.6.0 → 18.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.6.1] - 2026-10-04
6
+
7
+ ### Fixed
8
+
9
+ - Fixed compatibility with Command Code DeepSeek and other DeepSeek-family models by preserving the reasoning context required for warm OpenAI Responses sessions and correctly handling incomplete DSML tool-call wrappers in visible output.
10
+
5
11
  ## [18.6.0] - 2026-10-03
6
12
 
7
13
  ### Fixed
@@ -63,10 +63,10 @@ export declare class StreamMarkupHealing {
63
63
  /** Drain accumulated tool calls from calls to {@link feed}. */
64
64
  drainCompleted(): HealedToolCall[];
65
65
  /**
66
- * Flush held-back stream-end fragments as ordered events. Partial tool-call
67
- * sections/envelopes are dropped by the delegated scanners; unterminated
68
- * thinking blocks are emitted as thinking, matching the previous MiniMax parser
69
- * behavior.
66
+ * Flush held-back stream-end fragments as ordered events. Partial tool calls
67
+ * are dropped by the delegated scanners; a DSML wrapper without an invoke
68
+ * remains visible for agent-layer leak recovery. Unterminated thinking blocks
69
+ * are emitted as thinking.
70
70
  */
71
71
  flushEvents(): StreamMarkupHealingEvent[];
72
72
  /** Flush held-back text only. Reconstructed calls are retained for {@link drainCompleted}. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.6.0",
3
+ "version": "18.6.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.6.0",
159
- "@oh-my-pi/pi-catalog": "18.6.0",
160
- "@oh-my-pi/pi-natives": "18.6.0",
161
- "@oh-my-pi/pi-utils": "18.6.0",
162
- "@oh-my-pi/pi-wire": "18.6.0"
158
+ "@oh-my-pi/omptype": "18.6.1",
159
+ "@oh-my-pi/pi-catalog": "18.6.1",
160
+ "@oh-my-pi/pi-natives": "18.6.1",
161
+ "@oh-my-pi/pi-utils": "18.6.1",
162
+ "@oh-my-pi/pi-wire": "18.6.1"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -128,6 +128,8 @@ export class DeepSeekInbandScanner implements InbandScanner {
128
128
  #dsmlParamName = "";
129
129
  #dsmlParamIsString = true;
130
130
  #dsmlParamRaw = "";
131
+ /** Unclassified wrapper text is kept until an invoke proves it is a tool call. */
132
+ #pendingDsmlSection: string | undefined;
131
133
  #rawBlock = "";
132
134
  #stripLeadingWhitespace = false;
133
135
  /**
@@ -194,6 +196,12 @@ export class DeepSeekInbandScanner implements InbandScanner {
194
196
  if (!this.#consumeDsmlParam(final, events)) break;
195
197
  }
196
198
  if (final && this.#state === "thinking") this.#endThinking(events);
199
+ if (final && this.#pendingDsmlSection !== undefined) {
200
+ this.#emitText(this.#pendingDsmlSection + this.#buffer, events);
201
+ this.#pendingDsmlSection = undefined;
202
+ this.#buffer = "";
203
+ this.#state = "outside";
204
+ }
197
205
  if (final && this.#buffer.length === 0 && this.#rawBlock.length > 0) this.#rawBlock = "";
198
206
  return events;
199
207
  }
@@ -249,6 +257,7 @@ export class DeepSeekInbandScanner implements InbandScanner {
249
257
  ? DSML_TOOL_CALLS_OPEN_FULLWIDTH
250
258
  : DSML_TOOL_CALLS_OPEN_ASCII;
251
259
  this.#buffer = this.#buffer.slice(openToken.length);
260
+ this.#pendingDsmlSection = openToken;
252
261
  this.#state = "dsmlSection";
253
262
  return;
254
263
  }
@@ -381,16 +390,25 @@ export class DeepSeekInbandScanner implements InbandScanner {
381
390
  }
382
391
 
383
392
  #consumeDsmlSection(final: boolean, events: InbandScanEvent[]): boolean {
393
+ const initial = this.#buffer;
384
394
  while (this.#buffer.length > 0) {
385
395
  this.#skipWhitespace();
386
396
  const close = this.#matchingDsmlClose(DSML_TOOL_CALLS_CLOSE_FULLWIDTH, DSML_TOOL_CALLS_CLOSE_ASCII);
387
397
  if (close) {
398
+ if (this.#pendingDsmlSection !== undefined) {
399
+ this.#emitText(
400
+ this.#pendingDsmlSection + initial.slice(0, initial.length - this.#buffer.length) + close,
401
+ events,
402
+ );
403
+ this.#pendingDsmlSection = undefined;
404
+ }
388
405
  this.#buffer = this.#buffer.slice(close.length);
389
406
  this.#state = "outside";
390
407
  return true;
391
408
  }
392
409
  const invoke = this.#matchDsmlOpen("invoke");
393
410
  if (invoke) {
411
+ this.#pendingDsmlSection = undefined;
394
412
  this.#rawBlock = invoke.raw;
395
413
  this.#name = invoke.name;
396
414
  this.#id = mintToolCallId();
@@ -404,12 +422,15 @@ export class DeepSeekInbandScanner implements InbandScanner {
404
422
  (this.#buffer.startsWith("<|DSML|invoke") || this.#buffer.startsWith("<|DSML|invoke")) &&
405
423
  !this.#buffer.includes(">")
406
424
  )
407
- return false;
408
- if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) return false;
425
+ break;
426
+ if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) break;
409
427
  }
410
- if (this.#buffer.length === 0) return false;
428
+ if (this.#buffer.length === 0) break;
411
429
  this.#buffer = this.#buffer.slice(1);
412
430
  }
431
+ if (this.#pendingDsmlSection !== undefined) {
432
+ this.#pendingDsmlSection += initial.slice(0, initial.length - this.#buffer.length);
433
+ }
413
434
  return final;
414
435
  }
415
436
 
@@ -2303,6 +2303,30 @@ function createSyntheticResponsesReasoningItem(
2303
2303
  return item as ResponseReasoningItem;
2304
2304
  }
2305
2305
 
2306
+ /**
2307
+ * Fill a replayed reasoning item's missing `reasoning_text` for targets that
2308
+ * require it on every replayed turn (DeepSeek family, #10690). Servers that
2309
+ * stream reasoning as summary text return summary-only items; a warm session
2310
+ * replays them natively, so without this the wire carries no `reasoning_text`
2311
+ * while the cold rebuild of the same turn does (#14288). The summary is the
2312
+ * same text the cold path carries from the thinking block. Items carrying
2313
+ * `encrypted_content` are the server's opaque reasoning and replay untouched:
2314
+ * OpenRouter sets the tool-call requirement for every reasoning model, and its
2315
+ * OpenAI-family items must not gain a summary posing as raw reasoning text.
2316
+ */
2317
+ function withRequiredReasoningText(item: ResponseReasoningItem): ResponseReasoningItem {
2318
+ if (typeof item.encrypted_content === "string") return item;
2319
+ if (item.content?.some(part => part.type === "reasoning_text" && part.text.trim().length > 0)) return item;
2320
+ const summaryText = (item.summary ?? [])
2321
+ .map(part => part.text)
2322
+ .filter(text => text.trim().length > 0)
2323
+ .join("\n\n");
2324
+ return {
2325
+ ...item,
2326
+ content: [{ type: "reasoning_text", text: summaryText || SYNTHETIC_REASONING_REPLAY_PLACEHOLDER }],
2327
+ };
2328
+ }
2329
+
2306
2330
  function isResponsesAssistantTurnBoundary(item: ResponseInput[number]): boolean {
2307
2331
  if (responsesToolOutputKind(item.type) !== undefined) return true;
2308
2332
  if (item.type === "compaction") return true;
@@ -2317,7 +2341,8 @@ function ensureRequiredResponsesReasoningReplay(
2317
2341
  ): ResponseInput {
2318
2342
  if (stopReason === "error" || (!requiresAllTurns && !requiresToolCalls)) return items;
2319
2343
 
2320
- const insertBefore: number[] = [];
2344
+ const repaired: ResponseInput = [];
2345
+ let changed = false;
2321
2346
  let turnStart = 0;
2322
2347
  for (let index = 0; index <= items.length; index++) {
2323
2348
  if (index < items.length && !isResponsesAssistantTurnBoundary(items[index])) continue;
@@ -2334,23 +2359,21 @@ function ensureRequiredResponsesReasoningReplay(
2334
2359
  hasContent = true;
2335
2360
  if (classifyResponsesBatchItem(item) === "call") hasToolCall = true;
2336
2361
  }
2337
- if (hasContent && !hasReasoning && (requiresAllTurns || (requiresToolCalls && hasToolCall))) {
2338
- insertBefore.push(turnStart);
2339
- }
2340
- turnStart = index + 1;
2341
- }
2342
- if (insertBefore.length === 0) return items;
2343
-
2344
- const repaired: ResponseInput = [];
2345
- let insertionIndex = 0;
2346
- for (let index = 0; index < items.length; index++) {
2347
- if (insertBefore[insertionIndex] === index) {
2362
+ const required = hasContent && (requiresAllTurns || (requiresToolCalls && hasToolCall));
2363
+ if (required && !hasReasoning) {
2348
2364
  repaired.push(createSyntheticResponsesReasoningItem());
2349
- insertionIndex++;
2365
+ changed = true;
2350
2366
  }
2351
- repaired.push(items[index]);
2367
+ for (let turnIndex = turnStart; turnIndex < index; turnIndex++) {
2368
+ const item = items[turnIndex];
2369
+ const replayed = required && item.type === "reasoning" ? withRequiredReasoningText(item) : item;
2370
+ if (replayed !== item) changed = true;
2371
+ repaired.push(replayed);
2372
+ }
2373
+ if (index < items.length) repaired.push(items[index]);
2374
+ turnStart = index + 1;
2352
2375
  }
2353
- return repaired;
2376
+ return changed ? repaired : items;
2354
2377
  }
2355
2378
 
2356
2379
  export function convertResponsesAssistantMessage<TApi extends Api>(
@@ -2404,7 +2427,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
2404
2427
  }
2405
2428
  const reasoningItem = parseResponseReasoningReplayItem(block.thinkingSignature);
2406
2429
  if (reasoningItem) {
2407
- outputItems.push(reasoningItem);
2430
+ outputItems.push(requiresReasoningItem ? withRequiredReasoningText(reasoningItem) : reasoningItem);
2408
2431
  reasoningItemEmitted = true;
2409
2432
  }
2410
2433
  continue;
@@ -126,10 +126,10 @@ export class StreamMarkupHealing {
126
126
  }
127
127
 
128
128
  /**
129
- * Flush held-back stream-end fragments as ordered events. Partial tool-call
130
- * sections/envelopes are dropped by the delegated scanners; unterminated
131
- * thinking blocks are emitted as thinking, matching the previous MiniMax parser
132
- * behavior.
129
+ * Flush held-back stream-end fragments as ordered events. Partial tool calls
130
+ * are dropped by the delegated scanners; a DSML wrapper without an invoke
131
+ * remains visible for agent-layer leak recovery. Unterminated thinking blocks
132
+ * are emitted as thinking.
133
133
  */
134
134
  flushEvents(): StreamMarkupHealingEvent[] {
135
135
  const tail = this.#toolScanner ? this.#healThinking(this.#toolScanner.flush()) : [];