@oh-my-pi/pi-ai 16.5.2 → 17.0.0

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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.0] - 2026-07-15
6
+
7
+ ### Changed
8
+
9
+ - Improved Ollama streaming performance by parsing NDJSON response bytes directly instead of decoding and buffering network chunks as text.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Cursor TLS connection resets causing process-fatal uncaught exceptions, allowing the active turn to fail or retry gracefully without terminating the session.
14
+ - Fixed Amazon Bedrock stream error handling to correctly handle non-Error values that cannot be serialized by JSON.stringify.
15
+
5
16
  ## [16.5.2] - 2026-07-14
6
17
 
7
18
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.5.2",
4
+ "version": "17.0.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "16.5.2",
42
- "@oh-my-pi/pi-utils": "16.5.2",
43
- "@oh-my-pi/pi-wire": "16.5.2",
41
+ "@oh-my-pi/pi-catalog": "17.0.0",
42
+ "@oh-my-pi/pi-utils": "17.0.0",
43
+ "@oh-my-pi/pi-wire": "17.0.0",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -510,7 +510,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
510
510
  for (const block of output.content) {
511
511
  if (block.type === "toolCall") clearStreamingPartialJson(block);
512
512
  }
513
- const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
513
+ let baseMessage: string;
514
+ try {
515
+ baseMessage = error instanceof Error ? error.message : (JSON.stringify(error) ?? String(error));
516
+ } catch {
517
+ baseMessage = String(error);
518
+ }
514
519
  // Enrich error with thinking block diagnostics for signature-related failures
515
520
  let diagnostics = "";
516
521
  if (baseMessage.includes("signature") || baseMessage.includes("thinking")) {
@@ -351,6 +351,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
351
351
  let h2Request: http2.ClientHttp2Stream | null = null;
352
352
  let heartbeatTimer: NodeJS.Timeout | null = null;
353
353
  let debugResponseLogPromise: Promise<RequestDebugResponseLog | undefined> | undefined;
354
+ const h2Completion = Promise.withResolvers<void>();
355
+ let resolveH2: (() => void) | undefined = h2Completion.resolve;
354
356
 
355
357
  try {
356
358
  const apiKey = options?.apiKey;
@@ -406,6 +408,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
406
408
  } else {
407
409
  h2Client = http2.connect(baseUrl);
408
410
  }
411
+ h2Client.on("error", h2Completion.reject);
409
412
 
410
413
  h2Request = h2Client.request(requestHeaders);
411
414
 
@@ -449,8 +452,6 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
449
452
  conversationStateCache.set(conversationId, checkpoint);
450
453
  };
451
454
 
452
- let resolveH2: (() => void) | undefined;
453
-
454
455
  h2Request.on("response", headers => {
455
456
  debugResponseLogPromise = debugSession?.openResponseLog(
456
457
  `HTTP/2 ${headers[":status"] ?? ""}`.trim(),
@@ -517,8 +518,6 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
517
518
  }
518
519
  });
519
520
 
