magpie-gemini-web 0.7.0 → 0.8.1
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.en.md +33 -10
- package/README.md +33 -10
- package/docs/studio-release.md +33 -13
- package/index.mjs +5 -1
- package/lib/backend-release.json +14 -14
- package/lib/backend.mjs +127 -4
- package/lib/studio-native.mjs +7 -3
- package/lib/studio-release.json +45 -19
- package/lib/studio.mjs +75 -9
- package/lib/usage.mjs +47 -33
- package/package.json +2 -2
- package/patches/argon.patch +690 -53
- package/scripts/studio-managed-smoke.mjs +6 -2
- package/scripts/studio-release.mjs +89 -31
package/lib/studio-native.mjs
CHANGED
|
@@ -197,7 +197,8 @@ const ENV_KEYS = [
|
|
|
197
197
|
'ROUTING_STRATEGY', 'UPSTREAM_CHANNELS', 'MODEL_MAPPING', 'MODEL_CHANNELS', 'TEMPORARY_CHAT', 'WAA_BACKEND',
|
|
198
198
|
'MEDIA_OUTPUT', 'MEDIA_BASE_URL', 'AUTO_START', 'REQUEST_BODY_LOG', 'LOG_FILE', 'LOG_MAX_TOTAL_MB',
|
|
199
199
|
'ADMIN_AUTH_ENABLED', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'BUILD_NATIVE_NONSTREAM',
|
|
200
|
-
'NONSTREAM_KEEPALIVE', 'FAKE_STREAM', 'YOUTUBE_LINKS', '
|
|
200
|
+
'NONSTREAM_KEEPALIVE', 'FAKE_STREAM', 'YOUTUBE_LINKS', 'SEARCH_MODEL_ALIASES', 'GOOGLE_SEARCH',
|
|
201
|
+
'FIRST_EVENT_HEDGE', 'CONTINUATION_REWRITE', 'CONTINUATION_PROMPT'
|
|
201
202
|
];
|
|
202
203
|
|
|
203
204
|
// PROXY_ENV are the proxy variables the service would fall back to with no PROXY of its own.
|
|
@@ -216,7 +217,10 @@ const SETTING_DEFAULTS = {
|
|
|
216
217
|
ROUTING_STRATEGY: 'fill-first',
|
|
217
218
|
REQUEST_TIMEOUT: '30m',
|
|
218
219
|
INIT_TIMEOUT: '2m',
|
|
219
|
-
NONSTREAM_KEEPALIVE: 'false'
|
|
220
|
+
NONSTREAM_KEEPALIVE: 'false',
|
|
221
|
+
// Upstream lists each chat model that can search Google again as <id>-search (0.2.18), which
|
|
222
|
+
// would list most of Magpie's AI Studio models twice.
|
|
223
|
+
SEARCH_MODEL_ALIASES: 'false'
|
|
220
224
|
};
|
|
221
225
|
|
|
222
226
|
// pluginEnv is the settings this plugin depends on to reach, trust and drive the service. The
|
|
@@ -235,7 +239,7 @@ function pluginEnv(dataDir, config) {
|
|
|
235
239
|
ADMIN_USERNAME: config.adminUsername,
|
|
236
240
|
ADMIN_PASSWORD: config.adminPassword,
|
|
237
241
|
AUTO_START: 'true',
|
|
238
|
-
WAA_BACKEND: '
|
|
242
|
+
WAA_BACKEND: 'spidermonkey',
|
|
239
243
|
UPSTREAM_CHANNELS: 'playground',
|
|
240
244
|
BUILD_NATIVE_NONSTREAM: 'false'
|
|
241
245
|
};
|
package/lib/studio-release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
2
|
+
"version": "0.2.18-magpie.1",
|
|
3
3
|
"repository": "BUKOWSKIREAL/magpie-gemini-web",
|
|
4
|
-
"tag": "studio-v0.2.
|
|
4
|
+
"tag": "studio-v0.2.18-magpie.1",
|
|
5
5
|
"goVersion": "1.27.1",
|
|
6
6
|
"toolchains": {
|
|
7
7
|
"zig": "0.16.0",
|
|
@@ -26,12 +26,38 @@
|
|
|
26
26
|
"sha256": "fd5462b7f5d93b2ff981fcd3f9c7e427e47f3203c64ea91c27bcc444eebd74cd"
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
},
|
|
30
|
+
"spidermonkey": {
|
|
31
|
+
"release": "v152.0.4-aistudio2api.1",
|
|
32
|
+
"repository": "Mag1cFall/firefox",
|
|
33
|
+
"targets": {
|
|
34
|
+
"darwin-arm64": {
|
|
35
|
+
"asset": "spidermonkey-v152.0.4-aistudio2api.1-darwin-arm64.tar.gz",
|
|
36
|
+
"sha256": "19f690d52ce2cfe7dbaedcc9771976145fff0dbd81f24e89a2dd094a1424b673"
|
|
37
|
+
},
|
|
38
|
+
"darwin-x64": {
|
|
39
|
+
"asset": "spidermonkey-v152.0.4-aistudio2api.1-darwin-amd64.tar.gz",
|
|
40
|
+
"sha256": "8063d69f56711c72b1244c8415b6c5096aa64ae8b022f555722667531acfe659"
|
|
41
|
+
},
|
|
42
|
+
"linux-x64": {
|
|
43
|
+
"asset": "spidermonkey-v152.0.4-aistudio2api.1-linux-amd64.tar.gz",
|
|
44
|
+
"sha256": "d3ecebf1890355426ad261ab8723038849a524b348af53e1bb440ca53cc2cacb"
|
|
45
|
+
},
|
|
46
|
+
"linux-arm64": {
|
|
47
|
+
"asset": "spidermonkey-v152.0.4-aistudio2api.1-linux-arm64.tar.gz",
|
|
48
|
+
"sha256": "b3c9539b8d8378d99f86b267e0e3d007dd01ff80acebf4f52f3e1d6ac04d9d00"
|
|
49
|
+
},
|
|
50
|
+
"win32-x64": {
|
|
51
|
+
"asset": "spidermonkey-v152.0.4-aistudio2api.1-windows-amd64.tar.gz",
|
|
52
|
+
"sha256": "942a3b57b202f899d799cac678b7471d01d0b34714070f0810c18fca37ffd721"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
29
55
|
}
|
|
30
56
|
},
|
|
31
57
|
"upstream": {
|
|
32
|
-
"version": "0.2.
|
|
58
|
+
"version": "0.2.18",
|
|
33
59
|
"repository": "https://github.com/Mag1cFall/AIStudio2API.git",
|
|
34
|
-
"commit": "
|
|
60
|
+
"commit": "9b694a6b7c6439db1767a577994dfcc87a3cf588",
|
|
35
61
|
"camoufox": "152.0.4-beta.29"
|
|
36
62
|
},
|
|
37
63
|
"patches": [
|
|
@@ -39,29 +65,29 @@
|
|
|
39
65
|
],
|
|
40
66
|
"binaries": {
|
|
41
67
|
"darwin-arm64": {
|
|
42
|
-
"asset": "aistudio2api-v0.2.
|
|
43
|
-
"sha256": "
|
|
44
|
-
"binarySha256": "
|
|
68
|
+
"asset": "aistudio2api-v0.2.18-magpie.1-darwin-arm64.tar.gz",
|
|
69
|
+
"sha256": "9f70596a5b95f4bcf5347a52b88ad41e26106cd6e2a79304183a73d58d8a7a29",
|
|
70
|
+
"binarySha256": "d41e9d4c950efb479ad19727bdf496f8de05c01451110fd9eb7425a9054f5c66"
|
|
45
71
|
},
|
|
46
72
|
"darwin-x64": {
|
|
47
|
-
"asset": "aistudio2api-v0.2.
|
|
48
|
-
"sha256": "
|
|
49
|
-
"binarySha256": "
|
|
73
|
+
"asset": "aistudio2api-v0.2.18-magpie.1-darwin-amd64.tar.gz",
|
|
74
|
+
"sha256": "b118733750a3f882fe13754b2079f9cc89b2645d3cd3bce4bab26aaee24b54e5",
|
|
75
|
+
"binarySha256": "6a1017195809a881614032be647faf25754ebfe060e3301424dcdacefba32469"
|
|
50
76
|
},
|
|
51
77
|
"linux-x64": {
|
|
52
|
-
"asset": "aistudio2api-v0.2.
|
|
53
|
-
"sha256": "
|
|
54
|
-
"binarySha256": "
|
|
78
|
+
"asset": "aistudio2api-v0.2.18-magpie.1-linux-amd64.tar.gz",
|
|
79
|
+
"sha256": "e6310fb89c34295d1a4c60e40b30d7a3073e9f5fc16343650f4021f396225922",
|
|
80
|
+
"binarySha256": "1803037342daada925486f5acd40bfc0bc06bda54b0f1f4459da79d1e917d283"
|
|
55
81
|
},
|
|
56
82
|
"linux-arm64": {
|
|
57
|
-
"asset": "aistudio2api-v0.2.
|
|
58
|
-
"sha256": "
|
|
59
|
-
"binarySha256": "
|
|
83
|
+
"asset": "aistudio2api-v0.2.18-magpie.1-linux-arm64.tar.gz",
|
|
84
|
+
"sha256": "965336192889bcd2d9f6db1e5f9c035251ff64db86ff0e590d9ed89662867629",
|
|
85
|
+
"binarySha256": "81553da83a7322d608b2498e6c6a01b08fe963341d8655262ed2dd17ca7a83df"
|
|
60
86
|
},
|
|
61
87
|
"win32-x64": {
|
|
62
|
-
"asset": "aistudio2api-v0.2.
|
|
63
|
-
"sha256": "
|
|
64
|
-
"binarySha256": "
|
|
88
|
+
"asset": "aistudio2api-v0.2.18-magpie.1-windows-amd64.zip",
|
|
89
|
+
"sha256": "537884412868849665b22d5d9066101a94d389ad0f048555da8c8ea55c7240ba",
|
|
90
|
+
"binarySha256": "0024d8709eb97950ddb3ad6016612521a76c7aca553235db2dddb9183188806e"
|
|
65
91
|
}
|
|
66
92
|
}
|
|
67
93
|
}
|
package/lib/studio.mjs
CHANGED
|
@@ -10,6 +10,19 @@ const STUDIO_EXCLUDE = /(?:image|nano-banana|tts|audio|live|transcribe|robotics|
|
|
|
10
10
|
// Catalog capabilities of models made for images, speech or live sessions. Such a model can be
|
|
11
11
|
// marked chat_model and named like a text model too (gemini-omni-*), so the name can't tell.
|
|
12
12
|
const NON_TEXT_CAPS = ['image_route', 'speech_route', 'live_route', 'aspect_ratio', 'output_resolution', 'interactions_api', 'transcription_output'];
|
|
13
|
+
// Of those, the ones an image model (image_route) has. Such a model is listed for Magpie's image
|
|
14
|
+
// generation (one with any other is left out still): on Gemini's own API (@ai-sdk/google), as
|
|
15
|
+
// Magpie draws only with a plugin's Gemini image model there, asking generateContent for images
|
|
16
|
+
// (see forward). Magpie keeps it in its chat model lists too, if it is ticked there.
|
|
17
|
+
const IMAGE_CAPS = new Set(['image_route', 'aspect_ratio', 'output_resolution']);
|
|
18
|
+
// The aspect ratios of an image model whose catalog entry lists none (capability_options
|
|
19
|
+
// .image_aspect_ratios), as the service takes them then (openAIImageAspectRatios).
|
|
20
|
+
const DEFAULT_ASPECTS = ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'];
|
|
21
|
+
const ASPECT = /^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/;
|
|
22
|
+
// The image models each backend's last list named (listStudioModels), with their aspect ratios,
|
|
23
|
+
// for the requests on Gemini's API (imageModelOf); asked again once this old.
|
|
24
|
+
const IMAGE_CATALOG_MS = 10 * 60 * 1000;
|
|
25
|
+
const imageCatalogs = new Map();
|
|
13
26
|
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
14
27
|
|
|
15
28
|
// Magpie waits 10 minutes for a sign-in once authorize returns (signInTimeout), downloads and the
|
|
@@ -57,6 +70,7 @@ const KNOWN_STUDIO_NESTED = new Set([
|
|
|
57
70
|
'baseURL',
|
|
58
71
|
'allowTools',
|
|
59
72
|
'allowImages',
|
|
73
|
+
'imageModels',
|
|
60
74
|
'contextLimit',
|
|
61
75
|
'outputLimit',
|
|
62
76
|
'requestTimeoutMs',
|
|
@@ -78,7 +92,10 @@ export function studioSettings(options = {}) {
|
|
|
78
92
|
allowTools = options.allowTools;
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
|
|
95
|
+
// On unless turned off: AI Studio's chat models read a chat's images (seen on AIStudio2API
|
|
96
|
+
// 0.2.17). Gemini Web keeps it off by default (settings), as gemini-3.6-flash there never
|
|
97
|
+
// answered a chat with an image, each of two times.
|
|
98
|
+
let allowImages = true;
|
|
82
99
|
if (options.allowImages !== undefined) {
|
|
83
100
|
if (typeof options.allowImages !== 'boolean') {
|
|
84
101
|
throw new Error('Invalid option allowImages: must be a boolean');
|
|
@@ -102,6 +119,7 @@ export function studioSettings(options = {}) {
|
|
|
102
119
|
modelTimeoutMs = options.modelTimeoutMs;
|
|
103
120
|
}
|
|
104
121
|
|
|
122
|
+
let imageModels = true;
|
|
105
123
|
let baseURL = STUDIO_BASE;
|
|
106
124
|
let proxy;
|
|
107
125
|
let explicitContextLimit = null;
|
|
@@ -140,6 +158,12 @@ export function studioSettings(options = {}) {
|
|
|
140
158
|
}
|
|
141
159
|
allowImages = s.allowImages;
|
|
142
160
|
}
|
|
161
|
+
if (s.imageModels !== undefined) {
|
|
162
|
+
if (typeof s.imageModels !== 'boolean') {
|
|
163
|
+
throw new Error('Invalid option studio.imageModels: must be a boolean');
|
|
164
|
+
}
|
|
165
|
+
imageModels = s.imageModels;
|
|
166
|
+
}
|
|
143
167
|
if (s.requestTimeoutMs !== undefined) {
|
|
144
168
|
if (!Number.isInteger(s.requestTimeoutMs) || s.requestTimeoutMs <= 0) {
|
|
145
169
|
throw new Error('Invalid option studio.requestTimeoutMs: must be a positive integer');
|
|
@@ -173,6 +197,7 @@ export function studioSettings(options = {}) {
|
|
|
173
197
|
baseURL,
|
|
174
198
|
allowTools,
|
|
175
199
|
allowImages,
|
|
200
|
+
imageModels,
|
|
176
201
|
requestTimeoutMs,
|
|
177
202
|
modelTimeoutMs,
|
|
178
203
|
proxy,
|
|
@@ -225,6 +250,7 @@ export async function listStudioModels(c, opts) {
|
|
|
225
250
|
|
|
226
251
|
const result = Object.create(null);
|
|
227
252
|
const seen = new Set();
|
|
253
|
+
const images = new Map();
|
|
228
254
|
// A model no enabled account's subscription covers is listed without channels, and calls to it
|
|
229
255
|
// get a 403. A backend that lists no channels at all is older than that rule: keep its models.
|
|
230
256
|
const listsChannels = body.data.some(item => Array.isArray(item?.channels));
|
|
@@ -240,14 +266,17 @@ export async function listStudioModels(c, opts) {
|
|
|
240
266
|
if (!id.startsWith('gemini-')) continue;
|
|
241
267
|
// Build assistant models inject Google's coding-agent prompt and do not expose
|
|
242
268
|
// ordinary function calling. Keep them out of this general-purpose provider.
|
|
243
|
-
if (
|
|
269
|
+
if (/-build$/i.test(id)) continue;
|
|
244
270
|
|
|
245
271
|
const methods = Array.isArray(item.supported_generation_methods) ? item.supported_generation_methods : [];
|
|
246
272
|
if (!methods.includes('generateContent')) continue;
|
|
247
273
|
|
|
248
274
|
const caps = item.capabilities && typeof item.capabilities === 'object' ? item.capabilities : {};
|
|
249
275
|
if (caps.chat_model !== true || caps.assistant_route === true) continue;
|
|
250
|
-
|
|
276
|
+
// An image model is named for images, so the name filter is for the others only.
|
|
277
|
+
const draws = caps.image_route === true;
|
|
278
|
+
if (draws ? !opts.imageModels : STUDIO_EXCLUDE.test(id)) continue;
|
|
279
|
+
if (NON_TEXT_CAPS.some(cap => caps[cap] === true && !(draws && IMAGE_CAPS.has(cap)))) continue;
|
|
251
280
|
if (listsChannels && !(Array.isArray(item.channels) && item.channels.length > 0)) continue;
|
|
252
281
|
|
|
253
282
|
if (seen.has(id)) continue;
|
|
@@ -271,7 +300,14 @@ export async function listStudioModels(c, opts) {
|
|
|
271
300
|
? item.thinking_levels.filter(level => ['minimal', 'low', 'medium', 'high'].includes(level))
|
|
272
301
|
: ['minimal', 'low', 'medium', 'high'];
|
|
273
302
|
const variants = caps.thinking_level === true ? Object.fromEntries(levels.map(level => [level, {}])) : {};
|
|
274
|
-
|
|
303
|
+
// An image model takes images to edit or draw from, which allowImages (images in a chat on
|
|
304
|
+
// chat completions) doesn't cover.
|
|
305
|
+
const image = draws || Boolean(opts.allowImages && caps.media_resolution === true);
|
|
306
|
+
if (draws) {
|
|
307
|
+
const listed = item.capability_options?.image_aspect_ratios;
|
|
308
|
+
const aspects = Array.isArray(listed) ? listed.filter(r => typeof r === 'string' && ASPECT.test(r)) : [];
|
|
309
|
+
images.set(id, aspects.length > 0 ? aspects : DEFAULT_ASPECTS);
|
|
310
|
+
}
|
|
275
311
|
|
|
276
312
|
result[id] = {
|
|
277
313
|
id,
|
|
@@ -280,7 +316,7 @@ export async function listStudioModels(c, opts) {
|
|
|
280
316
|
api: {
|
|
281
317
|
id,
|
|
282
318
|
url: c.baseURL,
|
|
283
|
-
npm: '@ai-sdk/openai-compatible'
|
|
319
|
+
npm: draws ? '@ai-sdk/google' : '@ai-sdk/openai-compatible'
|
|
284
320
|
},
|
|
285
321
|
limit: {
|
|
286
322
|
context,
|
|
@@ -296,7 +332,8 @@ export async function listStudioModels(c, opts) {
|
|
|
296
332
|
image
|
|
297
333
|
},
|
|
298
334
|
output: {
|
|
299
|
-
text: true
|
|
335
|
+
text: true,
|
|
336
|
+
image: draws
|
|
300
337
|
}
|
|
301
338
|
},
|
|
302
339
|
cost: {
|
|
@@ -318,9 +355,31 @@ export async function listStudioModels(c, opts) {
|
|
|
318
355
|
throw new Error('No usable Gemini models found in AI Studio response');
|
|
319
356
|
}
|
|
320
357
|
|
|
358
|
+
imageCatalogs.set(catalogKey(c), { images, at: Date.now() });
|
|
321
359
|
return result;
|
|
322
360
|
}
|
|
323
361
|
|
|
362
|
+
function catalogKey(c) {
|
|
363
|
+
return `${c.baseURL}\n${c.key}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// imageModelOf is the aspect ratios of the image model id, which a request on Gemini's API names
|
|
367
|
+
// (see forward), as the backend's list says, asking it again once its last is old: null for a
|
|
368
|
+
// model that isn't one. When the list can't be had again, its last still holds; with none, what
|
|
369
|
+
// the model is isn't known, and the list's error is thrown for forward to answer with.
|
|
370
|
+
async function imageModelOf(c, opts, id) {
|
|
371
|
+
const key = catalogKey(c);
|
|
372
|
+
const cached = imageCatalogs.get(key);
|
|
373
|
+
if (!cached || Date.now() - cached.at > IMAGE_CATALOG_MS) {
|
|
374
|
+
try {
|
|
375
|
+
await listStudioModels(c, opts);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (!cached) throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return imageCatalogs.get(key).images.get(id) ?? null;
|
|
381
|
+
}
|
|
382
|
+
|
|
324
383
|
function sameBackend(c, local) {
|
|
325
384
|
return c.key === local.apiKey && c.baseURL === local.baseURL;
|
|
326
385
|
}
|
|
@@ -349,6 +408,13 @@ function staleManagedSignIn(auth, opts) {
|
|
|
349
408
|
|
|
350
409
|
async function forwardManagedStudio(input, init, getAuth, opts) {
|
|
351
410
|
const retryInput = input instanceof Request ? input.clone() : input;
|
|
411
|
+
// Gemini's API, for the image models alone (see forward)
|
|
412
|
+
const forwardOpts = {
|
|
413
|
+
...opts,
|
|
414
|
+
nativeThinking: true,
|
|
415
|
+
stripCitations: false,
|
|
416
|
+
imageModel: opts.imageModels ? (c, id) => imageModelOf(c, opts, id) : undefined
|
|
417
|
+
};
|
|
352
418
|
|
|
353
419
|
const auth = await getAuth();
|
|
354
420
|
if (auth?.metadata?.managed === true && staleManagedSignIn(auth, opts)) {
|
|
@@ -356,7 +422,7 @@ async function forwardManagedStudio(input, init, getAuth, opts) {
|
|
|
356
422
|
}
|
|
357
423
|
|
|
358
424
|
const callerSignal = input instanceof Request ? input.signal : init?.signal;
|
|
359
|
-
const res = await forward(input, init, getAuth,
|
|
425
|
+
const res = await forward(input, init, getAuth, forwardOpts);
|
|
360
426
|
if (res.headers.get(UNREACHABLE_HEADER) !== 'unreachable') return res;
|
|
361
427
|
|
|
362
428
|
if (callerSignal?.aborted) return res;
|
|
@@ -369,7 +435,7 @@ async function forwardManagedStudio(input, init, getAuth, opts) {
|
|
|
369
435
|
}
|
|
370
436
|
|
|
371
437
|
if (callerSignal?.aborted) return res;
|
|
372
|
-
return forward(retryInput, init, getAuth,
|
|
438
|
+
return forward(retryInput, init, getAuth, forwardOpts);
|
|
373
439
|
}
|
|
374
440
|
|
|
375
441
|
// accountSignIn is the Magpie sign-in for one Google account of the local service: the service's
|
|
@@ -670,7 +736,7 @@ export const AIStudioPlugin = async (_input, options = {}) => {
|
|
|
670
736
|
proxy = downloadProxy(studioDataDir(), opts.proxy);
|
|
671
737
|
} catch {}
|
|
672
738
|
const via = proxy ? `走代理 ${redactProxy(proxy)}` : '按 Magpie 的代理设置';
|
|
673
|
-
return `本地 AI Studio 服务还没启动:要先启动它,第一次使用或插件更新后还要先从 GitHub 下载(约
|
|
739
|
+
return `本地 AI Studio 服务还没启动:要先启动它,第一次使用或插件更新后还要先从 GitHub 下载(约 40 MB,${via}),可能要等几分钟。${SETUP_PROXY_HINT}\n`;
|
|
674
740
|
}
|
|
675
741
|
|
|
676
742
|
// addAccount signs a new Google account in through the service's own login window, setting the
|
package/lib/usage.mjs
CHANGED
|
@@ -35,14 +35,26 @@ const CLIENT_HEADERS = {
|
|
|
35
35
|
'Sec-CH-UA-Platform': '"Windows"'
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
// When an account's pools are read again
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
// When an account's pools are read again: whenever Magpie asks, once the last reading is
|
|
39
|
+
// USAGE_FRESH_MS old. Magpie keeps an account's reading a minute itself (loginUsageCache in its
|
|
40
|
+
// internal/provider/login_usage.go) and dates the card by when it got it, so a reading kept here
|
|
41
|
+
// any longer would show old figures as new, and a refresh would show them again (#34); use from
|
|
42
|
+
// the Gemini app, the website or another device leaves no trace here to read again on. On its
|
|
43
|
+
// own Magpie asks about once a minute while its Usage page or tray panel is open, whenever it
|
|
44
|
+
// brings the menu bar's allowances up to date (every 3 minutes unless set otherwise) and, a
|
|
45
|
+
// minute apart, while it routes requests among the accounts; it asks sooner only when it puts
|
|
46
|
+
// its minute aside. USAGE_FRESH_MS takes in asks that come together, a refresh pressed twice; a
|
|
47
|
+
// read under way is shared.
|
|
48
|
+
// A failed read stands USAGE_RETRY_MS, so a cookie Google turned away, or an account it is
|
|
49
|
+
// limiting, isn't asked about again every minute. Magpie asking again within
|
|
50
|
+
// USAGE_ASKED_AGAIN_MS of the last time tries it again at once: it does that only when it put
|
|
51
|
+
// its minute aside, for a card's refresh button, the Usage page or tray panel brought up or
|
|
52
|
+
// refreshed, `magpie quota` or a sign-in check. Its hook doesn't tell a refresh (auth.usage is
|
|
53
|
+
// given getAuth and the provider alone), so one pressed later than that tries again only when
|
|
54
|
+
// pressed once more.
|
|
55
|
+
export const USAGE_FRESH_MS = 30 * 1000;
|
|
56
|
+
export const USAGE_RETRY_MS = 10 * 60 * 1000;
|
|
57
|
+
export const USAGE_ASKED_AGAIN_MS = 60 * 1000;
|
|
46
58
|
|
|
47
59
|
// The pools the Usage page shows, under the names Magpie gives the same windows elsewhere.
|
|
48
60
|
const POOLS = [
|
|
@@ -64,17 +76,13 @@ async function openReadOnly(file) {
|
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
// poolAccounts lists the enabled accounts in the backend's pool, each with the proxy it is
|
|
67
|
-
// bound to, if any, and whether that proxy is enabled (proxyEnabled), whether the pool has
|
|
68
|
-
// proxies at all (pooled: disabled ones count, as they do for the backend)
|
|
69
|
-
// backend last used it (usedAt, Unix seconds): the later of when it last took the account for
|
|
70
|
-
// a request and when it last finished one.
|
|
79
|
+
// bound to, if any, and whether that proxy is enabled (proxyEnabled), and whether the pool has
|
|
80
|
+
// proxies at all (pooled: disabled ones count, as they do for the backend).
|
|
71
81
|
export async function poolAccounts(dataDir = getDataDir()) {
|
|
72
82
|
const db = await openReadOnly(path.join(dataDir, 'gemini.db'));
|
|
73
83
|
try {
|
|
74
84
|
return db.all(`SELECT a.id, a.label, a.cookie, p.url AS proxy, p.enabled AS proxyEnabled,
|
|
75
|
-
EXISTS (SELECT 1 FROM proxies) AS pooled
|
|
76
|
-
MAX(a.last_used_at, COALESCE((SELECT MAX(r.ts) FROM requests r
|
|
77
|
-
WHERE r.ts >= a.last_used_at AND r.account_id = a.id), 0)) AS usedAt
|
|
85
|
+
EXISTS (SELECT 1 FROM proxies) AS pooled
|
|
78
86
|
FROM accounts a LEFT JOIN proxies p ON p.id = a.proxy_id
|
|
79
87
|
WHERE a.status = 'enabled' ORDER BY a.id`);
|
|
80
88
|
} finally {
|
|
@@ -160,42 +168,48 @@ export async function accountUsage(account, options = {}) {
|
|
|
160
168
|
}, options));
|
|
161
169
|
}
|
|
162
170
|
|
|
163
|
-
// The last read of each account: id -> { at, read, pools }, pools once it has
|
|
171
|
+
// The last read of each account: id -> { at, read, pools, failed }, pools once it has
|
|
172
|
+
// succeeded, failed once it hasn't, neither while it is under way.
|
|
164
173
|
const reads = new Map();
|
|
174
|
+
// When Magpie last asked for each account's usage: id -> ms.
|
|
175
|
+
const asked = new Map();
|
|
165
176
|
|
|
166
|
-
// due says whether the account's last read is to be made again (see
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const age = now - last.at;
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
return last.pools.some(p => {
|
|
174
|
-
const at = Date.parse(p?.gxu?.windowRefreshesAt ?? '');
|
|
175
|
-
return at > last.at && at <= now;
|
|
176
|
-
});
|
|
177
|
+
// due says whether the account's last read is to be made again (see USAGE_FRESH_MS); one under
|
|
178
|
+
// way never is. again: Magpie asked within USAGE_ASKED_AGAIN_MS of the time before.
|
|
179
|
+
function due(last, again, options) {
|
|
180
|
+
const age = Date.now() - last.at;
|
|
181
|
+
const fresh = options.freshMs ?? USAGE_FRESH_MS;
|
|
182
|
+
if (last.pools) return age >= fresh;
|
|
183
|
+
return !!last.failed && age >= (again ? fresh : (options.retryMs ?? USAGE_RETRY_MS));
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
function readUsage(account, options) {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const again = now - asked.get(account.id) < (options.askedAgainMs ?? USAGE_ASKED_AGAIN_MS);
|
|
189
|
+
asked.set(account.id, now);
|
|
180
190
|
const last = reads.get(account.id);
|
|
181
|
-
if (last && !due(last,
|
|
182
|
-
const entry = { at:
|
|
183
|
-
entry.read = accountUsage(account, options).then(
|
|
191
|
+
if (last && !due(last, again, options)) return last.read;
|
|
192
|
+
const entry = { at: now };
|
|
193
|
+
entry.read = accountUsage(account, options).then(
|
|
194
|
+
u => ((entry.pools = u.pools), u),
|
|
195
|
+
err => { entry.failed = true; throw err; }
|
|
196
|
+
);
|
|
184
197
|
reads.set(account.id, entry);
|
|
185
198
|
return entry.read;
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
// The Google account's email, from the signed-in app page's bootstrap data (WIZ_global_data's
|
|
189
202
|
// oPEP7c), kept per backend account: the page is large and the email doesn't change. A page
|
|
190
|
-
// that didn't give it is asked for again no sooner than
|
|
203
|
+
// that didn't give it is asked for again no sooner than EMAIL_RETRY_MS: the email only names
|
|
191
204
|
// a card. An account with no certain way out (egress) isn't asked yet, and that isn't a try.
|
|
192
205
|
const emails = new Map(); // id -> { email }, or { at } since the last try that got none
|
|
193
206
|
const EMAIL_FIELD = /"oPEP7c":"([^"@\s]+@[^"\s]+)"/;
|
|
207
|
+
const EMAIL_RETRY_MS = 6 * 3600 * 1000;
|
|
194
208
|
|
|
195
209
|
export async function accountEmail(account, options = {}) {
|
|
196
210
|
const known = emails.get(account.id);
|
|
197
211
|
if (known?.email) return known.email;
|
|
198
|
-
if (known && Date.now() - known.at < (options.
|
|
212
|
+
if (known && Date.now() - known.at < (options.emailRetryMs ?? EMAIL_RETRY_MS)) return undefined;
|
|
199
213
|
egress(account, options);
|
|
200
214
|
emails.set(account.id, { at: Date.now() });
|
|
201
215
|
const page = await geminiRequest('https://gemini.google.com/app', account, {
|
|
@@ -250,7 +264,7 @@ export function poolWindows(read) {
|
|
|
250
264
|
// usage is the Magpie usage card: one account's 5-hour and weekly windows, as
|
|
251
265
|
// gemini.google.com/usage shows them, when the sign-in names its backend account; else the
|
|
252
266
|
// whole pool's, as one card. Accounts go by their Google emails (#id until that is known).
|
|
253
|
-
// An account is read from Google again only when due (see
|
|
267
|
+
// An account is read from Google again only when due (see USAGE_FRESH_MS).
|
|
254
268
|
// Magpie hides a card's windows behind an error, so a pool account that can't be read only
|
|
255
269
|
// says so in the tooltip while the others still show.
|
|
256
270
|
export async function usage(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-gemini-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Gemini Web and Google AI Studio provider for Magpie, backed by gemini-web2api-go and AIStudio2API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -83,6 +83,6 @@
|
|
|
83
83
|
},
|
|
84
84
|
"magpie": {
|
|
85
85
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSIyIiB5MT0iMyIgeDI9IjIwIiB5Mj0iMjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDZCNkQ0Ii8+PHN0b3Agb2Zmc2V0PSI1MCUiIHN0b3AtY29sb3I9IiMzQjgyRjYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM4QjVDRjYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE0IiB5MT0iMiIgeDI9IjIyIiB5Mj0iMTAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjOEI1Q0Y2Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjRUM0ODk5Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTEwIDMuMkMxMC4zNSAzLjIgMTAuNjIgMy40NSAxMC43MiAzLjhDMTEuNDUgNi44NSAxMy4xNSA4LjU1IDE2LjIgOS4yOEMxNi41NSA5LjM4IDE2LjggOS42NSAxNi44IDEwQzE2LjggMTAuMzUgMTYuNTUgMTAuNjIgMTYuMiAxMC43MkMxMy4xNSAxMS40NSAxMS40NSAxMy4xNSAxMC43MiAxNi4yQzEwLjYyIDE2LjU1IDEwLjM1IDE2LjggMTAgMTYuOEM5LjY1IDE2LjggOS4zOCAxNi41NSA5LjI4IDE2LjJDOC41NSAxMy4xNSA2Ljg1IDExLjQ1IDMuOCAxMC43MkMzLjQ1IDEwLjYyIDMuMiAxMC4zNSAzLjIgMTBDMy4yIDkuNjUgMy40NSA5LjM4IDMuOCA5LjI4QzYuODUgOC41NSA4LjU1IDYuODUgOS4yOCAzLjhDOS4zOCAzLjQ1IDkuNjUgMy4yIDEwIDMuMloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0wLjUgMi41KSBzY2FsZSgxLjEyKSIgZmlsbD0idXJsKCNhKSIvPjxwYXRoIGQ9Ik0xOC4yIDEuOEMxOC40MiAxLjggMTguNiAxLjk1IDE4LjY2IDIuMTZDMTkuMDggMy44MiAyMC4wMiA0Ljc2IDIxLjY4IDUuMThDMjEuODkgNS4yNCAyMi4wNCA1LjQyIDIyLjA0IDUuNjRDMjIuMDQgNS44NiAyMS44OSA2LjA0IDIxLjY4IDYuMUMyMC4wMiA2LjUyIDE5LjA4IDcuNDYgMTguNjYgOS4xMkMxOC42IDkuMzMgMTguNDIgOS40OCAxOC4yIDkuNDhDMTcuOTggOS40OCAxNy44IDkuMzMgMTcuNzQgOS4xMkMxNy4zMiA3LjQ2IDE2LjM4IDYuNTIgMTQuNzIgNi4xQzE0LjUxIDYuMDQgMTQuMzYgNS44NiAxNC4zNiA1LjY0QzE0LjM2IDUuNDIgMTQuNTEgNS4yNCAxNC43MiA1LjE4QzE2LjM4IDQuNzYgMTcuMzIgMy44MiAxNy43NCAyLjE2QzE3LjggMS45NSAxNy45OCAxLjggMTguMiAxLjhaIiBmaWxsPSJ1cmwoI2IpIi8+PGNpcmNsZSBjeD0iMTcuOCIgY3k9IjE4LjIiIHI9IjEuMzUiIGZpbGw9IiM2MzY2RjEiLz48L3N2Zz4=",
|
|
86
|
-
"maxConcurrency":
|
|
86
|
+
"maxConcurrency": 2
|
|
87
87
|
}
|
|
88
88
|
}
|