@oh-my-pi/pi-ai 16.2.0 → 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
@@ -12,6 +12,7 @@ import {
12
12
  normalizeGitHubCopilotEnterpriseDomain,
13
13
  OPENCODE_HEADERS,
14
14
  } from "@oh-my-pi/pi-catalog/wire/github-copilot";
15
+ import * as AIError from "../../error";
15
16
  import type { FetchImpl } from "../../types";
16
17
  import type { OAuthCredentials } from "./types";
17
18
 
@@ -63,7 +64,7 @@ async function fetchJson(url: string, init: RequestInit, fetchImpl: FetchImpl):
63
64
  const response = await fetchImpl(url, init);
64
65
  if (!response.ok) {
65
66
  const text = await response.text();
66
- throw new Error(`${response.status} ${response.statusText}: ${text}`);
67
+ throw new AIError.ProviderHttpError(`${response.status} ${response.statusText}: ${text}`, response.status);
67
68
  }
68
69
  return response.json();
69
70
  }
@@ -88,7 +89,7 @@ async function startDeviceFlow(domain: string, fetchImpl: FetchImpl): Promise<De
88
89
  );
89
90
 
90
91
  if (!data || typeof data !== "object") {
91
- throw new Error("Invalid device code response");
92
+ throw new AIError.OAuthError("Invalid device code response", { kind: "validation", provider: "github-copilot" });
92
93
  }
93
94
 
94
95
  const deviceCode = (data as Record<string, unknown>).device_code;
@@ -104,7 +105,10 @@ async function startDeviceFlow(domain: string, fetchImpl: FetchImpl): Promise<De
104
105
  typeof interval !== "number" ||
105
106
  typeof expiresIn !== "number"
106
107
  ) {
107
- throw new Error("Invalid device code response fields");
108
+ throw new AIError.OAuthError("Invalid device code response fields", {
109
+ kind: "validation",
110
+ provider: "github-copilot",
111
+ });
108
112
  }
109
113
 
