@lobehub/chat 1.92.3 → 1.93.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/changelog/v1.json +9 -0
  5. package/docs/development/database-schema.dbml +51 -1
  6. package/locales/ar/modelProvider.json +4 -0
  7. package/locales/ar/models.json +64 -34
  8. package/locales/ar/providers.json +3 -0
  9. package/locales/bg-BG/modelProvider.json +4 -0
  10. package/locales/bg-BG/models.json +64 -34
  11. package/locales/bg-BG/providers.json +3 -0
  12. package/locales/de-DE/modelProvider.json +4 -0
  13. package/locales/de-DE/models.json +64 -34
  14. package/locales/de-DE/providers.json +3 -0
  15. package/locales/en-US/modelProvider.json +4 -0
  16. package/locales/en-US/models.json +64 -34
  17. package/locales/en-US/providers.json +3 -0
  18. package/locales/es-ES/modelProvider.json +4 -0
  19. package/locales/es-ES/models.json +64 -34
  20. package/locales/es-ES/providers.json +3 -0
  21. package/locales/fa-IR/modelProvider.json +4 -0
  22. package/locales/fa-IR/models.json +64 -34
  23. package/locales/fa-IR/providers.json +3 -0
  24. package/locales/fr-FR/modelProvider.json +4 -0
  25. package/locales/fr-FR/models.json +64 -34
  26. package/locales/fr-FR/providers.json +3 -0
  27. package/locales/it-IT/modelProvider.json +4 -0
  28. package/locales/it-IT/models.json +64 -34
  29. package/locales/it-IT/providers.json +3 -0
  30. package/locales/ja-JP/modelProvider.json +4 -0
  31. package/locales/ja-JP/models.json +64 -34
  32. package/locales/ja-JP/providers.json +3 -0
  33. package/locales/ko-KR/modelProvider.json +4 -0
  34. package/locales/ko-KR/models.json +64 -34
  35. package/locales/ko-KR/providers.json +3 -0
  36. package/locales/nl-NL/modelProvider.json +4 -0
  37. package/locales/nl-NL/models.json +64 -34
  38. package/locales/nl-NL/providers.json +3 -0
  39. package/locales/pl-PL/modelProvider.json +4 -0
  40. package/locales/pl-PL/models.json +64 -34
  41. package/locales/pl-PL/providers.json +3 -0
  42. package/locales/pt-BR/modelProvider.json +4 -0
  43. package/locales/pt-BR/models.json +64 -34
  44. package/locales/pt-BR/providers.json +3 -0
  45. package/locales/ru-RU/modelProvider.json +4 -0
  46. package/locales/ru-RU/models.json +63 -33
  47. package/locales/ru-RU/providers.json +3 -0
  48. package/locales/tr-TR/modelProvider.json +4 -0
  49. package/locales/tr-TR/models.json +64 -34
  50. package/locales/tr-TR/providers.json +3 -0
  51. package/locales/vi-VN/modelProvider.json +4 -0
  52. package/locales/vi-VN/models.json +64 -34
  53. package/locales/vi-VN/providers.json +3 -0
  54. package/locales/zh-CN/modelProvider.json +4 -0
  55. package/locales/zh-CN/models.json +59 -29
  56. package/locales/zh-CN/providers.json +3 -0
  57. package/locales/zh-TW/modelProvider.json +4 -0
  58. package/locales/zh-TW/models.json +64 -34
  59. package/locales/zh-TW/providers.json +3 -0
  60. package/package.json +1 -1
  61. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +16 -0
  62. package/src/config/modelProviders/openai.ts +3 -1
  63. package/src/database/client/migrations.json +25 -0
  64. package/src/database/migrations/0025_add_provider_config.sql +1 -0
  65. package/src/database/migrations/meta/0025_snapshot.json +5703 -0
  66. package/src/database/migrations/meta/_journal.json +7 -0
  67. package/src/database/models/__tests__/aiProvider.test.ts +2 -0
  68. package/src/database/models/aiProvider.ts +5 -2
  69. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  70. package/src/database/schemas/_helpers.ts +5 -1
  71. package/src/database/schemas/aiInfra.ts +5 -1
  72. package/src/libs/model-runtime/openai/index.ts +21 -2
  73. package/src/libs/model-runtime/types/chat.ts +6 -9
  74. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +79 -5
  75. package/src/libs/model-runtime/utils/openaiHelpers.test.ts +145 -1
  76. package/src/libs/model-runtime/utils/openaiHelpers.ts +59 -0
  77. package/src/libs/model-runtime/utils/streams/openai/__snapshots__/responsesStream.test.ts.snap +193 -0
  78. package/src/libs/model-runtime/utils/streams/openai/index.ts +2 -0
  79. package/src/libs/model-runtime/utils/streams/{openai.test.ts → openai/openai.test.ts} +1 -1
  80. package/src/libs/model-runtime/utils/streams/{openai.ts → openai/openai.ts} +5 -5
  81. package/src/libs/model-runtime/utils/streams/openai/responsesStream.test.ts +826 -0
  82. package/src/libs/model-runtime/utils/streams/openai/responsesStream.ts +166 -0
  83. package/src/libs/model-runtime/utils/streams/protocol.ts +4 -1
  84. package/src/libs/model-runtime/utils/streams/utils.ts +20 -0
  85. package/src/libs/model-runtime/utils/usageConverter.ts +59 -0
  86. package/src/locales/default/modelProvider.ts +4 -0
  87. package/src/services/__tests__/chat.test.ts +27 -0
  88. package/src/services/chat.ts +8 -2
  89. package/src/store/aiInfra/slices/aiProvider/selectors.ts +11 -0
  90. package/src/types/aiProvider.ts +13 -1
