@oh-my-pi/pi-ai 16.2.1 → 16.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +3 -2
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/usage/openai-codex-base-url.d.ts +17 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/stream-markup-healing.d.ts +14 -5
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +40 -30
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +50 -24
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/usage/openai-codex-base-url.ts +30 -10
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/stream-markup-healing.ts +41 -18
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
41
|
+
throw new AIError.LoginCancelledError();
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const trimmed = apiKey.trim();
|
|
44
45
|
if (!trimmed) {
|
|
45
|
-
throw new
|
|
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
|
|
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)
|
|
98
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
58
|
+
throw new AIError.OAuthError(`Invalid xAI ${field}: ${url}`, { kind: "validation", provider: "xai" });
|
|
58
59
|
}
|
|
59
60
|
if (parsed.protocol !== "https:") {
|
|
60
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
170
|
+
throw new AIError.LoginCancelledError();
|
|
159
171
|
}
|
|
160
172
|
const trimmed = apiKey.trim();
|
|
161
173
|
if (!trimmed) {
|
|
162
|
-
throw new
|
|
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
|
|
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
|
|
201
|
+
throw new AIError.LoginCancelledError();
|
|
190
202
|
}
|
|
191
203
|
const trimmed = apiKey.trim();
|
|
192
204
|
if (!trimmed) {
|
|
193
|
-
throw new
|
|
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
|
|
9
|
+
throw new AIError.LoginCancelledError();
|
|
9
10
|
}
|
|
10
11
|
if (!options.onPrompt) {
|
|
11
|
-
throw new
|
|
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
|
|
23
|
+
throw new AIError.LoginCancelledError();
|
|
23
24
|
}
|
|
24
25
|
const trimmed = apiKey.trim();
|
|
25
26
|
if (!trimmed) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new AIError.ApiKeyRequiredError("Ollama Cloud API key is required");
|
|
27
28
|
}
|
|
28
29
|
return trimmed;
|
|
29
30
|
}
|
package/src/registry/ollama.ts
CHANGED
|
@@ -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
|
|
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
|
|
33
|
+
throw new AIError.LoginCancelledError();
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return apiKey.trim();
|
package/src/registry/parallel.ts
CHANGED
|
@@ -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
|
|
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
|
|
29
|
+
throw new AIError.LoginCancelledError();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const trimmed = apiKey.trim();
|
|
32
33
|
if (!trimmed) {
|
|
33
|
-
throw new
|
|
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
|
|
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
|
|
26
|
+
throw new AIError.LoginCancelledError();
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const trimmed = token.trim();
|
|
29
30
|
if (!trimmed) {
|
|
30
|
-
throw new
|
|
31
|
+
throw new AIError.ApiKeyRequiredError("Qwen token/API key is required");
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
options.onProgress?.("Validating credentials...");
|
package/src/registry/tavily.ts
CHANGED
|
@@ -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
|
|
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
|
|
29
|
+
throw new AIError.LoginCancelledError();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const trimmed = apiKey.trim();
|
|
32
33
|
if (!trimmed) {
|
|
33
|
-
throw new
|
|
34
|
+
throw new AIError.ApiKeyRequiredError();
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
return trimmed;
|