@oh-my-pi/pi-ai 17.4.0 → 17.4.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 (51) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +23 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/auth-retry.d.ts +8 -3
  5. package/dist/types/auth-storage.d.ts +9 -0
  6. package/dist/types/dialect/qwen-xml.d.ts +6 -0
  7. package/dist/types/error/auth-classify.d.ts +8 -8
  8. package/dist/types/error/oauth.d.ts +2 -1
  9. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  10. package/dist/types/providers/cursor-pi-args.d.ts +21 -0
  11. package/dist/types/providers/cursor.d.ts +23 -3
  12. package/dist/types/providers/google-types.d.ts +6 -0
  13. package/dist/types/providers/mock.d.ts +3 -1
  14. package/dist/types/providers/openai-shared.d.ts +6 -1
  15. package/dist/types/registry/oauth/google-antigravity.d.ts +2 -6
  16. package/dist/types/registry/oauth/index.d.ts +2 -3
  17. package/dist/types/registry/oauth/types.d.ts +2 -1
  18. package/dist/types/registry/oauth/xai-oauth.d.ts +2 -2
  19. package/dist/types/registry/registry.d.ts +1 -1
  20. package/dist/types/registry/types.d.ts +2 -1
  21. package/dist/types/registry/xai-oauth.d.ts +1 -1
  22. package/dist/types/types.d.ts +21 -0
  23. package/dist/types/utils/stream-markup-healing.d.ts +1 -1
  24. package/package.json +8 -6
  25. package/src/auth-retry.ts +41 -3
  26. package/src/auth-storage.ts +229 -75
  27. package/src/dialect/qwen-xml.ts +82 -0
  28. package/src/error/auth-classify.ts +10 -8
  29. package/src/error/oauth.ts +2 -1
  30. package/src/providers/amazon-bedrock.ts +12 -19
  31. package/src/providers/anthropic.ts +29 -35
  32. package/src/providers/cursor/exec-modern.ts +2 -0
  33. package/src/providers/cursor-pi-args.ts +38 -2
  34. package/src/providers/cursor.ts +357 -32
  35. package/src/providers/google-shared.ts +11 -12
  36. package/src/providers/google-types.ts +7 -0
  37. package/src/providers/mock.ts +4 -1
  38. package/src/providers/openai-codex-responses.ts +34 -8
  39. package/src/providers/openai-completions.ts +2 -2
  40. package/src/providers/openai-shared.ts +72 -14
  41. package/src/registry/oauth/google-antigravity.ts +247 -162
  42. package/src/registry/oauth/index.ts +3 -3
  43. package/src/registry/oauth/types.ts +2 -1
  44. package/src/registry/oauth/xai-oauth.ts +14 -7
  45. package/src/registry/types.ts +2 -1
  46. package/src/registry/xai-oauth.ts +2 -2
  47. package/src/stream.ts +8 -4
  48. package/src/types.ts +22 -0
  49. package/src/utils/schema/CONSTRAINTS.md +1 -0
  50. package/src/utils/schema/fields.ts +20 -4
  51. package/src/utils/stream-markup-healing.ts +9 -2
@@ -2,16 +2,17 @@
2
2
  * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
3
3
  * Uses different OAuth credentials than google-gemini-cli for access to additional models.
4
4
  */
5
- import { getAntigravityUserAgent, getAntigravityVersion } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
5
+ import { type } from "@oh-my-pi/omptype";
6
+ import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
6
7
  import * as AIError from "../../error";
8
+ import { raceWithSignal } from "../../utils/abort";
7
9
  import { oauthFetch, runGoogleOAuthLogin, throwIfLoginCancelled } from "./google-oauth-shared";
8
10
  import type { OAuthController, OAuthCredentials } from "./types";
9
11
 
