@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.
- package/CHANGELOG.md +27 -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 +1 -0
- 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/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/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 +34 -29
- 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 +46 -22
- 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/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/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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|