@gajae-code/ai 0.11.1 → 0.11.3
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 +6 -0
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +2 -1
- package/dist/types/providers/pi-native-server.d.ts +9 -17
- package/dist/types/types.d.ts +33 -0
- package/dist/types/utils/fallback-transport.d.ts +13 -2
- package/dist/types/utils/overflow.d.ts +4 -54
- package/package.json +2 -2
- package/src/auth-storage.ts +14 -3
- package/src/model-manager.ts +22 -14
- package/src/providers/anthropic-messages-server.ts +178 -17
- package/src/providers/anthropic.ts +193 -213
- package/src/providers/openai-chat-server.ts +93 -5
- package/src/providers/openai-codex-responses.ts +76 -13
- package/src/providers/openai-responses-server.ts +111 -36
- package/src/providers/openai-responses-shared.ts +71 -12
- package/src/providers/pi-native-server.ts +273 -20
- package/src/types.ts +19 -0
- package/src/utils/event-stream.ts +9 -5
- package/src/utils/fallback-transport.ts +111 -27
- package/src/utils/overflow.ts +72 -31
|
@@ -376,6 +376,9 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
376
376
|
item: StreamItem;
|
|
377
377
|
block: StreamBlock;
|
|
378
378
|
blockContentIndex: number;
|
|
379
|
+
summaryBuffer: string;
|
|
380
|
+
rawBuffer: string;
|
|
381
|
+
summaryStarted: boolean;
|
|
379
382
|
}
|
|
380
383
|
// Per-item argument buffer keyed on stable item identity. Multiple tool-call
|
|
381
384
|
// items can stream interleaved argument deltas in one response, so a single
|
|
@@ -412,7 +415,14 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
412
415
|
};
|
|
413
416
|
const registerEntry = (item: StreamItem, block: StreamBlock, outputIndex: number | undefined): ItemEntry => {
|
|
414
417
|
output.content.push(block);
|
|
415
|
-
const entry: ItemEntry = {
|
|
418
|
+
const entry: ItemEntry = {
|
|
419
|
+
item,
|
|
420
|
+
block,
|
|
421
|
+
blockContentIndex: output.content.length - 1,
|
|
422
|
+
summaryBuffer: "",
|
|
423
|
+
rawBuffer: "",
|
|
424
|
+
summaryStarted: false,
|
|
425
|
+
};
|
|
416
426
|
// Primary key prefers the stable item id; if the wire omits it, fall back to
|
|
417
427
|
// the positional index. A synthetic key keeps the entry addressable as lastKey
|
|
418
428
|
// for continuation-style non-tool events even when neither is present.
|
|
@@ -480,9 +490,13 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
480
490
|
}
|
|
481
491
|
} else if (event.type === "response.reasoning_summary_part.added") {
|
|
482
492
|
const entry = resolveEntry(event.item_id, event.output_index, "always");
|
|
483
|
-
if (entry?.item.type === "reasoning") {
|
|
493
|
+
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
484
494
|
entry.item.summary = entry.item.summary || [];
|
|
485
495
|
entry.item.summary.push(event.part);
|
|
496
|
+
if (!entry.summaryStarted) {
|
|
497
|
+
entry.summaryStarted = true;
|
|
498
|
+
stream.push({ type: "reasoning_summary_start", contentIndex: entry.blockContentIndex, partial: output });
|
|
499
|
+
}
|
|
486
500
|
}
|
|
487
501
|
} else if (event.type === "response.reasoning_summary_text.delta") {
|
|
488
502
|
const entry = resolveEntry(event.item_id, event.output_index, "always");
|
|
@@ -491,9 +505,10 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
491
505
|
const lastPart = entry.item.summary[entry.item.summary.length - 1];
|
|
492
506
|
if (lastPart) {
|
|
493
507
|
entry.block.thinking += event.delta;
|
|
508
|
+
entry.summaryBuffer += event.delta;
|
|
494
509
|
lastPart.text += event.delta;
|
|
495
510
|
stream.push({
|
|
496
|
-
type: "
|
|
511
|
+
type: "reasoning_summary_delta",
|
|
497
512
|
contentIndex: entry.blockContentIndex,
|
|
498
513
|
delta: event.delta,
|
|
499
514
|
partial: output,
|
|
@@ -507,9 +522,10 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
507
522
|
const lastPart = entry.item.summary[entry.item.summary.length - 1];
|
|
508
523
|
if (lastPart) {
|
|
509
524
|
entry.block.thinking += "\n\n";
|
|
525
|
+
entry.summaryBuffer += "\n\n";
|
|
510
526
|
lastPart.text += "\n\n";
|
|
511
527
|
stream.push({
|
|
512
|
-
type: "
|
|
528
|
+
type: "reasoning_summary_delta",
|
|
513
529
|
contentIndex: entry.blockContentIndex,
|
|
514
530
|
delta: "\n\n",
|
|
515
531
|
partial: output,
|
|
@@ -522,6 +538,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
522
538
|
const entry = resolveEntry(event.item_id, event.output_index, "always");
|
|
523
539
|
if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
|
|
524
540
|
entry.block.thinking += event.delta;
|
|
541
|
+
entry.rawBuffer += event.delta;
|
|
525
542
|
stream.push({
|
|
526
543
|
type: "thinking_delta",
|
|
527
544
|
contentIndex: entry.blockContentIndex,
|
|
@@ -608,12 +625,15 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
608
625
|
options?.onOutputItemDone?.(item);
|
|
609
626
|
const entry = resolveEntry(item.id, event.output_index, "never");
|
|
610
627
|
if (item.type === "reasoning") {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
628
|
+
// Prefer the streamed summary buffer only when it carries real text. When it
|
|
629
|
+
// holds only synthetic separators (e.g. a part.done arrived before/without any
|
|
630
|
+
// summary_text delta), fall back to the canonical `item.summary` from
|
|
631
|
+
// output_item.done so the materialized summaryText is not blank/separator-only.
|
|
632
|
+
const bufferSummary = entry?.summaryBuffer ?? "";
|
|
633
|
+
const itemSummary = item.summary?.map(part => part.text).join("\n\n") ?? "";
|
|
634
|
+
const summaryText = bufferSummary.trim() ? bufferSummary : itemSummary;
|
|
635
|
+
const rawText =
|
|
636
|
+
entry?.rawBuffer || (item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "");
|
|
617
637
|
const reasoningBlock =
|
|
618
638
|
entry?.block.type === "thinking"
|
|
619
639
|
? entry.block
|
|
@@ -621,14 +641,53 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
621
641
|
| ThinkingContent
|
|
622
642
|
| undefined);
|
|
623
643
|
if (reasoningBlock) {
|
|
624
|
-
|
|
644
|
+
const mutable = reasoningBlock as {
|
|
645
|
+
provenance?: "summary" | "raw" | "mixed";
|
|
646
|
+
summaryText?: string;
|
|
647
|
+
rawText?: string;
|
|
648
|
+
};
|
|
649
|
+
if (mutable.provenance === undefined) {
|
|
650
|
+
if (mutable.summaryText === undefined && summaryText) mutable.summaryText = summaryText;
|
|
651
|
+
if (mutable.rawText === undefined && rawText) mutable.rawText = rawText;
|
|
652
|
+
mutable.provenance =
|
|
653
|
+
summaryText && rawText ? "mixed" : summaryText ? "summary" : rawText ? "raw" : undefined;
|
|
654
|
+
}
|
|
655
|
+
// Finalized display string must exclude raw CoT when a summary exists.
|
|
656
|
+
// Derive it from the STORED write-once provenance fields (falling back to
|
|
657
|
+
// this event's locals only for a first classification) so a later or
|
|
658
|
+
// duplicate finalization carrying only raw can never overwrite a summary/
|
|
659
|
+
// mixed block's safe display with raw CoT. Raw-only stays raw.
|
|
660
|
+
{
|
|
661
|
+
const effSummary = mutable.summaryText ?? summaryText;
|
|
662
|
+
const effRaw = mutable.rawText ?? rawText;
|
|
663
|
+
reasoningBlock.thinking = mutable.provenance === "raw" ? effRaw : effSummary || effRaw;
|
|
664
|
+
}
|
|
625
665
|
reasoningBlock.thinkingSignature = JSON.stringify(item);
|
|
626
666
|
const reasoningBlockIndex =
|
|
627
667
|
entry?.block === reasoningBlock ? entry.blockContentIndex : output.content.indexOf(reasoningBlock);
|
|
668
|
+
if (summaryText) {
|
|
669
|
+
// If the summary text came only from the canonical item.summary (no
|
|
670
|
+
// streamed summary deltas/part.added), no reasoning_summary_start was
|
|
671
|
+
// emitted. Emit one now so consumers that open a summary on start
|
|
672
|
+
// (e.g. the Responses SSE encoder) don't receive an orphaned end.
|
|
673
|
+
if (!entry?.summaryStarted) {
|
|
674
|
+
stream.push({
|
|
675
|
+
type: "reasoning_summary_start",
|
|
676
|
+
contentIndex: reasoningBlockIndex,
|
|
677
|
+
partial: output,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
stream.push({
|
|
681
|
+
type: "reasoning_summary_end",
|
|
682
|
+
contentIndex: reasoningBlockIndex,
|
|
683
|
+
content: summaryText,
|
|
684
|
+
partial: output,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
628
687
|
stream.push({
|
|
629
688
|
type: "thinking_end",
|
|
630
689
|
contentIndex: reasoningBlockIndex,
|
|
631
|
-
content: thinking,
|
|
690
|
+
content: reasoningBlock.thinking,
|
|
632
691
|
partial: output,
|
|
633
692
|
});
|
|
634
693
|
}
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* translations impose on first-class pi-ai fields (service tier, cache
|
|
11
11
|
* markers, thinking budgets, tool-choice variants, …).
|
|
12
12
|
*
|
|
13
|
-
* The streaming wire is {@link AssistantMessageEvent} serialized
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* actual cost.
|
|
13
|
+
* The streaming wire is {@link AssistantMessageEvent} serialized as SSE. Public
|
|
14
|
+
* projections omit private raw reasoning and serialized Responses reasoning
|
|
15
|
+
* signatures while preserving provider-displayable summaries and genuine opaque
|
|
16
|
+
* signatures. Including `partial: AssistantMessage` on every delta is O(N²) in
|
|
17
|
+
* turn length on the wire — acceptable for the loopback / sidecar topology this
|
|
18
|
+
* transport is designed for; provider latency dominates the actual cost.
|
|
20
19
|
*
|
|
21
20
|
* Endpoint contract:
|
|
22
21
|
* POST /v1/pi/stream
|
|
@@ -25,7 +24,14 @@
|
|
|
25
24
|
* 200 JSON (stream=false): { message: AssistantMessage }
|
|
26
25
|
* 4xx/5xx: { error: { type, message } }
|
|
27
26
|
*/
|
|
28
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
AssistantMessage,
|
|
29
|
+
AssistantMessageEvent,
|
|
30
|
+
AssistantMessageEventStream,
|
|
31
|
+
Context,
|
|
32
|
+
SimpleStreamOptions,
|
|
33
|
+
ThinkingContent,
|
|
34
|
+
} from "../types";
|
|
29
35
|
|
|
30
36
|
export interface PiNativeParsedRequest {
|
|
31
37
|
modelId: string;
|
|
@@ -148,25 +154,272 @@ export function parseRequest(body: unknown, _headers?: Headers): PiNativeParsedR
|
|
|
148
154
|
const SSE_ENCODER = new TextEncoder();
|
|
149
155
|
const SSE_DONE = SSE_ENCODER.encode("data: [DONE]\n\n");
|
|
150
156
|
|
|
157
|
+
function isSerializedResponsesReasoningItem(signature: string): boolean {
|
|
158
|
+
try {
|
|
159
|
+
const parsed: unknown = JSON.parse(signature);
|
|
160
|
+
return (
|
|
161
|
+
typeof parsed === "object" &&
|
|
162
|
+
parsed !== null &&
|
|
163
|
+
!Array.isArray(parsed) &&
|
|
164
|
+
(parsed as { type?: unknown }).type === "reasoning"
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
* Clone a thinking block for public transport. Raw reasoning is private: omit
|
|
173
|
+
* raw-only blocks, retain only the displayable summary for mixed blocks, and
|
|
174
|
+
* never forward a serialized Responses reasoning item as a signature.
|
|
175
|
+
*/
|
|
176
|
+
function isResponsesFamilyApi(api: AssistantMessage["api"]): boolean {
|
|
177
|
+
return api === "openai-responses" || api === "openai-codex-responses";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sanitizeThinking(content: ThinkingContent, api: AssistantMessage["api"]): ThinkingContent | undefined {
|
|
181
|
+
if (isResponsesFamilyApi(api) && content.provenance === undefined) return undefined;
|
|
182
|
+
if (content.provenance === "raw") return undefined;
|
|
183
|
+
|
|
184
|
+
let thinking: string;
|
|
185
|
+
if (content.provenance === "mixed") {
|
|
186
|
+
if (content.summaryText === undefined) return undefined;
|
|
187
|
+
thinking = content.summaryText;
|
|
188
|
+
} else {
|
|
189
|
+
thinking = content.provenance === "summary" ? (content.summaryText ?? content.thinking) : content.thinking;
|
|
190
|
+
}
|
|
191
|
+
const signature =
|
|
192
|
+
content.thinkingSignature && isSerializedResponsesReasoningItem(content.thinkingSignature)
|
|
193
|
+
? undefined
|
|
194
|
+
: content.thinkingSignature;
|
|
195
|
+
const { rawText: _rawText, thinkingSignature: _thinkingSignature, ...rest } = content;
|
|
196
|
+
return signature === undefined ? { ...rest, thinking } : { ...rest, thinking, thinkingSignature: signature };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sanitizeMessage(message: AssistantMessage): AssistantMessage {
|
|
200
|
+
let changed = false;
|
|
201
|
+
const content: AssistantMessage["content"] = [];
|
|
202
|
+
for (const part of message.content) {
|
|
203
|
+
if (part.type !== "thinking") {
|
|
204
|
+
content.push(part);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const needsSanitizing =
|
|
208
|
+
(isResponsesFamilyApi(message.api) && part.provenance === undefined) ||
|
|
209
|
+
part.provenance !== undefined ||
|
|
210
|
+
part.rawText !== undefined ||
|
|
211
|
+
(part.thinkingSignature !== undefined && isSerializedResponsesReasoningItem(part.thinkingSignature));
|
|
212
|
+
if (!needsSanitizing) {
|
|
213
|
+
content.push(part);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const sanitized = sanitizeThinking(part, message.api);
|
|
217
|
+
changed = true;
|
|
218
|
+
if (sanitized !== undefined) content.push(sanitized);
|
|
219
|
+
}
|
|
220
|
+
return changed ? { ...message, content } : message;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function hasRawOrMixedThinking(partial: AssistantMessage, contentIndex: number): boolean {
|
|
224
|
+
const content = partial.content[contentIndex];
|
|
225
|
+
return content?.type === "thinking" && (content.provenance === "raw" || content.provenance === "mixed");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type ThinkingEvent = Extract<AssistantMessageEvent, { type: "thinking_start" | "thinking_delta" | "thinking_end" }>;
|
|
229
|
+
|
|
230
|
+
interface BufferedThinkingEvent {
|
|
231
|
+
event: ThinkingEvent;
|
|
232
|
+
sequence: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isFinalSafeThinking(partial: AssistantMessage, contentIndex: number): boolean {
|
|
236
|
+
const content = partial.content[contentIndex];
|
|
237
|
+
return (
|
|
238
|
+
content?.type === "thinking" &&
|
|
239
|
+
(!isResponsesFamilyApi(partial.api) || content.provenance !== undefined) &&
|
|
240
|
+
!hasRawOrMixedThinking(partial, contentIndex)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function maskBufferedThinking(message: AssistantMessage, contentIndexes: ReadonlySet<number>): AssistantMessage {
|
|
245
|
+
let changed = false;
|
|
246
|
+
const content = message.content.map((part, contentIndex) => {
|
|
247
|
+
if (!contentIndexes.has(contentIndex) || part.type !== "thinking") return part;
|
|
248
|
+
changed = true;
|
|
249
|
+
return { type: "thinking" as const, thinking: "", ...(part.itemId ? { itemId: part.itemId } : {}) };
|
|
250
|
+
});
|
|
251
|
+
return changed ? { ...message, content } : message;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function maskBufferedThinkingInEvent(
|
|
255
|
+
event: AssistantMessageEvent,
|
|
256
|
+
contentIndexes: ReadonlySet<number>,
|
|
257
|
+
): AssistantMessageEvent {
|
|
258
|
+
if (contentIndexes.size === 0) return event;
|
|
259
|
+
switch (event.type) {
|
|
260
|
+
case "done":
|
|
261
|
+
case "error":
|
|
262
|
+
case "toolChoiceIncapability":
|
|
263
|
+
return event;
|
|
264
|
+
case "start":
|
|
265
|
+
case "text_start":
|
|
266
|
+
case "text_delta":
|
|
267
|
+
case "text_end":
|
|
268
|
+
case "thinking_start":
|
|
269
|
+
case "thinking_delta":
|
|
270
|
+
case "thinking_end":
|
|
271
|
+
case "reasoning_summary_start":
|
|
272
|
+
case "reasoning_summary_delta":
|
|
273
|
+
case "reasoning_summary_end":
|
|
274
|
+
case "toolcall_start":
|
|
275
|
+
case "toolcall_delta":
|
|
276
|
+
case "toolcall_end":
|
|
277
|
+
return { ...event, partial: maskBufferedThinking(event.partial, contentIndexes) };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function withSummaryPartial<
|
|
282
|
+
T extends Extract<
|
|
283
|
+
AssistantMessageEvent,
|
|
284
|
+
{ type: "reasoning_summary_start" | "reasoning_summary_delta" | "reasoning_summary_end" }
|
|
285
|
+
>,
|
|
286
|
+
>(event: T, contentIndexes: ReadonlySet<number>, summaryText: string): T {
|
|
287
|
+
const partial = maskBufferedThinking(event.partial, contentIndexes);
|
|
288
|
+
const content = [...partial.content];
|
|
289
|
+
const original = event.partial.content[event.contentIndex];
|
|
290
|
+
content[event.contentIndex] = {
|
|
291
|
+
type: "thinking",
|
|
292
|
+
thinking: summaryText,
|
|
293
|
+
provenance: "summary",
|
|
294
|
+
summaryText,
|
|
295
|
+
...(original?.type === "thinking" && original.itemId ? { itemId: original.itemId } : {}),
|
|
296
|
+
};
|
|
297
|
+
return { ...event, partial: { ...partial, content } };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function sanitizeEvent(event: AssistantMessageEvent): AssistantMessageEvent | undefined {
|
|
301
|
+
switch (event.type) {
|
|
302
|
+
case "done":
|
|
303
|
+
return { ...event, message: sanitizeMessage(event.message) };
|
|
304
|
+
case "error":
|
|
305
|
+
return { ...event, error: sanitizeMessage(event.error) };
|
|
306
|
+
case "toolChoiceIncapability":
|
|
307
|
+
return event;
|
|
308
|
+
case "thinking_start":
|
|
309
|
+
case "thinking_delta":
|
|
310
|
+
case "thinking_end":
|
|
311
|
+
return hasRawOrMixedThinking(event.partial, event.contentIndex)
|
|
312
|
+
? undefined
|
|
313
|
+
: { ...event, partial: sanitizeMessage(event.partial) };
|
|
314
|
+
case "start":
|
|
315
|
+
case "text_start":
|
|
316
|
+
case "text_delta":
|
|
317
|
+
case "text_end":
|
|
318
|
+
case "reasoning_summary_start":
|
|
319
|
+
case "reasoning_summary_delta":
|
|
320
|
+
case "reasoning_summary_end":
|
|
321
|
+
case "toolcall_start":
|
|
322
|
+
case "toolcall_delta":
|
|
323
|
+
case "toolcall_end":
|
|
324
|
+
return { ...event, partial: sanitizeMessage(event.partial) };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Ship only public-safe {@link AssistantMessageEvent} projections. Unknown
|
|
330
|
+
* thinking blocks remain buffered until their terminal partial establishes that
|
|
331
|
+
* the provider-native block is safe; raw and mixed blocks never reach SSE.
|
|
162
332
|
*/
|
|
163
333
|
export function encodeStream(events: AssistantMessageEventStream): ReadableStream<Uint8Array> {
|
|
164
334
|
return new ReadableStream<Uint8Array>({
|
|
165
335
|
async start(controller) {
|
|
336
|
+
const bufferedThinking = new Map<number, BufferedThinkingEvent[]>();
|
|
337
|
+
const summaryTextByIndex = new Map<number, string>();
|
|
338
|
+
let sequence = 0;
|
|
339
|
+
const write = (event: AssistantMessageEvent): void => {
|
|
340
|
+
const sanitized = sanitizeEvent(event);
|
|
341
|
+
if (sanitized !== undefined) {
|
|
342
|
+
controller.enqueue(SSE_ENCODER.encode(`data: ${JSON.stringify(sanitized)}\n\n`));
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const emit = (event: AssistantMessageEvent): void => {
|
|
346
|
+
write(maskBufferedThinkingInEvent(event, new Set(bufferedThinking.keys())));
|
|
347
|
+
};
|
|
348
|
+
const flush = (buffered: BufferedThinkingEvent[]): void => {
|
|
349
|
+
for (const { event } of buffered.sort((a, b) => a.sequence - b.sequence)) emit(event);
|
|
350
|
+
};
|
|
351
|
+
const resolveBufferedThinking = (final: AssistantMessage): void => {
|
|
352
|
+
const ready: BufferedThinkingEvent[] = [];
|
|
353
|
+
for (const [contentIndex, buffered] of bufferedThinking) {
|
|
354
|
+
if (isFinalSafeThinking(final, contentIndex)) ready.push(...buffered);
|
|
355
|
+
}
|
|
356
|
+
bufferedThinking.clear();
|
|
357
|
+
flush(ready);
|
|
358
|
+
};
|
|
166
359
|
try {
|
|
167
360
|
for await (const event of events) {
|
|
168
|
-
|
|
169
|
-
|
|
361
|
+
switch (event.type) {
|
|
362
|
+
case "thinking_start":
|
|
363
|
+
case "thinking_delta": {
|
|
364
|
+
if (hasRawOrMixedThinking(event.partial, event.contentIndex)) {
|
|
365
|
+
bufferedThinking.delete(event.contentIndex);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
const buffered = bufferedThinking.get(event.contentIndex) ?? [];
|
|
369
|
+
buffered.push({ event, sequence: sequence++ });
|
|
370
|
+
bufferedThinking.set(event.contentIndex, buffered);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "thinking_end": {
|
|
374
|
+
const buffered = bufferedThinking.get(event.contentIndex) ?? [];
|
|
375
|
+
bufferedThinking.delete(event.contentIndex);
|
|
376
|
+
if (!isFinalSafeThinking(event.partial, event.contentIndex)) break;
|
|
377
|
+
buffered.push({ event, sequence: sequence++ });
|
|
378
|
+
flush(buffered);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case "done":
|
|
382
|
+
resolveBufferedThinking(event.message);
|
|
383
|
+
summaryTextByIndex.clear();
|
|
384
|
+
emit(event);
|
|
385
|
+
controller.enqueue(SSE_DONE);
|
|
386
|
+
controller.close();
|
|
387
|
+
return;
|
|
388
|
+
case "error":
|
|
389
|
+
resolveBufferedThinking(event.error);
|
|
390
|
+
summaryTextByIndex.clear();
|
|
391
|
+
emit(event);
|
|
392
|
+
controller.enqueue(SSE_DONE);
|
|
393
|
+
controller.close();
|
|
394
|
+
return;
|
|
395
|
+
case "reasoning_summary_start": {
|
|
396
|
+
summaryTextByIndex.set(event.contentIndex, "");
|
|
397
|
+
write(withSummaryPartial(event, new Set(bufferedThinking.keys()), ""));
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case "reasoning_summary_delta": {
|
|
401
|
+
const summaryText = `${summaryTextByIndex.get(event.contentIndex) ?? ""}${event.delta}`;
|
|
402
|
+
summaryTextByIndex.set(event.contentIndex, summaryText);
|
|
403
|
+
write(withSummaryPartial(event, new Set(bufferedThinking.keys()), summaryText));
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "reasoning_summary_end": {
|
|
407
|
+
const summaryText = event.content || summaryTextByIndex.get(event.contentIndex) || "";
|
|
408
|
+
summaryTextByIndex.delete(event.contentIndex);
|
|
409
|
+
write(withSummaryPartial(event, new Set(bufferedThinking.keys()), summaryText));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "start":
|
|
413
|
+
case "text_start":
|
|
414
|
+
case "text_delta":
|
|
415
|
+
case "text_end":
|
|
416
|
+
case "toolcall_start":
|
|
417
|
+
case "toolcall_delta":
|
|
418
|
+
case "toolcall_end":
|
|
419
|
+
case "toolChoiceIncapability":
|
|
420
|
+
emit(event);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
170
423
|
}
|
|
171
424
|
controller.enqueue(SSE_DONE);
|
|
172
425
|
controller.close();
|
package/src/types.ts
CHANGED
|
@@ -488,6 +488,9 @@ export interface ThinkingContent {
|
|
|
488
488
|
thinking: string;
|
|
489
489
|
thinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID
|
|
490
490
|
itemId?: string; // item.id from output_item.added, used to match output_item.done
|
|
491
|
+
readonly provenance?: "summary" | "raw" | "mixed";
|
|
492
|
+
readonly summaryText?: string;
|
|
493
|
+
readonly rawText?: string;
|
|
491
494
|
}
|
|
492
495
|
|
|
493
496
|
export interface RedactedThinkingContent {
|
|
@@ -729,6 +732,13 @@ export interface Tool<TParameters extends TSchema = TSchema> {
|
|
|
729
732
|
* calls route correctly. Absent for regular JSON function tools.
|
|
730
733
|
*/
|
|
731
734
|
customWireName?: string;
|
|
735
|
+
/**
|
|
736
|
+
* Optional safe projection for tool arguments or results. Extensions use this
|
|
737
|
+
* only for explicitly opt-in, display-safe summaries.
|
|
738
|
+
*/
|
|
739
|
+
safeSummary?: (kind: "args" | "result", value: unknown) => string | undefined;
|
|
740
|
+
/** Allowlisted argument/result field names for a safe fallback summary. */
|
|
741
|
+
safeSummaryFields?: { args?: string[]; result?: string[] };
|
|
732
742
|
}
|
|
733
743
|
|
|
734
744
|
export interface Context {
|
|
@@ -745,6 +755,9 @@ export type AssistantMessageEvent =
|
|
|
745
755
|
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
|
|
746
756
|
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
747
757
|
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
|
758
|
+
| { type: "reasoning_summary_start"; contentIndex: number; partial: AssistantMessage }
|
|
759
|
+
| { type: "reasoning_summary_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
760
|
+
| { type: "reasoning_summary_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
|
748
761
|
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
|
749
762
|
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
750
763
|
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
|
@@ -889,6 +902,12 @@ export interface AnthropicCompat extends ToolChoiceCompat {
|
|
|
889
902
|
supportsForcedToolChoice?: boolean;
|
|
890
903
|
/** Whether long prompt-cache retention (`ttl: "1h"`) is supported. Default: true for canonical Anthropic API. */
|
|
891
904
|
supportsLongCacheRetention?: boolean;
|
|
905
|
+
/**
|
|
906
|
+
* Prompt-cache transport accepted by this Anthropic-compatible endpoint.
|
|
907
|
+
* Canonical Anthropic defaults to `"automatic"`; noncanonical endpoints default
|
|
908
|
+
* to `"none"` and must explicitly opt into generated `"explicit"` markers.
|
|
909
|
+
*/
|
|
910
|
+
promptCacheMode?: "none" | "explicit" | "automatic";
|
|
892
911
|
}
|
|
893
912
|
|
|
894
913
|
/**
|
|
@@ -123,12 +123,16 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
123
123
|
|
|
124
124
|
push(event: T): void {
|
|
125
125
|
if (this.done) return;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
try {
|
|
127
|
+
if (this.isComplete(event)) {
|
|
128
|
+
const result = this.extractResult(event);
|
|
129
|
+
this.done = true;
|
|
130
|
+
this.resolveFinalResult(result);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.fail(error);
|
|
134
|
+
return;
|
|
130
135
|
}
|
|
131
|
-
|
|
132
136
|
this.deliver(event);
|
|
133
137
|
}
|
|
134
138
|
|