@oh-my-pi/pi-ai 16.2.1 → 16.2.2

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 (166) hide show
  1. package/CHANGELOG.md +27 -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 +1 -0
  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/utils/block-symbols.d.ts +20 -0
  42. package/dist/types/utils/openai-http.d.ts +4 -8
  43. package/dist/types/utils/retry.d.ts +2 -14
  44. package/dist/types/utils/thinking-loop.d.ts +3 -1
  45. package/package.json +8 -4
  46. package/src/api-registry.ts +3 -1
  47. package/src/auth-broker/discover.ts +7 -2
  48. package/src/auth-broker/remote-store.ts +8 -7
  49. package/src/auth-gateway/server.ts +27 -115
  50. package/src/auth-retry.ts +9 -21
  51. package/src/auth-storage.ts +34 -49
  52. package/src/dialect/demotion.ts +31 -0
  53. package/src/dialect/factory.ts +0 -2
  54. package/src/dialect/index.ts +2 -0
  55. package/src/dialect/owned-stream.ts +0 -1
  56. package/src/dialect/thinking.ts +22 -10
  57. package/src/error/abort.ts +18 -0
  58. package/src/error/auth-classify.ts +30 -0
  59. package/src/error/auth.ts +48 -0
  60. package/src/error/aws.ts +31 -0
  61. package/src/error/classes.ts +186 -0
  62. package/src/error/finalize.ts +69 -0
  63. package/src/error/flags.ts +486 -0
  64. package/src/error/format.ts +36 -0
  65. package/src/error/gateway.ts +96 -0
  66. package/src/error/index.ts +13 -0
  67. package/src/error/oauth.ts +58 -0
  68. package/src/error/provider.ts +56 -0
  69. package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
  70. package/src/error/retryable.ts +70 -0
  71. package/src/error/validation.ts +44 -0
  72. package/src/index.ts +1 -3
  73. package/src/providers/amazon-bedrock.ts +61 -57
  74. package/src/providers/anthropic-client.ts +13 -41
  75. package/src/providers/anthropic-messages-server.ts +9 -2
  76. package/src/providers/anthropic.ts +84 -147
  77. package/src/providers/aws-credentials.ts +41 -11
  78. package/src/providers/aws-eventstream.ts +12 -21
  79. package/src/providers/azure-openai-responses.ts +27 -17
  80. package/src/providers/cursor.ts +59 -53
  81. package/src/providers/devin.ts +28 -20
  82. package/src/providers/gitlab-duo-workflow.ts +30 -10
  83. package/src/providers/gitlab-duo.ts +22 -6
  84. package/src/providers/google-auth.ts +15 -5
  85. package/src/providers/google-gemini-cli.ts +46 -60
  86. package/src/providers/google-shared.ts +40 -38
  87. package/src/providers/google-vertex.ts +3 -2
  88. package/src/providers/google.ts +5 -1
  89. package/src/providers/mock.ts +10 -7
  90. package/src/providers/ollama.ts +34 -29
  91. package/src/providers/openai-chat-server.ts +2 -1
  92. package/src/providers/openai-codex/request-transformer.ts +13 -12
  93. package/src/providers/openai-codex/response-handler.ts +1 -1
  94. package/src/providers/openai-codex-responses.ts +931 -1012
  95. package/src/providers/openai-completions.ts +46 -36
  96. package/src/providers/openai-responses-server-schema.ts +3 -0
  97. package/src/providers/openai-responses-server.ts +82 -30
  98. package/src/providers/openai-responses-wire.ts +1 -1
  99. package/src/providers/openai-responses.ts +46 -22
  100. package/src/providers/openai-shared.ts +118 -51
  101. package/src/providers/pi-native-client.ts +12 -18
  102. package/src/providers/pi-native-server.ts +9 -6
  103. package/src/providers/register-builtins.ts +5 -2
  104. package/src/providers/transform-messages.ts +31 -63
  105. package/src/registry/alibaba-coding-plan.ts +6 -5
  106. package/src/registry/api-key-login.ts +4 -3
  107. package/src/registry/api-key-validation.ts +4 -3
  108. package/src/registry/cloudflare-ai-gateway.ts +4 -3
  109. package/src/registry/coreweave.ts +2 -1
  110. package/src/registry/deepseek.ts +2 -1
  111. package/src/registry/google-antigravity.ts +2 -1
  112. package/src/registry/google-gemini-cli.ts +2 -1
  113. package/src/registry/kagi.ts +4 -3
  114. package/src/registry/kilo.ts +32 -10
  115. package/src/registry/litellm.ts +4 -3
  116. package/src/registry/llama-cpp.ts +3 -2
  117. package/src/registry/lm-studio.ts +3 -2
  118. package/src/registry/nvidia.ts +7 -7
  119. package/src/registry/oauth/anthropic.ts +23 -6
  120. package/src/registry/oauth/callback-server.ts +5 -3
  121. package/src/registry/oauth/cursor.ts +18 -4
  122. package/src/registry/oauth/devin.ts +14 -3
  123. package/src/registry/oauth/github-copilot.ts +21 -10
  124. package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
  125. package/src/registry/oauth/gitlab-duo.ts +30 -6
  126. package/src/registry/oauth/google-antigravity.ts +14 -5
  127. package/src/registry/oauth/google-gemini-cli.ts +24 -7
  128. package/src/registry/oauth/google-oauth-shared.ts +6 -3
  129. package/src/registry/oauth/index.ts +19 -8
  130. package/src/registry/oauth/kimi.ts +40 -10
  131. package/src/registry/oauth/openai-codex.ts +26 -11
  132. package/src/registry/oauth/opencode.ts +4 -3
  133. package/src/registry/oauth/perplexity.ts +33 -11
  134. package/src/registry/oauth/xai-oauth.ts +62 -18
  135. package/src/registry/oauth/xiaomi.ts +21 -9
  136. package/src/registry/ollama-cloud.ts +5 -4
  137. package/src/registry/ollama.ts +3 -2
  138. package/src/registry/parallel.ts +4 -3
  139. package/src/registry/qwen-portal.ts +4 -3
  140. package/src/registry/tavily.ts +4 -3
  141. package/src/registry/vercel-ai-gateway.ts +4 -3
  142. package/src/registry/vllm.ts +3 -2
  143. package/src/stream.ts +79 -24
  144. package/src/types.ts +6 -0
  145. package/src/usage/claude.ts +2 -1
  146. package/src/usage/github-copilot.ts +4 -3
  147. package/src/usage/google-antigravity.ts +2 -1
  148. package/src/utils/abort.ts +4 -2
  149. package/src/utils/block-symbols.ts +32 -0
  150. package/src/utils/event-stream.ts +15 -3
  151. package/src/utils/http-inspector.ts +4 -4
  152. package/src/utils/idle-iterator.ts +6 -5
  153. package/src/utils/openai-http.ts +11 -46
  154. package/src/utils/parse-bind.ts +7 -5
  155. package/src/utils/proxy.ts +2 -1
  156. package/src/utils/retry.ts +7 -23
  157. package/src/utils/schema/normalize.ts +3 -2
  158. package/src/utils/thinking-loop.ts +15 -11
  159. package/src/utils/validation.ts +4 -3
  160. package/dist/types/dialect/pi.d.ts +0 -9
  161. package/dist/types/errors.d.ts +0 -24
  162. package/dist/types/utils/overflow.d.ts +0 -55
  163. package/src/dialect/pi.md +0 -55
  164. package/src/dialect/pi.ts +0 -600
  165. package/src/errors.ts +0 -32
  166. package/src/utils/overflow.ts +0 -140
