zyn-ai 1.3.4 → 1.3.6
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 +7 -11
- package/package.json +1 -1
- package/src/cli/commands.js +77 -7
- package/src/cli/print.js +3 -3
- package/src/config.js +28 -21
- package/src/core/agent.js +52 -11
- package/src/core/prompts.js +73 -10
- package/src/i18n.js +2 -2
- package/src/providers/catalog.js +27 -43
- package/src/providers/gemini/index.js +338 -0
- package/src/providers/scraperClient.js +3 -6
- package/src/tools/index.js +230 -0
- package/src/tui/app.mjs +96 -19
- package/src/utils/gmailAuth.js +427 -0
- package/src/utils/sessionStorage.js +16 -9
- package/src/web/public/index.html +3 -1
- package/src/web/server.js +10 -3
- package/src/web/webAgent.js +5 -3
- package/src/providers/ollama/index.js +0 -78
- package/src/providers/openaiCompatible/index.js +0 -97
package/src/providers/catalog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { MODELS_FILE, PROVIDERS_FILE, REQUEST_TIMEOUT_MS } = require('../config');
|
|
3
|
+
const { MODELS_FILE, PROVIDERS_FILE, REQUEST_TIMEOUT_MS, SUPPORTED_MODEL_PROVIDERS } = require('../config');
|
|
4
4
|
|
|
5
5
|
const DEFAULT_HEADERS = {
|
|
6
6
|
'Content-Type': 'application/json',
|
|
@@ -108,13 +108,17 @@ function loadExternalModels() {
|
|
|
108
108
|
if (Array.isArray(raw)) {
|
|
109
109
|
const output = {};
|
|
110
110
|
for (const item of raw) {
|
|
111
|
-
if (!item?.key) continue;
|
|
111
|
+
if (!item?.key || !SUPPORTED_MODEL_PROVIDERS.has(item.provider)) continue;
|
|
112
112
|
output[item.key] = item;
|
|
113
113
|
}
|
|
114
114
|
return output;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const rawModels = raw.models && typeof raw.models === 'object'
|
|
117
|
+
? raw.models
|
|
118
|
+
: (raw && typeof raw === 'object' ? raw : {});
|
|
119
|
+
return Object.fromEntries(
|
|
120
|
+
Object.entries(rawModels).filter(([, model]) => SUPPORTED_MODEL_PROVIDERS.has(model?.provider)),
|
|
121
|
+
);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function saveExternalModels(models) {
|
|
@@ -165,43 +169,6 @@ function buildModelRecord(providerKey, config, modelId, label, extra = {}) {
|
|
|
165
169
|
return record;
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
async function fetchOllamaModels(config) {
|
|
169
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl || 'http://127.0.0.1:11434');
|
|
170
|
-
const data = await fetchJson(`${baseUrl}/api/tags`, {
|
|
171
|
-
headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
|
|
172
|
-
});
|
|
173
|
-
const models = Array.isArray(data?.models) ? data.models : [];
|
|
174
|
-
return models.map(model => buildModelRecord(
|
|
175
|
-
'ollama',
|
|
176
|
-
{ ...config, baseUrl },
|
|
177
|
-
model.name || model.model || model.id,
|
|
178
|
-
model.name || model.model || model.id,
|
|
179
|
-
{
|
|
180
|
-
ollamaModel: model.name || model.model || model.id,
|
|
181
|
-
raw: model,
|
|
182
|
-
},
|
|
183
|
-
)).filter(item => item.modelId);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function fetchOpenAICompatibleModels(config) {
|
|
187
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
188
|
-
if (!baseUrl) throw new Error('Falta baseUrl para openai-compatible');
|
|
189
|
-
const data = await fetchJson(`${baseUrl}/v1/models`, {
|
|
190
|
-
headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
|
|
191
|
-
});
|
|
192
|
-
const models = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [];
|
|
193
|
-
return models.map(model => buildModelRecord(
|
|
194
|
-
'openai-compatible',
|
|
195
|
-
{ ...config, baseUrl },
|
|
196
|
-
model.id || model.name,
|
|
197
|
-
model.id || model.name,
|
|
198
|
-
{
|
|
199
|
-
openaiModel: model.id || model.name,
|
|
200
|
-
raw: model,
|
|
201
|
-
},
|
|
202
|
-
)).filter(item => item.modelId);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
172
|
async function fetchZenModels(config) {
|
|
206
173
|
const baseUrl = normalizeBaseUrl(config.baseUrl || 'https://opencode.ai/zen');
|
|
207
174
|
const data = await fetchJson(`${baseUrl}/v1/models`, {
|
|
@@ -238,10 +205,27 @@ async function fetchQwenModels(config) {
|
|
|
238
205
|
));
|
|
239
206
|
}
|
|
240
207
|
|
|
208
|
+
|
|
209
|
+
async function fetchGeminiModels(config) {
|
|
210
|
+
const models = [
|
|
211
|
+
{ id: 'gemini-flash', label: 'Gemini Flash' },
|
|
212
|
+
];
|
|
213
|
+
return models.map(model => buildModelRecord(
|
|
214
|
+
'gemini',
|
|
215
|
+
config,
|
|
216
|
+
model.id,
|
|
217
|
+
model.label,
|
|
218
|
+
{
|
|
219
|
+
geminiModel: model.id,
|
|
220
|
+
static: true,
|
|
221
|
+
},
|
|
222
|
+
));
|
|
223
|
+
}
|
|
224
|
+
|
|
241
225
|
async function fetchProviderModels(providerKey, config = {}) {
|
|
242
226
|
const key = String(providerKey || '').trim();
|
|
243
|
-
if (key
|
|
244
|
-
if (key === '
|
|
227
|
+
if (!SUPPORTED_MODEL_PROVIDERS.has(key)) throw new Error(`Proveedor no soportado: ${key}`);
|
|
228
|
+
if (key === 'gemini') return fetchGeminiModels(config);
|
|
245
229
|
if (key === 'zen') return fetchZenModels(config);
|
|
246
230
|
if (key === 'qwen') return fetchQwenModels(config);
|
|
247
231
|
throw new Error(`Proveedor no soportado: ${key}`);
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const { Buffer } = require('buffer');
|
|
2
|
+
const { REQUEST_TIMEOUT_MS } = require('../../config');
|
|
3
|
+
const { parseAgentResponse } = require('../../core/prompts');
|
|
4
|
+
|
|
5
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36';
|
|
6
|
+
const GEMINI_BASE = 'https://gemini.google.com';
|
|
7
|
+
const ANON_COOKIE_URL = `${GEMINI_BASE}/_/BardChatUi/data/batchexecute?rpcids=maGuAc&source-path=%2F&hl=en-US&rt=c`;
|
|
8
|
+
const STREAM_URL = `${GEMINI_BASE}/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?hl=en-US&rt=c`;
|
|
9
|
+
|
|
10
|
+
function btoa2(str) {
|
|
11
|
+
return Buffer.from(str, 'utf8').toString('base64');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function atob2(b64) {
|
|
15
|
+
return Buffer.from(b64, 'base64').toString('utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sleep(ms, signal) {
|
|
19
|
+
if (!ms || ms <= 0) return Promise.resolve();
|
|
20
|
+
if (signal?.aborted) return Promise.reject(new Error('aborted'));
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const timeout = setTimeout(() => {
|
|
23
|
+
cleanup();
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
const onAbort = () => {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
cleanup();
|
|
29
|
+
reject(new Error('aborted'));
|
|
30
|
+
};
|
|
31
|
+
function cleanup() {
|
|
32
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
33
|
+
}
|
|
34
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTimeoutSignal(signal, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
+
const onExternalAbort = () => controller.abort();
|
|
42
|
+
|
|
43
|
+
if (signal) {
|
|
44
|
+
if (signal.aborted) controller.abort();
|
|
45
|
+
else signal.addEventListener('abort', onExternalAbort, { once: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
cleanup() {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
if (signal) signal.removeEventListener('abort', onExternalAbort);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchWithTimeout(url, options = {}) {
|
|
58
|
+
const timeoutSignal = createTimeoutSignal(options.signal, options.timeoutMs || REQUEST_TIMEOUT_MS);
|
|
59
|
+
try {
|
|
60
|
+
return await fetch(url, {
|
|
61
|
+
...options,
|
|
62
|
+
signal: timeoutSignal.signal,
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
timeoutSignal.cleanup();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function walkDeep(node, visit, depth = 0, maxDepth = 7) {
|
|
70
|
+
if (depth > maxDepth) return;
|
|
71
|
+
if (visit(node, depth) === false) return;
|
|
72
|
+
if (Array.isArray(node)) {
|
|
73
|
+
for (const x of node) walkDeep(x, visit, depth + 1, maxDepth);
|
|
74
|
+
} else if (node && typeof node === 'object') {
|
|
75
|
+
for (const k of Object.keys(node)) walkDeep(node[k], visit, depth + 1, maxDepth);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function getAnonCookie(signal) {
|
|
80
|
+
const res = await fetchWithTimeout(ANON_COOKIE_URL, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
redirect: 'manual',
|
|
83
|
+
headers: {
|
|
84
|
+
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
85
|
+
'user-agent': UA,
|
|
86
|
+
},
|
|
87
|
+
body: 'f.req=%5B%5B%5B%22maGuAc%22%2C%22%5B0%5D%22%2Cnull%2C%22generic%22%5D%5D%5D&',
|
|
88
|
+
signal,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const setCookie = res.headers.get('set-cookie');
|
|
92
|
+
if (!setCookie) throw new Error('Gemini no devolvió cookies');
|
|
93
|
+
return setCookie.split(';')[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getXsrfToken(cookieHeader, signal) {
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetchWithTimeout(`${GEMINI_BASE}/app`, {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: {
|
|
101
|
+
'user-agent': UA,
|
|
102
|
+
cookie: cookieHeader,
|
|
103
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
104
|
+
},
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
const html = await res.text();
|
|
108
|
+
const m1 = html.match(/"SNlM0e":"([^"]+)"/);
|
|
109
|
+
if (m1?.[1]) return m1[1];
|
|
110
|
+
const m2 = html.match(/"at":"([^"]+)"/);
|
|
111
|
+
if (m2?.[1]) return m2[1];
|
|
112
|
+
} catch {}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
function extractLooseFinalContent(text) {
|
|
118
|
+
const raw = String(text || '').trim();
|
|
119
|
+
if (!/(?:["'])type(?:["'])\s*:\s*(?:["'])final(?:["'])/i.test(raw)) return null;
|
|
120
|
+
|
|
121
|
+
const contentMatch = raw.match(/(?:["'])content(?:["'])\s*:\s*(["'])/i);
|
|
122
|
+
if (!contentMatch) return null;
|
|
123
|
+
|
|
124
|
+
const quote = contentMatch[1];
|
|
125
|
+
const start = contentMatch.index + contentMatch[0].length;
|
|
126
|
+
let escaped = false;
|
|
127
|
+
|
|
128
|
+
for (let i = start; i < raw.length; i += 1) {
|
|
129
|
+
const ch = raw[i];
|
|
130
|
+
if (escaped) { escaped = false; continue; }
|
|
131
|
+
if (ch === '\\') { escaped = true; continue; }
|
|
132
|
+
if (ch !== quote) continue;
|
|
133
|
+
|
|
134
|
+
const tail = raw.slice(i + 1).trim();
|
|
135
|
+
if (!tail || /^}\s*$/.test(tail) || /^,\s*["']\w+["']\s*:/.test(tail)) {
|
|
136
|
+
return raw.slice(start, i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return raw.slice(start).replace(/"?\s*}\s*$/, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function unescapeLooseJsonString(text) {
|
|
144
|
+
return String(text || '')
|
|
145
|
+
.replace(/\\n/g, '\n')
|
|
146
|
+
.replace(/\\t/g, '\t')
|
|
147
|
+
.replace(/\\r/g, '\r')
|
|
148
|
+
.replace(/\\"/g, '"')
|
|
149
|
+
.replace(/\\\\/g, '\\')
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cleanGeminiResponseText(text) {
|
|
154
|
+
const raw = String(text || '').trim();
|
|
155
|
+
if (!raw) return '';
|
|
156
|
+
|
|
157
|
+
const unfenced = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
158
|
+
const parsed = parseAgentResponse(unfenced);
|
|
159
|
+
if (parsed?.type === 'final' && typeof parsed.content === 'string' && parsed.content.trim()) {
|
|
160
|
+
return parsed.content.trim();
|
|
161
|
+
}
|
|
162
|
+
if (parsed?.type === 'tool') return unfenced;
|
|
163
|
+
|
|
164
|
+
const looseFinal = extractLooseFinalContent(unfenced);
|
|
165
|
+
if (looseFinal !== null) return unescapeLooseJsonString(looseFinal);
|
|
166
|
+
|
|
167
|
+
return unfenced;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isLikelyText(value) {
|
|
171
|
+
if (typeof value !== 'string') return false;
|
|
172
|
+
const text = value.trim();
|
|
173
|
+
if (!text) return false;
|
|
174
|
+
if (text.length < 2) return false;
|
|
175
|
+
if (/^https?:\/\//i.test(text)) return false;
|
|
176
|
+
if (/^\/\/www\./i.test(text)) return false;
|
|
177
|
+
if (/maps\/vt\/data/i.test(text)) return false;
|
|
178
|
+
if (/^c_[0-9a-f]{6,}$/i.test(text)) return false;
|
|
179
|
+
if (/^[A-Za-z0-9_\-+/=]{16,}$/.test(text) && !/\s/.test(text)) return false;
|
|
180
|
+
if (/^\{.*\}$/.test(text) || /^\[.*\]$/.test(text)) return false;
|
|
181
|
+
return text.length >= 8 || /\s/.test(text);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pickBestTextFromAny(parsed) {
|
|
185
|
+
const found = [];
|
|
186
|
+
walkDeep(parsed, (node) => {
|
|
187
|
+
if (typeof node !== 'string' || !isLikelyText(node)) return;
|
|
188
|
+
const cleaned = cleanGeminiResponseText(node);
|
|
189
|
+
if (cleaned) found.push(cleaned);
|
|
190
|
+
});
|
|
191
|
+
found.sort((a, b) => b.length - a.length);
|
|
192
|
+
return found[0] || '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function pickFirstString(parsed, accept) {
|
|
196
|
+
let first = '';
|
|
197
|
+
walkDeep(parsed, (node) => {
|
|
198
|
+
if (first) return false;
|
|
199
|
+
if (typeof node !== 'string') return undefined;
|
|
200
|
+
const text = node.trim();
|
|
201
|
+
if (text && (!accept || accept(text))) first = text;
|
|
202
|
+
if (first) return false;
|
|
203
|
+
return undefined;
|
|
204
|
+
});
|
|
205
|
+
return first;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findInnerPayloadString(outer) {
|
|
209
|
+
const candidates = [];
|
|
210
|
+
const add = (value) => {
|
|
211
|
+
if (typeof value !== 'string') return;
|
|
212
|
+
const text = value.trim();
|
|
213
|
+
if (!text) return;
|
|
214
|
+
candidates.push(text);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
add(outer?.[0]?.[2]);
|
|
218
|
+
add(outer?.[2]);
|
|
219
|
+
add(outer?.[0]?.[0]?.[2]);
|
|
220
|
+
walkDeep(outer, (node) => {
|
|
221
|
+
if (typeof node === 'string') {
|
|
222
|
+
const text = node.trim();
|
|
223
|
+
if ((text.startsWith('[') || text.startsWith('{')) && text.length > 20) add(text);
|
|
224
|
+
}
|
|
225
|
+
}, 0, 5);
|
|
226
|
+
|
|
227
|
+
for (const candidate of candidates) {
|
|
228
|
+
try {
|
|
229
|
+
JSON.parse(candidate);
|
|
230
|
+
return candidate;
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseStream(data) {
|
|
237
|
+
if (typeof data !== 'string' || !data.trim()) throw new Error('Respuesta vacía');
|
|
238
|
+
|
|
239
|
+
const chunks = Array.from(
|
|
240
|
+
data.matchAll(/^\d+\r?\n([\s\S]+?)\r?\n(?=\d+\r?\n|$)/gm),
|
|
241
|
+
).map(match => match[1]).reverse();
|
|
242
|
+
|
|
243
|
+
if (!chunks.length) throw new Error('Respuesta inválida');
|
|
244
|
+
|
|
245
|
+
let best = { text: '', resumeArray: null, parsed: null };
|
|
246
|
+
for (const chunk of chunks) {
|
|
247
|
+
try {
|
|
248
|
+
const outer = JSON.parse(chunk);
|
|
249
|
+
const inner = findInnerPayloadString(outer);
|
|
250
|
+
if (!inner) continue;
|
|
251
|
+
const parsed = JSON.parse(inner);
|
|
252
|
+
const text = pickBestTextFromAny(parsed);
|
|
253
|
+
const resumeArray = Array.isArray(parsed?.[1]) ? parsed[1] : null;
|
|
254
|
+
if (!best.parsed || (text && text.length > (best.text?.length || 0))) {
|
|
255
|
+
best = { text, resumeArray, parsed };
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!best.parsed) throw new Error('Error de parseo');
|
|
261
|
+
|
|
262
|
+
let cleanText = cleanGeminiResponseText(best.text).replace(/\*\*(.+?)\*\*/g, '*$1*').trim();
|
|
263
|
+
if (!cleanText) {
|
|
264
|
+
const accept = text => !/^https?:\/\/|^\/\/www\.|maps\/vt\/data/i.test(text);
|
|
265
|
+
cleanText = cleanGeminiResponseText(pickFirstString(best.parsed, accept) || pickFirstString(best.parsed)).replace(/\*\*(.+?)\*\*/g, '*$1*').trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { text: cleanText, resumeArray: best.resumeArray };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function geminiScraper(prompt, previousId = null, options = {}) {
|
|
272
|
+
let resumeArray = null;
|
|
273
|
+
if (previousId) {
|
|
274
|
+
try {
|
|
275
|
+
const json = JSON.parse(atob2(previousId));
|
|
276
|
+
resumeArray = json?.resumeArray || null;
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let lastErr = null;
|
|
281
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
282
|
+
try {
|
|
283
|
+
if (options.signal?.aborted) throw new Error('aborted');
|
|
284
|
+
const cookie = await getAnonCookie(options.signal);
|
|
285
|
+
const xsrf = await getXsrfToken(cookie, options.signal);
|
|
286
|
+
const payload = [[prompt.trim()], ['en-US'], resumeArray];
|
|
287
|
+
const fReq = [null, JSON.stringify(payload)];
|
|
288
|
+
const params = new URLSearchParams({ 'f.req': JSON.stringify(fReq) });
|
|
289
|
+
if (xsrf) params.append('at', xsrf);
|
|
290
|
+
|
|
291
|
+
const response = await fetchWithTimeout(STREAM_URL, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: {
|
|
294
|
+
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
295
|
+
'user-agent': UA,
|
|
296
|
+
'x-same-domain': '1',
|
|
297
|
+
cookie,
|
|
298
|
+
},
|
|
299
|
+
body: params,
|
|
300
|
+
signal: options.signal,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const data = await response.text();
|
|
304
|
+
if (!response.ok) throw new Error(`Gemini fallo (${response.status}): ${data.slice(0, 200)}`);
|
|
305
|
+
const parsed = parseStream(data);
|
|
306
|
+
const id = btoa2(JSON.stringify({ resumeArray: parsed.resumeArray }));
|
|
307
|
+
return { status: true, response: cleanGeminiResponseText(parsed.text), id };
|
|
308
|
+
} catch (err) {
|
|
309
|
+
lastErr = err;
|
|
310
|
+
if (options.signal?.aborted) break;
|
|
311
|
+
if (attempt < 3) await sleep(700, options.signal);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { status: false, message: lastErr?.message || 'Gemini fallo' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function gemini(messages, _modelId, onChunk = null, options = {}) {
|
|
319
|
+
const prompt = Array.isArray(messages)
|
|
320
|
+
? messages.map(message => {
|
|
321
|
+
if (message.role === 'system') return `[Sistema]\n${message.content}`;
|
|
322
|
+
if (message.role === 'assistant') return `[Asistente]\n${message.content}`;
|
|
323
|
+
return `[Usuario]\n${message.content}`;
|
|
324
|
+
}).join('\n\n')
|
|
325
|
+
: String(messages || '');
|
|
326
|
+
|
|
327
|
+
const result = await geminiScraper(prompt, options.previousId || null, options);
|
|
328
|
+
if (!result.status) throw new Error(result.message || 'Gemini fallo');
|
|
329
|
+
if (onChunk && result.response) onChunk(result.response, 'answer');
|
|
330
|
+
return { text: result.response || '', thinking: '', id: result.id };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
gemini,
|
|
335
|
+
geminiScraper,
|
|
336
|
+
cleanGeminiResponseText,
|
|
337
|
+
parseStream,
|
|
338
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { qwen } = require('./qwen/index');
|
|
2
2
|
const { zen } = require('./zen/index');
|
|
3
|
-
const {
|
|
4
|
-
const { openaiCompatible } = require('./openaiCompatible/index');
|
|
3
|
+
const { gemini } = require('./gemini/index');
|
|
5
4
|
const { DEFAULT_MODEL_KEY, MODELS } = require('../config');
|
|
6
5
|
|
|
7
6
|
function buildPromptFromMessages(messages) {
|
|
@@ -29,10 +28,8 @@ async function runProvider(provider, messages, model, onChunk, options = {}) {
|
|
|
29
28
|
switch (provider) {
|
|
30
29
|
case 'zen':
|
|
31
30
|
return zen(messages, model.zenModel, onChunk, options);
|
|
32
|
-
case '
|
|
33
|
-
return
|
|
34
|
-
case 'openai-compatible':
|
|
35
|
-
return openaiCompatible(messages, model.openaiModel || model.model || model.label, onChunk, options);
|
|
31
|
+
case 'gemini':
|
|
32
|
+
return gemini(messages, model.geminiModel || 'gemini-flash', onChunk, options);
|
|
36
33
|
case 'qwen':
|
|
37
34
|
default: {
|
|
38
35
|
const prompt = buildPromptFromMessages(messages);
|