@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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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.
|
|
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.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.6.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.6.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.6.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.6.
|
|
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"
|
package/src/dialect/deepseek.ts
CHANGED
|
@@ -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
|
-
|
|
408
|
-
if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length)
|
|
425
|
+
break;
|
|
426
|
+
if (partialSuffixOverlapAny(this.#buffer, DSML_SECTION_TOKENS) === this.#buffer.length) break;
|
|
409
427
|
}
|
|
410
|
-
if (this.#buffer.length === 0)
|
|
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
|
|
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
|
-
|
|
2338
|
-
|
|
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
|
-
|
|
2365
|
+
changed = true;
|
|
2350
2366
|
}
|
|
2351
|
-
|
|
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
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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()) : [];
|