@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "8.12.1",
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.1",
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
- private queue: T[] = [];
6
- private waiting: ((value: IteratorResult<T>) => void)[] = [];
7
- private done = false;
8
- private finalResultPromise: Promise<R>;
9
- private resolveFinalResult!: (result: R) => void;
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
- private isComplete: (event: T) => boolean,
13
- private extractResult: (event: T) => R,
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
  }