110
114
  return {
@@ -134,7 +138,7 @@ async function pollForGitHubAccessToken(
134
138
 
135
139
  while (Date.now() < deadline) {
136
140
  if (signal?.aborted) {
137
- throw new Error("Login cancelled");
141
+ throw new AIError.LoginCancelledError();
138
142
  }
139
143
 
140
144
  const remainingMs = deadline - Date.now();
@@ -142,7 +146,7 @@ async function pollForGitHubAccessToken(
142
146
  try {
143
147
  await scheduler.wait(waitMs, { signal });
144
148
  } catch {
145
- throw new Error("Login cancelled");
149
+ throw new AIError.LoginCancelledError();
146
150
  }
147
151
 
148
152
  const raw = await fetchJson(
@@ -184,17 +188,21 @@ async function pollForGitHubAccessToken(
184
188
  }
185
189
 
186
190
  const descriptionSuffix = description ? `: ${description}` : "";
187
- throw new Error(`Device flow failed: ${error}${descriptionSuffix}`);
191
+ throw new AIError.OAuthError(`Device flow failed: ${error}${descriptionSuffix}`, {
192
+ kind: "polling",
193
+ provider: "github-copilot",
194
+ });
188
195
  }
189
196
  }
190
197
 
191
198
  if (slowDownResponses > 0) {
192
- throw new Error(
199
+ throw new AIError.OAuthError(
193
200
  "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.",
201
+ { kind: "timeout", provider: "github-copilot" },
194
202
  );
195
203
  }
196
204
 
197
- throw new Error("Device flow timed out");
205
+ throw new AIError.OAuthError("Device flow timed out", { kind: "timeout", provider: "github-copilot" });
198
206
  }
199
207
 
200
208
  /** Far-future expiry (10 years). GitHub OAuth tokens are long-lived; no JWT exchange needed. */
@@ -314,13 +322,16 @@ export async function loginGitHubCopilot(options: GitHubCopilotLoginOptions): Pr
314
322
  });
315
323
 
316
324
  if (options.signal?.aborted) {
317
- throw new Error("Login cancelled");
325
+ throw new AIError.LoginCancelledError();
318
326
  }
319
327
 
320
328
  const trimmed = input.trim();
321
329
  const normalizedDomain = normalizeDomain(input);
322
330
  if (trimmed && !normalizedDomain) {
323
- throw new Error("Invalid GitHub Enterprise URL/domain");
331
+ throw new AIError.OAuthError("Invalid GitHub Enterprise URL/domain", {
332
+ kind: "validation",
333
+ provider: "github-copilot",
334
+ });
324
335
  }
325
336
  const enterpriseDomain = normalizeGitHubCopilotEnterpriseDomain(normalizedDomain ?? undefined);
326
337
  const domain =
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../../error";
1
2
  import type { FetchImpl } from "../../types";
2
3
  import { OAuthCallbackFlow } from "./callback-server";
3
4
  import { generatePKCE } from "./pkce";
@@ -20,7 +21,10 @@ function mapTokenResponse(payload: {
20
21
  created_at?: number;
21
22
  }): OAuthCredentials {
22
23
  if (!payload.access_token || !payload.refresh_token || typeof payload.expires_in !== "number") {
23
- throw new Error("GitLab Duo Workflow OAuth token response missing required fields");
24
+ throw new AIError.OAuthError("GitLab Duo Workflow OAuth token response missing required fields", {
25
+ kind: "validation",
26
+ provider: "gitlab-duo-workflow",
27
+ });
24
28
  }
25
29
 
26
30
  const createdAtMs =
@@ -82,8 +86,9 @@ class GitLabDuoWorkflowOAuthFlow extends OAuthCallbackFlow {
82
86
  });
83
87
 
84
88
  if (!response.ok) {
85
- throw new Error(
89
+ throw new AIError.OAuthError(
86
90
  `GitLab Duo Workflow OAuth token exchange failed: ${response.status} ${await response.text()}`,
91
+ { kind: "token-exchange", provider: "gitlab-duo-workflow", status: response.status },
87
92
  );
88
93
  }
89
94
 
@@ -120,7 +125,14 @@ export async function refreshGitLabDuoWorkflowToken(
120
125
  });
121
126
 
122
127
  if (!response.ok) {
123
- throw new Error(`GitLab Duo Workflow OAuth refresh failed: ${response.status} ${await response.text()}`);
128
+ throw new AIError.OAuthError(
129
+ `GitLab Duo Workflow OAuth refresh failed: ${response.status} ${await response.text()}`,
130
+ {
131
+ kind: "token-refresh",
132
+ provider: "gitlab-duo-workflow",
133
+ status: response.status,
134
+ },
135
+ );
124
136
  }
125
137
 
126
138
  return mapTokenResponse(
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../../error";
1
2
  import { clearGitLabDuoDirectAccessCache } from "../../providers/gitlab-duo";
2
3
  import type { FetchImpl } from "../../types";
3
4
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
@@ -59,15 +60,24 @@ function resolveCallbackOptions(): OAuthCallbackFlowOptions {
59
60
  try {
60
61
  parsed = new URL(raw);
61
62
  } catch {
62
- throw new Error(`Invalid GITLAB_REDIRECT_URI: ${raw}`);
63
+ throw new AIError.OAuthError(`Invalid GITLAB_REDIRECT_URI: ${raw}`, {
64
+ kind: "configuration",
65
+ provider: "gitlab-duo",
66
+ });
63
67
  }
64
68
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
65
- throw new Error(`GITLAB_REDIRECT_URI must use http:// or https://, got: ${raw}`);
69
+ throw new AIError.OAuthError(`GITLAB_REDIRECT_URI must use http:// or https://, got: ${raw}`, {
70
+ kind: "configuration",
71
+ provider: "gitlab-duo",
72
+ });
66
73
  }
67
74
 
68
75
  const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
69
76
  if (isLoopback && parsed.protocol !== "http:") {
70
- throw new Error(`GITLAB_REDIRECT_URI loopback callbacks must use http://, got: ${raw}`);
77
+ throw new AIError.OAuthError(`GITLAB_REDIRECT_URI loopback callbacks must use http://, got: ${raw}`, {
78
+ kind: "configuration",
79
+ provider: "gitlab-duo",
80
+ });
71
81
  }
72
82
 
73
83
  const port = parsed.port ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : 80;
@@ -87,7 +97,10 @@ function mapTokenResponse(payload: {
87
97
  created_at?: number;
88
98
  }): OAuthCredentials {
89
99
  if (!payload.access_token || !payload.refresh_token || typeof payload.expires_in !== "number") {
90
- throw new Error("GitLab OAuth token response missing required fields");
100
+ throw new AIError.OAuthError("GitLab OAuth token response missing required fields", {
101
+ kind: "validation",
102
+ provider: "gitlab-duo",
103
+ });
91
104
  }
92
105
 
93
106
  const createdAtMs =
@@ -148,7 +161,14 @@ class GitLabDuoOAuthFlow extends OAuthCallbackFlow {
148
161
  });
149
162
 
150
163
  if (!response.ok) {
151
- throw new Error(`GitLab OAuth token exchange failed: ${response.status} ${await response.text()}`);
164
+ throw new AIError.OAuthError(
165
+ `GitLab OAuth token exchange failed: ${response.status} ${await response.text()}`,
166
+ {
167
+ kind: "token-exchange",
168
+ provider: "gitlab-duo",
169
+ status: response.status,
170
+ },
171
+ );
152
172
  }
153
173
 
154
174
  clearGitLabDuoDirectAccessCache();
@@ -183,7 +203,11 @@ export async function refreshGitLabDuoToken(credentials: OAuthCredentials): Prom
183
203
  });
