@oh-my-pi/pi-ai 16.2.1 → 16.2.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/auth-gateway/server.d.ts +0 -19
  3. package/dist/types/auth-retry.d.ts +2 -10
  4. package/dist/types/auth-storage.d.ts +1 -2
  5. package/dist/types/dialect/demotion.d.ts +22 -0
  6. package/dist/types/dialect/index.d.ts +2 -0
  7. package/dist/types/error/abort.d.ts +14 -0
  8. package/dist/types/error/auth-classify.d.ts +16 -0
  9. package/dist/types/error/auth.d.ts +27 -0
  10. package/dist/types/error/aws.d.ts +23 -0
  11. package/dist/types/error/classes.d.ts +102 -0
  12. package/dist/types/error/finalize.d.ts +39 -0
  13. package/dist/types/error/flags.d.ts +70 -0
  14. package/dist/types/error/format.d.ts +20 -0
  15. package/dist/types/error/gateway.d.ts +20 -0
  16. package/dist/types/error/index.d.ts +13 -0
  17. package/dist/types/error/oauth.d.ts +43 -0
  18. package/dist/types/error/provider.d.ts +42 -0
  19. package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
  20. package/dist/types/error/retryable.d.ts +27 -0
  21. package/dist/types/error/validation.d.ts +32 -0
  22. package/dist/types/index.d.ts +1 -3
  23. package/dist/types/providers/amazon-bedrock.d.ts +0 -5
  24. package/dist/types/providers/anthropic-client.d.ts +2 -16
  25. package/dist/types/providers/anthropic.d.ts +6 -1
  26. package/dist/types/providers/aws-eventstream.d.ts +2 -1
  27. package/dist/types/providers/cursor.d.ts +8 -7
  28. package/dist/types/providers/google-gemini-cli.d.ts +0 -5
  29. package/dist/types/providers/google-shared.d.ts +0 -5
  30. package/dist/types/providers/ollama.d.ts +0 -5
  31. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
  32. package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
  33. package/dist/types/providers/openai-codex-responses.d.ts +2 -2
  34. package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
  35. package/dist/types/providers/openai-responses-wire.d.ts +1 -1
  36. package/dist/types/providers/openai-responses.d.ts +3 -2
  37. package/dist/types/providers/openai-shared.d.ts +8 -6
  38. package/dist/types/providers/pi-native-client.d.ts +0 -9
  39. package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
  40. package/dist/types/types.d.ts +6 -0
  41. package/dist/types/usage/openai-codex-base-url.d.ts +17 -0
  42. package/dist/types/utils/block-symbols.d.ts +20 -0
  43. package/dist/types/utils/openai-http.d.ts +4 -8
  44. package/dist/types/utils/retry.d.ts +2 -14
  45. package/dist/types/utils/stream-markup-healing.d.ts +14 -5
  46. package/dist/types/utils/thinking-loop.d.ts +3 -1
  47. package/package.json +8 -4
  48. package/src/api-registry.ts +3 -1
  49. package/src/auth-broker/discover.ts +7 -2
  50. package/src/auth-broker/remote-store.ts +8 -7
  51. package/src/auth-gateway/server.ts +27 -115
  52. package/src/auth-retry.ts +9 -21
  53. package/src/auth-storage.ts +34 -49
  54. package/src/dialect/demotion.ts +31 -0
  55. package/src/dialect/factory.ts +0 -2
  56. package/src/dialect/index.ts +2 -0
  57. package/src/dialect/owned-stream.ts +0 -1
  58. package/src/dialect/thinking.ts +22 -10
  59. package/src/error/abort.ts +18 -0
  60. package/src/error/auth-classify.ts +30 -0
  61. package/src/error/auth.ts +48 -0
  62. package/src/error/aws.ts +31 -0
  63. package/src/error/classes.ts +186 -0
  64. package/src/error/finalize.ts +69 -0
  65. package/src/error/flags.ts +486 -0
  66. package/src/error/format.ts +36 -0
  67. package/src/error/gateway.ts +96 -0
  68. package/src/error/index.ts +13 -0
  69. package/src/error/oauth.ts +58 -0
  70. package/src/error/provider.ts +56 -0
  71. package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
  72. package/src/error/retryable.ts +70 -0
  73. package/src/error/validation.ts +44 -0
  74. package/src/index.ts +1 -3
  75. package/src/providers/amazon-bedrock.ts +61 -57
  76. package/src/providers/anthropic-client.ts +13 -41
  77. package/src/providers/anthropic-messages-server.ts +9 -2
  78. package/src/providers/anthropic.ts +84 -147
  79. package/src/providers/aws-credentials.ts +41 -11
  80. package/src/providers/aws-eventstream.ts +12 -21
  81. package/src/providers/azure-openai-responses.ts +27 -17
  82. package/src/providers/cursor.ts +59 -53
  83. package/src/providers/devin.ts +28 -20
  84. package/src/providers/gitlab-duo-workflow.ts +30 -10
  85. package/src/providers/gitlab-duo.ts +22 -6
  86. package/src/providers/google-auth.ts +15 -5
  87. package/src/providers/google-gemini-cli.ts +46 -60
  88. package/src/providers/google-shared.ts +40 -38
  89. package/src/providers/google-vertex.ts +3 -2
  90. package/src/providers/google.ts +5 -1
  91. package/src/providers/mock.ts +10 -7
  92. package/src/providers/ollama.ts +40 -30
  93. package/src/providers/openai-chat-server.ts +2 -1
  94. package/src/providers/openai-codex/request-transformer.ts +13 -12
  95. package/src/providers/openai-codex/response-handler.ts +1 -1
  96. package/src/providers/openai-codex-responses.ts +931 -1012
  97. package/src/providers/openai-completions.ts +46 -36
  98. package/src/providers/openai-responses-server-schema.ts +3 -0
  99. package/src/providers/openai-responses-server.ts +82 -30
  100. package/src/providers/openai-responses-wire.ts +1 -1
  101. package/src/providers/openai-responses.ts +50 -24
  102. package/src/providers/openai-shared.ts +118 -51
  103. package/src/providers/pi-native-client.ts +12 -18
  104. package/src/providers/pi-native-server.ts +9 -6
  105. package/src/providers/register-builtins.ts +5 -2
  106. package/src/providers/transform-messages.ts +31 -63
  107. package/src/registry/alibaba-coding-plan.ts +6 -5
  108. package/src/registry/api-key-login.ts +4 -3
  109. package/src/registry/api-key-validation.ts +4 -3
  110. package/src/registry/cloudflare-ai-gateway.ts +4 -3
  111. package/src/registry/coreweave.ts +2 -1
  112. package/src/registry/deepseek.ts +2 -1
  113. package/src/registry/google-antigravity.ts +2 -1
  114. package/src/registry/google-gemini-cli.ts +2 -1
  115. package/src/registry/kagi.ts +4 -3
  116. package/src/registry/kilo.ts +32 -10
  117. package/src/registry/litellm.ts +4 -3
  118. package/src/registry/llama-cpp.ts +3 -2
  119. package/src/registry/lm-studio.ts +3 -2
  120. package/src/registry/nvidia.ts +7 -7
  121. package/src/registry/oauth/anthropic.ts +23 -6
  122. package/src/registry/oauth/callback-server.ts +5 -3
  123. package/src/registry/oauth/cursor.ts +18 -4
  124. package/src/registry/oauth/devin.ts +14 -3
  125. package/src/registry/oauth/github-copilot.ts +21 -10
  126. package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
  127. package/src/registry/oauth/gitlab-duo.ts +30 -6
  128. package/src/registry/oauth/google-antigravity.ts +14 -5
  129. package/src/registry/oauth/google-gemini-cli.ts +24 -7
  130. package/src/registry/oauth/google-oauth-shared.ts +6 -3
  131. package/src/registry/oauth/index.ts +19 -8
  132. package/src/registry/oauth/kimi.ts +40 -10
  133. package/src/registry/oauth/openai-codex.ts +26 -11
  134. package/src/registry/oauth/opencode.ts +4 -3
  135. package/src/registry/oauth/perplexity.ts +33 -11
  136. package/src/registry/oauth/xai-oauth.ts +62 -18
  137. package/src/registry/oauth/xiaomi.ts +21 -9
  138. package/src/registry/ollama-cloud.ts +5 -4
  139. package/src/registry/ollama.ts +3 -2
  140. package/src/registry/parallel.ts +4 -3
  141. package/src/registry/qwen-portal.ts +4 -3
  142. package/src/registry/tavily.ts +4 -3
  143. package/src/registry/vercel-ai-gateway.ts +4 -3
  144. package/src/registry/vllm.ts +3 -2
  145. package/src/stream.ts +79 -24
  146. package/src/types.ts +6 -0
  147. package/src/usage/claude.ts +2 -1
  148. package/src/usage/github-copilot.ts +4 -3
  149. package/src/usage/google-antigravity.ts +2 -1
  150. package/src/usage/openai-codex-base-url.ts +30 -10
  151. package/src/utils/abort.ts +4 -2
  152. package/src/utils/block-symbols.ts +32 -0
  153. package/src/utils/event-stream.ts +15 -3
  154. package/src/utils/http-inspector.ts +4 -4
  155. package/src/utils/idle-iterator.ts +6 -5
  156. package/src/utils/openai-http.ts +11 -46
  157. package/src/utils/parse-bind.ts +7 -5
  158. package/src/utils/proxy.ts +2 -1
  159. package/src/utils/retry.ts +7 -23
  160. package/src/utils/schema/normalize.ts +3 -2
  161. package/src/utils/stream-markup-healing.ts +41 -18
  162. package/src/utils/thinking-loop.ts +15 -11
  163. package/src/utils/validation.ts +4 -3
  164. package/dist/types/dialect/pi.d.ts +0 -9
  165. package/dist/types/errors.d.ts +0 -24
  166. package/dist/types/utils/overflow.d.ts +0 -55
  167. package/src/dialect/pi.md +0 -55
  168. package/src/dialect/pi.ts +0 -600
  169. package/src/errors.ts +0 -32
  170. package/src/utils/overflow.ts +0 -140
