@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
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import * as apiKeyValidation from "./api-key-validation";
2
3
  import type { OAuthController, OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
3
4
  import type { ProviderDefinition } from "./types";
@@ -10,7 +11,7 @@ const VALIDATION_MODEL = "qwen3.5-plus";
10
11
 
11
12
  export async function loginAlibabaCodingPlan(options: OAuthController): Promise<OAuthCredentials> {
12
13
  if (!options.onPrompt) {
13
- throw new Error("Alibaba Coding Plan login requires onPrompt callback");
14
+ throw new AIError.OnPromptRequiredError("Alibaba Coding Plan");
14
15
  }
15
16
 
16
17
  // Ask which endpoint to use
@@ -21,7 +22,7 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<
21
22
 
22
23
  // Check for abort after endpoint selection (Escape returns "")
23
24
  if (options.signal?.aborted) {
24
- throw new Error("Login cancelled");
25
+ throw new AIError.LoginCancelledError();
25
26
  }
26
27
 
27
28
  const choice = endpointChoice.trim();
@@ -39,7 +40,7 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<
39
40
  });
40
41
  const trimmedUrl = customUrl.trim().replace(/\/+$/, "");
41
42
  if (!trimmedUrl) {
42
- throw new Error("Custom URL is required for option 3");
43
+ throw new AIError.ConfigurationError("Custom URL is required for option 3");
43
44
  }
44
45
  baseUrl = trimmedUrl;
45
46
  authUrl = DEFAULT_AUTH_URL;
@@ -61,12 +62,12 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<
61
62
  });
62
63
 
63
64
  if (options.signal?.aborted) {
64
- throw new Error("Login cancelled");
65
+ throw new AIError.LoginCancelledError();
65
66
  }
66
67
 
67
68
  const trimmed = apiKey.trim();
68
69
  if (!trimmed) {
69
- throw new Error("API key is required");
70
+ throw new AIError.ApiKeyRequiredError();
70
71
  }
71
72
 
72
73
  options.onProgress?.("Validating API key...");
@@ -6,6 +6,7 @@
6
6
  * optionally validate it, and return the trimmed key.
7
7
  */
8
8
 
