gemini-reverse 1.0.6 → 1.0.8

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 CHANGED
@@ -6,15 +6,18 @@ An unofficial Node.js client for [Google Gemini](https://gemini.google.com), ins
6
6
 
7
7
  ## Features
8
8
 
9
- - **Persistent Cookies** — Automatically refreshes cookies in the background. Optimized for always-on services.
9
+ - **Persistent Cookies** — Automatically refreshes cookies in the background with jitter to prevent synchronized requests. Optimized for always-on services.
10
10
  - **Image Generation** — Natively supports generating and editing images with natural language.
11
11
  - **Video & Audio Generation** — Supports generating videos and audio/music content natively.
12
12
  - **Deep Research** — Full deep research workflow with plan creation, status polling, and result retrieval.
13
+ - **Extended Thinking** — Enables deeper reasoning mode on supported models.
13
14
  - **System Prompt** — Supports customizing the model's system prompt with [Gemini Gems](https://gemini.google.com/gems/view).
14
15
  - **Extension Support** — Supports generating content with Gemini extensions such as YouTube and Gmail.
15
16
  - **Classified Outputs** — Categorizes text, thoughts, images, videos, and audio in the response.
16
- - **Streaming Mode** — Supports stream generation, yielding partial outputs as they are generated.
17
+ - **Streaming Mode** — Supports stream generation with an incremental stateful frame parser, yielding partial outputs as they are generated.
17
18
  - **Dynamic Model Discovery** — Automatically discovers available models from your account at initialization.
19
+ - **Quota & Usage Info** — Exposes account quota, compute usage, and abuse status after initialization.
20
+ - **Activity Watchdog** — Background heartbeat task that keeps the session alive automatically.
18
21
  - **TypeScript Support** — Full TypeScript type declarations included out of the box.
19
22
 
20
23
  ## Table of Contents
@@ -34,6 +37,7 @@ An unofficial Node.js client for [Google Gemini](https://gemini.google.com), ins
34
37
  - [Delete a Conversation](#delete-a-conversation)
35
38
  - [Temporary Mode](#temporary-mode)
36
39
  - [Streaming Mode](#streaming-mode)
40
+ - [Extended Thinking](#extended-thinking)
37
41
  - [Select Language Model](#select-language-model)
38
42
  - [List Available Models](#list-available-models)
39
43
  - [Apply System Prompt with Gemini Gems](#apply-system-prompt-with-gemini-gems)
@@ -49,6 +53,7 @@ An unofficial Node.js client for [Google Gemini](https://gemini.google.com), ins
49
53
  - [Check and Switch to Other Reply Candidates](#check-and-switch-to-other-reply-candidates)
50
54
  - [Deep Research](#deep-research)
51
55
  - [Account Status](#account-status)
56
+ - [Quota and Usage Info](#quota-and-usage-info)
52
57
  - [Error Handling](#error-handling)
53
58
  - [Cookie Persistence](#cookie-persistence)
54
59
  - [TypeScript](#typescript)
@@ -73,7 +78,7 @@ npm install gemini-reverse
73
78
 
74
79
  ### Initialization
75
80
 
76
- Import the package and initialize a client with your cookies. After a successful initialization, the client will automatically refresh `__Secure-1PSIDTS` in the background as long as the process is alive.
81
+ Import the package and initialize a client with your cookies. After a successful initialization, the client will automatically refresh `__Secure-1PSIDTS` in the background with random jitter, and start a heartbeat watchdog to keep the session alive.
77
82
 
78
83
  ```js
79
84
  const { GeminiClient } = require('gemini-reverse');
@@ -88,7 +93,7 @@ await client.init({
88
93
  timeout: 300000, // request timeout in ms, default 300000
89
94
  autoClose: false, // auto-close client after inactivity
90
95
  closeDelay: 300000, // inactivity delay before closing in ms
91
- autoRefresh: true, // auto-refresh cookies in the background
96
+ autoRefresh: true, // auto-refresh cookies + start activity watchdog
92
97
  refreshInterval: 540000 // cookie refresh interval in ms
93
98
  });
94
99
  ```
@@ -215,7 +220,7 @@ console.log(res2.text);
215
220
 
216
221
  ### Streaming Mode
217
222
 
218
- For longer responses, use streaming mode to receive partial outputs as they are generated. The `text_delta` attribute contains only the **new characters** received since the last yield, making it easy to display incremental updates.
223
+ For longer responses, use streaming mode to receive partial outputs as they are generated. The response uses a stateful `StreamingFrameParser` internally, so partial frames are buffered efficiently across chunks without rescanning. The `text_delta` attribute contains only the **new characters** received since the last yield.
219
224
 
220
225
  ```js
221
226
  for await (const chunk of client.generateContentStream({
@@ -235,6 +240,36 @@ for await (const chunk of chat.sendMessageStream({ prompt: 'Tell me a long story
235
240
  }
236
241
  ```
237
242
 
243
+ ### Extended Thinking
244
+
245
+ Pass `extended_thinking: true` to enable deeper reasoning mode. This causes the model to spend more time planning before responding. Supported on Pro and Advanced tier models.
246
+
247
+ ```js
248
+ const { Model } = require('gemini-reverse');
249
+
250
+ const response = await client.generateContent({
251
+ prompt: 'Solve this step by step: If a train travels at 120 km/h and needs to cover 450 km, how long does it take?',
252
+ model: Model.ADVANCED_PRO,
253
+ extended_thinking: true,
254
+ });
255
+
256
+ if (response.thoughts) {
257
+ console.log('Thinking process:', response.thoughts);
258
+ }
259
+ console.log('Answer:', response.text);
260
+ ```
261
+
262
+ Also works in streaming and chat:
263
+
264
+ ```js
265
+ const chat = client.startChat({ model: Model.ADVANCED_FLASH });
266
+ const res = await chat.sendMessage({
267
+ prompt: 'Explain the P vs NP problem.',
268
+ extended_thinking: true,
269
+ });
270
+ console.log(res.text);
271
+ ```
272
+
238
273
  ### Select Language Model
239
274
 
240
275
  Specify which language model to use by passing a `model` argument. Available models are discovered dynamically at init time based on your account tier.
@@ -248,7 +283,7 @@ const response1 = await client.generateContent({
248
283
  model: Model.BASIC_FLASH,
249
284
  });
250
285
 
251
- // Using a model name string
286
+ // Using a model name string (case-insensitive)
252
287
  const chat = client.startChat({ model: 'gemini-3-pro' });
253
288
 
254
289
  // Using a custom model header dict
@@ -256,7 +291,9 @@ const chat2 = client.startChat({
256
291
  model: {
257
292
  model_name: 'custom',
258
293
  model_header: {
259
- 'x-goog-ext-525001261-jspb': '[1,null,null,null,"MODEL_ID",null,null,0,[4],null,null,2]',
294
+ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"MODEL_ID",null,null,0,[4,5,6,8],null,null,1,null,null,1]',
295
+ 'x-goog-ext-73010989-jspb': '[0]',
296
+ 'x-goog-ext-73010990-jspb': '[0,0,0]',
260
297
  },
261
298
  },
262
299
  });
@@ -269,13 +306,13 @@ const chat2 = client.startChat({
269
306
  | `Model.UNSPECIFIED` | `unspecified` | Default, lets Gemini choose |
270
307
  | `Model.BASIC_PRO` | `gemini-3-pro` | Free tier |
271
308
  | `Model.BASIC_FLASH` | `gemini-3-flash` | Free tier, fastest |
272
- | `Model.BASIC_THINKING` | `gemini-3-flash-thinking` | Free tier, thinking model |
309
+ | `Model.BASIC_LITE` | `gemini-3-lite` | Free tier, lightweight |
273
310
  | `Model.PLUS_PRO` | `gemini-3-pro-plus` | Plus tier |
274
311
  | `Model.PLUS_FLASH` | `gemini-3-flash-plus` | Plus tier |
275
- | `Model.PLUS_THINKING` | `gemini-3-flash-thinking-plus` | Plus tier |
312
+ | `Model.PLUS_LITE` | `gemini-3-lite-plus` | Plus tier |
276
313
  | `Model.ADVANCED_PRO` | `gemini-3-pro-advanced` | Advanced tier |
277
314
  | `Model.ADVANCED_FLASH` | `gemini-3-flash-advanced` | Advanced tier |
278
- | `Model.ADVANCED_THINKING` | `gemini-3-flash-thinking-advanced` | Advanced tier |
315
+ | `Model.ADVANCED_LITE` | `gemini-3-lite-advanced` | Advanced tier |
279
316
 
280
317
  ### List Available Models
281
318
 
@@ -288,7 +325,7 @@ const models = client.listModels();
288
325
  if (models) {
289
326
  for (const model of models) {
290
327
  console.log(`${model.model_id} → ${model.model_name || model.display_name}`);
291
- console.log(` capacity: ${model.capacity}, advanced_only: ${model.advanced_only}`);
328
+ console.log(` capacity: ${model.capacity}, model_number: ${model.model_number}, advanced_only: ${model.advanced_only}`);
292
329
  }
293
330
  }
294
331
  ```
@@ -377,7 +414,7 @@ When using thinking-capable models, the model's internal reasoning is exposed vi
377
414
  ```js
378
415
  const response = await client.generateContent({
379
416
  prompt: 'What is 17 × 23?',
380
- model: Model.BASIC_THINKING,
417
+ model: Model.BASIC_FLASH,
381
418
  });
382
419
 
383
420
  if (response.thoughts) {
@@ -582,6 +619,41 @@ if (client.accountStatus === AccountStatus.AVAILABLE) {
582
619
  | `AccountStatus.GUARDIAN_APPROVAL_REQUIRED` | 1057 | Requires parental approval |
583
620
  | `AccountStatus.LOCATION_REJECTED` | 1060 | Not available in your country/region |
584
621
 
622
+ ### Quota and Usage Info
623
+
624
+ After initialization, the client fetches account quota limits and compute usage metrics automatically. Access them via `client.quotas`, `client.usageInfo`, and `client.abuseStatus`.
625
+
626
+ ```js
627
+ await client.init();
628
+
629
+ // Quota limits (Flash, Pro, extra features)
630
+ const quotas = client.quotas;
631
+ for (const [id, q] of Object.entries(quotas)) {
632
+ if (q.label) {
633
+ const remaining = q.remaining !== null ? `${q.remaining}/${q.total}` : 'unlimited';
634
+ console.log(`${q.label}: ${remaining} credits (${q.usage_percentage?.toFixed(1) ?? '?'}% used)`);
635
+ }
636
+ }
637
+
638
+ // Compute usage (5-hour and weekly windows)
639
+ const usage = client.usageInfo;
640
+ if (usage.tier) {
641
+ console.log(`Account tier: ${usage.tier.label}`);
642
+ }
643
+ if (usage.current_5h) {
644
+ console.log(`5h window: ${usage.current_5h.remaining_credits} credits remaining (${usage.current_5h.usage_percentage}% used)`);
645
+ }
646
+ if (usage.weekly) {
647
+ console.log(`Weekly: ${usage.weekly.remaining_credits} credits remaining`);
648
+ }
649
+
650
+ // Abuse status
651
+ const abuse = client.abuseStatus;
652
+ if (abuse) {
653
+ console.log(`Account clean: ${abuse.is_clean}`);
654
+ }
655
+ ```
656
+
585
657
  ## Error Handling
586
658
 
587
659
  ```js
@@ -651,13 +723,17 @@ import {
651
723
  AccountStatus,
652
724
  DeepResearchPlan,
653
725
  DeepResearchResult,
726
+ StreamingFrameParser,
654
727
  } from 'gemini-reverse';
655
728
 
656
729
  const client = new GeminiClient({ secure_1psid: '...' });
657
730
  await client.init();
658
731
 
659
732
  const chat: ChatSession = client.startChat({ model: 'gemini-3-flash' });
660
- const response: ModelOutput = await chat.sendMessage({ prompt: 'Hello!' });
733
+ const response: ModelOutput = await chat.sendMessage({
734
+ prompt: 'Hello!',
735
+ extended_thinking: false,
736
+ });
661
737
 
662
738
  console.log(response.text);
663
739
 
@@ -667,6 +743,11 @@ if (history) {
667
743
  }
668
744
 
669
745
  const models: AvailableModel[] | null = client.listModels();
746
+
747
+ // Access quota and usage after init
748
+ console.log(client.quotas);
749
+ console.log(client.usageInfo);
750
+ console.log(client.abuseStatus);
670
751
  ```
671
752
 
672
753
  ## Project Structure
@@ -692,9 +773,9 @@ gemini-reverse/
692
773
  │ └── video.js # Video, GeneratedVideo, GeneratedMedia
693
774
  ├── utils/
694
775
  │ ├── accessToken.js # cookie handling & init request
695
- │ ├── parsing.js # response parsing utilities
776
+ │ ├── parsing.js # response parsing + StreamingFrameParser
696
777
  │ ├── research.js # deep research payload extractors
697
- │ ├── rotate.js # cookie rotation (kept from original)
778
+ │ ├── rotate.js # cookie rotation
698
779
  │ └── upload.js # file upload helpers
699
780
  └── components/
700
781
  ├── chatMixin.js # chat history methods
package/client.js CHANGED
@@ -25,7 +25,7 @@ const { Gem } = require('./types/gem');
25
25
  const { getAccessToken, cookieStr, parseCookies, parseProxy } = require('./utils/accessToken');
26
26
  const { rotate1psidts } = require('./utils/rotate');
27
27
  const { uploadFile, parseFileName } = require('./utils/upload');
28
- const { getDeltaByFpLen, getNestedValue, parseResponseByFrame, extractJsonFromResponse } = require('./utils/parsing');
28
+ const { getDeltaByFpLen, getNestedValue, parseResponseByFrame, extractJsonFromResponse, StreamingFrameParser } = require('./utils/parsing');
29
29
  const { extractDeepResearchPlan } = require('./utils/research');
30
30
 
31
31
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
@@ -66,10 +66,16 @@ class GeminiClient {
66
66
  this.watchdogTimeout = 30000;
67
67
  this.verbose = false;
68
68
  this._running = false;
69
+ this._sessionid = uuidv4().toUpperCase();
69
70
  this._reqid = Math.floor(Math.random() * 90000) + 10000;
70
71
  this._modelRegistry = {};
71
72
  this._recentChats = null;
72
73
  this._gems = null;
74
+ this._quotas = {};
75
+ this._usageInfo = {};
76
+ this._abuseStatus = null;
77
+ this._lastActivityTime = 0;
78
+ this._activityTask = null;
73
79
 
74
80
  if (secure_1psid) {
75
81
  this.cookies['__Secure-1PSID'] = secure_1psid;
@@ -102,6 +108,7 @@ class GeminiClient {
102
108
  this.pushId = pushId || 'feeds/mcudyrk2a4khkz';
103
109
  this.cookies = validCookies;
104
110
  this._running = true;
111
+ this._sessionid = uuidv4().toUpperCase();
105
112
  this._reqid = Math.floor(Math.random() * 90000) + 10000;
106
113
  this.timeout = timeout;
107
114
  this.autoClose = autoClose;
@@ -117,6 +124,9 @@ class GeminiClient {
117
124
 
118
125
  await this._initRpc();
119
126
 
127
+ if (this._activityTask) { clearInterval(this._activityTask); this._activityTask = null; }
128
+ if (autoRefresh && this._checkAccountStatus()) this._startActivityWatchdog();
129
+
120
130
  if (this.verbose) console.log('Gemini client initialized successfully.');
121
131
  } catch (e) { await this.close(); throw e; }
122
132
  }
@@ -126,32 +136,68 @@ class GeminiClient {
126
136
  this._running = false;
127
137
  if (this.closeTask) { clearTimeout(this.closeTask); this.closeTask = null; }
128
138
  if (this.refreshTask) { clearInterval(this.refreshTask); this.refreshTask = null; }
139
+ if (this._activityTask) { clearInterval(this._activityTask); this._activityTask = null; }
129
140
  }
130
141
 
142
+ get quotas() { return this._quotas; }
143
+ get usageInfo() { return this._usageInfo; }
144
+ get abuseStatus() { return this._abuseStatus; }
145
+
131
146
  _resetCloseTask() {
132
147
  if (this.closeTask) { clearTimeout(this.closeTask); this.closeTask = null; }
133
148
  this.closeTask = setTimeout(() => this.close(), this.closeDelay);
134
149
  }
135
150
 
136
151
  _startAutoRefresh() {
137
- const interval = Math.max(this.refreshInterval, 60000);
138
- this.refreshTask = setInterval(async () => {
152
+ const baseInterval = Math.max(this.refreshInterval, 60000);
153
+ const jitter = () => (Math.random() - 0.5) * 30000;
154
+ const scheduleNext = () => {
139
155
  if (!this._running) return;
140
- try {
141
- const [new1psidts, rotatedCookies] = await rotate1psidts(this.cookies, this.proxy);
142
- if (rotatedCookies) Object.assign(this.cookies, rotatedCookies);
143
- if (!new1psidts) console.warn('Rotation response did not contain a new __Secure-1PSIDTS.');
144
- } catch (e) {
145
- console.warn(`Unexpected error while refreshing cookies: ${e.message}`);
146
- }
147
- }, interval);
156
+ this.refreshTask = setTimeout(async () => {
157
+ if (!this._running) return;
158
+ try {
159
+ const [new1psidts, rotatedCookies] = await rotate1psidts(this.cookies, this.proxy);
160
+ if (rotatedCookies) Object.assign(this.cookies, rotatedCookies);
161
+ if (!new1psidts) console.warn('Rotation response did not contain a new __Secure-1PSIDTS.');
162
+ } catch (e) {
163
+ console.warn(`Unexpected error while refreshing cookies: ${e.message}`);
164
+ }
165
+ scheduleNext();
166
+ }, Math.max(60000, baseInterval + jitter()));
167
+ };
168
+ scheduleNext();
169
+ }
170
+
171
+ _startActivityWatchdog() {
172
+ const scheduleNext = () => {
173
+ if (!this._running) return;
174
+ const interval = 60000 + Math.random() * 240000;
175
+ this._activityTask = setTimeout(async () => {
176
+ if (!this._running) return;
177
+ if (!this._checkAccountStatus()) return;
178
+ try {
179
+ await this._syncActivity();
180
+ } catch {}
181
+ scheduleNext();
182
+ }, interval);
183
+ };
184
+ scheduleNext();
185
+ }
186
+
187
+ _checkAccountStatus() {
188
+ return this.accountStatus === AccountStatus.AVAILABLE ||
189
+ this.accountStatus === AccountStatus.ACCOUNT_UNTRUSTED;
148
190
  }
149
191
 
150
192
  async _initRpc() {
151
193
  await this._fetchUserStatus();
152
- await this._sendBardSettings();
153
- await this._sendBardActivity();
194
+ await this._fetchPreferences();
195
+ await this._syncActivity();
154
196
  await this._fetchRecentChats();
197
+ await this._fetchQuota();
198
+ await this._fetchExtraQuota();
199
+ await this._fetchAbuseStatus();
200
+ await this._fetchUsageInfo();
155
201
  }
156
202
 
157
203
  async _fetchUserStatus() {
@@ -195,6 +241,7 @@ class GeminiClient {
195
241
  })();
196
242
  const [capacity, capacityField] = AvailableModel.computeCapacity(tierFlags, capabilityFlags);
197
243
  const idNameMapping = AvailableModel.buildModelIdNameMapping();
244
+ const idNumberMapping = AvailableModel.buildModelIdNumberMapping();
198
245
 
199
246
  for (const modelData of modelsList) {
200
247
  if (!Array.isArray(modelData)) continue;
@@ -216,6 +263,7 @@ class GeminiClient {
216
263
  description,
217
264
  capacity,
218
265
  capacity_field: capacityField,
266
+ model_number: idNumberMapping[modelId] || 1,
219
267
  is_available: isModelAvailable,
220
268
  });
221
269
  this._modelRegistry[modelId] = model;
@@ -228,18 +276,178 @@ class GeminiClient {
228
276
  }
229
277
  }
230
278
 
231
- async _sendBardSettings() {
279
+ async _fetchPreferences() {
232
280
  await this._batchExecute([
233
- new RPCData({ rpcid: GRPC.BARD_SETTINGS, payload: BARD_SETTINGS_PAYLOAD }),
281
+ new RPCData({ rpcid: GRPC.READ_USER_PREFERENCES, payload: BARD_SETTINGS_PAYLOAD }),
234
282
  ]);
235
283
  }
236
284
 
237
- async _sendBardActivity() {
285
+ async _syncActivity() {
286
+ this._lastActivityTime = Date.now();
287
+ if (!this._checkAccountStatus()) return;
238
288
  await this._batchExecute([
239
- new RPCData({ rpcid: GRPC.BARD_SETTINGS, payload: '[[["bard_activity_enabled"]]]' }),
289
+ new RPCData({ rpcid: GRPC.READ_USER_PREFERENCES, payload: '[[["bard_activity_enabled"]]]' }),
240
290
  ]);
241
291
  }
242
292
 
293
+ async _fetchQuota() {
294
+ if (!this._checkAccountStatus()) return;
295
+ const payloads = [
296
+ { key: 'flash', payload: '[[[1,11],[2,11],[6,11]]]' },
297
+ { key: 'advanced', payload: '[[[1,4],[6,6],[1,15]]]' },
298
+ ];
299
+ const actionLabels = { 4: 'Gemini Pro', 11: 'Gemini Flash', 15: 'Gemini Flash Thinking' };
300
+ for (const { key, payload } of payloads) {
301
+ try {
302
+ const res = await this._batchExecute([
303
+ new RPCData({ rpcid: GRPC.CHECK_GEMINI_QUOTA, payload }),
304
+ ]);
305
+ const parts = extractJsonFromResponse(res.data);
306
+ for (const part of parts) {
307
+ const bodyStr = getNestedValue(part, [2]);
308
+ if (!bodyStr) continue;
309
+ let body;
310
+ try { body = JSON.parse(bodyStr); } catch { continue; }
311
+ const items = getNestedValue(body, [0]);
312
+ if (!Array.isArray(items)) continue;
313
+ for (const item of items) {
314
+ const quotaIdList = getNestedValue(item, [0], []);
315
+ const actionId = getNestedValue(item, [0, 1]);
316
+ const usageLevel = getNestedValue(item, [2]);
317
+ const resetTs = getNestedValue(item, [3, 0]);
318
+ const total = getNestedValue(item, [4]);
319
+ const remaining = getNestedValue(item, [5]);
320
+ const quotaId = quotaIdList.filter(x => x != null).join('-') || String(actionId);
321
+ const label = actionLabels[actionId] || `Gemini ${key}`;
322
+ this._quotas[quotaId] = { usage_percentage: usageLevel, reset_time: resetTs, total, remaining, action_id: actionId, label: `${label} [${quotaId}]` };
323
+ }
324
+ }
325
+ } catch {}
326
+ }
327
+ }
328
+
329
+ async _fetchExtraQuota() {
330
+ try {
331
+ const res = await this._batchExecute([
332
+ new RPCData({ rpcid: GRPC.CHECK_QUOTA, payload: '[]' }),
333
+ ]);
334
+ const parts = extractJsonFromResponse(res.data);
335
+ for (const part of parts) {
336
+ const bodyStr = getNestedValue(part, [2]);
337
+ if (!bodyStr) continue;
338
+ let body;
339
+ try { body = JSON.parse(bodyStr); } catch { continue; }
340
+ const isBlocked = getNestedValue(body, [0]);
341
+ const usageLevel = getNestedValue(body, [1]);
342
+ const resetTs = getNestedValue(body, [2, 0]);
343
+ if (!this._quotas.extra) this._quotas.extra = {};
344
+ this._quotas.extra.default = {
345
+ is_blocked: isBlocked,
346
+ usage_percentage: typeof usageLevel === 'number' ? usageLevel * 100 : null,
347
+ reset_time: resetTs,
348
+ };
349
+ }
350
+ } catch {}
351
+ }
352
+
353
+ async _fetchAbuseStatus() {
354
+ try {
355
+ const res = await this._batchExecute([
356
+ new RPCData({ rpcid: GRPC.GET_ABUSE_STATUS, payload: '[]' }),
357
+ ]);
358
+ const parts = extractJsonFromResponse(res.data);
359
+ for (const part of parts) {
360
+ const bodyStr = getNestedValue(part, [2]);
361
+ if (!bodyStr) continue;
362
+ let body;
363
+ try { body = JSON.parse(bodyStr); } catch { continue; }
364
+ const abuseInfo = getNestedValue(body, [1]);
365
+ if (!abuseInfo) {
366
+ this._abuseStatus = { is_clean: true, status_code: null, signal: null };
367
+ } else {
368
+ const rawStatus = getNestedValue(abuseInfo, [1]);
369
+ const signal = getNestedValue(abuseInfo, [3, 1]);
370
+ const statusCode = typeof rawStatus === 'number' ? Math.floor(rawStatus / 1000000) : null;
371
+ this._abuseStatus = { is_clean: false, status_code: statusCode, signal };
372
+ if (this.verbose) console.warn(`Potential account restriction detected: ${JSON.stringify(this._abuseStatus)}`);
373
+ }
374
+ }
375
+ } catch {}
376
+ }
377
+
378
+ async _fetchUsageInfo() {
379
+ if (!this._checkAccountStatus()) return;
380
+ const tierLabels = { 1: 'FREE', 2: 'PRO', 3: 'ULTRA', 4: 'PLUS', 6: 'ULTRA' };
381
+ const metricWindows = { 1: ['current_5h', '5h'], 2: ['weekly', 'weekly'] };
382
+ try {
383
+ const res = await this._batchExecute([
384
+ new RPCData({ rpcid: GRPC.GET_USAGE_INFO, payload: '[]' }),
385
+ ], 2, true, '/usage');
386
+ const parts = extractJsonFromResponse(res.data);
387
+ for (const part of parts) {
388
+ const bodyStr = getNestedValue(part, [2]);
389
+ if (!bodyStr) continue;
390
+ let body;
391
+ try { body = JSON.parse(bodyStr); } catch { continue; }
392
+ const tierId = getNestedValue(body, [0]);
393
+ const usageItems = getNestedValue(body, [1], []);
394
+ const useOverage = getNestedValue(body, [2]);
395
+ const info = {
396
+ tier: { id: tierId, label: tierLabels[tierId] || null },
397
+ use_overage_ai_credits: useOverage,
398
+ current_5h: null,
399
+ weekly: null,
400
+ };
401
+ if (Array.isArray(usageItems)) {
402
+ for (const item of usageItems) {
403
+ const remaining = getNestedValue(item, [0]);
404
+ const usageLevel = getNestedValue(item, [1]);
405
+ const metricType = getNestedValue(item, [2]);
406
+ const resetTs = getNestedValue(item, [3, 0, 0]);
407
+ if (metricType === 3) { info.ai_credits_remaining = remaining; continue; }
408
+ const [metricLabel] = metricWindows[metricType] || [`type_${metricType}`];
409
+ info[metricLabel] = {
410
+ type: metricType,
411
+ remaining_credits: remaining,
412
+ usage_level: usageLevel,
413
+ usage_percentage: typeof usageLevel === 'number' ? Math.round(usageLevel * 100) : null,
414
+ reset_at: resetTs,
415
+ };
416
+ }
417
+ }
418
+ this._usageInfo = info;
419
+ this._quotas.usage_info = info;
420
+ }
421
+ } catch {}
422
+ }
423
+
424
+ _parseRpcResults(responseText, targetId) {
425
+ const results = [];
426
+ try {
427
+ const parts = extractJsonFromResponse(responseText);
428
+ for (const part of parts) {
429
+ if (getNestedValue(part, [1]) !== targetId) continue;
430
+ const rejectCode = getNestedValue(part, [5, 0]);
431
+ if (rejectCode === 7) {
432
+ this.accountStatus = AccountStatus.UNAUTHENTICATED;
433
+ break;
434
+ }
435
+ const bodyStr = getNestedValue(part, [2]);
436
+ if (!bodyStr) continue;
437
+ try { results.push(JSON.parse(bodyStr)); } catch {}
438
+ }
439
+ } catch {}
440
+ return results;
441
+ }
442
+
443
+ async _sendBardSettings() {
444
+ await this._fetchPreferences();
445
+ }
446
+
447
+ async _sendBardActivity() {
448
+ await this._syncActivity();
449
+ }
450
+
243
451
  listModels() {
244
452
  const vals = Object.values(this._modelRegistry);
245
453
  return vals.length > 0 ? vals : null;
@@ -247,8 +455,9 @@ class GeminiClient {
247
455
 
248
456
  _resolveModelByName(name) {
249
457
  if (name in this._modelRegistry) return this._modelRegistry[name];
458
+ const lower = name.toLowerCase();
250
459
  for (const m of Object.values(this._modelRegistry)) {
251
- if (m.model_name === name || m.display_name === name) return m;
460
+ if (m.model_name.toLowerCase() === lower || m.display_name.toLowerCase() === lower) return m;
252
461
  }
253
462
  return Model.fromName(name);
254
463
  }
@@ -280,7 +489,7 @@ class GeminiClient {
280
489
  return model;
281
490
  }
282
491
 
283
- async _batchExecute(payloads, retries = 2, closeOnError = true) {
492
+ async _batchExecute(payloads, retries = 2, closeOnError = true, sourcePath = '/app') {
284
493
  let lastErr;
285
494
  for (let attempt = 0; attempt <= retries; attempt++) {
286
495
  try {
@@ -292,7 +501,7 @@ class GeminiClient {
292
501
  hl: this.language || 'en',
293
502
  _reqid: String(_reqid),
294
503
  rt: 'c',
295
- 'source-path': '/app',
504
+ 'source-path': sourcePath,
296
505
  });
297
506
  if (this.buildLabel) params.set('bl', this.buildLabel);
298
507
  if (this.sessionId) params.set('f.sid', this.sessionId);
@@ -340,6 +549,7 @@ class GeminiClient {
340
549
  chat = null,
341
550
  temporary = false,
342
551
  deep_research = false,
552
+ extended_thinking = false,
343
553
  } = {}) {
344
554
  if (!this._running) {
345
555
  await this.init({
@@ -369,7 +579,7 @@ class GeminiClient {
369
579
  const ss = { last_texts: {}, last_thoughts: {}, last_progress_time: Date.now(), is_thinking: false, is_queueing: false };
370
580
  let output = null;
371
581
  for await (const out of this._generate({
372
- prompt, fileData, model, gem, chat, temporary, ss, deep_research,
582
+ prompt, fileData, model, gem, chat, temporary, ss, deep_research, extended_thinking,
373
583
  })) output = out;
374
584
  if (!output) throw new GeminiError('Failed to generate contents. No output data found in response.');
375
585
  if (chat instanceof ChatSession) { output.metadata = chat.metadata; chat.lastOutput = output; }
@@ -386,6 +596,7 @@ class GeminiClient {
386
596
  chat = null,
387
597
  temporary = false,
388
598
  deep_research = false,
599
+ extended_thinking = false,
389
600
  } = {}) {
390
601
  if (!this._running) {
391
602
  await this.init({
@@ -414,7 +625,7 @@ class GeminiClient {
414
625
  const ss = { last_texts: {}, last_thoughts: {} };
415
626
  let output = null;
416
627
  for await (const out of this._generate({
417
- prompt, fileData, model, gem, chat, temporary, ss, deep_research,
628
+ prompt, fileData, model, gem, chat, temporary, ss, deep_research, extended_thinking,
418
629
  })) {
419
630
  output = out;
420
631
  yield out;
@@ -431,6 +642,7 @@ class GeminiClient {
431
642
  temporary = false,
432
643
  ss = null,
433
644
  deep_research = false,
645
+ extended_thinking = false,
434
646
  }, retries = 5) {
435
647
  if (!prompt) throw new Error('Prompt cannot be empty.');
436
648
 
@@ -438,7 +650,7 @@ class GeminiClient {
438
650
 
439
651
  for (let attempt = 0; attempt <= retries; attempt++) {
440
652
  try {
441
- for await (const out of this._stream({ prompt, fileData, model, gem, chat, temporary, ss, deep_research })) yield out;
653
+ for await (const out of this._stream({ prompt, fileData, model, gem, chat, temporary, ss, deep_research, extended_thinking })) yield out;
442
654
  return;
443
655
  } catch (e) {
444
656
  if (
@@ -451,7 +663,7 @@ class GeminiClient {
451
663
  }
452
664
  }
453
665
 
454
- async* _stream({ prompt, fileData = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false, ss = null, deep_research = false }) {
666
+ async* _stream({ prompt, fileData = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false, ss = null, deep_research = false, extended_thinking = false }) {
455
667
  const _reqid = this._reqid;
456
668
  this._reqid += 100000;
457
669
  const gemId = gem instanceof Gem ? gem.id : gem;
@@ -463,7 +675,7 @@ class GeminiClient {
463
675
  rcid: chat.rcid,
464
676
  } : null;
465
677
 
466
- const inner = new Array(69).fill(null);
678
+ const inner = new Array(81).fill(null);
467
679
  inner[0] = [prompt, 0, null, fileData, null, null, 0];
468
680
  inner[1] = [this.language || 'en'];
469
681
  inner[2] = chat instanceof ChatSession ? chat.metadata : [...DEFAULT_METADATA];
@@ -489,11 +701,24 @@ class GeminiClient {
489
701
  inner[55] = [[1]];
490
702
  }
491
703
  inner[61] = [];
492
- inner[68] = 2;
704
+ inner[68] = 1;
705
+ inner[80] = extended_thinking ? 2 : 1;
493
706
 
494
707
  const uid = uuidv4().toUpperCase();
495
708
  inner[59] = uid;
496
709
 
710
+ const modelHeaders = { ...model.model_header };
711
+ if (MODEL_HEADER_KEY in modelHeaders) {
712
+ try {
713
+ const parsed = JSON.parse(modelHeaders[MODEL_HEADER_KEY]);
714
+ const modelNumber = typeof parsed[parsed.length - 1] === 'number' ? parsed[parsed.length - 1] : null;
715
+ if (typeof modelNumber === 'number') inner[79] = modelNumber;
716
+ parsed.push(extended_thinking ? 2 : 1);
717
+ parsed.push(this.sessionId || null);
718
+ modelHeaders[MODEL_HEADER_KEY] = JSON.stringify(parsed);
719
+ } catch {}
720
+ }
721
+
497
722
  const params = new URLSearchParams({
498
723
  hl: this.language || 'en',
499
724
  _reqid: String(_reqid),
@@ -516,7 +741,7 @@ class GeminiClient {
516
741
  {
517
742
  headers: {
518
743
  ...Headers.GEMINI,
519
- ...model.model_header,
744
+ ...modelHeaders,
520
745
  'x-goog-ext-525005358-jspb': `["${uid}",1]`,
521
746
  ...Headers.SAME_DOMAIN,
522
747
  'Cookie': cookieStr(this.cookies),
@@ -541,7 +766,7 @@ class GeminiClient {
541
766
  let isCompleted = false, isFinalChunk = false;
542
767
  let cid = chat instanceof ChatSession ? chat.cid : '';
543
768
  let rid = chat instanceof ChatSession ? chat.rid : '';
544
- let buf = '';
769
+ const frameParser = new StreamingFrameParser();
545
770
 
546
771
  const processParts = (parts) => {
547
772
  const outs = [];
@@ -676,10 +901,7 @@ class GeminiClient {
676
901
 
677
902
  res.data.on('data', chunk => {
678
903
  try {
679
- buf += chunk.toString('utf8');
680
- if (buf.startsWith(")]}'")) buf = buf.slice(4).trimStart();
681
- const [parts, rem] = parseResponseByFrame(buf);
682
- buf = rem;
904
+ const parts = frameParser.feed(chunk.toString('utf8'));
683
905
  const outs = processParts(parts);
684
906
  for (const o of outs) yielded.push(o);
685
907
  if (outs.length || isThinking || isQueueing) lastProg = Date.now();
@@ -692,10 +914,8 @@ class GeminiClient {
692
914
  res.data.on('end', () => {
693
915
  clearInterval(watchdog);
694
916
  try {
695
- if (buf) {
696
- const [p] = parseResponseByFrame(buf);
697
- for (const o of processParts(p)) yielded.push(o);
698
- }
917
+ const remaining = frameParser.flush();
918
+ for (const o of processParts(remaining)) yielded.push(o);
699
919
  if (!isCompleted && !isFinalChunk) reject(new APIError('Stream interrupted or truncated.'));
700
920
  else resolve();
701
921
  } catch (e) { reject(e); }
@@ -923,7 +1143,7 @@ class ChatSession {
923
1143
  get rcid() { return this._metadata[2]; }
924
1144
  set rcid(v) { this._metadata[2] = v; }
925
1145
 
926
- async sendMessage({ prompt, files = null, temporary = false, deep_research = false } = {}) {
1146
+ async sendMessage({ prompt, files = null, temporary = false, deep_research = false, extended_thinking = false } = {}) {
927
1147
  return this.geminiclient.generateContent({
928
1148
  prompt,
929
1149
  files,
@@ -932,10 +1152,11 @@ class ChatSession {
932
1152
  chat: this,
933
1153
  temporary,
934
1154
  deep_research,
1155
+ extended_thinking,
935
1156
  });
936
1157
  }
937
1158
 
938
- async* sendMessageStream({ prompt, files = null, temporary = false, deep_research = false } = {}) {
1159
+ async* sendMessageStream({ prompt, files = null, temporary = false, deep_research = false, extended_thinking = false } = {}) {
939
1160
  for await (const out of this.geminiclient.generateContentStream({
940
1161
  prompt,
941
1162
  files,
@@ -944,6 +1165,7 @@ class ChatSession {
944
1165
  chat: this,
945
1166
  temporary,
946
1167
  deep_research,
1168
+ extended_thinking,
947
1169
  })) yield out;
948
1170
  }
949
1171
 
@@ -14,6 +14,7 @@ class ChatMixin {
14
14
  }
15
15
 
16
16
  async _fetchRecentChats(recent = 13) {
17
+ if (!this._checkAccountStatus()) return;
17
18
  const fetchBatch = async (payload) => {
18
19
  return this._batchExecute([
19
20
  new RPCData({ rpcid: GRPC.LIST_CHATS, payload: JSON.stringify([recent, null, payload]) }),
package/constants.js CHANGED
@@ -9,11 +9,11 @@ const ARTIFACTS_RE = /http:\/\/googleusercontent\.com\/\w+\/\d+\n*/g;
9
9
  const DEFAULT_METADATA = ['', '', '', null, null, null, null, null, null, ''];
10
10
  const MODEL_HEADER_KEY = 'x-goog-ext-525001261-jspb';
11
11
 
12
- function buildModelHeader(modelId, capacityTail) {
12
+ function buildModelHeader(modelId, capacityTail, modelNumber = 1) {
13
13
  return {
14
- [MODEL_HEADER_KEY]: `[1,null,null,null,"${modelId}",null,null,0,[4],null,null,${capacityTail}]`,
14
+ [MODEL_HEADER_KEY]: `[1,null,null,null,"${modelId}",null,null,0,[4,5,6,8],null,null,${capacityTail},null,null,${modelNumber}]`,
15
15
  'x-goog-ext-73010989-jspb': '[0]',
16
- 'x-goog-ext-73010990-jspb': '[0]',
16
+ 'x-goog-ext-73010990-jspb': '[0,0,0]',
17
17
  };
18
18
  }
19
19
 
@@ -29,22 +29,56 @@ const Endpoint = {
29
29
  const GRPC = {
30
30
  LIST_CHATS: 'MaZiqc',
31
31
  READ_CHAT: 'hNvQHb',
32
+ GET_CONVERSATION_TURN: 'EqPOKe',
32
33
  DELETE_CHAT_1: 'GzXR5e',
33
34
  DELETE_CHAT_2: 'qWymEb',
35
+ UPDATE_CONVERSATION: 'MUAZcd',
36
+ MARK_LAST_CONVERSATION_TURN: 'kOWVAe',
37
+ GENERATE_HEADLINE: 'ukz1Fe',
38
+
34
39
  LIST_GEMS: 'CNgdBe',
35
40
  CREATE_GEM: 'oMH3Zd',
41
+ GET_GEM: 'HcT8bb',
36
42
  UPDATE_GEM: 'kHv0Vd',
37
43
  DELETE_GEM: 'UXcSJb',
44
+ DELETE_GEM_AND_CONVERSATIONS: 'Nwkn9',
45
+
46
+ CREATE_TASK: 'Jba3ib',
47
+ GET_TASK: 'kwDCne',
48
+ GET_ALL_TASKS: 'XPSWpd',
49
+ GET_TASKS_IN_CONVERSATION: 'qWymEb',
50
+ GET_CANDIDATES: 'PCck7e',
51
+ LIST_DISCOVERY_CARDS: 'ku4Jyf',
52
+ GET_DISCOVERY_CARD: 'oApPWc',
53
+ LIST_DISCOVERY_BANNERS: 'Te6DCf',
54
+
38
55
  DEEP_RESEARCH_STATUS: 'kwDCne',
39
56
  DEEP_RESEARCH_PREFS: 'L5adhe',
40
57
  DEEP_RESEARCH_BOOTSTRAP: 'ku4Jyf',
41
58
  DEEP_RESEARCH_MODEL_STATE: 'qpEbW',
42
59
  DEEP_RESEARCH_CAPS: 'aPya6c',
43
60
  DEEP_RESEARCH_ACK: 'PCck7e',
61
+
62
+ LIST_GEMINI_APP_ARTIFACTS: 'jGArJ',
63
+ DELETE_GEMINI_APP_ARTIFACTS: 'PGX16d',
64
+
65
+ LIST_MEMORIES: 'ZKcapf',
66
+ CREATE_MEMORY: 'xVRQX',
67
+ UPDATE_MEMORY: 'gSnMcd',
68
+ DELETE_MEMORY: 'Ok9j9b',
69
+ DELETE_ALL_MEMORIES: 'YgU2Cc',
70
+
44
71
  GET_USER_STATUS: 'otAQ7b',
45
72
  LIST_MODELS: 'otAQ7b',
73
+ CHECK_GEMINI_QUOTA: 'qpEbW',
74
+ CHECK_QUOTA: 'aPya6c',
46
75
  GET_FULL_SIZE_IMAGE: 'c8o8Fe',
76
+ GET_ABUSE_STATUS: 'GPRiHf',
77
+ UPDATE_USER_PREFERENCES: 'L5adhe',
78
+ READ_USER_PREFERENCES: 'ESY5D',
47
79
  BARD_SETTINGS: 'ESY5D',
80
+ CONTINUE_SHARED_CONVERSATION: 'ra9Swb',
81
+ GET_USAGE_INFO: 'jSf9Qc',
48
82
  };
49
83
 
50
84
  const Headers = {
@@ -66,67 +100,69 @@ const Headers = {
66
100
  },
67
101
  UPLOAD: { 'X-Tenant-Id': 'bard-storage' },
68
102
  BATCH_EXEC: {
69
- 'x-goog-ext-525001261-jspb': '[1,null,null,null,null,null,null,null,[4]]',
103
+ 'x-goog-ext-525001261-jspb': '[1,null,null,null,null,null,null,null,[4,5,6,8],null,null,null,null,null,null,null]',
70
104
  'x-goog-ext-73010989-jspb': '[0]',
71
105
  },
72
106
  };
73
107
 
74
108
  const _MODEL_KEYS = [
75
- 'UNSPECIFIED', 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_THINKING',
76
- 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_THINKING',
77
- 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_THINKING',
109
+ 'UNSPECIFIED',
110
+ 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_LITE',
111
+ 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_LITE',
112
+ 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_LITE',
78
113
  ];
79
114
 
80
115
  const Model = {
81
116
  UNSPECIFIED: { model_name: 'unspecified', model_header: {}, advanced_only: false },
82
117
  BASIC_PRO: {
83
118
  model_name: 'gemini-3-pro',
84
- model_header: buildModelHeader('9d8ca3786ebdfbea', 1),
119
+ model_header: buildModelHeader('9d8ca3786ebdfbea', 1, 3),
85
120
  advanced_only: false,
86
121
  },
87
122
  BASIC_FLASH: {
88
123
  model_name: 'gemini-3-flash',
89
- model_header: buildModelHeader('fbb127bbb056c959', 1),
124
+ model_header: buildModelHeader('fbb127bbb056c959', 1, 1),
90
125
  advanced_only: false,
91
126
  },
92
- BASIC_THINKING: {
93
- model_name: 'gemini-3-flash-thinking',
94
- model_header: buildModelHeader('5bf011840784117a', 1),
127
+ BASIC_LITE: {
128
+ model_name: 'gemini-3-lite',
129
+ model_header: buildModelHeader('cf41b0e0dd7d53e5', 1, 6),
95
130
  advanced_only: false,
96
131
  },
97
132
  PLUS_PRO: {
98
133
  model_name: 'gemini-3-pro-plus',
99
- model_header: buildModelHeader('e6fa609c3fa255c0', 4),
134
+ model_header: buildModelHeader('e6fa609c3fa255c0', 4, 3),
100
135
  advanced_only: true,
101
136
  },
102
137
  PLUS_FLASH: {
103
138
  model_name: 'gemini-3-flash-plus',
104
- model_header: buildModelHeader('56fdd199312815e2', 4),
139
+ model_header: buildModelHeader('56fdd199312815e2', 4, 1),
105
140
  advanced_only: true,
106
141
  },
107
- PLUS_THINKING: {
108
- model_name: 'gemini-3-flash-thinking-plus',
109
- model_header: buildModelHeader('e051ce1aa80aa576', 4),
142
+ PLUS_LITE: {
143
+ model_name: 'gemini-3-lite-plus',
144
+ model_header: buildModelHeader('8c46e95b1a07cecc', 4, 6),
110
145
  advanced_only: true,
111
146
  },
112
147
  ADVANCED_PRO: {
113
148
  model_name: 'gemini-3-pro-advanced',
114
- model_header: buildModelHeader('e6fa609c3fa255c0', 2),
149
+ model_header: buildModelHeader('e6fa609c3fa255c0', 2, 3),
115
150
  advanced_only: true,
116
151
  },
117
152
  ADVANCED_FLASH: {
118
153
  model_name: 'gemini-3-flash-advanced',
119
- model_header: buildModelHeader('56fdd199312815e2', 2),
154
+ model_header: buildModelHeader('56fdd199312815e2', 2, 1),
120
155
  advanced_only: true,
121
156
  },
122
- ADVANCED_THINKING: {
123
- model_name: 'gemini-3-flash-thinking-advanced',
124
- model_header: buildModelHeader('e051ce1aa80aa576', 2),
157
+ ADVANCED_LITE: {
158
+ model_name: 'gemini-3-lite-advanced',
159
+ model_header: buildModelHeader('8c46e95b1a07cecc', 2, 6),
125
160
  advanced_only: true,
126
161
  },
127
162
  fromName(name) {
163
+ const lower = name.toLowerCase();
128
164
  for (const k of _MODEL_KEYS) {
129
- if (Model[k].model_name === name) return Model[k];
165
+ if (Model[k].model_name === lower) return Model[k];
130
166
  }
131
167
  const names = _MODEL_KEYS.map(k => Model[k].model_name).join(', ');
132
168
  throw new Error(`Unknown model name: ${name}. Available: ${names}`);
package/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface ModelDict {
18
18
 
19
19
  export type ModelInput = string | ModelDef | ModelDict | AvailableModel | null;
20
20
 
21
- export declare function buildModelHeader(modelId: string, capacityTail: string | number): ModelHeader;
21
+ export declare function buildModelHeader(modelId: string, capacityTail: string | number, modelNumber?: number): ModelHeader;
22
22
 
23
23
  export declare const MODEL_HEADER_KEY: string;
24
24
  export declare const STREAMING_FLAG_INDEX: number;
@@ -125,6 +125,7 @@ export declare class AvailableModel {
125
125
  description: string;
126
126
  capacity: number;
127
127
  capacity_field: number;
128
+ model_number: number;
128
129
  is_available: boolean;
129
130
  constructor(opts: {
130
131
  model_id: string;
@@ -133,6 +134,7 @@ export declare class AvailableModel {
133
134
  description: string;
134
135
  capacity: number;
135
136
  capacity_field?: number;
137
+ model_number?: number;
136
138
  is_available?: boolean;
137
139
  });
138
140
  get model_header(): ModelHeader;
@@ -141,6 +143,7 @@ export declare class AvailableModel {
141
143
  repr(): string;
142
144
  static computeCapacity(tierFlags: number[], capabilityFlags: number[]): [number, number];
143
145
  static buildModelIdNameMapping(): Record<string, string>;
146
+ static buildModelIdNumberMapping(): Record<string, number>;
144
147
  }
145
148
 
146
149
  export interface ImageSaveOptions {
@@ -417,6 +420,7 @@ export interface GenerateOptions {
417
420
  chat?: ChatSession | null;
418
421
  temporary?: boolean;
419
422
  deep_research?: boolean;
423
+ extended_thinking?: boolean;
420
424
  }
421
425
 
422
426
  export interface StartChatOptions {
@@ -461,6 +465,9 @@ export declare class GeminiClient {
461
465
  refreshInterval: number;
462
466
  verbose: boolean;
463
467
  watchdogTimeout: number;
468
+ readonly quotas: Record<string, Record<string, unknown>>;
469
+ readonly usageInfo: Record<string, unknown>;
470
+ readonly abuseStatus: { is_clean: boolean; status_code: number | null; signal: unknown } | null;
464
471
 
465
472
  constructor(opts?: GeminiClientOptions);
466
473
 
@@ -508,6 +515,7 @@ export interface SendMessageOptions {
508
515
  files?: (string | Buffer)[] | null;
509
516
  temporary?: boolean;
510
517
  deep_research?: boolean;
518
+ extended_thinking?: boolean;
511
519
  }
512
520
 
513
521
  export declare class ChatSession {
package/index.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  AuthError, APIError, ImageGenerationError, GeminiError, TimeoutError,
12
12
  UsageLimitExceeded, ModelInvalid, TemporarilyBlocked,
13
13
  } = require('./exceptions');
14
+ const { StreamingFrameParser } = require('./utils/parsing');
14
15
  const {
15
16
  Candidate, ChatHistory, ChatTurn, ChatInfo, Gem, GemJar, RPCData,
16
17
  Image, WebImage, GeneratedImage, ModelOutput,
@@ -61,4 +62,5 @@ module.exports = {
61
62
  GeneratedVideo,
62
63
  GeneratedMedia,
63
64
  AvailableModel,
65
+ StreamingFrameParser,
64
66
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-reverse",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Unofficial Node.js client for gemini.google.com — inspired by Gemini-API (Python). Supports streaming, chat sessions, gems, file uploads, and TypeScript.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -3,13 +3,14 @@
3
3
  const { buildModelHeader, MODEL_HEADER_KEY, Model } = require('../constants');
4
4
 
5
5
  class AvailableModel {
6
- constructor({ model_id, model_name, display_name, description, capacity, capacity_field = 12, is_available = true } = {}) {
6
+ constructor({ model_id, model_name, display_name, description, capacity, capacity_field = 12, model_number = 1, is_available = true } = {}) {
7
7
  this.model_id = model_id;
8
8
  this.model_name = model_name;
9
9
  this.display_name = display_name;
10
10
  this.description = description;
11
11
  this.capacity = capacity;
12
12
  this.capacity_field = capacity_field;
13
+ this.model_number = model_number;
13
14
  this.is_available = is_available;
14
15
  }
15
16
 
@@ -20,7 +21,7 @@ class AvailableModel {
20
21
  } else {
21
22
  tail = String(this.capacity);
22
23
  }
23
- return buildModelHeader(this.model_id, tail);
24
+ return buildModelHeader(this.model_id, tail, this.model_number);
24
25
  }
25
26
 
26
27
  get advanced_only() {
@@ -47,9 +48,9 @@ class AvailableModel {
47
48
  static buildModelIdNameMapping() {
48
49
  const result = {};
49
50
  const keys = [
50
- 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_THINKING',
51
- 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_THINKING',
52
- 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_THINKING',
51
+ 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_LITE',
52
+ 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_LITE',
53
+ 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_LITE',
53
54
  ];
54
55
  for (const key of keys) {
55
56
  const member = Model[key];
@@ -71,6 +72,32 @@ class AvailableModel {
71
72
  }
72
73
  return result;
73
74
  }
75
+
76
+ static buildModelIdNumberMapping() {
77
+ const result = {};
78
+ const keys = [
79
+ 'BASIC_PRO', 'BASIC_FLASH', 'BASIC_LITE',
80
+ 'PLUS_PRO', 'PLUS_FLASH', 'PLUS_LITE',
81
+ 'ADVANCED_PRO', 'ADVANCED_FLASH', 'ADVANCED_LITE',
82
+ ];
83
+ for (const key of keys) {
84
+ const member = Model[key];
85
+ if (!member) continue;
86
+ const headerValue = member.model_header[MODEL_HEADER_KEY];
87
+ if (!headerValue) continue;
88
+ try {
89
+ const parsed = JSON.parse(headerValue);
90
+ const modelId = parsed && parsed[4];
91
+ const modelNumber = parsed && parsed[parsed.length - 1];
92
+ if (modelId && typeof modelNumber === 'number' && !(modelId in result)) {
93
+ result[modelId] = modelNumber;
94
+ }
95
+ } catch {
96
+ continue;
97
+ }
98
+ }
99
+ return result;
100
+ }
74
101
  }
75
102
 
76
103
  module.exports = { AvailableModel };
package/utils/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { getAccessToken, sendInitRequest, cookieStr, parseCookies, parseProxy, cacheDir } = require('./accessToken');
4
- const { getDeltaByFpLen, getCleanText, getNestedValue, parseResponseByFrame, extractJsonFromResponse } = require('./parsing');
4
+ const { getDeltaByFpLen, getCleanText, getNestedValue, parseResponseByFrame, extractJsonFromResponse, StreamingFrameParser } = require('./parsing');
5
5
  const { rotate1psidts } = require('./rotate');
6
6
  const { uploadFile, parseFileName, generateRandomName } = require('./upload');
7
7
  const {
@@ -28,6 +28,7 @@ module.exports = {
28
28
  getNestedValue,
29
29
  parseResponseByFrame,
30
30
  extractJsonFromResponse,
31
+ StreamingFrameParser,
31
32
  rotate1psidts,
32
33
  uploadFile,
33
34
  parseFileName,
package/utils/parsing.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const FLICKER_ESC_RE = /\\+[`*_~].*$/;
4
+ const LENGTH_MARKER_RE = /^(\d+)\n/;
4
5
 
5
6
  function getCleanText(s) {
6
7
  if (!s) return '';
@@ -90,45 +91,121 @@ function getNestedValue(data, path, defaultVal = null) {
90
91
  return cur != null ? cur : defaultVal;
91
92
  }
92
93
 
93
- function parseResponseByFrame(content) {
94
- let pos = 0;
95
- const frames = [];
96
- while (pos < content.length) {
97
- while (pos < content.length && /\s/.test(content[pos])) pos++;
98
- if (pos >= content.length) break;
99
- const m = /^(\d+)\n/.exec(content.slice(pos));
100
- if (!m) break;
101
- const len = parseInt(m[1]);
102
- const start = pos + m[1].length;
103
- let chars = 0, units = 0, idx = start;
104
- while (units < len && idx < content.length) {
105
- const cp = content.codePointAt(idx);
94
+ class StreamingFrameParser {
95
+ constructor() {
96
+ this.buffer = '';
97
+ this.expectedUnits = null;
98
+ this.payloadStart = 0;
99
+ this.scannedChars = 0;
100
+ this.scannedUnits = 0;
101
+ this.prefixChecked = false;
102
+ }
103
+
104
+ reset() {
105
+ this.buffer = '';
106
+ this._resetFrameState();
107
+ this.prefixChecked = false;
108
+ }
109
+
110
+ feed(content) {
111
+ if (typeof content !== 'string') throw new TypeError(`Expected string, got ${typeof content}`);
112
+ if (content) this.buffer += content;
113
+ this._stripPrefixOnce();
114
+
115
+ const parsed = [];
116
+ while (true) {
117
+ if (this.expectedUnits === null && !this._readLengthMarker()) break;
118
+ if (this.expectedUnits === null) break;
119
+
120
+ this._scanAvailablePayload();
121
+ if (this.scannedUnits < this.expectedUnits) break;
122
+
123
+ const endPos = this.payloadStart + this.scannedChars;
124
+ const chunk = this.buffer.slice(this.payloadStart, endPos);
125
+ this.buffer = this.buffer.slice(endPos);
126
+ this._resetFrameState();
127
+
128
+ if (!chunk.trim()) continue;
129
+
130
+ try {
131
+ const p = JSON.parse(chunk);
132
+ if (Array.isArray(p)) parsed.push(...p);
133
+ else parsed.push(p);
134
+ } catch {}
135
+ }
136
+ return parsed;
137
+ }
138
+
139
+ flush() {
140
+ return this.feed('');
141
+ }
142
+
143
+ _resetFrameState() {
144
+ this.expectedUnits = null;
145
+ this.payloadStart = 0;
146
+ this.scannedChars = 0;
147
+ this.scannedUnits = 0;
148
+ }
149
+
150
+ _stripPrefixOnce() {
151
+ if (this.prefixChecked) return;
152
+ const prefix = ")]}'";
153
+ if (this.buffer.length < prefix.length && prefix.startsWith(this.buffer)) return;
154
+ if (this.buffer.startsWith(prefix)) {
155
+ this.buffer = this.buffer.slice(prefix.length).trimStart();
156
+ }
157
+ this.prefixChecked = true;
158
+ }
159
+
160
+ _readLengthMarker() {
161
+ let pos = 0;
162
+ while (pos < this.buffer.length && /\s/.test(this.buffer[pos])) pos++;
163
+ if (pos) {
164
+ this.buffer = this.buffer.slice(pos);
165
+ }
166
+ if (!this.buffer.length) return false;
167
+
168
+ const m = LENGTH_MARKER_RE.exec(this.buffer);
169
+ if (!m) return false;
170
+
171
+ const lenStr = m[1];
172
+ this.expectedUnits = parseInt(lenStr, 10);
173
+ this.payloadStart = lenStr.length;
174
+ this.scannedChars = 0;
175
+ this.scannedUnits = 0;
176
+ return true;
177
+ }
178
+
179
+ _scanAvailablePayload() {
180
+ if (this.expectedUnits === null) return;
181
+ let idx = this.payloadStart + this.scannedChars;
182
+ const limit = this.buffer.length;
183
+
184
+ while (this.scannedUnits < this.expectedUnits && idx < limit) {
185
+ const cp = this.buffer.codePointAt(idx);
106
186
  const u = cp > 0xFFFF ? 2 : 1;
107
- if (units + u > len) break;
108
- units += u;
109
- chars += cp > 0xFFFF ? 2 : 1;
187
+ if (this.scannedUnits + u > this.expectedUnits) break;
188
+ this.scannedUnits += u;
189
+ this.scannedChars += cp > 0xFFFF ? 2 : 1;
110
190
  idx += cp > 0xFFFF ? 2 : 1;
111
191
  }
112
- if (units < len) break;
113
- const end = start + chars;
114
- const chunk = content.slice(start, end).trim();
115
- pos = end;
116
- if (!chunk) continue;
117
- try {
118
- const parsed = JSON.parse(chunk);
119
- if (Array.isArray(parsed)) frames.push(...parsed);
120
- else frames.push(parsed);
121
- } catch {}
122
192
  }
123
- return [frames, content.slice(pos)];
193
+ }
194
+
195
+ function parseResponseByFrame(content) {
196
+ const parser = new StreamingFrameParser();
197
+ const frames = parser.feed(content);
198
+ frames.push(...parser.flush());
199
+ return [frames, parser.buffer];
124
200
  }
125
201
 
126
202
  function extractJsonFromResponse(text) {
127
203
  if (typeof text !== 'string') throw new TypeError(`Expected string, got ${typeof text}`);
128
- let c = text.startsWith(")]}'") ? text.slice(4) : text;
129
- c = c.trimStart();
130
- const [r] = parseResponseByFrame(c);
131
- if (r.length) return r;
204
+ const parser = new StreamingFrameParser();
205
+ const result = parser.feed(text);
206
+ result.push(...parser.flush());
207
+ if (result.length) return result;
208
+ const c = text.startsWith(")]}'") ? text.slice(4).trimStart() : text.trimStart();
132
209
  try {
133
210
  const p = JSON.parse(c.trim());
134
211
  return Array.isArray(p) ? p : [p];
@@ -145,4 +222,4 @@ function extractJsonFromResponse(text) {
145
222
  throw new Error('Could not find valid JSON in response.');
146
223
  }
147
224
 
148
- module.exports = { getCleanText, getDeltaByFpLen, getNestedValue, parseResponseByFrame, extractJsonFromResponse };
225
+ module.exports = { getCleanText, getDeltaByFpLen, getNestedValue, parseResponseByFrame, extractJsonFromResponse, StreamingFrameParser };