520
- h2Request.write(frameConnectMessage(requestBytes));
521
-
522
521
  const sendHeartbeat = () => {
523
522
  if (!h2Request || h2Request.closed) {
524
523
  return;
@@ -530,57 +529,55 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
530
529
  h2Request.write(frameConnectMessage(heartbeatBytes));
531
530
  };
532
531
 
533
- heartbeatTimer = setInterval(sendHeartbeat, 5000);
534
-
535
- await new Promise<void>((resolve, reject) => {
536
- resolveH2 = resolve;
532
+ const closeDebugLog = async (): Promise<void> => {
533
+ const log = await debugResponseLogPromise;
534
+ await log?.close();
535
+ };
537
536
 
538
- const closeDebugLog = async (): Promise<void> => {
539
- const log = await debugResponseLogPromise;
540
- await log?.close();
541
- };
537
+ h2Request.on("trailers", trailers => {
538
+ const status = trailers["grpc-status"];
539
+ const msg = trailers["grpc-message"];
540
+ if (status && status !== "0") {
541
+ void closeDebugLog().finally(() => {
542
+ h2Completion.reject(
543
+ new AIError.ProviderResponseError(
544
+ `gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
545
+ { kind: "envelope" },
546
+ ),
547
+ );
548
+ });
549
+ }
550
+ });
542
551
 
543
- h2Request!.on("trailers", trailers => {
544
- const status = trailers["grpc-status"];
545
- const msg = trailers["grpc-message"];
546
- if (status && status !== "0") {
547
- void closeDebugLog().finally(() => {
548
- reject(
549
- new AIError.ProviderResponseError(
550
- `gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
551
- { kind: "envelope" },
552
- ),
553
- );
554
- });
555
- }
556
- });
552
+ h2Request.on("end", () => {
553
+ resolveH2 = undefined;
554
+ void closeDebugLog()
555
+ .then(() => {
556
+ if (endStreamError) {
557
+ h2Completion.reject(endStreamError);
558
+ return;
559
+ }
560
+ h2Completion.resolve();
561
+ })
562
+ .catch(h2Completion.reject);
563
+ });
557
564
 
558
- h2Request!.on("end", () => {
559
- resolveH2 = undefined;
560
- void closeDebugLog()
561
- .then(() => {
562
- if (endStreamError) {
563
- reject(endStreamError);
564
- return;
565
- }
566
- resolve();
567
- })
568
- .catch(reject);
569
- });
565
+ h2Request.on("error", error => {
566
+ void closeDebugLog().finally(() => h2Completion.reject(error));
567
+ });
570
568
 
571
- h2Request!.on("error", error => {
572
- void closeDebugLog().finally(() => reject(error));
569
+ if (options?.signal) {
570
+ options.signal.addEventListener("abort", () => {
571
+ h2Request?.close();
572
+ void closeDebugLog().finally(() => {
573
+ h2Completion.reject(new AIError.AbortError());
574
+ });
573
575
  });
576
+ }
574
577
 
575
- if (options?.signal) {
576
- options.signal.addEventListener("abort", () => {
577
- h2Request?.close();
578
- void closeDebugLog().finally(() => {
579
- reject(new AIError.AbortError());
580
- });
581
- });
582
- }
583
- });
578
+ h2Request.write(frameConnectMessage(requestBytes));
579
+ heartbeatTimer = setInterval(sendHeartbeat, 5000);
580
+ await h2Completion.promise;
584
581
 
585
582
  endCurrentTextBlock(output, stream, state);
586
583
  endCurrentThinkingBlock(output, stream, state);
@@ -1,4 +1,4 @@
1
- import { fetchWithRetry, parseStreamingJson } from "@oh-my-pi/pi-utils";
1
+ import { fetchWithRetry, parseStreamingJson, readJsonl } from "@oh-my-pi/pi-utils";
2
2
  import * as AIError from "../error";
3
3
  import { getEnvApiKey } from "../stream";
4
4
  import type {
@@ -347,36 +347,6 @@ async function captureHttpErrorResponse(response: Response): Promise<CapturedHtt
347
347
  };
348
348
  }
349
349
 
350
- async function* iterateNdjson(stream: ReadableStream<Uint8Array>): AsyncGenerator<OllamaChatChunk> {
351
- const reader = stream.getReader();
352
- const decoder = new TextDecoder();
353
- let buffer = "";
354
- while (true) {
355
- const { done, value } = await reader.read();
356
- if (done) {
357
- break;
358
- }
359
- buffer += decoder.decode(value, { stream: true });
360
- while (true) {
361
- const newlineIndex = buffer.indexOf("\n");
362
- if (newlineIndex < 0) {
363
- break;
364
- }
365
- const line = buffer.slice(0, newlineIndex).trim();
366
- buffer = buffer.slice(newlineIndex + 1);
367
- if (!line) {
368
- continue;
369
- }
370
- yield JSON.parse(line) as OllamaChatChunk;
371
- }
372
- }
373
- buffer += decoder.decode();
374
- const tail = buffer.trim();
375
- if (tail) {
376
- yield JSON.parse(tail) as OllamaChatChunk;
377
- }
378
- }
379
-
380
350
  function createEmptyOutput(model: Model<"ollama-chat">): AssistantMessage {
381
351
  return {
382
352
  role: "assistant",
@@ -622,7 +592,7 @@ const streamOllamaOnce = (
622
592
  });
623
593
  }
624
594
  stream.push({ type: "start", partial: output });
625
- for await (const chunk of iterateNdjson(response.body)) {
595
+ for await (const chunk of readJsonl<OllamaChatChunk>(response.body)) {
626
596
  if (chunk.message?.thinking) {
627
597
  suppressHealedThinking = true;
628
598
  endActiveTextBlock();