memi-agent 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/README.md +90 -0
- package/memi-agent.js +1364 -0
- package/memi-client/README.md +43 -0
- package/memi-client/dist/assets/index-CmQIBT8Z.js +210 -0
- package/memi-client/dist/assets/index-Djh6rbJ-.css +1 -0
- package/memi-client/dist/index.html +13 -0
- package/memi-config/workspace/IDENTITY.md +5 -0
- package/memi-config/workspace/MEMORY.md +3 -0
- package/memi-config/workspace/SOUL.md +4 -0
- package/memi-config/workspace/TOOLS.md +3 -0
- package/memi-config/workspace/USER.md +3 -0
- package/memi-dashboard.html +359 -0
- package/memi-server/README.md +56 -0
- package/memi-server/gateway.js +532 -0
- package/memi-server/index.js +152 -0
- package/memi-server/package-lock.json +1658 -0
- package/memi-server/package.json +16 -0
- package/memi-server/routes/api.js +744 -0
- package/memi-server/utils/agent.js +1068 -0
- package/memi-server/utils/aiProxy.js +871 -0
- package/memi-server/utils/importSkill.js +74 -0
- package/package.json +27 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const https = require("https");
|
|
4
|
+
|
|
5
|
+
// ─── 厂商标识 ──────────────────────────────────────────────
|
|
6
|
+
const PROVIDER = {
|
|
7
|
+
OPENAI: "openai",
|
|
8
|
+
GEMINI: "gemini",
|
|
9
|
+
ANTHROPIC: "anthropic",
|
|
10
|
+
NVIDIA: "nvidia",
|
|
11
|
+
POLLINATIONS: "pollinations",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ─── 厂商探测:先按 baseUrl 关键词,再按模型名前缀兜底 ──
|
|
15
|
+
function detectProvider(baseUrl, model) {
|
|
16
|
+
const url = (baseUrl || "").toLowerCase();
|
|
17
|
+
const mdl = (model || "").toLowerCase();
|
|
18
|
+
|
|
19
|
+
if (url.includes("googleapis.com") || url.includes("generativelanguage")) return PROVIDER.GEMINI;
|
|
20
|
+
if (url.includes("anthropic.com")) return PROVIDER.ANTHROPIC;
|
|
21
|
+
if (url.includes("nvidia.com") || url.includes("integrate.api.nvidia.com")) return PROVIDER.NVIDIA;
|
|
22
|
+
if (url.includes("pollinations")) return PROVIDER.POLLINATIONS;
|
|
23
|
+
if (url.includes("openai.com")) return PROVIDER.OPENAI;
|
|
24
|
+
|
|
25
|
+
// 模型名兜底 — 适用于 baseUrl 是通用反向代理的场景
|
|
26
|
+
if (mdl.startsWith("gemini")) return PROVIDER.GEMINI;
|
|
27
|
+
if (mdl.startsWith("claude")) return PROVIDER.ANTHROPIC;
|
|
28
|
+
|
|
29
|
+
return PROVIDER.OPENAI; // 默认 OpenAI 兼容
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 普通文本和视觉模型请求超时,兼容 CPU 上较慢的本地 Ollama
|
|
33
|
+
const REQUEST_TIMEOUT = 60000;
|
|
34
|
+
const IMAGE_GEN_TIMEOUT = 180000;
|
|
35
|
+
const VISION_TIMEOUT = 180000;
|
|
36
|
+
const IMAGE_DOWNLOAD_TIMEOUT = 60000;
|
|
37
|
+
const MAX_VISION_IMAGE_BYTES = 100 * 1024;
|
|
38
|
+
const IMAGE_DOWNLOAD_HEADERS_BASE = {
|
|
39
|
+
"User-Agent":
|
|
40
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
41
|
+
Accept: "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
|
|
42
|
+
Referer: "https://pollinations.ai/",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function resolveDownloadHeaders(imageUrlOrHost) {
|
|
46
|
+
// 只有 enterprise 端点才发 token;image.pollinations.ai 发 token 会被识别为认证请求而拒之
|
|
47
|
+
const target = imageUrlOrHost ? String(imageUrlOrHost) : "";
|
|
48
|
+
if (target.includes("enter.pollinations.ai")) {
|
|
49
|
+
return { ...IMAGE_DOWNLOAD_HEADERS_BASE, "x-enter-token": "1" };
|
|
50
|
+
}
|
|
51
|
+
return IMAGE_DOWNLOAD_HEADERS_BASE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let sharp = null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
sharp = require("sharp");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// sharp 不可用时仍允许视觉审核继续,只是不做压缩
|
|
60
|
+
console.warn("图片未压缩,可能导致 API 请求过大");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildUrl(baseUrl, path) {
|
|
64
|
+
return `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getHeaders(apiKey, provider) {
|
|
68
|
+
const headers = { "Content-Type": "application/json" };
|
|
69
|
+
|
|
70
|
+
if (provider === PROVIDER.ANTHROPIC) {
|
|
71
|
+
headers["x-api-key"] = apiKey;
|
|
72
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
73
|
+
} else {
|
|
74
|
+
// OpenAI 兼容 — 包括 NVIDIA NIM / Ollama 等
|
|
75
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getErrorMessage(error, timeoutMessage) {
|
|
82
|
+
if (error.code === "ECONNABORTED") {
|
|
83
|
+
return timeoutMessage || "API 请求超时";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const status = error.response?.status;
|
|
87
|
+
const message =
|
|
88
|
+
error.response?.data?.error?.message ||
|
|
89
|
+
error.response?.data?.message ||
|
|
90
|
+
error.code ||
|
|
91
|
+
error.message ||
|
|
92
|
+
"未知错误";
|
|
93
|
+
|
|
94
|
+
if (status === 401) {
|
|
95
|
+
return "API Key 无效";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return `API 错误: ${status || "未知状态"} ${message}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseImageData(content) {
|
|
102
|
+
if (typeof content !== "string") {
|
|
103
|
+
return content;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const text = content.trim();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(text);
|
|
110
|
+
return data.url || data.imageUrl || data.image_url || data.base64 || data.b64_json || data.image || data;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const dataUrlMatch = text.match(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/);
|
|
113
|
+
const urlMatch = text.match(/https?:\/\/\S+/);
|
|
114
|
+
|
|
115
|
+
return dataUrlMatch?.[0] || urlMatch?.[0] || text;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const RETRYABLE_STATUSES = [429, 502, 503];
|
|
120
|
+
|
|
121
|
+
async function postJson(url, apiKey, body, options = {}) {
|
|
122
|
+
const timeout = options.timeout || REQUEST_TIMEOUT;
|
|
123
|
+
const timeoutMessage = options.timeoutMessage;
|
|
124
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
125
|
+
const provider = options.provider || PROVIDER.OPENAI;
|
|
126
|
+
|
|
127
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const response = await axios.post(url, body, {
|
|
130
|
+
headers: getHeaders(apiKey, provider),
|
|
131
|
+
timeout,
|
|
132
|
+
proxy: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return response.data;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const status = error.response?.status;
|
|
138
|
+
const isRetryable = RETRYABLE_STATUSES.includes(status);
|
|
139
|
+
|
|
140
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
141
|
+
throw new Error(getErrorMessage(error, timeoutMessage));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 指数退避:800ms, 1600ms, 3200ms...
|
|
145
|
+
const delay = 800 * Math.pow(2, attempt - 1);
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// unreachable — last attempt always throws
|
|
151
|
+
throw new Error("postJson: 意外退出重试循环");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function compressVisionImage(buffer) {
|
|
155
|
+
if (!sharp) {
|
|
156
|
+
console.warn("图片未压缩,可能导致 API 请求过大");
|
|
157
|
+
return buffer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const attempts = [
|
|
161
|
+
{ size: 512, quality: 80 },
|
|
162
|
+
{ size: 512, quality: 70 },
|
|
163
|
+
{ size: 448, quality: 70 },
|
|
164
|
+
{ size: 384, quality: 65 },
|
|
165
|
+
{ size: 320, quality: 60 },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
for (const attempt of attempts) {
|
|
169
|
+
// 将图片等比例压缩到指定尺寸以内,并输出为 JPEG
|
|
170
|
+
const compressed = await sharp(buffer)
|
|
171
|
+
.resize(attempt.size, attempt.size, { fit: "inside" })
|
|
172
|
+
.jpeg({ quality: attempt.quality })
|
|
173
|
+
.toBuffer();
|
|
174
|
+
|
|
175
|
+
if (compressed.length <= MAX_VISION_IMAGE_BYTES) {
|
|
176
|
+
return compressed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 极端情况下继续返回最后一档压缩结果,尽量降低请求体大小
|
|
181
|
+
return sharp(buffer)
|
|
182
|
+
.resize(256, 256, { fit: "inside" })
|
|
183
|
+
.jpeg({ quality: 55 })
|
|
184
|
+
.toBuffer();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isImageBuffer(buffer) {
|
|
188
|
+
if (buffer.length < 4) return false;
|
|
189
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return true;
|
|
190
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return true;
|
|
191
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) return true;
|
|
192
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) return true;
|
|
193
|
+
if (buffer[0] === 0x42 && buffer[1] === 0x4d) return true;
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function downloadImageWithNative(imageUrl) {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const client = imageUrl.startsWith("https://") ? https : http;
|
|
200
|
+
const request = client.get(
|
|
201
|
+
imageUrl,
|
|
202
|
+
{
|
|
203
|
+
headers: resolveDownloadHeaders(imageUrl),
|
|
204
|
+
timeout: IMAGE_DOWNLOAD_TIMEOUT,
|
|
205
|
+
},
|
|
206
|
+
(response) => {
|
|
207
|
+
const chunks = [];
|
|
208
|
+
|
|
209
|
+
if (!response) {
|
|
210
|
+
reject(new Error("图片服务器没有返回响应"));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
215
|
+
response.on("end", () => {
|
|
216
|
+
const buffer = Buffer.concat(chunks);
|
|
217
|
+
|
|
218
|
+
if (response.statusCode !== 200) {
|
|
219
|
+
const errorText = buffer.toString("utf8").slice(0, 200);
|
|
220
|
+
reject(
|
|
221
|
+
new Error(
|
|
222
|
+
`图片服务器返回 Status Code ${response.statusCode}${
|
|
223
|
+
errorText ? `,内容:${errorText}` : ""
|
|
224
|
+
}`
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (buffer.length === 0) {
|
|
231
|
+
reject(new Error("图片服务器返回了空内容"));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!isImageBuffer(buffer)) {
|
|
236
|
+
const textPreview = buffer.toString("utf8").slice(0, 200);
|
|
237
|
+
reject(
|
|
238
|
+
new Error(
|
|
239
|
+
`响应内容不是有效图片格式,内容预览:${textPreview}`
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
resolve(buffer);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
request.on("timeout", () => {
|
|
251
|
+
request.destroy(new Error("API 请求超时"));
|
|
252
|
+
});
|
|
253
|
+
request.on("error", reject);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function downloadVisionImage(imageUrl) {
|
|
258
|
+
let lastError = null;
|
|
259
|
+
|
|
260
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
261
|
+
try {
|
|
262
|
+
// 视觉审核前先下载图片,Pollinations 偶发 5xx 时短暂重试
|
|
263
|
+
return await downloadImageWithNative(imageUrl);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
lastError = error;
|
|
266
|
+
|
|
267
|
+
if (!error.message.includes("Status Code 5") || attempt === 3) {
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await new Promise((resolve) => {
|
|
272
|
+
setTimeout(resolve, 800 * attempt);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw lastError;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildPollinationsUrl(baseUrl, prompt, width, height, seed, extraParams = "&nologo=true") {
|
|
281
|
+
return `${baseUrl}/prompt/${encodeURIComponent(
|
|
282
|
+
prompt
|
|
283
|
+
)}?width=${width}&height=${height}&seed=${seed}${extraParams}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getPromptSeed(prompt) {
|
|
287
|
+
let hash = 0;
|
|
288
|
+
|
|
289
|
+
for (let index = 0; index < prompt.length; index += 1) {
|
|
290
|
+
hash = (hash * 31 + prompt.charCodeAt(index)) >>> 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return hash || 42;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const POLLINATIONS_PUBLIC_HOST = "https://image.pollinations.ai";
|
|
297
|
+
|
|
298
|
+
const POLLINATIONS_URL_VARIANTS = [
|
|
299
|
+
"&nologo=true", // 标准格式
|
|
300
|
+
"", // 无 nologo
|
|
301
|
+
"&nologo=true&format=jpeg", // 标准 + JPEG
|
|
302
|
+
"&format=jpeg", // 仅 JPEG
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const POLLINATIONS_LONG_TIMEOUT = 180000; // 180s — Pollinations 生成慢时可能需长时间排队
|
|
306
|
+
const isLegacyEndpointError = (msg) => msg && typeof msg === "string" && msg.includes("legacy endpoint");
|
|
307
|
+
|
|
308
|
+
async function downloadPollinationsImage(baseUrl, prompt, width, height) {
|
|
309
|
+
const seeds = [
|
|
310
|
+
getPromptSeed(prompt),
|
|
311
|
+
42,
|
|
312
|
+
20240523,
|
|
313
|
+
Math.floor(Math.random() * 1000000000),
|
|
314
|
+
];
|
|
315
|
+
const hosts = [baseUrl, POLLINATIONS_PUBLIC_HOST];
|
|
316
|
+
let lastError = "";
|
|
317
|
+
let fallbackUrl = "";
|
|
318
|
+
let hitLegacyError = false;
|
|
319
|
+
|
|
320
|
+
for (const host of hosts) {
|
|
321
|
+
for (const variant of POLLINATIONS_URL_VARIANTS) {
|
|
322
|
+
const trySeeds = variant === "&nologo=true" ? seeds : [seeds[0]];
|
|
323
|
+
for (const seed of trySeeds) {
|
|
324
|
+
const imageUrl = buildPollinationsUrl(host, prompt, width, height, seed, variant);
|
|
325
|
+
if (host === POLLINATIONS_PUBLIC_HOST && seed === seeds[0]) {
|
|
326
|
+
fallbackUrl = imageUrl;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const response = await axios.get(imageUrl, {
|
|
330
|
+
responseType: "arraybuffer",
|
|
331
|
+
timeout: POLLINATIONS_LONG_TIMEOUT,
|
|
332
|
+
headers: resolveDownloadHeaders(imageUrl),
|
|
333
|
+
proxy: false,
|
|
334
|
+
// 402 = queue full,允许响应而不是直接抛异常
|
|
335
|
+
validateStatus: (status) => (status >= 200 && status < 300) || status === 402 || status === 500,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const buffer = Buffer.from(response.data);
|
|
339
|
+
const ct = (response.headers["content-type"] || "").toLowerCase();
|
|
340
|
+
const status = response.status;
|
|
341
|
+
|
|
342
|
+
// 402 queue full → 等一会重试
|
|
343
|
+
if (status === 402) {
|
|
344
|
+
lastError = `队列满: ${buffer.toString("utf8").slice(0, 100)}`;
|
|
345
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// JSON/文本响应 → Pollinations 返回了错误信息
|
|
350
|
+
if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
|
|
351
|
+
const text = buffer.toString("utf8").slice(0, 500);
|
|
352
|
+
if (isLegacyEndpointError(text)) {
|
|
353
|
+
hitLegacyError = true;
|
|
354
|
+
lastError = "Pollinations 旧版端点已废弃";
|
|
355
|
+
} else {
|
|
356
|
+
lastError = text;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 图片响应
|
|
362
|
+
if (buffer.length > 0 && isImageBuffer(buffer)) {
|
|
363
|
+
return `data:image/jpeg;base64,${buffer.toString("base64")}`;
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
lastError = error.message;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const truncated = lastError.length > 120 ? lastError.slice(0, 120) + "..." : lastError;
|
|
373
|
+
console.error(`[Pollinations] All attempts failed (${truncated}), fallback: ${fallbackUrl.slice(0, 100)}...`);
|
|
374
|
+
|
|
375
|
+
if (hitLegacyError) {
|
|
376
|
+
throw new Error("Pollinations 已将旧版端点标记为废弃,生图可能失败。建议更换到其他 API2 提供商(如设置中更换 baseUrl)。");
|
|
377
|
+
}
|
|
378
|
+
return fallbackUrl || buildPollinationsUrl(POLLINATIONS_PUBLIC_HOST, prompt, width, height, seeds[0], "");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function getVisionImageUrl(imageUrl) {
|
|
382
|
+
if (typeof imageUrl === "string" && imageUrl.startsWith("data:image")) {
|
|
383
|
+
return imageUrl;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
typeof imageUrl === "string" &&
|
|
388
|
+
(imageUrl.startsWith("http://") || imageUrl.startsWith("https://"))
|
|
389
|
+
) {
|
|
390
|
+
try {
|
|
391
|
+
// Ollama 和 NVIDIA NIM 的部分视觉模型不支持远程图片 URL,这里先下载并转成 base64
|
|
392
|
+
const downloadedImage = await downloadVisionImage(imageUrl);
|
|
393
|
+
const imageBuffer = await compressVisionImage(downloadedImage);
|
|
394
|
+
const base64 = imageBuffer.toString("base64");
|
|
395
|
+
|
|
396
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw new Error(`图片下载失败:${getErrorMessage(error)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return imageUrl;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ─── LLM 调用:厂商专用实现 ────────────────────────────
|
|
406
|
+
|
|
407
|
+
async function callLLMOpenAI(baseUrl, apiKey, model, messages, extra) {
|
|
408
|
+
const { maxTokens, temperature } = extra || {};
|
|
409
|
+
const body = {
|
|
410
|
+
model,
|
|
411
|
+
messages,
|
|
412
|
+
temperature: temperature ?? 0.7,
|
|
413
|
+
};
|
|
414
|
+
if (maxTokens > 0) body.max_tokens = maxTokens;
|
|
415
|
+
|
|
416
|
+
const data = await postJson(
|
|
417
|
+
buildUrl(baseUrl, "/chat/completions"),
|
|
418
|
+
apiKey,
|
|
419
|
+
body,
|
|
420
|
+
{ provider: PROVIDER.OPENAI },
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
return data.choices?.[0]?.message?.content || "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function callLLMGemini(baseUrl, apiKey, model, messages, extra) {
|
|
427
|
+
const { maxTokens, temperature } = extra || {};
|
|
428
|
+
const endpoint = buildUrl(baseUrl, `/models/${model}:generateContent`);
|
|
429
|
+
|
|
430
|
+
// Gemini role map: "assistant" → "model", "system" → "user"(Gemini 无 system role)
|
|
431
|
+
const contents = [{ role: "user", parts: [] }];
|
|
432
|
+
for (const msg of messages) {
|
|
433
|
+
if (msg.role === "system") {
|
|
434
|
+
// Gemini 不支持 system role,合并到第一条 user 内容前
|
|
435
|
+
contents[0].parts.push({ text: `[System instruction]\n${msg.content}\n[/System instruction]\n` });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const role = msg.role === "assistant" ? "model" : "user";
|
|
439
|
+
// 如果是 system 之后的 user,追加到当前 user block
|
|
440
|
+
if (role === "user" && contents[contents.length - 1].role === "user") {
|
|
441
|
+
contents[contents.length - 1].parts.push({ text: msg.content });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
contents.push({ role, parts: [{ text: msg.content }] });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const body = { contents, generationConfig: { temperature: temperature ?? 0.7 } };
|
|
448
|
+
if (maxTokens > 0) body.generationConfig.maxOutputTokens = maxTokens;
|
|
449
|
+
|
|
450
|
+
const headers = { "Content-Type": "application/json" };
|
|
451
|
+
if (apiKey) headers["x-goog-api-key"] = apiKey;
|
|
452
|
+
|
|
453
|
+
const response = await axios.post(endpoint, body, { headers, timeout: REQUEST_TIMEOUT, proxy: false });
|
|
454
|
+
const text = response.data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
455
|
+
return text;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function callLLMAnthropic(baseUrl, apiKey, model, messages, extra) {
|
|
459
|
+
const { maxTokens, temperature } = extra || {};
|
|
460
|
+
|
|
461
|
+
// Anthropic 消息格式:[{role, content}],合并连续同角色消息
|
|
462
|
+
const anthropicMessages = [];
|
|
463
|
+
for (const m of messages) {
|
|
464
|
+
if (m.role === "system") continue;
|
|
465
|
+
const role = m.role === "assistant" ? "assistant" : "user";
|
|
466
|
+
const last = anthropicMessages[anthropicMessages.length - 1];
|
|
467
|
+
if (last && last.role === role) {
|
|
468
|
+
last.content += "\n" + m.content;
|
|
469
|
+
} else {
|
|
470
|
+
anthropicMessages.push({ role, content: m.content || "" });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 提取 system prompt(Anthropic 使用顶层 system 参数)
|
|
475
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
476
|
+
|
|
477
|
+
const body = {
|
|
478
|
+
model,
|
|
479
|
+
messages: anthropicMessages,
|
|
480
|
+
max_tokens: maxTokens > 0 ? maxTokens : 1024,
|
|
481
|
+
temperature: temperature ?? 0.7,
|
|
482
|
+
};
|
|
483
|
+
if (systemMsg) body.system = systemMsg.content;
|
|
484
|
+
|
|
485
|
+
const headers = {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
"x-api-key": apiKey,
|
|
488
|
+
"anthropic-version": "2023-06-01",
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const response = await axios.post(buildUrl(baseUrl, "/messages"), body, {
|
|
492
|
+
headers,
|
|
493
|
+
timeout: REQUEST_TIMEOUT,
|
|
494
|
+
proxy: false,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return response.data?.content?.[0]?.text || "";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function callLLM(provider, messages, extra = {}) {
|
|
501
|
+
const { baseUrl, apiKey, model } = provider;
|
|
502
|
+
const detected = detectProvider(baseUrl, model);
|
|
503
|
+
|
|
504
|
+
switch (detected) {
|
|
505
|
+
case PROVIDER.GEMINI:
|
|
506
|
+
return callLLMGemini(baseUrl, apiKey, model, messages, extra);
|
|
507
|
+
case PROVIDER.ANTHROPIC:
|
|
508
|
+
return callLLMAnthropic(baseUrl, apiKey, model, messages, extra);
|
|
509
|
+
default:
|
|
510
|
+
return callLLMOpenAI(baseUrl, apiKey, model, messages, extra);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function callImageGen(provider, prompt, width, height) {
|
|
515
|
+
const { baseUrl, apiKey, model } = provider;
|
|
516
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
517
|
+
const detected = detectProvider(baseUrl, model);
|
|
518
|
+
|
|
519
|
+
// Gemini 原生生图
|
|
520
|
+
if (detected === PROVIDER.GEMINI) {
|
|
521
|
+
return callGeminiImage(normalizedBaseUrl, apiKey, model, prompt, width, height);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Pollinations 专用流程
|
|
525
|
+
if (detected === PROVIDER.POLLINATIONS) {
|
|
526
|
+
return downloadPollinationsImage(normalizedBaseUrl, prompt, width, height);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// OpenAI DALL-E 风格 /images/generations
|
|
530
|
+
const hasImageGenEndpoint =
|
|
531
|
+
baseUrl.includes("images/generations") || detected === PROVIDER.OPENAI;
|
|
532
|
+
|
|
533
|
+
if (hasImageGenEndpoint) {
|
|
534
|
+
const url = baseUrl.includes("images/generations")
|
|
535
|
+
? baseUrl
|
|
536
|
+
: buildUrl(baseUrl, "/images/generations");
|
|
537
|
+
|
|
538
|
+
const data = await postJson(
|
|
539
|
+
url,
|
|
540
|
+
apiKey,
|
|
541
|
+
{
|
|
542
|
+
model,
|
|
543
|
+
prompt,
|
|
544
|
+
n: 1,
|
|
545
|
+
size: `${width}x${height}`,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
timeout: IMAGE_GEN_TIMEOUT,
|
|
549
|
+
timeoutMessage: "生图 API 请求超时(120秒),请检查网络或模型是否可用",
|
|
550
|
+
provider: detected,
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
return data.data?.[0]?.url || data.data?.[0]?.b64_json || "";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Hugging Face Inference API
|
|
558
|
+
if (normalizedBaseUrl.includes("huggingface")) {
|
|
559
|
+
return callHuggingFaceImage(normalizedBaseUrl, apiKey, model, prompt, width, height);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 通用兼容模式:让模型返回图片 URL 或 base64
|
|
563
|
+
const data = await postJson(
|
|
564
|
+
buildUrl(baseUrl, "/chat/completions"),
|
|
565
|
+
apiKey,
|
|
566
|
+
{
|
|
567
|
+
model,
|
|
568
|
+
messages: [
|
|
569
|
+
{
|
|
570
|
+
role: "user",
|
|
571
|
+
content: `请根据以下提示生成图片,并只返回图片 URL 或 base64 数据:${prompt}。图片尺寸:${width}x${height}`,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
temperature: 0.7,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
timeout: IMAGE_GEN_TIMEOUT,
|
|
578
|
+
timeoutMessage: "生图 API 请求超时(120秒),请检查网络或模型是否可用",
|
|
579
|
+
provider: detected,
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
583
|
+
|
|
584
|
+
return parseImageData(content);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Hugging Face Inference API 生图。
|
|
589
|
+
* POST {baseUrl} body: { inputs: prompt }
|
|
590
|
+
* 返回原始图片 bytes。
|
|
591
|
+
*/
|
|
592
|
+
async function callHuggingFaceImage(baseUrl, apiKey, model, prompt, width, height) {
|
|
593
|
+
const endpoint = baseUrl.includes("models/") ? baseUrl : buildUrl(baseUrl, `/models/${model}`);
|
|
594
|
+
const headers = { "Content-Type": "application/json" };
|
|
595
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
596
|
+
|
|
597
|
+
const body = {
|
|
598
|
+
inputs: prompt + `, ${width}x${height}`,
|
|
599
|
+
parameters: { width, height },
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const response = await axios.post(endpoint, body, {
|
|
604
|
+
headers,
|
|
605
|
+
timeout: IMAGE_GEN_TIMEOUT,
|
|
606
|
+
responseType: "arraybuffer",
|
|
607
|
+
proxy: false,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const buffer = Buffer.from(response.data);
|
|
611
|
+
const ct = (response.headers["content-type"] || "").toLowerCase();
|
|
612
|
+
|
|
613
|
+
if (ct.startsWith("image/")) {
|
|
614
|
+
return `data:${ct};base64,${buffer.toString("base64")}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 部分 HF 模型返回 JSON 中的图片 URL
|
|
618
|
+
const text = buffer.toString("utf8");
|
|
619
|
+
const parsed = parseImageData(text);
|
|
620
|
+
if (parsed && parsed !== text) return parsed;
|
|
621
|
+
|
|
622
|
+
throw new Error(`HF 未返回图片:${text.slice(0, 200)}`);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (error.message.startsWith("HF")) throw error;
|
|
625
|
+
if (error.response?.data) {
|
|
626
|
+
const msg = typeof error.response.data === "string"
|
|
627
|
+
? error.response.data.slice(0, 200)
|
|
628
|
+
: JSON.stringify(error.response.data).slice(0, 200);
|
|
629
|
+
throw new Error(`Hugging Face 生图失败: ${msg}`);
|
|
630
|
+
}
|
|
631
|
+
throw new Error(`Hugging Face 生图失败: ${getErrorMessage(error, "请求超时(180秒)")}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Google Gemini 原生生图。
|
|
637
|
+
* API: POST {baseUrl}/models/{model}:generateContent
|
|
638
|
+
* 返回 inlineData(base64 图片)或文本中的 URL。
|
|
639
|
+
*/
|
|
640
|
+
async function callGeminiImage(baseUrl, apiKey, model, prompt, width, height) {
|
|
641
|
+
const endpoint = buildUrl(baseUrl, `/models/${model}:generateContent`);
|
|
642
|
+
|
|
643
|
+
const headers = { "Content-Type": "application/json" };
|
|
644
|
+
if (apiKey) {
|
|
645
|
+
headers["x-goog-api-key"] = apiKey;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const body = {
|
|
649
|
+
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
650
|
+
generationConfig: {
|
|
651
|
+
responseModalities: ["IMAGE", "TEXT"],
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const response = await axios.post(endpoint, body, {
|
|
657
|
+
headers,
|
|
658
|
+
timeout: IMAGE_GEN_TIMEOUT,
|
|
659
|
+
proxy: false,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const candidates = response.data?.candidates || [];
|
|
663
|
+
|
|
664
|
+
// 优先取 inlineData(图片本体)
|
|
665
|
+
for (const candidate of candidates) {
|
|
666
|
+
const parts = candidate?.content?.parts || [];
|
|
667
|
+
for (const part of parts) {
|
|
668
|
+
if (part.inlineData?.data) {
|
|
669
|
+
const mime = part.inlineData.mimeType || "image/png";
|
|
670
|
+
return `data:${mime};base64,${part.inlineData.data}`;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 兜底:文本中提取图片 URL
|
|
676
|
+
for (const candidate of candidates) {
|
|
677
|
+
const parts = candidate?.content?.parts || [];
|
|
678
|
+
for (const part of parts) {
|
|
679
|
+
if (part.text) {
|
|
680
|
+
const parsed = parseImageData(part.text);
|
|
681
|
+
if (typeof parsed === "string" && (parsed.startsWith("data:") || parsed.startsWith("http"))) {
|
|
682
|
+
return parsed;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
throw new Error("Gemini 未返回图片数据");
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (error.response) {
|
|
691
|
+
const msg = error.response?.data?.error?.message || error.message;
|
|
692
|
+
throw new Error(`Gemini 生图失败: ${msg}`);
|
|
693
|
+
}
|
|
694
|
+
throw new Error(`Gemini 生图失败: ${getErrorMessage(error, "请求超时")}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ─── Vision 调用:厂商专用实现 ──────────────────────────
|
|
699
|
+
|
|
700
|
+
async function callVisionOpenAI(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt) {
|
|
701
|
+
const messages = [
|
|
702
|
+
{
|
|
703
|
+
role: "user",
|
|
704
|
+
content: [
|
|
705
|
+
{ type: "text", text: prompt },
|
|
706
|
+
{ type: "image_url", image_url: { url: visionImageUrl } },
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
];
|
|
710
|
+
|
|
711
|
+
if (systemPrompt) {
|
|
712
|
+
messages.unshift({ role: "system", content: systemPrompt });
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const data = await postJson(
|
|
716
|
+
buildUrl(baseUrl, "/chat/completions"),
|
|
717
|
+
apiKey,
|
|
718
|
+
{ model, messages, temperature: 0.7 },
|
|
719
|
+
{
|
|
720
|
+
timeout: VISION_TIMEOUT,
|
|
721
|
+
timeoutMessage: "审核 API 请求超时(180秒),请检查网络或模型是否可用",
|
|
722
|
+
provider: PROVIDER.OPENAI,
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
return data.choices?.[0]?.message?.content || "";
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function callVisionNVIDIA(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt) {
|
|
730
|
+
const reviewText = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
|
|
731
|
+
|
|
732
|
+
// NVIDIA NIM 使用单条 user 消息承载文字审核指令和 base64 图片
|
|
733
|
+
const data = await postJson(
|
|
734
|
+
buildUrl(baseUrl, "/chat/completions"),
|
|
735
|
+
apiKey,
|
|
736
|
+
{
|
|
737
|
+
model,
|
|
738
|
+
messages: [
|
|
739
|
+
{
|
|
740
|
+
role: "user",
|
|
741
|
+
content: [
|
|
742
|
+
{ type: "text", text: reviewText },
|
|
743
|
+
{ type: "image_url", image_url: { url: visionImageUrl } },
|
|
744
|
+
],
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
max_tokens: 1024,
|
|
748
|
+
temperature: 0.2,
|
|
749
|
+
top_p: 0.9,
|
|
750
|
+
stream: false,
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
timeout: VISION_TIMEOUT,
|
|
754
|
+
timeoutMessage: "审核 API 请求超时(180秒),请检查网络或模型是否可用",
|
|
755
|
+
provider: PROVIDER.NVIDIA,
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
return data.choices?.[0]?.message?.content || "";
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function callVisionGemini(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt) {
|
|
763
|
+
const endpoint = buildUrl(baseUrl, `/models/${model}:generateContent`);
|
|
764
|
+
|
|
765
|
+
// Gemini 用 inlineData 传图
|
|
766
|
+
const parts = [{ text: prompt }];
|
|
767
|
+
|
|
768
|
+
// 系统指令 → Gemini generationConfig.systemInstruction
|
|
769
|
+
let systemInstruction = null;
|
|
770
|
+
if (systemPrompt) {
|
|
771
|
+
systemInstruction = { parts: [{ text: systemPrompt }] };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// 如果 visionImageUrl 是 data URI,提取 base64 数据
|
|
775
|
+
let imagePart = null;
|
|
776
|
+
if (visionImageUrl.startsWith("data:image")) {
|
|
777
|
+
const [meta, b64] = visionImageUrl.split(",");
|
|
778
|
+
const mime = meta.match(/data:(.*?);/)?.[1] || "image/jpeg";
|
|
779
|
+
imagePart = { inlineData: { mimeType: mime, data: b64 } };
|
|
780
|
+
} else if (visionImageUrl.startsWith("http")) {
|
|
781
|
+
// Gemini 支持直接传 URL
|
|
782
|
+
imagePart = { fileData: { fileUri: visionImageUrl, mimeType: "image/jpeg" } };
|
|
783
|
+
} else {
|
|
784
|
+
imagePart = { text: visionImageUrl };
|
|
785
|
+
}
|
|
786
|
+
parts.push(imagePart);
|
|
787
|
+
|
|
788
|
+
const body = {
|
|
789
|
+
contents: [{ role: "user", parts }],
|
|
790
|
+
generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
|
|
791
|
+
};
|
|
792
|
+
if (systemInstruction) body.systemInstruction = systemInstruction;
|
|
793
|
+
|
|
794
|
+
const headers = { "Content-Type": "application/json" };
|
|
795
|
+
if (apiKey) headers["x-goog-api-key"] = apiKey;
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
const response = await axios.post(endpoint, body, { headers, timeout: VISION_TIMEOUT, proxy: false });
|
|
799
|
+
return response.data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (error.response) {
|
|
802
|
+
const msg = error.response?.data?.error?.message || error.message;
|
|
803
|
+
throw new Error(`Gemini 审核失败: ${msg}`);
|
|
804
|
+
}
|
|
805
|
+
throw new Error(`Gemini 审核失败: ${getErrorMessage(error, "请求超时")}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function callVisionAnthropic(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt) {
|
|
810
|
+
// Anthropic 使用 type:"image" content block
|
|
811
|
+
const content = [{ type: "text", text: prompt }];
|
|
812
|
+
|
|
813
|
+
if (visionImageUrl.startsWith("data:image")) {
|
|
814
|
+
const [meta, b64] = visionImageUrl.split(",");
|
|
815
|
+
const mime = meta.match(/data:(.*?);/)?.[1] || "image/jpeg";
|
|
816
|
+
content.push({
|
|
817
|
+
type: "image",
|
|
818
|
+
source: { type: "base64", media_type: mime, data: b64 },
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const body = {
|
|
823
|
+
model,
|
|
824
|
+
messages: [{ role: "user", content }],
|
|
825
|
+
max_tokens: 1024,
|
|
826
|
+
temperature: 0.2,
|
|
827
|
+
};
|
|
828
|
+
if (systemPrompt) body.system = systemPrompt;
|
|
829
|
+
|
|
830
|
+
const headers = {
|
|
831
|
+
"Content-Type": "application/json",
|
|
832
|
+
"x-api-key": apiKey,
|
|
833
|
+
"anthropic-version": "2023-06-01",
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
const response = await axios.post(buildUrl(baseUrl, "/messages"), body, {
|
|
838
|
+
headers, timeout: VISION_TIMEOUT, proxy: false,
|
|
839
|
+
});
|
|
840
|
+
return response.data?.content?.[0]?.text || "";
|
|
841
|
+
} catch (error) {
|
|
842
|
+
if (error.response) {
|
|
843
|
+
const msg = error.response?.data?.error?.message || error.message;
|
|
844
|
+
throw new Error(`Anthropic 审核失败: ${msg}`);
|
|
845
|
+
}
|
|
846
|
+
throw new Error(`Anthropic 审核失败: ${getErrorMessage(error, "请求超时")}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function callVision(provider, imageUrl, prompt, systemPrompt) {
|
|
851
|
+
const { baseUrl, apiKey, model } = provider;
|
|
852
|
+
const visionImageUrl = await getVisionImageUrl(imageUrl);
|
|
853
|
+
const detected = detectProvider(baseUrl, model);
|
|
854
|
+
|
|
855
|
+
switch (detected) {
|
|
856
|
+
case PROVIDER.GEMINI:
|
|
857
|
+
return callVisionGemini(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt);
|
|
858
|
+
case PROVIDER.ANTHROPIC:
|
|
859
|
+
return callVisionAnthropic(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt);
|
|
860
|
+
case PROVIDER.NVIDIA:
|
|
861
|
+
return callVisionNVIDIA(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt);
|
|
862
|
+
default:
|
|
863
|
+
return callVisionOpenAI(baseUrl, apiKey, model, visionImageUrl, prompt, systemPrompt);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
module.exports = {
|
|
868
|
+
callLLM,
|
|
869
|
+
callImageGen,
|
|
870
|
+
callVision,
|
|
871
|
+
};
|