@lobehub/chat 1.131.0 → 1.131.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +2 -16
  3. package/changelog/v1.json +18 -0
  4. package/locales/ar/models.json +3 -0
  5. package/locales/bg-BG/models.json +3 -0
  6. package/locales/de-DE/models.json +3 -0
  7. package/locales/en-US/models.json +3 -0
  8. package/locales/es-ES/models.json +3 -0
  9. package/locales/fa-IR/models.json +3 -0
  10. package/locales/fr-FR/models.json +3 -0
  11. package/locales/it-IT/models.json +3 -0
  12. package/locales/ja-JP/models.json +3 -0
  13. package/locales/ko-KR/models.json +3 -0
  14. package/locales/nl-NL/models.json +3 -0
  15. package/locales/pl-PL/models.json +3 -0
  16. package/locales/pt-BR/models.json +3 -0
  17. package/locales/ru-RU/models.json +3 -0
  18. package/locales/tr-TR/models.json +3 -0
  19. package/locales/vi-VN/models.json +3 -0
  20. package/locales/zh-CN/models.json +3 -0
  21. package/locales/zh-TW/models.json +3 -0
  22. package/package.json +7 -7
  23. package/packages/model-bank/src/aiModels/vercelaigateway.ts +31 -120
  24. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +68 -34
  25. package/packages/types/src/aiProvider.ts +18 -3
  26. package/packages/utils/src/client/index.ts +1 -0
  27. package/packages/utils/src/fetch/__tests__/fetchSSE.test.ts +3 -3
  28. package/packages/utils/src/fetch/fetchSSE.ts +1 -1
  29. package/src/app/[variants]/(main)/settings/provider/features/CreateNewProvider/index.tsx +9 -8
  30. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/UpdateProviderInfo/SettingModal.tsx +3 -6
  31. package/src/app/[variants]/(main)/settings/provider/features/customProviderSdkOptions.ts +12 -0
  32. package/src/config/modelProviders/vercelaigateway.ts +2 -2
  33. package/src/services/chat/contextEngineering.test.ts +13 -21
  34. package/src/services/chat/contextEngineering.ts +30 -26
  35. package/src/services/chat/index.ts +14 -14
  36. /package/packages/utils/src/{fetch → client}/fetchEventSource/index.ts +0 -0
  37. /package/packages/utils/src/{fetch → client}/fetchEventSource/parse.ts +0 -0
@@ -1,62 +1,96 @@
1
1
  import { createOpenAICompatibleRuntime } from '../../core/openaiCompatibleFactory';
2
- import { AgentRuntimeErrorType, ChatCompletionErrorPayload, ModelProvider } from '../../types';
2
+ import { ModelProvider } from '../../types';
3
3
  import { processMultiProviderModelList } from '../../utils/modelParse';
4
4
 
5
5
  export interface VercelAIGatewayModelCard {
6
+ context_window?: number;
7
+ created?: number | string;
8
+ description?: string;
6
9
  id: string;
10
+ max_tokens?: number;
11
+ name?: string;
12
+ pricing?: {
13
+ input?: string | number;
14
+ output?: string | number;
15
+ };
16
+ tags?: string[];
17
+ type?: string;
7
18
  }
8
19
 