184
204
 
185
205
  if (!response.ok) {
186
- throw new Error(`GitLab OAuth refresh failed: ${response.status} ${await response.text()}`);
206
+ throw new AIError.OAuthError(`GitLab OAuth refresh failed: ${response.status} ${await response.text()}`, {
207
+ kind: "token-refresh",
208
+ provider: "gitlab-duo",
209
+ status: response.status,
210
+ });
187
211
  }
188
212
 
189
213
  clearGitLabDuoDirectAccessCache();
@@ -3,6 +3,7 @@
3
3
  * Uses different OAuth credentials than google-gemini-cli for access to additional models.
4
4
  */
5
5
  import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
6
+ import * as AIError from "../../error";
6
7
  import { runGoogleOAuthLogin } from "./google-oauth-shared";
7
8
  import type { OAuthController, OAuthCredentials } from "./types";
8
9
 
@@ -89,7 +90,10 @@ async function onboardProjectWithRetries(
89
90
 
90
91
  if (!onboardResponse.ok) {
91
92
  const errorText = await onboardResponse.text();
92
- throw new Error(`onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`);
93
+ throw new AIError.OAuthError(
94
+ `onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`,
95
+ { kind: "provisioning", status: onboardResponse.status },
96
+ );
93
97
  }
94
98
 
95
99
  const operation = (await onboardResponse.json()) as LongRunningOperationResponse;
@@ -103,8 +107,9 @@ async function onboardProjectWithRetries(
103
107
  }
104
108
  }
105
109
 
106
- throw new Error(
110
+ throw new AIError.OAuthError(
107
111
  `onboardUser did not return a provisioned project id after ${PROJECT_ONBOARD_MAX_ATTEMPTS} attempts`,
112
+ { kind: "provisioning" },
108
113
  );
109
114
  }
110
115
 
@@ -128,7 +133,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
128
133
 
129
134
  if (!loadResponse.ok) {
130
135
  const errorText = await loadResponse.text();
131
- throw new Error(`loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`);
136
+ throw new AIError.OAuthError(
137
+ `loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`,
138
+ { kind: "discovery", status: loadResponse.status },
139
+ );
132
140
  }
