@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
@@ -4,6 +4,7 @@ import {
4
4
  discoverGitLabDuoWorkflowRuntimeNamespace,
5
5
  type GitLabDuoWorkflowNamespaceSelection,
6
6
  } from "@oh-my-pi/pi-catalog/discovery/gitlab-duo-workflow";
7
+ import * as AIError from "../error";
7
8
  import type {
8
9
  Api,
9
10
  AssistantMessage,
@@ -109,7 +110,7 @@ const GITLAB_DUO_WORKFLOW_GOAL_SOFT_OVERFLOW_BYTES = 1_048_576;
109
110
  const GITLAB_DUO_WORKFLOW_GOAL_HARD_OVERFLOW_BYTES = 2_000_000;
110
111
 
111
112
  // An overflow-pattern message for an oversized goal. The "prompt is too long" prefix
112
- // is one of the shared `OVERFLOW_PATTERNS` (packages/ai/src/utils/overflow.ts), so
113
+ // is one of the shared overflow classifier patterns, so
113
114
  // `isContextOverflow` recognizes it and the session triggers auto-compaction instead
114
115
  // of surfacing a hard failure. Byte counts (not tokens) are reported because the
115
116
  // budget is a byte budget.
@@ -953,7 +954,7 @@ async function runGitLabDuoWorkflow(
953
954
  state: GitLabDuoWorkflowStreamState,
954
955
  ): Promise<void> {
955
956
  const apiKey = options.apiKey;
956
- if (!apiKey) throw new Error("No API key for provider: gitlab-duo-agent");
957
+ if (!apiKey) throw new AIError.MissingApiKeyError("gitlab-duo-agent");
957
958
  const baseUrl = normalizeGitLabBaseUrl(model.baseUrl || DEFAULT_GITLAB_BASE_URL);
958
959
  const fetchImpl = options.fetch ?? fetch;
959
960
  const providerSessionState = getGitLabDuoWorkflowProviderSessionState(
@@ -1604,16 +1605,20 @@ async function requestGitLabDuoWorkflowDirectAccess(
1604
1605
  // when the assistant error exposes `errorStatus` or the message embeds an
1605
1606
  // `HTTP <status>` token. A 401 `{"message":"Unauthorized"}` or a 429 quota
1606
1607
  // body would otherwise surface as a hard failure with no recoverable status.
1607
- throw new Error(
1608
+ throw new AIError.GitLabDuoWorkflowApiError(
1608
1609
  message
1609
1610
  ? `GitLab Duo Workflow direct_access failed with HTTP ${response.status}: ${message}`
1610
1611
  : `GitLab Duo Workflow direct_access failed with HTTP ${response.status}`,
1612
+ response.status,
1611
1613
  );
1612
1614
  }
1613
1615
  const payload = (await response.json()) as GitLabDirectAccessResponse;
1614
1616
  const token = extractGitLabWorkflowToken(payload);
1615
1617
  if (!token) {
1616
- throw new Error("GitLab Duo Workflow direct_access did not return credentials");
1618
+ throw new AIError.ProviderResponseError("GitLab Duo Workflow direct_access did not return credentials", {
1619
+ provider: "gitlab-duo-agent",
1620
+ kind: "empty-body",
1621
+ });
1617
1622
  }
1618
1623
  traceGitLabDuoWorkflow("direct_access.token", { hasToken: true });
1619
1624
  const serviceEndpoint = !payload.gitlab_rails?.token && Boolean(payload.duo_workflow_service?.base_url);
@@ -1658,12 +1663,18 @@ async function createGitLabDuoWorkflow(
1658
1663
  hasProjectId: Boolean(projectId),
1659
1664
  });
1660
1665
  if (!response.ok) {
1661
- throw new Error(`GitLab Duo Workflow create failed with HTTP ${response.status}`);
1666
+ throw new AIError.GitLabDuoWorkflowApiError(
1667
+ `GitLab Duo Workflow create failed with HTTP ${response.status}`,
1668
+ response.status,
1669
+ );
1662
1670
  }
1663
1671
  const payload = (await response.json()) as GitLabCreateWorkflowResponse;
1664
1672
  const workflowId = payload.id ?? payload.workflow_id ?? payload.workflowId;
1665
1673
  if (workflowId === undefined) {
1666
- throw new Error(`GitLab Duo Workflow create response missing workflow id (HTTP ${response.status})`);
1674
+ throw new AIError.ProviderResponseError(
1675
+ `GitLab Duo Workflow create response missing workflow id (HTTP ${response.status})`,
1676
+ { provider: "gitlab-duo-agent", kind: "empty-body" },
1677
+ );
1667
1678
  }
1668
1679
  traceGitLabDuoWorkflow("workflow.create.id", { workflowId });
1669
1680
  return String(workflowId);
@@ -1815,7 +1826,7 @@ export function runGitLabDuoWorkflowSocket(
1815
1826
  };
1816
1827
  const abort = (): void => {
1817
1828
  close();
1818
- settle("closed", new Error("GitLab Duo Workflow request aborted"));
1829
+ settle("closed", new AIError.AbortError("GitLab Duo Workflow request aborted"));
1819
1830
  };
1820
1831
  if (options.signal?.aborted) {
1821
1832
  abort();
@@ -1856,7 +1867,13 @@ export function runGitLabDuoWorkflowSocket(
1856
1867
  ws.onerror = event => {
1857
1868
  const detail = describeGitLabDuoWorkflowSocketEvent(event);
1858
1869
  traceGitLabDuoWorkflow("websocket.error", { event: detail });
1859
- settle("closed", new Error(`GitLab Duo Workflow WebSocket error: ${detail}`));
1870
+ settle(
1871
+ "closed",
1872
+ new AIError.ProviderResponseError(`GitLab Duo Workflow WebSocket error: ${detail}`, {
1873
+ provider: "gitlab-duo-agent",
1874
+ kind: "runtime",
1875
+ }),
1876
+ );
1860
1877
  };
1861
1878
  ws.onclose = event => {
1862
1879
  traceGitLabDuoWorkflow("websocket.close", { code: event.code, reason: event.reason });
@@ -2775,7 +2792,10 @@ export async function resolveGitLabDuoWorkflowNamespaceSelection(
2775
2792
  cwd: options.cwd,
2776
2793
  });
2777
2794
  } catch (error) {
2778
- throw new Error(`GitLab Duo Workflow runtime namespace resolution failed: ${gitLabDuoWorkflowErrorText(error)}`);
2795
+ throw new AIError.ProviderResponseError(
2796
+ `GitLab Duo Workflow runtime namespace resolution failed: ${gitLabDuoWorkflowErrorText(error)}`,
2797
+ { provider: "gitlab-duo-agent", kind: "runtime" },
2798
+ );
2779
2799
  }
2780
2800
  }
2781
2801
 
@@ -3008,7 +3028,7 @@ function requireGitLabDuoWorkflowRequestID(
3008
3028
  source: Record<string, unknown>,
3009
3029
  ): string {
3010
3030
  if (requestID) return requestID;
3011
- throw new Error(
3031
+ throw new AIError.ValidationError(
3012
3032
  `GitLab Duo Workflow action "${actionName}" missing requestID (keys: ${Object.keys(source).slice(0, 20).join(", ")})`,
3013
3033
  );
3014
3034
  }
@@ -1,4 +1,5 @@
1
1
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
2
+ import * as AIError from "../error";
2
3
  import { ANTHROPIC_THINKING, mapAnthropicToolChoice } from "../stream";
3
4
  import type { Api, Context, FetchImpl, Model, ModelSpec, SimpleStreamOptions } from "../types";
4
5
  import { AssistantMessageEventStream } from "../utils/event-stream";
@@ -198,17 +199,29 @@ async function getDirectAccessToken(
198
199
  if (!response.ok) {
199
200
  const detail = await response.text();
200
201
  if (response.status === 403) {
201
- throw new Error(`GitLab Duo access denied. Ensure Duo is enabled for this account. ${detail}`);
202
+ throw new AIError.ProviderResponseError(
203
+ `GitLab Duo access denied. Ensure Duo is enabled for this account. ${detail}`,
204
+ { provider: "gitlab-duo", kind: "runtime" },
205
+ );
202
206
  }
203
- throw new Error(`Failed to get GitLab Duo direct access token: ${response.status} ${detail}`);
207
+ throw new AIError.GitLabDuoApiError(
208
+ `Failed to get GitLab Duo direct access token: ${response.status} ${detail}`,
209
+ response.status,
210
+ );
204
211
  }
205
212
 
206
213
  const payload = (await response.json()) as { token?: string; headers?: Record<string, string> };
207
214
  if (!payload.token || typeof payload.token !== "string") {
208
- throw new Error("GitLab Duo direct access response missing token");
215
+ throw new AIError.ProviderResponseError("GitLab Duo direct access response missing token", {
216
+ provider: "gitlab-duo",
217
+ kind: "envelope",
218
+ });
209
219
  }
210
220
  if (!payload.headers || typeof payload.headers !== "object") {
211
- throw new Error("GitLab Duo direct access response missing headers");
221
+ throw new AIError.ProviderResponseError("GitLab Duo direct access response missing headers", {
222
+ provider: "gitlab-duo",
223
+ kind: "envelope",
224
+ });
212
225
  }
213
226
 
214
227
  const token: DirectAccessToken = {
@@ -239,12 +252,15 @@ export function streamGitLabDuo(
239
252
  try {
240
253
  const apiKey = typeof options?.apiKey === "string" ? options.apiKey : undefined;
241
254
  if (!apiKey || !options) {
242
- throw new Error("Missing GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN.");
255
+ throw new AIError.MissingApiKeyError(
256
+ undefined,
257
+ "Missing GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN.",
258
+ );
243
259
  }
244
260
 
245
261
  const mapping = getModelMapping(model.id);
246
262
  if (!mapping) {
247
- throw new Error(`Unsupported GitLab Duo model: ${model.id}`);
263
+ throw new AIError.ConfigurationError(`Unsupported GitLab Duo model: ${model.id}`);
248
264
  }
249
265
 
250
266
  const directAccess = await getDirectAccessToken(apiKey, options.fetch);
@@ -16,6 +16,7 @@ import { Buffer } from "node:buffer";
16
16
  import * as os from "node:os";
17
17
  import * as path from "node:path";
18
18
  import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
19
+ import * as AIError from "../error";
19
20
  import type { FetchImpl } from "../types";
20
21
  import { raceWithSignal } from "../utils/abort";
21
22
 
@@ -83,7 +84,7 @@ async function loadAdcCredentials(): Promise<{ source: string; creds: AdcFileCre
83
84
  if (gacPath) {
84
85
  const creds = await readJsonFile<AdcFileCredentials>(gacPath);
85
86
  if (!creds) {
86
- throw new Error(`GOOGLE_APPLICATION_CREDENTIALS points to a missing file: ${gacPath}`);
87
+ throw new AIError.ConfigurationError(`GOOGLE_APPLICATION_CREDENTIALS points to a missing file: ${gacPath}`);
87
88
  }
88
89
  return { source: `gac:${gacPath}`, creds };
89
90
  }
@@ -103,7 +104,7 @@ function pemToPkcs8(pem: string): Uint8Array<ArrayBuffer> {
103
104
  .replace(/-----BEGIN [^-]+-----/g, "")
104
105
  .replace(/-----END [^-]+-----/g, "")
105
106
  .replace(/\s+/g, "");
106
- if (!body) throw new Error("Invalid PEM: empty body");
107
+ if (!body) throw new AIError.ConfigurationError("Invalid PEM: empty body");
107
108
  return Uint8Array.fromBase64(body);
108
109
  }
109
110
 
@@ -193,7 +194,11 @@ async function postForToken(
193
194
  });
194
195
  if (!response.ok) {
195
196
  const detail = await response.text().catch(() => "");
196
- throw new Error(`Google OAuth token exchange failed (${response.status}): ${detail}`);
197
+ throw new AIError.OAuthError(`Google OAuth token exchange failed (${response.status}): ${detail}`, {
198
+ kind: "token-exchange",
199
+ provider: "google-vertex",
200
+ status: response.status,
201
+ });
197
202
  }
198
203
  return (await response.json()) as TokenResponse;
199
204
  }
@@ -239,7 +244,11 @@ async function resolveAccessTokenUncached(
239
244
  );
240
245
  if (!response.ok) {
241
246
  const detail = await response.text().catch(() => "");
242
- throw new Error(`Google Impersonation token exchange failed (${response.status}): ${detail}`);
247
+ throw new AIError.OAuthError(`Google Impersonation token exchange failed (${response.status}): ${detail}`, {
248
+ kind: "token-exchange",
249
+ provider: "google-vertex",
250
+ status: response.status,
251
+ });
243
252
  }
244
253
  const data = (await response.json()) as { accessToken: string; expireTime: string };
245
254
  const expiresIn = Math.max(0, Math.floor((new Date(data.expireTime).getTime() - Date.now()) / 1000));
@@ -254,7 +263,8 @@ async function resolveAccessTokenUncached(
254
263
  }
255
264
  const metadata = await fetchMetadataToken(signal, fetchImpl);
256
265
  if (metadata) return { source: "metadata", token: metadata };
257
- throw new Error(
266
+ throw new AIError.MissingApiKeyError(
267
+ undefined,
258
268
  "Vertex AI requires Application Default Credentials. Set GOOGLE_APPLICATION_CREDENTIALS, run `gcloud auth application-default login`, or run on a GCE/Cloud Run instance with a service account.",
259
269
  );
260
270
  }
@@ -14,7 +14,7 @@ import {
14
14
  } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
15
15
  import { extractHttpStatusFromError, fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
16
16
  import { type } from "arktype";
17
- import { ProviderHttpError } from "../errors";
17
+ import * as AIError from "../error";
18
18
  import type {
19
19
  Api,
20
20
  AssistantMessage,
@@ -30,7 +30,7 @@ import type {
30
30
  import { normalizeSystemPrompts } from "../utils";
31
31
  import { AssistantMessageEventStream } from "../utils/event-stream";
32
32
  import { extractGoogleValidationUrl, formatGoogleValidationRequiredMessage } from "../utils/google-validation";
33
- import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump } from "../utils/http-inspector";
33
+ import type { RawHttpRequestDump } from "../utils/http-inspector";
34
34
  import { armPreResponseTimeout, getStreamFirstEventTimeoutMs } from "../utils/idle-iterator";
35
35
  // Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
36
36
  // the stream provider trusts the access token threaded through `options.apiKey`.
@@ -59,11 +59,6 @@ import {
59
59
  */
60
60
  export type { GoogleThinkingLevel };
61
61
 
62
- /** Non-2xx response (or in-stream error chunk) from the Cloud Code Assist API. */
63
- export class GeminiCliApiError extends ProviderHttpError {
64
- override readonly name = "GeminiCliApiError";
65
- }
66
-
67
62
  function isPlanningLeakPrefix(text: string): boolean {
68
63
  const trimmed = text.trimStart();
69
64
  if (!trimmed.startsWith("{")) {
@@ -345,22 +340,6 @@ function shouldInjectAntigravitySystemInstruction(modelId: string): boolean {
345
340
  return normalized.includes("claude") || normalized.includes("gemini-3");
346
341
  }
347
342
 
348
- /**
349
- * Extract a clean, user-friendly error message from Google API error response.
350
- * Parses JSON error responses and returns just the message field.
351
- */
352
- function extractErrorMessage(errorText: string): string {
353
- try {
354
- const parsed = JSON.parse(errorText) as { error?: { message?: string } };
355
- if (parsed.error?.message) {
356
- return parsed.error.message;
357
- }
358
- } catch {
359
- // Not JSON, return as-is
360
- }
361
- return errorText;
362
- }
363
-
364
343
  const optionalCredentialString = type("unknown").pipe(raw => {
365
344
  const out = type("string")(raw);
366
345
  return out instanceof type.errors ? undefined : out;
@@ -406,16 +385,16 @@ export function parseGeminiCliCredentials(apiKeyRaw: string): ParsedGeminiCliCre
406
385
  try {
407
386
  rawCredentials = JSON.parse(apiKeyRaw);
408
387
  } catch {
409
- throw new Error(invalidCredentialsMessage);
388
+ throw new AIError.ValidationError(invalidCredentialsMessage);
410
389
  }
411
390
  const parsed = geminiCliCredentialsSchema(rawCredentials);
412
391
  if (parsed instanceof type.errors) {
413
- throw new Error(invalidCredentialsMessage);
392
+ throw new AIError.ValidationError(invalidCredentialsMessage);
414
393
  }
415
394
 
416
395
  const projectId = parsed.projectId ?? parsed.project_id;
417
396
  if (parsed.token === undefined || projectId === undefined) {
418
- throw new Error(missingCredentialsMessage);
397
+ throw new AIError.ValidationError(missingCredentialsMessage);
419
398
  }
420
399
 
421
400
  const refreshToken = parsed.refreshToken ?? parsed.refresh;
@@ -517,7 +496,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
517
496
  const stream = new AssistantMessageEventStream();
518
497
 
519
498
  (async () => {
520
- const startTime = Date.now();
499
+ const startTime = performance.now();
521
500
  let firstTokenTime: number | undefined;
522
501
 
523
502
  const output: AssistantMessage = {
@@ -542,7 +521,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
542
521
  try {
543
522
  const apiKeyRaw = options?.apiKey;
544
523
  if (!apiKeyRaw) {
545
- throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.");
524
+ throw new AIError.ConfigurationError(
525
+ "Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.",
526
+ );
546
527
  }
547
528
 
548
529
  const isAntigravity = model.provider === "google-antigravity";
@@ -558,8 +539,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
558
539
  parsedCredentials.expiresAt !== undefined &&
559
540
  Date.now() >= parsedCredentials.expiresAt
560
541
  ) {
561
- throw new Error(
542
+ throw new AIError.OAuthError(
562
543
  "OAuth token expired before request — please retry; AuthStorage will refresh on the next attempt.",
544
+ { kind: "token-refresh", provider: model.provider },
563
545
  );
564
546
  }
565
547
  const baseUrl = model.baseUrl?.trim();
@@ -646,7 +628,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
646
628
  let lastResponseId: string | undefined;
647
629
  const ensureStarted = () => {
648
630
  if (!started) {
649
- if (!firstTokenTime) firstTokenTime = Date.now();
631
+ if (!firstTokenTime) firstTokenTime = performance.now();
650
632
  stream.push({ type: "start", partial: output });
651
633
  started = true;
652
634
  }
@@ -670,7 +652,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
670
652
 
671
653
  const streamResponse = async (activeResponse: Response): Promise<boolean> => {
672
654
  if (!activeResponse.body) {
673
- throw new Error("No response body");
655
+ throw new AIError.ProviderResponseError("No response body", {
656
+ provider: model.provider,
657
+ kind: "empty-body",
658
+ });
674
659
  }
675
660
 
676
661
  // Scoped per attempt so a failed/empty retry cannot leak its
@@ -706,16 +691,17 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
706
691
  const detail = chunk.error.message || chunk.error.status || "unknown error";
707
692
  const message = `Cloud Code Assist stream error: ${detail}`;
708
693
  throw typeof chunk.error.code === "number" && chunk.error.code >= 400
709
- ? new GeminiCliApiError(message, chunk.error.code)
710
- : new Error(message);
694
+ ? new AIError.GeminiCliApiError(message, chunk.error.code)
695
+ : new AIError.ProviderResponseError(message, { provider: model.provider, kind: "runtime" });
711
696
  }
712
697
  const responseData = chunk.response;
713
698
  if (!responseData) continue;
714
699
  if (responseData.responseId) lastResponseId = responseData.responseId;
715
700
  if (!responseData.candidates?.length && responseData.promptFeedback?.blockReason) {
716
701
  const detail = responseData.promptFeedback.blockReasonMessage;
717
- throw new Error(
702
+ throw new AIError.ProviderResponseError(
718
703
  `Request blocked by Google (${responseData.promptFeedback.blockReason})${detail ? `: ${detail}` : ""}`,
704
+ { provider: model.provider, kind: "content-blocked" },
719
705
  );
720
706
  }
721
707
 
@@ -908,7 +894,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
908
894
  }
909
895
 
910
896
  if (!response.ok) {
911
- if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
897
+ if (AIError.isTransientStatus(response.status)) {
912
898
  if (!isLastEndpoint) {
913
899
  continue;
914
900
  }
@@ -921,8 +907,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
921
907
  "retry your request",
922
908
  parsedCredentials.email,
923
909
  )
924
- : extractErrorMessage(errorText);
925
- throw new GeminiCliApiError(
910
+ : errorText;
911
+ throw new AIError.GeminiCliApiError(
926
912
  `Cloud Code Assist API error (${response.status}): ${errorMessage}`,
927
913
  response.status,
928
914
  { headers: response.headers },
@@ -934,7 +920,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
934
920
 
935
921
  for (let emptyAttempt = 0; emptyAttempt <= MAX_EMPTY_STREAM_RETRIES; emptyAttempt++) {
936
922
  if (options?.signal?.aborted) {
937
- throw new Error("Request was aborted");
923
+ throw new AIError.AbortError("Request was aborted");
938
924
  }
939
925
 
940
926
  if (emptyAttempt > 0) {
@@ -942,11 +928,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
942
928
  try {
943
929
  await scheduler.wait(backoffMs, { signal: options?.signal });
944
930
  } catch {
945
- throw new Error("Request was aborted");
931
+ throw new AIError.AbortError("Request was aborted");
946
932
  }
947
933
 
948
934
  if (!requestUrl) {
949
- throw new Error("Missing request URL");
935
+ throw new AIError.ConfigurationError("Missing request URL");
950
936
  }
951
937
 
952
938
  currentResponse = await (options?.fetch ?? fetch)(requestUrl, {
@@ -958,7 +944,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
958
944
 
959
945
  if (!currentResponse.ok) {
960
946
  const retryErrorText = await currentResponse.text();
961
- throw new GeminiCliApiError(
947
+ throw new AIError.GeminiCliApiError(
962
948
  `Cloud Code Assist API error (${currentResponse.status}): ${retryErrorText}`,
963
949
  currentResponse.status,
964
950
  { headers: currentResponse.headers },
@@ -978,16 +964,20 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
978
964
  }
979
965
 
980
966
  if (!receivedContent) {
981
- throw new Error("Cloud Code Assist API returned an empty response");
967
+ throw new AIError.ProviderResponseError("Cloud Code Assist API returned an empty response", {
968
+ provider: model.provider,
969
+ kind: "empty-body",
970
+ });
982
971
  }
983
972
 
984
973
  if (options?.signal?.aborted) {
985
- throw new Error("Request was aborted");
974
+ throw new AIError.AbortError("Request was aborted");
986
975
  }
987
976
 
988
977
  if (!sawFinishReason) {
989
- throw new Error(
978
+ throw new AIError.ProviderResponseError(
990
979
  "Cloud Code Assist stream ended without a finish reason (connection dropped or response truncated)",
980
+ { provider: model.provider, kind: "incomplete-stream" },
991
981
  );
992
982
  }
993
983
 
@@ -1007,7 +997,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1007
997
  break;
1008
998
  } catch (error) {
1009
999
  const status = extractHttpStatusFromError(error);
1010
- if (status === 429 || (status !== undefined && status >= 500 && status < 600)) {
1000
+ if (AIError.isTransientStatus(status)) {
1011
1001
  if (!isLastEndpoint && !started) {
1012
1002
  continue;
1013
1003
  }
@@ -1017,27 +1007,23 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1017
1007
  }
1018
1008
 
1019
1009
  if (output.stopReason === "aborted" || output.stopReason === "error") {
1020
- throw new Error(output.errorMessage ?? "An unknown error occurred");
1010
+ throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
1011
+ provider: model.provider,
1012
+ kind: "output",
1013
+ });
1021
1014
  }
1022
1015
 
1023
- output.duration = Date.now() - startTime;
1016
+ output.duration = performance.now() - startTime;
1024
1017
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1025
1018
  stream.push({ type: "done", reason: output.stopReason, message: output });
1026
1019
  stream.end();
1027
1020
  } catch (error) {
1028
- for (const block of output.content) {
1029
- if ("index" in block) {
1030
- delete (block as { index?: number }).index;
1031
- }
1032
- }
1033
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
1034
- output.errorStatus = extractHttpStatusFromError(error);
1035
- output.errorMessage = await appendRawHttpRequestDumpFor400(
1036
- error instanceof Error ? error.message : JSON.stringify(error),
1037
- error,
1038
- rawRequestDump,
1039
- );
1040
- output.duration = Date.now() - startTime;
1021
+ const result = await AIError.finalize(error, { api: model.api, signal: options?.signal, rawRequestDump });
1022
+ output.stopReason = result.stopReason;
1023
+ output.errorStatus = result.status;
1024
+ output.errorId = result.id;
1025
+ output.errorMessage = result.message;
1026
+ output.duration = performance.now() - startTime;
1041
1027
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
1042
1028
  stream.push({ type: "error", reason: output.stopReason, error: output });
1043
1029
  stream.end();