@oh-my-pi/pi-ai 14.7.2 → 14.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "14.7.2",
4
+ "version": "14.7.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,8 +46,8 @@
46
46
  "@aws-sdk/credential-provider-node": "^3.972.39",
47
47
  "@bufbuild/protobuf": "^2.12.0",
48
48
  "@google/genai": "^1.52.0",
49
- "@oh-my-pi/pi-natives": "14.7.2",
50
- "@oh-my-pi/pi-utils": "14.7.2",
49
+ "@oh-my-pi/pi-natives": "14.7.3",
50
+ "@oh-my-pi/pi-utils": "14.7.3",
51
51
  "@sinclair/typebox": "^0.34.49",
52
52
  "@smithy/node-http-handler": "^4.6.1",
53
53
  "ajv": "^8.20.0",
@@ -8,7 +8,7 @@ import type {
8
8
  MessageParam,
9
9
  RawMessageStreamEvent,
10
10
  } from "@anthropic-ai/sdk/resources/messages";
11
- import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils";
11
+ import { $env, abortableSleep, isEnoent, readSseEvents } from "@oh-my-pi/pi-utils";
12
12
  import { hasOpus47ApiRestrictions, mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
13
13
  import { calculateCost } from "../models";
14
14
  import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
@@ -658,18 +658,6 @@ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]):
658
658
  // We surface the resulting provider error ourselves, so keep the SDK quiet.
659
659
  const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
660
660
 
661
- interface ServerSentEvent {
662
- event: string | null;
663
- data: string;
664
- raw: string[];
665
- }
666
-
667
- interface SseDecoderState {
668
- event: string | null;
669
- data: string[];
670
- raw: string[];
671
- }
672
-
673
661
  const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
674
662
  "message_start",
675
663
  "message_delta",
@@ -679,136 +667,6 @@ const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
679
667
  "content_block_stop",
680
668
  ]);
681
669
 
