@oh-my-pi/pi-ai 13.17.5 → 13.17.6

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.17.6] - 2026-04-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Anthropic first-event timeouts to exclude stream connection setup from the watchdog, preserve timeout-specific retry classification after local aborts, and reset retry state cleanly between attempts
10
+
5
11
  ## [13.17.5] - 2026-04-01
6
12
  ### Changed
7
13
 
@@ -1959,4 +1965,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
1959
1965
 
1960
1966
  ## [0.9.4] - 2025-11-26
1961
1967
 
1962
- Initial release with multi-provider LLM support.
1968
+ Initial release with multi-provider LLM support.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "13.17.5",
4
+ "version": "13.17.6",
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",
@@ -41,7 +41,7 @@
41
41
  "@aws-sdk/client-bedrock-runtime": "^3",
42
42
  "@bufbuild/protobuf": "^2.11",
43
43
  "@google/genai": "^1.43",
44
- "@oh-my-pi/pi-utils": "13.17.5",
44
+ "@oh-my-pi/pi-utils": "13.17.6",
45
45
  "@sinclair/typebox": "^0.34",
46
46
  "@smithy/node-http-handler": "^4.4",
47
47
  "ajv": "^8.18",
@@ -29,11 +29,13 @@ import type {
29
29
  Tool,
30
30
  ToolCall,
31
31
  ToolResultMessage,
32
+ Usage,
32
33
  } from "../types";
33
34
  import { isAnthropicOAuthToken, normalizeToolCallId, resolveCacheRetention } from "../utils";
35
+ import { createAbortSourceTracker } from "../utils/abort";
34
36
  import { AssistantMessageEventStream } from "../utils/event-stream";
35
37
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
36
- import { getStreamFirstEventTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
38
+ import { createFirstEventWatchdog, getStreamFirstEventTimeoutMs, markFirstStreamEvent } from "../utils/idle-iterator";
37
39
  import { parseStreamingJson } from "../utils/json-parse";
38
40
  import {
39
41
  buildCopilotDynamicHeaders,
@@ -581,6 +583,18 @@ export function isProviderRetryableError(error: unknown): boolean {
581
583
  );
582
584
  }
583
585
 
586
+ function createEmptyUsage(premiumRequests?: number): Usage {
587
+ return {
588
+ input: 0,
589
+ output: 0,
590
+ cacheRead: 0,
591
+ cacheWrite: 0,
592
+ totalTokens: 0,
593
+ ...(premiumRequests === undefined ? {} : { premiumRequests }),
594
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
595
+ };
596
+ }
597
+
584
598
  export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
585
599
  model: Model<"anthropic-messages">,
586
600
  context: Context,
@@ -608,18 +622,12 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
608
622
  api: model.api as Api,
609
623
  provider: model.provider,
610
624
  model: model.id,
611
- usage: {
612
- input: 0,
613
- output: 0,
614
- cacheRead: 0,
615
- cacheWrite: 0,
616
- totalTokens: 0,
617
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
618
- },
625
+ usage: createEmptyUsage(copilotDynamicHeaders?.premiumRequests),
619
626
  stopReason: "stop",
620
627
  timestamp: Date.now(),
621
628
  };
622
629
  let rawRequestDump: RawHttpRequestDump | undefined;
630
+ let activeAbortTracker = createAbortSourceTracker(options?.signal);
623
631
 
624
632
  try {
625
633
  let client: Anthropic;
@@ -675,22 +683,20 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
675
683
  let providerRetryAttempt = 0;
676
684
  let started = false;
677
685
  do {
678
- const requestAbortController = new AbortController();
679
- const requestSignal = options?.signal
680
- ? AbortSignal.any([options.signal, requestAbortController.signal])
681
- : requestAbortController.signal;
686
+ activeAbortTracker = createAbortSourceTracker(options?.signal);
687
+ const firstEventTimeoutAbortError = new Error(
688
+ "Anthropic stream timed out while waiting for the first event",
689
+ );
690
+ const { requestSignal } = activeAbortTracker;
682
691
  const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: requestSignal });
