gemini-reverse 1.0.5 → 1.0.7

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/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.join('-');
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,14 +549,27 @@ class GeminiClient {
340
549
  chat = null,
341
550
  temporary = false,
342
551
  deep_research = false,
552
+ extended_thinking = false,
343
553
  } = {}) {
554
+ if (!this._running) {
555
+ await this.init({
556
+ timeout: this.timeout,
557
+ autoClose: this.autoClose,
558
+ closeDelay: this.closeDelay,
559
+ autoRefresh: this.autoRefresh,
560
+ refreshInterval: this.refreshInterval,
561
+ verbose: this.verbose,
562
+ watchdogTimeout: this.watchdogTimeout,
563
+ });
564
+ if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContent. Client initialization failed.`);
565
+ }
344
566
  if (this.autoClose) this._resetCloseTask();
345
567
 
346
568
  let fileData = null;
347
569
  if (files && files.length) {
348
570
  await this._sendBardActivity();
349
571
  const uploaded = await Promise.all(
350
- files.map(f => uploadFile(f, this.proxy, this.pushId)),
572
+ files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
351
573
  );
352
574
  fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
353
575
  }
@@ -357,7 +579,7 @@ class GeminiClient {
357
579
  const ss = { last_texts: {}, last_thoughts: {}, last_progress_time: Date.now(), is_thinking: false, is_queueing: false };
358
580
  let output = null;
359
581
  for await (const out of this._generate({
360
- prompt, fileData, model, gem, chat, temporary, ss, deep_research,
582
+ prompt, fileData, model, gem, chat, temporary, ss, deep_research, extended_thinking,
361
583
  })) output = out;
362
584
  if (!output) throw new GeminiError('Failed to generate contents. No output data found in response.');
363
585
  if (chat instanceof ChatSession) { output.metadata = chat.metadata; chat.lastOutput = output; }
@@ -374,14 +596,27 @@ class GeminiClient {
374
596
  chat = null,
375
597
  temporary = false,
376
598
  deep_research = false,
599
+ extended_thinking = false,
377
600
  } = {}) {
601
+ if (!this._running) {
602
+ await this.init({
603
+ timeout: this.timeout,
604
+ autoClose: this.autoClose,
605
+ closeDelay: this.closeDelay,
606
+ autoRefresh: this.autoRefresh,
607
+ refreshInterval: this.refreshInterval,
608
+ verbose: this.verbose,
609
+ watchdogTimeout: this.watchdogTimeout,
610
+ });
611
+ if (!this._running) throw new APIError(`Invalid function call: GeminiClient.generateContentStream. Client initialization failed.`);
612
+ }
378
613
  if (this.autoClose) this._resetCloseTask();
379
614
 
380
615
  let fileData = null;
381
616
  if (files && files.length) {
382
617
  await this._sendBardActivity();
383
618
  const uploaded = await Promise.all(
384
- files.map(f => uploadFile(f, this.proxy, this.pushId)),
619
+ files.map(f => uploadFile(f, this.proxy, this.pushId, this.cookies)),
385
620
  );
386
621
  fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
387
622
  }
@@ -390,7 +625,7 @@ class GeminiClient {
390
625
  const ss = { last_texts: {}, last_thoughts: {} };
391
626
  let output = null;
392
627
  for await (const out of this._generate({
393
- prompt, fileData, model, gem, chat, temporary, ss, deep_research,
628
+ prompt, fileData, model, gem, chat, temporary, ss, deep_research, extended_thinking,
394
629
  })) {
395
630
  output = out;
396
631
  yield out;
@@ -407,6 +642,7 @@ class GeminiClient {
407
642
  temporary = false,
408
643
  ss = null,
409
644
  deep_research = false,
645
+ extended_thinking = false,
410
646
  }, retries = 5) {
411
647
  if (!prompt) throw new Error('Prompt cannot be empty.');
412
648
 
@@ -414,7 +650,7 @@ class GeminiClient {
414
650
 
415
651
  for (let attempt = 0; attempt <= retries; attempt++) {
416
652
  try {
417
- 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;
418
654
  return;
419
655
  } catch (e) {
420
656
  if (
@@ -427,7 +663,7 @@ class GeminiClient {
427
663
  }
428
664
  }
429
665
 
430
- 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 }) {
431
667
  const _reqid = this._reqid;
432
668
  this._reqid += 100000;
433
669
  const gemId = gem instanceof Gem ? gem.id : gem;
@@ -439,7 +675,7 @@ class GeminiClient {
439
675
  rcid: chat.rcid,
440
676
  } : null;
441
677
 
442
- const inner = new Array(69).fill(null);
678
+ const inner = new Array(81).fill(null);
443
679
  inner[0] = [prompt, 0, null, fileData, null, null, 0];
444
680
  inner[1] = [this.language || 'en'];
445
681
  inner[2] = chat instanceof ChatSession ? chat.metadata : [...DEFAULT_METADATA];
@@ -465,11 +701,24 @@ class GeminiClient {
465
701
  inner[55] = [[1]];
466
702
  }
467
703
  inner[61] = [];
468
- inner[68] = 2;
704
+ inner[68] = 1;
705
+ inner[80] = extended_thinking ? 2 : 1;
469
706
 
470
707
  const uid = uuidv4().toUpperCase();
471
708
  inner[59] = uid;
472
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
+
473
722
  const params = new URLSearchParams({
474
723
  hl: this.language || 'en',
475
724
  _reqid: String(_reqid),
@@ -492,7 +741,7 @@ class GeminiClient {
492
741
  {
493
742
  headers: {
494
743
  ...Headers.GEMINI,
495
- ...model.model_header,
744
+ ...modelHeaders,
496
745
  'x-goog-ext-525005358-jspb': `["${uid}",1]`,
497
746
  ...Headers.SAME_DOMAIN,
498
747
  'Cookie': cookieStr(this.cookies),
@@ -517,7 +766,7 @@ class GeminiClient {
517
766
  let isCompleted = false, isFinalChunk = false;
518
767
  let cid = chat instanceof ChatSession ? chat.cid : '';
519
768
  let rid = chat instanceof ChatSession ? chat.rid : '';
520
- let buf = '';
769
+ const frameParser = new StreamingFrameParser();
521
770
 
522
771
  const processParts = (parts) => {
523
772
  const outs = [];
@@ -536,7 +785,7 @@ class GeminiClient {
536
785
  case ErrorCode.TEMPORARY_ERROR_1013:
537
786
  throw new APIError('Temporary error (1013). Retrying...');
538
787
  default:
539
- throw new APIError(`Unknown API error code: ${ec}.`);
788
+ throw new APIError(`Failed to generate contents. Unknown API error code: ${ec}. This might be a content policy rejection or a temporary Google service issue.`);
540
789
  }
541
790
  }
542
791
 
@@ -652,10 +901,7 @@ class GeminiClient {
652
901
 
653
902
  res.data.on('data', chunk => {
654
903
  try {
655
- buf += chunk.toString('utf8');
656
- if (buf.startsWith(")]}'")) buf = buf.slice(4).trimStart();
657
- const [parts, rem] = parseResponseByFrame(buf);
658
- buf = rem;
904
+ const parts = frameParser.feed(chunk.toString('utf8'));
659
905
  const outs = processParts(parts);
660
906
  for (const o of outs) yielded.push(o);
661
907
  if (outs.length || isThinking || isQueueing) lastProg = Date.now();
@@ -668,10 +914,8 @@ class GeminiClient {
668
914
  res.data.on('end', () => {
669
915
  clearInterval(watchdog);
670
916
  try {
671
- if (buf) {
672
- const [p] = parseResponseByFrame(buf);
673
- for (const o of processParts(p)) yielded.push(o);
674
- }
917
+ const remaining = frameParser.flush();
918
+ for (const o of processParts(remaining)) yielded.push(o);
675
919
  if (!isCompleted && !isFinalChunk) reject(new APIError('Stream interrupted or truncated.'));
676
920
  else resolve();
677
921
  } catch (e) { reject(e); }
@@ -899,7 +1143,7 @@ class ChatSession {
899
1143
  get rcid() { return this._metadata[2]; }
900
1144
  set rcid(v) { this._metadata[2] = v; }
901
1145
 
902
- async sendMessage({ prompt, files = null, temporary = false, deep_research = false } = {}) {
1146
+ async sendMessage({ prompt, files = null, temporary = false, deep_research = false, extended_thinking = false } = {}) {
903
1147
  return this.geminiclient.generateContent({
904
1148
  prompt,
905
1149
  files,
@@ -908,10 +1152,11 @@ class ChatSession {
908
1152
  chat: this,
909
1153
  temporary,
910
1154
  deep_research,
1155
+ extended_thinking,
911
1156
  });
912
1157
  }
913
1158
 
914
- async* sendMessageStream({ prompt, files = null, temporary = false, deep_research = false } = {}) {
1159
+ async* sendMessageStream({ prompt, files = null, temporary = false, deep_research = false, extended_thinking = false } = {}) {
915
1160
  for await (const out of this.geminiclient.generateContentStream({
916
1161
  prompt,
917
1162
  files,
@@ -920,6 +1165,7 @@ class ChatSession {
920
1165
  chat: this,
921
1166
  temporary,
922
1167
  deep_research,
1168
+ extended_thinking,
923
1169
  })) yield out;
924
1170
  }
925
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]) }),
@@ -16,7 +16,8 @@ class GemMixin {
16
16
  return this._gems;
17
17
  }
18
18
 
19
- async fetchGems({ includeHidden = false, language = 'en' } = {}) {
19
+ async fetchGems({ includeHidden = false } = {}) {
20
+ const language = this.language || 'en';
20
21
  const response = await this._batchExecute([
21
22
  new RPCData({ rpcid: GRPC.LIST_GEMS, payload: includeHidden ? `[4,['${language}'],0]` : `[3,['${language}'],0]`, identifier: 'system' }),
22
23
  new RPCData({ rpcid: GRPC.LIST_GEMS, payload: `[2,['${language}'],0]`, identifier: 'custom' }),