gemini-reverse 1.0.0
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/LICENSE +21 -0
- package/README.md +345 -0
- package/client.js +428 -0
- package/components/chatMixin.js +104 -0
- package/components/gemMixin.js +87 -0
- package/components/index.js +6 -0
- package/constants.js +94 -0
- package/exceptions.js +35 -0
- package/index.d.ts +329 -0
- package/index.js +34 -0
- package/package.json +35 -0
- package/types/candidate.js +39 -0
- package/types/conversation.js +32 -0
- package/types/gem.js +58 -0
- package/types/grpc.js +19 -0
- package/types/image.js +74 -0
- package/types/index.js +10 -0
- package/types/modeloutput.js +39 -0
- package/utils/accessToken.js +105 -0
- package/utils/index.js +25 -0
- package/utils/parsing.js +119 -0
- package/utils/rotate.js +43 -0
- package/utils/upload.js +51 -0
package/client.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const { Endpoint, GRPC, Headers, Model, ErrorCode, TEMPORARY_CHAT_FLAG_INDEX } = require('./constants');
|
|
6
|
+
const { APIError, GeminiError, TimeoutError, UsageLimitExceeded, ModelInvalid, TemporarilyBlocked } = require('./exceptions');
|
|
7
|
+
const { ChatMixin } = require('./components/chatMixin');
|
|
8
|
+
const { GemMixin } = require('./components/gemMixin');
|
|
9
|
+
const { Candidate } = require('./types/candidate');
|
|
10
|
+
const { ModelOutput } = require('./types/modeloutput');
|
|
11
|
+
const { WebImage, GeneratedImage } = require('./types/image');
|
|
12
|
+
const { RPCData } = require('./types/grpc');
|
|
13
|
+
const { getAccessToken, cookieStr, parseCookies, parseProxy } = require('./utils/accessToken');
|
|
14
|
+
const { rotate1psidts } = require('./utils/rotate');
|
|
15
|
+
const { uploadFile, parseFileName } = require('./utils/upload');
|
|
16
|
+
const { getDeltaByFpLen, getNestedValue, parseResponseByFrame, extractJsonFromResponse } = require('./utils/parsing');
|
|
17
|
+
|
|
18
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
19
|
+
|
|
20
|
+
function applyMixins(Base, ...mixins) {
|
|
21
|
+
for (const mixin of mixins) {
|
|
22
|
+
for (const key of Object.getOwnPropertyNames(mixin.prototype)) {
|
|
23
|
+
if (key === 'constructor') continue;
|
|
24
|
+
Object.defineProperty(Base.prototype, key, Object.getOwnPropertyDescriptor(mixin.prototype, key));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class GeminiClient {
|
|
30
|
+
constructor({ secure_1psid = null, secure_1psidts = null, proxy = null, cookies = {} } = {}) {
|
|
31
|
+
this.cookies = { ...cookies };
|
|
32
|
+
this.proxy = proxy;
|
|
33
|
+
this._running = false;
|
|
34
|
+
this.accessToken = null;
|
|
35
|
+
this.buildLabel = null;
|
|
36
|
+
this.sessionId = null;
|
|
37
|
+
this.timeout = 300000;
|
|
38
|
+
this.autoClose = false;
|
|
39
|
+
this.closeDelay = 300000;
|
|
40
|
+
this.closeTask = null;
|
|
41
|
+
this.autoRefresh = true;
|
|
42
|
+
this.refreshInterval = 540000;
|
|
43
|
+
this.refreshTask = null;
|
|
44
|
+
this.verbose = true;
|
|
45
|
+
this.watchdogTimeout = 30000;
|
|
46
|
+
this._reqid = Math.floor(Math.random() * 90000) + 10000;
|
|
47
|
+
this._gems = null;
|
|
48
|
+
|
|
49
|
+
if (secure_1psid) {
|
|
50
|
+
this.cookies['__Secure-1PSID'] = secure_1psid;
|
|
51
|
+
if (secure_1psidts) this.cookies['__Secure-1PSIDTS'] = secure_1psidts;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async init({ timeout = 300000, autoClose = false, closeDelay = 300000, autoRefresh = true, refreshInterval = 540000, verbose = true, watchdogTimeout = 30000 } = {}) {
|
|
56
|
+
if (this._running) return;
|
|
57
|
+
try {
|
|
58
|
+
this.verbose = verbose;
|
|
59
|
+
this.watchdogTimeout = watchdogTimeout;
|
|
60
|
+
|
|
61
|
+
const [accessToken, buildLabel, sessionId, validCookies] = await getAccessToken(this.cookies, this.proxy, this.verbose);
|
|
62
|
+
this.accessToken = accessToken;
|
|
63
|
+
this.buildLabel = buildLabel;
|
|
64
|
+
this.sessionId = sessionId;
|
|
65
|
+
this.cookies = validCookies;
|
|
66
|
+
this._running = true;
|
|
67
|
+
this.timeout = timeout;
|
|
68
|
+
this.autoClose = autoClose;
|
|
69
|
+
this.closeDelay = closeDelay;
|
|
70
|
+
|
|
71
|
+
if (autoClose) this._resetCloseTask();
|
|
72
|
+
this.autoRefresh = autoRefresh;
|
|
73
|
+
this.refreshInterval = refreshInterval;
|
|
74
|
+
if (this.refreshTask) { clearInterval(this.refreshTask); this.refreshTask = null; }
|
|
75
|
+
if (autoRefresh) this._startAutoRefresh();
|
|
76
|
+
if (this.verbose) console.log('Gemini client initialized successfully.');
|
|
77
|
+
} catch (e) { await this.close(); throw e; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async close(delay = 0) {
|
|
81
|
+
if (delay) await sleep(delay);
|
|
82
|
+
this._running = false;
|
|
83
|
+
if (this.closeTask) { clearTimeout(this.closeTask); this.closeTask = null; }
|
|
84
|
+
if (this.refreshTask) { clearInterval(this.refreshTask); this.refreshTask = null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_resetCloseTask() {
|
|
88
|
+
if (this.closeTask) { clearTimeout(this.closeTask); this.closeTask = null; }
|
|
89
|
+
this.closeTask = setTimeout(() => this.close(), this.closeDelay);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_startAutoRefresh() {
|
|
93
|
+
const interval = Math.max(this.refreshInterval, 60000);
|
|
94
|
+
this.refreshTask = setInterval(async () => {
|
|
95
|
+
if (!this._running) return;
|
|
96
|
+
try {
|
|
97
|
+
const [new1psidts, rotatedCookies] = await rotate1psidts(this.cookies, this.proxy);
|
|
98
|
+
if (rotatedCookies) Object.assign(this.cookies, rotatedCookies);
|
|
99
|
+
if (!new1psidts) console.warn('Rotation response did not contain a new __Secure-1PSIDTS.');
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn(`Unexpected error while refreshing cookies: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}, interval);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async _batchExecute(payloads, retries = 2) {
|
|
107
|
+
let lastErr;
|
|
108
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
const _reqid = this._reqid;
|
|
111
|
+
this._reqid += 100000;
|
|
112
|
+
|
|
113
|
+
const params = new URLSearchParams({
|
|
114
|
+
rpcids: payloads.map(p => p.rpcid).join(','),
|
|
115
|
+
_reqid: String(_reqid), rt: 'c', 'source-path': '/app',
|
|
116
|
+
});
|
|
117
|
+
if (this.buildLabel) params.set('bl', this.buildLabel);
|
|
118
|
+
if (this.sessionId) params.set('f.sid', this.sessionId);
|
|
119
|
+
|
|
120
|
+
const body = new URLSearchParams({
|
|
121
|
+
at: this.accessToken || '',
|
|
122
|
+
'f.req': JSON.stringify([payloads.map(p => p.serialize())]),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const res = await axios.post(`${Endpoint.BATCH_EXEC}?${params}`, body.toString(), {
|
|
126
|
+
headers: { ...Headers.GEMINI, 'Cookie': cookieStr(this.cookies) },
|
|
127
|
+
timeout: this.timeout,
|
|
128
|
+
...(this.proxy ? { proxy: parseProxy(this.proxy) } : {}),
|
|
129
|
+
validateStatus: null,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
Object.assign(this.cookies, parseCookies(res.headers));
|
|
133
|
+
if (res.status !== 200) { await this.close(); throw new APIError(`Batch execution failed with status code ${res.status}`); }
|
|
134
|
+
return res;
|
|
135
|
+
} catch (e) { lastErr = e; if (attempt < retries) await sleep(1000 * (attempt + 1)); }
|
|
136
|
+
}
|
|
137
|
+
throw lastErr;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async generateContent({ prompt, files = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false } = {}) {
|
|
141
|
+
if (this.autoClose) this._resetCloseTask();
|
|
142
|
+
if (!(chat instanceof ChatSession && chat.cid))
|
|
143
|
+
this._reqid = Math.floor(Math.random() * 90000) + 10000;
|
|
144
|
+
|
|
145
|
+
let fileData = null;
|
|
146
|
+
if (files && files.length) {
|
|
147
|
+
await this._batchExecute([new RPCData({ rpcid: GRPC.BARD_ACTIVITY, payload: '[[["bard_activity_enabled"]]]' })]);
|
|
148
|
+
const uploaded = await Promise.all(files.map(f => uploadFile(f, this.proxy)));
|
|
149
|
+
fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await this._batchExecute([new RPCData({ rpcid: GRPC.BARD_ACTIVITY, payload: '[[["bard_activity_enabled"]]]' })]);
|
|
154
|
+
const ss = { last_texts: {}, last_thoughts: {}, last_progress_time: Date.now() };
|
|
155
|
+
let output = null;
|
|
156
|
+
for await (const out of this._generate({ prompt, fileData, model, gem, chat, temporary, ss })) output = out;
|
|
157
|
+
if (!output) throw new GeminiError('Failed to generate contents. No output data found in response.');
|
|
158
|
+
if (chat instanceof ChatSession) { output.metadata = chat.metadata; chat.lastOutput = output; }
|
|
159
|
+
return output;
|
|
160
|
+
} finally {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async* generateContentStream({ prompt, files = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false } = {}) {
|
|
164
|
+
if (this.autoClose) this._resetCloseTask();
|
|
165
|
+
if (!(chat instanceof ChatSession && chat.cid))
|
|
166
|
+
this._reqid = Math.floor(Math.random() * 90000) + 10000;
|
|
167
|
+
|
|
168
|
+
let fileData = null;
|
|
169
|
+
if (files && files.length) {
|
|
170
|
+
await this._batchExecute([new RPCData({ rpcid: GRPC.BARD_ACTIVITY, payload: '[[["bard_activity_enabled"]]]' })]);
|
|
171
|
+
const uploaded = await Promise.all(files.map(f => uploadFile(f, this.proxy)));
|
|
172
|
+
fileData = uploaded.map((url, i) => [[url], parseFileName(files[i])]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await this._batchExecute([new RPCData({ rpcid: GRPC.BARD_ACTIVITY, payload: '[[["bard_activity_enabled"]]]' })]);
|
|
176
|
+
const ss = { last_texts: {}, last_thoughts: {}, last_progress_time: Date.now() };
|
|
177
|
+
let output = null;
|
|
178
|
+
for await (const out of this._generate({ prompt, fileData, model, gem, chat, temporary, ss })) {
|
|
179
|
+
output = out; yield out;
|
|
180
|
+
}
|
|
181
|
+
if (output && chat instanceof ChatSession) { output.metadata = chat.metadata; chat.lastOutput = output; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async* _generate({ prompt, fileData = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false, ss = null }, retries = 5) {
|
|
185
|
+
if (!prompt) throw new Error('Prompt cannot be empty.');
|
|
186
|
+
if (typeof model === 'string') model = Model.fromName(model);
|
|
187
|
+
else if (model && typeof model === 'object' && !model.model_name) model = Model.fromDict(model);
|
|
188
|
+
|
|
189
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
190
|
+
try {
|
|
191
|
+
for await (const out of this._stream({ prompt, fileData, model, gem, chat, temporary, ss })) yield out;
|
|
192
|
+
return;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (e instanceof GeminiError || e instanceof ModelInvalid || e instanceof UsageLimitExceeded || e instanceof TemporarilyBlocked) throw e;
|
|
195
|
+
if (attempt >= retries) throw e;
|
|
196
|
+
await sleep(1000 * (attempt + 1));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async* _stream({ prompt, fileData = null, model = Model.UNSPECIFIED, gem = null, chat = null, temporary = false, ss = null }) {
|
|
202
|
+
const _reqid = this._reqid;
|
|
203
|
+
this._reqid += 100000;
|
|
204
|
+
const gemId = gem && typeof gem === 'object' ? gem.id : gem;
|
|
205
|
+
|
|
206
|
+
const inner = new Array(69).fill(null);
|
|
207
|
+
inner[0] = [prompt, 0, null, fileData, null, null, 0];
|
|
208
|
+
inner[2] = chat ? chat.metadata : ['', '', '', null, null, null, null, null, null, ''];
|
|
209
|
+
inner[7] = 1;
|
|
210
|
+
if (gemId) inner[19] = gemId;
|
|
211
|
+
if (temporary) inner[TEMPORARY_CHAT_FLAG_INDEX] = 1;
|
|
212
|
+
inner[1] = ['en']; inner[6] = [0]; inner[10] = 1; inner[11] = 0;
|
|
213
|
+
inner[17] = [[0]]; inner[18] = 0; inner[27] = 1; inner[30] = [4];
|
|
214
|
+
inner[41] = [1]; inner[53] = 0; inner[61] = []; inner[68] = 1;
|
|
215
|
+
const uid = uuidv4();
|
|
216
|
+
inner[59] = uid;
|
|
217
|
+
|
|
218
|
+
const params = new URLSearchParams({ _reqid: String(_reqid), rt: 'c' });
|
|
219
|
+
if (this.buildLabel) params.set('bl', this.buildLabel);
|
|
220
|
+
if (this.sessionId) params.set('f.sid', this.sessionId);
|
|
221
|
+
|
|
222
|
+
const body = new URLSearchParams({ at: this.accessToken || '', 'f.req': JSON.stringify([null, JSON.stringify(inner)]) });
|
|
223
|
+
|
|
224
|
+
if (ss) {
|
|
225
|
+
if (!('original_cid' in ss)) ss.original_cid = chat ? chat.cid : null;
|
|
226
|
+
if (!('original_rcid' in ss)) ss.original_rcid = chat instanceof ChatSession ? chat.rcid : null;
|
|
227
|
+
if (!('had_response_data' in ss)) ss.had_response_data = false;
|
|
228
|
+
|
|
229
|
+
if (chat && chat.cid && (!ss.original_cid || ss.had_response_data)) {
|
|
230
|
+
const delays = [30000, 45000, 60000, 90000];
|
|
231
|
+
let allStale = true;
|
|
232
|
+
for (let i = 0; i < delays.length; i++) {
|
|
233
|
+
console.warn(`Stream failed for cid=${chat.cid}. READ_CHAT attempt ${i + 1}/${delays.length}: waiting ${delays[i] / 1000}s...`);
|
|
234
|
+
await sleep(delays[i]);
|
|
235
|
+
try {
|
|
236
|
+
const recovered = await this.fetchLatestChatResponse(chat.cid);
|
|
237
|
+
if (recovered) {
|
|
238
|
+
if (ss.original_cid && recovered.rcid && recovered.rcid === ss.original_rcid) continue;
|
|
239
|
+
if (chat instanceof ChatSession) chat.metadata = recovered.metadata;
|
|
240
|
+
yield recovered; return;
|
|
241
|
+
}
|
|
242
|
+
allStale = false;
|
|
243
|
+
} catch (e) { allStale = false; console.warn(`READ_CHAT attempt ${i + 1} failed: ${e.message}`); }
|
|
244
|
+
}
|
|
245
|
+
if (allStale) {
|
|
246
|
+
if (chat instanceof ChatSession) { chat.rid = ''; chat.rcid = ''; }
|
|
247
|
+
ss.had_response_data = false;
|
|
248
|
+
throw new APIError(`Stream failed for cid=${chat.cid}. All READ_CHAT stale. Retrying.`);
|
|
249
|
+
}
|
|
250
|
+
throw new GeminiError(`Stream failed for cid=${chat.cid}. Recovery returned no data.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const res = await axios.post(`${Endpoint.GENERATE}?${params}`, body.toString(), {
|
|
255
|
+
headers: { ...Headers.GEMINI, ...model.model_header, 'x-goog-ext-525005358-jspb': `["${uid}",1]`, 'Cookie': cookieStr(this.cookies) },
|
|
256
|
+
responseType: 'stream', timeout: this.timeout, validateStatus: null,
|
|
257
|
+
...(this.proxy ? { proxy: parseProxy(this.proxy) } : {}),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (res.status !== 200) { await this.close(); throw new APIError(`Failed to generate contents. Status: ${res.status}`); }
|
|
261
|
+
Object.assign(this.cookies, parseCookies(res.headers));
|
|
262
|
+
|
|
263
|
+
const lTxt = ss ? ss.last_texts : {}, lThought = ss ? ss.last_thoughts : {};
|
|
264
|
+
let lastProg = Date.now();
|
|
265
|
+
let isThinking = false, isQueueing = false, hasCandidates = false, isCompleted = false, isFinalChunk = false;
|
|
266
|
+
let buf = '';
|
|
267
|
+
|
|
268
|
+
const processParts = parts => {
|
|
269
|
+
const outs = [];
|
|
270
|
+
for (const part of parts) {
|
|
271
|
+
const ec = getNestedValue(part, [5, 2, 0, 1, 0]);
|
|
272
|
+
if (ec) {
|
|
273
|
+
switch (ec) {
|
|
274
|
+
case ErrorCode.USAGE_LIMIT_EXCEEDED: throw new UsageLimitExceeded(`Usage limit exceeded for model '${model.model_name}'.`);
|
|
275
|
+
case ErrorCode.MODEL_INCONSISTENT: throw new ModelInvalid('Model inconsistent with conversation history.');
|
|
276
|
+
case ErrorCode.MODEL_HEADER_INVALID: throw new ModelInvalid(`Model '${model.model_name}' unavailable or request structure outdated.`);
|
|
277
|
+
case ErrorCode.IP_TEMPORARILY_BLOCKED: throw new TemporarilyBlocked('IP temporarily blocked by Google.');
|
|
278
|
+
case ErrorCode.TEMPORARY_ERROR_1013: throw new APIError('Temporary error (1013). Retrying...');
|
|
279
|
+
default: throw new APIError(`Unknown API error code: ${ec}.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (JSON.stringify(part).includes('data_analysis_tool')) isThinking = true;
|
|
284
|
+
const status = getNestedValue(part, [5]);
|
|
285
|
+
if (Array.isArray(status) && status.length) isQueueing = true;
|
|
286
|
+
|
|
287
|
+
const innerStr = getNestedValue(part, [2]);
|
|
288
|
+
if (!innerStr) continue;
|
|
289
|
+
let pj; try { pj = JSON.parse(innerStr); } catch { continue; }
|
|
290
|
+
|
|
291
|
+
const mData = getNestedValue(pj, [1]);
|
|
292
|
+
if (mData && chat instanceof ChatSession) { chat.metadata = mData; if (ss) ss.had_response_data = true; }
|
|
293
|
+
|
|
294
|
+
const ctx = getNestedValue(pj, [25]);
|
|
295
|
+
if (typeof ctx === 'string') {
|
|
296
|
+
isCompleted = true; isThinking = false; isQueueing = false;
|
|
297
|
+
if (chat instanceof ChatSession) { const m = [...chat.metadata]; m[9] = ctx; chat.metadata = m; }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const clist = getNestedValue(pj, [4], []);
|
|
301
|
+
if (!clist || !clist.length) continue;
|
|
302
|
+
|
|
303
|
+
const outCands = [];
|
|
304
|
+
for (let i = 0; i < clist.length; i++) {
|
|
305
|
+
const cd = clist[i];
|
|
306
|
+
const rcid = getNestedValue(cd, [0]); if (!rcid) continue;
|
|
307
|
+
if (chat instanceof ChatSession) chat.rcid = rcid;
|
|
308
|
+
|
|
309
|
+
let txt = getNestedValue(cd, [1, 0], '');
|
|
310
|
+
if (/^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(txt))
|
|
311
|
+
txt = getNestedValue(cd, [22, 0]) || txt;
|
|
312
|
+
txt = txt.replace(/http:\/\/googleusercontent\.com\/\w+\/\d+\n*/g, '');
|
|
313
|
+
|
|
314
|
+
const thoughts = getNestedValue(cd, [37, 0, 0]) || '';
|
|
315
|
+
const webImgs = [], genImgs = [];
|
|
316
|
+
|
|
317
|
+
for (const wi of (getNestedValue(cd, [12, 1], []) || [])) {
|
|
318
|
+
const url = getNestedValue(wi, [0, 0, 0]);
|
|
319
|
+
if (url) webImgs.push(new WebImage({ url, title: getNestedValue(wi, [7, 0], ''), alt: getNestedValue(wi, [0, 4], ''), proxy: this.proxy }));
|
|
320
|
+
}
|
|
321
|
+
for (const gi of (getNestedValue(cd, [12, 7, 0], []) || [])) {
|
|
322
|
+
const url = getNestedValue(gi, [0, 3, 3]);
|
|
323
|
+
if (url) {
|
|
324
|
+
const imgNum = getNestedValue(gi, [3, 6]);
|
|
325
|
+
genImgs.push(new GeneratedImage({ url, title: imgNum ? `[Generated Image ${imgNum}]` : '[Generated Image]', alt: getNestedValue(gi, [3, 5, 0], ''), proxy: this.proxy, cookies: this.cookies }));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
isFinalChunk = Array.isArray(getNestedValue(cd, [2])) || getNestedValue(cd, [8, 0], 1) === 2;
|
|
330
|
+
|
|
331
|
+
const [td, nft] = getDeltaByFpLen(txt, lTxt[rcid] || lTxt[`idx_${i}`] || '', isFinalChunk);
|
|
332
|
+
let thdelta = '', nfth = '';
|
|
333
|
+
if (thoughts) [thdelta, nfth] = getDeltaByFpLen(thoughts, lThought[rcid] || lThought[`idx_${i}`] || '', isFinalChunk);
|
|
334
|
+
|
|
335
|
+
if (td || thdelta || webImgs.length || genImgs.length) hasCandidates = true;
|
|
336
|
+
lTxt[rcid] = lTxt[`idx_${i}`] = nft;
|
|
337
|
+
lThought[rcid] = lThought[`idx_${i}`] = nfth;
|
|
338
|
+
|
|
339
|
+
outCands.push(new Candidate({ rcid, text: txt, text_delta: td, thoughts: thoughts || null, thoughts_delta: thdelta, web_images: webImgs, generated_images: genImgs }));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (outCands.length) { isThinking = false; isQueueing = false; outs.push(new ModelOutput(getNestedValue(pj, [1], []), outCands)); }
|
|
343
|
+
}
|
|
344
|
+
return outs;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const yielded = [];
|
|
348
|
+
await new Promise((resolve, reject) => {
|
|
349
|
+
res.data.on('data', chunk => {
|
|
350
|
+
try {
|
|
351
|
+
buf += chunk.toString('utf8');
|
|
352
|
+
if (buf.startsWith(")]}'")) buf = buf.slice(4).trimStart();
|
|
353
|
+
const [parts, rem] = parseResponseByFrame(buf);
|
|
354
|
+
buf = rem;
|
|
355
|
+
const outs = processParts(parts);
|
|
356
|
+
for (const o of outs) yielded.push(o);
|
|
357
|
+
if (outs.length || isThinking || isQueueing) { lastProg = Date.now(); if (ss) ss.last_progress_time = lastProg; }
|
|
358
|
+
else if (Date.now() - lastProg > Math.min(this.timeout, this.watchdogTimeout))
|
|
359
|
+
reject(new APIError('Response stalled (zombie stream).'));
|
|
360
|
+
} catch (e) { reject(e); }
|
|
361
|
+
});
|
|
362
|
+
res.data.on('end', () => {
|
|
363
|
+
try {
|
|
364
|
+
if (buf) { const [p] = parseResponseByFrame(buf); for (const o of processParts(p)) yielded.push(o); }
|
|
365
|
+
if (!(isCompleted || isFinalChunk) || isThinking || isQueueing) reject(new APIError('Stream interrupted or truncated.'));
|
|
366
|
+
else resolve();
|
|
367
|
+
} catch (e) { reject(e); }
|
|
368
|
+
});
|
|
369
|
+
res.data.on('error', e => reject(new APIError(`Stream error: ${e.message}`)));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
for (const o of yielded) yield o;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
startChat(opts = {}) { return new ChatSession({ geminiclient: this, ...opts }); }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
applyMixins(GeminiClient, ChatMixin, GemMixin);
|
|
379
|
+
|
|
380
|
+
class ChatSession {
|
|
381
|
+
constructor({ geminiclient, metadata = null, cid = null, rid = null, rcid = null, model = null, gem = null } = {}) {
|
|
382
|
+
this._metadata = ['', '', '', null, null, null, null, null, null, ''];
|
|
383
|
+
this.geminiclient = geminiclient;
|
|
384
|
+
this.lastOutput = null;
|
|
385
|
+
this.model = ChatSession._resolveModel(model);
|
|
386
|
+
this.gem = gem;
|
|
387
|
+
if (metadata) this.metadata = metadata;
|
|
388
|
+
if (cid) this.cid = cid;
|
|
389
|
+
if (rid) this.rid = rid;
|
|
390
|
+
if (rcid) this.rcid = rcid;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
get metadata() { return this._metadata; }
|
|
394
|
+
set metadata(v) {
|
|
395
|
+
if (!Array.isArray(v)) return;
|
|
396
|
+
for (let i = 0; i < v.length && i < 10; i++) if (v[i] != null) this._metadata[i] = v[i];
|
|
397
|
+
}
|
|
398
|
+
get cid() { return this._metadata[0]; } set cid(v) { this._metadata[0] = v; }
|
|
399
|
+
get rid() { return this._metadata[1]; } set rid(v) { this._metadata[1] = v; }
|
|
400
|
+
get rcid() { return this._metadata[2]; } set rcid(v) { this._metadata[2] = v; }
|
|
401
|
+
|
|
402
|
+
async sendMessage({ prompt, files = null, temporary = false } = {}) {
|
|
403
|
+
return this.geminiclient.generateContent({ prompt, files, model: this.model, gem: this.gem, chat: this, temporary });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async* sendMessageStream({ prompt, files = null, temporary = false } = {}) {
|
|
407
|
+
for await (const out of this.geminiclient.generateContentStream({ prompt, files, model: this.model, gem: this.gem, chat: this, temporary })) yield out;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
chooseCandidate(index) {
|
|
411
|
+
if (!this.lastOutput) throw new Error('No previous output data found in this chat session.');
|
|
412
|
+
if (index >= this.lastOutput.candidates.length) throw new Error(`Index ${index} exceeds number of candidates.`);
|
|
413
|
+
this.lastOutput.chosen = index;
|
|
414
|
+
this.rcid = this.lastOutput.rcid;
|
|
415
|
+
return this.lastOutput;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
static _resolveModel(model) {
|
|
419
|
+
if (!model) return Model.UNSPECIFIED;
|
|
420
|
+
if (typeof model === 'string') return Model.fromName(model);
|
|
421
|
+
if (typeof model === 'object' && !model.model_name) return Model.fromDict(model);
|
|
422
|
+
return model;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
toString() { return `ChatSession(cid='${this.cid}', rid='${this.rid}', rcid='${this.rcid}')`; }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = { GeminiClient, ChatSession };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { GRPC } = require('../constants');
|
|
4
|
+
const { RPCData } = require('../types/grpc');
|
|
5
|
+
const { Candidate } = require('../types/candidate');
|
|
6
|
+
const { ModelOutput } = require('../types/modeloutput');
|
|
7
|
+
const { ConversationTurn } = require('../types/conversation');
|
|
8
|
+
const { extractJsonFromResponse, getNestedValue } = require('../utils/parsing');
|
|
9
|
+
|
|
10
|
+
class ChatMixin {
|
|
11
|
+
async _fetchChatTurns(cid, maxTurns = 10) {
|
|
12
|
+
const response = await this._batchExecute([
|
|
13
|
+
new RPCData({ rpcid: GRPC.READ_CHAT, payload: JSON.stringify([cid, maxTurns, null, 1, [0], [4], null, 1]) }),
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const responseJson = extractJsonFromResponse(response.data);
|
|
17
|
+
|
|
18
|
+
for (const part of responseJson) {
|
|
19
|
+
const bodyStr = getNestedValue(part, [2]);
|
|
20
|
+
if (!bodyStr) continue;
|
|
21
|
+
const body = JSON.parse(bodyStr);
|
|
22
|
+
const turns = getNestedValue(body, [0]);
|
|
23
|
+
if (!turns || !Array.isArray(turns)) continue;
|
|
24
|
+
return turns;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.warn(`_fetchChatTurns(${cid}) found no turns`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async fetchLatestChatResponse(cid) {
|
|
32
|
+
try {
|
|
33
|
+
const turns = await this._fetchChatTurns(cid, 10);
|
|
34
|
+
if (!turns) return null;
|
|
35
|
+
|
|
36
|
+
const convTurn = turns[turns.length - 1];
|
|
37
|
+
if (!convTurn) return null;
|
|
38
|
+
|
|
39
|
+
const candidatesList = getNestedValue(convTurn, [3, 0]);
|
|
40
|
+
if (!candidatesList) return null;
|
|
41
|
+
|
|
42
|
+
const candidateData = getNestedValue(candidatesList, [0]);
|
|
43
|
+
if (!candidateData) return null;
|
|
44
|
+
|
|
45
|
+
const rcid = getNestedValue(candidateData, [0], '');
|
|
46
|
+
const text = getNestedValue(candidateData, [1, 0], '');
|
|
47
|
+
if (!text) return null;
|
|
48
|
+
|
|
49
|
+
const turnMeta = getNestedValue(convTurn, [0]);
|
|
50
|
+
const rid = Array.isArray(turnMeta) && turnMeta.length >= 2 && typeof turnMeta[1] === 'string' ? turnMeta[1] : '';
|
|
51
|
+
const metadata = rid ? [cid, rid] : [cid];
|
|
52
|
+
|
|
53
|
+
return new ModelOutput(metadata, [new Candidate({ rcid, text })]);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn(`fetchLatestChatResponse(${cid}) error: ${e.message}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async readChat(cid, maxTurns = 100) {
|
|
61
|
+
try {
|
|
62
|
+
const turns = await this._fetchChatTurns(cid, maxTurns);
|
|
63
|
+
if (!turns) return [];
|
|
64
|
+
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const turn of turns) {
|
|
67
|
+
if (!Array.isArray(turn) || turn.length < 4) continue;
|
|
68
|
+
|
|
69
|
+
const turnMeta = getNestedValue(turn, [0]);
|
|
70
|
+
const rid = Array.isArray(turnMeta) ? (getNestedValue(turnMeta, [1], '') || '') : '';
|
|
71
|
+
const userPrompt = getNestedValue(turn, [2, 0, 0], '');
|
|
72
|
+
const candidateData = getNestedValue(turn, [3, 0, 0]);
|
|
73
|
+
if (!candidateData) continue;
|
|
74
|
+
|
|
75
|
+
const rcid = getNestedValue(candidateData, [0], '');
|
|
76
|
+
const text = getNestedValue(candidateData, [1, 0], '');
|
|
77
|
+
if (!text) continue;
|
|
78
|
+
|
|
79
|
+
const thoughts = getNestedValue(candidateData, [37, 0, 0]) || null;
|
|
80
|
+
const tsData = getNestedValue(turn, [4]);
|
|
81
|
+
let timestamp = null;
|
|
82
|
+
if (Array.isArray(tsData) && tsData.length && typeof tsData[0] === 'number') {
|
|
83
|
+
try { timestamp = new Date(tsData[0] * 1000); } catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
result.push(new ConversationTurn({ rid, user_prompt: userPrompt, assistant_response: text, rcid, thoughts, timestamp }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
result.reverse();
|
|
90
|
+
return result;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.warn(`readChat(${cid}) error: ${e.message}`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async deleteChat(cid) {
|
|
98
|
+
await this._batchExecute([
|
|
99
|
+
new RPCData({ rpcid: GRPC.DELETE_CHAT, payload: JSON.stringify([cid]) }),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { ChatMixin };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { GRPC } = require('../constants');
|
|
4
|
+
const { APIError } = require('../exceptions');
|
|
5
|
+
const { Gem, GemJar } = require('../types/gem');
|
|
6
|
+
const { RPCData } = require('../types/grpc');
|
|
7
|
+
const { extractJsonFromResponse, getNestedValue } = require('../utils/parsing');
|
|
8
|
+
|
|
9
|
+
class GemMixin {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._gems = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get gems() {
|
|
15
|
+
if (!this._gems) throw new Error('Gems not fetched yet. Call fetchGems() first.');
|
|
16
|
+
return this._gems;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async fetchGems({ includeHidden = false, language = 'en' } = {}) {
|
|
20
|
+
const response = await this._batchExecute([
|
|
21
|
+
new RPCData({ rpcid: GRPC.LIST_GEMS, payload: includeHidden ? `[4,['${language}'],0]` : `[3,['${language}'],0]`, identifier: 'system' }),
|
|
22
|
+
new RPCData({ rpcid: GRPC.LIST_GEMS, payload: `[2,['${language}'],0]`, identifier: 'custom' }),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const responseJson = extractJsonFromResponse(response.data);
|
|
27
|
+
let predefinedGems = [], customGems = [];
|
|
28
|
+
|
|
29
|
+
for (const part of responseJson) {
|
|
30
|
+
const identifier = getNestedValue(part, [-1]);
|
|
31
|
+
const bodyStr = getNestedValue(part, [2]);
|
|
32
|
+
if (!bodyStr) continue;
|
|
33
|
+
const body = JSON.parse(bodyStr);
|
|
34
|
+
if (identifier === 'system') predefinedGems = getNestedValue(body, [2], []);
|
|
35
|
+
else if (identifier === 'custom') customGems = getNestedValue(body, [2], []);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!predefinedGems.length && !customGems.length) throw new Error('Empty gem list.');
|
|
39
|
+
|
|
40
|
+
const mkGem = (g, predefined) => new Gem({ id: g[0], name: g[1][0], description: g[1][1], prompt: g[2] ? g[2][0] : null, predefined });
|
|
41
|
+
this._gems = new GemJar([
|
|
42
|
+
...predefinedGems.map(g => [g[0], mkGem(g, true)]),
|
|
43
|
+
...customGems.map(g => [g[0], mkGem(g, false)]),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
return this._gems;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
await this.close();
|
|
49
|
+
throw new APIError('Failed to fetch gems. Unexpected response data structure.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createGem({ name, prompt, description = '' } = {}) {
|
|
54
|
+
const response = await this._batchExecute([
|
|
55
|
+
new RPCData({ rpcid: GRPC.CREATE_GEM, payload: JSON.stringify([[name, description, prompt, null, null, null, null, null, 0, null, 1, null, null, null, []]]) }),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const responseJson = extractJsonFromResponse(response.data);
|
|
60
|
+
const bodyStr = getNestedValue(responseJson, [0, 2]);
|
|
61
|
+
if (!bodyStr) throw new Error();
|
|
62
|
+
const id = getNestedValue(JSON.parse(bodyStr), [0]);
|
|
63
|
+
if (!id) throw new Error();
|
|
64
|
+
return new Gem({ id, name, description, prompt, predefined: false });
|
|
65
|
+
} catch {
|
|
66
|
+
await this.close();
|
|
67
|
+
throw new APIError('Failed to create gem. Unexpected response data structure.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async updateGem({ gem, name, prompt, description = '' } = {}) {
|
|
72
|
+
const id = gem instanceof Gem ? gem.id : gem;
|
|
73
|
+
await this._batchExecute([
|
|
74
|
+
new RPCData({ rpcid: GRPC.UPDATE_GEM, payload: JSON.stringify([id, [name, description, prompt, null, null, null, null, null, 0, null, 1, null, null, null, [], 0]]) }),
|
|
75
|
+
]);
|
|
76
|
+
return new Gem({ id, name, description, prompt, predefined: false });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async deleteGem(gem) {
|
|
80
|
+
const id = gem instanceof Gem ? gem.id : gem;
|
|
81
|
+
await this._batchExecute([
|
|
82
|
+
new RPCData({ rpcid: GRPC.DELETE_GEM, payload: JSON.stringify([id]) }),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { GemMixin };
|