682
- function flushSseEvent(state: SseDecoderState): ServerSentEvent | null {
683
- if (!state.event && state.data.length === 0) {
684
- return null;
685
- }
686
-
687
- const event: ServerSentEvent = {
688
- event: state.event,
689
- data: state.data.join("\n"),
690
- raw: [...state.raw],
691
- };
692
- state.event = null;
693
- state.data = [];
694
- state.raw = [];
695
- return event;
696
- }
697
-
698
- function decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {
699
- if (line === "") {
700
- return flushSseEvent(state);
701
- }
702
-
703
- state.raw.push(line);
704
- if (line.startsWith(":")) {
705
- return null;
706
- }
707
-
708
- const delimiterIndex = line.indexOf(":");
709
- const fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);
710
- let value = delimiterIndex === -1 ? "" : line.slice(delimiterIndex + 1);
711
- if (value.startsWith(" ")) {
712
- value = value.slice(1);
713
- }
714
-
715
- if (fieldName === "event") {
716
- state.event = value;
717
- } else if (fieldName === "data") {
718
- state.data.push(value);
719
- }
720
-
721
- return null;
722
- }
723
-
724
- function nextLineBreakIndex(text: string): number {
725
- const carriageReturnIndex = text.indexOf("\r");
726
- const newlineIndex = text.indexOf("\n");
727
- if (carriageReturnIndex === -1) {
728
- return newlineIndex;
729
- }
730
- if (newlineIndex === -1) {
731
- return carriageReturnIndex;
732
- }
733
- return Math.min(carriageReturnIndex, newlineIndex);
734
- }
735
-
736
- function consumeLine(text: string): { line: string; rest: string } | null {
737
- const lineBreakIndex = nextLineBreakIndex(text);
738
- if (lineBreakIndex === -1) {
739
- return null;
740
- }
741
-
742
- let nextIndex = lineBreakIndex + 1;
743
- if (text[lineBreakIndex] === "\r" && text[nextIndex] === "\n") {
744
- nextIndex += 1;
745
- }
746
-
747
- return {
748
- line: text.slice(0, lineBreakIndex),
749
- rest: text.slice(nextIndex),
750
- };
751
- }
752
-
753
- async function* iterateSseMessages(
754
- body: ReadableStream<Uint8Array>,
755
- signal?: AbortSignal,
756
- ): AsyncGenerator<ServerSentEvent> {
757
- const reader = body.getReader();
758
- const decoder = new TextDecoder();
759
- const state: SseDecoderState = { event: null, data: [], raw: [] };
760
- let buffer = "";
761
-
762
- try {
763
- while (true) {
764
- if (signal?.aborted) {
765
- throw new Error("Request was aborted");
766
- }
767
-
768
- const { value, done } = await reader.read();
769
- if (done) {
770
- break;
771
- }
772
-
773
- buffer += decoder.decode(value, { stream: true });
774
- let consumed = consumeLine(buffer);
775
- while (consumed) {
776
- buffer = consumed.rest;
777
- const event = decodeSseLine(consumed.line, state);
778
- if (event) {
779
- yield event;
780
- }
781
- consumed = consumeLine(buffer);
782
- }
783
- }
784
-
785
- buffer += decoder.decode();
786
- let consumed = consumeLine(buffer);
787
- while (consumed) {
788
- buffer = consumed.rest;
789
- const event = decodeSseLine(consumed.line, state);
790
- if (event) {
791
- yield event;
792
- }
793
- consumed = consumeLine(buffer);
794
- }
795
-
796
- if (buffer.length > 0) {
797
- const event = decodeSseLine(buffer, state);
798
- if (event) {
799
- yield event;
800
- }
801
- }
802
-
803
- const trailingEvent = flushSseEvent(state);
804
- if (trailingEvent) {
805
- yield trailingEvent;
806
- }
807
- } finally {
808
- reader.releaseLock();
809
- }
810
- }
811
-
812
670
  async function* iterateAnthropicEvents(
813
671
  response: Response,
814
672
  signal?: AbortSignal,
@@ -820,7 +678,7 @@ async function* iterateAnthropicEvents(
820
678
  let sawMessageStart = false;
821
679
  let sawMessageEnd = false;
822
680
 
823
- for await (const sse of iterateSseMessages(response.body, signal)) {
681
+ for await (const sse of readSseEvents(response.body, signal)) {
824
682
  if (sse.event === "error") {
825
683
  throw new Error(sse.data);
826
684
  }
@@ -105,23 +105,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
105
105
  }
106
106
  }
107
107
 
108
- // Delta events that can be batched for throttling
109
- type DeltaEvent =
110
- | { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
111
- | { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
112
- | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage };
113
-
114
- function isDeltaEvent(event: AssistantMessageEvent): event is DeltaEvent {
115
- return event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta";
116
- }
117
-
118
108
  export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
119
- // Throttling state
120
- #deltaBuffer: DeltaEvent[] = [];
121
- #flushTimer?: NodeJS.Timeout;
122
- #lastFlushTime = 0;
123
- readonly #throttleMs = 50; // 20 updates/sec
124
-
125
109
  constructor() {
126
110
  super(
127
111
  event => event.type === "done" || event.type === "error",
@@ -139,103 +123,20 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
139
123
  override push(event: AssistantMessageEvent): void {
140
124
  if (this.done) return;
141
125
 
142
- // Check for completion first
126
+ // Completion resolves the final result and still emits the terminal event.
143
127
  if (this.isComplete(event)) {
144
- this.#flushDeltas(); // Flush any pending deltas before completing
145
128
  this.done = true;
146
129
  this.resolveFinalResult(this.extractResult(event));
147
130
  }
148
131
 
149
- // Delta events get batched and throttled
150
- if (isDeltaEvent(event)) {
151
- this.#deltaBuffer.push(event);
152
- this.#scheduleFlush();
153
- return;
154
- }
155
-
156
- // Non-delta events flush pending deltas immediately, then emit
157
- this.#flushDeltas();
158
132
  this.deliver(event);
159
133
  }
160
134
 
161
135
  override end(result?: AssistantMessage): void {
162
- this.#flushDeltas();
163
136
  this.done = true;
164
137
  if (result !== undefined) {
165
138
  this.resolveFinalResult(result);
166
139
  }
167
140
  this.endWaiting();
168
141
  }
169
-
170
- override fail(err: unknown): void {
171
- if (this.#flushTimer) {
172
- clearTimeout(this.#flushTimer);
173
- this.#flushTimer = undefined;
174
- }
175
- this.#deltaBuffer = [];
176
- super.fail(err);
177
- }
178
-
179
- #scheduleFlush(): void {
180
- if (this.#flushTimer) return; // Already scheduled
181
-
182
- const now = Bun.nanoseconds();
183
- const timeSinceLastFlush = (now - this.#lastFlushTime) / 1e6;
184
-
185
- if (timeSinceLastFlush >= this.#throttleMs) {
186
- // Flush immediately if throttle window has passed
187
- this.#flushDeltas();
188
- } else {
189
- // Schedule flush for when throttle window expires
190
- const delay = this.#throttleMs - timeSinceLastFlush;
191
- this.#flushTimer = setTimeout(() => {
192
- this.#flushTimer = undefined;
193
- this.#flushDeltas();
194
- }, delay);
195
- }
196
- }
197
-
198
- #flushDeltas(): void {
199
- if (this.#flushTimer) {
200
- clearTimeout(this.#flushTimer);
201
- this.#flushTimer = undefined;
202
- }
203
-
204
- if (this.#deltaBuffer.length === 0) return;
205
-
206
- // Merge consecutive deltas for the same content block and type
207
- const merged = this.#mergeDeltas(this.#deltaBuffer);
208
- this.#deltaBuffer = [];
209
- this.#lastFlushTime = Bun.nanoseconds();
210
-
211
- for (const event of merged) {
212
- this.deliver(event);
213
- }
214
- }
215
-
216
- #mergeDeltas(deltas: DeltaEvent[]): AssistantMessageEvent[] {
217
- if (deltas.length === 0) return [];
218
- if (deltas.length === 1) return [deltas[0]];
219
-
220
- const result: AssistantMessageEvent[] = [];
221
- let current = deltas[0];
222
-
223
- for (let i = 1; i < deltas.length; i++) {
224
- const next = deltas[i];
225
- // Can merge if same type, same content index
226
- if (next.type === current.type && next.contentIndex === current.contentIndex) {
227
- current = {
228
- ...current,
229
- delta: current.delta + next.delta,
230
- partial: next.partial, // Use latest partial
231
- } as DeltaEvent;
232
- } else {
233
- result.push(current);
234
- current = next;
235
- }
236
- }
237
- result.push(current);
238
-
239
- return result;
240
- }
241
142
  }