opencode-gemini-auth 1.3.9 → 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 +43 -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} +35 -198
- 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 +106 -476
- package/src/plugin/project.ts +0 -544
- 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,266 +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
|
-
|
|
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;
|
|
336
140
|
}
|
|
337
141
|
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return {};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
345
|
-
try {
|
|
346
|
-
const url = new URL(trimmed);
|
|
347
|
-
return {
|
|
348
|
-
code: url.searchParams.get("code") || undefined,
|
|
349
|
-
state: url.searchParams.get("state") || undefined,
|
|
350
|
-
};
|
|
351
|
-
} catch {
|
|
352
|
-
return {};
|
|
353
|
-
}
|
|
142
|
+
function normalizeProviderModelCosts(provider: Provider): void {
|
|
143
|
+
if (!provider.models) {
|
|
144
|
+
return;
|
|
354
145
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const params = new URLSearchParams(candidate);
|
|
359
|
-
const code = params.get("code") || undefined;
|
|
360
|
-
const state = params.get("state") || undefined;
|
|
361
|
-
if (code || state) {
|
|
362
|
-
return { code, state };
|
|
146
|
+
for (const model of Object.values(provider.models)) {
|
|
147
|
+
if (model) {
|
|
148
|
+
model.cost = { input: 0, output: 0 };
|
|
363
149
|
}
|
|
364
150
|
}
|
|
365
|
-
|
|
366
|
-
return { code: trimmed };
|
|
367
151
|
}
|
|
368
152
|
|
|
369
|
-
function
|
|
153
|
+
async function ensureProjectContextOrThrow(
|
|
154
|
+
authRecord: OAuthAuthDetails,
|
|
155
|
+
client: PluginClient,
|
|
156
|
+
configuredProjectId?: string,
|
|
157
|
+
) {
|
|
370
158
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
? "open"
|
|
376
|
-
: platform === "win32"
|
|
377
|
-
? "rundll32"
|
|
378
|
-
: "xdg-open";
|
|
379
|
-
const args =
|
|
380
|
-
platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
381
|
-
const child = spawn(command, args, {
|
|
382
|
-
stdio: "ignore",
|
|
383
|
-
detached: true,
|
|
384
|
-
});
|
|
385
|
-
child.unref?.();
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function fetchWithRetry(input: RequestInfo, init: RequestInit | undefined): Promise<Response> {
|
|
391
|
-
const maxRetries = DEFAULT_MAX_RETRIES;
|
|
392
|
-
const baseDelayMs = DEFAULT_BASE_DELAY_MS;
|
|
393
|
-
const maxDelayMs = DEFAULT_MAX_DELAY_MS;
|
|
394
|
-
|
|
395
|
-
if (!canRetryRequest(init)) {
|
|
396
|
-
return fetch(input, init);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let attempt = 0;
|
|
400
|
-
while (true) {
|
|
401
|
-
const response = await fetch(input, init);
|
|
402
|
-
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt >= maxRetries) {
|
|
403
|
-
return response;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const delayMs = await getRetryDelayMs(response, attempt, baseDelayMs, maxDelayMs);
|
|
407
|
-
if (!delayMs || delayMs <= 0) {
|
|
408
|
-
return response;
|
|
159
|
+
return await ensureProjectContext(authRecord, client, configuredProjectId);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
console.error(error.message);
|
|
409
163
|
}
|
|
410
|
-
|
|
411
|
-
if (init?.signal?.aborted) {
|
|
412
|
-
return response;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await wait(delayMs);
|
|
416
|
-
attempt += 1;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function canRetryRequest(init: RequestInit | undefined): boolean {
|
|
421
|
-
if (!init?.body) {
|
|
422
|
-
return true;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const body = init.body;
|
|
426
|
-
if (typeof body === "string") {
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) {
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function getRetryDelayMs(
|
|
446
|
-
response: Response,
|
|
447
|
-
attempt: number,
|
|
448
|
-
baseDelayMs: number,
|
|
449
|
-
maxDelayMs: number,
|
|
450
|
-
): Promise<number | null> {
|
|
451
|
-
const headerDelayMs = parseRetryAfterMs(response.headers.get("retry-after-ms"));
|
|
452
|
-
if (headerDelayMs !== null) {
|
|
453
|
-
return clampDelay(headerDelayMs, maxDelayMs);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
457
|
-
if (retryAfter !== null) {
|
|
458
|
-
return clampDelay(retryAfter, maxDelayMs);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const bodyDelayMs = await parseRetryDelayFromBody(response);
|
|
462
|
-
if (bodyDelayMs !== null) {
|
|
463
|
-
return clampDelay(bodyDelayMs, maxDelayMs);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const fallback = baseDelayMs * Math.pow(2, attempt);
|
|
467
|
-
return clampDelay(fallback, maxDelayMs);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function clampDelay(delayMs: number, maxDelayMs: number): number {
|
|
471
|
-
if (!Number.isFinite(delayMs)) {
|
|
472
|
-
return maxDelayMs;
|
|
473
|
-
}
|
|
474
|
-
return Math.min(Math.max(0, delayMs), maxDelayMs);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function parseRetryAfterMs(value: string | null): number | null {
|
|
478
|
-
if (!value) {
|
|
479
|
-
return null;
|
|
480
|
-
}
|
|
481
|
-
const parsed = Number(value);
|
|
482
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
483
|
-
return null;
|
|
164
|
+
throw error;
|
|
484
165
|
}
|
|
485
|
-
return parsed;
|
|
486
166
|
}
|
|
487
167
|
|
|
488
|
-
function
|
|
489
|
-
if (
|
|
490
|
-
return
|
|
491
|
-
}
|
|
492
|
-
const trimmed = value.trim();
|
|
493
|
-
if (!trimmed) {
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
496
|
-
const asNumber = Number(trimmed);
|
|
497
|
-
if (Number.isFinite(asNumber)) {
|
|
498
|
-
return Math.max(0, Math.round(asNumber * 1000));
|
|
168
|
+
function toUrlString(value: RequestInfo): string {
|
|
169
|
+
if (typeof value === "string") {
|
|
170
|
+
return value;
|
|
499
171
|
}
|
|
500
|
-
const
|
|
501
|
-
if (
|
|
502
|
-
return
|
|
172
|
+
const candidate = (value as Request).url;
|
|
173
|
+
if (candidate) {
|
|
174
|
+
return candidate;
|
|
503
175
|
}
|
|
504
|
-
return
|
|
176
|
+
return value.toString();
|
|
505
177
|
}
|
|
506
178
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
let parsed: any;
|
|
520
|
-
try {
|
|
521
|
-
parsed = JSON.parse(text);
|
|
522
|
-
} catch {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const details = parsed?.error?.details;
|
|
527
|
-
if (!Array.isArray(details)) {
|
|
528
|
-
return null;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
for (const detail of details) {
|
|
532
|
-
if (!detail || typeof detail !== "object") {
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
const retryDelay = (detail as Record<string, unknown>).retryDelay;
|
|
536
|
-
if (!retryDelay) {
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
const delayMs = parseRetryDelayValue(retryDelay);
|
|
540
|
-
if (delayMs !== null) {
|
|
541
|
-
return delayMs;
|
|
542
|
-
}
|
|
179
|
+
/**
|
|
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.
|
|
184
|
+
*/
|
|
185
|
+
async function maybeLogAvailableQuotaModels(
|
|
186
|
+
accessToken: string,
|
|
187
|
+
projectId: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
if (!isGeminiDebugEnabled() || !projectId) {
|
|
190
|
+
return;
|
|
543
191
|
}
|
|
544
192
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
function parseRetryDelayValue(value: unknown): number | null {
|
|
549
|
-
if (!value) {
|
|
550
|
-
return null;
|
|
193
|
+
if (loggedQuotaModelsByProject.has(projectId)) {
|
|
194
|
+
return;
|
|
551
195
|
}
|
|
196
|
+
loggedQuotaModelsByProject.add(projectId);
|
|
552
197
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
const seconds = Number(match[1]);
|
|
559
|
-
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
return Math.round(seconds * 1000);
|
|
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;
|
|
563
202
|
}
|
|
564
203
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
const totalMs = Math.round(seconds * 1000 + nanos / 1e6);
|
|
573
|
-
return totalMs > 0 ? totalMs : null;
|
|
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;
|
|
574
208
|
}
|
|
575
209
|
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function wait(ms: number): Promise<void> {
|
|
580
|
-
return new Promise((resolve) => {
|
|
581
|
-
setTimeout(resolve, ms);
|
|
582
|
-
});
|
|
210
|
+
logGeminiDebugMessage(
|
|
211
|
+
`Code Assist models visible via quota buckets (${projectId}): ${modelIds.join(", ")}`,
|
|
212
|
+
);
|
|
583
213
|
}
|