@lobehub/chat 1.112.4 → 1.113.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.
@@ -0,0 +1,175 @@
1
+ export interface TaskResult<T> {
2
+ data?: T;
3
+ error?: any;
4
+ status: 'pending' | 'success' | 'failed';
5
+ }
6
+
7
+ export interface PollingErrorContext {
8
+ consecutiveFailures: number;
9
+ error: any;
10
+ retries: number;
11
+ }
12
+
13
+ export interface PollingErrorResult {
14
+ error?: any;
15
+ isContinuePolling: boolean; // If provided, will replace the original error when thrown
16
+ }
17
+
18
+ export interface AsyncifyPollingOptions<T, R> {
19
+ // Default 5000ms
20
+ backoffMultiplier?: number;
21
+
22
+ // Status check function to determine task result
23
+ checkStatus: (result: T) => TaskResult<R>;
24
+
25
+ // Retry configuration
26
+ initialInterval?: number;
27
+ // Optional logger
28
+ logger?: {
29
+ debug?: (...args: any[]) => void;
30
+ error?: (...args: any[]) => void;
31
+ };
32
+ // Default 1.5
33
+ maxConsecutiveFailures?: number;
34
+ // Default 500ms
35
+ maxInterval?: number; // Default 3
36
+ maxRetries?: number; // Default Infinity
37
+
38
+ // Optional custom error handler for polling query failures
39
+ onPollingError?: (context: PollingErrorContext) => PollingErrorResult;
40
+
41
+ // The polling function to execute repeatedly
42
+ pollingQuery: () => Promise<T>;
43
+ }
44
+
45
+ /**
46
+ * Convert polling pattern to async/await pattern
47
+ *
48
+ * @param options Polling configuration options
49
+ * @returns Promise<R> The data returned when task completes
50
+ * @throws Error When task fails or times out
51
+ */
52
+ export async function asyncifyPolling<T, R>(options: AsyncifyPollingOptions<T, R>): Promise<R> {
53
+ const {
54
+ pollingQuery,
55
+ checkStatus,
56
+ initialInterval = 500,
57
+ maxInterval = 5000,
58
+ backoffMultiplier = 1.5,
59
+ maxConsecutiveFailures = 3,
60
+ maxRetries = Infinity,
61
+ onPollingError,
62
+ logger,
63
+ } = options;
64
+
65
+ let retries = 0;
66
+ let consecutiveFailures = 0;
67
+
68
+ while (retries < maxRetries) {
69
+ let pollingResult: T;
70
+
71
+ try {
72
+ // Execute polling function
73
+ pollingResult = await pollingQuery();
74
+
75
+ // Reset consecutive failures counter on successful execution
76
+ consecutiveFailures = 0;
77
+ } catch (error) {
78
+ // Polling function execution failed (network error, etc.)
79
+ consecutiveFailures++;
80
+
81
+ logger?.error?.(
82
+ `Failed to execute polling function (attempt ${retries + 1}/${maxRetries === Infinity ? '∞' : maxRetries}, consecutive failures: ${consecutiveFailures}/${maxConsecutiveFailures}):`,
83
+ error,
84
+ );
85
+
86
+ // Handle custom error processing if provided
87
+ if (onPollingError) {
88
+ const errorResult = onPollingError({
89
+ consecutiveFailures,
90
+ error,
91
+ retries,
92
+ });
93
+
94
+ if (!errorResult.isContinuePolling) {
95
+ // Custom error handler decided to stop polling
96
+ throw errorResult.error || error;
97
+ }
98
+
99
+ // Custom error handler decided to continue polling
100
+ logger?.debug?.('Custom error handler decided to continue polling');
101
+ } else {
102
+ // Default behavior: check if maximum consecutive failures reached
103
+ if (consecutiveFailures >= maxConsecutiveFailures) {
104
+ throw new Error(
105
+ `Failed to execute polling function after ${consecutiveFailures} consecutive attempts: ${error}`,
106
+ );
107
+ }
108
+ }
109
+
110
+ // Wait before retry and continue to next loop iteration
111
+ if (retries < maxRetries - 1) {
112
+ const currentInterval = Math.min(
113
+ initialInterval * Math.pow(backoffMultiplier, retries),
114
+ maxInterval,
115
+ );
116
+
117
+ logger?.debug?.(`Waiting ${currentInterval}ms before next retry`);
118
+
119
+ await new Promise((resolve) => {
120
+ setTimeout(resolve, currentInterval);
121
+ });
122
+ }
123
+
124
+ retries++;
125
+ continue;
126
+ }
127
+
128
+ // Check task status
129
+ const statusResult = checkStatus(pollingResult);
130
+
131
+ logger?.debug?.(`Task status: ${statusResult.status} (attempt ${retries + 1})`);
132
+
133
+ switch (statusResult.status) {
134
+ case 'success': {
135
+ return statusResult.data as R;
136
+ }
137
+
138
+ case 'failed': {
139
+ // Task logic failed, throw error immediately (not counted as consecutive failure)
140
+ throw statusResult.error || new Error('Task failed');
141
+ }
142
+
143
+ case 'pending': {
144
+ // Continue polling
145
+ break;
146
+ }
147
+
148
+ default: {
149
+ // Unknown status, treat as pending
150
+ break;
151
+ }
152
+ }
153
+
154
+ // Wait before next retry if not the last attempt
155
+ if (retries < maxRetries - 1) {
156
+ // Calculate dynamic retry interval with exponential backoff
157
+ const currentInterval = Math.min(
158
+ initialInterval * Math.pow(backoffMultiplier, retries),
159
+ maxInterval,
160
+ );
161
+
162
+ logger?.debug?.(`Waiting ${currentInterval}ms before next retry`);
163
+
164
+ // Wait for retry interval
165
+ await new Promise((resolve) => {
166
+ setTimeout(resolve, currentInterval);
167
+ });
168
+ }
169
+
170
+ retries++;
171
+ }
172
+
173
+ // Maximum retries reached
174
+ throw new Error(`Task timeout after ${maxRetries} attempts`);
175
+ }
@@ -11,8 +11,6 @@ import { Flexbox } from 'react-layout-kit';
11
11
  import Notification from '@/components/Notification';