133
141
 
134
142
  const loadPayload = (await loadResponse.json()) as LoadCodeAssistPayload;
@@ -146,8 +154,9 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
146
154
  const provisionedProject = await onboardProjectWithRetries(endpoint, headers, onboardBody, onProgress);
147
155
  return provisionedProject;
148
156
  } catch (error) {
149
- throw new Error(
157
+ throw new AIError.OAuthError(
150
158
  `Could not discover or provision an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
159
+ { kind: "discovery", cause: error },
151
160
  );
152
161
  }
153
162
  }
@@ -182,7 +191,7 @@ export async function refreshAntigravityToken(refreshToken: string, projectId: s
182
191
 
183
192
  if (!response.ok) {
184
193
  const error = await response.text();
185
- throw new Error(`Antigravity token refresh failed: ${error}`);
194
+ throw new AIError.OAuthError(`Antigravity token refresh failed: ${error}`, { kind: "token-refresh" });
186
195
  }
187
196
 
188
197
  const data = (await response.json()) as {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { getGeminiCliHeaders } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
7
7
  import { $env } from "@oh-my-pi/pi-utils";
8
+ import * as AIError from "../../error";
8
9
  import { runGoogleOAuthLogin } from "./google-oauth-shared";
9
10
  import type { OAuthController, OAuthCredentials } from "./types";
10
11
 
@@ -80,7 +81,11 @@ async function pollOperation(
80
81
  });
81
82
 
82
83
  if (!response.ok) {
83
- throw new Error(`Failed to poll operation: ${response.status} ${response.statusText}`);
84
+ throw new AIError.OAuthError(`Failed to poll operation: ${response.status} ${response.statusText}`, {
85
+ kind: "polling",
86
+ provider: "google-gemini-cli",
87
+ status: response.status,
88
+ });
84
89
  }
85
90
 
86
91
  const data = (await response.json()) as LongRunningOperationResponse;
@@ -130,7 +135,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
130
135
  data = { currentTier: { id: TIER_STANDARD } };
131
136
  } else {
132
137
  const errorText = await loadResponse.text();
133
- throw new Error(`loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`);
138
+ throw new AIError.OAuthError(
139
+ `loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`,
140
+ { kind: "discovery", provider: "google-gemini-cli", status: loadResponse.status },
141
+ );
134
142
  }
135
143
  } else {
136
144
  data = (await loadResponse.json()) as LoadCodeAssistPayload;
@@ -143,9 +151,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
143
151
  if (envProjectId) {
144
152
  return envProjectId;
145
153
  }
146
- throw new Error(
154
+ throw new AIError.OAuthError(
147
155
  "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
148
156
  "See https://goo.gle/gemini-cli-auth-docs#workspace-gca",
157
+ { kind: "configuration", provider: "google-gemini-cli" },
149
158
  );
150
159
  }
151
160
 
@@ -153,9 +162,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
153
162
  const tierId = tier?.id ?? TIER_FREE;
154
163
 
155
164
  if (tierId !== TIER_FREE && !envProjectId) {
156
- throw new Error(
165
+ throw new AIError.OAuthError(
157
166
  "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
158
167
  "See https://goo.gle/gemini-cli-auth-docs#workspace-gca",
168
+ { kind: "configuration", provider: "google-gemini-cli" },
159
169
  );
160
170
  }
161
171
 
@@ -183,7 +193,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
183
193
 
184
194
  if (!onboardResponse.ok) {
185
195
  const errorText = await onboardResponse.text();
186
- throw new Error(`onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`);
196
+ throw new AIError.OAuthError(
197
+ `onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`,
198
+ { kind: "provisioning", provider: "google-gemini-cli", status: onboardResponse.status },
199
+ );
187
200
  }
188
201
 
189
202
  let lroData = (await onboardResponse.json()) as LongRunningOperationResponse;
@@ -201,10 +214,11 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
201
214
  return envProjectId;
202
215
  }
203
216
 
204
- throw new Error(
217
+ throw new AIError.OAuthError(
205
218
  "Could not discover or provision a Google Cloud project. " +
206
219
  "Try setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " +
207
220
  "See https://goo.gle/gemini-cli-auth-docs#workspace-gca",
221
+ { kind: "validation", provider: "google-gemini-cli" },
208
222
  );
209
223
  }
210
224
 
@@ -238,7 +252,10 @@ export async function refreshGoogleCloudToken(refreshToken: string, projectId: s
238
252
 
239
253
  if (!response.ok) {
240
254
  const error = await response.text();
241
- throw new Error(`Google Cloud token refresh failed: ${error}`);
255
+ throw new AIError.OAuthError(`Google Cloud token refresh failed: ${error}`, {
256
+ kind: "token-refresh",
257
+ provider: "google-gemini-cli",
258
+ });
242
259
  }
243
260
 
244
261
  const data = (await response.json()) as {
@@ -4,6 +4,7 @@
4
4
  * Both providers use the same authorization-code flow shape; only the client
5
5
  * credentials, scopes, endpoint constants, and project-discovery logic differ.
6
6
  */
7
+ import * as AIError from "../../error";
7
8
  import { extractGoogleValidationUrl, formatGoogleValidationRequiredMessage } from "../../utils/google-validation";
8
9
  import { OAuthCallbackFlow } from "./callback-server";
9
10
  import type { OAuthController, OAuthCredentials } from "./types";
@@ -79,7 +80,7 @@ export class GoogleOAuthFlow extends OAuthCallbackFlow {
79
80
 
80
81
  if (!tokenResponse.ok) {
81
82
  const error = await tokenResponse.text();
82
- throw new Error(`Token exchange failed: ${error}`);
83
+ throw new AIError.OAuthError(`Token exchange failed: ${error}`, { kind: "token-exchange" });
83
84
  }
84
85
 
85
86
  const tokenData = (await tokenResponse.json()) as {
@@ -89,7 +90,7 @@ export class GoogleOAuthFlow extends OAuthCallbackFlow {
89
90
  };
90
91
 
91
92
  if (!tokenData.refresh_token) {
92
- throw new Error("No refresh token received. Please try again.");
93
+ throw new AIError.OAuthError("No refresh token received. Please try again.", { kind: "validation" });
93
94
  }
94
95
 
95
96
  this.ctrl.onProgress?.("Getting user info...");
@@ -100,7 +101,9 @@ export class GoogleOAuthFlow extends OAuthCallbackFlow {
100
101
  } catch (err) {
101
102
  const validationUrl = extractGoogleValidationUrl(err instanceof Error ? err.message : String(err));
102
103
  if (!validationUrl) throw err;
103
- throw new Error(formatGoogleValidationRequiredMessage(validationUrl, "sign in again", email));
104
+ throw new AIError.OAuthError(formatGoogleValidationRequiredMessage(validationUrl, "sign in again", email), {
105
+ kind: "validation",
106
+ });
104
107
  }
105
108
 
106
109
  return {
@@ -2,6 +2,7 @@
2
2
  // High-level API
3
3
  // ============================================================================
4
4
 
5
+ import * as AIError from "../../error";
5
6
  import { getProviderDefinition, PROVIDER_REGISTRY } from "../registry";
6
7
  import type {
7
8
  OAuthCredentials,
@@ -46,14 +47,14 @@ async function abortableDeviceFlowSleep(ms: number, signal: AbortSignal | undefi
46
47
  return;
47
48
  }
48
49
  if (signal.aborted) {
49
- throw new Error(DEVICE_FLOW_CANCEL_MESSAGE);
50
+ throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
50
51
  }
51
52
 
52
53
  const { promise, resolve, reject } = Promise.withResolvers<void>();
53
54
  let timer: Timer | undefined;
54
55
  const onAbort = () => {
55
56
  if (timer) clearTimeout(timer);
56
- reject(new Error(DEVICE_FLOW_CANCEL_MESSAGE));
57
+ reject(new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE));
57
58
  };
58
59
  timer = setTimeout(() => {
59
60
  signal.removeEventListener("abort", onAbort);
@@ -77,14 +78,14 @@ export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOpt
77
78
 
78
79
  while (Date.now() < deadline) {
79
80
  if (options.signal?.aborted) {
80
- throw new Error(DEVICE_FLOW_CANCEL_MESSAGE);
81
+ throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
81
82
  }
82
83
  const result = await options.poll();
83
84
  if (result.status === "complete") {
84
85
  return result.value;
85
86
  }
86
87
  if (result.status === "failed") {
87
- throw new Error(result.message);
88
+ throw new AIError.OAuthError(result.message, { kind: "polling" });
88
89
  }
89
90
  if (result.status === "slow_down") {
90
91
  slowDownResponses += 1;
@@ -98,7 +99,10 @@ export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOpt
98
99
  await abortableDeviceFlowSleep(Math.min(intervalMs, remainingMs), options.signal);
99
100
  }
100
101
 
101
- throw new Error(slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE);
102
+ throw new AIError.OAuthError(
103
+ slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE,
104
+ { kind: "timeout" },
105
+ );
102
106
  }
103
107
 
104
108
  const builtInOAuthProviders: OAuthProviderInfo[] = PROVIDER_REGISTRY.filter(
@@ -146,11 +150,17 @@ export async function refreshOAuthToken(
146
150
  credentials: OAuthCredentials,
147
151
  ): Promise<OAuthCredentials> {
148
152
  if (!credentials) {
149
- throw new Error(`No OAuth credentials found for ${provider}`);
153
+ throw new AIError.OAuthError(`No OAuth credentials found for ${provider}`, {
154
+ kind: "validation",
155
+ provider,
156
+ });
150
157
  }
151
158
  const def = getProviderDefinition(provider);
152
159
  if (!def?.login) {
153
- throw new Error(`Unknown OAuth provider: ${provider}`);
160
+ throw new AIError.OAuthError(`Unknown OAuth provider: ${provider}`, {
161
+ kind: "validation",
162
+ provider,
163
+ });
154
164
  }
155
165
  // Providers without a real refresher (static bearer tokens / API keys that
156
166
  // don't expire) return the credentials unchanged.
@@ -219,8 +229,9 @@ export async function getOAuthApiKey(
219
229
  return { newCredentials: fallbackCredentials, apiKey: fallbackCredentials.access };
220
230
  }
221
231
  }
222
- throw new Error(
232
+ throw new AIError.OAuthError(
223
233
  `OAuth credential for ${provider} is expired and must be refreshed via AuthStorage before getOAuthApiKey is called`,
234
+ { kind: "validation", provider },
224
235
  );
225
236
  }
226
237
  // For providers that need request-time credential metadata, return JSON.
@@ -9,6 +9,7 @@ import * as path from "node:path";
9
9
  import { scheduler } from "node:timers/promises";
10
10
  import { $env, getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
11
11
  import packageJson from "../../../package.json" with { type: "json" };
12
+ import * as AIError from "../../error";
12
13
  import type { OAuthController, OAuthCredentials } from "./types";
13
14
 
14
15
  const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
@@ -113,7 +114,11 @@ async function requestDeviceAuthorization(): Promise<{
113
114
 
114
115
  if (!response.ok) {
115
116
  const text = await response.text();
116
- throw new Error(`Kimi device authorization failed: ${response.status} ${text}`);
117
+ throw new AIError.OAuthError(`Kimi device authorization failed: ${response.status} ${text}`, {
118
+ kind: "device-auth",
119
+ provider: "kimi",
120
+ status: response.status,
121
+ });
117
122
  }
118
123
 
119
124
  const payload = (await response.json()) as DeviceAuthorizationResponse;
@@ -123,7 +128,10 @@ async function requestDeviceAuthorization(): Promise<{
123
128
  const verificationUriComplete = payload.verification_uri_complete;
124
129
 
125
130
  if (!userCode || !deviceCode || !verificationUri) {
126
- throw new Error("Kimi device authorization response missing required fields");
131
+ throw new AIError.OAuthError("Kimi device authorization response missing required fields", {
132
+ kind: "validation",
133
+ provider: "kimi",
134
+ });
127
135
  }
128
136
 
129
137
  const expiresInMs = typeof payload.expires_in === "number" ? payload.expires_in * 1000 : DEFAULT_DEVICE_FLOW_TTL_MS;
@@ -142,12 +150,18 @@ async function requestDeviceAuthorization(): Promise<{
142
150
 
143
151
  function parseTokenPayload(payload: TokenResponse, refreshTokenFallback?: string): OAuthCredentials {
144
152
  if (!payload.access_token || typeof payload.expires_in !== "number") {
145
- throw new Error("Kimi token response missing required fields");
153
+ throw new AIError.OAuthError("Kimi token response missing required fields", {
154
+ kind: "validation",
155
+ provider: "kimi",
156
+ });
146
157
  }
147
158
 
148
159
  const refresh = payload.refresh_token ?? refreshTokenFallback;
149
160
  if (!refresh) {
150
- throw new Error("Kimi token response missing refresh token");
161
+ throw new AIError.OAuthError("Kimi token response missing refresh token", {
162
+ kind: "validation",
163
+ provider: "kimi",
164
+ });
151
165
  }
152
166
 
153
167
  return {
@@ -168,7 +182,7 @@ async function pollForToken(
168
182
 
169
183
  while (Date.now() < deadline) {
170
184
  if (signal?.aborted) {
171
- throw new Error("Login cancelled");
185
+ throw new AIError.LoginCancelledError();
172
186
  }
173
187
 
174
188
  const response = await fetch(`${resolveOAuthHost()}/api/oauth/token`, {
@@ -204,18 +218,30 @@ async function pollForToken(
204
218
  }
205
219
 
206
220
  if (error === "expired_token") {
207
- throw new Error("Kimi device authorization expired");
221
+ throw new AIError.OAuthError("Kimi device authorization expired", {
222
+ kind: "validation",
223
+ provider: "kimi",
224
+ });
208
225
  }
209
226
 
210
227
  if (error === "access_denied") {
211
- throw new Error("Kimi device authorization denied");
228
+ throw new AIError.OAuthError("Kimi device authorization denied", {
229
+ kind: "validation",
230
+ provider: "kimi",
231
+ });
212
232
  }
213
233
 
214
234
  const description = payload.error_description ? `: ${payload.error_description}` : "";
215
- throw new Error(`Kimi device flow failed: ${error ?? response.status}${description}`);
235
+ throw new AIError.OAuthError(`Kimi device flow failed: ${error ?? response.status}${description}`, {
236
+ kind: "polling",
237
+ provider: "kimi",
238
+ });
216
239
  }
217
240
 
218
- throw new Error("Kimi device flow timed out");
241
+ throw new AIError.OAuthError("Kimi device flow timed out", {
242
+ kind: "timeout",
243
+ provider: "kimi",
244
+ });
219
245
  }
220
246
 
221
247
  /**
@@ -251,7 +277,11 @@ export async function refreshKimiToken(refreshToken: string): Promise<OAuthCrede
251
277
  if (!response.ok) {
252
278
  const payload = (await response.json().catch(() => undefined)) as TokenResponse | undefined;
253
279
  const description = payload?.error_description ? `: ${payload.error_description}` : "";
254
- throw new Error(`Kimi token refresh failed: ${response.status}${description}`);
280
+ throw new AIError.OAuthError(`Kimi token refresh failed: ${response.status}${description}`, {
281
+ kind: "token-refresh",
282
+ provider: "kimi",
283
+ status: response.status,
284
+ });
255
285
  }
256
286
 
257
287
  const payload = (await response.json()) as TokenResponse;