@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
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { OPENAI_HEADER_VALUES } from "@oh-my-pi/pi-catalog/wire/codex";
6
+ import * as AIError from "../../error";
6
7
  import type { FetchImpl } from "../../types";
7
8
  import { isRecord } from "../../utils";
8
9
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
@@ -175,7 +176,10 @@ async function exchangeCodeForToken(
175
176
 
176
177
  if (!tokenResponse.ok) {
177
178
  const bodyText = await tokenResponse.text();
178
- throw new Error(`Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}`);
179
+ throw new AIError.OAuthError(
180
+ `Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}`,
181
+ { kind: "token-exchange", status: tokenResponse.status },
182
+ );
179
183
  }
180
184
 
181
185
  const tokenData = (await tokenResponse.json()) as {
@@ -185,12 +189,12 @@ async function exchangeCodeForToken(
185
189
  };
186
190
 
187
191
  if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
188
- throw new Error("Token response missing required fields");
192
+ throw new AIError.OAuthError("Token response missing required fields", { kind: "validation" });
189
193
  }
190
194
 
191
195
  const { accountId, email } = getTokenProfile(tokenData.access_token);
192
196
  if (!accountId) {
193
- throw new Error("Failed to extract accountId from token");
197
+ throw new AIError.OAuthError("Failed to extract accountId from token", { kind: "validation" });
194
198
  }
195
199
 
196
200
  return {
@@ -235,7 +239,10 @@ export async function loginOpenAICodexDevice(ctrl: OAuthController): Promise<OAu
235
239
  });
236
240
 
237
241
  if (!initResponse.ok) {
238
- throw new Error(`Device authorization initiation failed: ${initResponse.status}`);
242
+ throw new AIError.OAuthError(`Device authorization initiation failed: ${initResponse.status}`, {
243
+ kind: "device-auth",
244
+ status: initResponse.status,
245
+ });
239
246
  }
240
247
 
241
248
  const initData = (await initResponse.json()) as {
@@ -245,7 +252,7 @@ export async function loginOpenAICodexDevice(ctrl: OAuthController): Promise<OAu
245
252
  };
246
253
 
247
254
  if (!initData.device_auth_id || !initData.user_code) {
248
- throw new Error("Device authorization response missing required fields");
255
+ throw new AIError.OAuthError("Device authorization response missing required fields", { kind: "validation" });
249
256
  }
250
257
 
251
258
  const userCode = initData.user_code;
@@ -267,7 +274,7 @@ export async function loginOpenAICodexDevice(ctrl: OAuthController): Promise<OAu
267
274
  await Bun.sleep(poll === 0 ? Math.min(pollIntervalMs, DEVICE_POLL_INTERVAL_MS) : pollIntervalMs);
268
275
 
269
276
  if (ctrl.signal?.aborted) {
270
- throw new Error("Device authorization cancelled");
277
+ throw new AIError.LoginCancelledError("Device authorization cancelled");
271
278
  }
272
279
 
273
280
  const pollResponse = await fetch(DEVICE_TOKEN_URL, {
@@ -286,7 +293,10 @@ export async function loginOpenAICodexDevice(ctrl: OAuthController): Promise<OAu
286
293
  }
287
294
 
288
295
  if (!pollResponse.ok) {
289
- throw new Error(`Device token polling failed: ${pollResponse.status}`);
296
+ throw new AIError.OAuthError(`Device token polling failed: ${pollResponse.status}`, {
297
+ kind: "polling",
298
+ status: pollResponse.status,
299
+ });
290
300
  }
291
301
 
292
302
  const pollData = (await pollResponse.json()) as {
@@ -295,14 +305,18 @@ export async function loginOpenAICodexDevice(ctrl: OAuthController): Promise<OAu
295
305
  };
296
306
 
297
307
  if (!pollData.authorization_code || !pollData.code_verifier) {
298
- throw new Error("Device token response missing authorization_code or code_verifier");
308
+ throw new AIError.OAuthError("Device token response missing authorization_code or code_verifier", {
309
+ kind: "validation",
310
+ });
299
311
  }
300
312
 
301
313
  ctrl.onProgress?.("Exchanging authorization code for tokens…");
302
314
  return exchangeCodeForToken(pollData.authorization_code, pollData.code_verifier, DEVICE_REDIRECT_URI);
303
315
  }