@@ -0,0 +1,166 @@
1
+ import OpenAI from 'openai';
2
+ import type { Stream } from 'openai/streaming';
3
+
4
+ import { ChatMessageError } from '@/types/message';
5
+
6
+ import { AgentRuntimeErrorType } from '../../../error';
7
+ import { convertResponseUsage } from '../../usageConverter';
8
+ import {
9
+ FIRST_CHUNK_ERROR_KEY,
10
+ StreamContext,
11
+ StreamProtocolChunk,
12
+ StreamProtocolToolCallChunk,
13
+ StreamToolCallChunkData,
14
+ convertIterableToStream,
15
+ createCallbacksTransformer,
16
+ createFirstErrorHandleTransformer,
17
+ createSSEProtocolTransformer,
18
+ createTokenSpeedCalculator,
19
+ } from '../protocol';
20
+ import { OpenAIStreamOptions } from './openai';
21
+
22
+ const transformOpenAIStream = (
23
+ chunk: OpenAI.Responses.ResponseStreamEvent,
24
+ streamContext: StreamContext,
25
+ ): StreamProtocolChunk | StreamProtocolChunk[] => {
26
+ // handle the first chunk error
27
+ if (FIRST_CHUNK_ERROR_KEY in chunk) {
28
+ delete chunk[FIRST_CHUNK_ERROR_KEY];
29
+ // @ts-ignore
30
+ delete chunk['name'];
31
+ // @ts-ignore
32
+ delete chunk['stack'];
33
+
34
+ const errorData = {
35
+ body: chunk,
36
+ type: 'errorType' in chunk ? chunk.errorType : AgentRuntimeErrorType.ProviderBizError,
37
+ } as ChatMessageError;
38
+ return { data: errorData, id: 'first_chunk_error', type: 'error' };
39
+ }
40
+
41
+ try {
42
+ switch (chunk.type) {
43
+ case 'response.created': {
44
+ streamContext.id = chunk.response.id;
45
+
46
+ return { data: chunk.response.status, id: streamContext.id, type: 'data' };
47
+ }
48
+
49
+ case 'response.output_item.added': {
50
+ switch (chunk.item.type) {
51
+ case 'function_call': {
52
+ streamContext.toolIndex =
53
+ typeof streamContext.toolIndex === 'undefined' ? 0 : streamContext.toolIndex + 1;
54
+ streamContext.tool = {
55
+ id: chunk.item.call_id,
56
+ index: streamContext.toolIndex,
57
+ name: chunk.item.name,
58
+ };
59
+
60
+ return {
61
+ data: [
62
+ {
63
+ function: { arguments: chunk.item.arguments, name: chunk.item.name },
64
+ id: chunk.item.call_id,
65
+ index: streamContext.toolIndex!,
66
+ type: 'function',
67
+ } satisfies StreamToolCallChunkData,
68
+ ],
69
+ id: streamContext.id,
70
+ type: 'tool_calls',
71
+ } satisfies StreamProtocolToolCallChunk;
72
+ }
73
+ }
74
+
75
+ return { data: chunk.item, id: streamContext.id, type: 'data' };
76
+ }
77
+
78
+ case 'response.function_call_arguments.delta': {
79
+ return {
80
+ data: [
81
+ {
82
+ function: { arguments: chunk.delta, name: streamContext.tool?.name },
83
+ id: streamContext.tool?.id,
84
+ index: streamContext.toolIndex!,
85
+ type: 'function',
86
+ } satisfies StreamToolCallChunkData,
87
+ ],
88
+ id: streamContext.id,
89
+ type: 'tool_calls',
90
+ } satisfies StreamProtocolToolCallChunk;
91
+ }
92
+ case 'response.output_text.delta': {
93
+ return { data: chunk.delta, id: chunk.item_id, type: 'text' };
94
+ }
95
+
96
+ case 'response.reasoning_summary_part.added': {
97
+ if (!streamContext.startReasoning) {
98
+ streamContext.startReasoning = true;
99
+ return { data: '', id: chunk.item_id, type: 'reasoning' };
100
+ } else {
101
+ return { data: '\n', id: chunk.item_id, type: 'reasoning' };
102
+ }
103
+ }
104
+
105
+ case 'response.reasoning_summary_text.delta': {
106
+ return { data: chunk.delta, id: chunk.item_id, type: 'reasoning' };
107
+ }
108
+
109
+ case 'response.completed': {
110
+ if (chunk.response.usage) {
111
+ return {
112
+ data: convertResponseUsage(chunk.response.usage),
113
+ id: chunk.response.id,
114
+ type: 'usage',
115
+ };
116
+ }
117
+
118
+ return { data: chunk, id: streamContext.id, type: 'data' };
119
+ }
120
+
121
+ default: {
122
+ return { data: chunk, id: streamContext.id, type: 'data' };
123
+ }
124
+ }
125
+ } catch (e) {
126
+ const errorName = 'StreamChunkError';
127
+ console.error(`[${errorName}]`, e);
128
+ console.error(`[${errorName}] raw chunk:`, chunk);
129
+
130
+ const err = e as Error;
131
+
132
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
133
+ const errorData = {
134
+ body: {
135
+ message:
136
+ 'chat response streaming chunk parse error, please contact your API Provider to fix it.',
137
+ context: { error: { message: err.message, name: err.name }, chunk },
138
+ },
139
+ type: errorName,
140
+ } as ChatMessageError;
141
+ /* eslint-enable */
142
+
143
+ return { data: errorData, id: streamContext.id, type: 'error' };
144
+ }
145
+ };
146
+
147
+ export const OpenAIResponsesStream = (
148
+ stream: Stream<OpenAI.Responses.ResponseStreamEvent> | ReadableStream,
149
+ { callbacks, provider, bizErrorTypeTransformer, inputStartAt }: OpenAIStreamOptions = {},
150
+ ) => {
151
+ const streamStack: StreamContext = { id: '' };
152
+
153
+ const readableStream =
154
+ stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
155
+
156
+ return (
157
+ readableStream
158
+ // 1. handle the first error if exist
159
+ // provider like huggingface or minimax will return error in the stream,
160
+ // so in the first Transformer, we need to handle the error
161
+ .pipeThrough(createFirstErrorHandleTransformer(bizErrorTypeTransformer, provider))
162
+ .pipeThrough(createTokenSpeedCalculator(transformOpenAIStream, { inputStartAt, streamStack }))
163
+ .pipeThrough(createSSEProtocolTransformer((c) => c, streamStack))
164
+ .pipeThrough(createCallbacksTransformer(callbacks))
165
+ );
166
+ };
@@ -23,6 +23,10 @@ export interface StreamContext {
23
23
  * This array accumulates all citation items received during the streaming response.
24
24
  */
25
25
  returnedCitationArray?: CitationItem[];
26
+ /**
27
+ * O series models need a condition to separate part
28
+ */
29
+ startReasoning?: boolean;
26
30
  thinking?: {
27
31
  id: string;
28
32
  name: string;
@@ -78,7 +82,6 @@ export interface StreamToolCallChunkData {
78
82
  export interface StreamProtocolToolCallChunk {
79
83
  data: StreamToolCallChunkData[];
80
84
  id: string;
81
- index: number;
82
85
  type: 'tool_calls';
83
86
  }
84
87
 
@@ -0,0 +1,20 @@
1
+ export const createReadableStream = <T>(chunks: T[]) =>
2
+ new ReadableStream({
3
+ start(controller) {
4
+ chunks.forEach((chunk) => controller.enqueue(chunk));
5
+
6
+ controller.close();
7
+ },
8
+ });
9
+
10
+ export const readStreamChunk = async (stream: ReadableStream) => {
11
+ const decoder = new TextDecoder();
12
+ const chunks = [];
13
+
14
+ // @ts-ignore
15
+ for await (const chunk of stream) {
16
+ chunks.push(decoder.decode(chunk, { stream: true }));
17
+ }
18
+
19
+ return chunks;
20
+ };
@@ -48,3 +48,62 @@ export const convertUsage = (usage: OpenAI.Completions.CompletionUsage): ModelTo
48
48
 
49
49
  return finalData;
50
50
  };
51
+
52
+ export const convertResponseUsage = (usage: OpenAI.Responses.ResponseUsage): ModelTokensUsage => {
53
+ // 1. Extract and default primary values
54
+ const totalInputTokens = usage.input_tokens || 0;
55
+ const inputCachedTokens = usage.input_tokens_details?.cached_tokens || 0;
56
+
57
+ const totalOutputTokens = usage.output_tokens || 0;
58
+ const outputReasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
59
+
60
+ const overallTotalTokens = usage.total_tokens || 0;
61
+
62
+ // 2. Calculate derived values
63
+ const inputCacheMissTokens = totalInputTokens - inputCachedTokens;
64
+
65
+ // For ResponseUsage, inputTextTokens is effectively totalInputTokens as no further breakdown is given.
66
+ const inputTextTokens = totalInputTokens;
67
+
68
+ // For ResponseUsage, outputTextTokens is totalOutputTokens minus reasoning, as no audio output tokens are specified.
69
+ const outputTextTokens = totalOutputTokens - outputReasoningTokens;
70
+
71
+ // 3. Construct the comprehensive data object (matching ModelTokensUsage structure)
72
+ const data = {
73
+ // Fields from ModelTokensUsage that are not in ResponseUsage will be undefined or 0
74
+ // and potentially filtered out later.
75
+ acceptedPredictionTokens: undefined, // Not in ResponseUsage
76
+ inputAudioTokens: undefined, // Not in ResponseUsage
77
+ inputCacheMissTokens: inputCacheMissTokens,
78
+ inputCachedTokens: inputCachedTokens,
79
+ inputCitationTokens: undefined, // Not in ResponseUsage
80
+ inputTextTokens: inputTextTokens,
81
+ outputAudioTokens: undefined, // Not in ResponseUsage
82
+ outputReasoningTokens: outputReasoningTokens,
83
+ outputTextTokens: outputTextTokens,
84
+ rejectedPredictionTokens: undefined, // Not in ResponseUsage
85
+ totalInputTokens: totalInputTokens,
86
+ totalOutputTokens: totalOutputTokens,
87
+ totalTokens: overallTotalTokens,
88
+ } satisfies ModelTokensUsage; // This helps ensure all keys of ModelTokensUsage are considered
89
+
90
+ // 4. Filter out zero/falsy values, as done in the reference implementation
91
+ const finalData: Partial<ModelTokensUsage> = {}; // Use Partial for type safety during construction
92
+ Object.entries(data).forEach(([key, value]) => {
93
+ if (
94
+ value !== undefined &&
95
+ value !== null &&
96
+ (typeof value !== 'number' || value !== 0) && // A more explicit check than `!!value` if we want to be very specific about
97
+ // keeping non-numeric truthy values, but the reference uses `!!value`.
98
+ // `!!value` will filter out 0, which is often desired for token counts.
99
+ // Let's stick to the reference's behavior:
100
+ !!value
101
+ ) {
102
+ // @ts-ignore - We are building an object that will conform to ModelTokensUsage
103
+ // by selectively adding properties.
104
+ finalData[key as keyof ModelTokensUsage] = value as number;
105
+ }
106
+ });
107
+
108
+ return finalData as ModelTokensUsage; // Cast because we've built it to match
109
+ };
@@ -210,6 +210,10 @@ export default {
210
210
  title: '使用客户端请求模式',
211
211
  },
212
212
  helpDoc: '配置教程',
213
+ responsesApi: {
214
+ desc: '采用 OpenAI 新一代请求格式规范,解锁思维链等进阶特性',
215
+ title: '使用 Responses API 规范',
216
+ },
213
217
  waitingForMore: '更多模型正在 <1>计划接入</1> 中,敬请期待',
214
218
  },
215
219
  createNew: {
@@ -618,6 +618,32 @@ describe('ChatService', () => {
618
618
  stream: true,
619
619
  ...DEFAULT_AGENT_CONFIG.params,
620
620
  ...params,
621
+ apiMode: 'responses',
622
+ };
623
+
624
+ await chatService.getChatCompletion(params, options);
625
+
626
+ expect(global.fetch).toHaveBeenCalledWith(expect.any(String), {
627
+ body: JSON.stringify(expectedPayload),
628
+ headers: expect.any(Object),
629
+ method: 'POST',
630
+ });
631
+ });
632
+ it('should make a POST request without response in non-openai provider payload', async () => {
633
+ const params: Partial<ChatStreamPayload> = {
634
+ model: 'deepseek-reasoner',
635
+ provider: 'deepseek',
636
+ messages: [],
637
+ };
638
+
639
+ const options = {};
640
+
641
+ const expectedPayload = {
642
+ model: 'deepseek-reasoner',
643
+ stream: true,
644
+ ...DEFAULT_AGENT_CONFIG.params,
645
+ messages: [],
646
+ provider: undefined,
621
647
  };
622
648
 
623
649
  await chatService.getChatCompletion(params, options);
@@ -656,6 +682,7 @@ describe('ChatService', () => {
656
682
  stream: true,
657
683
  ...DEFAULT_AGENT_CONFIG.params,
658
684
  ...params,
685
+ apiMode: 'responses',
659
686
  };
660
687
 
661
688
  const result = await chatService.getChatCompletion(params, options);
@@ -37,11 +37,11 @@ import { ChatErrorType } from '@/types/fetch';
37
37
  import { ChatMessage, MessageToolCall } from '@/types/message';
38
38
  import type { ChatStreamPayload, OpenAIChatMessage } from '@/types/openai/chat';
39
39
  import { UserMessageContentPart } from '@/types/openai/chat';
40
+ import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
40
41
  import { createErrorResponse } from '@/utils/errorResponse';
41
42
  import { FetchSSEOptions, fetchSSE, getMessageError } from '@/utils/fetch';
42
43
  import { genToolCallingName } from '@/utils/toolCall';
43
44
  import { createTraceHeader, getTraceId } from '@/utils/trace';
44
- import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
45
45
 
46
46
  import { createHeaderWithAuth, createPayloadWithKeyVaults } from './_auth';
47
47
  import { API_ENDPOINTS } from './_url';
@@ -315,9 +315,15 @@ class ChatService {
315
315
  model = findDeploymentName(model, provider);
316
316
  }
317
317
 
318
+ const apiMode = aiProviderSelectors.isProviderEnableResponseApi(provider)(
319
+ getAiInfraStoreState(),
320
+ )
321
+ ? 'responses'
322
+ : undefined;
323
+
318
324
  const payload = merge(
319
325
  { model: DEFAULT_AGENT_CONFIG.model, stream: true, ...DEFAULT_AGENT_CONFIG.params },
320
- { ...res, model },
326
+ { ...res, apiMode, model },
321
327
  );
322
328
 
323
329
  /**
@@ -99,6 +99,16 @@ const isProviderHasBuiltinSearchConfig = (id: string) => (s: AIProviderStoreStat
99
99
  return !!providerCfg?.settings.searchMode && providerCfg?.settings.searchMode !== 'internal';
100
100
  };
101
101
 
102
+ const isProviderEnableResponseApi = (id: string) => (s: AIProviderStoreState) => {
103
+ const providerCfg = providerConfigById(id)(s);
104
+
105
+ const enableResponseApi = providerCfg?.config?.enableResponseApi;
106
+
107
+ if (typeof enableResponseApi === 'boolean') return enableResponseApi;
108
+
109
+ return id === 'openai';
110
+ };
111
+
102
112
  export const aiProviderSelectors = {
103
113
  activeProviderConfig,
104
114
  disabledAiProviderList,
@@ -107,6 +117,7 @@ export const aiProviderSelectors = {
107
117
  isActiveProviderEndpointNotEmpty,
108
118
  isAiProviderConfigLoading,
109
119
  isProviderConfigUpdating,
120
+ isProviderEnableResponseApi,
110
121
  isProviderEnabled,
111
122
  isProviderFetchOnClient,
112
123
  isProviderHasBuiltinSearch,
@@ -79,6 +79,7 @@ export interface AiProviderSettings {
79
79
  * whether to smoothing the output
80
80
  */
81
81
  smoothing?: SmoothingParams;
82
+ supportResponsesApi?: boolean;
82
83
  }
83
84
 
84
85
  const AiProviderSettingsSchema = z.object({
@@ -106,8 +107,13 @@ const AiProviderSettingsSchema = z.object({
106
107
  toolsCalling: z.boolean().optional(),
107
108
  })
108
109
  .optional(),
110
+ supportResponsesApi: z.boolean().optional(),
109
111
  });
110
112
 
113
+ export interface AiProviderConfig {
114
+ enableResponseApi?: boolean;
115
+ }
116
+
111
117
  // create
112
118
  export const CreateAiProviderSchema = z.object({
113
119
  config: z.object({}).passthrough().optional(),
@@ -206,8 +212,13 @@ export type UpdateAiProviderParams = z.infer<typeof UpdateAiProviderSchema>;
206
212
 
207
213
  export const UpdateAiProviderConfigSchema = z.object({
208
214
  checkModel: z.string().optional(),
215
+ config: z
216
+ .object({
217
+ enableResponseApi: z.boolean().optional(),
218
+ })
219
+ .optional(),
209
220
  fetchOnClient: z.boolean().nullable().optional(),
210
- keyVaults: z.object({}).passthrough().optional(),
221
+ keyVaults: z.record(z.string(), z.string().optional()).optional(),
211
222
  });
212
223
 
213
224
  export type UpdateAiProviderConfigParams = z.infer<typeof UpdateAiProviderConfigSchema>;
@@ -235,6 +246,7 @@ export interface EnabledProviderWithModels {
235
246
  }
236
247
 
237
248
  export interface AiProviderRuntimeConfig {
249
+ config: AiProviderConfig;
238
250
  fetchOnClient?: boolean;
239
251
  keyVaults: Record<string, string>;
240
252
  settings: AiProviderSettings;