@@ -0,0 +1,58 @@
1
+ import { attach, create, Flag } from "./flags";
2
+
3
+ /**
4
+ * What stage of an OAuth / device-code login flow failed. Discriminates the
5
+ * single {@link OAuthError} class so login flows don't each mint a bespoke
6
+ * error type.
7
+ */
8
+ export type OAuthErrorKind =
9
+ /** Token-exchange / refresh / discovery HTTP response was non-2xx or unparseable. */
10
+ | "http"
11
+ /** Response body was missing required fields (token, account id, endpoints, …). */
12
+ | "validation"
13
+ /** Authorization-code → token exchange failed. */
14
+ | "token-exchange"
15
+ /** Refresh-token grant failed. */
16
+ | "token-refresh"
17
+ /** Device-code / authorization polling failed (server error, too many retries). */
18
+ | "polling"
19
+ /** The flow exceeded its deadline (device-code expiry, polling timeout). */
20
+ | "timeout"
21
+ /** Device authorization was denied or cancelled by the user/provider. */
22
+ | "device-auth"
23
+ /** Misconfiguration (bad redirect URI, missing projectId, callback bind, …). */
24
+ | "configuration"
25
+ /** Cloud project provisioning / onboarding (loadCodeAssist, onboardUser). */
26
+ | "provisioning"
27
+ /** OIDC / endpoint discovery failed. */
28
+ | "discovery";
29
+
30
+ export interface OAuthErrorOptions {
31
+ kind?: OAuthErrorKind;
32
+ provider?: string;
33
+ status?: number;
34
+ cause?: unknown;
35
+ }
36
+
37
+ /**
38
+ * A failure inside an interactive OAuth / device-code login flow. The `kind`
39
+ * pinpoints the stage. Timeout/polling are classified transient; everything
40
+ * else is a hard auth failure so the credential layer does not silently retry.
41
+ */
42
+ export class OAuthError extends Error {
43
+ readonly kind: OAuthErrorKind;
44
+ readonly provider: string | undefined;
45
+ readonly status: number | undefined;
46
+
47
+ constructor(message: string, options: OAuthErrorOptions = {}) {
48
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
49
+ this.name = "OAuthError";
50
+ this.kind = options.kind ?? "http";
51
+ this.provider = options.provider;
52
+ this.status = options.status;
53
+ attach(
54
+ this,
55
+ this.kind === "timeout" || this.kind === "polling" ? create(Flag.Transient) : create(Flag.AuthFailed),
56
+ );
57
+ }
58
+ }
@@ -0,0 +1,56 @@
1
+ import { ProviderHttpError } from "./classes";
2
+ import { attach, create, Flag } from "./flags";
3
+
4
+ /** Which part of a provider exchange produced a non-HTTP error. */
5
+ export type ProviderResponseErrorKind =
6
+ /** Stream closed before a terminal completion/response event. */
7
+ | "incomplete-stream"
8
+ /** Terminal event carried an error / unexpected stop reason. */
9
+ | "output"
10
+ /** Response body was empty/missing when content was required. */
11
+ | "empty-body"
12
+ /** Malformed wire envelope (unexpected message ordering / shape). */
13
+ | "envelope"
14
+ /** Content was blocked by a provider safety filter. */
15
+ | "content-blocked"
16
+ /** Runtime/namespace resolution or other provider-internal failure. */
17
+ | "runtime";
18
+
19
+ export interface ProviderResponseErrorOptions {
20
+ provider?: string;
21
+ kind?: ProviderResponseErrorKind;
22
+ cause?: unknown;
23
+ }
24
+
25
+ /**
26
+ * A non-HTTP provider failure: a truncated stream, an error stop reason, an
27
+ * empty body, a malformed envelope, or a runtime fault. For non-2xx HTTP
28
+ * responses use {@link ProviderHttpError} (or a provider subclass) instead.
29
+ */
30
+ export class ProviderResponseError extends Error {
31
+ readonly provider: string | undefined;
32
+ readonly kind: ProviderResponseErrorKind;
33
+
34
+ constructor(message: string, options: ProviderResponseErrorOptions = {}) {
35
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
36
+ this.name = "ProviderResponseError";
37
+ this.provider = options.provider;
38
+ this.kind = options.kind ?? "output";
39
+ if (this.kind === "content-blocked") attach(this, create(Flag.ProviderFinishError));
40
+ }
41
+ }
42
+
43
+ /** Non-2xx response from the Devin API. */
44
+ export class DevinApiError extends ProviderHttpError {
45
+ override readonly name = "DevinApiError";
46
+ }
47
+
48
+ /** Non-2xx response from the GitLab Duo direct-access API. */
49
+ export class GitLabDuoApiError extends ProviderHttpError {
50
+ override readonly name = "GitLabDuoApiError";
51
+ }
52
+
53
+ /** Non-2xx response from the GitLab Duo Workflow API. */
54
+ export class GitLabDuoWorkflowApiError extends ProviderHttpError {
55
+ override readonly name = "GitLabDuoWorkflowApiError";
56
+ }
@@ -131,7 +131,7 @@ export function isUsageLimitStatus(status: number | undefined): boolean {
131
131
  * credentials.
132
132
  */
133
133
  export function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean {
134
- if (message && isUsageLimitError(message)) return true;
134
+ if (message && matchesUsageLimitText(message)) return true;
135
135
  if (!isUsageLimitStatus(status)) return false;
136
136
  if (!message || isOpaqueStatusBody(message)) return true;
137
137
  return parseRateLimitReason(message) === "QUOTA_EXHAUSTED";
@@ -143,13 +143,19 @@ export function isUsageLimitOutcome(status: number | undefined, message: string
143
143
  * generic punctuation. Anything else (retry hints, capacity wording, error
144
144
  * descriptions) is informative enough to defer to the classifier.
145
145
  */
146
- function isOpaqueStatusBody(message: string): boolean {
146
+ export function isOpaqueStatusBody(message: string): boolean {
147
147
  const cleaned = message
148
148
  .replace(/\b429\b/g, "")
149
149
  .replace(/\b(?:http|https|status|error|code|response|message)\b/gi, "");
150
150
  return !/[a-z\d]{3,}/i.test(cleaned);
151
151
  }
152
152
 
153
- export function isUsageLimitError(errorMessage: string): boolean {
153
+ /**
154
+ * Internal text matcher for usage/quota-limit phrasing. NOT part of the public
155
+ * API — callers classify through {@link import("./flags").isUsageLimit} (the
156
+ * flag accessor). `flags.ts` consumes this to populate `Flag.UsageLimit`, and
157
+ * {@link isUsageLimitOutcome} uses it for the account-rotation decision.
158
+ */
159
+ export function matchesUsageLimitText(errorMessage: string): boolean {
154
160
  return USAGE_LIMIT_PATTERN.test(errorMessage) || ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage);
155
161
  }
@@ -0,0 +1,70 @@
1
+ import { isRetryableError, isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils";
2
+ import {
3
+ isRetryableStreamEnvelopeError,
4
+ isTransientStreamParseError,
5
+ isUsageLimit,
6
+ status,
7
+ TRANSIENT_TRANSPORT_PATTERN,
8
+ } from "./flags";
9
+
10
+ /**
11
+ * Whether a numeric HTTP status is in the canonical transient/retryable set:
12
+ * 408 (Request Timeout), 429 (Too Many Requests), and any 5xx.
13
+ *
14
+ * This is a pure predicate over a status code already in hand — distinct from
15
+ * {@link classify}, which inspects a whole error (including message text) and
16
+ * may match more. Use this when you only have a `status: number`.
17
+ */
18
+ export function isTransientStatus(status: number | undefined): boolean {
19
+ return status !== undefined && (status === 408 || status === 429 || status >= 500);
20
+ }
21
+
22
+ // Provider-stream transient phrasings not covered by the shared
23
+ // TRANSIENT_TRANSPORT_PATTERN (TLS record corruption, HTTP/2 peer stream
24
+ // errors, upstream code 1302). The shared pattern already covers rate-limit /
25
+ // overloaded / 5xx / timeout / first-event wording.
26
+ const PROVIDER_TRANSIENT_EXTRA_PATTERN = /bad record mac|stream error.*received from peer|1302/i;
27
+
28
+ function isTransientTransportMessage(message: string): boolean {
29
+ return message.includes("tls: bad record mac") || message.includes("type=server_error");
30
+ }
31
+
32
+ /** Hook for provider-specific transient detection that the error module must not import directly. */
33
+ export interface ProviderRetryableHooks {
34
+ /** Provider id of the failing request, used to gate provider-specific checks. */
35
+ provider?: string;
36
+ /** Provider-specific transient predicate (e.g. Copilot `model_not_supported`). */
37
+ isProviderTransient?: (error: Error) => boolean;
38
+ }
39
+
40
+ /**
41
+ * Whether a provider stream error should be retried against the same credential.
42
+ *
43
+ * Account-level usage/quota limits are deliberately treated as **non**-retryable
44
+ * here — they are owned by the credential-rotation layer (auth-gateway /
45
+ * `streamSimple` a/b/c policy), not this seconds-scale provider backoff.
46
+ *
47
+ * Provider-specific transient cases are injected via {@link ProviderRetryableHooks}
48
+ * so this stays free of provider imports.
49
+ */
50
+ export function isProviderRetryableError(error: unknown, hooks: ProviderRetryableHooks = {}): boolean {
51
+ if (!(error instanceof Error)) return false;
52
+ if (hooks.isProviderTransient?.(error)) return true;
53
+ if (isUsageLimit(error)) return false;
54
+ const httpStatus = status(error);
55
+ if (httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500 && httpStatus !== 408 && httpStatus !== 429) {
56
+ return false;
57
+ }
58
+ const msg = error.message.toLowerCase();
59
+ if (
60
+ isUnexpectedSocketCloseMessage(msg) ||
61
+ isTransientTransportMessage(msg) ||
62
+ TRANSIENT_TRANSPORT_PATTERN.test(msg) ||
63
+ PROVIDER_TRANSIENT_EXTRA_PATTERN.test(msg) ||
64
+ isTransientStreamParseError(error) ||
65
+ isRetryableStreamEnvelopeError(error)
66
+ ) {
67
+ return true;
68
+ }
69
+ return isRetryableError(error);
70
+ }
@@ -0,0 +1,44 @@
1
+ import { attach, create, Flag } from "./flags";
2
+
3
+ /**
4
+ * Caller-supplied input failed validation before/while building a provider
5
+ * request: bad request body, malformed tool arguments, unsupported content
6
+ * type, a schema that cannot be normalized, an unknown tool, etc.
7
+ *
8
+ * This is a programmer/config/contract error, not a transient provider fault —
9
+ * it is never retried.
10
+ */
11
+ export class ValidationError extends Error {
12
+ constructor(message: string, options?: { cause?: unknown }) {
13
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
14
+ this.name = "ValidationError";
15
+ }
16
+ }
17
+
18
+ /** A referenced tool was not found in the active tool set. */
19
+ export class ToolNotFoundError extends ValidationError {
20
+ constructor(toolName: string) {
21
+ super(`Tool "${toolName}" not found`);
22
+ this.name = "ToolNotFoundError";
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Provider/auth configuration was missing or malformed (env var pointing at a
28
+ * missing file, missing projectId, bad bind string, mTLS half-configured, …).
29
+ */
30
+ export class ConfigurationError extends Error {
31
+ constructor(message: string, options?: { cause?: unknown }) {
32
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
33
+ this.name = "ConfigurationError";
34
+ }
35
+ }
36
+
37
+ /** A request was abandoned because it exceeded a stream/idle/first-event deadline. */
38
+ export class StreamTimeoutError extends Error {
39
+ constructor(message = "Request timed out.", options?: { cause?: unknown }) {
40
+ super(message, options?.cause === undefined ? undefined : { cause: options.cause });
41
+ this.name = "StreamTimeoutError";
42
+ attach(this, create(Flag.Transient, Flag.Timeout));
43
+ }
44
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/serve
6
6
  export * from "./auth-gateway/types";
7
7
  export * from "./auth-retry";
8
8
  export * from "./auth-storage";
9
- export * from "./errors";
9
+ export * from "./error/rate-limit";
10
10
  export * from "./provider-details";
11
11
  export * from "./providers/anthropic";
12
12
  export * from "./providers/anthropic-client";
@@ -24,7 +24,6 @@ export * from "./providers/openai-codex-responses";
24
24
  export * from "./providers/openai-completions";
25
25
  export * from "./providers/openai-responses";
26
26
  export * from "./providers/synthetic";
27
- export * from "./rate-limit-utils";
28
27
  export * from "./registry";
29
28
  export * from "./stream";
30
29
  export * from "./types";
@@ -43,7 +42,6 @@ export * from "./usage/zai";
43
42
  export * from "./utils/anthropic-auth";
44
43
  export * from "./utils/event-stream";
45
44
  export * from "./utils/openrouter-headers";
46
- export * from "./utils/overflow";
47
45
  export * from "./utils/retry";
48
46
  export * from "./utils/schema";
49
47
  export * from "./utils/thinking-loop";
@@ -10,15 +10,9 @@
10
10
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
11
11
  import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "@oh-my-pi/pi-catalog/model-thinking";
12
12
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
13
- import {
14
- $env,
15
- $flag,
16
- extractHttpStatusFromError,
17
- fetchWithRetry,
18
- parseStreamingJson,
19
- parseStreamingJsonThrottled,
20
- } from "@oh-my-pi/pi-utils";
21
- import { ProviderHttpError } from "../errors";
13
+ import { $env, $flag, fetchWithRetry, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
14
+ import { renderDemotedThinking } from "../dialect/demotion";
15
+ import * as AIError from "../error";
22
16
  import type {
23
17
  Api,
24
18
  AssistantMessage,
@@ -36,8 +30,14 @@ import type {
36
30
  ToolResultMessage,
37
31
  } from "../types";
38
32
  import { normalizeToolCallId, resolveCacheRetention } from "../utils";
33
+ import {
34
+ clearStreamingPartialJson,
35
+ kStreamingBlockIndex,
36
+ kStreamingLastParseLen,
37
+ kStreamingPartialJson,
38
+ } from "../utils/block-symbols";
39
39
  import { AssistantMessageEventStream } from "../utils/event-stream";
40
- import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump } from "../utils/http-inspector";
40
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
41
41
  import { armPreResponseTimeout, getStreamFirstEventTimeoutMs } from "../utils/idle-iterator";
42
42
  import { toolWireSchema } from "../utils/schema/wire";
43
43
  import { invalidateAwsCredentialCache, resolveAwsCredentials } from "./aws-credentials";
@@ -45,11 +45,6 @@ import { decodeEventStream } from "./aws-eventstream";
45
45
  import { signRequest } from "./aws-sigv4";
46
46
  import { transformMessages } from "./transform-messages";
47
47
 
48
- /** Non-2xx response (or in-stream exception event) from the Bedrock runtime API. */
49
- export class BedrockApiError extends ProviderHttpError {
50
- override readonly name = "BedrockApiError";
51
- }
52
-
53
48
  export type BedrockThinkingDisplay = "summarized" | "omitted";
54
49
 
55
50
  export interface BedrockOptions extends StreamOptions {
@@ -164,9 +159,9 @@ function resolveBedrockRegion(modelId: string, options: BedrockOptions): string
164
159
  }
165
160
 
166
161
  type Block = (TextContent | ThinkingContent | ToolCall) & {
167
- index?: number;
168
- partialJson?: string;
169
- lastParseLen?: number;
162
+ [kStreamingBlockIndex]?: number;
163
+ [kStreamingPartialJson]?: string;
164
+ [kStreamingLastParseLen]?: number;
170
165
  };
171
166
 
172
167
  // ---------- Bedrock wire-format types ----------
@@ -288,7 +283,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
288
283
  const stream = new AssistantMessageEventStream();
289
284
 
290
285
  (async () => {
291
- const startTime = Date.now();
286
+ const startTime = performance.now();
292
287
  let firstTokenTime: number | undefined;
293
288
 
294
289
  const output: AssistantMessage = {
@@ -421,11 +416,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
421
416
  invalidateAwsCredentialCache({ profile: options.profile, region });
422
417
  }
423
418
  const errBody = await response.text().catch(() => "");
424
- throw new BedrockApiError(`Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}`, response.status, {
425
- headers: response.headers,
426
- });
419
+ throw new AIError.BedrockApiError(
420
+ `Bedrock HTTP ${response.status}: ${errBody.slice(0, 1000)}`,
421
+ response.status,
422
+ {
423
+ headers: response.headers,
424
+ },
425
+ );
427
426
  }
428
- if (!response.body) throw new Error("Bedrock response has no body");
427
+ if (!response.body) throw new AIError.BedrockApiError("Bedrock response has no body", response.status);
429
428
 
430
429
  // Track first event for the abort/diagnostic path (currently informational).
431
430
  for await (const message of decodeEventStream(response.body)) {
@@ -437,14 +436,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
437
436
  const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
438
437
  const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
439
438
  const text = `${exceptionType}: ${errorMessage}`;
440
- throw exceptionType === "validationException"
441
- ? new BedrockApiError(text, 400, { code: exceptionType })
442
- : new Error(text);
439
+ throw new AIError.BedrockApiError(text, 400, { code: exceptionType });
443
440
  }
444
441
  if (messageType === "error") {
445
442
  const code = message.headers[":error-code"] || "UnknownError";
446
443
  const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
447
- throw new Error(`${code}: ${errorMessage}`);
444
+ throw new AIError.BedrockApiError(`${code}: ${errorMessage}`, 400, { code });
448
445
  }
449
446
  if (messageType !== "event") continue;
450
447
 
@@ -456,18 +453,21 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
456
453
  // no-op: first event marker is implicit by stream entry.
457
454
  const ev = payload as MessageStartEvent;
458
455
  if (ev.role !== "assistant") {
459
- throw new Error("Unexpected assistant message start but got user message start instead");
456
+ throw new AIError.BedrockApiError(
457
+ "Unexpected assistant message start but got user message start instead",
458
+ 0,
459
+ );
460
460
  }
461
461
  stream.push({ type: "start", partial: output });
462
462
  break;
463
463
  }
464
464
  case "contentBlockStart": {
465
- if (!firstTokenTime) firstTokenTime = Date.now();
465
+ if (!firstTokenTime) firstTokenTime = performance.now();
466
466
  handleContentBlockStart(payload as ContentBlockStartEvent, blocks, output, stream, sentinelInjected);
467
467
  break;
468
468
  }
469
469
  case "contentBlockDelta": {
470
- if (!firstTokenTime) firstTokenTime = Date.now();
470
+ if (!firstTokenTime) firstTokenTime = performance.now();
471
471
  handleContentBlockDelta(payload as ContentBlockDeltaEvent, blocks, output, stream);
472
472
  break;
473
473
  }
@@ -496,23 +496,20 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
496
496
  }
497
497
  }
498
498
 
499
- if (options.signal?.aborted) throw new Error("Request was aborted");
499
+ if (options.signal?.aborted) throw new AIError.AbortError();
500
500
 
501
501
  if (output.stopReason === "error" || output.stopReason === "aborted") {
502
- throw new Error(output.errorMessage ?? "An unknown error occurred");
502
+ throw new AIError.BedrockApiError(output.errorMessage ?? "An unknown error occurred", 0);
503
503
  }
504
504
 
505
- output.duration = Date.now() - startTime;
505
+ output.duration = performance.now() - startTime;
506
506
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
507
507
  stream.push({ type: "done", reason: output.stopReason, message: output });
508
508
  stream.end();
509
509
  } catch (error) {
510
510
  for (const block of output.content) {
511
- delete (block as Block).index;
512
- delete (block as Block).partialJson;
511
+ if (block.type === "toolCall") clearStreamingPartialJson(block);
513
512
  }
514
- output.stopReason = options.signal?.aborted ? "aborted" : "error";
515
- output.errorStatus = extractHttpStatusFromError(error);
516
513
  const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
517
514
  // Enrich error with thinking block diagnostics for signature-related failures
518
515
  let diagnostics = "";
@@ -534,8 +531,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
534
531
  diagnostics = `\n[thinking-diag] ${JSON.stringify(thinkingBlocks)}`;
535
532
  }
536
533
  }
537
- output.errorMessage = await appendRawHttpRequestDumpFor400(baseMessage + diagnostics, error, rawRequestDump);
538
- output.duration = Date.now() - startTime;
534
+ const result = await AIError.finalize(error, { api: model.api, signal: options.signal, rawRequestDump });
535
+ output.stopReason = result.stopReason;
536
+ output.errorStatus = result.status;
537
+ output.errorId = result.id;
538
+ output.errorMessage = result.message + diagnostics;
539
+ output.duration = performance.now() - startTime;
539
540
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
540
541
  stream.push({ type: "error", reason: output.stopReason, error: output });
541
542
  stream.end();
@@ -575,8 +576,8 @@ function handleContentBlockStart(
575
576
  id: normalizeToolCallId(start.toolUse.toolUseId || ""),
576
577
  name: start.toolUse.name || "",
577
578
  arguments: {},
578
- partialJson: "",
579
- index,
579
+ [kStreamingPartialJson]: "",
580
+ [kStreamingBlockIndex]: index,
580
581
  };
581
582
  output.content.push(block);
582
583
  stream.push({ type: "toolcall_start", contentIndex: blocks.length - 1, partial: output });
@@ -591,13 +592,13 @@ function handleContentBlockDelta(
591
592
  ): void {
592
593
  const contentBlockIndex = event.contentBlockIndex;
593
594
  const delta = event.delta;
594
- let index = blocks.findIndex(b => b.index === contentBlockIndex);
595
+ let index = blocks.findIndex(b => b[kStreamingBlockIndex] === contentBlockIndex);
595
596
  let block = blocks[index];
596
597
 
597
598
  if (delta?.text !== undefined) {
598
599
  // If no text block exists yet, create one — `handleContentBlockStart` is not sent for text blocks
599
600
  if (!block) {
600
- const newBlock: Block = { type: "text", text: "", index: contentBlockIndex };
601
+ const newBlock: Block = { type: "text", text: "", [kStreamingBlockIndex]: contentBlockIndex };
601
602
  output.content.push(newBlock);
602
603
  index = blocks.length - 1;
603
604
  block = blocks[index];
@@ -608,11 +609,11 @@ function handleContentBlockDelta(
608
609
  stream.push({ type: "text_delta", contentIndex: index, delta: delta.text, partial: output });
609
610
  }
610
611
  } else if (delta?.toolUse && block?.type === "toolCall") {
611
- block.partialJson = (block.partialJson || "") + (delta.toolUse.input || "");
612
- const throttled = parseStreamingJsonThrottled(block.partialJson, block.lastParseLen ?? 0);
612
+ block[kStreamingPartialJson] = (block[kStreamingPartialJson] || "") + (delta.toolUse.input || "");
613
+ const throttled = parseStreamingJsonThrottled(block[kStreamingPartialJson], block[kStreamingLastParseLen] ?? 0);
613
614
  if (throttled) {
614
615
  block.arguments = throttled.value;
615
- block.lastParseLen = throttled.parsedLen;
616
+ block[kStreamingLastParseLen] = throttled.parsedLen;
616
617
  }
617
618
  stream.push({ type: "toolcall_delta", contentIndex: index, delta: delta.toolUse.input || "", partial: output });
618
619
  } else if (delta?.reasoningContent) {
@@ -620,7 +621,12 @@ function handleContentBlockDelta(
620
621
  let thinkingIndex = index;
621
622
 
622
623
  if (!thinkingBlock) {
623
- const newBlock: Block = { type: "thinking", thinking: "", thinkingSignature: "", index: contentBlockIndex };
624
+ const newBlock: Block = {
625
+ type: "thinking",
626
+ thinking: "",
627
+ thinkingSignature: "",
628
+ [kStreamingBlockIndex]: contentBlockIndex,
629
+ };
624
630
  output.content.push(newBlock);
625
631
  thinkingIndex = blocks.length - 1;
626
632
  thinkingBlock = blocks[thinkingIndex];
@@ -662,10 +668,9 @@ function handleContentBlockStop(
662
668
  output: AssistantMessage,
663
669
  stream: AssistantMessageEventStream,
664
670
  ): void {
665
- const index = blocks.findIndex(b => b.index === event.contentBlockIndex);
671
+ const index = blocks.findIndex(b => b[kStreamingBlockIndex] === event.contentBlockIndex);
666
672
  const block = blocks[index];
667
673
  if (!block) return;
668
- delete (block as Block).index;
669
674
 
670
675
  switch (block.type) {
671
676
  case "text":
@@ -675,9 +680,8 @@ function handleContentBlockStop(
675
680
  stream.push({ type: "thinking_end", contentIndex: index, content: block.thinking, partial: output });
676
681
  break;
677
682
  case "toolCall":
678
- block.arguments = parseStreamingJson(block.partialJson);
679
- delete (block as Block).partialJson;
680
- delete (block as Block).lastParseLen;
683
+ block.arguments = parseStreamingJson(block[kStreamingPartialJson]);
684
+ clearStreamingPartialJson(block);
681
685
  stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: output });
682
686
  break;
683
687
  }
@@ -772,7 +776,7 @@ function convertMessages(
772
776
  contentBlocks.push({ image: createImageBlock(c.mimeType, c.data) });
773
777
  break;
774
778
  default:
775
- throw new Error("Unknown user content type");
779
+ throw new AIError.ValidationError("Unknown user content type");
776
780
  }
777
781
  }
778
782
  // Skip message if all blocks filtered out
@@ -820,11 +824,11 @@ function convertMessages(
820
824
  });
821
825
  } else {
822
826
  // Model requires signature but we don't have one — demote to text
823
- contentBlocks.push({ text: `[Thinking]: ${c.thinking.toWellFormed()}` });
827
+ contentBlocks.push({ text: renderDemotedThinking(model.id, c.thinking) });
824
828
  }
825
829
  break;
826
830
  default:
827
- throw new Error("Unknown assistant content type");
831
+ throw new AIError.ValidationError("Unknown assistant content type");
828
832
  }
829
833
  }
830
834
  // Skip if all content blocks were filtered out
@@ -870,7 +874,7 @@ function convertMessages(
870
874
  break;
871
875
  }
872
876
  default:
873
- throw new Error("Unknown message role");
877
+ throw new AIError.ValidationError("Unknown message role");
874
878
  }
875
879
  }
876
880
 
@@ -1032,7 +1036,7 @@ function createImageBlock(mimeType: string, data: string): ImageBlockWire["image
1032
1036
  format = "webp";
1033
1037
  break;
1034
1038
  default:
1035
- throw new Error(`Unknown image type: ${mimeType}`);
1039
+ throw new AIError.ValidationError(`Unknown image type: ${mimeType}`);
1036
1040
  }
1037
1041
  return { source: { bytes: data }, format };
1038
1042
  }
@@ -21,7 +21,11 @@
21
21
  * with up to 25% jitter).
22
22
  */
23
23
  import { scheduler } from "node:timers/promises";
24
- import { ProviderHttpError } from "../errors";
24
+ import * as AIError from "../error";
25
+ import { AnthropicApiError, AnthropicConnectionError, AnthropicConnectionTimeoutError } from "../error";
26
+
27
+ export { AnthropicApiError, AnthropicConnectionError, AnthropicConnectionTimeoutError };
28
+
25
29
  import type { FetchImpl } from "../types";
26
30
  import type { MessageCreateParamsStreaming } from "./anthropic-wire";
27
31
 
@@ -77,42 +81,8 @@ export interface AnthropicClientOptions {
77
81
  fetchOptions?: AnthropicFetchOptions;
78
82
  }
79
83
 
80
- /** Non-2xx response from the Anthropic API. */
81
- export class AnthropicApiError extends ProviderHttpError {
82
- declare readonly headers: Headers;
83
- readonly requestId: string | null;
84
-
85
- constructor(status: number, message: string, headers: Headers) {
86
- super(message, status, { headers });
87
- this.name = "AnthropicApiError";
88
- this.requestId = headers.get("request-id");
89
- }
90
-
91
- static async fromResponse(response: Response): Promise<AnthropicApiError> {
92
- const body = await response.text().catch(() => "");
93
- const detail = body.trim() || "status code (no body)";
94
- return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers);
95
- }
96
- }
97
-
98
- /** Network-level failure (DNS, TLS, socket reset) after retries were exhausted. */
99
- export class AnthropicConnectionError extends Error {
100
- constructor(cause: unknown) {
101
- super("Connection error.", { cause });
102
- this.name = "AnthropicConnectionError";
103
- }
104
- }
105
-
106
- /** No response headers arrived within the configured request timeout. */
107
- export class AnthropicConnectionTimeoutError extends Error {
108
- constructor() {
109
- super("Request timed out.");
110
- this.name = "AnthropicConnectionTimeoutError";
111
- }
112
- }
113
-
114
84
  function createAbortError(): Error {
115
- return new Error("Request was aborted.");
85
+ return new AIError.AbortError("Request was aborted.");
116
86
  }