20
+ const formatPrice = (price?: string | number) => {
21
+ if (price === undefined || price === null) return undefined;
22
+ const n = typeof price === 'number' ? price : Number(price);
23
+ if (Number.isNaN(n)) return undefined;
24
+ // Convert per-token price (USD) to per million tokens
25
+ return Number((n * 1e6).toPrecision(5));
26
+ };
27
+
9
28
  export const LobeVercelAIGatewayAI = createOpenAICompatibleRuntime({
10
29
  baseURL: 'https://ai-gateway.vercel.sh/v1',
11
30
  chatCompletion: {
12
- handleError: (error: any): Omit<ChatCompletionErrorPayload, 'provider'> | undefined => {
13
- let errorResponse: Response | undefined;
14
- if (error instanceof Response) {
15
- errorResponse = error;
16
- } else if ('status' in (error as any)) {
17
- errorResponse = error as Response;
31
+ handlePayload: (payload) => {
32
+ const { model, reasoning_effort, verbosity, ...rest } = payload;
33
+
34
+ const providerOptions: any = {};
35
+ if (reasoning_effort) {
36
+ providerOptions.openai = {
37
+ reasoningEffort: reasoning_effort,
38
+ reasoningSummary: 'auto'
39
+ };
18
40
  }
19
- if (errorResponse) {
20
- if (errorResponse.status === 401) {
21
- return {
22
- error: errorResponse.status,
23
- errorType: AgentRuntimeErrorType.InvalidProviderAPIKey,
24
- };
25
- }
26
-
27
- if (errorResponse.status === 403) {
28
- return {
29
- error: errorResponse.status,
30
- errorType: AgentRuntimeErrorType.ProviderBizError,
31
- message:
32
- 'Please check if your API key has sufficient balance or if you have access to the requested model.',
33
- };
34
- }
41
+ if (verbosity) {
42
+ providerOptions.openai.textVerbosity = verbosity;
35
43
  }
36
- return {
37
- error,
38
- };
39
- },
40
- handlePayload: (payload) => {
41
- const { model, ...rest } = payload;
44
+
42
45
  return {
43
46
  ...rest,
44
47
  model,
48
+ providerOptions,
45
49
  } as any;
46
50
  },
47
51
  },
52
+ constructorOptions: {
53
+ defaultHeaders: {
54
+ 'http-referer': 'https://lobehub.com',
55
+ 'x-title': 'LobeHub',
56
+ },
57
+ },
48
58
  debug: {
49
59
  chatCompletion: () => process.env.DEBUG_VERCELAIGATEWAY_CHAT_COMPLETION === '1',
50
60
  },
51
- errorType: {
52
- bizError: AgentRuntimeErrorType.ProviderBizError,
53
- invalidAPIKey: AgentRuntimeErrorType.InvalidProviderAPIKey,
54
- },
55
61
  models: async ({ client }) => {
56
62
  const modelsPage = (await client.models.list()) as any;
57
63
  const modelList: VercelAIGatewayModelCard[] = modelsPage.data;
58
64
 
59
- return processMultiProviderModelList(modelList, 'vercelaigateway');
65
+ const formattedModels = (modelList || []).map((m) => {
66
+ const tags = Array.isArray(m.tags) ? m.tags : [];
67
+
68
+ const inputPrice = formatPrice(m.pricing?.input);
69
+ const outputPrice = formatPrice(m.pricing?.output);
70
+ let displayName = m.name ?? m.id;
71
+ if (inputPrice === 0 && outputPrice === 0) {
72
+ displayName += ' (free)';
73
+ }
74
+
75
+ return {
76
+ contextWindowTokens: m.context_window ?? undefined,
77
+ created: m.created,
78
+ description: m.description ?? '',
79
+ displayName,
80
+ functionCall: tags.includes('tool-use') || false,
81
+ id: m.id,
82
+ maxOutput: typeof m.max_tokens === 'number' ? m.max_tokens : undefined,
83
+ pricing: {
84
+ input: inputPrice,
85
+ output: outputPrice,
86
+ },
87
+ reasoning: tags.includes('reasoning') || false,
88
+ type: m.type === 'embedding' ? 'embedding' : 'chat',
89
+ vision: tags.includes('vision') || false,
90
+ } as any;
91
+ });
92
+
93
+ return await processMultiProviderModelList(formattedModels, 'vercelaigateway');
60
94
  },
61
95
  provider: ModelProvider.VercelAIGateway,
62
96
  });
@@ -38,6 +38,21 @@ export const AiProviderSDKEnum = {
38
38
 
39
39
  export type AiProviderSDKType = (typeof AiProviderSDKEnum)[keyof typeof AiProviderSDKEnum];
40
40
 
41
+ const AiProviderSdkTypes = [
42
+ 'anthropic',
43
+ 'openai',
44
+ 'ollama',
45
+ 'azure',
46
+ 'azureai',
47
+ 'bedrock',
48
+ 'cloudflare',
49
+ 'google',
50
+ 'huggingface',
51
+ 'router',
52
+ 'volcengine',
53
+ 'qwen',
54
+ ] as const satisfies readonly AiProviderSDKType[];
55
+
41
56
  export interface AiProviderSettings {
42
57
  /**
43
58
  * whether provider show browser request option by default
@@ -109,7 +124,7 @@ const AiProviderSettingsSchema = z.object({
109
124
  })
110
125
  .or(ResponseAnimationType)
111
126
  .optional(),
112
- sdkType: z.enum(['anthropic', 'openai', 'ollama']).optional(),
127
+ sdkType: z.enum(AiProviderSdkTypes).optional(),
113
128
  searchMode: z.enum(['params', 'internal']).optional(),
114
129
  showAddNewModel: z.boolean().optional(),
115
130
  showApiKey: z.boolean().optional(),
@@ -131,7 +146,7 @@ export const CreateAiProviderSchema = z.object({
131
146
  keyVaults: z.any().optional(),
132
147
  logo: z.string().optional(),
133
148
  name: z.string(),
134
- sdkType: z.enum(['openai', 'anthropic']).optional(),
149
+ sdkType: z.enum(AiProviderSdkTypes).optional(),
135
150
  settings: AiProviderSettingsSchema.optional(),
136
151
  source: z.enum(['builtin', 'custom']),
137
152
  // checkModel: z.string().optional(),
@@ -213,7 +228,7 @@ export const UpdateAiProviderSchema = z.object({
213
228
  description: z.string().nullable().optional(),
214
229
  logo: z.string().nullable().optional(),
215
230
  name: z.string(),
216
- sdkType: z.enum(['openai', 'anthropic']).optional(),
231
+ sdkType: z.enum(AiProviderSdkTypes).optional(),
217
232
  settings: AiProviderSettingsSchema.optional(),
218
233
  });
219
234
 
@@ -1,4 +1,5 @@
1
1
  export * from './clipboard';
2
2
  export * from './downloadFile';
3
3
  export * from './exportFile';
4
+ export * from './fetchEventSource';
4
5
  export * from './sanitize';
@@ -2,9 +2,9 @@ import { MESSAGE_CANCEL_FLAT } from '@lobechat/const';
2
2
  import { ChatMessageError } from '@lobechat/types';
3
3
  import { afterEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
+ import { FetchEventSourceInit } from '../../client/fetchEventSource';
6
+ import { fetchEventSource } from '../../client/fetchEventSource';
5
7
  import { sleep } from '../../sleep';
6
- import { FetchEventSourceInit } from '../fetchEventSource';
7
- import { fetchEventSource } from '../fetchEventSource';
8
8
  import { fetchSSE } from '../fetchSSE';
9
9
 
10
10
  // 模拟 i18next
@@ -12,7 +12,7 @@ vi.mock('i18next', () => ({
12
12
  t: vi.fn((key) => `translated_${key}`),
13
13
  }));
14
14
 
15
- vi.mock('../fetchEventSource', () => ({
15
+ vi.mock('../../client/fetchEventSource', () => ({
16
16
  fetchEventSource: vi.fn(),
17
17
  }));
18
18
 
@@ -15,8 +15,8 @@ import {
15
15
  ResponseAnimationStyle,
16
16
  } from '@lobechat/types';
17
17
 
18
+ import { fetchEventSource } from '../client/fetchEventSource';
18
19
  import { nanoid } from '../uuid';
19
- import { fetchEventSource } from './fetchEventSource';
20
20
  import { getMessageError } from './parseError';
21
21
 
22
22
  type SSEFinishType = 'done' | 'error' | 'abort';
@@ -18,6 +18,7 @@ import { Flexbox } from 'react-layout-kit';
18
18
  import { useAiInfraStore } from '@/store/aiInfra/store';
19
19
  import { CreateAiProviderParams } from '@/types/aiProvider';
20
20
 
21
+ import { CUSTOM_PROVIDER_SDK_OPTIONS } from '../customProviderSdkOptions';
21
22
  import { KeyVaultsConfigKey, LLMProviderApiTokenKey, LLMProviderBaseUrlKey } from '../../const';
22
23
 
23
24
  interface CreateNewProviderProps {
@@ -35,7 +36,13 @@ const CreateNewProvider = memo<CreateNewProviderProps>(({ onClose, open }) => {
35
36
  setLoading(true);
36
37
 
37
38
  try {
38
- await createNewAiProvider(values);
39
+ // 如果 name 为空,使用 id 作为 name
40
+ const finalValues = {
41
+ ...values,
42
+ name: values.name || values.id,
43
+ };
44
+
45
+ await createNewAiProvider(finalValues);
39
46
  setLoading(false);
40
47
  router.push(`/settings/provider/${values.id}`);
41
48
  message.success(t('createNewAiProvider.createSuccess'));
@@ -70,7 +77,6 @@ const CreateNewProvider = memo<CreateNewProviderProps>(({ onClose, open }) => {
70
77
  label: t('createNewAiProvider.name.title'),
71
78
  minWidth: 400,
72
79
  name: 'name',
73
- rules: [{ message: t('createNewAiProvider.name.required'), required: true }],
74
80
  },
75
81
  {
76
82
  children: (
@@ -102,12 +108,7 @@ const CreateNewProvider = memo<CreateNewProviderProps>(({ onClose, open }) => {
102
108
  {label}
103
109
  </Flexbox>
104
110
  )}
105
- options={[
106
- { label: 'OpenAI', value: 'openai' },
107
- { label: 'Anthropic', value: 'anthropic' },
108
- { label: 'Ollama', value: 'ollama' },
109
- // { label: 'Azure AI', value: 'azureai' },
110
- ]}
111
+ options={CUSTOM_PROVIDER_SDK_OPTIONS}
111
112
  placeholder={t('createNewAiProvider.sdkType.placeholder')}
112
113
  variant={'filled'}
113
114
  />
@@ -10,6 +10,8 @@ import { Flexbox } from 'react-layout-kit';
10
10
  import { useAiInfraStore } from '@/store/aiInfra/store';
11
11
  import { AiProviderDetailItem, UpdateAiProviderParams } from '@/types/aiProvider';
12
12
 
13
+ import { CUSTOM_PROVIDER_SDK_OPTIONS } from '../../customProviderSdkOptions';
14
+
13
15
  interface CreateNewProviderProps {
14
16
  id: string;
15
17
  initialValues: AiProviderDetailItem;
@@ -88,12 +90,7 @@ const CreateNewProvider = memo<CreateNewProviderProps>(({ onClose, open, initial
88
90
  {label}
89
91
  </Flexbox>
90
92
  )}
91
- options={[
92
- { label: 'OpenAI', value: 'openai' },
93
- { label: 'Anthropic', value: 'anthropic' },
94
- { label: 'Ollama', value: 'ollama' },
95
- // { label: 'Azure AI', value: 'azureai' },
96
- ]}
93
+ options={CUSTOM_PROVIDER_SDK_OPTIONS}
97
94
  placeholder={t('createNewAiProvider.sdkType.placeholder')}
98
95
  variant={'filled'}
99
96
  />
@@ -0,0 +1,12 @@
1
+ import type { AiProviderSDKType } from '@/types/aiProvider';
2
+
3
+ export const CUSTOM_PROVIDER_SDK_OPTIONS = [
4
+ { label: 'OpenAI', value: 'openai' },
5
+ { label: 'Azure OpenAI', value: 'azure' },
6
+ { label: 'Anthropic', value: 'anthropic' },
7
+ { label: 'Google', value: 'google' },
8
+ { label: 'Cloudflare', value: 'cloudflare' },
9
+ { label: 'Qwen', value: 'qwen' },
10
+ { label: 'Volcengine', value: 'volcengine' },
11
+ { label: 'Ollama', value: 'ollama' },
12
+ ] satisfies { label: string; value: AiProviderSDKType }[];
@@ -3,15 +3,15 @@ import { ModelProviderCard } from '@/types/llm';
3
3
  const VercelAIGateway: ModelProviderCard = {
4
4
  apiKeyUrl: 'https://vercel.com/dashboard/ai-gateway',
5
5
  chatModels: [],
6
- checkModel: 'openai/gpt-4o-mini',
6
+ checkModel: 'openai/gpt-5-nano',
7
7
  description:
8
8
  'Vercel AI Gateway 提供统一的 API 来访问 100+ 模型,通过单一端点即可使用 OpenAI、Anthropic、Google 等多个提供商的模型。支持预算设置、使用监控、请求负载均衡和故障转移。',
9
- enabled: true,
10
9
  id: 'vercelaigateway',
11
10
  modelList: { showModelFetcher: true },
12
11
  modelsUrl: 'https://vercel.com/ai-gateway/models',
13
12
  name: 'Vercel AI Gateway',
14
13
  settings: {
14
+ disableBrowserRequest: true, // CORS error
15
15
  responseAnimation: 'smooth',
16
16
  showModelFetcher: true,
17
17
  },
@@ -260,17 +260,13 @@ describe('contextEngineering', () => {
260
260
  },
261
261
  ];
262
262
 
263
- const result = await contextEngineering(
264
- {
265
- messages,
266
- model: 'gpt-4',
267
- provider: 'openai',
268
- },
269
- {
270
- isWelcomeQuestion: true,
271
- trace: { sessionId: 'inbox' },
272
- },
273
- );
263
+ const result = await contextEngineering({
264
+ messages,
265
+ model: 'gpt-4',
266
+ provider: 'openai',
267
+ isWelcomeQuestion: true,
268
+ sessionId: 'inbox',
269
+ });
274
270
 
275
271
  // Should have system message with inbox guide content
276
272
  const systemMessage = result.find((msg) => msg.role === 'system');
@@ -295,16 +291,12 @@ describe('contextEngineering', () => {
295
291
  },
296
292
  ];
297
293
 
298
- const result = await contextEngineering(
299
- {
300
- messages,
301
- model: 'gpt-4',
302
- provider: 'openai',
303
- },
304
- {
305
- historySummary,
306
- },
307
- );
294
+ const result = await contextEngineering({
295
+ messages,
296
+ model: 'gpt-4',
297
+ historySummary,
298
+ provider: 'openai',
299
+ });
308
300
 
309
301
  // Should have system message with history summary
310
302
  const systemMessage = result.find((msg) => msg.role === 'system');
@@ -24,30 +24,34 @@ import { VARIABLE_GENERATORS } from '@/utils/client/parserPlaceholder';
24
24
  import { genToolCallingName } from '@/utils/toolCall';
25
25
 
26
26
  import { isCanUseFC, isCanUseVision } from './helper';
27
- import { FetchOptions } from './types';
28
27
 
29
- export const contextEngineering = async (
30
- {
31
- messages = [],
32
- tools,
33
- model,
34
- provider,
35
- systemRole,
36
- inputTemplate,
37
- enableHistoryCount,
38
- historyCount,
39
- }: {
40
- enableHistoryCount?: boolean;
41
- historyCount?: number;
42
- inputTemplate?: string;
43
- messages: ChatMessage[];
44
- model: string;
45
- provider: string;
46
- systemRole?: string;
47
- tools?: string[];
48
- },
49
- options?: FetchOptions,
50
- ): Promise<OpenAIChatMessage[]> => {
28
+ interface ContextEngineeringContext {
29
+ enableHistoryCount?: boolean;
30
+ historyCount?: number;
31
+ historySummary?: string;
32
+ inputTemplate?: string;
33
+ isWelcomeQuestion?: boolean;
34
+ messages: ChatMessage[];
35
+ model: string;
36
+ provider: string;
37
+ sessionId?: string;
38
+ systemRole?: string;
39
+ tools?: string[];
40
+ }
41
+
42
+ export const contextEngineering = async ({
43
+ messages = [],
44
+ tools,
45
+ model,
46
+ provider,
47
+ systemRole,
48
+ inputTemplate,
49
+ enableHistoryCount,
50
+ historyCount,
51
+ historySummary,
52
+ sessionId,
53
+ isWelcomeQuestion,
54
+ }: ContextEngineeringContext): Promise<OpenAIChatMessage[]> => {
51
55
  const pipeline = new ContextEngine({
52
56
  pipeline: [
53
57
  // 1. History truncation (MUST be first, before any message injection)
@@ -62,8 +66,8 @@ export const contextEngineering = async (
62
66
  new InboxGuideProvider({
63
67
  inboxGuideSystemRole: INBOX_GUIDE_SYSTEMROLE,
64
68
  inboxSessionId: INBOX_SESSION_ID,
65
- isWelcomeQuestion: options?.isWelcomeQuestion,
66
- sessionId: options?.trace?.sessionId,
69
+ isWelcomeQuestion: isWelcomeQuestion,
70
+ sessionId: sessionId,
67
71
  }),
68
72
 
69
73
  // 4. Tool system role injection
@@ -78,7 +82,7 @@ export const contextEngineering = async (
78
82
  // 5. History summary injection
79
83
  new HistorySummaryProvider({
80
84
  formatHistorySummary: historySummaryPrompt,
81
- historySummary: options?.historySummary,
85
+ historySummary: historySummary,
82
86
  }),
83
87
 
84
88
  // Create message processing processors
@@ -114,20 +114,20 @@ class ChatService {
114
114
  const agentConfig = agentSelectors.currentAgentConfig(agentStoreState);
115
115
 
116
116
  // Apply context engineering with preprocessing configuration
117
- const oaiMessages = await contextEngineering(
118
- {
119
- enableHistoryCount: agentChatConfigSelectors.enableHistoryCount(agentStoreState),
120
- // include user messages
121
- historyCount: agentChatConfigSelectors.historyCount(agentStoreState) + 2,
122
- inputTemplate: chatConfig.inputTemplate,
123
- messages,
124
- model: payload.model,
125
- provider: payload.provider!,
126
- systemRole: agentConfig.systemRole,
127
- tools: pluginIds,
128
- },
129
- options,
130
- );
117
+ const oaiMessages = await contextEngineering({
118
+ enableHistoryCount: agentChatConfigSelectors.enableHistoryCount(agentStoreState),
119
+ // include user messages
120
+ historyCount: agentChatConfigSelectors.historyCount(agentStoreState) + 2,
121
+ historySummary: options?.historySummary,
122
+ inputTemplate: chatConfig.inputTemplate,
123
+ isWelcomeQuestion: options?.isWelcomeQuestion,
124
+ messages,
125
+ model: payload.model,
126
+ provider: payload.provider!,
127
+ sessionId: options?.trace?.sessionId,
128
+ systemRole: agentConfig.systemRole,
129
+ tools: pluginIds,
130
+ });
131
131
 
132
132
  // ============ 2. preprocess tools ============ //
133
133