304
316
 
305
- throw new Error("Device authorization timed out — user did not complete login in time");
317
+ throw new AIError.OAuthError("Device authorization timed out — user did not complete login in time", {
318
+ kind: "timeout",
319
+ });
306
320
  }
307
321
 
308
322
  /**
@@ -322,8 +336,9 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
322
336
 
323
337
  if (!response.ok) {
324
338
  const bodyText = await response.text();
325
- throw new Error(
339
+ throw new AIError.OAuthError(
326
340
  `OpenAI Codex token refresh failed: ${formatOpenAICodexTokenEndpointError(response.status, bodyText)}`,
341
+ { kind: "token-refresh", status: response.status },
327
342
  );
328
343
  }
329
344
 
@@ -334,7 +349,7 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
334
349
  };
335
350
 
336
351
  if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
337
- throw new Error("Token response missing required fields");
352
+ throw new AIError.OAuthError("Token response missing required fields", { kind: "validation" });
338
353
  }
339
354
 
340
355
  const { accountId, email } = getTokenProfile(tokenData.access_token);
@@ -9,6 +9,7 @@
9
9
  * 3. User pastes the API key back into the CLI
10
10
  */
11
11
 
12
+ import * as AIError from "../../error";
12
13
  import type { OAuthController } from "./types";
13
14
 
14
15
  const AUTH_URL = "https://opencode.ai/auth";
@@ -21,7 +22,7 @@ const AUTH_URL = "https://opencode.ai/auth";
21
22
  */
22
23
  export async function loginOpenCode(options: OAuthController): Promise<string> {
23
24
  if (!options.onPrompt) {
24
- throw new Error("OpenCode Zen login requires onPrompt callback");
25
+ throw new AIError.OnPromptRequiredError("OpenCode Zen");
25
26
  }
26
27
 
27
28
  // Open browser to auth page
@@ -37,12 +38,12 @@ export async function loginOpenCode(options: OAuthController): Promise<string> {
37
38
  });
38
39
 
39
40
  if (options.signal?.aborted) {
40
- throw new Error("Login cancelled");
41
+ throw new AIError.LoginCancelledError();
41
42
  }
42
43
 
43
44
  const trimmed = apiKey.trim();
44
45
  if (!trimmed) {
45
- throw new Error("API key is required");
46
+ throw new AIError.ApiKeyRequiredError();
46
47
  }
47
48
 
48
49
  return trimmed;
@@ -14,6 +14,7 @@
14
14
  import * as os from "node:os";
15
15
  import { $env } from "@oh-my-pi/pi-utils";
16
16
  import { $ } from "bun";
17
+ import * as AIError from "../../error";
17
18
  import type { OAuthController, OAuthCredentials } from "./types";
18
19
 
19
20
  const API_VERSION = "2.18";
