@oh-my-pi/pi-ai 8.12.1 → 8.12.4
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/package.json +2 -2
- package/src/utils/event-stream.ts +133 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.4",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@connectrpc/connect-node": "^2.1.1",
|
|
64
64
|
"@google/genai": "^1.38.0",
|
|
65
65
|
"@mistralai/mistralai": "^1.13.0",
|
|
66
|
-
"@oh-my-pi/pi-utils": "8.12.
|
|
66
|
+
"@oh-my-pi/pi-utils": "8.12.4",
|
|
67
67
|
"@sinclair/typebox": "^0.34.48",
|
|
68
68
|
"@smithy/node-http-handler": "^4.4.8",
|
|
69
69
|
"ajv": "^8.17.1",
|
|
@@ -2,15 +2,15 @@ import type { AssistantMessage, AssistantMessageEvent } from "../types";
|
|
|
2
2
|
|
|
3
3
|
// Generic event stream class for async iteration
|
|
4
4
|
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
protected queue: T[] = [];
|
|
6
|
+
protected waiting: ((value: IteratorResult<T>) => void)[] = [];
|
|
7
|
+
protected done = false;
|
|
8
|
+
protected finalResultPromise: Promise<R>;
|
|
9
|
+
protected resolveFinalResult!: (result: R) => void;
|
|
10
10
|
|
|
11
11
|
constructor(
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
protected isComplete: (event: T) => boolean,
|
|
13
|
+
protected extractResult: (event: T) => R,
|
|
14
14
|
) {
|
|
15
15
|
const { promise, resolve } = Promise.withResolvers<R>();
|
|
16
16
|
this.finalResultPromise = promise;
|
|
@@ -34,6 +34,15 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
protected deliver(event: T): void {
|
|
38
|
+
const waiter = this.waiting.shift();
|
|
39
|
+
if (waiter) {
|
|
40
|
+
waiter({ value: event, done: false });
|
|
41
|
+
} else {
|
|
42
|
+
this.queue.push(event);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
end(result?: R): void {
|
|
38
47
|
this.done = true;
|
|
39
48
|
if (result !== undefined) {
|
|
@@ -46,6 +55,13 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
protected endWaiting(): void {
|
|
59
|
+
while (this.waiting.length > 0) {
|
|
60
|
+
const waiter = this.waiting.shift()!;
|
|
61
|
+
waiter({ value: undefined as any, done: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
50
66
|
while (true) {
|
|
51
67
|
if (this.queue.length > 0) {
|
|
@@ -65,7 +81,23 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
// Delta events that can be batched for throttling
|
|
85
|
+
type DeltaEvent =
|
|
86
|
+
| { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
87
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
88
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage };
|
|
89
|
+
|
|
90
|
+
function isDeltaEvent(event: AssistantMessageEvent): event is DeltaEvent {
|
|
91
|
+
return event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta";
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
95
|
+
// Throttling state
|
|
96
|
+
private deltaBuffer: DeltaEvent[] = [];
|
|
97
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
98
|
+
private lastFlushTime = 0;
|
|
99
|
+
private readonly throttleMs = 50; // 20 updates/sec
|
|
100
|
+
|
|
69
101
|
constructor() {
|
|
70
102
|
super(
|
|
71
103
|
event => event.type === "done" || event.type === "error",
|
|
@@ -79,4 +111,98 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
79
111
|
},
|
|
80
112
|
);
|
|
81
113
|
}
|
|
114
|
+
|
|
115
|
+
override push(event: AssistantMessageEvent): void {
|
|
116
|
+
if (this.done) return;
|
|
117
|
+
|
|
118
|
+
// Check for completion first
|
|
119
|
+
if (this.isComplete(event)) {
|
|
120
|
+
this.flushDeltas(); // Flush any pending deltas before completing
|
|
121
|
+
this.done = true;
|
|
122
|
+
this.resolveFinalResult(this.extractResult(event));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Delta events get batched and throttled
|
|
126
|
+
if (isDeltaEvent(event)) {
|
|
127
|
+
this.deltaBuffer.push(event);
|
|
128
|
+
this.scheduleFlush();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Non-delta events flush pending deltas immediately, then emit
|
|
133
|
+
this.flushDeltas();
|
|
134
|
+
this.deliver(event);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override end(result?: AssistantMessage): void {
|
|
138
|
+
this.flushDeltas();
|
|
139
|
+
this.done = true;
|
|
140
|
+
if (result !== undefined) {
|
|
141
|
+
this.resolveFinalResult(result);
|
|
142
|
+
}
|
|
143
|
+
this.endWaiting();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private scheduleFlush(): void {
|
|
147
|
+
if (this.flushTimer) return; // Already scheduled
|
|
148
|
+
|
|
149
|
+
const now = performance.now();
|
|
150
|
+
const timeSinceLastFlush = now - this.lastFlushTime;
|
|
151
|
+
|
|
152
|
+
if (timeSinceLastFlush >= this.throttleMs) {
|
|
153
|
+
// Flush immediately if throttle window has passed
|
|
154
|
+
this.flushDeltas();
|
|
155
|
+
} else {
|
|
156
|
+
// Schedule flush for when throttle window expires
|
|
157
|
+
const delay = this.throttleMs - timeSinceLastFlush;
|
|
158
|
+
this.flushTimer = setTimeout(() => {
|
|
159
|
+
this.flushTimer = null;
|
|
160
|
+
this.flushDeltas();
|
|
161
|
+
}, delay);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private flushDeltas(): void {
|
|
166
|
+
if (this.flushTimer) {
|
|
167
|
+
clearTimeout(this.flushTimer);
|
|
168
|
+
this.flushTimer = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.deltaBuffer.length === 0) return;
|
|
172
|
+
|
|
173
|
+
// Merge consecutive deltas for the same content block and type
|
|
174
|
+
const merged = this.mergeDeltas(this.deltaBuffer);
|
|
175
|
+
this.deltaBuffer = [];
|
|
176
|
+
this.lastFlushTime = performance.now();
|
|
177
|
+
|
|
178
|
+
for (const event of merged) {
|
|
179
|
+
this.deliver(event);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private mergeDeltas(deltas: DeltaEvent[]): AssistantMessageEvent[] {
|
|
184
|
+
if (deltas.length === 0) return [];
|
|
185
|
+
if (deltas.length === 1) return [deltas[0]];
|
|
186
|
+
|
|
187
|
+
const result: AssistantMessageEvent[] = [];
|
|
188
|
+
let current = deltas[0];
|
|
189
|
+
|
|
190
|
+
for (let i = 1; i < deltas.length; i++) {
|
|
191
|
+
const next = deltas[i];
|
|
192
|
+
// Can merge if same type, same content index
|
|
193
|
+
if (next.type === current.type && next.contentIndex === current.contentIndex) {
|
|
194
|
+
current = {
|
|
195
|
+
...current,
|
|
196
|
+
delta: current.delta + next.delta,
|
|
197
|
+
partial: next.partial, // Use latest partial
|
|
198
|
+
} as DeltaEvent;
|
|
199
|
+
} else {
|
|
200
|
+
result.push(current);
|
|
201
|
+
current = next;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
result.push(current);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
82
208
|
}
|