117
87
 
118
88
  /** `x-should-retry` override, then 408/409/429/5xx. */
@@ -121,7 +91,9 @@ function shouldRetryResponse(response: Response): boolean {
121
91
  if (shouldRetryHeader === "true") return true;
122
92
  if (shouldRetryHeader === "false") return false;
123
93
  const status = response.status;
124
- return status === 408 || status === 409 || status === 429 || status >= 500;
94
+ // Canonical transient set (408/429/5xx) plus 409, which Anthropic's client
95
+ // also retries.
96
+ return AIError.isTransientStatus(status) || status === 409;
125
97
  }
126
98
 
127
99
  /** Server-suggested delay (`retry-after-ms`, then `retry-after` seconds or HTTP date). */
@@ -260,8 +232,8 @@ export class AnthropicMessagesClient implements AnthropicMessagesClientLike {
260
232
  await this.#backoff(attempt, undefined, callerSignal);
261
233
  continue;
262
234
  }
263
- if (error instanceof AnthropicConnectionTimeoutError) throw error;
264
- throw new AnthropicConnectionError(error);
235
+ if (error instanceof AIError.AnthropicConnectionTimeoutError) throw error;
236
+ throw new AIError.AnthropicConnectionError(error);
265
237
  }
266
238
 
267
239
  if (response.ok) return response;
@@ -271,7 +243,7 @@ export class AnthropicMessagesClient implements AnthropicMessagesClientLike {
271
243
  await this.#backoff(attempt, response.headers, callerSignal);
272
244
  continue;
273
245
  }
274
- throw await AnthropicApiError.fromResponse(response);
246
+ throw await AIError.AnthropicApiError.fromResponse(response);
275
247
  }
276
248
  }
277
249
 
@@ -300,7 +272,7 @@ export class AnthropicMessagesClient implements AnthropicMessagesClientLike {
300
272
  signal: controller.signal,
301
273
  });
302
274
  } catch (error) {
303
- if (timedOut && !callerSignal?.aborted) throw new AnthropicConnectionTimeoutError();
275
+ if (timedOut && !callerSignal?.aborted) throw new AIError.AnthropicConnectionTimeoutError();
304
276
  throw error;
305
277
  } finally {
306
278
  clearTimeout(timer);