opencode-gemini-auth 1.3.10 → 1.3.11
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/README.md +32 -0
- package/package.json +4 -1
- package/src/plugin/auth.test.ts +58 -0
- package/src/plugin/oauth-authorize.ts +198 -0
- package/src/plugin/project/api.ts +209 -0
- package/src/plugin/project/context.ts +187 -0
- package/src/plugin/project/index.ts +6 -0
- package/src/plugin/project/types.ts +55 -0
- package/src/plugin/project/utils.ts +120 -0
- package/src/plugin/request/identifiers.ts +100 -0
- package/src/plugin/request/index.ts +3 -0
- package/src/plugin/request/openai.ts +128 -0
- package/src/plugin/request/prepare.ts +190 -0
- package/src/plugin/request/response.ts +191 -0
- package/src/plugin/request/shared.ts +72 -0
- package/src/plugin/{request-helpers.ts → request-helpers/errors.ts} +34 -213
- package/src/plugin/request-helpers/index.ts +12 -0
- package/src/plugin/request-helpers/parsing.ts +44 -0
- package/src/plugin/request-helpers/thinking.ts +36 -0
- package/src/plugin/request-helpers/types.ts +78 -0
- package/src/plugin/request-helpers.test.ts +84 -0
- package/src/plugin/request.test.ts +91 -0
- package/src/plugin/retry/helpers.ts +175 -0
- package/src/plugin/retry/index.ts +81 -0
- package/src/plugin/retry/quota.ts +210 -0
- package/src/plugin/retry.test.ts +106 -0
- package/src/plugin/token.test.ts +31 -0
- package/src/plugin/token.ts +24 -0
- package/src/plugin.ts +102 -588
- package/src/plugin/project.ts +0 -551
- package/src/plugin/request.ts +0 -483
package/src/plugin.ts
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { GEMINI_PROVIDER_ID, GEMINI_REDIRECT_URI } from "./constants";
|
|
4
|
-
import {
|
|
5
|
-
authorizeGemini,
|
|
6
|
-
exchangeGeminiWithVerifier,
|
|
7
|
-
} from "./gemini/oauth";
|
|
8
|
-
import type { GeminiTokenExchangeResult } from "./gemini/oauth";
|
|
1
|
+
import { GEMINI_PROVIDER_ID } from "./constants";
|
|
2
|
+
import { createOAuthAuthorizeMethod } from "./plugin/oauth-authorize";
|
|
9
3
|
import { accessTokenExpired, isOAuthAuth } from "./plugin/auth";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
resolveProjectContextFromAccessToken,
|
|
13
|
-
} from "./plugin/project";
|
|
4
|
+
import { resolveCachedAuth } from "./plugin/cache";
|
|
5
|
+
import { ensureProjectContext, retrieveUserQuota } from "./plugin/project";
|
|
14
6
|
import { isGeminiDebugEnabled, logGeminiDebugMessage, startGeminiDebugRequest } from "./plugin/debug";
|
|
15
7
|
import {
|
|
16
8
|
isGenerativeLanguageRequest,
|
|
17
9
|
prepareGeminiRequest,
|
|
18
10
|
transformGeminiResponse,
|
|
19
11
|
} from "./plugin/request";
|
|
12
|
+
import { fetchWithRetry } from "./plugin/retry";
|
|
20
13
|
import { refreshAccessToken } from "./plugin/token";
|
|
21
|
-
import { startOAuthListener, type OAuthListener } from "./plugin/server";
|
|
22
14
|
import type {
|
|
23
15
|
GetAuth,
|
|
24
16
|
LoaderResult,
|
|
25
17
|
OAuthAuthDetails,
|
|
18
|
+
PluginClient,
|
|
26
19
|
PluginContext,
|
|
27
20
|
PluginResult,
|
|
28
|
-
ProjectContextResult,
|
|
29
21
|
Provider,
|
|
30
22
|
} from "./plugin/types";
|
|
31
23
|
|
|
@@ -44,29 +36,8 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
44
36
|
return null;
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
50
|
-
: undefined;
|
|
51
|
-
const projectIdFromConfig =
|
|
52
|
-
providerOptions && typeof providerOptions.projectId === "string"
|
|
53
|
-
? providerOptions.projectId.trim()
|
|
54
|
-
: "";
|
|
55
|
-
const projectIdFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
|
|
56
|
-
const googleProjectIdFromEnv =
|
|
57
|
-
process.env.GOOGLE_CLOUD_PROJECT?.trim() ??
|
|
58
|
-
process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() ??
|
|
59
|
-
"";
|
|
60
|
-
const configuredProjectId =
|
|
61
|
-
projectIdFromEnv || projectIdFromConfig || googleProjectIdFromEnv || undefined;
|
|
62
|
-
|
|
63
|
-
if (provider.models) {
|
|
64
|
-
for (const model of Object.values(provider.models)) {
|
|
65
|
-
if (model) {
|
|
66
|
-
model.cost = { input: 0, output: 0 };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
39
|
+
const configuredProjectId = resolveConfiguredProjectId(provider);
|
|
40
|
+
normalizeProviderModelCosts(provider);
|
|
70
41
|
|
|
71
42
|
return {
|
|
72
43
|
apiKey: "",
|
|
@@ -80,7 +51,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
80
51
|
return fetch(input, init);
|
|
81
52
|
}
|
|
82
53
|
|
|
83
|
-
let authRecord = latestAuth;
|
|
54
|
+
let authRecord = resolveCachedAuth(latestAuth);
|
|
84
55
|
if (accessTokenExpired(authRecord)) {
|
|
85
56
|
const refreshed = await refreshAccessToken(authRecord, client);
|
|
86
57
|
if (!refreshed) {
|
|
@@ -89,53 +60,46 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
89
60
|
authRecord = refreshed;
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
|
|
93
|
-
if (!accessToken) {
|
|
63
|
+
if (!authRecord.access) {
|
|
94
64
|
return fetch(input, init);
|
|
95
65
|
}
|
|
96
66
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const projectContext = await resolveProjectContext();
|
|
112
|
-
|
|
113
|
-
const {
|
|
114
|
-
request,
|
|
115
|
-
init: transformedInit,
|
|
116
|
-
streaming,
|
|
117
|
-
requestedModel,
|
|
118
|
-
} = prepareGeminiRequest(
|
|
67
|
+
const projectContext = await ensureProjectContextOrThrow(
|
|
68
|
+
authRecord,
|
|
69
|
+
client,
|
|
70
|
+
configuredProjectId,
|
|
71
|
+
);
|
|
72
|
+
await maybeLogAvailableQuotaModels(
|
|
73
|
+
authRecord.access,
|
|
74
|
+
projectContext.effectiveProjectId,
|
|
75
|
+
);
|
|
76
|
+
const transformed = prepareGeminiRequest(
|
|
119
77
|
input,
|
|
120
78
|
init,
|
|
121
|
-
|
|
79
|
+
authRecord.access,
|
|
122
80
|
projectContext.effectiveProjectId,
|
|
123
81
|
);
|
|
124
|
-
|
|
125
|
-
const originalUrl = toUrlString(input);
|
|
126
|
-
const resolvedUrl = toUrlString(request);
|
|
127
82
|
const debugContext = startGeminiDebugRequest({
|
|
128
|
-
originalUrl,
|
|
129
|
-
resolvedUrl,
|
|
130
|
-
method:
|
|
131
|
-
headers:
|
|
132
|
-
body:
|
|
133
|
-
streaming,
|
|
83
|
+
originalUrl: toUrlString(input),
|
|
84
|
+
resolvedUrl: toUrlString(transformed.request),
|
|
85
|
+
method: transformed.init.method,
|
|
86
|
+
headers: transformed.init.headers,
|
|
87
|
+
body: transformed.init.body,
|
|
88
|
+
streaming: transformed.streaming,
|
|
134
89
|
projectId: projectContext.effectiveProjectId,
|
|
135
90
|
});
|
|
136
91
|
|
|
137
|
-
|
|
138
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Retry transport/429 failures while preserving the requested model.
|
|
94
|
+
* We intentionally do not auto-downgrade model tiers to avoid misleading users.
|
|
95
|
+
*/
|
|
96
|
+
const response = await fetchWithRetry(transformed.request, transformed.init);
|
|
97
|
+
return transformGeminiResponse(
|
|
98
|
+
response,
|
|
99
|
+
transformed.streaming,
|
|
100
|
+
debugContext,
|
|
101
|
+
transformed.requestedModel,
|
|
102
|
+
);
|
|
139
103
|
},
|
|
140
104
|
};
|
|
141
105
|
},
|
|
@@ -143,170 +107,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
143
107
|
{
|
|
144
108
|
label: "OAuth with Google (Gemini CLI)",
|
|
145
109
|
type: "oauth",
|
|
146
|
-
authorize:
|
|
147
|
-
const maybeHydrateProjectId = async (
|
|
148
|
-
result: GeminiTokenExchangeResult,
|
|
149
|
-
): Promise<GeminiTokenExchangeResult> => {
|
|
150
|
-
if (result.type !== "success") {
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const accessToken = result.access;
|
|
155
|
-
if (!accessToken) {
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const projectFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
|
|
160
|
-
const googleProjectFromEnv =
|
|
161
|
-
process.env.GOOGLE_CLOUD_PROJECT?.trim() ??
|
|
162
|
-
process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() ??
|
|
163
|
-
"";
|
|
164
|
-
const configuredProjectId =
|
|
165
|
-
projectFromEnv || googleProjectFromEnv || undefined;
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const authSnapshot = {
|
|
169
|
-
type: "oauth",
|
|
170
|
-
refresh: result.refresh,
|
|
171
|
-
access: result.access,
|
|
172
|
-
expires: result.expires,
|
|
173
|
-
} satisfies OAuthAuthDetails;
|
|
174
|
-
const projectContext = await resolveProjectContextFromAccessToken(
|
|
175
|
-
authSnapshot,
|
|
176
|
-
accessToken,
|
|
177
|
-
configuredProjectId,
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
if (projectContext.auth.refresh !== result.refresh) {
|
|
181
|
-
if (isGeminiDebugEnabled()) {
|
|
182
|
-
logGeminiDebugMessage(
|
|
183
|
-
`OAuth project resolved during auth: ${projectContext.effectiveProjectId || "none"}`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
return { ...result, refresh: projectContext.auth.refresh };
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (isGeminiDebugEnabled()) {
|
|
190
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
-
console.warn(`[Gemini OAuth] Project resolution skipped: ${message}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return result;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const isHeadless = !!(
|
|
199
|
-
process.env.SSH_CONNECTION ||
|
|
200
|
-
process.env.SSH_CLIENT ||
|
|
201
|
-
process.env.SSH_TTY ||
|
|
202
|
-
process.env.OPENCODE_HEADLESS
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
let listener: OAuthListener | null = null;
|
|
206
|
-
if (!isHeadless) {
|
|
207
|
-
try {
|
|
208
|
-
listener = await startOAuthListener();
|
|
209
|
-
} catch (error) {
|
|
210
|
-
if (error instanceof Error) {
|
|
211
|
-
console.log(
|
|
212
|
-
`Warning: Couldn't start the local callback listener (${error.message}). You'll need to paste the callback URL or authorization code.`,
|
|
213
|
-
);
|
|
214
|
-
} else {
|
|
215
|
-
console.log(
|
|
216
|
-
"Warning: Couldn't start the local callback listener. You'll need to paste the callback URL or authorization code.",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
console.log(
|
|
222
|
-
"Headless environment detected. You'll need to paste the callback URL or authorization code.",
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const authorization = await authorizeGemini();
|
|
227
|
-
if (!isHeadless) {
|
|
228
|
-
openBrowserUrl(authorization.url);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (listener) {
|
|
232
|
-
return {
|
|
233
|
-
url: authorization.url,
|
|
234
|
-
instructions:
|
|
235
|
-
"Complete the sign-in flow in your browser. We'll automatically detect the redirect back to localhost.",
|
|
236
|
-
method: "auto",
|
|
237
|
-
callback: async (): Promise<GeminiTokenExchangeResult> => {
|
|
238
|
-
try {
|
|
239
|
-
const callbackUrl = await listener.waitForCallback();
|
|
240
|
-
const code = callbackUrl.searchParams.get("code");
|
|
241
|
-
const state = callbackUrl.searchParams.get("state");
|
|
242
|
-
|
|
243
|
-
if (!code || !state) {
|
|
244
|
-
return {
|
|
245
|
-
type: "failed",
|
|
246
|
-
error: "Missing code or state in callback URL",
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (state !== authorization.state) {
|
|
251
|
-
return {
|
|
252
|
-
type: "failed",
|
|
253
|
-
error: "State mismatch in callback URL (possible CSRF attempt)",
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return await maybeHydrateProjectId(
|
|
258
|
-
await exchangeGeminiWithVerifier(code, authorization.verifier),
|
|
259
|
-
);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
return {
|
|
262
|
-
type: "failed",
|
|
263
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
264
|
-
};
|
|
265
|
-
} finally {
|
|
266
|
-
try {
|
|
267
|
-
await listener?.close();
|
|
268
|
-
} catch {
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
url: authorization.url,
|
|
277
|
-
instructions:
|
|
278
|
-
"Complete OAuth in your browser, then paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...) or just the authorization code.",
|
|
279
|
-
method: "code",
|
|
280
|
-
callback: async (callbackUrl: string): Promise<GeminiTokenExchangeResult> => {
|
|
281
|
-
try {
|
|
282
|
-
const { code, state } = parseOAuthCallbackInput(callbackUrl);
|
|
283
|
-
|
|
284
|
-
if (!code) {
|
|
285
|
-
return {
|
|
286
|
-
type: "failed",
|
|
287
|
-
error: "Missing authorization code in callback input",
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (state && state !== authorization.state) {
|
|
292
|
-
return {
|
|
293
|
-
type: "failed",
|
|
294
|
-
error: "State mismatch in callback input (possible CSRF attempt)",
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return await maybeHydrateProjectId(
|
|
299
|
-
await exchangeGeminiWithVerifier(code, authorization.verifier),
|
|
300
|
-
);
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return {
|
|
303
|
-
type: "failed",
|
|
304
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
},
|
|
110
|
+
authorize: createOAuthAuthorizeMethod(),
|
|
310
111
|
},
|
|
311
112
|
{
|
|
312
113
|
provider: GEMINI_PROVIDER_ID,
|
|
@@ -318,382 +119,95 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
318
119
|
});
|
|
319
120
|
|
|
320
121
|
export const GoogleOAuthPlugin = GeminiCLIOAuthPlugin;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
return value.toString();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function parseOAuthCallbackInput(input: string): { code?: string; state?: string } {
|
|
344
|
-
const trimmed = input.trim();
|
|
345
|
-
if (!trimmed) {
|
|
346
|
-
return {};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
350
|
-
try {
|
|
351
|
-
const url = new URL(trimmed);
|
|
352
|
-
return {
|
|
353
|
-
code: url.searchParams.get("code") || undefined,
|
|
354
|
-
state: url.searchParams.get("state") || undefined,
|
|
355
|
-
};
|
|
356
|
-
} catch {
|
|
357
|
-
return {};
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const candidate = trimmed.startsWith("?") ? trimmed.slice(1) : trimmed;
|
|
362
|
-
if (candidate.includes("=")) {
|
|
363
|
-
const params = new URLSearchParams(candidate);
|
|
364
|
-
const code = params.get("code") || undefined;
|
|
365
|
-
const state = params.get("state") || undefined;
|
|
366
|
-
if (code || state) {
|
|
367
|
-
return { code, state };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return { code: trimmed };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function openBrowserUrl(url: string): void {
|
|
375
|
-
try {
|
|
376
|
-
// Best-effort: don't block auth flow if spawning fails.
|
|
377
|
-
const platform = process.platform;
|
|
378
|
-
const command =
|
|
379
|
-
platform === "darwin"
|
|
380
|
-
? "open"
|
|
381
|
-
: platform === "win32"
|
|
382
|
-
? "rundll32"
|
|
383
|
-
: "xdg-open";
|
|
384
|
-
const args =
|
|
385
|
-
platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
386
|
-
const child = spawn(command, args, {
|
|
387
|
-
stdio: "ignore",
|
|
388
|
-
detached: true,
|
|
389
|
-
});
|
|
390
|
-
child.unref?.();
|
|
391
|
-
} catch {
|
|
392
|
-
}
|
|
122
|
+
const loggedQuotaModelsByProject = new Set<string>();
|
|
123
|
+
|
|
124
|
+
function resolveConfiguredProjectId(provider: Provider): string | undefined {
|
|
125
|
+
const providerOptions =
|
|
126
|
+
provider && typeof provider === "object"
|
|
127
|
+
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
128
|
+
: undefined;
|
|
129
|
+
const projectIdFromConfig =
|
|
130
|
+
providerOptions && typeof providerOptions.projectId === "string"
|
|
131
|
+
? providerOptions.projectId.trim()
|
|
132
|
+
: "";
|
|
133
|
+
const projectIdFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
|
|
134
|
+
const googleProjectIdFromEnv =
|
|
135
|
+
process.env.GOOGLE_CLOUD_PROJECT?.trim() ??
|
|
136
|
+
process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() ??
|
|
137
|
+
"";
|
|
138
|
+
|
|
139
|
+
return projectIdFromEnv || projectIdFromConfig || googleProjectIdFromEnv || undefined;
|
|
393
140
|
}
|
|
394
141
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
*/
|
|
399
|
-
async function fetchWithRetry(input: RequestInfo, init: RequestInit | undefined): Promise<Response> {
|
|
400
|
-
const maxRetries = DEFAULT_MAX_RETRIES;
|
|
401
|
-
const baseDelayMs = DEFAULT_BASE_DELAY_MS;
|
|
402
|
-
const maxDelayMs = DEFAULT_MAX_DELAY_MS;
|
|
403
|
-
|
|
404
|
-
if (!canRetryRequest(init)) {
|
|
405
|
-
return fetch(input, init);
|
|
142
|
+
function normalizeProviderModelCosts(provider: Provider): void {
|
|
143
|
+
if (!provider.models) {
|
|
144
|
+
return;
|
|
406
145
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const response = await fetch(input, init);
|
|
411
|
-
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt >= maxRetries) {
|
|
412
|
-
return response;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
let retryDelayMs: number | null = null;
|
|
416
|
-
if (response.status === 429) {
|
|
417
|
-
const quotaContext = await classifyQuotaResponse(response);
|
|
418
|
-
if (quotaContext?.terminal) {
|
|
419
|
-
return response;
|
|
420
|
-
}
|
|
421
|
-
retryDelayMs = quotaContext?.retryDelayMs ?? null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const delayMs = await getRetryDelayMs(
|
|
425
|
-
response,
|
|
426
|
-
attempt,
|
|
427
|
-
baseDelayMs,
|
|
428
|
-
maxDelayMs,
|
|
429
|
-
retryDelayMs,
|
|
430
|
-
);
|
|
431
|
-
if (!delayMs || delayMs <= 0) {
|
|
432
|
-
return response;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (init?.signal?.aborted) {
|
|
436
|
-
return response;
|
|
146
|
+
for (const model of Object.values(provider.models)) {
|
|
147
|
+
if (model) {
|
|
148
|
+
model.cost = { input: 0, output: 0 };
|
|
437
149
|
}
|
|
438
|
-
|
|
439
|
-
await wait(delayMs);
|
|
440
|
-
attempt += 1;
|
|
441
150
|
}
|
|
442
151
|
}
|
|
443
152
|
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const body = init.body;
|
|
450
|
-
if (typeof body === "string") {
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) {
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Resolves a retry delay from headers, body hints, or exponential backoff.
|
|
471
|
-
* Honors RetryInfo/Retry-After hints emitted by Code Assist.
|
|
472
|
-
*/
|
|
473
|
-
async function getRetryDelayMs(
|
|
474
|
-
response: Response,
|
|
475
|
-
attempt: number,
|
|
476
|
-
baseDelayMs: number,
|
|
477
|
-
maxDelayMs: number,
|
|
478
|
-
bodyDelayMs: number | null = null,
|
|
479
|
-
): Promise<number | null> {
|
|
480
|
-
const headerDelayMs = parseRetryAfterMs(response.headers.get("retry-after-ms"));
|
|
481
|
-
if (headerDelayMs !== null) {
|
|
482
|
-
return clampDelay(headerDelayMs, maxDelayMs);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
486
|
-
if (retryAfter !== null) {
|
|
487
|
-
return clampDelay(retryAfter, maxDelayMs);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const parsedBodyDelayMs = bodyDelayMs ?? (await parseRetryDelayFromBody(response));
|
|
491
|
-
if (parsedBodyDelayMs !== null) {
|
|
492
|
-
return clampDelay(parsedBodyDelayMs, maxDelayMs);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const fallback = baseDelayMs * Math.pow(2, attempt);
|
|
496
|
-
return clampDelay(fallback, maxDelayMs);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function clampDelay(delayMs: number, maxDelayMs: number): number {
|
|
500
|
-
if (!Number.isFinite(delayMs)) {
|
|
501
|
-
return maxDelayMs;
|
|
502
|
-
}
|
|
503
|
-
return Math.min(Math.max(0, delayMs), maxDelayMs);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function parseRetryAfterMs(value: string | null): number | null {
|
|
507
|
-
if (!value) {
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
const parsed = Number(value);
|
|
511
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
return parsed;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function parseRetryAfter(value: string | null): number | null {
|
|
518
|
-
if (!value) {
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
const trimmed = value.trim();
|
|
522
|
-
if (!trimmed) {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
const asNumber = Number(trimmed);
|
|
526
|
-
if (Number.isFinite(asNumber)) {
|
|
527
|
-
return Math.max(0, Math.round(asNumber * 1000));
|
|
528
|
-
}
|
|
529
|
-
const asDate = Date.parse(trimmed);
|
|
530
|
-
if (!Number.isNaN(asDate)) {
|
|
531
|
-
return Math.max(0, asDate - Date.now());
|
|
532
|
-
}
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
async function parseRetryDelayFromBody(response: Response): Promise<number | null> {
|
|
537
|
-
let text = "";
|
|
538
|
-
try {
|
|
539
|
-
text = await response.clone().text();
|
|
540
|
-
} catch {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (!text) {
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
let parsed: any;
|
|
153
|
+
async function ensureProjectContextOrThrow(
|
|
154
|
+
authRecord: OAuthAuthDetails,
|
|
155
|
+
client: PluginClient,
|
|
156
|
+
configuredProjectId?: string,
|
|
157
|
+
) {
|
|
549
158
|
try {
|
|
550
|
-
|
|
551
|
-
} catch {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const details = parsed?.error?.details;
|
|
556
|
-
if (!Array.isArray(details)) {
|
|
557
|
-
const message = typeof parsed?.error?.message === "string" ? parsed.error.message : "";
|
|
558
|
-
return parseRetryDelayFromMessage(message);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
for (const detail of details) {
|
|
562
|
-
if (!detail || typeof detail !== "object") {
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const retryDelay = (detail as Record<string, unknown>).retryDelay;
|
|
566
|
-
if (!retryDelay) {
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
const delayMs = parseRetryDelayValue(retryDelay);
|
|
570
|
-
if (delayMs !== null) {
|
|
571
|
-
return delayMs;
|
|
159
|
+
return await ensureProjectContext(authRecord, client, configuredProjectId);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
console.error(error.message);
|
|
572
163
|
}
|
|
164
|
+
throw error;
|
|
573
165
|
}
|
|
574
|
-
|
|
575
|
-
const message = typeof parsed?.error?.message === "string" ? parsed.error.message : "";
|
|
576
|
-
return parseRetryDelayFromMessage(message);
|
|
577
166
|
}
|
|
578
167
|
|
|
579
|
-
function
|
|
580
|
-
if (!value) {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
|
|
168
|
+
function toUrlString(value: RequestInfo): string {
|
|
584
169
|
if (typeof value === "string") {
|
|
585
|
-
|
|
586
|
-
if (!match || !match[1]) {
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
const seconds = Number(match[1]);
|
|
590
|
-
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
return Math.round(seconds * 1000);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (typeof value === "object") {
|
|
597
|
-
const record = value as Record<string, unknown>;
|
|
598
|
-
const seconds = typeof record.seconds === "number" ? record.seconds : 0;
|
|
599
|
-
const nanos = typeof record.nanos === "number" ? record.nanos : 0;
|
|
600
|
-
if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
const totalMs = Math.round(seconds * 1000 + nanos / 1e6);
|
|
604
|
-
return totalMs > 0 ? totalMs : null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Parses retry delays embedded in error message strings (e.g., "Please retry in 5s").
|
|
612
|
-
*/
|
|
613
|
-
function parseRetryDelayFromMessage(message: string): number | null {
|
|
614
|
-
if (!message) {
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
const retryMatch = message.match(/Please retry in ([0-9.]+(?:ms|s))/);
|
|
618
|
-
if (retryMatch?.[1]) {
|
|
619
|
-
return parseRetryDelayValue(retryMatch[1]);
|
|
170
|
+
return value;
|
|
620
171
|
}
|
|
621
|
-
const
|
|
622
|
-
if (
|
|
623
|
-
return
|
|
172
|
+
const candidate = (value as Request).url;
|
|
173
|
+
if (candidate) {
|
|
174
|
+
return candidate;
|
|
624
175
|
}
|
|
625
|
-
return
|
|
176
|
+
return value.toString();
|
|
626
177
|
}
|
|
627
178
|
|
|
628
179
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
180
|
+
* Debug-only, best-effort model visibility log from Code Assist quota buckets.
|
|
181
|
+
*
|
|
182
|
+
* Why: it gives a concrete backend-side list of model IDs currently visible to the
|
|
183
|
+
* current account/project, which helps explain 404/notFound model failures quickly.
|
|
631
184
|
*/
|
|
632
|
-
async function
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
} catch {
|
|
639
|
-
return null;
|
|
185
|
+
async function maybeLogAvailableQuotaModels(
|
|
186
|
+
accessToken: string,
|
|
187
|
+
projectId: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
if (!isGeminiDebugEnabled() || !projectId) {
|
|
190
|
+
return;
|
|
640
191
|
}
|
|
641
192
|
|
|
642
|
-
if (
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
let parsed: any;
|
|
647
|
-
try {
|
|
648
|
-
parsed = JSON.parse(text);
|
|
649
|
-
} catch {
|
|
650
|
-
return null;
|
|
193
|
+
if (loggedQuotaModelsByProject.has(projectId)) {
|
|
194
|
+
return;
|
|
651
195
|
}
|
|
196
|
+
loggedQuotaModelsByProject.add(projectId);
|
|
652
197
|
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const errorInfo = details.find(
|
|
658
|
-
(detail: any) =>
|
|
659
|
-
detail &&
|
|
660
|
-
typeof detail === "object" &&
|
|
661
|
-
detail["@type"] === "type.googleapis.com/google.rpc.ErrorInfo",
|
|
662
|
-
);
|
|
663
|
-
|
|
664
|
-
if (errorInfo?.domain && !CLOUDCODE_DOMAINS.includes(errorInfo.domain)) {
|
|
665
|
-
return null;
|
|
198
|
+
const quota = await retrieveUserQuota(accessToken, projectId);
|
|
199
|
+
if (!quota?.buckets) {
|
|
200
|
+
logGeminiDebugMessage(`Code Assist quota model lookup returned no buckets for project: ${projectId}`);
|
|
201
|
+
return;
|
|
666
202
|
}
|
|
667
203
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return { terminal: false, retryDelayMs };
|
|
204
|
+
const modelIds = [...new Set(quota.buckets.map((bucket) => bucket.modelId).filter(Boolean))];
|
|
205
|
+
if (modelIds.length === 0) {
|
|
206
|
+
logGeminiDebugMessage(`Code Assist quota buckets contained no model IDs for project: ${projectId}`);
|
|
207
|
+
return;
|
|
673
208
|
}
|
|
674
209
|
|
|
675
|
-
|
|
676
|
-
(
|
|
677
|
-
detail &&
|
|
678
|
-
typeof detail === "object" &&
|
|
679
|
-
detail["@type"] === "type.googleapis.com/google.rpc.QuotaFailure",
|
|
210
|
+
logGeminiDebugMessage(
|
|
211
|
+
`Code Assist models visible via quota buckets (${projectId}): ${modelIds.join(", ")}`,
|
|
680
212
|
);
|
|
681
|
-
|
|
682
|
-
if (quotaFailure?.violations && Array.isArray(quotaFailure.violations)) {
|
|
683
|
-
const combined = quotaFailure.violations
|
|
684
|
-
.map((violation: any) => String(violation?.description ?? "").toLowerCase())
|
|
685
|
-
.join(" ");
|
|
686
|
-
if (combined.includes("daily") || combined.includes("per day")) {
|
|
687
|
-
return { terminal: true, retryDelayMs };
|
|
688
|
-
}
|
|
689
|
-
return { terminal: false, retryDelayMs };
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return { terminal: false, retryDelayMs };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function wait(ms: number): Promise<void> {
|
|
696
|
-
return new Promise((resolve) => {
|
|
697
|
-
setTimeout(resolve, ms);
|
|
698
|
-
});
|
|
699
213
|
}
|