@lobehub/lobehub 2.0.0-next.166 → 2.0.0-next.167
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/.husky/pre-commit +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +25 -0
- package/CLAUDE.md +2 -2
- package/GEMINI.md +1 -1
- package/apps/desktop/package.json +4 -4
- package/apps/desktop/tsconfig.json +4 -13
- package/changelog/v1.json +9 -0
- package/docs/development/database-schema.dbml +1 -0
- package/locales/ar/models.json +12 -0
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/models.json +12 -0
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/models.json +12 -0
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/models.json +12 -0
- package/locales/en-US/providers.json +3 -0
- package/locales/es-ES/models.json +12 -0
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/models.json +12 -0
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/models.json +12 -0
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/models.json +12 -0
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/models.json +12 -0
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/models.json +12 -0
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/models.json +12 -0
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/models.json +12 -0
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/models.json +12 -0
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/models.json +12 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/models.json +12 -0
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/models.json +12 -0
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/models.json +12 -0
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-TW/models.json +12 -0
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +43 -43
- package/packages/database/migrations/0060_add_user_last_active_at.sql +1 -0
- package/packages/database/migrations/meta/0060_snapshot.json +8481 -0
- package/packages/database/migrations/meta/_journal.json +8 -1
- package/packages/database/src/core/migrations.json +9 -1
- package/packages/database/src/schemas/user.ts +1 -0
- package/packages/fetch-sse/src/__tests__/headers.test.ts +2 -2
- package/packages/model-bank/package.json +1 -0
- package/packages/model-bank/src/aiModels/index.ts +3 -0
- package/packages/model-bank/src/aiModels/replicate.ts +90 -0
- package/packages/model-bank/src/const/modelProvider.ts +1 -0
- package/packages/model-runtime/docs/test-coverage.md +5 -5
- package/packages/model-runtime/package.json +2 -1
- package/packages/model-runtime/src/core/ModelRuntime.ts +11 -1
- package/packages/model-runtime/src/providers/replicate/index.ts +424 -0
- package/packages/model-runtime/src/runtimeMap.ts +2 -0
- package/packages/model-runtime/src/utils/modelParse.ts +13 -0
- package/packages/ssrf-safe-fetch/index.browser.ts +22 -2
- package/packages/ssrf-safe-fetch/index.ts +30 -6
- package/packages/types/src/aiProvider.ts +2 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/config/modelProviders/replicate.ts +23 -0
- package/src/server/routers/lambda/__tests__/user.test.ts +2 -0
- package/src/server/routers/lambda/market/index.ts +5 -2
- package/src/server/routers/lambda/user.ts +5 -0
- package/src/services/mcp.ts +1 -0
- package/src/store/test-coverage.md +19 -19
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import Replicate from 'replicate';
|
|
2
|
+
|
|
3
|
+
import { LobeRuntimeAI } from '../../core/BaseAI';
|
|
4
|
+
import {
|
|
5
|
+
type ChatCompletionErrorPayload,
|
|
6
|
+
ChatMethodOptions,
|
|
7
|
+
ChatStreamPayload,
|
|
8
|
+
CreateImagePayload,
|
|
9
|
+
} from '../../types';
|
|
10
|
+
import { AgentRuntimeErrorType } from '../../types/error';
|
|
11
|
+
import { AgentRuntimeError } from '../../utils/createError';
|
|
12
|
+
import { desensitizeUrl } from '../../utils/desensitizeUrl';
|
|
13
|
+
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BASE_URL = 'https://api.replicate.com';
|
|
16
|
+
|
|
17
|
+
interface ReplicateAIParams {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
baseURL?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class LobeReplicateAI implements LobeRuntimeAI {
|
|
24
|
+
private client: Replicate;
|
|
25
|
+
|
|
26
|
+
baseURL: string;
|
|
27
|
+
apiKey?: string;
|
|
28
|
+
private id: string;
|
|
29
|
+
|
|
30
|
+
constructor({ apiKey, baseURL = DEFAULT_BASE_URL, id }: ReplicateAIParams = {}) {
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.client = new Replicate({
|
|
36
|
+
auth: apiKey,
|
|
37
|
+
baseUrl: baseURL !== DEFAULT_BASE_URL ? baseURL : undefined,
|
|
38
|
+
useFileOutput: false, // Return URLs instead of binary data
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.baseURL = baseURL;
|
|
42
|
+
this.apiKey = apiKey;
|
|
43
|
+
this.id = id || 'replicate';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connectivity check for Replicate (non-chat provider)
|
|
48
|
+
* We verify the model exists and stream a minimal SSE "text" event so the checker UI can pass.
|
|
49
|
+
*/
|
|
50
|
+
async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
|
|
51
|
+
const modelId = payload.model;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (!modelId || typeof modelId !== 'string' || !modelId.includes('/')) {
|
|
55
|
+
throw new Error('Invalid model id for Replicate connectivity check');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [owner, ...nameParts] = modelId.split('/');
|
|
59
|
+
const nameWithVersion = nameParts.join('/');
|
|
60
|
+
const [name] = nameWithVersion.split(':'); // drop :version if present
|
|
61
|
+
|
|
62
|
+
// Fast auth + existence check via SDK; no inference cost
|
|
63
|
+
await this.client.models.get(owner, name, { signal: options?.signal });
|
|
64
|
+
|
|
65
|
+
const encoder = new TextEncoder();
|
|
66
|
+
const stream = new ReadableStream({
|
|
67
|
+
start(controller) {
|
|
68
|
+
const textPayload = JSON.stringify(`Replicate connectivity ok for ${modelId}`);
|
|
69
|
+
const stopPayload = JSON.stringify('stop');
|
|
70
|
+
controller.enqueue(encoder.encode(`event: text\ndata: ${textPayload}\n\n`));
|
|
71
|
+
controller.enqueue(encoder.encode(`event: stop\ndata: ${stopPayload}\n\n`));
|
|
72
|
+
controller.close();
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return new Response(stream, {
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'text/event-stream',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw this.handleError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Image generation support for LobeChat async image generation (FLUX, Stable Diffusion, etc.)
|
|
88
|
+
*/
|
|
89
|
+
async createImage(payload: CreateImagePayload) {
|
|
90
|
+
try {
|
|
91
|
+
const { model, params } = payload;
|
|
92
|
+
const { prompt, width, height, cfg, steps, seed, imageUrl, aspectRatio } = params;
|
|
93
|
+
|
|
94
|
+
this.debugLog('[Replicate createImage] === START ===');
|
|
95
|
+
this.debugLog('[Replicate createImage] Model:', model);
|
|
96
|
+
this.debugLog('[Replicate createImage] Params received:', JSON.stringify(params, null, 2));
|
|
97
|
+
|
|
98
|
+
const input: Record<string, any> = {};
|
|
99
|
+
|
|
100
|
+
// Redux models don't use prompt - they only use the input image
|
|
101
|
+
if (!model.includes('redux')) {
|
|
102
|
+
input.prompt = prompt;
|
|
103
|
+
this.debugLog('[Replicate createImage] Added prompt:', prompt);
|
|
104
|
+
} else {
|
|
105
|
+
this.debugLog('[Replicate createImage] Skipping prompt (Redux model)');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle image-to-image models
|
|
109
|
+
if (imageUrl) {
|
|
110
|
+
this.debugLog('[Replicate createImage] imageUrl provided:', imageUrl);
|
|
111
|
+
|
|
112
|
+
// Determine the parameter name based on model type
|
|
113
|
+
let imageParamName: string;
|
|
114
|
+
if (model.includes('redux')) {
|
|
115
|
+
imageParamName = 'redux_image';
|
|
116
|
+
this.debugLog('[Replicate createImage] Will map to redux_image');
|
|
117
|
+
} else if (model.includes('canny') || model.includes('depth')) {
|
|
118
|
+
imageParamName = 'control_image';
|
|
119
|
+
this.debugLog('[Replicate createImage] Will map to control_image');
|
|
120
|
+
} else if (model.includes('fill')) {
|
|
121
|
+
imageParamName = 'image';
|
|
122
|
+
this.debugLog('[Replicate createImage] Will map to image (fill)');
|
|
123
|
+
} else {
|
|
124
|
+
imageParamName = 'image';
|
|
125
|
+
this.debugLog('[Replicate createImage] Will map to image (generic)');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if URL is accessible from internet or local
|
|
129
|
+
// Parse via URL and classify by hostname so it works for any scheme (http, https, etc.)
|
|
130
|
+
let isLocalUrl = false;
|
|
131
|
+
try {
|
|
132
|
+
const parsedUrl = new URL(imageUrl);
|
|
133
|
+
const hostname = parsedUrl.hostname;
|
|
134
|
+
|
|
135
|
+
const isLoopbackHost =
|
|
136
|
+
hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
137
|
+
|
|
138
|
+
const isPrivate10Range = hostname.startsWith('10.');
|
|
139
|
+
const isPrivate192Range = hostname.startsWith('192.168.');
|
|
140
|
+
|
|
141
|
+
// 172.16.0.0 – 172.31.255.255
|
|
142
|
+
const isPrivate172Range = /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
|
|
143
|
+
|
|
144
|
+
const isLocalTld = hostname.endsWith('.local');
|
|
145
|
+
|
|
146
|
+
isLocalUrl =
|
|
147
|
+
isLoopbackHost ||
|
|
148
|
+
isPrivate10Range ||
|
|
149
|
+
isPrivate172Range ||
|
|
150
|
+
isPrivate192Range ||
|
|
151
|
+
isLocalTld;
|
|
152
|
+
} catch {
|
|
153
|
+
// If the URL cannot be parsed as an absolute URL, treat it as local/untrusted
|
|
154
|
+
// to ensure we take the SSRF-safe path.
|
|
155
|
+
isLocalUrl = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isLocalUrl) {
|
|
159
|
+
this.debugLog(
|
|
160
|
+
'[Replicate createImage] Local URL detected, will fetch and upload as data',
|
|
161
|
+
);
|
|
162
|
+
try {
|
|
163
|
+
const { ssrfSafeFetch } = await import('ssrf-safe-fetch');
|
|
164
|
+
const imageResponse = await ssrfSafeFetch(imageUrl);
|
|
165
|
+
if (!imageResponse.ok) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Failed to fetch image: ${imageResponse.status} ${imageResponse.statusText}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get image as buffer
|
|
172
|
+
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
|
|
173
|
+
this.debugLog(
|
|
174
|
+
'[Replicate createImage] Fetched image, size:',
|
|
175
|
+
imageBuffer.length,
|
|
176
|
+
'bytes',
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Check size limit (100MB)
|
|
180
|
+
if (imageBuffer.length > 100 * 1024 * 1024) {
|
|
181
|
+
throw new Error(`Image too large: ${imageBuffer.length} bytes (max 100MB)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Replicate SDK accepts Buffer objects directly
|
|
185
|
+
input[imageParamName] = imageBuffer;
|
|
186
|
+
this.debugLog('[Replicate createImage] Mapped to', imageParamName, 'as Buffer');
|
|
187
|
+
} catch (fetchError: any) {
|
|
188
|
+
this.debugLog('[Replicate createImage] Error fetching local image:', fetchError);
|
|
189
|
+
throw new Error(`Failed to fetch local image: ${fetchError.message}`);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Public URL - use directly
|
|
193
|
+
input[imageParamName] = imageUrl;
|
|
194
|
+
this.debugLog('[Replicate createImage] Public URL, mapped directly to', imageParamName);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
this.debugLog('[Replicate createImage] No imageUrl provided');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Map LobeChat params to Replicate params
|
|
201
|
+
if (width && height) {
|
|
202
|
+
input.width = width;
|
|
203
|
+
input.height = height;
|
|
204
|
+
this.debugLog('[Replicate createImage] Set dimensions:', width, 'x', height);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// For FLUX models, convert to aspect_ratio
|
|
208
|
+
if (model.includes('flux')) {
|
|
209
|
+
// Use explicit aspectRatio if provided (for Redux models)
|
|
210
|
+
if (aspectRatio) {
|
|
211
|
+
input.aspect_ratio = aspectRatio;
|
|
212
|
+
this.debugLog('[Replicate createImage] Set aspect_ratio from param:', aspectRatio);
|
|
213
|
+
} else if (width && height) {
|
|
214
|
+
if (width === height) {
|
|
215
|
+
input.aspect_ratio = '1:1';
|
|
216
|
+
} else if (width === 1280 && height === 720) {
|
|
217
|
+
input.aspect_ratio = '16:9';
|
|
218
|
+
} else if (width === 720 && height === 1280) {
|
|
219
|
+
input.aspect_ratio = '9:16';
|
|
220
|
+
} else if (width > height) {
|
|
221
|
+
input.aspect_ratio = '16:9';
|
|
222
|
+
} else {
|
|
223
|
+
input.aspect_ratio = '9:16';
|
|
224
|
+
}
|
|
225
|
+
this.debugLog('[Replicate createImage] Calculated aspect_ratio:', input.aspect_ratio);
|
|
226
|
+
}
|
|
227
|
+
// Remove width/height for FLUX models (unless it's Fill which needs dimensions)
|
|
228
|
+
if (!model.includes('fill')) {
|
|
229
|
+
delete input.width;
|
|
230
|
+
delete input.height;
|
|
231
|
+
this.debugLog('[Replicate createImage] Removed width/height (using aspect_ratio)');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Add optional parameters
|
|
236
|
+
if (cfg !== undefined) {
|
|
237
|
+
input.guidance_scale = cfg;
|
|
238
|
+
this.debugLog('[Replicate createImage] Set guidance_scale:', cfg);
|
|
239
|
+
}
|
|
240
|
+
if (steps !== undefined) {
|
|
241
|
+
// Redux uses num_inference_steps, control models use steps
|
|
242
|
+
if (model.includes('redux')) {
|
|
243
|
+
input.num_inference_steps = steps;
|
|
244
|
+
this.debugLog('[Replicate createImage] Set num_inference_steps:', steps);
|
|
245
|
+
} else if (model.includes('canny') || model.includes('depth') || model.includes('fill')) {
|
|
246
|
+
input.steps = steps;
|
|
247
|
+
this.debugLog('[Replicate createImage] Set steps:', steps);
|
|
248
|
+
} else {
|
|
249
|
+
input.num_inference_steps = steps;
|
|
250
|
+
this.debugLog('[Replicate createImage] Set num_inference_steps:', steps);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (seed !== undefined && seed !== null) {
|
|
254
|
+
input.seed = seed;
|
|
255
|
+
this.debugLog('[Replicate createImage] Set seed:', seed);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Run prediction - with useFileOutput: false, returns URL strings
|
|
259
|
+
// Log input object without Buffer data (which would be huge)
|
|
260
|
+
const inputForLogging = { ...input };
|
|
261
|
+
for (const key in inputForLogging) {
|
|
262
|
+
if (Buffer.isBuffer(inputForLogging[key])) {
|
|
263
|
+
inputForLogging[key] = `<Buffer ${inputForLogging[key].length} bytes>`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.debugLog(
|
|
267
|
+
'[Replicate createImage] Final input object:',
|
|
268
|
+
JSON.stringify(inputForLogging, null, 2),
|
|
269
|
+
);
|
|
270
|
+
this.debugLog('[Replicate createImage] Calling client.run...');
|
|
271
|
+
|
|
272
|
+
const output = await this.client.run(model as any, { input });
|
|
273
|
+
|
|
274
|
+
this.debugLog('[Replicate createImage] Raw output:', output);
|
|
275
|
+
this.debugLog(
|
|
276
|
+
'[Replicate createImage] Output type:',
|
|
277
|
+
typeof output,
|
|
278
|
+
'Is array:',
|
|
279
|
+
Array.isArray(output),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Extract URL from output
|
|
283
|
+
let outputImageUrl: string;
|
|
284
|
+
|
|
285
|
+
if (Array.isArray(output)) {
|
|
286
|
+
if (output.length === 0) {
|
|
287
|
+
throw new Error('Replicate returned empty array');
|
|
288
|
+
}
|
|
289
|
+
// First item should be the URL string
|
|
290
|
+
outputImageUrl = output[0];
|
|
291
|
+
this.debugLog('[Replicate] Extracted URL from array:', outputImageUrl);
|
|
292
|
+
} else if (typeof output === 'string') {
|
|
293
|
+
outputImageUrl = output;
|
|
294
|
+
this.debugLog('[Replicate] Output is direct string URL:', outputImageUrl);
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(`Unexpected output format from Replicate: ${typeof output}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (typeof outputImageUrl !== 'string') {
|
|
300
|
+
throw new Error(`Expected URL string, got ${typeof outputImageUrl}: ${outputImageUrl}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.debugLog('[Replicate] Final imageUrl:', outputImageUrl);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
height: height,
|
|
307
|
+
imageUrl: outputImageUrl,
|
|
308
|
+
width: width,
|
|
309
|
+
};
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw this.handleError(error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Fetch image generation models from Replicate using search API
|
|
317
|
+
* Uses targeted searches for relevant model categories instead of listing all public models
|
|
318
|
+
*/
|
|
319
|
+
async models() {
|
|
320
|
+
try {
|
|
321
|
+
const modelMap = new Map<string, { created?: number; displayName?: string; id: string }>();
|
|
322
|
+
|
|
323
|
+
// Search queries for different image model categories
|
|
324
|
+
const searchQueries = [
|
|
325
|
+
'flux image generation',
|
|
326
|
+
'stable diffusion',
|
|
327
|
+
'sdxl',
|
|
328
|
+
'ideogram',
|
|
329
|
+
'image to image',
|
|
330
|
+
'text to image',
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
// Search for each category and collect unique models
|
|
334
|
+
for (const query of searchQueries) {
|
|
335
|
+
try {
|
|
336
|
+
// Use paginate for search results (limited results per query)
|
|
337
|
+
for await (const models of this.client.paginate(() => this.client.models.search(query))) {
|
|
338
|
+
for (const model of models) {
|
|
339
|
+
const modelId = `${model.owner}/${model.name}`;
|
|
340
|
+
// Deduplicate by model ID
|
|
341
|
+
if (!modelMap.has(modelId)) {
|
|
342
|
+
modelMap.set(modelId, {
|
|
343
|
+
created: model.latest_version
|
|
344
|
+
? new Date(model.latest_version.created_at).getTime()
|
|
345
|
+
: undefined,
|
|
346
|
+
displayName: model.name,
|
|
347
|
+
id: modelId,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Limit to first page of results per query to avoid too many results
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
// Continue with other searches if one fails
|
|
356
|
+
this.debugLog(`[Replicate models] Search failed for query: ${query}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const modelList = [...modelMap.values()];
|
|
361
|
+
this.debugLog(`[Replicate models] Found ${modelList.length} unique models`);
|
|
362
|
+
|
|
363
|
+
return processModelList(modelList, MODEL_LIST_CONFIGS.replicate, 'replicate');
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw this.handleError(error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Error handling
|
|
371
|
+
*/
|
|
372
|
+
private handleError(error: any): ChatCompletionErrorPayload {
|
|
373
|
+
let desensitizedEndpoint = this.baseURL;
|
|
374
|
+
|
|
375
|
+
if (this.baseURL !== DEFAULT_BASE_URL) {
|
|
376
|
+
desensitizedEndpoint = desensitizeUrl(this.baseURL);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Handle authentication errors
|
|
380
|
+
if (error?.message?.includes('authentication') || error?.message?.includes('API token')) {
|
|
381
|
+
throw AgentRuntimeError.chat({
|
|
382
|
+
endpoint: desensitizedEndpoint,
|
|
383
|
+
error: error as any,
|
|
384
|
+
errorType: AgentRuntimeErrorType.InvalidProviderAPIKey,
|
|
385
|
+
provider: this.id,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Handle model not found
|
|
390
|
+
if (error?.message?.includes('not found')) {
|
|
391
|
+
throw AgentRuntimeError.chat({
|
|
392
|
+
endpoint: desensitizedEndpoint,
|
|
393
|
+
error: error as any,
|
|
394
|
+
errorType: AgentRuntimeErrorType.ModelNotFound,
|
|
395
|
+
provider: this.id,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Generic error
|
|
400
|
+
throw AgentRuntimeError.chat({
|
|
401
|
+
endpoint: desensitizedEndpoint,
|
|
402
|
+
error: error,
|
|
403
|
+
errorType: AgentRuntimeErrorType.ProviderBizError,
|
|
404
|
+
provider: this.id,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Gate verbose logging to avoid noisy output in production
|
|
410
|
+
*/
|
|
411
|
+
private debugLog(...args: any[]) {
|
|
412
|
+
const isReplicateDebug =
|
|
413
|
+
process.env.DEBUG_REPLICATE === '1' ||
|
|
414
|
+
process.env.DEBUG_REPLICATE_CHAT_COMPLETION === '1' ||
|
|
415
|
+
process.env.NODE_ENV !== 'production';
|
|
416
|
+
|
|
417
|
+
if (!isReplicateDebug) return;
|
|
418
|
+
|
|
419
|
+
// eslint-disable-next-line no-console
|
|
420
|
+
console.log(...args);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export default LobeReplicateAI;
|
|
@@ -44,6 +44,7 @@ import { LobePerplexityAI } from './providers/perplexity';
|
|
|
44
44
|
import { LobePPIOAI } from './providers/ppio';
|
|
45
45
|
import { LobeQiniuAI } from './providers/qiniu';
|
|
46
46
|
import { LobeQwenAI } from './providers/qwen';
|
|
47
|
+
import { LobeReplicateAI } from './providers/replicate';
|
|
47
48
|
import { LobeSambaNovaAI } from './providers/sambanova';
|
|
48
49
|
import { LobeSearch1API } from './providers/search1api';
|
|
49
50
|
import { LobeSenseNovaAI } from './providers/sensenova';
|
|
@@ -112,6 +113,7 @@ export const providerRuntimeMap = {
|
|
|
112
113
|
ppio: LobePPIOAI,
|
|
113
114
|
qiniu: LobeQiniuAI,
|
|
114
115
|
qwen: LobeQwenAI,
|
|
116
|
+
replicate: LobeReplicateAI,
|
|
115
117
|
router: LobeNewAPIAI,
|
|
116
118
|
sambanova: LobeSambaNovaAI,
|
|
117
119
|
search1api: LobeSearch1API,
|
|
@@ -88,6 +88,18 @@ export const MODEL_LIST_CONFIGS = {
|
|
|
88
88
|
reasoningKeywords: ['qvq', 'qwq', 'qwen3', '!-instruct-', '!-coder-', '!-max-'],
|
|
89
89
|
visionKeywords: ['qvq', '-vl', '-omni'],
|
|
90
90
|
},
|
|
91
|
+
replicate: {
|
|
92
|
+
imageOutputKeywords: [
|
|
93
|
+
'flux',
|
|
94
|
+
'stable-diffusion',
|
|
95
|
+
'sdxl',
|
|
96
|
+
'ideogram',
|
|
97
|
+
'canny',
|
|
98
|
+
'depth',
|
|
99
|
+
'fill',
|
|
100
|
+
'redux',
|
|
101
|
+
],
|
|
102
|
+
},
|
|
91
103
|
v0: {
|
|
92
104
|
functionCallKeywords: ['v0'],
|
|
93
105
|
reasoningKeywords: ['v0-1.5'],
|
|
@@ -132,6 +144,7 @@ export const MODEL_OWNER_DETECTION_CONFIG = {
|
|
|
132
144
|
moonshot: ['moonshot', 'kimi'],
|
|
133
145
|
openai: ['o1', 'o3', 'o4', 'gpt-'],
|
|
134
146
|
qwen: ['qwen', 'qwq', 'qvq'],
|
|
147
|
+
replicate: [],
|
|
135
148
|
v0: ['v0'],
|
|
136
149
|
volcengine: ['doubao'],
|
|
137
150
|
wenxin: ['ernie', 'qianfan'],
|
|
@@ -4,11 +4,31 @@
|
|
|
4
4
|
* as SSRF attacks are not applicable in client-side code
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Options for per-call SSRF configuration overrides
|
|
9
|
+
* (ignored in browser - kept for API parity with server version)
|
|
10
|
+
*/
|
|
11
|
+
export interface SSRFOptions {
|
|
12
|
+
/** List of IP addresses to allow */
|
|
13
|
+
allowIPAddressList?: string[];
|
|
14
|
+
/** Whether to allow private/local IP addresses */
|
|
15
|
+
allowPrivateIPAddress?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
/**
|
|
8
19
|
* Browser-safe fetch implementation
|
|
9
20
|
* Uses native fetch API in browser environments
|
|
21
|
+
* @param url - The URL to fetch
|
|
22
|
+
* @param options - Standard fetch options
|
|
23
|
+
* @param _ssrfOptions - Ignored in browser (kept for API parity)
|
|
10
24
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
export const ssrfSafeFetch = async (
|
|
26
|
+
url: string,
|
|
27
|
+
// eslint-disable-next-line no-undef
|
|
28
|
+
options?: RequestInit,
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
30
|
+
_ssrfOptions?: SSRFOptions,
|
|
31
|
+
// eslint-disable-next-line no-undef
|
|
32
|
+
): Promise<Response> => {
|
|
13
33
|
return fetch(url, options);
|
|
14
34
|
};
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { RequestFilteringAgentOptions, useAgent as ssrfAgent } from 'request-filtering-agent';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Options for per-call SSRF configuration overrides
|
|
6
|
+
*/
|
|
7
|
+
export interface SSRFOptions {
|
|
8
|
+
/** List of IP addresses to allow */
|
|
9
|
+
allowIPAddressList?: string[];
|
|
10
|
+
/** Whether to allow private/local IP addresses */
|
|
11
|
+
allowPrivateIPAddress?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* SSRF-safe fetch implementation for server-side use
|
|
6
16
|
* Uses request-filtering-agent to prevent requests to private IP addresses
|
|
7
17
|
*
|
|
18
|
+
* @param url - The URL to fetch
|
|
19
|
+
* @param options - Standard fetch options
|
|
20
|
+
* @param ssrfOptions - Optional per-call SSRF configuration overrides
|
|
8
21
|
* @see https://lobehub.com/docs/self-hosting/environment-variables/basic#ssrf-allow-private-ip-address
|
|
9
22
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
export const ssrfSafeFetch = async (
|
|
24
|
+
url: string,
|
|
25
|
+
// eslint-disable-next-line no-undef
|
|
26
|
+
options?: RequestInit,
|
|
27
|
+
ssrfOptions?: SSRFOptions,
|
|
28
|
+
// eslint-disable-next-line no-undef
|
|
29
|
+
): Promise<Response> => {
|
|
12
30
|
try {
|
|
13
|
-
// Configure SSRF protection options
|
|
31
|
+
// Configure SSRF protection options with proper precedence using nullish coalescing
|
|
32
|
+
const envAllowPrivate = process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS === '1';
|
|
33
|
+
const allowPrivate = ssrfOptions?.allowPrivateIPAddress ?? envAllowPrivate;
|
|
34
|
+
|
|
14
35
|
const agentOptions: RequestFilteringAgentOptions = {
|
|
15
|
-
allowIPAddressList:
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
allowIPAddressList:
|
|
37
|
+
ssrfOptions?.allowIPAddressList ??
|
|
38
|
+
process.env.SSRF_ALLOW_IP_ADDRESS_LIST?.split(',').filter(Boolean) ??
|
|
39
|
+
[],
|
|
40
|
+
allowMetaIPAddress: allowPrivate,
|
|
41
|
+
allowPrivateIPAddress: allowPrivate,
|
|
18
42
|
denyIPAddressList: [],
|
|
19
43
|
};
|
|
20
44
|
|
|
@@ -31,6 +31,7 @@ export const AiProviderSDKEnum = {
|
|
|
31
31
|
Ollama: 'ollama',
|
|
32
32
|
Openai: 'openai',
|
|
33
33
|
Qwen: 'qwen',
|
|
34
|
+
Replicate: 'replicate',
|
|
34
35
|
Router: 'router',
|
|
35
36
|
Volcengine: 'volcengine',
|
|
36
37
|
} as const;
|
|
@@ -48,6 +49,7 @@ const AiProviderSdkTypes = [
|
|
|
48
49
|
'cloudflare',
|
|
49
50
|
'google',
|
|
50
51
|
'huggingface',
|
|
52
|
+
'replicate',
|
|
51
53
|
'router',
|
|
52
54
|
'volcengine',
|
|
53
55
|
'qwen',
|
|
@@ -46,6 +46,7 @@ import PerplexityProvider from './perplexity';
|
|
|
46
46
|
import PPIOProvider from './ppio';
|
|
47
47
|
import QiniuProvider from './qiniu';
|
|
48
48
|
import QwenProvider from './qwen';
|
|
49
|
+
import ReplicateProvider from './replicate';
|
|
49
50
|
import SambaNovaProvider from './sambanova';
|
|
50
51
|
import Search1APIProvider from './search1api';
|
|
51
52
|
import SenseNovaProvider from './sensenova';
|
|
@@ -187,6 +188,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
|
|
|
187
188
|
InfiniAIProvider,
|
|
188
189
|
AkashChatProvider,
|
|
189
190
|
QiniuProvider,
|
|
191
|
+
ReplicateProvider,
|
|
190
192
|
NebiusProvider,
|
|
191
193
|
CometAPIProvider,
|
|
192
194
|
VercelAIGatewayProvider,
|
|
@@ -250,6 +252,7 @@ export { default as PerplexityProviderCard } from './perplexity';
|
|
|
250
252
|
export { default as PPIOProviderCard } from './ppio';
|
|
251
253
|
export { default as QiniuProviderCard } from './qiniu';
|
|
252
254
|
export { default as QwenProviderCard } from './qwen';
|
|
255
|
+
export { default as ReplicateProviderCard } from './replicate';
|
|
253
256
|
export { default as SambaNovaProviderCard } from './sambanova';
|
|
254
257
|
export { default as Search1APIProviderCard } from './search1api';
|
|
255
258
|
export { default as SenseNovaProviderCard } from './sensenova';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ModelProviderCard } from '@/types/llm';
|
|
2
|
+
|
|
3
|
+
// Ref: https://replicate.com/docs
|
|
4
|
+
const Replicate: ModelProviderCard = {
|
|
5
|
+
chatModels: [],
|
|
6
|
+
checkModel: 'black-forest-labs/flux-1.1-pro',
|
|
7
|
+
description: 'Replicate 通过简单的云 API 运行 FLUX 和 Stable Diffusion 等开源图像模型。',
|
|
8
|
+
id: 'replicate',
|
|
9
|
+
modelList: { showModelFetcher: true },
|
|
10
|
+
modelsUrl: 'https://replicate.com/explore',
|
|
11
|
+
name: 'Replicate',
|
|
12
|
+
settings: {
|
|
13
|
+
disableBrowserRequest: true,
|
|
14
|
+
proxyUrl: {
|
|
15
|
+
placeholder: 'https://api.replicate.com',
|
|
16
|
+
},
|
|
17
|
+
sdkType: 'replicate',
|
|
18
|
+
showModelFetcher: true,
|
|
19
|
+
},
|
|
20
|
+
url: 'https://replicate.com',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default Replicate;
|
|
@@ -98,6 +98,7 @@ describe('userRouter', () => {
|
|
|
98
98
|
() =>
|
|
99
99
|
({
|
|
100
100
|
getUserState: vi.fn().mockResolvedValue(mockState),
|
|
101
|
+
updateUser: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
|
101
102
|
}) as any,
|
|
102
103
|
);
|
|
103
104
|
|
|
@@ -163,6 +164,7 @@ describe('userRouter', () => {
|
|
|
163
164
|
preference: { telemetry: null },
|
|
164
165
|
settings: {},
|
|
165
166
|
}),
|
|
167
|
+
updateUser: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
|
166
168
|
}) as any,
|
|
167
169
|
);
|
|
168
170
|
|
|
@@ -4,6 +4,7 @@ import { serialize } from 'cookie';
|
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
+
import { ToolCallContent } from '@/libs/mcp';
|
|
7
8
|
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
|
|
8
9
|
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
9
10
|
import { DiscoverService } from '@/server/services/discover';
|
|
@@ -80,13 +81,15 @@ export const marketRouter = router({
|
|
|
80
81
|
toolName: input.toolName,
|
|
81
82
|
userAccessToken,
|
|
82
83
|
});
|
|
84
|
+
const cloudResultContent = (cloudResult?.content ?? []) as ToolCallContent[];
|
|
83
85
|
|
|
84
86
|
// Format the cloud result to MCPToolCallResult format
|
|
85
87
|
// Process content blocks (upload images, etc.)
|
|
86
88
|
const newContent =
|
|
87
89
|
cloudResult?.isError || !ctx.fileService
|
|
88
|
-
?
|
|
89
|
-
:
|
|
90
|
+
? cloudResultContent
|
|
91
|
+
: // FIXME: the type assertion here is a temporary solution, need to remove it after refactoring
|
|
92
|
+
await processContentBlocks(cloudResultContent, ctx.fileService);
|
|
90
93
|
|
|
91
94
|
// Convert content blocks to string
|
|
92
95
|
const content = contentBlocksToString(newContent);
|
|
@@ -55,6 +55,11 @@ export const userRouter = router({
|
|
|
55
55
|
}),
|
|
56
56
|
|
|
57
57
|
getUserState: userProcedure.query(async ({ ctx }): Promise<UserInitializationState> => {
|
|
58
|
+
// don't block following process
|
|
59
|
+
ctx.userModel.updateUser({ lastActiveAt: new Date() }).catch((err) => {
|
|
60
|
+
console.error('update lastActiveAt failed, error:', err);
|
|
61
|
+
});
|
|
62
|
+
|
|
58
63
|
let state: Awaited<ReturnType<UserModel['getUserState']>> | undefined;
|
|
59
64
|
|
|
60
65
|
// get or create first-time user
|
package/src/services/mcp.ts
CHANGED
|
@@ -102,6 +102,7 @@ class MCPService {
|
|
|
102
102
|
// Call cloud gateway via lambda market endpoint
|
|
103
103
|
// Server will automatically get user access token from database
|
|
104
104
|
// and format the result to MCPToolCallResult
|
|
105
|
+
// @ts-ignore tsgo 误报错误
|
|
105
106
|
result = await lambdaClient.market.callCloudMcpEndpoint.mutate({
|
|
106
107
|
apiParams,
|
|
107
108
|
identifier,
|