@@ -87,15 +88,19 @@ async function extractFromNativeApp(): Promise<string | null> {
87
88
  */
88
89
  async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials> {
89
90
  if (!ctrl.onPrompt) {
90
- throw new Error("Perplexity login requires onPrompt callback");
91
+ throw new AIError.OnPromptRequiredError("Perplexity");
91
92
  }
92
93
  const email = await ctrl.onPrompt({
93
94
  message: "Enter your Perplexity email address",
94
95
  placeholder: "user@example.com",
95
96
  });
96
97
  const trimmedEmail = email.trim();
97
- if (!trimmedEmail) throw new Error("Email is required for Perplexity login");
98
- if (ctrl.signal?.aborted) throw new Error("Login cancelled");
98
+ if (!trimmedEmail)
99
+ throw new AIError.OAuthError("Email is required for Perplexity login", {
100
+ kind: "validation",
101
+ provider: "perplexity",
102
+ });
103
+ if (ctrl.signal?.aborted) throw new AIError.LoginCancelledError();
99
104
 
100
105
  ctrl.onProgress?.("Fetching Perplexity CSRF token...");
101
106
  const csrfResponse = await fetch("https://www.perplexity.ai/api/auth/csrf", {
@@ -107,12 +112,18 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
107
112
  });
108
113
 
109
114
  if (!csrfResponse.ok) {
110
- throw new Error(`Perplexity CSRF request failed: ${csrfResponse.status}`);
115
+ throw new AIError.ProviderHttpError(
116
+ `Perplexity CSRF request failed: ${csrfResponse.status}`,
117
+ csrfResponse.status,
118
+ );
111
119
  }
112
120
 
113
121
  const csrfData = (await csrfResponse.json()) as { csrfToken?: string };
114
122
  if (!csrfData.csrfToken) {
115
- throw new Error("Perplexity CSRF response missing csrfToken");
123
+ throw new AIError.OAuthError("Perplexity CSRF response missing csrfToken", {
124
+ kind: "validation",
125
+ provider: "perplexity",
126
+ });
116
127
  }
117
128
  ctrl.onProgress?.("Sending login code to your email...");
118
129
  const sendResponse = await fetch("https://www.perplexity.ai/api/auth/signin-email", {
@@ -131,15 +142,19 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
131
142
 
132
143
  if (!sendResponse.ok) {
133
144
  const body = await sendResponse.text();
134
- throw new Error(`Perplexity send login code failed (${sendResponse.status}): ${body}`);
145
+ throw new AIError.ProviderHttpError(
146
+ `Perplexity send login code failed (${sendResponse.status}): ${body}`,
147
+ sendResponse.status,
148
+ );
135
149
  }
136
150
  const otp = await ctrl.onPrompt({
137
151
  message: "Enter the code sent to your email",
138
152
  placeholder: "123456",
139
153
  });
140
154
  const trimmedOtp = otp.trim();
141
- if (!trimmedOtp) throw new Error("OTP code is required");
142
- if (ctrl.signal?.aborted) throw new Error("Login cancelled");
155
+ if (!trimmedOtp)
156
+ throw new AIError.OAuthError("OTP code is required", { kind: "validation", provider: "perplexity" });
157
+ if (ctrl.signal?.aborted) throw new AIError.LoginCancelledError();
143
158
  ctrl.onProgress?.("Verifying login code...");
144
159
  const verifyResponse = await fetch("https://www.perplexity.ai/api/auth/signin-otp", {
145
160
  method: "POST",
@@ -165,11 +180,18 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
165
180
 
166
181
  if (!verifyResponse.ok) {
167
182
  const reason = verifyData.text ?? verifyData.error_code ?? verifyData.status ?? "OTP verification failed";
168
- throw new Error(`Perplexity OTP verification failed: ${reason}`);
183
+ throw new AIError.OAuthError(`Perplexity OTP verification failed: ${reason}`, {
184
+ kind: "validation",
185
+ provider: "perplexity",
186
+ status: verifyResponse.status,
187
+ });
169
188
  }
170
189
 
171
190
  if (!verifyData.token) {
172
- throw new Error("Perplexity OTP verification response missing token");
191
+ throw new AIError.OAuthError("Perplexity OTP verification response missing token", {
192
+ kind: "validation",
193
+ provider: "perplexity",
194
+ });
173
195
  }
174
196
 
175
197
  return jwtToCredentials(verifyData.token, trimmedEmail);
@@ -188,7 +210,7 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
188
210
  */
189
211
  export async function loginPerplexity(ctrl: OAuthController): Promise<OAuthCredentials> {
190
212
  if (!ctrl.onPrompt) {
191
- throw new Error("Perplexity login requires onPrompt callback");
213
+ throw new AIError.OnPromptRequiredError("Perplexity");
192
214
  }
193
215
 
194
216
  // Path 1: Native macOS app JWT (skip if PI_AUTH_NO_BORROW=1)
@@ -10,6 +10,7 @@
10
10
  * rejected on every call site, not just the first.
11
11
  */
12
12
 
13
+ import * as AIError from "../../error";
13
14
  import type { FetchImpl } from "../../types";
14
15
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
15
16
  import { generatePKCE } from "./pkce";
@@ -54,14 +55,14 @@ export function validateXAIEndpoint(url: string, field: string): string {
54
55
  try {
55
56
  parsed = new URL(url);
56
57
  } catch {
57
- throw new Error(`Invalid xAI ${field}: ${url}`);
58
+ throw new AIError.OAuthError(`Invalid xAI ${field}: ${url}`, { kind: "validation", provider: "xai" });
58
59
  }
59
60
  if (parsed.protocol !== "https:") {
60
- throw new Error(`Invalid xAI ${field}: ${url}`);
61
+ throw new AIError.OAuthError(`Invalid xAI ${field}: ${url}`, { kind: "validation", provider: "xai" });
61
62
  }
62
63
  const host = parsed.hostname.toLowerCase();
63
64
  if (!host || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
64
- throw new Error(`Invalid xAI ${field}: ${url}`);
65
+ throw new AIError.OAuthError(`Invalid xAI ${field}: ${url}`, { kind: "validation", provider: "xai" });
65
66
  }
66
67
  return url;
67
68
  }
@@ -84,28 +85,46 @@ async function xaiOAuthDiscovery(
84
85
  signal: AbortSignal.timeout(timeoutMs),
85
86
  });
86
87
  } catch (error) {
87
- throw new Error(`xAI OIDC discovery failed: ${error instanceof Error ? error.message : String(error)}`);
88
+ throw new AIError.OAuthError(
89
+ `xAI OIDC discovery failed: ${error instanceof Error ? error.message : String(error)}`,
90
+ {
91
+ kind: "discovery",
92
+ provider: "xai",
93
+ cause: error,
94
+ },
95
+ );
88
96
  }
89
97
  if (response.status !== 200) {
90
- throw new Error(`xAI OIDC discovery returned status ${response.status}.`);
98
+ throw new AIError.OAuthError(`xAI OIDC discovery returned status ${response.status}.`, {
99
+ kind: "discovery",
100
+ provider: "xai",
101
+ status: response.status,
102
+ });
91
103
  }
92
104
  let payload: unknown;
93
105
  try {
94
106
  payload = await response.json();
95
107
  } catch (error) {
96
- throw new Error(
108
+ throw new AIError.OAuthError(
97
109
  `xAI OIDC discovery returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
110
+ { kind: "validation", provider: "xai", cause: error },
98
111
  );
99
112
  }
100
113
  if (!payload || typeof payload !== "object") {
101
- throw new Error("xAI OIDC discovery response was not a JSON object.");
114
+ throw new AIError.OAuthError("xAI OIDC discovery response was not a JSON object.", {
115
+ kind: "validation",
116
+ provider: "xai",
117
+ });
102
118
  }
103
119
  const obj = payload as Record<string, unknown>;
104
120
  const authorizationEndpoint =
105
121
  typeof obj.authorization_endpoint === "string" ? obj.authorization_endpoint.trim() : "";
106
122
  const tokenEndpoint = typeof obj.token_endpoint === "string" ? obj.token_endpoint.trim() : "";
107
123
  if (!authorizationEndpoint || !tokenEndpoint) {
108
- throw new Error("xAI OIDC discovery response was missing required endpoints.");
124
+ throw new AIError.OAuthError("xAI OIDC discovery response was missing required endpoints.", {
125
+ kind: "validation",
126
+ provider: "xai",
127
+ });
109
128
  }
110
129
  validateXAIEndpoint(authorizationEndpoint, "authorization_endpoint");
111
130
  validateXAIEndpoint(tokenEndpoint, "token_endpoint");
@@ -245,26 +264,40 @@ export class XAIOAuthFlow extends OAuthCallbackFlow {
245
264
  } catch {
246
265
  // Ignore body-read failures; the status code is the diagnostic.
247
266
  }
248
- throw new Error(`xAI token exchange failed: ${response.status}${detail ? ` ${detail}` : ""}`);
267
+ throw new AIError.OAuthError(`xAI token exchange failed: ${response.status}${detail ? ` ${detail}` : ""}`, {
268
+ kind: "token-exchange",
269
+ provider: "xai",
270
+ status: response.status,
271
+ });
249
272
  }
250
273
 
251
274
  let tokenData: { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
252
275
  try {
253
276
  tokenData = (await response.json()) as typeof tokenData;
254
277
  } catch (error) {
255
- throw new Error(
278
+ throw new AIError.OAuthError(
256
279
  `xAI token exchange returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
280
+ { kind: "validation", provider: "xai", cause: error },
257
281
  );
258
282
  }
259
283
 
260
284
  if (typeof tokenData.access_token !== "string" || !tokenData.access_token) {
261
- throw new Error("xAI token exchange response missing access_token");
285
+ throw new AIError.OAuthError("xAI token exchange response missing access_token", {
286
+ kind: "validation",
287
+ provider: "xai",
288
+ });
262
289
  }
263
290
  if (typeof tokenData.refresh_token !== "string" || !tokenData.refresh_token) {
264
- throw new Error("xAI token exchange response missing refresh_token");
291
+ throw new AIError.OAuthError("xAI token exchange response missing refresh_token", {
292
+ kind: "validation",
293
+ provider: "xai",
294
+ });
265
295
  }
266
296
  if (typeof tokenData.expires_in !== "number" || !Number.isFinite(tokenData.expires_in)) {
267
- throw new Error("xAI token exchange response missing expires_in");
297
+ throw new AIError.OAuthError("xAI token exchange response missing expires_in", {
298
+ kind: "validation",
299
+ provider: "xai",
300
+ });
268
301
  }
269
302
 
270
303
  return {
@@ -292,7 +325,7 @@ export async function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredent
292
325
  export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials> {
293
326
  const fetchImpl = fetchOverride ?? fetch;
294
327
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
295
- throw new Error("missing refresh_token");
328
+ throw new AIError.OAuthError("missing refresh_token", { kind: "validation", provider: "xai" });
296
329
  }
297
330
 
298
331
  const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl);
@@ -321,23 +354,34 @@ export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?:
321
354
  } catch {
322
355
  // Ignore body-read failures; the status code is the diagnostic.
323
356
  }
324
- throw new Error(`xAI token refresh failed: ${response.status}${detail ? ` ${detail}` : ""}`);
357
+ throw new AIError.OAuthError(`xAI token refresh failed: ${response.status}${detail ? ` ${detail}` : ""}`, {
358
+ kind: "token-refresh",
359
+ provider: "xai",
360
+ status: response.status,
361
+ });
325
362
  }
326
363
 
327
364
  let data: { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
328
365
  try {
329
366
  data = (await response.json()) as typeof data;
330
367
  } catch (error) {
331
- throw new Error(
368
+ throw new AIError.OAuthError(
332
369
  `xAI token refresh returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
370
+ { kind: "validation", provider: "xai", cause: error },
333
371
  );
334
372
  }
335
373
 
336
374
  if (typeof data.access_token !== "string" || !data.access_token) {
337
- throw new Error("xAI token refresh response missing access_token");
375
+ throw new AIError.OAuthError("xAI token refresh response missing access_token", {
376
+ kind: "validation",
377
+ provider: "xai",
378
+ });
338
379
  }
339
380
  if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in)) {
340
- throw new Error("xAI token refresh response missing expires_in");
381
+ throw new AIError.OAuthError("xAI token refresh response missing expires_in", {
382
+ kind: "validation",
383
+ provider: "xai",
384
+ });
341
385
  }
342
386
 
343
387
  const newRefresh = typeof data.refresh_token === "string" && data.refresh_token ? data.refresh_token : refreshToken;
@@ -8,6 +8,7 @@
8
8
  * login opens plan management so users copy the regional `tp-...` key.
9
9
  */
10
10
 
11
+ import * as AIError from "../../error";
11
12
  import type { FetchImpl } from "../../types";
12
13
  import type { OAuthController } from "./types";
13
14
 
@@ -103,10 +104,11 @@ async function validateXiaomiApiKey(
103
104
  } catch {
104
105
  // ignore body parse errors, status is enough
105
106
  }
106
- lastError = new Error(
107
+ lastError = new AIError.OAuthError(
107
108
  details
108
109
  ? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
109
110
  : `${PROVIDER_NAME} API key validation failed (${response.status})`,
111
+ { kind: "validation", provider: PROVIDER_ID, status: response.status },
110
112
  );
111
113
  continue;
112
114
  }
@@ -121,7 +123,11 @@ async function validateXiaomiApiKey(
121
123
  const message = details
122
124
  ? `${PROVIDER_NAME} API key validation failed (${response.status}): ${details}`
123
125
  : `${PROVIDER_NAME} API key validation failed (${response.status})`;
124
- throw new Error(message);
126
+ throw new AIError.OAuthError(message, {
127
+ kind: "validation",
128
+ provider: PROVIDER_ID,
129
+ status: response.status,
130
+ });
125
131
  } catch (e) {
126
132
  // Only re-throw AbortError when the caller explicitly cancelled.
127
133
  // Timeout aborts (from AbortSignal.timeout) should fall through to
@@ -132,7 +138,13 @@ async function validateXiaomiApiKey(
132
138
  lastError = e instanceof Error ? e : new Error(String(e));
133
139
  }
134
140
  }
135
- throw lastError ?? new Error(`${PROVIDER_NAME} API key validation failed`);
141
+ throw (
142
+ lastError ??
143
+ new AIError.OAuthError(`${PROVIDER_NAME} API key validation failed`, {
144
+ kind: "validation",
145
+ provider: PROVIDER_ID,
146
+ })
147
+ );
136
148
  }
137
149
 
138
150
  /**
@@ -144,7 +156,7 @@ async function validateXiaomiApiKey(
144
156
  export async function loginXiaomi(options: OAuthController): Promise<string> {
145
157
  const fetchImpl = options.fetch ?? fetch;
146
158
  if (!options.onPrompt) {
147
- throw new Error(`${PROVIDER_NAME} login requires onPrompt callback`);
159
+ throw new AIError.OnPromptRequiredError(PROVIDER_NAME);
148
160
  }
149
161
  options.onAuth?.({
150
162
  url: STANDARD_AUTH_URL,
@@ -155,11 +167,11 @@ export async function loginXiaomi(options: OAuthController): Promise<string> {
155
167
  placeholder: "sk-... or tp-...",
156
168
  });
157
169
  if (options.signal?.aborted) {
158
- throw new Error("Login cancelled");
170
+ throw new AIError.LoginCancelledError();
159
171
  }
160
172
  const trimmed = apiKey.trim();
161
173
  if (!trimmed) {
162
- throw new Error("API key is required");
174
+ throw new AIError.ApiKeyRequiredError();
163
175
  }
164
176
 
165
177
  options.onProgress?.(`Validating ${PROVIDER_ID} API key...`);
@@ -175,7 +187,7 @@ export async function loginXiaomi(options: OAuthController): Promise<string> {
175
187
  export async function loginXiaomiTokenPlan(options: OAuthController, region: XiaomiTokenPlanRegion): Promise<string> {
176
188
  const fetchImpl = options.fetch ?? fetch;
177
189
  if (!options.onPrompt) {
178
- throw new Error(`Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) login requires onPrompt callback`);
190
+ throw new AIError.OnPromptRequiredError(`Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]})`);
179
191
  }
180
192
  options.onAuth?.({
181
193
  url: TOKEN_PLAN_AUTH_URL,
@@ -186,11 +198,11 @@ export async function loginXiaomiTokenPlan(options: OAuthController, region: Xia
186
198
  placeholder: "tp-...",
187
199
  });
188
200
  if (options.signal?.aborted) {
189
- throw new Error("Login cancelled");
201
+ throw new AIError.LoginCancelledError();
190
202
  }
191
203
  const trimmed = apiKey.trim();
192
204
  if (!trimmed) {
193
- throw new Error("API key is required");
205
+ throw new AIError.ApiKeyRequiredError();
194
206
  }
195
207
 
196
208
  options.onProgress?.(`Validating Xiaomi Token Plan (${TOKEN_PLAN_REGION_NAMES[region]}) API key...`);
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -5,10 +6,10 @@ const OLLAMA_CLOUD_KEYS_URL = "https://ollama.com/settings/keys";
5
6
 
6
7
  export async function loginOllamaCloud(options: OAuthController): Promise<string> {
7
8
  if (options.signal?.aborted) {
8
- throw new Error("Login cancelled");
9
+ throw new AIError.LoginCancelledError();
9
10
  }
10
11
  if (!options.onPrompt) {
11
- throw new Error("Interactive prompt is required for Ollama Cloud login");
12
+ throw new AIError.ConfigurationError("Interactive prompt is required for Ollama Cloud login");
12
13
  }
13
14
  options.onAuth?.({
14
15
  url: OLLAMA_CLOUD_KEYS_URL,
@@ -19,11 +20,11 @@ export async function loginOllamaCloud(options: OAuthController): Promise<string
19
20
  placeholder: "ollama-cloud-api-key",
20
21
  });
21
22
  if (options.signal?.aborted) {
22
- throw new Error("Login cancelled");
23
+ throw new AIError.LoginCancelledError();
23
24
  }
24
25
  const trimmed = apiKey.trim();
25
26
  if (!trimmed) {
26
- throw new Error("Ollama Cloud API key is required");
27
+ throw new AIError.ApiKeyRequiredError("Ollama Cloud API key is required");
27
28
  }
28
29
  return trimmed;
29
30
  }
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthController } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -10,7 +11,7 @@ const OLLAMA_DOCS_URL = "https://github.com/ollama/ollama/blob/main/docs/api.md"
10
11
  */
11
12
  export async function loginOllama(options: OAuthController): Promise<string> {
12
13
  if (options.signal?.aborted) {
13
- throw new Error("Login cancelled");
14
+ throw new AIError.LoginCancelledError();
14
15
  }
15
16
  if (!options.onPrompt) {
16
17
  return "";
@@ -29,7 +30,7 @@ export async function loginOllama(options: OAuthController): Promise<string> {
29
30
  });
30
31
 
31
32
  if (options.signal?.aborted) {
32
- throw new Error("Login cancelled");
33
+ throw new AIError.LoginCancelledError();
33
34
  }
34
35
 
35
36
  return apiKey.trim();
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -11,7 +12,7 @@ const AUTH_URL = "https://platform.parallel.ai/settings?tab=api-keys";
11
12
  */
12
13
  export async function loginParallel(options: OAuthController): Promise<string> {
13
14
  if (!options.onPrompt) {
14
- throw new Error("Parallel login requires onPrompt callback");
15
+ throw new AIError.OnPromptRequiredError("Parallel");
15
16
  }
16
17
 
17
18
  options.onAuth?.({
@@ -25,12 +26,12 @@ export async function loginParallel(options: OAuthController): Promise<string> {
25
26
  });
26
27
 
27
28
  if (options.signal?.aborted) {
28
- throw new Error("Login cancelled");
29
+ throw new AIError.LoginCancelledError();
29
30
  }
30
31
 
31
32
  const trimmed = apiKey.trim();
32
33
  if (!trimmed) {
33
- throw new Error("API key is required");
34
+ throw new AIError.ApiKeyRequiredError();
34
35
  }
35
36
 
36
37
  return trimmed;
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import { validateOpenAICompatibleApiKey } from "./api-key-validation";
2
3
  import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
3
4
  import type { ProviderDefinition } from "./types";
@@ -8,7 +9,7 @@ const VALIDATION_MODEL = "coder-model";
8
9
 
9
10
  export async function loginQwenPortal(options: OAuthController): Promise<string> {
10
11
  if (!options.onPrompt) {
11
- throw new Error("Qwen Portal login requires onPrompt callback");
12
+ throw new AIError.OnPromptRequiredError("Qwen Portal");
12
13
  }
13
14
 
14
15
  options.onAuth?.({
@@ -22,12 +23,12 @@ export async function loginQwenPortal(options: OAuthController): Promise<string>
22
23
  });
23
24
 
24
25
  if (options.signal?.aborted) {
25
- throw new Error("Login cancelled");
26
+ throw new AIError.LoginCancelledError();
26
27
  }
27
28
 
28
29
  const trimmed = token.trim();
29
30
  if (!trimmed) {
30
- throw new Error("Qwen token/API key is required");
31
+ throw new AIError.ApiKeyRequiredError("Qwen token/API key is required");
31
32
  }
32
33
 
33
34
  options.onProgress?.("Validating credentials...");
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthLoginCallbacks } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -11,7 +12,7 @@ const AUTH_URL = "https://app.tavily.com/home";
11
12
  */
12
13
  export async function loginTavily(options: OAuthLoginCallbacks): Promise<string> {
13
14
  if (!options.onPrompt) {
14
- throw new Error("Tavily login requires onPrompt callback");
15
+ throw new AIError.OnPromptRequiredError("Tavily");
15
16
  }
16
17
 
17
18
  options.onAuth?.({
@@ -25,12 +26,12 @@ export async function loginTavily(options: OAuthLoginCallbacks): Promise<string>
25
26
  });
26
27
 
27
28
  if (options.signal?.aborted) {
28
- throw new Error("Login cancelled");
29
+ throw new AIError.LoginCancelledError();
29
30
  }
30
31
 
31
32
  const trimmed = apiKey.trim();
32
33
  if (!trimmed) {
33
- throw new Error("API key is required");
34
+ throw new AIError.ApiKeyRequiredError();
34
35
  }
35
36
 
36
37
  return trimmed;