12
12
  import { BRANDING_NAME } from '@/const/branding';
13
13
  import { PRIVACY_URL } from '@/const/url';
14
- import { useServerConfigStore } from '@/store/serverConfig';
15
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
16
14
  import { useUserStore } from '@/store/user';
17
15
  import { preferenceSelectors } from '@/store/user/selectors';
18
16
 
@@ -30,7 +28,6 @@ const TelemetryNotification = memo<{ mobile?: boolean }>(({ mobile }) => {
30
28
  const { styles, theme } = useStyles();
31
29
 
32
30
  const { t } = useTranslation('common');
33
- const shouldCheck = useServerConfigStore(serverConfigSelectors.enabledTelemetryChat);
34
31
  const isPreferenceInit = useUserStore(preferenceSelectors.isPreferenceInit);
35
32
 
36
33
  const [useCheckTrace, updatePreference] = useUserStore((s) => [
@@ -38,7 +35,7 @@ const TelemetryNotification = memo<{ mobile?: boolean }>(({ mobile }) => {
38
35
  s.updatePreference,
39
36
  ]);
40
37
 
41
- const { data: showModal, mutate } = useCheckTrace(shouldCheck && isPreferenceInit);
38
+ const { data: showModal, mutate } = useCheckTrace(isPreferenceInit);
42
39
 
43
40
  const updateTelemetry = (telemetry: boolean) => {
44
41
  updatePreference({ telemetry });
@@ -46,7 +46,7 @@ const ConfigPanel = memo(() => {
46
46
  const { showDimensionControl } = useDimensionControl();
47
47
 
48
48
  return (
49
- <Flexbox gap={32} padding={12}>
49
+ <Flexbox gap={32} padding={12} style={{ overflow: 'auto' }}>
50
50
  <ConfigItemLayout>
51
51
  <ModelSelect />
52
52
  </ConfigItemLayout>
@@ -10,8 +10,6 @@ import { Flexbox } from 'react-layout-kit';
10
10
 
11
11
  import { BRANDING_EMAIL, BRANDING_NAME, SOCIAL_URL } from '@/const/branding';
12
12
  import { BLOG, OFFICIAL_SITE, PRIVACY_URL, TERMS_URL, mailTo } from '@/const/url';
13
- import { useServerConfigStore } from '@/store/serverConfig';
14
- import { serverConfigSelectors } from '@/store/serverConfig/selectors';
15
13
 
16
14
  import AboutList from './features/AboutList';
17
15
  import Analytics from './features/Analytics';
@@ -30,7 +28,6 @@ const useStyles = createStyles(({ css, token }) => ({
30
28
  const Page = memo<{ mobile?: boolean }>(({ mobile }) => {
31
29
  const { t } = useTranslation('common');
32
30
  const { styles } = useStyles();
33
- const enabledTelemetryChat = useServerConfigStore(serverConfigSelectors.enabledTelemetryChat);
34
31
 
35
32
  return (
36
33
  <>
@@ -122,7 +119,7 @@ const Page = memo<{ mobile?: boolean }>(({ mobile }) => {
122
119
  />
123
120
  </Flexbox>
124
121
  </Form.Group>
125
- {enabledTelemetryChat && <Analytics />}
122
+ <Analytics />
126
123
  </>
127
124
  );
128
125
  });
@@ -0,0 +1,145 @@
1
+ import { PRESET_ASPECT_RATIOS } from '@/const/image';
2
+ import { ModelParamsSchema } from '@/libs/standard-parameters';
3
+ import { AIImageModelCard } from '@/types/aiModel';
4
+
5
+ // https://docs.bfl.ai/api-reference/tasks/edit-or-create-an-image-with-flux-kontext-pro
6
+ // official support 21:9 ~ 9:21 (ratio 0.43 ~ 2.33)
7
+ const calculateRatio = (aspectRatio: string): number => {
8
+ const [width, height] = aspectRatio.split(':').map(Number);
9
+ return width / height;
10
+ };
11
+
12
+ const defaultAspectRatios = PRESET_ASPECT_RATIOS.filter((ratio) => {
13
+ const value = calculateRatio(ratio);
14
+ // BFL API supports ratio range: 21:9 ~ 9:21 (approximately 0.43 ~ 2.33)
15
+ // Use a small tolerance for floating point comparison
16
+ return value >= 9 / 21 - 0.001 && value <= 21 / 9 + 0.001;
17
+ });
18
+
19
+ const fluxKontextSeriesParamsSchema: ModelParamsSchema = {
20
+ aspectRatio: {
21
+ default: '1:1',
22
+ enum: defaultAspectRatios,
23
+ },
24
+ imageUrls: {
25
+ default: [],
26
+ },
27
+ prompt: { default: '' },
28
+ seed: { default: null },
29
+ };
30
+
31
+ const imageModels: AIImageModelCard[] = [
32
+ // https://docs.bfl.ai/api-reference/tasks/edit-or-create-an-image-with-flux-kontext-pro
33
+ {
34
+ description: '最先进的上下文图像生成和编辑——结合文本和图像以获得精确、连贯的结果。',
35
+ displayName: 'FLUX.1 Kontext [pro]',
36
+ enabled: true,
37
+ id: 'flux-kontext-pro',
38
+ parameters: fluxKontextSeriesParamsSchema,
39
+ // check: https://bfl.ai/pricing
40
+ pricing: {
41
+ units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
42
+ },
43
+ releasedAt: '2025-05-29',
44
+ type: 'image',
45
+ },
46
+ // https://docs.bfl.ai/api-reference/tasks/edit-or-create-an-image-with-flux-kontext-max
47
+ {
48
+ description: '最先进的上下文图像生成和编辑——结合文本和图像以获得精确、连贯的结果。',
49
+ displayName: 'FLUX.1 Kontext [max]',
50
+ enabled: true,
51
+ id: 'flux-kontext-max',
52
+ parameters: fluxKontextSeriesParamsSchema,
53
+ pricing: {
54
+ units: [{ name: 'imageGeneration', rate: 0.08, strategy: 'fixed', unit: 'image' }],
55
+ },
56
+ releasedAt: '2025-05-29',
57
+ type: 'image',
58
+ },
59
+ // https://docs.bfl.ai/api-reference/tasks/generate-an-image-with-flux-11-[pro]
60
+ {
61
+ description: '升级版专业级AI图像生成模型——提供卓越的图像质量和精确的提示词遵循能力。',
62
+ displayName: 'FLUX1.1 [pro] ',
63
+ enabled: true,
64
+ id: 'flux-pro-1.1',
65
+ parameters: {
66
+ height: { default: 768, max: 1440, min: 256, step: 32 },
67
+ imageUrl: { default: null },
68
+ prompt: { default: '' },
69
+ seed: { default: null },
70
+ width: { default: 1024, max: 1440, min: 256, step: 32 },
71
+ },
72
+ pricing: {
73
+ units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
74
+ },
75
+ releasedAt: '2024-10-02',
76
+ type: 'image',
77
+ },
78
+ // https://docs.bfl.ai/api-reference/tasks/generate-an-image-with-flux-11-[pro]-with-ultra-mode-and-optional-raw-mode
79
+ {
80
+ description: '超高分辨率AI图像生成——支持4兆像素输出,10秒内生成超清图像。',
81
+ displayName: 'FLUX1.1 [pro] Ultra',
82
+ enabled: true,
83
+ id: 'flux-pro-1.1-ultra',
84
+ parameters: {
85
+ aspectRatio: {
86
+ default: '16:9',
87
+ enum: defaultAspectRatios,
88
+ },
89
+ imageUrl: { default: null },
90
+ prompt: { default: '' },
91
+ seed: { default: null },
92
+ },
93
+ pricing: {
94
+ units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
95
+ },
96
+ releasedAt: '2024-11-06',
97
+ type: 'image',
98
+ },
99
+ // https://docs.bfl.ai/api-reference/tasks/generate-an-image-with-flux1-[pro]
100
+ {
101
+ description: '顶级商用AI图像生成模型——无与伦比的图像质量和多样化输出表现。',
102
+ displayName: 'FLUX.1 [pro]',
103
+ enabled: true,
104
+ id: 'flux-pro',
105
+ parameters: {
106
+ cfg: { default: 2.5, max: 5, min: 1.5, step: 0.1 },
107
+ height: { default: 768, max: 1440, min: 256, step: 32 },
108
+ imageUrl: { default: null },
109
+ prompt: { default: '' },
110
+ seed: { default: null },
111
+ steps: { default: 40, max: 50, min: 1 },
112
+ width: { default: 1024, max: 1440, min: 256, step: 32 },
113
+ },
114
+ pricing: {
115
+ units: [{ name: 'imageGeneration', rate: 0.025, strategy: 'fixed', unit: 'image' }],
116
+ },
117
+ releasedAt: '2024-08-01',
118
+ type: 'image',
119
+ },
120
+ // https://docs.bfl.ai/api-reference/tasks/generate-an-image-with-flux1-[dev]
121
+ {
122
+ description: '开源研发版AI图像生成模型——高效优化,适合非商业用途的创新研究。',
123
+ displayName: 'FLUX.1 [dev]',
124
+ enabled: true,
125
+ id: 'flux-dev',
126
+ parameters: {
127
+ cfg: { default: 3, max: 5, min: 1.5, step: 0.1 },
128
+ height: { default: 768, max: 1440, min: 256, step: 32 },
129
+ imageUrl: { default: null },
130
+ prompt: { default: '' },
131
+ seed: { default: null },
132
+ steps: { default: 28, max: 50, min: 1 },
133
+ width: { default: 1024, max: 1440, min: 256, step: 32 },
134
+ },
135
+ pricing: {
136
+ units: [{ name: 'imageGeneration', rate: 0.025, strategy: 'fixed', unit: 'image' }],
137
+ },
138
+ releasedAt: '2024-08-01',
139
+ type: 'image',
140
+ },
141
+ ];
142
+
143
+ export const allModels = [...imageModels];
144
+
145
+ export default allModels;
@@ -9,6 +9,7 @@ import { default as azure } from './azure';
9
9
  import { default as azureai } from './azureai';
10
10
  import { default as baichuan } from './baichuan';
11
11
  import { default as bedrock } from './bedrock';
12
+ import { default as bfl } from './bfl';
12
13
  import { default as cloudflare } from './cloudflare';
13
14
  import { default as cohere } from './cohere';
14
15
  import { default as deepseek } from './deepseek';
@@ -87,6 +88,7 @@ export const LOBE_DEFAULT_MODEL_LIST = buildDefaultModelList({
87
88
  azureai,
88
89
  baichuan,
89
90
  bedrock,
91
+ bfl,
90
92
  cloudflare,
91
93
  cohere,
92
94
  deepseek,
@@ -146,6 +148,7 @@ export { default as azure } from './azure';
146
148
  export { default as azureai } from './azureai';
147
149
  export { default as baichuan } from './baichuan';
148
150
  export { default as bedrock } from './bedrock';
151
+ export { default as bfl } from './bfl';
149
152
  export { default as cloudflare } from './cloudflare';
150
153
  export { default as cohere } from './cohere';
151
154
  export { default as deepseek } from './deepseek';
package/src/config/llm.ts CHANGED
@@ -166,13 +166,15 @@ export const getLLMConfig = () => {
166
166
  ENABLED_FAL: z.boolean(),
167
167
  FAL_API_KEY: z.string().optional(),
168
168
 
169
+ ENABLED_BFL: z.boolean(),
170
+ BFL_API_KEY: z.string().optional(),
171
+
169
172
  ENABLED_MODELSCOPE: z.boolean(),
170
173
  MODELSCOPE_API_KEY: z.string().optional(),
171
174
 
172
175
  ENABLED_V0: z.boolean(),
173
176
  V0_API_KEY: z.string().optional(),
174
177
 
175
-
176
178
  ENABLED_AI302: z.boolean(),
177
179
  AI302_API_KEY: z.string().optional(),
178
180
 
@@ -342,6 +344,9 @@ export const getLLMConfig = () => {
342
344
  ENABLED_FAL: process.env.ENABLED_FAL !== '0',
343
345
  FAL_API_KEY: process.env.FAL_API_KEY,
344
346
 
347
+ ENABLED_BFL: !!process.env.BFL_API_KEY,
348
+ BFL_API_KEY: process.env.BFL_API_KEY,
349
+
345
350
  ENABLED_MODELSCOPE: !!process.env.MODELSCOPE_API_KEY,
346
351
  MODELSCOPE_API_KEY: process.env.MODELSCOPE_API_KEY,
347
352
 
@@ -0,0 +1,21 @@
1
+ import { ModelProviderCard } from '@/types/llm';
2
+
3
+ /**
4
+ * @see https://docs.bfl.ai/
5
+ */
6
+ const Bfl: ModelProviderCard = {
7
+ chatModels: [],
8
+ description: '领先的前沿人工智能研究实验室,构建明日的视觉基础设施。',
9
+ enabled: true,
10
+ id: 'bfl',
11
+ name: 'Black Forest Labs',
12
+ settings: {
13
+ disableBrowserRequest: true,
14
+ showAddNewModel: false,
15
+ showChecker: false,
16
+ showModelFetcher: false,
17
+ },
18
+ url: 'https://bfl.ai/',
19
+ };
20
+
21
+ export default Bfl;
@@ -9,6 +9,7 @@ import AzureProvider from './azure';
9
9
  import AzureAIProvider from './azureai';
10
10
  import BaichuanProvider from './baichuan';
11
11
  import BedrockProvider from './bedrock';
12
+ import BflProvider from './bfl';
12
13
  import CloudflareProvider from './cloudflare';
13
14
  import CohereProvider from './cohere';
14
15
  import DeepSeekProvider from './deepseek';
@@ -132,6 +133,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
132
133
  HuggingFaceProvider,
133
134
  CloudflareProvider,
134
135
  GithubProvider,
136
+ BflProvider,
135
137
  NovitaProvider,
136
138
  PPIOProvider,
137
139
  NvidiaProvider,
@@ -191,6 +193,7 @@ export { default as AzureProviderCard } from './azure';
191
193
  export { default as AzureAIProviderCard } from './azureai';
192
194
  export { default as BaichuanProviderCard } from './baichuan';
193
195
  export { default as BedrockProviderCard } from './bedrock';
196
+ export { default as BflProviderCard } from './bfl';
194
197
  export { default as CloudflareProviderCard } from './cloudflare';
195
198
  export { default as CohereProviderCard } from './cohere';
196
199
  export { default as DeepSeekProviderCard } from './deepseek';
@@ -540,17 +540,14 @@ export const marketRouter = router({
540
540
  z.object({
541
541
  callDurationMs: z.number(),
542
542
  clientId: z.string().optional(),
543
- clientIp: z.string().optional(),
544
543
  customPluginInfo: z.any().optional(),
545
544
  errorCode: z.string().optional(),
546
545
  errorMessage: z.string().optional(),
547
546
  identifier: z.string(),
548
- inputParams: z.any().optional(),
549
547
  isCustomPlugin: z.boolean().optional(),
550
548
  metadata: z.record(z.any()).optional(),
551
549
  methodName: z.string(),
552
550
  methodType: z.enum(['tool', 'prompt', 'resource']),
553
- outputResult: z.any().optional(),
554
551
  platform: z.string().optional(),
555
552
  requestSizeBytes: z.number().optional(),
556
553
  responseSizeBytes: z.number().optional(),
@@ -3,6 +3,8 @@ import { CallReportRequest, InstallReportRequest } from '@lobehub/market-types';
3
3
 
4
4
  import { lambdaClient } from '@/libs/trpc/client';
5
5
  import { globalHelpers } from '@/store/global/helpers';
6
+ import { useUserStore } from '@/store/user';
7
+ import { preferenceSelectors } from '@/store/user/selectors';
6
8
  import {
7
9
  AssistantListResponse,
8
10
  AssistantQueryParams,
@@ -162,6 +164,10 @@ class DiscoverService {
162
164
  * 上报插件调用结果
163
165
  */
164
166
  reportPluginCall = async (reportData: CallReportRequest) => {
167
+ const allow = useUserStore(preferenceSelectors.userAllowTrace);
168
+
169
+ if (!allow) return;
170
+
165
171
  await this.injectMPToken();
166
172
 
167
173
  lambdaClient.market.reportCall.mutate(cleanObject(reportData)).catch((reportError) => {
@@ -103,7 +103,6 @@ class MCPService {
103
103
  errorCode,
104
104
  errorMessage,
105
105
  identifier,
106
- inputParams,
107
106
  isCustomPlugin,
108
107
  metadata: {
109
108
  appVersion: CURRENT_VERSION,
@@ -112,7 +111,6 @@ class MCPService {
112
111
  },
113
112
  methodName: apiName,
114
113
  methodType: 'tool' as const,
115
- outputResult: success ? result : undefined,
116
114
  requestSizeBytes,
117
115
  responseSizeBytes,
118
116
  sessionId: topicId,
@@ -77,17 +77,13 @@ export function useDimensionControl() {
77
77
  const aspectRatioOptions = useMemo(() => {
78
78
  const modelOptions = paramsSchema?.aspectRatio?.enum || [];
79
79
 
80
- // 合并选项,优先使用预设选项,然后添加模型特有的选项
81
- const allOptions = [...PRESET_ASPECT_RATIOS];
80
+ // 如果 schema 里面有 aspectRatio 并且不为空,直接使用 schema 里面的选项
81
+ if (modelOptions.length > 0) {
82
+ return modelOptions;
83
+ }
82
84
 
83
- // 添加模型选项中不在预设中的选项
84
- modelOptions.forEach((option) => {
85
- if (!allOptions.includes(option)) {
86
- allOptions.push(option);
87
- }
88
- });
89
-
90
- return allOptions;
85
+ // 否则使用预设选项
86
+ return PRESET_ASPECT_RATIOS;
91
87
  }, [paramsSchema]);
92
88
 
93
89
  // 只要不是所有维度相关的控件都不显示,那么这个容器就应该显示