@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
@@ -17,6 +17,8 @@
17
17
  * practice (`:event-type`, `:message-type`, `:content-type`, `:exception-type`).
18
18
  */
19
19
 
20
+ import * as AIError from "../error";
21
+
20
22
  const PRELUDE_LEN = 8;
21
23
  const PRELUDE_CRC_LEN = 4;
22
24
  const MESSAGE_CRC_LEN = 4;
@@ -30,20 +32,8 @@ export interface EventStreamMessage {
30
32
  }
31
33
 
32
34
  /** CRC32 (IEEE / zlib polynomial 0xEDB88320), matches `@aws-crypto/crc32`. */
33
- const CRC_TABLE = (() => {
34
- const t = new Uint32Array(256);
35
- for (let i = 0; i < 256; i++) {
36
- let c = i;
37
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
38
- t[i] = c >>> 0;
39
- }
40
- return t;
41
- })();
42
-
43
- export function crc32(bytes: Uint8Array, seed = 0): number {
44
- let c = (seed ^ 0xffffffff) >>> 0;
45
- for (let i = 0; i < bytes.length; i++) c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
46
- return (c ^ 0xffffffff) >>> 0;
35
+ export function crc32(bytes: Uint8Array): number {
36
+ return Bun.hash.crc32(bytes) >>> 0;
47
37
  }
48
38
 
49
39
  /**
@@ -53,17 +43,18 @@ export function crc32(bytes: Uint8Array, seed = 0): number {
53
43
  * frames.
54
44
  */
55
45
  export function decodeMessage(frame: Uint8Array): EventStreamMessage {
56
- if (frame.length < MIN_MESSAGE_LEN) throw new Error("eventstream: frame too short");
46
+ if (frame.length < MIN_MESSAGE_LEN) throw new AIError.EventStreamFrameError("frame too short");
57
47
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
58
48
  const total = view.getUint32(0, false);
59
- if (total !== frame.length) throw new Error(`eventstream: framed length ${total} != buffer ${frame.length}`);
49
+ if (total !== frame.length)
50
+ throw new AIError.EventStreamFrameError(`framed length ${total} != buffer ${frame.length}`);
60
51
  const headersLen = view.getUint32(4, false);
61
52
  const preludeCrc = view.getUint32(8, false);
62
53
  const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
63
- if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
54
+ if (computedPreludeCrc !== preludeCrc) throw new AIError.EventStreamFrameError("prelude CRC mismatch");
64
55
  const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
65
56
  const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
66
- if (computedMsgCrc !== msgCrc) throw new Error("eventstream: message CRC mismatch");
57
+ if (computedMsgCrc !== msgCrc) throw new AIError.EventStreamFrameError("message CRC mismatch");
67
58
 
68
59
  const headersBytes = frame.subarray(HEADER_BLOCK_OFFSET, HEADER_BLOCK_OFFSET + headersLen);
69
60
  const payload = frame.subarray(HEADER_BLOCK_OFFSET + headersLen, total - MESSAGE_CRC_LEN);
@@ -136,7 +127,7 @@ function parseHeaders(buf: Uint8Array): Record<string, string> {
136
127
  break;
137
128
  }
138
129
  default:
139
- throw new Error(`eventstream: unknown header value type ${type}`);
130
+ throw new AIError.EventStreamFrameError(`unknown header value type ${type}`);
140
131
  }
141
132
  }
142
133
  return out;