683
- if (copilotDynamicHeaders && output.usage.premiumRequests === undefined) {
684
- output.usage.premiumRequests = copilotDynamicHeaders.premiumRequests;
685
- }
686
692
 
687
693
  try {
688
- for await (const event of iterateWithIdleTimeout(anthropicStream, {
689
- firstItemTimeoutMs: getStreamFirstEventTimeoutMs(),
690
- errorMessage: "Anthropic stream stalled while waiting for the next event",
691
- firstItemErrorMessage: "Anthropic stream timed out while waiting for the first event",
692
- onFirstItemTimeout: () => requestAbortController.abort(),
693
- })) {
694
+ await anthropicStream.withResponse();
695
+ const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(), () =>
696
+ activeAbortTracker.abortLocally(firstEventTimeoutAbortError),
697
+ );
698
+
699
+ for await (const event of markFirstStreamEvent(anthropicStream, firstEventWatchdog)) {
694
700
  started = true;
695
701
  if (event.type === "message_start") {
696
702
  output.responseId = event.message.id;
@@ -857,7 +863,11 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
857
863
  }
858
864
  }
859
865
 
860
- if (options?.signal?.aborted) {
866
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
867
+ if (firstEventTimeoutError) {
868
+ throw firstEventTimeoutError;
869
+ }
870
+ if (activeAbortTracker.wasCallerAbort()) {
861
871
  throw new Error("Request was aborted");
862
872
  }
863
873
 
@@ -866,23 +876,28 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
866
876
  }
867
877
  break; // Stream completed successfully
868
878
  } catch (streamError) {
879
+ const streamFailure = activeAbortTracker.getLocalAbortReason() ?? streamError;
869
880
  // Transient stream parse errors (truncated JSON) are retryable even after content
870
881
  // has started streaming, since the partial response is unusable anyway.
871
882
  // Rate-limit/overload errors are only retried before content starts.
872
- const isTransient = isTransientStreamParseError(streamError);
883
+ const isTransient = isTransientStreamParseError(streamFailure);
873
884
  if (
874
- options?.signal?.aborted ||
885
+ activeAbortTracker.wasCallerAbort() ||
875
886
  providerRetryAttempt >= PROVIDER_MAX_RETRIES ||
876
887
  (!isTransient && firstTokenTime !== undefined) ||
877
- (!isTransient && !isProviderRetryableError(streamError))
888
+ (!isTransient && !isProviderRetryableError(streamFailure))
878
889
  ) {
879
- throw streamError;
890
+ throw streamFailure;
880
891
  }
881
892
  providerRetryAttempt++;
882
893
  const delayMs = PROVIDER_BASE_DELAY_MS * 2 ** (providerRetryAttempt - 1);
883
894
  await abortableSleep(delayMs, options?.signal);
884
895
  // Reset output state for clean retry
885
896
  output.content.length = 0;
897
+ output.responseId = undefined;
898
+ output.errorMessage = undefined;
899
+ output.providerPayload = undefined;
900
+ output.usage = createEmptyUsage(copilotDynamicHeaders?.premiumRequests);
886
901
  output.stopReason = "stop";
887
902
  firstTokenTime = undefined;
888
903
  started = false;
@@ -894,9 +909,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
894
909
  stream.push({ type: "done", reason: output.stopReason, message: output });
895
910
  stream.end();
896
911
  } catch (error) {
897
- for (const block of output.content) delete (block as any).index;
898
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
899
- output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
912
+ for (const block of output.content) delete (block as { index?: number }).index;
913
+ const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
914
+ output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
915
+ output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
900
916
  output.duration = Date.now() - startTime;
901
917
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
902
918
  stream.push({ type: "error", reason: output.stopReason, error: output });