@@ -1,6 +1,7 @@
1
1
  import { logger } from "@oh-my-pi/pi-utils";
2
2
  import { type } from "arktype";
3
3
  import { captureRequestHeaders, resolvePromptCacheKey } from "../auth-gateway/http";
4
+ import * as AIError from "../error";
4
5
  import type {
5
6
  AssistantMessage,
6
7
  AssistantMessageEventStream,
@@ -293,7 +294,7 @@ function deriveCacheRetention(data: {
293
294
  export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
294
295
  const data = anthropicMessagesRequestSchema(body);
295
296
  if (data instanceof type.errors) {
296
- throw new Error(`anthropic-messages: ${data.summary}`);
297
+ throw new AIError.ValidationError(`anthropic-messages: ${data.summary}`);
297
298
  }
298
299
 
299
300
  const now = Date.now();
@@ -457,7 +458,13 @@ function encodeUsage(message: AssistantMessage): Record<string, unknown> {
457
458
 
458
459
  export function encodeResponse(message: AssistantMessage, requestedModelId: string): Record<string, unknown> {
459
460
  if (message.stopReason === "error" || message.stopReason === "aborted") {
460
- throw new Error(message.errorMessage ?? `anthropic-messages: upstream ${message.stopReason}`);
461
+ throw new AIError.ProviderResponseError(
462
+ message.errorMessage ?? `anthropic-messages: upstream ${message.stopReason}`,
463
+ {
464
+ provider: "anthropic",
465
+ kind: "output",
466
+ },
467
+ );
461
468
  }
462
469
  return {
463
470
  id: message.responseId ?? newMessageId(),
@@ -9,17 +9,15 @@ import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
9
9
  import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
10
10
  import {
11
11
  $env,
12
- extractHttpStatusFromError,
13
12
  getInstallId,
14
13
  isEnoent,
15
- isRetryableError,
16
- isUnexpectedSocketCloseMessage,
17
14
  logger,
18
15
  parseJsonWithRepair,
19
16
  parseStreamingJsonThrottled,
20
17
  readSseEvents,
21
18
  } from "@oh-my-pi/pi-utils";
22
- import { isUsageLimitError } from "../rate-limit-utils";
19
+ import { renderDemotedThinking } from "../dialect/demotion";
20
+ import * as AIError from "../error";
23
21
  import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
24
22
  import type {
25
23
  Api,
@@ -48,13 +46,18 @@ import type {
48
46
  import { resolveServiceTier } from "../types";
49
47
  import { isRecord, normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
50
48
  import { createAbortSourceTracker } from "../utils/abort";
49
+ import {
50
+ clearStreamingPartialJson,
51
+ kStreamingBlockIndex,
52
+ kStreamingLastParseLen,
53
+ kStreamingPartialJson,
54
+ } from "../utils/block-symbols";
51
55
  import { withEmptyCompletionRetry } from "../utils/empty-completion-retry";
52
56
  import { AssistantMessageEventStream } from "../utils/event-stream";
53
57
  import { isFoundryEnabled } from "../utils/foundry";
54
- import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
58
+ import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
55
59
  import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
56
60
  import { notifyProviderResponse } from "../utils/provider-response";
57
- import { isCopilotTransientModelError } from "../utils/retry";
58
61
  import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
59
62
  import { spillToDescription } from "../utils/schema/spill";
60
63
  import { createSdkStreamRequestOptions } from "../utils/sdk-stream-timeout";
@@ -384,38 +387,6 @@ export function clearAnthropicFastModeFallback(
384
387
  }
385
388
  }
386
389
 
387
- function isAnthropicStrictGrammarTooLargeError(error: unknown): boolean {
388
- if (extractHttpStatusFromError(error) !== 400) return false;
389
- const message = error instanceof Error ? error.message : String(error);
390
- const isStrictGrammarTooLarge = /compiled grammar/i.test(message) && /too large/i.test(message);
391
- const isSchemaCompilationTooComplex =
392
- /schema/i.test(message) && /too complex/i.test(message) && /compil/i.test(message);
393
- return /invalid_request_error/i.test(message) && (isStrictGrammarTooLarge || isSchemaCompilationTooComplex);
394
- }
395
-
396
- export function isAnthropicFastModeUnsupportedError(error: unknown): boolean {
397
- const status = extractHttpStatusFromError(error);
398
- if (status !== 400 && status !== 429) return false;
399
- const message = error instanceof Error ? error.message : String(error);
400
- // 400 invalid_request_error — model doesn't accept `speed` at all.
401
- // Observed: "'claude-opus-4-5-20251101' does not support the `speed` parameter."
402
- // Stay tolerant of phrasing drift ("is not supported", quoted vs backticked field).
403
- if (
404
- status === 400 &&
405
- /invalid_request_error/i.test(message) &&
406
- /\bspeed\b/i.test(message) &&
407
- /not support/i.test(message)
408
- ) {
409
- return true;
410
- }
411
- // 429 rate_limit_error — account lacks the extra-usage entitlement fast mode requires.
412
- // Observed: "Extra usage is required for fast mode."
413
- if (status === 429 && /rate_limit_error/i.test(message) && /fast mode/i.test(message)) {
414
- return true;
415
- }
416
- return false;
417
- }
418
-
419
390
  function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
420
391
  return params.tools?.some(tool => tool.strict === true) ?? false;
421
392
  }
@@ -1228,7 +1199,7 @@ function resolvePemValue(value: string | undefined, name: string): string | unde
1228
1199
  return fs.readFileSync(trimmed, "utf8");
1229
1200
  } catch (error) {
1230
1201
  if (isEnoent(error)) {
1231
- throw new Error(`${name} path does not exist: ${trimmed}`);
1202
+ throw new AIError.ValidationError(`${name} path does not exist: ${trimmed}`);
1232
1203
  }
1233
1204
  throw error;
1234
1205
  }
@@ -1249,7 +1220,9 @@ function resolveFoundryTlsOptions(model: Model<"anthropic-messages">): FoundryTl
1249
1220
  const key = resolvePemValue($env.CLAUDE_CODE_CLIENT_KEY, "CLAUDE_CODE_CLIENT_KEY");
1250
1221
 
1251
1222
  if ((cert && !key) || (!cert && key)) {
1252
- throw new Error("Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.");
1223
+ throw new AIError.ConfigurationError(
1224
+ "Both CLAUDE_CODE_CLIENT_CERT and CLAUDE_CODE_CLIENT_KEY must be set for mTLS.",
1225
+ );
1253
1226
  }
1254
1227
 
1255
1228
  const options: FoundryTlsOptions = {};
@@ -1339,14 +1312,15 @@ function createAnthropicSseStreamError(data: string): Error {
1339
1312
  const errorType = typeof parsed?.error?.type === "string" ? parsed.error.type : undefined;
1340
1313
  const message = typeof parsed?.error?.message === "string" ? parsed.error.message : undefined;
1341
1314
  if (message) {
1342
- return new Error(
1315
+ return new AIError.ProviderResponseError(
1343
1316
  errorType ? `Anthropic stream error (${errorType}): ${message}` : `Anthropic stream error: ${message}`,
1317
+ { provider: "anthropic", kind: "output" },
1344
1318
  );
1345
1319
  }
1346
1320
  } catch {
1347
1321
  // Not a JSON envelope; fall through to the raw payload.
1348
1322
  }
1349
- return new Error(data);
1323
+ return new AIError.ProviderResponseError(data, { provider: "anthropic", kind: "output" });
1350
1324
  }
1351
1325
 
1352
1326
  async function* iterateAnthropicEvents(
@@ -1355,7 +1329,7 @@ async function* iterateAnthropicEvents(
1355
1329
  onSseEvent?: AnthropicOptions["onSseEvent"],
1356
1330
  ): AsyncGenerator<AnthropicStreamEvent> {
1357
1331
  if (!response.body) {
1358
- throw new Error("Attempted to iterate over an Anthropic response with no body");
1332
+ throw new AIError.AnthropicStreamEnvelopeError("Attempted to iterate over an Anthropic response with no body");
1359
1333
  }
1360
1334
 
1361
1335
  let sawMessageStart = false;
@@ -1444,7 +1418,7 @@ async function getAnthropicStreamResponse(
1444
1418
  const { data, response, request_id } = await request.withResponse();
1445
1419
  return { events: data, response, requestId: request_id, recordsRawSseEvents: false };
1446
1420
  }
1447
- throw new Error("Anthropic SDK request did not expose a stream response");
1421
+ throw new AIError.AnthropicStreamEnvelopeError("Anthropic SDK request did not expose a stream response");
1448
1422
  }
1449
1423
 
1450
1424
  async function* observeDecodedAnthropicSdkEvents(
@@ -1461,23 +1435,9 @@ async function* observeDecodedAnthropicSdkEvents(
1461
1435
 
1462
1436
  const PROVIDER_MAX_RETRIES = 10;
1463
1437
 
1464
- /** Transient stream corruption errors where the response was truncated mid-JSON. */
1465
- function isTransientStreamParseError(error: unknown): boolean {
1466
- if (!(error instanceof Error)) return false;
1467
- return /unterminated string|unexpected end of json input|unexpected end of data|unexpected eof|end of file|eof while parsing|truncated/i.test(
1468
- error.message,
1469
- );
1470
- }
1471
-
1472
- const ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
1473
-
1474
- function createAnthropicStreamEnvelopeError(message: string): Error {
1475
- return new Error(`${ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX} ${message}`);
1476
- }
1477
-
1478
1438
  /**
1479
1439
  * Log a malformed-stream-envelope anomaly without aborting the turn. The strict
1480
- * parser would `throw createAnthropicStreamEnvelopeError(...)` here; we instead
1440
+ * parser would `throw new AnthropicStreamEnvelopeError(...)` here; we instead
1481
1441
  * surface a warning and let the caller skip the offending event (or finalize what
1482
1442
  * already streamed) so a non-conforming endpoint degrades to best-effort content
1483
1443
  * rather than failing the request.
@@ -1492,49 +1452,18 @@ function shouldIgnoreAnthropicPreambleEvent(eventType: unknown): boolean {
1492
1452
  return !ANTHROPIC_MESSAGE_EVENTS.has(eventType);
1493
1453
  }
1494
1454
 
1495
- function isTransientStreamEnvelopeError(error: unknown): boolean {
1496
- if (!(error instanceof Error)) return false;
1497
- return (
1498
- error.message.includes(ANTHROPIC_STREAM_ENVELOPE_ERROR_PREFIX) ||
1499
- /stream event order|before message_start/i.test(error.message)
1500
- );
1501
- }
1502
-
1503
- function isProviderRetryableStreamEnvelopeError(error: unknown): boolean {
1504
- if (!(error instanceof Error)) return false;
1505
- return /stream event order|before message_start/i.test(error.message);
1506
- }
1507
-
1508
- function isAnthropicTransientTransportMessage(message: string): boolean {
1509
- return message.includes("tls: bad record mac") || message.includes("type=server_error");
1510
- }
1511
-
1455
+ /**
1456
+ * Whether an Anthropic (or Copilot-over-Anthropic) stream error should be
1457
+ * retried. The classification lives in {@link AIError.isProviderRetryableError};
1458
+ * this wrapper injects the Copilot-specific `model_not_supported` transient
1459
+ * check, which the error module must not import directly.
1460
+ */
1512
1461
  export function isProviderRetryableError(error: unknown, provider?: string): boolean {
1513
- if (!(error instanceof Error)) return false;
1514
- if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
1515
- // Account-level usage/quota limits ("usage_limit_reached", "exceed your
1516
- // account's rate limit", "quota exceeded") are persistent the server
1517
- // parks the credential for minutes-to-hours (see the long `retry-after`).
1518
- // Retrying the same key with the provider's seconds-scale backoff never
1519
- // helps; these are owned by the credential-rotation layer (auth-gateway /
1520
- // `streamSimple` a/b/c policy), so surface them immediately instead of
1521
- // burning the retry budget here.
1522
- if (isUsageLimitError(error.message)) return false;
1523
- const status = extractHttpStatusFromError(error);
1524
- if (status !== undefined && status >= 400 && status < 500 && status !== 408 && status !== 429) return false;
1525
- const msg = error.message.toLowerCase();
1526
- if (
1527
- isUnexpectedSocketCloseMessage(msg) ||
1528
- isAnthropicTransientTransportMessage(msg) ||
1529
- /rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|server_error|bad record mac|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
1530
- msg,
1531
- ) ||
1532
- isTransientStreamParseError(error) ||
1533
- isProviderRetryableStreamEnvelopeError(error)
1534
- ) {
1535
- return true;
1536
- }
1537
- return isRetryableError(error);
1462
+ return AIError.isProviderRetryableError(error, {
1463
+ provider,
1464
+ isProviderTransient:
1465
+ provider === "github-copilot" ? (err): boolean => AIError.isCopilotTransientModelError(err) : undefined,
1466
+ });
1538
1467
  }
1539
1468
 
1540
1469
  const THINKING_ENVELOPE_OPEN = "<thinking>";
@@ -1609,7 +1538,7 @@ const streamAnthropicOnce = (
1609
1538
  const stream = new AssistantMessageEventStream();
1610
1539
 
1611
1540
  (async () => {
1612
- const startTime = Date.now();
1541
+ const startTime = performance.now();
1613
1542
  let firstTokenTime: number | undefined;
1614
1543
 
1615
1544
  const output: AssistantMessage = {
@@ -1772,15 +1701,14 @@ const streamAnthropicOnce = (
1772
1701
  | ThinkingContent
1773
1702
  | RedactedThinkingContent
1774
1703
  | TextContent
1775
- | (ToolCall & { partialJson: string; lastParseLen?: number })
1776
- ) & { index: number };
1704
+ | (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number })
1705
+ ) & { [kStreamingBlockIndex]: number };
1777
1706
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
1778
1707
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
1779
1708
  const requestTimeoutMs =
1780
1709
  firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
1781
1710
  const blocks = output.content as Block[];
1782
1711
  const finalizeStreamBlock = (block: Block, contentIndex: number): void => {
1783
- delete (block as { index?: number }).index;
1784
1712
  if (block.type === "text") {
1785
1713
  stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
1786
1714
  } else if (block.type === "thinking") {
@@ -1792,7 +1720,9 @@ const streamAnthropicOnce = (
1792
1720
  stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
1793
1721
  } else if (block.type === "toolCall") {
1794
1722
  const finalJson =
1795
- block.partialJson.length > 0 ? block.partialJson : JSON.stringify(block.arguments ?? {});
1723
+ block[kStreamingPartialJson].length > 0
1724
+ ? block[kStreamingPartialJson]
1725
+ : JSON.stringify(block.arguments ?? {});
1796
1726
  try {
1797
1727
  block.arguments = parseJsonWithRepair(finalJson) as ToolCall["arguments"];
1798
1728
  } catch (parseError) {
@@ -1814,8 +1744,7 @@ const streamAnthropicOnce = (
1814
1744
  };
1815
1745
  }
1816
1746
  }
1817
- delete (block as { partialJson?: string }).partialJson;
1818
- delete (block as { lastParseLen?: number }).lastParseLen;
1747
+ clearStreamingPartialJson(block);
1819
1748
  stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
1820
1749
  }
1821
1750
  };
@@ -1824,8 +1753,12 @@ const streamAnthropicOnce = (
1824
1753
  // Provider-level transport/rate-limit failures: only before any streamed content starts.
1825
1754
  // Malformed envelopes/JSON: only before replay-unsafe text/tool events are visible on this stream.
1826
1755
  let providerRetryAttempt = 0;
1827
- const firstEventTimeoutAbortError = new Error("Anthropic stream timed out while waiting for the first event");
1828
- const idleTimeoutAbortError = new Error("Anthropic stream stalled while waiting for the next event");
1756
+ const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
1757
+ "Anthropic stream timed out while waiting for the first event",
1758
+ );
1759
+ const idleTimeoutAbortError = new AIError.StreamTimeoutError(
1760
+ "Anthropic stream stalled while waiting for the next event",
1761
+ );
1829
1762
  while (true) {
1830
1763
  activeAbortTracker = createAbortSourceTracker(options?.signal);
1831
1764
  const { requestSignal } = activeAbortTracker;
@@ -1945,7 +1878,7 @@ const streamAnthropicOnce = (
1945
1878
  if (shouldIgnoreAnthropicPreambleEvent(event.type)) {
1946
1879
  continue;
1947
1880
  }
1948
- throw createAnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
1881
+ throw new AIError.AnthropicStreamEnvelopeError(`received ${event.type} before message_start`);
1949
1882
  }
1950
1883
 
1951
1884
  if (event.type === "content_block_start") {
@@ -1971,13 +1904,13 @@ const streamAnthropicOnce = (
1971
1904
  reportAnthropicEnvelopeAnomaly("content_block_start missing content_block payload");
1972
1905
  continue;
1973
1906
  }
1974
- if (!firstTokenTime) firstTokenTime = Date.now();
1907
+ if (!firstTokenTime) firstTokenTime = performance.now();
1975
1908
  if (event.content_block.type === "text") {
1976
1909
  streamedReplayUnsafeContent = true;
1977
1910
  const block: Block = {
1978
1911
  type: "text",
1979
1912
  text: "",
1980
- index: event.index,
1913
+ [kStreamingBlockIndex]: event.index,
1981
1914
  };
1982
1915
  output.content.push(block);
1983
1916
  const contentIndex = output.content.length - 1;
@@ -1993,7 +1926,7 @@ const streamAnthropicOnce = (
1993
1926
  type: "thinking",
1994
1927
  thinking: "",
1995
1928
  thinkingSignature: "",
1996
- index: event.index,
1929
+ [kStreamingBlockIndex]: event.index,
1997
1930
  };
1998
1931
  output.content.push(block);
1999
1932
  const contentIndex = output.content.length - 1;
@@ -2008,7 +1941,7 @@ const streamAnthropicOnce = (
2008
1941
  const block: Block = {
2009
1942
  type: "redactedThinking",
2010
1943
  data: event.content_block.data,
2011
- index: event.index,
1944
+ [kStreamingBlockIndex]: event.index,
2012
1945
  };
2013
1946
  output.content.push(block);
2014
1947
  openBlocks.set(event.index, {
@@ -2026,8 +1959,8 @@ const streamAnthropicOnce = (
2026
1959
  model.compat.escapeBuiltinToolNames,
2027
1960
  ),
2028
1961
  arguments: event.content_block.input ?? {},
2029
- partialJson: "",
2030
- index: event.index,
1962
+ [kStreamingPartialJson]: "",
1963
+ [kStreamingBlockIndex]: event.index,
2031
1964
  };
2032
1965
  output.content.push(block);
2033
1966
  const contentIndex = output.content.length - 1;
@@ -2090,11 +2023,14 @@ const streamAnthropicOnce = (
2090
2023
  continue;
2091
2024
  }
2092
2025
  streamedReplayUnsafeContent = true;
2093
- block.partialJson += event.delta.partial_json;
2094
- const throttled = parseStreamingJsonThrottled(block.partialJson, block.lastParseLen ?? 0);
2026
+ block[kStreamingPartialJson] += event.delta.partial_json;
2027
+ const throttled = parseStreamingJsonThrottled(
2028
+ block[kStreamingPartialJson],
2029
+ block[kStreamingLastParseLen] ?? 0,
2030
+ );
2095
2031
  if (throttled) {
2096
2032
  block.arguments = throttled.value;
2097
- block.lastParseLen = throttled.parsedLen;
2033
+ block[kStreamingLastParseLen] = throttled.parsedLen;
2098
2034
  }
2099
2035
  stream.push({
2100
2036
  type: "toolcall_delta",
@@ -2197,10 +2133,10 @@ const streamAnthropicOnce = (
2197
2133
  throw firstEventTimeoutError;
2198
2134
  }
2199
2135
  if (activeAbortTracker.wasCallerAbort()) {
2200
- throw new Error("Request was aborted");
2136
+ throw new AIError.AbortError();
2201
2137
  }
2202
2138
  if (!sawEvent || !sawMessageStart) {
2203
- throw createAnthropicStreamEnvelopeError("stream ended before message_start");
2139
+ throw new AIError.AnthropicStreamEnvelopeError("stream ended before message_start");
2204
2140
  }
2205
2141
  if (!sawMessageStop) {
2206
2142
  reportAnthropicEnvelopeAnomaly("stream ended before message_stop");
@@ -2218,7 +2154,10 @@ const streamAnthropicOnce = (
2218
2154
  }
2219
2155
 
2220
2156
  if (output.stopReason === "aborted" || output.stopReason === "error") {
2221
- throw new Error(output.errorMessage ?? "An unknown error occurred");
2157
+ throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
2158
+ provider: model.provider,
2159
+ kind: "output",
2160
+ });
2222
2161
  }
2223
2162
  break;
2224
2163
  } catch (streamError) {
@@ -2227,7 +2166,7 @@ const streamAnthropicOnce = (
2227
2166
  !disableStrictTools &&
2228
2167
  firstTokenTime === undefined &&
2229
2168
  hasStrictAnthropicTools(params) &&
2230
- isAnthropicStrictGrammarTooLargeError(streamFailure)
2169
+ AIError.isGrammarError(streamFailure)
2231
2170
  ) {
2232
2171
  // Log-only: the retried turn must not carry an errorMessage on
2233
2172
  // success (consumers treat its presence as failure).
@@ -2254,7 +2193,7 @@ const streamAnthropicOnce = (
2254
2193
  !dropFastMode &&
2255
2194
  resolveServiceTier(options?.serviceTier, model.provider) === "priority" &&
2256
2195
  firstTokenTime === undefined &&
2257
- isAnthropicFastModeUnsupportedError(streamFailure)
2196
+ AIError.isFastModeUnsupported(streamFailure)
2258
2197
  ) {
2259
2198
  logger.debug("anthropic: fast mode unsupported, retrying without speed", {
2260
2199
  model: model.id,
@@ -2276,7 +2215,7 @@ const streamAnthropicOnce = (
2276
2215
  continue;
2277
2216
  }
2278
2217
  const isTransientEnvelopeFailure =
2279
- isTransientStreamParseError(streamFailure) || isTransientStreamEnvelopeError(streamFailure);
2218
+ AIError.isTransientStreamParseError(streamFailure) || AIError.isStreamEnvelopeError(streamFailure);
2280
2219
  const isLocalIdleTimeout =
2281
2220
  streamFailure === idleTimeoutAbortError ||
2282
2221
  (streamFailure instanceof Error && streamFailure.message === idleTimeoutAbortError.message);
@@ -2299,7 +2238,9 @@ const streamAnthropicOnce = (
2299
2238
  // 429/529-style failures: retrying sooner than the server asked is a
2300
2239
  // guaranteed failure that just burns the retry budget.
2301
2240
  const headerDelayMs =
2302
- streamFailure instanceof AnthropicApiError ? retryDelayFromHeaders(streamFailure.headers) : undefined;
2241
+ streamFailure instanceof Error && streamFailure instanceof AnthropicApiError
2242
+ ? retryDelayFromHeaders(streamFailure.headers)
2243
+ : undefined;
2303
2244
  const delayMs = headerDelayMs !== undefined ? Math.max(headerDelayMs, backoffDelayMs) : backoffDelayMs;
2304
2245
  if (options?.providerRetryWait) {
2305
2246
  await options.providerRetryWait(delayMs, options.signal);
@@ -2316,7 +2257,7 @@ const streamAnthropicOnce = (
2316
2257
  firstTokenTime = undefined;
2317
2258
  }
2318
2259
  }
2319
- output.duration = Date.now() - startTime;
2260
+ output.duration = performance.now() - startTime;
2320
2261
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
2321
2262
  if (dropFastMode && resolveServiceTier(options?.serviceTier, model.provider) === "priority") {
2322
2263
  output.disabledFeatures = [...(output.disabledFeatures ?? []), "priority"];
@@ -2325,23 +2266,19 @@ const streamAnthropicOnce = (
2325
2266
  stream.end();
2326
2267
  } catch (error) {
2327
2268
  for (const block of output.content) {
2328
- delete (block as { index?: number }).index;
2329
- delete (block as { partialJson?: string }).partialJson;
2330
- delete (block as { lastParseLen?: number }).lastParseLen;
2269
+ if (block.type === "toolCall") clearStreamingPartialJson(block);
2331
2270
  }
2332
- const firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();
2333
- output.stopReason = activeAbortTracker.wasCallerAbort() ? "aborted" : "error";
2334
- output.errorStatus = extractHttpStatusFromError(error);
2335
- try {
2336
- output.errorMessage =
2337
- firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
2338
- output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
2339
- } catch {
2340
- // finalizeErrorMessage must never take the stream down with it — a
2341
- // throw here would skip stream.end() and hang result() forever.
2342
- output.errorMessage = error instanceof Error ? error.message : String(error);
2343
- }
2344
- output.duration = Date.now() - startTime;
2271
+ const result = await AIError.finalize(error, {
2272
+ api: model.api,
2273
+ provider: model.provider,
2274
+ abortTracker: activeAbortTracker,
2275
+ rawRequestDump,
2276
+ });
2277
+ output.stopReason = result.stopReason;
2278
+ output.errorStatus = result.status;
2279
+ output.errorId = result.id;
2280
+ output.errorMessage = result.message;
2281
+ output.duration = performance.now() - startTime;
2345
2282
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
2346
2283
  stream.push({ type: "error", reason: output.stopReason, error: output });
2347
2284
  stream.end();
@@ -2631,7 +2568,7 @@ function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, maxAll
2631
2568
 
2632
2569
  const clampedBudget = raisedMaxTokens - OUTPUT_FALLBACK_BUFFER;
2633
2570
  if (clampedBudget <= 0) {
2634
- throw new Error(
2571
+ throw new AIError.ConfigurationError(
2635
2572
  `Anthropic thinking budget requires max_tokens greater than ${OUTPUT_FALLBACK_BUFFER}; got ${raisedMaxTokens}`,
2636
2573
  );
2637
2574
  }
@@ -3238,7 +3175,7 @@ export function convertAnthropicMessages(
3238
3175
  if (block.thinking.trim().length === 0) continue;
3239
3176
  blocks.push({
3240
3177
  type: "text",
3241
- text: block.thinking.toWellFormed(),
3178
+ text: renderDemotedThinking(model.id, block.thinking),
3242
3179
  });
3243
3180
  continue;
3244
3181
  }
@@ -3260,7 +3197,7 @@ export function convertAnthropicMessages(
3260
3197
  } else {
3261
3198
  blocks.push({
3262
3199
  type: "text",
3263
- text: block.thinking.toWellFormed(),
3200
+ text: renderDemotedThinking(model.id, block.thinking),
3264
3201
  });
3265
3202
  }
3266
3203
  } else {
@@ -23,6 +23,7 @@ import * as fs from "node:fs";
23
23
  import * as os from "node:os";
24
24
  import * as path from "node:path";
25
25
  import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
26
+ import * as AIError from "../error";
26
27
  import type { FetchImpl } from "../types";
27
28
  import { raceWithSignal } from "../utils/abort";
28
29
  import type { AwsCredentials } from "./aws-sigv4";
@@ -111,9 +112,10 @@ async function resolveFresh(
111
112
  if (imdsCreds) return imdsCreds;
112
113
  }
113
114
 
114
- throw new Error(
115
+ throw new AIError.AwsCredentialsError(
115
116
  `Unable to resolve AWS credentials. Set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, ` +
116
117
  `or configure profile '${profile}' in ~/.aws/credentials (or ~/.aws/config for SSO).`,
118
+ "resolution",
117
119
  );
118
120
  }
119
121
 
@@ -245,11 +247,17 @@ async function readSsoCredentials(
245
247
 
246
248
  const token = await loadSsoCachedToken(startUrl, sessionName);
247
249
  if (!token?.accessToken) {
248
- throw new Error(`AWS SSO token for ${startUrl} not found in ~/.aws/sso/cache. Run 'aws sso login' first.`);
250
+ throw new AIError.AwsCredentialsError(
251
+ `AWS SSO token for ${startUrl} not found in ~/.aws/sso/cache. Run 'aws sso login' first.`,
252
+ "sso-token-missing",
253
+ );
249
254
  }
250
255
  const expiresAt = token.expiresAt ? Date.parse(token.expiresAt) : Number.POSITIVE_INFINITY;
251
256
  if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
252
- throw new Error(`AWS SSO token for ${startUrl} has expired. Run 'aws sso login' to refresh.`);
257
+ throw new AIError.AwsCredentialsError(
258
+ `AWS SSO token for ${startUrl} has expired. Run 'aws sso login' to refresh.`,
259
+ "sso-token-expired",
260
+ );
253
261
  }
254
262
 
255
263
  const url =
@@ -263,13 +271,20 @@ async function readSsoCredentials(
263
271
  });
264
272
  if (!response.ok) {
265
273
  const body = await response.text().catch(() => "");
266
- throw new Error(`AWS SSO GetRoleCredentials failed: ${response.status} ${body.slice(0, 200)}`);
274
+ throw new AIError.AwsCredentialsError(
275
+ `AWS SSO GetRoleCredentials failed: ${response.status} ${body.slice(0, 200)}`,
276
+ "sso-role",
277
+ );
267
278
  }
268
279
  const json = (await response.json()) as {
269
280
  roleCredentials?: { accessKeyId: string; secretAccessKey: string; sessionToken: string; expiration: number };
270
281
  };
271
282
  const role = json.roleCredentials;
272
- if (!role) throw new Error("AWS SSO GetRoleCredentials: missing roleCredentials in response");
283
+ if (!role)
284
+ throw new AIError.AwsCredentialsError(
285
+ "AWS SSO GetRoleCredentials: missing roleCredentials in response",
286
+ "sso-role",
287
+ );
273
288
 
274
289
  // region is honored at the caller; we only consume defaultRegion to keep the
275
290
  // param wired for symmetry with other resolution paths.
@@ -359,23 +374,32 @@ async function readCredentialProcess(
359
374
  ]);
360
375
  if (exitCode !== 0) {
361
376
  const tail = stderr.trim().slice(-512) || stdout.trim().slice(-512) || "(no output)";
362
- throw new Error(`AWS credential_process for profile '${profile}' exited ${exitCode}: ${tail}`);
377
+ throw new AIError.AwsCredentialsError(
378
+ `AWS credential_process for profile '${profile}' exited ${exitCode}: ${tail}`,
379
+ "credential-process",
380
+ );
363
381
  }
364
382
 
365
383
  let parsed: CredentialProcessEnvelope;
366
384
  try {
367
385
  parsed = JSON.parse(stdout) as CredentialProcessEnvelope;
368
386
  } catch (err) {
369
- throw new Error(`AWS credential_process for profile '${profile}' did not emit valid JSON: ${String(err)}`);
387
+ throw new AIError.AwsCredentialsError(
388
+ `AWS credential_process for profile '${profile}' did not emit valid JSON: ${String(err)}`,
389
+ "credential-process",
390
+ { cause: err },
391
+ );
370
392
  }
371
393
  if (parsed.Version !== 1) {
372
- throw new Error(
394
+ throw new AIError.AwsCredentialsError(
373
395
  `AWS credential_process for profile '${profile}' returned unsupported Version ${parsed.Version ?? "<missing>"}; expected 1.`,
396
+ "credential-process",
374
397
  );
375
398
  }
376
399
  if (!parsed.AccessKeyId || !parsed.SecretAccessKey) {
377
- throw new Error(
400
+ throw new AIError.AwsCredentialsError(
378
401
  `AWS credential_process for profile '${profile}' returned envelope without AccessKeyId/SecretAccessKey.`,
402
+ "credential-process",
379
403
  );
380
404
  }
381
405
 
@@ -397,7 +421,10 @@ async function readCredentialProcess(
397
421
  function buildCredentialProcessArgv(profile: string, command: string): string[] {
398
422
  const tokens = tokenizeCredentialProcessCommand(command);
399
423
  if (tokens.length === 0) {
400
- throw new Error(`AWS credential_process for profile '${profile}' is empty.`);
424
+ throw new AIError.AwsCredentialsError(
425
+ `AWS credential_process for profile '${profile}' is empty.`,
426
+ "credential-process",
427
+ );
401
428
  }
402
429
  if (process.platform === "win32" && isBatchScript(tokens[0])) {
403
430
  return ["cmd.exe", "/d", "/s", "/c", command];
@@ -479,7 +506,10 @@ export function tokenizeCredentialProcessCommand(cmd: string): string[] {
479
506
  current += ch;
480
507
  }
481
508
  if (mode !== "normal") {
482
- throw new Error("AWS credential_process command has an unterminated quote.");
509
+ throw new AIError.AwsCredentialsError(
510
+ "AWS credential_process command has an unterminated quote.",
511
+ "credential-process",
512
+ );
483
513
  }
484
514
  if (hasToken) tokens.push(current);
485
515
  return tokens;