9
+ import * as AIError from "../error";
9
10
  import {
10
11
  validateAnthropicCompatibleApiKey,
11
12
  validateApiKeyAgainstModelsEndpoint,
@@ -52,7 +53,7 @@ export type ApiKeyLoginConfig = {
52
53
  export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthController) => Promise<string> {
53
54
  return async function login(options: OAuthController): Promise<string> {
54
55
  if (!options.onPrompt) {
55
- throw new Error(`${config.providerLabel} login requires onPrompt callback`);
56
+ throw new AIError.OnPromptRequiredError(config.providerLabel);
56
57
  }
57
58
 
58
59
  options.onAuth?.({
@@ -66,12 +67,12 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon
66
67
  });
67
68
 
68
69
  if (options.signal?.aborted) {
69
- throw new Error("Login cancelled");
70
+ throw new AIError.LoginCancelledError();
70
71
  }
71
72
 
72
73
  const trimmed = apiKey.trim();
73
74
  if (!trimmed) {
74
- throw new Error("API key is required");
75
+ throw new AIError.ApiKeyRequiredError();
75
76
  }
76
77
 
77
78
  if (config.validation) {
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { FetchImpl } from "../types";
2
3
 
3
4
  type OpenAICompatibleValidationOptions = {
@@ -78,7 +79,7 @@ export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleVa
78
79
  const message = details
79
80
  ? `${options.provider} API key validation failed (${response.status}): ${details}`
80
81
  : `${options.provider} API key validation failed (${response.status})`;
81
- throw new Error(message);
82
+ throw new AIError.ApiKeyRequiredError(message);
82
83
  }
83
84
 
84
85
  /**
@@ -119,7 +120,7 @@ export async function validateAnthropicCompatibleApiKey(options: AnthropicCompat
119
120
  const message = details
120
121
  ? `${options.provider} API key validation failed (${response.status}): ${details}`
121
122
  : `${options.provider} API key validation failed (${response.status})`;
122
- throw new Error(message);
123
+ throw new AIError.ApiKeyRequiredError(message);
123
124
  }
124
125
 
125
126
  /**
@@ -156,5 +157,5 @@ export async function validateApiKeyAgainstModelsEndpoint(options: ModelListVali
156
157
  const message = details
157
158
  ? `${options.provider} API key validation failed (${response.status}): ${details}`
158
159
  : `${options.provider} API key validation failed (${response.status})`;
159
- throw new Error(message);
160
+ throw new AIError.ApiKeyRequiredError(message);
160
161
  }
@@ -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://developers.cloudflare.com/ai-gateway/configuration/aut
11
12
  */
12
13
  export async function loginCloudflareAiGateway(options: OAuthController): Promise<string> {
13
14
  if (!options.onPrompt) {
14
- throw new Error("Cloudflare AI Gateway login requires onPrompt callback");
15
+ throw new AIError.OnPromptRequiredError("Cloudflare AI Gateway");
15
16
  }
16
17
 
17
18
  options.onAuth?.({
@@ -26,12 +27,12 @@ export async function loginCloudflareAiGateway(options: OAuthController): Promis
26
27
  });
27
28
 
28
29
  if (options.signal?.aborted) {
29
- throw new Error("Login cancelled");
30
+ throw new AIError.LoginCancelledError();
30
31
  }
31
32
 
32
33
  const trimmed = apiKey.trim();
33
34
  if (!trimmed) {
34
- throw new Error("API key is required");
35
+ throw new AIError.ApiKeyRequiredError();
35
36
  }
36
37
 
37
38
  return trimmed;
@@ -1,5 +1,6 @@
1
1
  import { coreWeaveProjectHeaders } from "@oh-my-pi/pi-catalog/wire/coreweave";
2
2
  import { $env } from "@oh-my-pi/pi-utils";
3
+ import * as AIError from "../error";
3
4
  import { createApiKeyLogin } from "./api-key-login";
4
5
  import type { OAuthLoginCallbacks } from "./oauth/types";
5
6
  import type { ProviderDefinition } from "./types";
@@ -10,7 +11,7 @@ const PROJECT_SETUP_INSTRUCTIONS =
10
11
  function requireCoreWeaveProjectHeaders(): Record<string, string> {
11
12
  const headers = coreWeaveProjectHeaders($env);
12
13
  if (!headers) {
13
- throw new Error(
14
+ throw new AIError.ConfigurationError(
14
15
  "CoreWeave Serverless Inference requires OpenAI-Project. Set COREWEAVE_PROJECT=<team>/<project> before running /login coreweave.",
15
16
  );
16
17
  }
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import { createApiKeyLogin } from "./api-key-login";
2
3
  import type { OAuthController, OAuthLoginCallbacks, OAuthPrompt } from "./oauth/types";
3
4
  import type { ProviderDefinition } from "./types";
@@ -22,7 +23,7 @@ export function normalizeDeepSeekApiKey(raw: string): string {
22
23
  }
23
24
  const stripped = trimmed.replace(/^bearer\b\s*/i, "");
24
25
  if (!stripped) {
25
- throw new Error("DeepSeek API key is empty after stripping Bearer prefix");
26
+ throw new AIError.ApiKeyRequiredError("DeepSeek API key is empty after stripping Bearer prefix");
26
27
  }
27
28
  return stripped;
28
29
  }
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -11,7 +12,7 @@ export const googleAntigravityProvider = {
11
12
  },
12
13
  refreshToken: async (credentials: OAuthCredentials) => {
13
14
  if (!credentials.projectId) {
14
- throw new Error("Antigravity credentials missing projectId");
15
+ throw new AIError.ConfigurationError("Antigravity credentials missing projectId");
15
16
  }
16
17
  const { refreshAntigravityToken } = await import("./oauth/google-antigravity");
17
18
  return refreshAntigravityToken(credentials.refresh, credentials.projectId);
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -11,7 +12,7 @@ export const googleGeminiCliProvider = {
11
12
  },
12
13
  refreshToken: async (credentials: OAuthCredentials) => {
13
14
  if (!credentials.projectId) {
14
- throw new Error("Google Cloud credentials missing projectId");
15
+ throw new AIError.ConfigurationError("Google Cloud credentials missing projectId");
15
16
  }
16
17
  const { refreshGoogleCloudToken } = await import("./oauth/google-gemini-cli");
17
18
  return refreshGoogleCloudToken(credentials.refresh, credentials.projectId);
@@ -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://kagi.com/settings/api";
11
12
  */
12
13
  export async function loginKagi(options: OAuthController): Promise<string> {
13
14
  if (!options.onPrompt) {
14
- throw new Error("Kagi login requires onPrompt callback");
15
+ throw new AIError.OnPromptRequiredError("Kagi");
15
16
  }
16
17
 
17
18
  options.onAuth?.({
@@ -26,12 +27,12 @@ export async function loginKagi(options: OAuthController): Promise<string> {
26
27
  });
27
28
 
28
29
  if (options.signal?.aborted) {
29
- throw new Error("Login cancelled");
30
+ throw new AIError.LoginCancelledError();
30
31
  }
31
32
 
32
33
  const trimmed = apiKey.trim();
33
34
  if (!trimmed) {
34
- throw new Error("API key is required");
35
+ throw new AIError.ApiKeyRequiredError();
35
36
  }
36
37
 
37
38
  return trimmed;
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../error";
1
2
  import type { OAuthController, OAuthCredentials } from "./oauth/types";
2
3
  import type { ProviderDefinition } from "./types";
3
4
 
@@ -25,9 +26,17 @@ export async function loginKilo(callbacks: OAuthController): Promise<OAuthCreden
25
26
 
26
27
  if (!initiateResponse.ok) {
27
28
  if (initiateResponse.status === 429) {
28
- throw new Error("Too many pending authorization requests. Please try again later.");
29
+ throw new AIError.OAuthError("Too many pending authorization requests. Please try again later.", {
30
+ kind: "polling",
31
+ provider: "kilo",
32
+ status: initiateResponse.status,
33
+ });
29
34
  }
30
- throw new Error(`Failed to initiate device authorization: ${initiateResponse.status}`);
35
+ throw new AIError.OAuthError(`Failed to initiate device authorization: ${initiateResponse.status}`, {
36
+ kind: "device-auth",
37
+ provider: "kilo",
38
+ status: initiateResponse.status,
39
+ });
31
40
  }
32
41
 
33
42
  const initiateData = (await initiateResponse.json()) as KiloDeviceAuthCodeResponse;
@@ -35,7 +44,10 @@ export async function loginKilo(callbacks: OAuthController): Promise<OAuthCreden
35
44
  const verificationUrl = initiateData.verificationUrl;
36
45
  const expiresInSeconds = initiateData.expiresIn;
37
46
  if (!userCode || !verificationUrl || typeof expiresInSeconds !== "number" || expiresInSeconds <= 0) {
38
- throw new Error("Kilo device authorization response missing required fields");
47
+ throw new AIError.OAuthError("Kilo device authorization response missing required fields", {
48
+ kind: "validation",
49
+ provider: "kilo",
50
+ });
39
51
  }
40
52
 
41
53
  callbacks.onAuth?.({
@@ -46,7 +58,7 @@ export async function loginKilo(callbacks: OAuthController): Promise<OAuthCreden
46
58
  const deadline = Date.now() + expiresInSeconds * 1000;
47
59
  while (Date.now() < deadline) {
48
60
  if (callbacks.signal?.aborted) {
49
- throw new Error("Login cancelled");
61
+ throw new AIError.LoginCancelledError();
50
62
  }
51
63
 
52
64
  const pollResponse = await fetchImpl(`${KILO_DEVICE_AUTH_BASE_URL}/codes/${encodeURIComponent(userCode)}`);
@@ -55,13 +67,20 @@ export async function loginKilo(callbacks: OAuthController): Promise<OAuthCreden
55
67
  continue;
56
68
  }
57
69
  if (pollResponse.status === 403) {
58
- throw new Error("Authorization was denied");
70
+ throw new AIError.OAuthError("Authorization was denied", { kind: "device-auth", provider: "kilo" });
59
71
  }
60
72
  if (pollResponse.status === 410) {
61
- throw new Error("Authorization code expired. Please try again.");
73
+ throw new AIError.OAuthError("Authorization code expired. Please try again.", {
74
+ kind: "device-auth",
75
+ provider: "kilo",
76
+ });
62
77
  }
63
78
  if (!pollResponse.ok) {
64
- throw new Error(`Failed to poll device authorization: ${pollResponse.status}`);
79
+ throw new AIError.OAuthError(`Failed to poll device authorization: ${pollResponse.status}`, {
80
+ kind: "polling",
81
+ provider: "kilo",
82
+ status: pollResponse.status,
83
+ });
65
84
  }
66
85
 
67
86
  const pollData = (await pollResponse.json()) as KiloDeviceAuthPollResponse;
@@ -73,16 +92,19 @@ export async function loginKilo(callbacks: OAuthController): Promise<OAuthCreden
73
92
  };
74
93
  }
75
94
  if (pollData.status === "denied") {
76
- throw new Error("Authorization was denied");
95
+ throw new AIError.OAuthError("Authorization was denied", { kind: "device-auth", provider: "kilo" });
77
96
  }
78
97
  if (pollData.status === "expired") {
79
- throw new Error("Authorization code expired. Please try again.");
98
+ throw new AIError.OAuthError("Authorization code expired. Please try again.", {
99
+ kind: "device-auth",
100
+ provider: "kilo",
101
+ });
80
102
  }
81
103
 
82
104
  await Bun.sleep(POLL_INTERVAL_MS);
83
105
  }
84
106
 
85
- throw new Error("Authentication timed out. Please try again.");
107
+ throw new AIError.OAuthError("Authentication timed out. Please try again.", { kind: "timeout", provider: "kilo" });
86
108
  }
87
109
 
88
110
  export const kiloProvider = {
@@ -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://docs.litellm.ai/docs/proxy/deploy";
11
12
  */
12
13
  export async function loginLiteLLM(options: OAuthController): Promise<string> {
13
14
  if (!options.onPrompt) {
14
- throw new Error("LiteLLM login requires onPrompt callback");
15
+ throw new AIError.OnPromptRequiredError("LiteLLM");
15
16
  }
16
17
 
17
18
  options.onAuth?.({
@@ -26,12 +27,12 @@ export async function loginLiteLLM(options: OAuthController): Promise<string> {
26
27
  });
27
28
 
28
29
  if (options.signal?.aborted) {
29
- throw new Error("Login cancelled");
30
+ throw new AIError.LoginCancelledError();
30
31
  }
31
32
 
32
33
  const trimmed = apiKey.trim();
33
34
  if (!trimmed) {
34
- throw new Error("API key is required");
35
+ throw new AIError.ApiKeyRequiredError();
35
36
  }
36
37
 
37
38
  return trimmed;
@@ -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
 
@@ -8,7 +9,7 @@ const DEFAULT_LOCAL_TOKEN = "llama-cpp-local";
8
9
 
9
10
  export async function loginLlamaCpp(options: OAuthController): Promise<string> {
10
11
  if (!options.onPrompt) {
11
- throw new Error(`${PROVIDER_ID} login requires onPrompt callback`);
12
+ throw new AIError.OnPromptRequiredError(PROVIDER_ID);
12
13
  }
13
14
  options.onAuth?.({
14
15
  url: AUTH_URL,
@@ -20,7 +21,7 @@ export async function loginLlamaCpp(options: OAuthController): Promise<string> {
20
21
  allowEmpty: true,
21
22
  });
22
23
  if (options.signal?.aborted) {
23
- throw new Error("Login cancelled");
24
+ throw new AIError.LoginCancelledError();
24
25
  }
25
26
  const trimmed = apiKey.trim();
26
27
  return trimmed || DEFAULT_LOCAL_TOKEN;
@@ -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
 
@@ -6,7 +7,7 @@ export const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
6
7
 
7
8
  export async function loginLmStudio(options: OAuthController): Promise<string> {
8
9
  if (!options.onPrompt) {
9
- throw new Error(`${PROVIDER_ID} login requires onPrompt callback`);
10
+ throw new AIError.OnPromptRequiredError(PROVIDER_ID);
10
11
  }
11
12
 
12
13
  const apiKey = await options.onPrompt({
@@ -16,7 +17,7 @@ export async function loginLmStudio(options: OAuthController): Promise<string> {
16
17
  });
17
18
 
18
19
  if (options.signal?.aborted) {
19
- throw new Error("Login cancelled");
20
+ throw new AIError.LoginCancelledError();
20
21
  }
21
22
 
22
23
  const trimmed = apiKey.trim();
@@ -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";
@@ -9,7 +10,7 @@ const PROVIDER_ID = "nvidia";
9
10
 
10
11
  export async function loginNvidia(options: OAuthController): Promise<string> {
11
12
  if (!options.onPrompt) {
12
- throw new Error("NVIDIA login requires onPrompt callback");
13
+ throw new AIError.OnPromptRequiredError("NVIDIA");
13
14
  }
14
15
 
15
16
  options.onAuth?.({
@@ -23,12 +24,12 @@ export async function loginNvidia(options: OAuthController): Promise<string> {
23
24
  });
24
25
 
25
26
  if (options.signal?.aborted) {
26
- throw new Error("Login cancelled");
27
+ throw new AIError.LoginCancelledError();
27
28
  }
28
29
 
29
30
  const trimmed = apiKey.trim();
30
31
  if (!trimmed) {
31
- throw new Error("API key is required");
32
+ throw new AIError.ApiKeyRequiredError();
32
33
  }
33
34
 
34
35
  options.onProgress?.("Validating API key (optional)...");
@@ -42,10 +43,9 @@ export async function loginNvidia(options: OAuthController): Promise<string> {
42
43
  fetch: options.fetch,
43
44
  });
44
45
  } catch (error) {
45
- const message = error instanceof Error ? error.message : String(error);
46
- const statusMatch = message.match(/\((\d{3})\)/);
47
- const statusCode = statusMatch?.[1];
48
- if (statusCode === "401" || statusCode === "403") {
46
+ // A real auth rejection (401/403) is fatal; any other validation-endpoint
47
+ // failure is non-fatal — skip validation and trust the supplied key.
48
+ if (AIError.is(AIError.classify(error), AIError.Flag.AuthFailed)) {
49
49
  throw error;
50
50
  }
51
51
  options.onProgress?.("Skipping NVIDIA validation endpoint; continuing with provided API key.");
@@ -2,6 +2,7 @@
2
2
  * Anthropic OAuth flow (Claude Pro/Max)
3
3
  */
4
4
 
5
+ import * as AIError from "../../error";
5
6
  import { claudeCodeVersion } from "../../providers/anthropic";
6
7
  import type { FetchImpl } from "../../types";
7
8
  import { OAuthCallbackFlow } from "./callback-server";
@@ -59,7 +60,10 @@ async function postJson(
59
60
 
60
61
  const responseBody = await response.text();
61
62
  if (!response.ok) {
62
- throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
63
+ throw new AIError.ProviderHttpError(
64
+ `HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`,
65
+ response.status,
66
+ );
63
67
  }
64
68
  return responseBody;
65
69
  }
@@ -88,8 +92,9 @@ function parseOAuthTokenResponse(responseBody: string, operation: string): Anthr
88
92
  try {
89
93
  return JSON.parse(responseBody) as AnthropicTokenResponse;
90
94
  } catch (error) {
91
- throw new Error(
95
+ throw new AIError.OAuthError(
92
96
  `Anthropic ${operation} returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`,
97
+ { kind: "validation", provider: "anthropic", cause: error },
93
98
  );
94
99
  }
95
100
  }
@@ -131,14 +136,18 @@ async function fetchBootstrapIdentity(
131
136
  });
132
137
  const responseBody = await response.text();
133
138
  if (!response.ok) {
134
- throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
139
+ throw new AIError.ProviderHttpError(
140
+ `HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`,
141
+ response.status,
142
+ );
135
143
  }
136
144
  let data: AnthropicBootstrapResponse;
137
145
  try {
138
146
  data = JSON.parse(responseBody) as AnthropicBootstrapResponse;
139
147
  } catch (error) {
140
- throw new Error(
148
+ throw new AIError.OAuthError(
141
149
  `Anthropic bootstrap returned invalid JSON. url=${url}; body=${responseBody}; details=${formatErrorDetails(error)}`,
150
+ { kind: "validation", provider: "anthropic", cause: error },
142
151
  );
143
152
  }
144
153
  const accountUuid = data.oauth_account?.account_uuid;
@@ -227,8 +236,9 @@ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
227
236
  this.#fetch,
228
237
  );
229
238
  } catch (error) {
230
- throw new Error(
239
+ throw new AIError.OAuthError(
231
240
  `Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`,
241
+ { kind: "token-exchange", provider: "anthropic", cause: error },
232
242
  );
233
243
  }
234
244
 
@@ -278,7 +288,14 @@ export async function refreshAnthropicToken(
278
288
  },
279
289
  );
280
290
  } catch (error) {
281
- throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`);
291
+ throw new AIError.OAuthError(
292
+ `Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`,
293
+ {
294
+ kind: "token-refresh",
295
+ provider: "anthropic",
296
+ cause: error,
297
+ },
298
+ );
282
299
  }
283
300
 
284
301
  const data = parseOAuthTokenResponse(responseBody, "token refresh");
@@ -10,6 +10,7 @@
10
10
  * - generateAuthUrl(): Build provider-specific authorization URL
11
11
  * - exchangeToken(): Exchange authorization code for tokens
12
12
  */
13
+ import * as AIError from "../../error";
13
14
  import templateHtml from "./oauth.html" with { type: "text" };
14
15
  import type { OAuthController, OAuthCredentials } from "./types";
15
16
 
@@ -127,7 +128,7 @@ export abstract class OAuthCallbackFlow {
127
128
  return { server, redirectUri };
128
129
  } catch {
129
130
  if (this.redirectUri) {
130
- throw new Error(
131
+ throw new AIError.ConfigurationError(
131
132
  `OAuth callback port ${this.preferredPort} unavailable; cannot fall back to a random port when oauth.redirectUri is set`,
132
133
  );
133
134
  }
@@ -143,8 +144,9 @@ export abstract class OAuthCallbackFlow {
143
144
  * Create HTTP server for OAuth callback.
144
145
  */
145
146
  #createServer(port: number, expectedState: string): Bun.Server<unknown> {
147
+ const hostname = this.callbackHostname === DEFAULT_HOSTNAME ? undefined : this.callbackHostname;
146
148
  return Bun.serve({
147
- hostname: this.callbackHostname,
149
+ ...(hostname === undefined ? {} : { hostname }),
148
150
  port,
149
151
  reusePort: false,
150
152
  fetch: req => this.#handleCallback(req, expectedState),
@@ -214,7 +216,7 @@ export abstract class OAuthCallbackFlow {
214
216
  signal.addEventListener("abort", () => {
215
217
  this.#callbackResolve = undefined;
216
218
  this.#callbackReject = undefined;
217
- reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
219
+ reject(new AIError.LoginCancelledError(`OAuth callback cancelled: ${signal.reason}`));
218
220
  });
219
221
  });
220
222
 
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../../error";
1
2
  import { generatePKCE } from "./pkce";
2
3
  import type { OAuthCredentials } from "./types";
3
4
 
@@ -63,16 +64,26 @@ export async function pollCursorAuth(
63
64
  };
64
65
  }
65
66
 
66
- throw new Error(`Poll failed: ${response.status}`);
67
+ throw new AIError.OAuthError(`Poll failed: ${response.status}`, {
68
+ kind: "polling",
69
+ provider: "cursor",
70
+ status: response.status,
71
+ });
67
72
  } catch {
68
73
  consecutiveErrors++;
69
74
  if (consecutiveErrors >= 3) {
70
- throw new Error("Too many consecutive errors during Cursor auth polling");
75
+ throw new AIError.OAuthError("Too many consecutive errors during Cursor auth polling", {
76
+ kind: "polling",
77
+ provider: "cursor",
78
+ });
71
79
  }
72
80
  }
73
81
  }
74
82
 
75
- throw new Error("Cursor authentication polling timeout");
83
+ throw new AIError.OAuthError("Cursor authentication polling timeout", {
84
+ kind: "timeout",
85
+ provider: "cursor",
86
+ });
76
87
  }
77
88
 
78
89
  export async function loginCursor(
@@ -107,7 +118,10 @@ export async function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<
107
118
 
108
119
  if (!response.ok) {
109
120
  const error = await response.text();
110
- throw new Error(`Cursor token refresh failed: ${error}`);
121
+ throw new AIError.OAuthError(`Cursor token refresh failed: ${error}`, {
122
+ kind: "token-refresh",
123
+ provider: "cursor",
124
+ });
111
125
  }
112
126
 
113
127
  const data = (await response.json()) as {
@@ -1,3 +1,4 @@
1
+ import * as AIError from "../../error";
1
2
  import { OAuthCallbackFlow } from "./callback-server";
2
3
  import { generatePKCE } from "./pkce";
3
4
  import type { OAuthController, OAuthCredentials } from "./types";
@@ -54,7 +55,10 @@ class DevinOAuthFlow extends OAuthCallbackFlow {
54
55
 
55
56
  async exchangeToken(code: string): Promise<OAuthCredentials> {
56
57
  if (!this.#pkce) {
57
- throw new Error("Devin PKCE verifier was not initialized");
58
+ throw new AIError.OAuthError("Devin PKCE verifier was not initialized", {
59
+ kind: "configuration",
60
+ provider: "devin",
61
+ });
58
62
  }
59
63
  const token = await exchangeDevinCliToken(code, this.#pkce.verifier, this.ctrl.fetch);
60
64
 
@@ -87,12 +91,19 @@ export async function exchangeDevinCliToken(
87
91
 
88
92
  if (!response.ok) {
89
93
  const error = await response.text();
90
- throw new Error(`Devin CLI token exchange failed: ${response.status} ${error}`.trim());
94
+ throw new AIError.OAuthError(`Devin CLI token exchange failed: ${response.status} ${error}`.trim(), {
95
+ kind: "token-exchange",
96
+ provider: "devin",
97
+ status: response.status,
98
+ });
91
99
  }
92
100
 
93
101
  const data = (await response.json()) as { token?: unknown };
94
102
  if (typeof data.token !== "string" || data.token.length === 0) {
95
- throw new Error("Devin CLI token exchange returned an empty token");
103
+ throw new AIError.OAuthError("Devin CLI token exchange returned an empty token", {
104
+ kind: "validation",
105
+ provider: "devin",
106
+ });
96
107
  }
97
108
  return data.token;
98
109
  }