10
- const decode = (s: string) => atob(s);
11
- const CLIENT_ID = decode(
12
+ const CLIENT_ID = atob(
12
13
  "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
13
14
  );
14
- const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
15
+ const CLIENT_SECRET = atob("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
15
16
  const CALLBACK_PORT = 51121;
16
17
  const CALLBACK_PATH = "/oauth-callback";
17
18
 
@@ -25,127 +26,250 @@ const SCOPES = [
25
26
 
26
27
  const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
27
28
  const TOKEN_URL = "https://oauth2.googleapis.com/token";
28
- const CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
29
- const DAILY_CLOUD_CODE_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
30
- const NODE_API_CLIENT_USER_AGENT = "google-api-nodejs-client/10.3.0";
31
- const GOOG_API_CLIENT_HEADER = "gl-node/22.21.1";
32
- const TIER_FREE = "free-tier";
33
- const PROJECT_ONBOARD_MAX_ATTEMPTS = 5;
34
- const PROJECT_ONBOARD_INTERVAL_MS = 2000;
35
- interface LoadCodeAssistPayload {
36
- cloudaicompanionProject?: string | { id?: string };
37
- currentTier?: { id?: string };
38
- allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
29
+ const CLOUD_CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
30
+ const LOAD_CODE_ASSIST_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;
31
+ const ONBOARD_USER_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`;
32
+ const OPERATIONS_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal`;
33
+ const FREE_TIER_ID = "free-tier";
34
+ const ONBOARD_TIMEOUT_MS = 30_000;
35
+ const ONBOARD_POLL_INTERVAL_MS = 1_000;
36
+ const PROVIDER = "google-antigravity";
37
+
38
+ /** Cloud Code Assist metadata sent by native Antigravity control-plane requests. */
39
+ export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
40
+ ideType: "ANTIGRAVITY",
41
+ });
42
+
43
+ interface CloudCodeContext {
44
+ headers: Record<string, string>;
45
+ signal?: AbortSignal;
39
46
  }
40
47
 
41
- interface LongRunningOperationResponse {
42
- done?: boolean;
43
- response?: {
44
- cloudaicompanionProject?: string | { id?: string };
45
- };
48
+ interface CloudCodeRequest extends CloudCodeContext {
49
+ label: string;
50
+ url: string;
51
+ method: "GET" | "POST";
52
+ body?: string;
53
+ timeoutMs?: number;
46
54
  }
47
55
 
48
- export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
49
- ideType: "ANTIGRAVITY",
56
+ const userTierSchema = type({
57
+ "id?": "string",
50
58
  });
51
59
 
52
- export interface AntigravityOnboardMetadata {
53
- ide_type: string;
54
- ide_version: string;
55
- ide_name: string;
60
+ const ineligibleTierSchema = type({
61
+ "tierId?": "string",
62
+ "reasonMessage?": "string",
63
+ "validationUrl?": "string",
64
+ });
65
+
66
+ const loadCodeAssistResponseSchema = type({
67
+ "currentTier?": userTierSchema.or("null"),
68
+ "paidTier?": userTierSchema.or("null"),
69
+ "allowedTiers?": userTierSchema.array(),
70
+ "ineligibleTiers?": ineligibleTierSchema.array(),
71
+ "cloudaicompanionProject?": "string",
72
+ });
73
+ type LoadCodeAssistResponse = typeof loadCodeAssistResponseSchema.infer;
74
+
75
+ const operationErrorSchema = type({
76
+ "code?": "number",
77
+ "message?": "string",
78
+ });
79
+ type OperationError = typeof operationErrorSchema.infer;
80
+
81
+ const onboardUserResponseSchema = type({
82
+ "@type": "string",
83
+ "cloudaicompanionProject?": "string",
84
+ });
85
+
86
+ const onboardOperationSchema = type({
87
+ "name?": "string",
88
+ "done?": "boolean",
89
+ "error?": operationErrorSchema.or("null"),
90
+ "response?": onboardUserResponseSchema.or("null"),
91
+ });
92
+ type OnboardOperation = typeof onboardOperationSchema.infer;
93
+
94
+ function parseLoadCodeAssistResponse(payload: unknown): LoadCodeAssistResponse {
95
+ const result = loadCodeAssistResponseSchema(payload);
96
+ if (result instanceof type.errors) {
97
+ throw new AIError.OAuthError(`failed to unmarshal LoadCodeAssistResponse: ${result.summary}`, {
98
+ kind: "provisioning",
99
+ provider: PROVIDER,
100
+ });
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function parseOnboardOperation(payload: unknown): OnboardOperation {
106
+ const result = onboardOperationSchema(payload);
107
+ if (result instanceof type.errors) {
108
+ throw new AIError.OAuthError(`failed to unmarshal OnboardUser operation: ${result.summary}`, {
109
+ kind: "provisioning",
110
+ provider: PROVIDER,
111
+ });
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function extractProjectId(payload: LoadCodeAssistResponse): string | undefined {
117
+ const projectId = payload.cloudaicompanionProject;
118
+ return projectId && projectId.length > 0 ? projectId : undefined;
119
+ }
120
+
121
+ function hasMessageField(payload: LoadCodeAssistResponse, field: "currentTier" | "paidTier"): boolean {
122
+ return payload[field] !== undefined && payload[field] !== null;
123
+ }
124
+
125
+ function isFreeTierAllowed(payload: LoadCodeAssistResponse): boolean {
126
+ return payload.allowedTiers?.some(tier => tier.id === FREE_TIER_ID) === true;
56
127
  }
57
128
 
58
- export function getAntigravityOnboardMetadata(): AntigravityOnboardMetadata {
129
+ function getFreeTierIneligibility(
130
+ payload: LoadCodeAssistResponse,
131
+ ): { reasonMessage: string; validationUrl: string | undefined } | undefined {
132
+ const tier = payload.ineligibleTiers?.find(candidate => candidate.tierId === FREE_TIER_ID);
133
+ if (!tier?.reasonMessage) return undefined;
59
134
  return {
60
- ide_type: "ANTIGRAVITY",
61
- ide_version: getAntigravityVersion(),
62
- ide_name: "antigravity",
135
+ reasonMessage: tier.reasonMessage,
136
+ validationUrl: tier.validationUrl && tier.validationUrl.length > 0 ? tier.validationUrl : undefined,
63
137
  };
64
138
  }
65
139
 
66
- function readProjectId(value: unknown): string | undefined {
67
- if (typeof value === "string" && value.length > 0) {
68
- return value.trim();
69
- }
70
- if (value && typeof value === "object" && "id" in value && typeof (value as { id?: unknown }).id === "string") {
71
- const id = (value as { id: string }).id.trim();
72
- if (id.length > 0) return id;
73
- }
74
- return undefined;
140
+ function assertFreeTierEligible(payload: LoadCodeAssistResponse): void {
141
+ if (isFreeTierAllowed(payload)) return;
142
+ const ineligibility = getFreeTierIneligibility(payload);
143
+ if (!ineligibility) return;
144
+ const validation = ineligibility.validationUrl ? `\n${ineligibility.validationUrl}` : "";
145
+ throw new AIError.OAuthError(`${ineligibility.reasonMessage}${validation}`, {
146
+ kind: "provisioning",
147
+ provider: PROVIDER,
148
+ });
75
149
  }
76
150
 
77
- function extractProjectId(payload: unknown): string | undefined {
78
- if (!payload || typeof payload !== "object") return undefined;
79
- const record = payload as Record<string, unknown>;
80
- for (const key of ["cloudaicompanionProject", "projectId", "project"]) {
81
- const id = readProjectId(record[key]);
82
- if (id) return id;
151
+ async function requestCloudCodeAssist({
152
+ label,
153
+ url,
154
+ method,
155
+ headers,
156
+ body,
157
+ signal,
158
+ timeoutMs,
159
+ }: CloudCodeRequest): Promise<unknown> {
160
+ throwIfLoginCancelled(signal);
161
+ const init: RequestInit = body === undefined ? { method, headers } : { method, headers, body };
162
+ const response = await oauthFetch(url, init, { provider: PROVIDER, signal, timeoutMs });
163
+ if (response.status !== 200) {
164
+ const errorText = await response.text();
165
+ throw new AIError.OAuthError(`${label} failed: ${response.status} ${response.statusText}: ${errorText}`, {
166
+ kind: "provisioning",
167
+ provider: PROVIDER,
168
+ status: response.status,
169
+ });
83
170
  }
84
- return undefined;
171
+ return response.json();
85
172
  }
86
173
 
87
- function getDefaultTierId(
88
- allowedTiers?: Array<{ id?: string; isDefault?: boolean }>,
89
- currentTier?: { id?: string },
90
- ): string {
91
- if (allowedTiers && allowedTiers.length > 0) {
92
- const defaultTier = allowedTiers.find(
93
- tier => tier.isDefault && typeof tier.id === "string" && tier.id.trim().length > 0,
94
- );
95
- if (defaultTier?.id) {
96
- return defaultTier.id.trim();
97
- }
98
- }
99
- if (currentTier && typeof currentTier.id === "string" && currentTier.id.trim().length > 0) {
100
- return currentTier.id.trim();
174
+ async function postLoadCodeAssist(
175
+ context: CloudCodeContext,
176
+ body: Record<string, unknown>,
177
+ ): Promise<LoadCodeAssistResponse> {
178
+ const payload = await requestCloudCodeAssist({
179
+ ...context,
180
+ label: "loadCodeAssist",
181
+ url: LOAD_CODE_ASSIST_URL,
182
+ method: "POST",
183
+ body: JSON.stringify(body),
184
+ });
185
+ return parseLoadCodeAssistResponse(payload);
186
+ }
187
+
188
+ async function loadCodeAssist(context: CloudCodeContext): Promise<LoadCodeAssistResponse> {
189
+ let payload = await postLoadCodeAssist(context, {
190
+ metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
191
+ });
192
+ const projectId = extractProjectId(payload);
193
+ if (!hasMessageField(payload, "paidTier") && projectId) {
194
+ payload = await postLoadCodeAssist(context, {
195
+ cloudaicompanionProject: projectId,
196
+ metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
197
+ });
101
198
  }
102
- return TIER_FREE;
199
+ return payload;
103
200
  }
104
201
 
105
- async function onboardProjectWithRetries(
106
- endpoint: string,
107
- headers: Record<string, string>,
108
- onboardBody: { tier_id: string; metadata: AntigravityOnboardMetadata },
109
- signal: AbortSignal | undefined,
110
- onProgress?: (message: string) => void,
111
- ): Promise<string> {
112
- for (let attempt = 1; attempt <= PROJECT_ONBOARD_MAX_ATTEMPTS; attempt += 1) {
113
- if (attempt > 1) {
114
- onProgress?.(`Waiting for project provisioning (attempt ${attempt}/${PROJECT_ONBOARD_MAX_ATTEMPTS})...`);
115
- throwIfLoginCancelled(signal);
116
- await Bun.sleep(PROJECT_ONBOARD_INTERVAL_MS);
117
- }
202
+ function remainingOnboardTime(deadline: number): number {
203
+ const remaining = deadline - Date.now();
204
+ if (remaining > 0) return remaining;
205
+ throw new AIError.OAuthError(`onboardUser timed out after ${ONBOARD_TIMEOUT_MS}ms`, {
206
+ kind: "timeout",
207
+ provider: PROVIDER,
208
+ });
209
+ }
118
210
 
119
- throwIfLoginCancelled(signal);
120
- const onboardResponse = await oauthFetch(
121
- `${endpoint}/v1internal:onboardUser`,
122
- { method: "POST", headers, body: JSON.stringify(onboardBody) },
123
- { provider: "google-antigravity", signal },
124
- );
211
+ function describeOperationError(error: OperationError): string {
212
+ if (error.message) {
213
+ return typeof error.code === "number" ? `${error.code}: ${error.message}` : error.message;
214
+ }
215
+ return JSON.stringify(error) ?? String(error);
216
+ }
125
217
 
126
- if (!onboardResponse.ok) {
127
- const errorText = await onboardResponse.text();
128
- throw new AIError.OAuthError(
129
- `onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`,
130
- { kind: "provisioning", provider: "google-antigravity", status: onboardResponse.status },
131
- );
132
- }
218
+ async function onboardUser(context: CloudCodeContext): Promise<void> {
219
+ const deadline = Date.now() + ONBOARD_TIMEOUT_MS;
220
+ let operation = parseOnboardOperation(
221
+ await requestCloudCodeAssist({
222
+ ...context,
223
+ label: "onboardUser",
224
+ url: ONBOARD_USER_URL,
225
+ method: "POST",
226
+ body: JSON.stringify({
227
+ tierId: FREE_TIER_ID,
228
+ metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
229
+ }),
230
+ timeoutMs: remainingOnboardTime(deadline),
231
+ }),
232
+ );
133
233
 
134
- const operation = (await onboardResponse.json()) as LongRunningOperationResponse;
135
- if (!operation.done) {
136
- continue;
234
+ while (true) {
235
+ if (operation.done === true) {
236
+ if (operation.error !== undefined && operation.error !== null) {
237
+ throw new AIError.OAuthError(`OnboardUser operation failed: ${describeOperationError(operation.error)}`, {
238
+ kind: "provisioning",
239
+ provider: PROVIDER,
240
+ });
241
+ }
242
+ if (operation.response === undefined || operation.response === null) {
243
+ throw new AIError.OAuthError("failed to unmarshal OnboardUserResponse", {
244
+ kind: "provisioning",
245
+ provider: PROVIDER,
246
+ });
247
+ }
248
+ return;
137
249
  }
138
250
 
139
- const projectId = extractProjectId(operation.response);
140
- if (projectId) {
141
- return projectId;
251
+ await raceWithSignal(
252
+ Bun.sleep(Math.min(ONBOARD_POLL_INTERVAL_MS, remainingOnboardTime(deadline))),
253
+ context.signal,
254
+ );
255
+ throwIfLoginCancelled(context.signal);
256
+ const operationName = operation.name ?? "";
257
+ if (operationName.length === 0) {
258
+ throw new AIError.OAuthError("onboardUser returned an operation without a name", {
259
+ kind: "provisioning",
260
+ provider: PROVIDER,
261
+ });
142
262
  }
263
+ operation = parseOnboardOperation(
264
+ await requestCloudCodeAssist({
265
+ ...context,
266
+ label: "onboardUser operation",
267
+ url: `${OPERATIONS_URL}/${operationName}`,
268
+ method: "GET",
269
+ timeoutMs: remainingOnboardTime(deadline),
270
+ }),
271
+ );
143
272
  }
144
-
145
- throw new AIError.OAuthError(
146
- `onboardUser did not return a provisioned project id after ${PROJECT_ONBOARD_MAX_ATTEMPTS} attempts`,
147
- { kind: "provisioning", provider: "google-antigravity" },
148
- );
149
273
  }
150
274
 
151
275
  async function discoverProject(
@@ -153,84 +277,45 @@ async function discoverProject(
153
277
  onProgress?: (message: string) => void,
154
278
  signal?: AbortSignal,
155
279
  ): Promise<string> {
156
- const headers = {
157
- Authorization: `Bearer ${accessToken}`,
158
- "Content-Type": "application/json",
159
- "User-Agent": getAntigravityUserAgent(),
280
+ const context: CloudCodeContext = {
281
+ headers: {
282
+ Authorization: `Bearer ${accessToken}`,
283
+ "Content-Type": "application/json",
284
+ "User-Agent": getAntigravityUserAgent(),
285
+ },
286
+ signal,
160
287
  };
161
288
 
162
- onProgress?.("Checking for existing project...");
289
+ onProgress?.("Checking Cloud Code Assist account status...");
163
290
  try {
164
- let lastErrorText: string | undefined;
165
- let lastStatus: number | undefined;
166
- let fallbackTierId = TIER_FREE;
167
- let loadedSuccessfully = false;
168
-
169
- for (const endpoint of [DAILY_CLOUD_CODE_ENDPOINT, CLOUD_CODE_ENDPOINT]) {
170
- throwIfLoginCancelled(signal);
171
- const loadResponse = await oauthFetch(
172
- `${endpoint}/v1internal:loadCodeAssist`,
173
- {
174
- method: "POST",
175
- headers,
176
- body: JSON.stringify({
177
- metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
178
- }),
179
- },
180
- { provider: "google-antigravity", signal },
181
- );
182
-
183
- if (!loadResponse.ok) {
184
- lastStatus = loadResponse.status;
185
- lastErrorText = await loadResponse.text();
186
- continue;
187
- }
188
-
189
- loadedSuccessfully = true;
190
- const loadPayload = (await loadResponse.json()) as LoadCodeAssistPayload;
191
- const existingProject = extractProjectId(loadPayload);
192
- if (existingProject) {
193
- return existingProject;
194
- }
195
- fallbackTierId = getDefaultTierId(loadPayload.allowedTiers, loadPayload.currentTier);
196
- }
197
-
198
- if (!loadedSuccessfully && lastStatus !== undefined) {
199
- throw new AIError.OAuthError(`loadCodeAssist failed: ${lastStatus}: ${lastErrorText || "unknown error"}`, {
200
- kind: "discovery",
201
- status: lastStatus,
202
- });
291
+ const initial = await loadCodeAssist(context);
292
+ assertFreeTierEligible(initial);
293
+ if (!hasMessageField(initial, "currentTier")) {
294
+ onProgress?.("Provisioning the Antigravity free tier...");
295
+ await onboardUser(context);
203
296
  }
204
297
 
205
- onProgress?.("Provisioning project...");
206
- const onboardBody = {
207
- tier_id: fallbackTierId,
208
- metadata: getAntigravityOnboardMetadata(),
209
- };
210
- const onboardHeaders: Record<string, string> = {
211
- ...headers,
212
- "User-Agent": `${headers["User-Agent"]} ${NODE_API_CLIENT_USER_AGENT}`,
213
- "X-Goog-Api-Client": GOOG_API_CLIENT_HEADER,
214
- };
215
- const provisionedProject = await onboardProjectWithRetries(
216
- DAILY_CLOUD_CODE_ENDPOINT,
217
- onboardHeaders,
218
- onboardBody,
219
- signal,
220
- onProgress,
221
- );
222
- return provisionedProject;
298
+ onProgress?.("Refreshing Cloud Code Assist project...");
299
+ const refreshed = await loadCodeAssist(context);
300
+ const projectId = extractProjectId(refreshed);
301
+ if (projectId) return projectId;
302
+ throw new AIError.OAuthError("loadCodeAssist did not return a cloudaicompanionProject", {
303
+ kind: "provisioning",
304
+ provider: PROVIDER,
305
+ });
223
306
  } catch (error) {
307
+ throwIfLoginCancelled(signal);
224
308
  if (error instanceof AIError.LoginCancelledError || error instanceof AIError.OAuthError) {
225
309
  throw error;
226
310
  }
227
311
  throw new AIError.OAuthError(
228
- `Could not discover or provision an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
229
- { kind: "discovery", provider: "google-antigravity", cause: error },
312
+ `Could not discover an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
313
+ { kind: "discovery", provider: PROVIDER, cause: error },
230
314
  );
231
315
  }
232
316
  }
233
317
 
318
+ /** Authenticate an Antigravity account and resolve its Cloud Code Assist project. */
234
319
  export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
235
320
  return runGoogleOAuthLogin(ctrl, {
236
321
  provider: "google-antigravity",
@@ -60,12 +60,12 @@ export function unregisterOAuthProviders(sourceId: string): void {
60
60
  }
61
61
 
62
62
  /**
63
- * Refresh token for any OAuth provider.
64
- * Saves the new credentials and returns the new access token.
63
+ * Refresh a built-in OAuth grant, cancelling provider work when refresh ownership ends.
65
64
  */
66
65
  export async function refreshOAuthToken(
67
66
  provider: OAuthProvider,
68
67
  credentials: OAuthCredentials,
68
+ signal?: AbortSignal,
69
69
  ): Promise<OAuthCredentials> {
70
70
  if (!credentials) {
71
71
  throw new AIError.OAuthError(`No OAuth credentials found for ${provider}`, {
@@ -82,7 +82,7 @@ export async function refreshOAuthToken(
82
82
  }
83
83
  // Providers without a real refresher (static bearer tokens / API keys that
84
84
  // don't expire) return the credentials unchanged.
85
- return def.refreshToken ? def.refreshToken(credentials) : credentials;
85
+ return def.refreshToken ? def.refreshToken(credentials, signal) : credentials;
86
86
  }
87
87
  function getPerplexityJwtExpiryMs(token: string): number | undefined {
88
88
  const parts = token.split(".");
@@ -89,7 +89,8 @@ export interface OAuthProviderInterface {
89
89
  readonly name: string;
90
90
  readonly sourceId?: string;
91
91
  login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
92
- refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
92
+ /** Refresh a stored grant; the signal bounds provider network work to refresh ownership. */
93
+ refreshToken?(credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
93
94
  getApiKey?(credentials: OAuthCredentials): string;
94
95
  /** Store resulting OAuth credentials under a different provider id. */
95
96
  readonly storeCredentialsAs?: string;
@@ -106,6 +106,7 @@ export function validateXAIBillingEndpoint(url: string, field: string = "billing
106
106
  async function xaiOAuthDiscovery(
107
107
  timeoutMs: number = DISCOVERY_TIMEOUT_MS,
108
108
  fetchOverride?: FetchImpl,
109
+ signal?: AbortSignal,
109
110
  ): Promise<XAIOAuthDiscovery> {
110
111
  const fetchImpl = fetchOverride ?? fetch;
111
112
  let response: Response;
@@ -113,7 +114,7 @@ async function xaiOAuthDiscovery(
113
114
  response = await fetchImpl(XAI_OAUTH_DISCOVERY_URL, {
114
115
  method: "GET",
115
116
  headers: { Accept: "application/json" },
116
- signal: AbortSignal.timeout(timeoutMs),
117
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
117
118
  });
118
119
  } catch (error) {
119
120
  throw new AIError.OAuthError(
@@ -482,7 +483,7 @@ async function pollXAIDeviceToken(
482
483
  /** Log in to xAI Grok with the RFC 8628 device authorization grant. */
483
484
  export async function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredentials> {
484
485
  const fetchImpl = ctrl.fetch ?? fetch;
485
- const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl);
486
+ const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl, ctrl.signal);
486
487
  const device = await requestXAIDeviceAuthorization(fetchImpl, ctrl.signal);
487
488
  ctrl.onAuth?.({
488
489
  url: device.verificationUriComplete,
@@ -503,15 +504,19 @@ export async function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredent
503
504
  * Refresh an xAI OAuth access token using a stored refresh_token.
504
505
  *
505
506
  * Re-runs OIDC discovery and re-validates the token endpoint before sending
506
- * the stored refresh token.
507
+ * the stored refresh token. Caller cancellation aborts every network request.
507
508
  */
508
- export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials> {
509
+ export async function refreshXAIOAuthToken(
510
+ refreshToken: string,
511
+ fetchOverride?: FetchImpl,
512
+ signal?: AbortSignal,
513
+ ): Promise<OAuthCredentials> {
509
514
  const fetchImpl = fetchOverride ?? fetch;
510
515
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
511
516
  throw new AIError.OAuthError("missing refresh_token", { kind: "validation", provider: "xai" });
512
517
  }
513
518
 
514
- const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl);
519
+ const discovery = await xaiOAuthDiscovery(DISCOVERY_TIMEOUT_MS, fetchImpl, signal);
515
520
  const tokenEndpoint = validateXAIEndpoint(discovery.token_endpoint, "token_endpoint");
516
521
 
517
522
  const body = new URLSearchParams({
@@ -528,7 +533,9 @@ export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?:
528
533
  },
529
534
  body,
530
535
  redirect: "error",
531
- signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
536
+ signal: signal
537
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)])
538
+ : AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
532
539
  });
533
540
 
534
541
  if (!response.ok) {
@@ -555,5 +562,5 @@ export async function refreshXAIOAuthToken(refreshToken: string, fetchOverride?:
555
562
  );
556
563
  }
557
564
  const credentials = parseXAITokenResponse(payload, "xAI token refresh response", refreshToken);
558
- return withXAIOAuthIdentity(credentials, fetchImpl);
565
+ return withXAIOAuthIdentity(credentials, fetchImpl, signal);
559
566
  }
@@ -74,7 +74,8 @@ export interface ProviderDefinition {
74
74
  readonly prepareModelDiscovery?: ProviderModelDiscoveryPreparer;
75
75
  // --- interactive login (OAuthProviderInterface-compatible) ---
76
76
  readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
77
- readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
77
+ /** Refresh a stored grant; the signal bounds provider network work to refresh ownership. */
78
+ readonly refreshToken?: (credentials: OAuthCredentials, signal?: AbortSignal) => Promise<OAuthCredentials>;
78
79
  readonly getApiKey?: (credentials: OAuthCredentials) => string;
79
80
  /** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
80
81
  readonly storeCredentialsAs?: string;
@@ -9,9 +9,9 @@ export const xaiOauthProvider = {
9
9
  const { loginXAIOAuth } = await import("./oauth/xai-oauth");
10
10
  return loginXAIOAuth(cb);
11
11
  },
12
- refreshToken: async (credentials: OAuthCredentials) => {
12
+ refreshToken: async (credentials: OAuthCredentials, signal?: AbortSignal) => {
13
13
  // Lazy import: keep heavy OAuth flow modules out of the eager registry graph.
14
14
  const { refreshXAIOAuthToken } = await import("./oauth/xai-oauth");
15
- return refreshXAIOAuthToken(credentials.refresh);
15
+ return refreshXAIOAuthToken(credentials.refresh, undefined, signal);
16
16
  },
17
17
  } as const satisfies ProviderDefinition;
package/src/stream.ts CHANGED
@@ -20,7 +20,6 @@ import { getCustomApi } from "./api-registry";
20
20
  import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry";
21
21
  import * as AIError from "./error";
22
22
  import { ProviderHttpError } from "./error";
23
- import { isInvalidatedOAuthTokenError } from "./error/auth-classify";
24
23
  import { isConcurrencyCapExclusion, isUsageLimitOutcome } from "./error/rate-limit";
25
24
  import type { BedrockOptions } from "./providers/amazon-bedrock";
26
25
  import type { AnthropicOptions } from "./providers/anthropic";
@@ -1091,6 +1090,7 @@ function isRetryableUpstreamError(
1091
1090
  status: number | undefined,
1092
1091
  message: string | undefined,
1093
1092
  ): boolean {
1093
+ if (AIError.isAuthRetryableError(error)) return true;
1094
1094
  // 401 means the credential is bad; 403 is its valid-token twin (access
1095
1095
  // denied by plan, model policy, or org restriction — a sibling account may
1096
1096
  // not share it). Explicit account-scoped policy errors such as Codex
@@ -1107,9 +1107,6 @@ function isRetryableUpstreamError(
1107
1107
  // classify as RATE_LIMIT_EXCEEDED in `parseRateLimitReason` and stay in the
1108
1108
  // provider's own backoff layer instead of burning siblings.
1109
1109
  if (AIError.isCodexChatGPTAccountPolicyError(error, model.provider, model.id)) return true;
1110
- if (AIError.isAccountPolicyError(error)) return true;
1111
- if (AIError.isUsageLimit(error)) return true;
1112
- if (isInvalidatedOAuthTokenError(error)) return true;
1113
1110
  if (status === 401 || (status === 403 && !isConcurrencyCapExclusion(status, message))) return true;
1114
1111
  return isUsageLimitOutcome(status, message);
1115
1112
  }
@@ -1926,6 +1923,7 @@ function mapOptionsForApi<TApi extends Api>(
1926
1923
  codexSseMaxAttempts: options?.codexSseMaxAttempts,
1927
1924
  providerSessionState: options?.providerSessionState,
1928
1925
  maxInFlightRequests: options?.maxInFlightRequests,
1926
+ toolNamespacesInfo: options?.toolNamespacesInfo,
1929
1927
  onPayload: options?.onPayload,
1930
1928
  onResponse: options?.onResponse,
1931
1929
  onSseEvent: options?.onSseEvent,
@@ -2311,10 +2309,16 @@ function mapOptionsForApi<TApi extends Api>(
2311
2309
  case "cursor-agent": {
2312
2310
  const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
2313
2311
  const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
2312
+ const cursorModel = model as Model<"cursor-agent">;
2313
+ const effort =
2314
+ options?.reasoning && !options.disableReasoning && !options.forceReasoningOff && cursorModel.reasoning
2315
+ ? requireSupportedEffort(cursorModel, options.reasoning)
2316
+ : undefined;
2314
2317
  return castApi<"cursor-agent">({
2315
2318
  ...base,
2316
2319
  execHandlers,
2317
2320
  onToolResult,
2321
+ wireModelId: resolveWireModelId(cursorModel, effort),
2318
2322
  });
2319
2323
  }
2320
2324