@lobehub/chat 1.112.5 → 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.
- package/.vscode/settings.json +1 -1
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/package.json +2 -2
- package/packages/const/src/image.ts +9 -1
- package/packages/model-runtime/src/bfl/createImage.test.ts +846 -0
- package/packages/model-runtime/src/bfl/createImage.ts +279 -0
- package/packages/model-runtime/src/bfl/index.test.ts +269 -0
- package/packages/model-runtime/src/bfl/index.ts +49 -0
- package/packages/model-runtime/src/bfl/types.ts +113 -0
- package/packages/model-runtime/src/index.ts +1 -0
- package/packages/model-runtime/src/qwen/createImage.ts +37 -82
- package/packages/model-runtime/src/runtimeMap.ts +2 -0
- package/packages/model-runtime/src/utils/asyncifyPolling.test.ts +491 -0
- package/packages/model-runtime/src/utils/asyncifyPolling.ts +175 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +1 -1
- package/src/config/aiModels/bfl.ts +145 -0
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/llm.ts +6 -1
- package/src/config/modelProviders/bfl.ts +21 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/store/image/slices/generationConfig/hooks.ts +6 -10
@@ -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
|
+
}
|
@@ -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>
|
@@ -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';
|
@@ -77,17 +77,13 @@ export function useDimensionControl() {
|
|
77
77
|
const aspectRatioOptions = useMemo(() => {
|
78
78
|
const modelOptions = paramsSchema?.aspectRatio?.enum || [];
|
79
79
|
|
80
|
-
//
|
81
|
-
|
80
|
+
// 如果 schema 里面有 aspectRatio 并且不为空,直接使用 schema 里面的选项
|
81
|
+
if (modelOptions.length > 0) {
|
82
|
+
return modelOptions;
|
83
|
+
}
|
82
84
|
|
83
|
-
//
|
84
|
-
|
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
|
// 只要不是所有维度相关的控件都不显示,那么这个容器就应该显示
|