@@ -170,7 +161,7 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
170
161
  while (buf.length - offset >= 4) {
171
162
  const dv = new DataView(buf.buffer, buf.byteOffset + offset, buf.length - offset);
172
163
  const total = dv.getUint32(0, false);
173
- if (total < MIN_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} below minimum`);
164
+ if (total < MIN_MESSAGE_LEN) throw new AIError.EventStreamFrameError(`total length ${total} below minimum`);
174
165
  if (buf.length - offset < total) break;
175
166
  const frame = buf.subarray(offset, offset + total);
176
167
  yield decodeMessage(frame);
@@ -179,7 +170,7 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
179
170
  if (offset > 0) buf = buf.slice(offset);
180
171
  if (done) break;
181
172
  }
182
- if (buf.length > 0) throw new Error("eventstream: truncated message at end of stream");
173
+ if (buf.length > 0) throw new AIError.EventStreamFrameError("truncated message at end of stream");
183
174
  completed = true;
184
175
  } finally {
185
176
  // On abnormal exit (consumer threw/broke, decode error) cancel the body so the
@@ -1,4 +1,5 @@
1
- import { $env, extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
1
+ import { $env } from "@oh-my-pi/pi-utils";
2
+ import * as AIError from "../error";
2
3
  import { getEnvApiKey } from "../stream";
3
4
  import type {
4
5
  AssistantMessage,
@@ -12,7 +13,7 @@ import type {
12
13
  } from "../types";
13
14
  import { createAbortSourceTracker } from "../utils/abort";
14
15
  import { AssistantMessageEventStream } from "../utils/event-stream";
15
- import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
16
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
16
17
  import {
17
18
  getOpenAIStreamFirstEventTimeoutMs,
18
19
  getOpenAIStreamIdleTimeoutMs,
@@ -85,7 +86,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
85
86
 
86
87
  // Start async processing
87
88
  (async () => {
88
- const startTime = Date.now();
89
+ const startTime = performance.now();
89
90
  let firstTokenTime: number | undefined;
90
91
  const deploymentName = resolveDeploymentName(model, options);
91
92
 
@@ -96,7 +97,9 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
96
97
  );
97
98
  let rawRequestDump: RawHttpRequestDump | undefined;
98
99
  const abortTracker = createAbortSourceTracker(options?.signal);
99
- const firstEventTimeoutAbortError = new Error(AZURE_OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE);
100
+ const firstEventTimeoutAbortError = new AIError.StreamTimeoutError(
101
+ AZURE_OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE,
102
+ );
100
103
  const { requestAbortController, requestSignal } = abortTracker;
101
104
  const onSseEvent = options?.onSseEvent;
102
105
  const rawSseObserver = onSseEvent
@@ -203,7 +206,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
203
206
  let sawTerminalResponseEvent = false;
204
207
  await processResponsesStream(timedOpenaiStream, output, stream, model, {
205
208
  onFirstToken: () => {
206
- if (!firstTokenTime) firstTokenTime = Date.now();
209
+ if (!firstTokenTime) firstTokenTime = performance.now();
207
210
  },
208
211
  onCompleted: () => {
209
212
  sawTerminalResponseEvent = true;
@@ -216,28 +219,34 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
216
219
  }
217
220
 
218
221
  if (abortTracker.wasCallerAbort()) {
219
- throw new Error("Request was aborted");
222
+ throw new AIError.AbortError();
220
223
  }
221
224
 
222
225
  if (!sawTerminalResponseEvent) {
223
- throw new Error("Azure OpenAI responses stream closed before a terminal response event was received");
226
+ throw new AIError.ProviderResponseError(
227
+ "Azure OpenAI responses stream closed before a terminal response event was received",
228
+ { provider: model.provider, kind: "incomplete-stream" },
229
+ );
224
230
  }
225
231
 
226
232
  if (output.stopReason === "aborted" || output.stopReason === "error") {
227
- throw new Error(output.errorMessage ?? "An unknown error occurred");
233
+ throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
234
+ provider: model.provider,
235
+ kind: "output",
236
+ });
228
237
  }
229
238
 
230
- output.duration = Date.now() - startTime;
239
+ output.duration = performance.now() - startTime;
231
240
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
232
241
  stream.push({ type: "done", reason: output.stopReason, message: output });
233
242
  stream.end();
234
243
  } catch (error) {
235
- for (const block of output.content) delete (block as { index?: number }).index;
236
- const firstEventTimeoutError = abortTracker.getLocalAbortReason();
237
- output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
238
- output.errorStatus = extractHttpStatusFromError(error);
239
- output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
240
- output.duration = Date.now() - startTime;
244
+ const result = await AIError.finalize(error, { api: model.api, abortTracker, rawRequestDump });
245
+ output.stopReason = result.stopReason;
246
+ output.errorStatus = result.status;
247
+ output.errorId = result.id;
248
+ output.errorMessage = result.message;
249
+ output.duration = performance.now() - startTime;
241
250
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
242
251
  stream.push({ type: "error", reason: output.stopReason, error: output });
243
252
  stream.end();
@@ -267,7 +276,7 @@ function resolveAzureConfig(
267
276
  }
268
277
 
269
278
  if (!resolvedBaseUrl) {
270
- throw new Error(
279
+ throw new AIError.ConfigurationError(
271
280
  "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.",
272
281
  );
273
282
  }
@@ -295,7 +304,8 @@ function buildAzureResponsesRequest(
295
304
  if (!apiKey) {
296
305
  const envKey = $env.AZURE_OPENAI_API_KEY;
297
306
  if (!envKey) {
298
- throw new Error(
307
+ throw new AIError.MissingApiKeyError(
308
+ undefined,
299
309
  "Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.",
300
310
  );
301
311
  }
@@ -102,13 +102,8 @@ import {
102
102
  WriteSuccessSchema,
103
103
  } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
104
104
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
105
- import {
106
- $env,
107
- extractHttpStatusFromError,
108
- parseJsonWithRepair,
109
- parseStreamingJson,
110
- sanitizeText,
111
- } from "@oh-my-pi/pi-utils";
105
+ import { $env, parseJsonWithRepair, parseStreamingJson, sanitizeText } from "@oh-my-pi/pi-utils";
106
+ import * as AIError from "../error";
112
107
  import type {
113
108
  Api,
114
109
  AssistantMessage,
@@ -130,11 +125,16 @@ import type {
130
125
  ToolResultMessage,
131
126
  } from "../types";
132
127
  import { normalizeSystemPrompts } from "../utils";
128
+ import {
129
+ clearStreamingPartialJson,
130
+ kStreamingBlockIndex,
131
+ kStreamingBlockKind,
132
+ kStreamingPartialJson,
133
+ } from "../utils/block-symbols";
133
134
  import { deterministicUuid } from "../utils/deterministic-id";
134
135
  import { AssistantMessageEventStream } from "../utils/event-stream";
135
136
  import { connectProxiedSocket, getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
136
137
  import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
137
- import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
138
138
  import { toolWireSchema } from "../utils/schema/wire";
139
139
 
140
140
  export const CURSOR_API_URL = "https://api2.cursor.sh";
@@ -194,11 +194,11 @@ function parseConnectEndStream(data: Uint8Array): Error | null {
194
194
  if (error) {
195
195
  const code = typeof error.code === "string" ? error.code : "unknown";
196
196
  const message = typeof error.message === "string" ? error.message : "Unknown error";
197
- return new Error(`Connect error ${code}: ${message}`);
197
+ return new AIError.ProviderResponseError(`Connect error ${code}: ${message}`, { kind: "envelope" });
198
198
  }
199
199
  return null;
200
200
  } catch {
201
- return new Error("Failed to parse Connect end stream");
201
+ return new AIError.ProviderResponseError("Failed to parse Connect end stream", { kind: "envelope" });
202
202
  }
203
203
  }
204
204
 
@@ -315,7 +315,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
315
315
  const stream = new AssistantMessageEventStream();
316
316
 
317
317
  (async () => {
318
- const startTime = Date.now();
318
+ const startTime = performance.now();
319
319
  let firstTokenTime: number | undefined;
320
320
 
321
321
  const output: AssistantMessage = {
@@ -344,7 +344,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
344
344
  try {
345
345
  const apiKey = options?.apiKey;
346
346
  if (!apiKey) {
347
- throw new Error("Cursor API key (access token) is required");
347
+ throw new AIError.MissingApiKeyError(undefined, "Cursor API key (access token) is required");
348
348
  }
349
349
 
350
350
  const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
@@ -399,8 +399,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
399
399
 
400
400
  let pendingBuffer = Buffer.alloc(0);
401
401
  let endStreamError: Error | null = null;
402
- let currentTextBlock: (TextContent & { index: number }) | null = null;
403
- let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
402
+ let currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number }) | null = null;
403
+ let currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null = null;
404
404
  let currentToolCall: ToolCallState | null = null;
405
405
  const usageState: UsageState = { sawTokenDelta: false };
406
406
 
@@ -427,7 +427,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
427
427
  currentToolCall = t;
428
428
  },
429
429
  setFirstTokenTime: () => {
430
- if (!firstTokenTime) firstTokenTime = Date.now();
430
+ if (!firstTokenTime) firstTokenTime = performance.now();
431
431
  },
432
432
  };
433
433
 
@@ -531,7 +531,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
531
531
  const msg = trailers["grpc-message"];
532
532
  if (status && status !== "0") {
533
533
  void closeDebugLog().finally(() => {
534
- reject(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
534
+ reject(
535
+ new AIError.ProviderResponseError(
536
+ `gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`,
537
+ { kind: "envelope" },
538
+ ),
539
+ );
535
540
  });
536
541
  }
537
542
  });
@@ -557,7 +562,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
557
562
  options.signal.addEventListener("abort", () => {
558
563
  h2Request?.close();
559
564
  void closeDebugLog().finally(() => {
560
- reject(new Error("Request was aborted"));
565
+ reject(new AIError.AbortError());
561
566
  });
562
567
  });
563
568
  }
@@ -567,9 +572,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
567
572
  endCurrentThinkingBlock(output, stream, state);
568
573
  if (state.currentToolCall) {
569
574
  const idx = output.content.indexOf(state.currentToolCall);
570
- state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
571
- delete (state.currentToolCall as any).partialJson;
572
- delete (state.currentToolCall as any).index;
575
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall[kStreamingPartialJson]);
576
+ clearStreamingPartialJson(state.currentToolCall);
573
577
  stream.push({
574
578
  type: "toolcall_end",
575
579
  contentIndex: idx,
@@ -580,7 +584,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
580
584
 
581
585
  calculateCost(model, output.usage);
582
586
 
583
- output.duration = Date.now() - startTime;
587
+ output.duration = performance.now() - startTime;
584
588
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
585
589
  stream.push({
586
590
  type: "done",
@@ -589,10 +593,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
589
593
  });
590
594
  stream.end();
591
595
  } catch (error) {
592
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
593
- output.errorStatus = extractHttpStatusFromError(error);
594
- output.errorMessage = formatErrorMessageWithRetryAfter(error);
595
- output.duration = Date.now() - startTime;
596
+ const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
597
+ output.stopReason = result.stopReason;
598
+ output.errorStatus = result.status;
599
+ output.errorId = result.id;
600
+ output.errorMessage = result.message;
601
+ output.duration = performance.now() - startTime;
596
602
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
597
603
  stream.push({ type: "error", reason: output.stopReason, error: output });
598
604
  stream.end();
@@ -611,15 +617,19 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
611
617
  return stream;
612
618
  };
613
619
 
614
- export type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo" };
620
+ export type ToolCallState = ToolCall & {
621
+ [kStreamingBlockIndex]: number;
622
+ [kStreamingPartialJson]?: string;
623
+ [kStreamingBlockKind]: "mcp" | "todo";
624
+ };
615
625
 
616
626
  export interface BlockState {
617
- currentTextBlock: (TextContent & { index: number }) | null;
618
- currentThinkingBlock: (ThinkingContent & { index: number }) | null;
627
+ currentTextBlock: (TextContent & { [kStreamingBlockIndex]: number }) | null;
628
+ currentThinkingBlock: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null;
619
629
  currentToolCall: ToolCallState | null;
620
630
  firstTokenTime: number | undefined;
621
- setTextBlock: (b: (TextContent & { index: number }) | null) => void;
622
- setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void;
631
+ setTextBlock: (b: (TextContent & { [kStreamingBlockIndex]: number }) | null) => void;
632
+ setThinkingBlock: (b: (ThinkingContent & { [kStreamingBlockIndex]: number }) | null) => void;
623
633
  setToolCall: (t: ToolCallState | null) => void;
624
634
  setFirstTokenTime: () => void;
625
635
  }
@@ -757,7 +767,7 @@ async function handleShellStreamArgs(
757
767
  ): Promise<void> {
758
768
  const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
759
769
  const normalizedArgs: ShellArgs = { ...args, workingDirectory: normalizedWorkingDirectory };
760
- const startTs = Date.now();
770
+ const startTs = performance.now();
761
771
  log("shellStream", "start", {
762
772
  command: (args as any).command,
763
773
  workingDirectory: normalizedWorkingDirectory,
@@ -895,7 +905,7 @@ async function handleShellStreamArgs(
895
905
  sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
896
906
  sendExecClientStreamClose(h2Request, execMsg);
897
907
 
898
- log("shellStream", "done", { elapsed: Date.now() - startTs });
908
+ log("shellStream", "done", { elapsed: performance.now() - startTs });
899
909
  }
900
910
 
901
911
  function sendShellStreamExitFromResult(
@@ -1972,7 +1982,6 @@ function endCurrentTextBlock(output: AssistantMessage, stream: AssistantMessageE
1972
1982
  const block = state.currentTextBlock;
1973
1983
  if (!block) return;
1974
1984
  const idx = output.content.indexOf(block);
1975
- delete (block as { index?: number }).index;
1976
1985
  stream.push({
1977
1986
  type: "text_end",
1978
1987
  contentIndex: idx,
@@ -1990,7 +1999,6 @@ function endCurrentThinkingBlock(
1990
1999
  const block = state.currentThinkingBlock;
1991
2000
  if (!block) return;
1992
2001
  const idx = output.content.indexOf(block);
1993
- delete (block as { index?: number }).index;
1994
2002
  stream.push({
1995
2003
  type: "thinking_end",
1996
2004
  contentIndex: idx,
@@ -2016,10 +2024,10 @@ export function processInteractionUpdate(
2016
2024
  state.setFirstTokenTime();
2017
2025
  const delta = update.message.value.text || "";
2018
2026
  if (!state.currentTextBlock) {
2019
- const block: TextContent & { index: number } = {
2027
+ const block: TextContent & { [kStreamingBlockIndex]: number } = {
2020
2028
  type: "text",
2021
2029
  text: "",
2022
- index: output.content.length,
2030
+ [kStreamingBlockIndex]: output.content.length,
2023
2031
  };
2024
2032
  output.content.push(block);
2025
2033
  state.setTextBlock(block);
@@ -2032,10 +2040,10 @@ export function processInteractionUpdate(
2032
2040
  state.setFirstTokenTime();
2033
2041
  const delta = update.message.value.text || "";
2034
2042
  if (!state.currentThinkingBlock) {
2035
- const block: ThinkingContent & { index: number } = {
2043
+ const block: ThinkingContent & { [kStreamingBlockIndex]: number } = {
2036
2044
  type: "thinking",
2037
2045
  thinking: "",
2038
- index: output.content.length,
2046
+ [kStreamingBlockIndex]: output.content.length,
2039
2047
  };
2040
2048
  output.content.push(block);
2041
2049
  state.setThinkingBlock(block);
@@ -2059,9 +2067,9 @@ export function processInteractionUpdate(
2059
2067
  id: args.toolCallId || crypto.randomUUID(),
2060
2068
  name: args.name || args.toolName || "",
2061
2069
  arguments: {},
2062
- index: output.content.length,
2063
- partialJson: "",
2064
- kind: "mcp",
2070
+ [kStreamingBlockIndex]: output.content.length,
2071
+ [kStreamingPartialJson]: "",
2072
+ [kStreamingBlockKind]: "mcp",
2065
2073
  };
2066
2074
  output.content.push(block);
2067
2075
  state.setToolCall(block);
@@ -2077,8 +2085,8 @@ export function processInteractionUpdate(
2077
2085
  id: callId,
2078
2086
  name: "todo",
2079
2087
  arguments: todoArgs,
2080
- index: output.content.length,
2081
- kind: "todo",
2088
+ [kStreamingBlockIndex]: output.content.length,
2089
+ [kStreamingBlockKind]: "todo",
2082
2090
  };
2083
2091
  output.content.push(block);
2084
2092
  state.setToolCall(block);
@@ -2086,41 +2094,39 @@ export function processInteractionUpdate(
2086
2094
  }
2087
2095
  }
2088
2096
  } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
2089
- if (state.currentToolCall?.kind === "mcp") {
2097
+ if (state.currentToolCall?.[kStreamingBlockKind] === "mcp") {
2090
2098
  // Cursor's `args_text_delta` is "aggregated args text so far" per agent.proto: each
2091
2099
  // delta is a cumulative snapshot of the JSON-text args. Strip the prefix we already
2092
2100
  // have to recover the new suffix; fall back to treating the value as an incremental
2093
2101
  // fragment when it doesn't extend the buffer.
2094
2102
  const snapshot: string = update.message.value.argsTextDelta || "";
2095
- const current = state.currentToolCall.partialJson ?? "";
2103
+ const current = state.currentToolCall[kStreamingPartialJson] ?? "";
2096
2104
  const chunk = snapshot.startsWith(current) ? snapshot.slice(current.length) : snapshot;
2097
2105
  if (chunk.length === 0) {
2098
2106
  return;
2099
2107
  }
2100
- state.currentToolCall.partialJson = current + chunk;
2101
- state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
2108
+ state.currentToolCall[kStreamingPartialJson] = current + chunk;
2109
+ state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall[kStreamingPartialJson]);
2102
2110
  const idx = output.content.indexOf(state.currentToolCall);
2103
2111
  stream.push({ type: "toolcall_delta", contentIndex: idx, delta: chunk, partial: output });
2104
2112
  }
2105
2113
  } else if (updateCase === "toolCallCompleted") {
2106
2114
  if (state.currentToolCall) {
2107
2115
  const toolCall = update.message.value.toolCall;
2108
- if (state.currentToolCall.kind === "mcp") {
2116
+ if (state.currentToolCall[kStreamingBlockKind] === "mcp") {
2109
2117
  const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
2110
2118
  state.currentToolCall.arguments = mergeCursorMcpToolCallArgs(
2111
2119
  state.currentToolCall.arguments as Record<string, unknown> | undefined,
2112
2120
  decodedArgs,
2113
2121
  );
2114
- } else if (state.currentToolCall.kind === "todo" && toolCall) {
2122
+ } else if (state.currentToolCall[kStreamingBlockKind] === "todo" && toolCall) {
2115
2123
  const todoArgs = buildTodoArgs(toolCall);
2116
2124
  if (todoArgs) {
2117
2125
  state.currentToolCall.arguments = todoArgs;
2118
2126
  }
2119
2127
  }
2120
2128
  const idx = output.content.indexOf(state.currentToolCall);
2121
- delete (state.currentToolCall as any).partialJson;
2122
- delete (state.currentToolCall as any).index;
2123
- delete (state.currentToolCall as any).kind;
2129
+ clearStreamingPartialJson(state.currentToolCall);
2124
2130
  stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
2125
2131
  state.setToolCall(null);
2126
2132
  }
@@ -2167,7 +2173,7 @@ function storeCursorBlob(blobStore: Map<string, Uint8Array>, data: Uint8Array):
2167
2173
  function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array): Uint8Array {
2168
2174
  const data = blobStore.get(Buffer.from(blobId).toString("hex"));
2169
2175
  if (!data) {
2170
- throw new Error("Cursor blob not found");
2176
+ throw new AIError.ValidationError("Cursor blob not found");
2171
2177
  }
2172
2178
  return data;
2173
2179
  }
@@ -28,7 +28,8 @@ import {
28
28
  StopReason,
29
29
  } from "@oh-my-pi/pi-catalog/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb";
30
30
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
31
- import { extractHttpStatusFromError, logger, parseStreamingJson } from "@oh-my-pi/pi-utils";
31
+ import { logger, parseStreamingJson } from "@oh-my-pi/pi-utils";
32
+ import * as AIError from "../error";
32
33
  import type {
33
34
  Api,
34
35
  AssistantMessage,
@@ -44,7 +45,6 @@ import type {
44
45
  } from "../types";
45
46
  import { deterministicUuid } from "../utils/deterministic-id";
46
47
  import { AssistantMessageEventStream } from "../utils/event-stream";
47
- import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
48
48
  import { toolWireSchema } from "../utils/schema/wire";
49
49
 
50
50
  /** Base host for Codeium/Windsurf's Cascade chat API (Connect protocol over HTTP/1.1). */
@@ -78,7 +78,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
78
78
  const stream = new AssistantMessageEventStream();
79
79
 
80
80
  (async () => {
81
- const startTime = Date.now();
81
+ const startTime = performance.now();
82
82
  let firstTokenTime: number | undefined;
83
83
 
84
84
  const output: AssistantMessage = {
@@ -109,7 +109,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
109
109
  let latestStopReason = StopReason.UNSPECIFIED;
110
110
 
111
111
  const markFirstToken = () => {
112
- if (firstTokenTime === undefined) firstTokenTime = Date.now();
112
+ if (firstTokenTime === undefined) firstTokenTime = performance.now();
113
113
  };
114
114
 
115
115
  const endTextBlock = () => {
@@ -169,12 +169,16 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
169
169
 
170
170
  if (!response.ok) {
171
171
  const text = await response.text();
172
- throw Object.assign(new Error(`Devin API error ${response.status} ${response.statusText}: ${text}`), {
173
- status: response.status,
174
- });
172
+ throw new AIError.DevinApiError(
173
+ `Devin API error ${response.status} ${response.statusText}: ${text}`,
174
+ response.status,
175
+ );
175
176
  }
176
177
  if (!response.body) {
177
- throw new Error("Devin API error: response body is empty");
178
+ throw new AIError.ProviderResponseError("Devin API error: response body is empty", {
179
+ provider: model.provider,
180
+ kind: "empty-body",
181
+ });
178
182
  }
179
183
  const body = response.body;
180
184
 
@@ -199,7 +203,7 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
199
203
  if (flag & CONNECT_END_STREAM_FLAG) {
200
204
  const trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;
201
205
  const trailerError = readConnectTrailerError(trailerBytes.toString("utf8").trim());
202
- if (trailerError) throw new Error(trailerError);
206
+ if (trailerError) throw new AIError.ValidationError(trailerError);
203
207
  continue;
204
208
  }
205
209
 
@@ -318,20 +322,21 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
318
322
  output.stopReason = doneReason;
319
323
 
320
324
  calculateCost(model, output.usage);
321
- output.duration = Date.now() - startTime;
325
+ output.duration = performance.now() - startTime;
322
326
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
323
327
 
324
328
  stream.push({ type: "done", reason: doneReason, message: output });
325
329
  stream.end();
326
330
  } catch (error) {
327
331
  logger.error("devin: stream failed", { error: String(error) });
328
- const errorReason: "aborted" | "error" = options?.signal?.aborted ? "aborted" : "error";
329
- output.stopReason = errorReason;
330
- output.errorStatus = extractHttpStatusFromError(error);
331
- output.errorMessage = formatErrorMessageWithRetryAfter(error);
332
- output.duration = Date.now() - startTime;
332
+ const result = await AIError.finalize(error, { api: model.api, signal: options?.signal });
333
+ output.stopReason = result.stopReason;
334
+ output.errorStatus = result.status;
335
+ output.errorId = result.id;
336
+ output.errorMessage = result.message;
337
+ output.duration = performance.now() - startTime;
333
338
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
334
- stream.push({ type: "error", reason: errorReason, error: output });
339
+ stream.push({ type: "error", reason: result.stopReason, error: output });
335
340
  stream.end();
336
341
  }
337
342
  })();
@@ -372,14 +377,17 @@ async function fetchDevinAuthMetadata(
372
377
  });
373
378
  const payload = new Uint8Array(await response.arrayBuffer());
374
379
  if (!response.ok) {
375
- throw Object.assign(
376
- new Error(`Devin auth error ${response.status} ${response.statusText}: ${new TextDecoder().decode(payload)}`),
377
- { status: response.status },
380
+ throw new AIError.DevinApiError(
381
+ `Devin auth error ${response.status} ${response.statusText}: ${new TextDecoder().decode(payload)}`,
382
+ response.status,
378
383
  );
379
384
  }
380
385
  const decoded = decodeDevinUserJwtResponse(payload);
381
386
  if (!decoded.userJwt) {
382
- throw new Error("Devin auth error: GetUserJwt returned an empty user JWT");
387
+ throw new AIError.ProviderResponseError("Devin auth error: GetUserJwt returned an empty user JWT", {
388
+ provider: "devin",
389
+ kind: "runtime",
390
+ });
383
391
  }
384
392
  const customBaseUrl = decoded.customApiServerUrl.trim();
385
393
  return { userJwt: decoded.userJwt, ...(customBaseUrl ? { baseUrl: customBaseUrl.replace(/\/+$/, "") } : undefined) };