@lobehub/chat 1.100.0 → 1.100.1
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/.cursor/rules/testing-guide/testing-guide.mdc +173 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +25 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +25 -0
- package/docs/self-hosting/faq/vercel-ai-image-timeout.mdx +65 -0
- package/docs/self-hosting/faq/vercel-ai-image-timeout.zh-CN.mdx +63 -0
- package/docs/usage/providers/fal.mdx +6 -6
- package/docs/usage/providers/fal.zh-CN.mdx +6 -6
- package/package.json +1 -1
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +1 -1
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +2 -1
- package/src/server/globalConfig/genServerAiProviderConfig.test.ts +235 -0
- package/src/server/globalConfig/genServerAiProviderConfig.ts +9 -10
- package/src/store/aiInfra/slices/aiProvider/action.ts +2 -1
- package/src/utils/getFallbackModelProperty.test.ts +193 -0
- package/src/utils/getFallbackModelProperty.ts +36 -0
- package/src/utils/parseModels.test.ts +150 -48
- package/src/utils/parseModels.ts +26 -11
@@ -0,0 +1,235 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { ModelProvider } from '@/libs/model-runtime';
|
4
|
+
import { AiFullModelCard } from '@/types/aiModel';
|
5
|
+
|
6
|
+
import { genServerAiProvidersConfig } from './genServerAiProviderConfig';
|
7
|
+
|
8
|
+
// Mock dependencies using importOriginal to preserve real provider data
|
9
|
+
vi.mock('@/config/aiModels', async (importOriginal) => {
|
10
|
+
const actual = await importOriginal<typeof import('@/config/aiModels')>();
|
11
|
+
return {
|
12
|
+
...actual,
|
13
|
+
// Keep the original exports but we can override specific ones if needed
|
14
|
+
};
|
15
|
+
});
|
16
|
+
|
17
|
+
vi.mock('@/config/llm', () => ({
|
18
|
+
getLLMConfig: vi.fn(() => ({
|
19
|
+
ENABLED_OPENAI: true,
|
20
|
+
ENABLED_ANTHROPIC: false,
|
21
|
+
ENABLED_AI21: false,
|
22
|
+
})),
|
23
|
+
}));
|
24
|
+
|
25
|
+
vi.mock('@/utils/parseModels', () => ({
|
26
|
+
extractEnabledModels: vi.fn((providerId: string, modelString?: string) => {
|
27
|
+
if (!modelString) return undefined;
|
28
|
+
return [`${providerId}-model-1`, `${providerId}-model-2`];
|
29
|
+
}),
|
30
|
+
transformToAiModelList: vi.fn((params) => {
|
31
|
+
return params.defaultModels;
|
32
|
+
}),
|
33
|
+
}));
|
34
|
+
|
35
|
+
describe('genServerAiProvidersConfig', () => {
|
36
|
+
beforeEach(() => {
|
37
|
+
vi.clearAllMocks();
|
38
|
+
// Clear environment variables
|
39
|
+
Object.keys(process.env).forEach((key) => {
|
40
|
+
if (key.includes('MODEL_LIST')) {
|
41
|
+
delete process.env[key];
|
42
|
+
}
|
43
|
+
});
|
44
|
+
});
|
45
|
+
|
46
|
+
it('should generate basic provider config with default settings', () => {
|
47
|
+
const result = genServerAiProvidersConfig({});
|
48
|
+
|
49
|
+
expect(result).toHaveProperty('openai');
|
50
|
+
expect(result).toHaveProperty('anthropic');
|
51
|
+
|
52
|
+
expect(result.openai).toEqual({
|
53
|
+
enabled: true,
|
54
|
+
enabledModels: undefined,
|
55
|
+
serverModelLists: expect.any(Array),
|
56
|
+
});
|
57
|
+
|
58
|
+
expect(result.anthropic).toEqual({
|
59
|
+
enabled: false,
|
60
|
+
enabledModels: undefined,
|
61
|
+
serverModelLists: expect.any(Array),
|
62
|
+
});
|
63
|
+
});
|
64
|
+
|
65
|
+
it('should use custom enabled settings from specificConfig', () => {
|
66
|
+
const specificConfig = {
|
67
|
+
openai: {
|
68
|
+
enabled: false,
|
69
|
+
},
|
70
|
+
anthropic: {
|
71
|
+
enabled: true,
|
72
|
+
},
|
73
|
+
};
|
74
|
+
|
75
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
76
|
+
|
77
|
+
expect(result.openai.enabled).toBe(false);
|
78
|
+
expect(result.anthropic.enabled).toBe(true);
|
79
|
+
});
|
80
|
+
|
81
|
+
it('should use custom enabledKey from specificConfig', async () => {
|
82
|
+
const specificConfig = {
|
83
|
+
openai: {
|
84
|
+
enabledKey: 'CUSTOM_OPENAI_ENABLED',
|
85
|
+
},
|
86
|
+
};
|
87
|
+
|
88
|
+
// Mock the LLM config to include our custom key
|
89
|
+
const { getLLMConfig } = vi.mocked(await import('@/config/llm'));
|
90
|
+
getLLMConfig.mockReturnValue({
|
91
|
+
ENABLED_OPENAI: true,
|
92
|
+
ENABLED_ANTHROPIC: false,
|
93
|
+
CUSTOM_OPENAI_ENABLED: true,
|
94
|
+
} as any);
|
95
|
+
|
96
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
97
|
+
|
98
|
+
expect(result.openai.enabled).toBe(true);
|
99
|
+
});
|
100
|
+
|
101
|
+
it('should use environment variables for model lists', async () => {
|
102
|
+
process.env.OPENAI_MODEL_LIST = '+gpt-4,+gpt-3.5-turbo';
|
103
|
+
|
104
|
+
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
105
|
+
extractEnabledModels.mockReturnValue(['gpt-4', 'gpt-3.5-turbo']);
|
106
|
+
|
107
|
+
const result = genServerAiProvidersConfig({});
|
108
|
+
|
109
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4,+gpt-3.5-turbo', false);
|
110
|
+
expect(result.openai.enabledModels).toEqual(['gpt-4', 'gpt-3.5-turbo']);
|
111
|
+
});
|
112
|
+
|
113
|
+
it('should use custom modelListKey from specificConfig', async () => {
|
114
|
+
const specificConfig = {
|
115
|
+
openai: {
|
116
|
+
modelListKey: 'CUSTOM_OPENAI_MODELS',
|
117
|
+
},
|
118
|
+
};
|
119
|
+
|
120
|
+
process.env.CUSTOM_OPENAI_MODELS = '+custom-model';
|
121
|
+
|
122
|
+
const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
|
123
|
+
|
124
|
+
genServerAiProvidersConfig(specificConfig);
|
125
|
+
|
126
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+custom-model', false);
|
127
|
+
});
|
128
|
+
|
129
|
+
it('should handle withDeploymentName option', async () => {
|
130
|
+
const specificConfig = {
|
131
|
+
openai: {
|
132
|
+
withDeploymentName: true,
|
133
|
+
},
|
134
|
+
};
|
135
|
+
|
136
|
+
process.env.OPENAI_MODEL_LIST = '+gpt-4->deployment1';
|
137
|
+
|
138
|
+
const { extractEnabledModels, transformToAiModelList } = vi.mocked(
|
139
|
+
await import('@/utils/parseModels'),
|
140
|
+
);
|
141
|
+
|
142
|
+
genServerAiProvidersConfig(specificConfig);
|
143
|
+
|
144
|
+
expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4->deployment1', true);
|
145
|
+
expect(transformToAiModelList).toHaveBeenCalledWith({
|
146
|
+
defaultModels: expect.any(Array),
|
147
|
+
modelString: '+gpt-4->deployment1',
|
148
|
+
providerId: 'openai',
|
149
|
+
withDeploymentName: true,
|
150
|
+
});
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should include fetchOnClient when specified in config', () => {
|
154
|
+
const specificConfig = {
|
155
|
+
openai: {
|
156
|
+
fetchOnClient: true,
|
157
|
+
},
|
158
|
+
};
|
159
|
+
|
160
|
+
const result = genServerAiProvidersConfig(specificConfig);
|
161
|
+
|
162
|
+
expect(result.openai).toHaveProperty('fetchOnClient', true);
|
163
|
+
});
|
164
|
+
|
165
|
+
it('should not include fetchOnClient when not specified in config', () => {
|
166
|
+
const result = genServerAiProvidersConfig({});
|
167
|
+
|
168
|
+
expect(result.openai).not.toHaveProperty('fetchOnClient');
|
169
|
+
});
|
170
|
+
|
171
|
+
it('should handle all available providers', () => {
|
172
|
+
const result = genServerAiProvidersConfig({});
|
173
|
+
|
174
|
+
// Check that result includes some key providers
|
175
|
+
expect(result).toHaveProperty('openai');
|
176
|
+
expect(result).toHaveProperty('anthropic');
|
177
|
+
|
178
|
+
// Check structure for each provider
|
179
|
+
Object.keys(result).forEach((provider) => {
|
180
|
+
expect(result[provider]).toHaveProperty('enabled');
|
181
|
+
expect(result[provider]).toHaveProperty('serverModelLists');
|
182
|
+
// enabled can be boolean or undefined (when no config is provided)
|
183
|
+
expect(['boolean', 'undefined']).toContain(typeof result[provider].enabled);
|
184
|
+
expect(Array.isArray(result[provider].serverModelLists)).toBe(true);
|
185
|
+
});
|
186
|
+
});
|
187
|
+
});
|
188
|
+
|
189
|
+
describe('genServerAiProvidersConfig Error Handling', () => {
|
190
|
+
it('should throw error when a provider is not found in aiModels', async () => {
|
191
|
+
// Reset all mocks to create a clean test environment
|
192
|
+
vi.resetModules();
|
193
|
+
|
194
|
+
// Mock dependencies with a missing provider scenario
|
195
|
+
vi.doMock('@/config/aiModels', () => ({
|
196
|
+
// Explicitly set openai to undefined to simulate missing provider
|
197
|
+
openai: undefined,
|
198
|
+
anthropic: [
|
199
|
+
{
|
200
|
+
id: 'claude-3',
|
201
|
+
displayName: 'Claude 3',
|
202
|
+
type: 'chat',
|
203
|
+
enabled: true,
|
204
|
+
},
|
205
|
+
],
|
206
|
+
}));
|
207
|
+
|
208
|
+
vi.doMock('@/config/llm', () => ({
|
209
|
+
getLLMConfig: vi.fn(() => ({})),
|
210
|
+
}));
|
211
|
+
|
212
|
+
vi.doMock('@/utils/parseModels', () => ({
|
213
|
+
extractEnabledModels: vi.fn(() => undefined),
|
214
|
+
transformToAiModelList: vi.fn(() => []),
|
215
|
+
}));
|
216
|
+
|
217
|
+
// Mock ModelProvider to include the missing provider
|
218
|
+
vi.doMock('@/libs/model-runtime', () => ({
|
219
|
+
ModelProvider: {
|
220
|
+
openai: 'openai', // This exists in enum
|
221
|
+
anthropic: 'anthropic', // This exists in both enum and aiModels
|
222
|
+
},
|
223
|
+
}));
|
224
|
+
|
225
|
+
// Import the function with the new mocks
|
226
|
+
const { genServerAiProvidersConfig } = await import(
|
227
|
+
'./genServerAiProviderConfig?v=' + Date.now()
|
228
|
+
);
|
229
|
+
|
230
|
+
// This should throw because 'openai' is in ModelProvider but not in aiModels
|
231
|
+
expect(() => {
|
232
|
+
genServerAiProvidersConfig({});
|
233
|
+
}).toThrow();
|
234
|
+
});
|
235
|
+
});
|
@@ -3,7 +3,7 @@ import { getLLMConfig } from '@/config/llm';
|
|
3
3
|
import { ModelProvider } from '@/libs/model-runtime';
|
4
4
|
import { AiFullModelCard } from '@/types/aiModel';
|
5
5
|
import { ProviderConfig } from '@/types/user/settings';
|
6
|
-
import { extractEnabledModels,
|
6
|
+
import { extractEnabledModels, transformToAiModelList } from '@/utils/parseModels';
|
7
7
|
|
8
8
|
interface ProviderSpecificConfig {
|
9
9
|
enabled?: boolean;
|
@@ -19,19 +19,17 @@ export const genServerAiProvidersConfig = (specificConfig: Record<any, ProviderS
|
|
19
19
|
return Object.values(ModelProvider).reduce(
|
20
20
|
(config, provider) => {
|
21
21
|
const providerUpperCase = provider.toUpperCase();
|
22
|
-
const
|
22
|
+
const aiModels = AiModels[provider] as AiFullModelCard[];
|
23
23
|
|
24
|
-
if (!
|
24
|
+
if (!aiModels)
|
25
25
|
throw new Error(
|
26
26
|
`Provider [${provider}] not found in aiModels, please make sure you have exported the provider in the \`aiModels/index.ts\``,
|
27
27
|
);
|
28
28
|
|
29
29
|
const providerConfig = specificConfig[provider as keyof typeof specificConfig] || {};
|
30
|
-
const
|
30
|
+
const modelString =
|
31
31
|
process.env[providerConfig.modelListKey ?? `${providerUpperCase}_MODEL_LIST`];
|
32
32
|
|
33
|
-
const defaultChatModels = providerCard.filter((c) => c.type === 'chat');
|
34
|
-
|
35
33
|
config[provider] = {
|
36
34
|
enabled:
|
37
35
|
typeof providerConfig.enabled !== 'undefined'
|
@@ -39,12 +37,13 @@ export const genServerAiProvidersConfig = (specificConfig: Record<any, ProviderS
|
|
39
37
|
: llmConfig[providerConfig.enabledKey || `ENABLED_${providerUpperCase}`],
|
40
38
|
|
41
39
|
enabledModels: extractEnabledModels(
|
42
|
-
|
40
|
+
provider,
|
41
|
+
modelString,
|
43
42
|
providerConfig.withDeploymentName || false,
|
44
43
|
),
|
45
|
-
serverModelLists:
|
46
|
-
|
47
|
-
modelString
|
44
|
+
serverModelLists: transformToAiModelList({
|
45
|
+
defaultModels: aiModels || [],
|
46
|
+
modelString,
|
48
47
|
providerId: provider,
|
49
48
|
withDeploymentName: providerConfig.withDeploymentName || false,
|
50
49
|
}),
|
@@ -19,6 +19,7 @@ import {
|
|
19
19
|
UpdateAiProviderConfigParams,
|
20
20
|
UpdateAiProviderParams,
|
21
21
|
} from '@/types/aiProvider';
|
22
|
+
import { getModelPropertyWithFallback } from '@/utils/getFallbackModelProperty';
|
22
23
|
|
23
24
|
enum AiProviderSwrKey {
|
24
25
|
fetchAiProviderItem = 'FETCH_AI_PROVIDER_ITEM',
|
@@ -216,7 +217,7 @@ export const createAiProviderSlice: StateCreator<
|
|
216
217
|
...(model.type === 'image' && {
|
217
218
|
parameters:
|
218
219
|
(model as AIImageModelCard).parameters ||
|
219
|
-
|
220
|
+
getModelPropertyWithFallback(model.id, 'parameters'),
|
220
221
|
}),
|
221
222
|
}));
|
222
223
|
|
@@ -0,0 +1,193 @@
|
|
1
|
+
import { vi } from 'vitest';
|
2
|
+
|
3
|
+
import { getModelPropertyWithFallback } from './getFallbackModelProperty';
|
4
|
+
|
5
|
+
// Mock LOBE_DEFAULT_MODEL_LIST for testing
|
6
|
+
vi.mock('@/config/aiModels', () => ({
|
7
|
+
LOBE_DEFAULT_MODEL_LIST: [
|
8
|
+
{
|
9
|
+
id: 'gpt-4',
|
10
|
+
providerId: 'openai',
|
11
|
+
type: 'chat',
|
12
|
+
displayName: 'GPT-4',
|
13
|
+
contextWindowTokens: 8192,
|
14
|
+
enabled: true,
|
15
|
+
abilities: {
|
16
|
+
functionCall: true,
|
17
|
+
vision: true,
|
18
|
+
},
|
19
|
+
parameters: {
|
20
|
+
temperature: 0.7,
|
21
|
+
maxTokens: 4096,
|
22
|
+
},
|
23
|
+
},
|
24
|
+
{
|
25
|
+
id: 'gpt-4',
|
26
|
+
providerId: 'azure',
|
27
|
+
type: 'chat',
|
28
|
+
displayName: 'GPT-4 Azure',
|
29
|
+
contextWindowTokens: 8192,
|
30
|
+
enabled: true,
|
31
|
+
abilities: {
|
32
|
+
functionCall: true,
|
33
|
+
},
|
34
|
+
},
|
35
|
+
{
|
36
|
+
id: 'claude-3',
|
37
|
+
providerId: 'anthropic',
|
38
|
+
type: 'chat',
|
39
|
+
displayName: 'Claude 3',
|
40
|
+
contextWindowTokens: 200000,
|
41
|
+
enabled: false,
|
42
|
+
},
|
43
|
+
{
|
44
|
+
id: 'dall-e-3',
|
45
|
+
providerId: 'openai',
|
46
|
+
type: 'image',
|
47
|
+
displayName: 'DALL-E 3',
|
48
|
+
enabled: true,
|
49
|
+
parameters: {
|
50
|
+
size: '1024x1024',
|
51
|
+
quality: 'standard',
|
52
|
+
},
|
53
|
+
},
|
54
|
+
],
|
55
|
+
}));
|
56
|
+
|
57
|
+
describe('getModelPropertyWithFallback', () => {
|
58
|
+
describe('when providerId is specified', () => {
|
59
|
+
it('should return exact match value when model exists with specified provider', () => {
|
60
|
+
const result = getModelPropertyWithFallback('gpt-4', 'displayName', 'openai');
|
61
|
+
expect(result).toBe('GPT-4');
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should return exact match type when model exists with specified provider', () => {
|
65
|
+
const result = getModelPropertyWithFallback('gpt-4', 'type', 'openai');
|
66
|
+
expect(result).toBe('chat');
|
67
|
+
});
|
68
|
+
|
69
|
+
it('should return exact match contextWindowTokens when model exists with specified provider', () => {
|
70
|
+
const result = getModelPropertyWithFallback('gpt-4', 'contextWindowTokens', 'azure');
|
71
|
+
expect(result).toBe(8192);
|
72
|
+
});
|
73
|
+
|
74
|
+
it('should fall back to other provider when exact provider match not found', () => {
|
75
|
+
const result = getModelPropertyWithFallback('gpt-4', 'displayName', 'fake-provider');
|
76
|
+
expect(result).toBe('GPT-4'); // Falls back to openai provider
|
77
|
+
});
|
78
|
+
|
79
|
+
it('should return nested property like abilities', () => {
|
80
|
+
const result = getModelPropertyWithFallback('gpt-4', 'abilities', 'openai');
|
81
|
+
expect(result).toEqual({
|
82
|
+
functionCall: true,
|
83
|
+
vision: true,
|
84
|
+
});
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should return parameters property correctly', () => {
|
88
|
+
const result = getModelPropertyWithFallback('dall-e-3', 'parameters', 'openai');
|
89
|
+
expect(result).toEqual({
|
90
|
+
size: '1024x1024',
|
91
|
+
quality: 'standard',
|
92
|
+
});
|
93
|
+
});
|
94
|
+
});
|
95
|
+
|
96
|
+
describe('when providerId is not specified', () => {
|
97
|
+
it('should return fallback match value when model exists', () => {
|
98
|
+
const result = getModelPropertyWithFallback('claude-3', 'displayName');
|
99
|
+
expect(result).toBe('Claude 3');
|
100
|
+
});
|
101
|
+
|
102
|
+
it('should return fallback match type when model exists', () => {
|
103
|
+
const result = getModelPropertyWithFallback('claude-3', 'type');
|
104
|
+
expect(result).toBe('chat');
|
105
|
+
});
|
106
|
+
|
107
|
+
it('should return fallback match enabled property', () => {
|
108
|
+
const result = getModelPropertyWithFallback('claude-3', 'enabled');
|
109
|
+
expect(result).toBe(false);
|
110
|
+
});
|
111
|
+
});
|
112
|
+
|
113
|
+
describe('when model is not found', () => {
|
114
|
+
it('should return default value "chat" for type property', () => {
|
115
|
+
const result = getModelPropertyWithFallback('non-existent-model', 'type');
|
116
|
+
expect(result).toBe('chat');
|
117
|
+
});
|
118
|
+
|
119
|
+
it('should return default value "chat" for type property even with providerId', () => {
|
120
|
+
const result = getModelPropertyWithFallback('non-existent-model', 'type', 'fake-provider');
|
121
|
+
expect(result).toBe('chat');
|
122
|
+
});
|
123
|
+
|
124
|
+
it('should return undefined for non-type properties when model not found', () => {
|
125
|
+
const result = getModelPropertyWithFallback('non-existent-model', 'displayName');
|
126
|
+
expect(result).toBeUndefined();
|
127
|
+
});
|
128
|
+
|
129
|
+
it('should return undefined for contextWindowTokens when model not found', () => {
|
130
|
+
const result = getModelPropertyWithFallback('non-existent-model', 'contextWindowTokens');
|
131
|
+
expect(result).toBeUndefined();
|
132
|
+
});
|
133
|
+
|
134
|
+
it('should return undefined for enabled property when model not found', () => {
|
135
|
+
const result = getModelPropertyWithFallback('non-existent-model', 'enabled');
|
136
|
+
expect(result).toBeUndefined();
|
137
|
+
});
|
138
|
+
});
|
139
|
+
|
140
|
+
describe('provider precedence logic', () => {
|
141
|
+
it('should prioritize exact provider match over general match', () => {
|
142
|
+
// gpt-4 exists in both openai and azure providers with different displayNames
|
143
|
+
const openaiResult = getModelPropertyWithFallback('gpt-4', 'displayName', 'openai');
|
144
|
+
const azureResult = getModelPropertyWithFallback('gpt-4', 'displayName', 'azure');
|
145
|
+
|
146
|
+
expect(openaiResult).toBe('GPT-4');
|
147
|
+
expect(azureResult).toBe('GPT-4 Azure');
|
148
|
+
});
|
149
|
+
|
150
|
+
it('should fall back to first match when specified provider not found', () => {
|
151
|
+
// When asking for 'fake-provider', should fall back to first match (openai)
|
152
|
+
const result = getModelPropertyWithFallback('gpt-4', 'displayName', 'fake-provider');
|
153
|
+
expect(result).toBe('GPT-4');
|
154
|
+
});
|
155
|
+
});
|
156
|
+
|
157
|
+
describe('property existence handling', () => {
|
158
|
+
it('should handle undefined properties gracefully', () => {
|
159
|
+
// claude-3 doesn't have abilities property defined
|
160
|
+
const result = getModelPropertyWithFallback('claude-3', 'abilities');
|
161
|
+
expect(result).toBeUndefined();
|
162
|
+
});
|
163
|
+
|
164
|
+
it('should handle properties that exist but have falsy values', () => {
|
165
|
+
// claude-3 has enabled: false
|
166
|
+
const result = getModelPropertyWithFallback('claude-3', 'enabled');
|
167
|
+
expect(result).toBe(false);
|
168
|
+
});
|
169
|
+
|
170
|
+
it('should distinguish between undefined and null values', () => {
|
171
|
+
// Testing that we check for undefined specifically, not just falsy values
|
172
|
+
const result = getModelPropertyWithFallback('claude-3', 'contextWindowTokens');
|
173
|
+
expect(result).toBe(200000); // Should find the defined value
|
174
|
+
});
|
175
|
+
});
|
176
|
+
|
177
|
+
describe('edge cases', () => {
|
178
|
+
it('should handle empty string modelId', () => {
|
179
|
+
const result = getModelPropertyWithFallback('', 'type');
|
180
|
+
expect(result).toBe('chat'); // Should fall back to default
|
181
|
+
});
|
182
|
+
|
183
|
+
it('should handle empty string providerId', () => {
|
184
|
+
const result = getModelPropertyWithFallback('gpt-4', 'type', '');
|
185
|
+
expect(result).toBe('chat'); // Should still find the model via fallback
|
186
|
+
});
|
187
|
+
|
188
|
+
it('should handle case-sensitive modelId correctly', () => {
|
189
|
+
const result = getModelPropertyWithFallback('GPT-4', 'type'); // Wrong case
|
190
|
+
expect(result).toBe('chat'); // Should fall back to default since no match
|
191
|
+
});
|
192
|
+
});
|
193
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { LOBE_DEFAULT_MODEL_LIST } from '@/config/aiModels';
|
2
|
+
import { AiFullModelCard } from '@/types/aiModel';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Get the model property value, first from the specified provider, and then from other providers as a fallback.
|
6
|
+
* @param modelId The ID of the model.
|
7
|
+
* @param propertyName The name of the property.
|
8
|
+
* @param providerId Optional provider ID for an exact match.
|
9
|
+
* @returns The property value or a default value.
|
10
|
+
*/
|
11
|
+
export const getModelPropertyWithFallback = <T>(
|
12
|
+
modelId: string,
|
13
|
+
propertyName: keyof AiFullModelCard,
|
14
|
+
providerId?: string,
|
15
|
+
): T => {
|
16
|
+
// Step 1: If providerId is provided, prioritize an exact match (same provider + same id)
|
17
|
+
if (providerId) {
|
18
|
+
const exactMatch = LOBE_DEFAULT_MODEL_LIST.find(
|
19
|
+
(m) => m.id === modelId && m.providerId === providerId,
|
20
|
+
);
|
21
|
+
|
22
|
+
if (exactMatch && exactMatch[propertyName] !== undefined) {
|
23
|
+
return exactMatch[propertyName] as T;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
// Step 2: Fallback to a match ignoring the provider (match id only)
|
28
|
+
const fallbackMatch = LOBE_DEFAULT_MODEL_LIST.find((m) => m.id === modelId);
|
29
|
+
|
30
|
+
if (fallbackMatch && fallbackMatch[propertyName] !== undefined) {
|
31
|
+
return fallbackMatch[propertyName] as T;
|
32
|
+
}
|
33
|
+
|
34
|
+
// Step 3: Return a default value
|
35
|
+
return (propertyName === 'type' ? 'chat' : undefined) as T;
|
36
|
+
};
|