@lobehub/chat 1.100.0 → 1.100.2

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.
@@ -41,10 +41,7 @@ export const getJWKS = (): object => {
41
41
  }
42
42
  };
43
43
 
44
- /**
45
- * 从环境变量中获取 JWKS 并提取第一个 RSA 密钥
46
- */
47
- const getJWKSPublicKey = async () => {
44
+ const getVerificationKey = async () => {
48
45
  try {
49
46
  const jwksString = oidcEnv.OIDC_JWKS_KEY;
50
47
 
@@ -58,20 +55,32 @@ const getJWKSPublicKey = async () => {
58
55
  throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
59
56
  }
60
57
 
61
- // 查找 RS256 算法的 RSA 密钥
62
- const rsaKey = jwks.keys.find((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
63
-
64
- if (!rsaKey) {
58
+ const privateRsaKey = jwks.keys.find((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
59
+ if (!privateRsaKey) {
65
60
  throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
66
61
  }
67
62
 
68
- // 导入 JWK 为公钥
69
- const publicKey = await importJWK(rsaKey, 'RS256');
63
+ // 创建一个只包含公钥组件的“纯净”JWK对象。
64
+ // RSA公钥的关键字段是 kty, n, e。其他如 kid, alg, use 也是公共的。
65
+ const publicKeyJwk = {
66
+ alg: privateRsaKey.alg,
67
+ e: privateRsaKey.e,
68
+ kid: privateRsaKey.kid,
69
+ kty: privateRsaKey.kty,
70
+ n: privateRsaKey.n,
71
+ use: privateRsaKey.use,
72
+ };
73
+
74
+ // 移除任何可能存在的 undefined 字段,保持对象干净
75
+ Object.keys(publicKeyJwk).forEach(
76
+ (key) => (publicKeyJwk as any)[key] === undefined && delete (publicKeyJwk as any)[key],
77
+ );
70
78
 
71
- return publicKey;
79
+ // 现在,无论在哪个环境下,`importJWK` 都会将这个对象正确地识别为一个公钥。
80
+ return await importJWK(publicKeyJwk, 'RS256');
72
81
  } catch (error) {
73
82
  log('获取 JWKS 公钥失败: %O', error);
74
- throw new Error(`JWKS 公钥获取失败: ${(error as Error).message}`);
83
+ throw new Error(`JWKS 公key获取失败: ${(error as Error).message}`);
75
84
  }
76
85
  };
77
86
 
@@ -85,7 +94,7 @@ export const validateOIDCJWT = async (token: string) => {
85
94
  log('开始验证 OIDC JWT token');
86
95
 
87
96
  // 获取公钥
88
- const publicKey = await getJWKSPublicKey();
97
+ const publicKey = await getVerificationKey();
89
98
 
90
99
  // 验证 JWT
91
100
  const { payload } = await jwtVerify(token, publicKey, {
@@ -3,7 +3,13 @@ import debug from 'debug';
3
3
  import { User } from 'next-auth';
4
4
  import { NextRequest } from 'next/server';
5
5
 
6
- import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
6
+ import {
7
+ JWTPayload,
8
+ LOBE_CHAT_AUTH_HEADER,
9
+ LOBE_CHAT_OIDC_AUTH_HEADER,
10
+ enableClerk,
11
+ enableNextAuth,
12
+ } from '@/const/auth';
7
13
  import { oidcEnv } from '@/envs/oidc';
8
14
  import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
9
15
  import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
@@ -102,7 +108,7 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
102
108
  if (oidcEnv.ENABLE_OIDC) {
103
109
  log('OIDC enabled, attempting OIDC authentication');
104
110
  const standardAuthorization = request.headers.get('Authorization');
105
- const oidcAuthToken = request.headers.get('Oidc-Auth');
111
+ const oidcAuthToken = request.headers.get(LOBE_CHAT_OIDC_AUTH_HEADER);
106
112
  log('Standard Authorization header: %s', standardAuthorization ? 'exists' : 'not found');
107
113
  log('Oidc-Auth header: %s', oidcAuthToken ? 'exists' : 'not found');
108
114
 
@@ -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, transformToAiChatModelList } from '@/utils/parseModels';
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 providerCard = AiModels[provider] as AiFullModelCard[];
22
+ const aiModels = AiModels[provider] as AiFullModelCard[];
23
23
 
24
- if (!providerCard)
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 providerModelList =
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
- providerModelList,
40
+ provider,
41
+ modelString,
43
42
  providerConfig.withDeploymentName || false,
44
43
  ),
45
- serverModelLists: transformToAiChatModelList({
46
- defaultChatModels: defaultChatModels || [],
47
- modelString: providerModelList,
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
- LOBE_DEFAULT_MODEL_LIST.find((m) => m.id === model.id)?.parameters,
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
+ };