@lobehub/chat 0.156.1 → 0.157.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 (115) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/Dockerfile +4 -1
  3. package/package.json +3 -2
  4. package/src/config/modelProviders/anthropic.ts +3 -0
  5. package/src/config/modelProviders/google.ts +3 -0
  6. package/src/config/modelProviders/groq.ts +5 -1
  7. package/src/config/modelProviders/minimax.ts +10 -7
  8. package/src/config/modelProviders/mistral.ts +1 -0
  9. package/src/config/modelProviders/moonshot.ts +3 -0
  10. package/src/config/modelProviders/zhipu.ts +2 -6
  11. package/src/config/server/provider.ts +1 -1
  12. package/src/database/client/core/db.ts +32 -0
  13. package/src/database/client/core/schemas.ts +9 -0
  14. package/src/database/client/models/__tests__/message.test.ts +2 -2
  15. package/src/database/client/schemas/message.ts +8 -1
  16. package/src/features/AgentSetting/store/action.ts +15 -6
  17. package/src/features/Conversation/Actions/Tool.tsx +16 -0
  18. package/src/features/Conversation/Actions/index.ts +2 -2
  19. package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +78 -0
  20. package/src/features/Conversation/Messages/Assistant/ToolCalls/style.ts +25 -0
  21. package/src/features/Conversation/Messages/Assistant/index.tsx +47 -0
  22. package/src/features/Conversation/Messages/Default.tsx +4 -1
  23. package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/index.tsx +34 -35
  24. package/src/features/Conversation/Messages/Tool/index.tsx +44 -0
  25. package/src/features/Conversation/Messages/index.ts +3 -2
  26. package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +1 -1
  27. package/src/features/Conversation/components/SkeletonList.tsx +2 -2
  28. package/src/features/Conversation/index.tsx +2 -3
  29. package/src/libs/agent-runtime/BaseAI.ts +2 -9
  30. package/src/libs/agent-runtime/anthropic/index.test.ts +195 -0
  31. package/src/libs/agent-runtime/anthropic/index.ts +71 -15
  32. package/src/libs/agent-runtime/azureOpenai/index.ts +12 -13
  33. package/src/libs/agent-runtime/bedrock/index.ts +24 -18
  34. package/src/libs/agent-runtime/google/index.test.ts +154 -0
  35. package/src/libs/agent-runtime/google/index.ts +91 -10
  36. package/src/libs/agent-runtime/groq/index.test.ts +41 -72
  37. package/src/libs/agent-runtime/groq/index.ts +7 -0
  38. package/src/libs/agent-runtime/minimax/index.test.ts +2 -2
  39. package/src/libs/agent-runtime/minimax/index.ts +14 -37
  40. package/src/libs/agent-runtime/mistral/index.test.ts +0 -53
  41. package/src/libs/agent-runtime/mistral/index.ts +1 -0
  42. package/src/libs/agent-runtime/moonshot/index.test.ts +1 -71
  43. package/src/libs/agent-runtime/ollama/index.test.ts +197 -0
  44. package/src/libs/agent-runtime/ollama/index.ts +3 -3
  45. package/src/libs/agent-runtime/openai/index.test.ts +0 -53
  46. package/src/libs/agent-runtime/openrouter/index.test.ts +1 -53
  47. package/src/libs/agent-runtime/perplexity/index.test.ts +0 -71
  48. package/src/libs/agent-runtime/perplexity/index.ts +2 -3
  49. package/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +886 -0
  50. package/src/libs/agent-runtime/togetherai/fixtures/models.json +8111 -0
  51. package/src/libs/agent-runtime/togetherai/index.test.ts +16 -54
  52. package/src/libs/agent-runtime/types/chat.ts +19 -3
  53. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +120 -1
  54. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +67 -4
  55. package/src/libs/agent-runtime/utils/debugStream.test.ts +70 -0
  56. package/src/libs/agent-runtime/utils/debugStream.ts +39 -9
  57. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +521 -0
  58. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +76 -5
  59. package/src/libs/agent-runtime/utils/response.ts +12 -0
  60. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +197 -0
  61. package/src/libs/agent-runtime/utils/streams/anthropic.ts +91 -0
  62. package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +21 -0
  63. package/src/libs/agent-runtime/utils/streams/bedrock/common.ts +32 -0
  64. package/src/libs/agent-runtime/utils/streams/bedrock/index.ts +3 -0
  65. package/src/libs/agent-runtime/utils/streams/bedrock/llama.test.ts +196 -0
  66. package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +51 -0
  67. package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +97 -0
  68. package/src/libs/agent-runtime/utils/streams/google-ai.ts +68 -0
  69. package/src/libs/agent-runtime/utils/streams/index.ts +7 -0
  70. package/src/libs/agent-runtime/utils/streams/minimax.ts +39 -0
  71. package/src/libs/agent-runtime/utils/streams/ollama.test.ts +77 -0
  72. package/src/libs/agent-runtime/utils/streams/ollama.ts +38 -0
  73. package/src/libs/agent-runtime/utils/streams/openai.test.ts +263 -0
  74. package/src/libs/agent-runtime/utils/streams/openai.ts +79 -0
  75. package/src/libs/agent-runtime/utils/streams/protocol.ts +100 -0
  76. package/src/libs/agent-runtime/zeroone/index.test.ts +1 -53
  77. package/src/libs/agent-runtime/zhipu/index.test.ts +1 -1
  78. package/src/libs/agent-runtime/zhipu/index.ts +3 -2
  79. package/src/locales/default/plugin.ts +3 -4
  80. package/src/migrations/FromV4ToV5/fixtures/from-v1-to-v5-output.json +245 -0
  81. package/src/migrations/FromV4ToV5/fixtures/function-input-v4.json +96 -0
  82. package/src/migrations/FromV4ToV5/fixtures/function-output-v5.json +120 -0
  83. package/src/migrations/FromV4ToV5/index.ts +58 -0
  84. package/src/migrations/FromV4ToV5/migrations.test.ts +49 -0
  85. package/src/migrations/FromV4ToV5/types/v4.ts +21 -0
  86. package/src/migrations/FromV4ToV5/types/v5.ts +27 -0
  87. package/src/migrations/index.ts +8 -1
  88. package/src/services/__tests__/chat.test.ts +10 -20
  89. package/src/services/chat.ts +78 -65
  90. package/src/store/chat/slices/enchance/action.ts +15 -10
  91. package/src/store/chat/slices/message/action.test.ts +36 -86
  92. package/src/store/chat/slices/message/action.ts +70 -79
  93. package/src/store/chat/slices/message/reducer.ts +18 -1
  94. package/src/store/chat/slices/message/selectors.test.ts +38 -68
  95. package/src/store/chat/slices/message/selectors.ts +1 -22
  96. package/src/store/chat/slices/plugin/action.test.ts +147 -203
  97. package/src/store/chat/slices/plugin/action.ts +96 -82
  98. package/src/store/chat/slices/share/action.test.ts +3 -3
  99. package/src/store/chat/slices/share/action.ts +1 -1
  100. package/src/store/chat/slices/topic/action.ts +7 -2
  101. package/src/store/tool/selectors/tool.ts +6 -24
  102. package/src/store/tool/slices/builtin/action.test.ts +90 -0
  103. package/src/types/llm.ts +1 -1
  104. package/src/types/message/index.ts +9 -4
  105. package/src/types/message/tools.ts +57 -0
  106. package/src/types/openai/chat.ts +6 -0
  107. package/src/utils/fetch.test.ts +245 -1
  108. package/src/utils/fetch.ts +120 -44
  109. package/src/utils/toolCall.ts +21 -0
  110. package/src/features/Conversation/Messages/Assistant.tsx +0 -26
  111. package/src/features/Conversation/Messages/Function.tsx +0 -35
  112. package/src/libs/agent-runtime/ollama/stream.ts +0 -31
  113. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/PluginResultJSON.tsx +0 -0
  114. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/Settings.tsx +0 -0
  115. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/style.ts +0 -0
@@ -2,9 +2,10 @@
2
2
  import OpenAI from 'openai';
3
3
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
- import { ChatStreamCallbacks, LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
5
+ import { LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
6
6
 
7
7
  import * as debugStreamModule from '../utils/debugStream';
8
+ import models from './fixtures/models.json';
8
9
  import { LobeTogetherAI } from './index';
9
10
 
10
11
  const provider = 'togetherai';
@@ -81,6 +82,7 @@ describe('LobeTogetherAI', () => {
81
82
  messages: [{ content: 'Hello', role: 'user' }],
82
83
  model: 'mistralai/mistral-7b-instruct:free',
83
84
  temperature: 0.7,
85
+ stream: true,
84
86
  top_p: 1,
85
87
  },
86
88
  { headers: { Accept: '*/*' } },
@@ -253,59 +255,6 @@ describe('LobeTogetherAI', () => {
253
255
  });
254
256
  });
255
257
 
256
- describe('LobeTogetherAI chat with callback and headers', () => {
257
- it('should handle callback and headers correctly', async () => {
258
- // 模拟 chat.completions.create 方法返回一个可读流
259
- const mockCreateMethod = vi
260
- .spyOn(instance['client'].chat.completions, 'create')
261
- .mockResolvedValue(
262
- new ReadableStream({
263
- start(controller) {
264
- controller.enqueue({
265
- id: 'chatcmpl-8xDx5AETP8mESQN7UB30GxTN2H1SO',
266
- object: 'chat.completion.chunk',
267
- created: 1709125675,
268
- model: 'mistralai/mistral-7b-instruct:free',
269
- system_fingerprint: 'fp_86156a94a0',
270
- choices: [
271
- { index: 0, delta: { content: 'hello' }, logprobs: null, finish_reason: null },
272
- ],
273
- });
274
- controller.close();
275
- },
276
- }) as any,
277
- );
278
-
279
- // 准备 callback 和 headers
280
- const mockCallback: ChatStreamCallbacks = {
281
- onStart: vi.fn(),
282
- onToken: vi.fn(),
283
- };
284
- const mockHeaders = { 'Custom-Header': 'TestValue' };
285
-
286
- // 执行测试
287
- const result = await instance.chat(
288
- {
289
- messages: [{ content: 'Hello', role: 'user' }],
290
- model: 'mistralai/mistral-7b-instruct:free',
291
- temperature: 0,
292
- },
293
- { callback: mockCallback, headers: mockHeaders },
294
- );
295
-
296
- // 验证 callback 被调用
297
- await result.text(); // 确保流被消费
298
- expect(mockCallback.onStart).toHaveBeenCalled();
299
- expect(mockCallback.onToken).toHaveBeenCalledWith('hello');
300
-
301
- // 验证 headers 被正确传递
302
- expect(result.headers.get('Custom-Header')).toEqual('TestValue');
303
-
304
- // 清理
305
- mockCreateMethod.mockRestore();
306
- });
307
- });
308
-
309
258
  describe('DEBUG', () => {
310
259
  it('should call debugStream and return StreamingTextResponse when DEBUG_TOGETHERAI_CHAT_COMPLETION is 1', async () => {
311
260
  // Arrange
@@ -347,4 +296,17 @@ describe('LobeTogetherAI', () => {
347
296
  });
348
297
  });
349
298
  });
299
+
300
+ describe('models', () => {
301
+ it('should get models', async () => {
302
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
303
+ json: async () => models,
304
+ ok: true,
305
+ } as Response);
306
+
307
+ const list = await instance.models();
308
+
309
+ expect(list).toMatchSnapshot();
310
+ });
311
+ });
350
312
  });
@@ -1,6 +1,6 @@
1
- import { OpenAIStreamCallbacks } from 'ai';
1
+ import { MessageToolCall } from '@/types/message';
2
2
 
3
- export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function';
3
+ export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function' | 'tool';
4
4
 
5
5
  interface UserMessageContentPartText {
6
6
  text: string;
@@ -30,6 +30,8 @@ export interface OpenAIChatMessage {
30
30
  * @description 消息发送者的角色
31
31
  */
32
32
  role: LLMRoleType;
33
+ tool_call_id?: string;
34
+ tool_calls?: MessageToolCall[];
33
35
  }
34
36
 
35
37
  /**
@@ -127,4 +129,18 @@ export interface ChatCompletionTool {
127
129
  type: 'function';
128
130
  }
129
131
 
130
- export type ChatStreamCallbacks = OpenAIStreamCallbacks;
132
+ export interface ChatStreamCallbacks {
133
+ /**
134
+ * `onCompletion`: Called for each tokenized message.
135
+ **/
136
+ onCompletion?: (completion: string) => Promise<void> | void;
137
+ /** `onFinal`: Called once when the stream is closed with the final completion message. */
138
+ onFinal?: (completion: string) => Promise<void> | void;
139
+ /** `onStart`: Called once when the stream is initialized. */
140
+ onStart?: () => Promise<void> | void;
141
+ /** `onText`: Called for each text chunk. */
142
+ onText?: (text: string) => Promise<void> | void;
143
+ /** `onToken`: Called for each tokenized message. */
144
+ onToken?: (token: string) => Promise<void> | void;
145
+ onToolCall?: () => Promise<void> | void;
146
+ }
@@ -1,3 +1,4 @@
1
+ import { OpenAI } from 'openai';
1
2
  import { describe, expect, it } from 'vitest';
2
3
 
3
4
  import { OpenAIChatMessage, UserMessageContentPart } from '../types/chat';
@@ -5,6 +6,7 @@ import {
5
6
  buildAnthropicBlock,
6
7
  buildAnthropicMessage,
7
8
  buildAnthropicMessages,
9
+ buildAnthropicTools,
8
10
  } from './anthropicHelpers';
9
11
  import { parseDataUri } from './uriParser';
10
12
 
@@ -49,7 +51,87 @@ describe('anthropicHelpers', () => {
49
51
  role: 'system',
50
52
  };
51
53
  const result = buildAnthropicMessage(message);
52
- expect(result).toEqual({ content: [{ type: 'text', text: 'Hello!' }], role: 'assistant' });
54
+ expect(result).toEqual({ content: [{ type: 'text', text: 'Hello!' }], role: 'user' });
55
+ });
56
+
57
+ it('should correctly convert user message with string content', () => {
58
+ const message: OpenAIChatMessage = {
59
+ content: 'Hello!',
60
+ role: 'user',
61
+ };
62
+ const result = buildAnthropicMessage(message);
63
+ expect(result).toEqual({ content: 'Hello!', role: 'user' });
64
+ });
65
+
66
+ it('should correctly convert user message with content parts', () => {
67
+ const message: OpenAIChatMessage = {
68
+ content: [
69
+ { type: 'text', text: 'Check out this image:' },
70
+ { type: 'image_url', image_url: { url: 'data:image/png;base64,abc123' } },
71
+ ],
72
+ role: 'user',
73
+ };
74
+ const result = buildAnthropicMessage(message);
75
+ expect(result.role).toBe('user');
76
+ expect(result.content).toHaveLength(2);
77
+ expect((result.content[1] as any).type).toBe('image');
78
+ });
79
+
80
+ it('should correctly convert tool message', () => {
81
+ const message: OpenAIChatMessage = {
82
+ content: 'Tool result content',
83
+ role: 'tool',
84
+ tool_call_id: 'tool123',
85
+ };
86
+ const result = buildAnthropicMessage(message);
87
+ expect(result.role).toBe('user');
88
+ expect(result.content).toEqual([
89
+ {
90
+ content: 'Tool result content',
91
+ tool_use_id: 'tool123',
92
+ type: 'tool_result',
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it('should correctly convert assistant message with tool calls', () => {
98
+ const message: OpenAIChatMessage = {
99
+ content: 'Here is the result:',
100
+ role: 'assistant',
101
+ tool_calls: [
102
+ {
103
+ id: 'call1',
104
+ type: 'function',
105
+ function: {
106
+ name: 'search',
107
+ arguments: '{"query":"anthropic"}',
108
+ },
109
+ },
110
+ ],
111
+ };
112
+ const result = buildAnthropicMessage(message);
113
+ expect(result.role).toBe('assistant');
114
+ expect(result.content).toEqual([
115
+ { text: 'Here is the result:', type: 'text' },
116
+ {
117
+ id: 'call1',
118
+ input: { query: 'anthropic' },
119
+ name: 'search',
120
+ type: 'tool_use',
121
+ },
122
+ ]);
123
+ });
124
+
125
+ it('should correctly convert function message', () => {
126
+ const message: OpenAIChatMessage = {
127
+ content: 'def hello(name):\n return f"Hello {name}"',
128
+ role: 'function',
129
+ };
130
+ const result = buildAnthropicMessage(message);
131
+ expect(result).toEqual({
132
+ content: 'def hello(name):\n return f"Hello {name}"',
133
+ role: 'assistant',
134
+ });
53
135
  });
54
136
  });
55
137
 
@@ -111,4 +193,41 @@ describe('anthropicHelpers', () => {
111
193
  ]);
112
194
  });
113
195
  });
196
+
197
+ describe('buildAnthropicTools', () => {
198
+ it('should correctly convert OpenAI tools to Anthropic format', () => {
199
+ const tools: OpenAI.ChatCompletionTool[] = [
200
+ {
201
+ type: 'function',
202
+ function: {
203
+ name: 'search',
204
+ description: 'Searches the web',
205
+ parameters: {
206
+ type: 'object',
207
+ properties: {
208
+ query: { type: 'string' },
209
+ },
210
+ required: ['query'],
211
+ },
212
+ },
213
+ },
214
+ ];
215
+
216
+ const result = buildAnthropicTools(tools);
217
+
218
+ expect(result).toEqual([
219
+ {
220
+ name: 'search',
221
+ description: 'Searches the web',
222
+ input_schema: {
223
+ type: 'object',
224
+ properties: {
225
+ query: { type: 'string' },
226
+ },
227
+ required: ['query'],
228
+ },
229
+ },
230
+ ]);
231
+ });
232
+ });
114
233
  });
@@ -1,4 +1,5 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
+ import OpenAI from 'openai';
2
3
 
3
4
  import { OpenAIChatMessage, UserMessageContentPart } from '../types';
4
5
  import { parseDataUri } from './uriParser';
@@ -30,10 +31,63 @@ export const buildAnthropicMessage = (
30
31
  message: OpenAIChatMessage,
31
32
  ): Anthropic.Messages.MessageParam => {
32
33
  const content = message.content as string | UserMessageContentPart[];
33
- return {
34
- content: typeof content === 'string' ? content : content.map((c) => buildAnthropicBlock(c)),
35
- role: message.role === 'function' || message.role === 'system' ? 'assistant' : message.role,
36
- };
34
+
35
+ switch (message.role) {
36
+ case 'system': {
37
+ return { content: content as string, role: 'user' };
38
+ }
39
+
40
+ case 'user': {
41
+ return {
42
+ content: typeof content === 'string' ? content : content.map((c) => buildAnthropicBlock(c)),
43
+ role: 'user',
44
+ };
45
+ }
46
+
47
+ case 'tool': {
48
+ // refs: https://docs.anthropic.com/claude/docs/tool-use#tool-use-and-tool-result-content-blocks
49
+ return {
50
+ content: [
51
+ {
52
+ content: message.content,
53
+ tool_use_id: message.tool_call_id,
54
+ type: 'tool_result',
55
+ } as any,
56
+ ],
57
+ role: 'user',
58
+ };
59
+ }
60
+
61
+ case 'assistant': {
62
+ // if there is tool_calls , we need to covert the tool_calls to tool_use content block
63
+ // refs: https://docs.anthropic.com/claude/docs/tool-use#tool-use-and-tool-result-content-blocks
64
+ if (message.tool_calls) {
65
+ return {
66
+ content: [
67
+ // avoid empty text content block
68
+ !!message.content && {
69
+ text: message.content as string,
70
+ type: 'text',
71
+ },
72
+ ...(message.tool_calls.map((tool) => ({
73
+ id: tool.id,
74
+ input: JSON.parse(tool.function.arguments),
75
+ name: tool.function.name,
76
+ type: 'tool_use',
77
+ })) as any),
78
+ ].filter(Boolean),
79
+ role: 'assistant',
80
+ };
81
+ }
82
+
83
+ // or it's a plain assistant message
84
+ return { content: content as string, role: 'assistant' };
85
+ }
86
+
87
+ case 'function': {
88
+ return { content: content as string, role: 'assistant' };
89
+ }
90
+ }
37
91
  };
38
92
 
39
93
  export const buildAnthropicMessages = (
@@ -55,3 +109,12 @@ export const buildAnthropicMessages = (
55
109
 
56
110
  return messages;
57
111
  };
112
+
113
+ export const buildAnthropicTools = (tools?: OpenAI.ChatCompletionTool[]) =>
114
+ tools?.map(
115
+ (tool): Anthropic.Beta.Tools.Tool => ({
116
+ description: tool.function.description,
117
+ input_schema: tool.function.parameters as Anthropic.Beta.Tools.Tool.InputSchema,
118
+ name: tool.function.name,
119
+ }),
120
+ );
@@ -0,0 +1,70 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { debugStream } from './debugStream';
4
+
5
+ describe('debugStream', () => {
6
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
7
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
8
+
9
+ beforeEach(() => {
10
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
11
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
12
+ });
13
+
14
+ afterEach(() => {
15
+ consoleLogSpy.mockRestore();
16
+ consoleErrorSpy.mockRestore();
17
+ });
18
+
19
+ it('should log stream start and end messages', async () => {
20
+ const stream = new ReadableStream({
21
+ start(controller) {
22
+ controller.enqueue('test chunk');
23
+ controller.close();
24
+ },
25
+ });
26
+
27
+ await debugStream(stream);
28
+
29
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringMatching(/^\[stream start\]/));
30
+ });
31
+
32
+ it('should handle and log stream errors', async () => {
33
+ const stream = new ReadableStream({
34
+ start(controller) {
35
+ controller.enqueue('test chunk');
36
+ },
37
+ });
38
+
39
+ await debugStream(stream);
40
+
41
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[debugStream error]', expect.any(Error));
42
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[error chunk value:]', 'test chunk');
43
+ });
44
+
45
+ it('should decode ArrayBuffer chunk values', async () => {
46
+ const stream = new ReadableStream({
47
+ start(controller) {
48
+ controller.enqueue(new TextEncoder().encode('test chunk'));
49
+ controller.close();
50
+ },
51
+ });
52
+
53
+ await debugStream(stream);
54
+
55
+ expect(consoleLogSpy).toHaveBeenCalledWith('test chunk');
56
+ });
57
+
58
+ it('should stringify non-string chunk values', async () => {
59
+ const stream = new ReadableStream({
60
+ start(controller) {
61
+ controller.enqueue({ test: 'chunk' });
62
+ controller.close();
63
+ },
64
+ });
65
+
66
+ await debugStream(stream);
67
+
68
+ expect(consoleLogSpy).toHaveBeenCalledWith('{"test":"chunk"}');
69
+ });
70
+ });
@@ -1,18 +1,48 @@
1
+ // no need to introduce a package to get the current time as this module is just a debug utility
2
+ const getTime = () => {
3
+ const date = new Date();
4
+ return `${date.getFullYear()}-${date.getDate()}-${date.getDay()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`;
5
+ };
6
+
1
7
  export const debugStream = async (stream: ReadableStream) => {
2
- let done = false;
8
+ let finished = false;
3
9
  let chunk = 0;
10
+ let chunkValue: any;
4
11
  const decoder = new TextDecoder();
5
12
 
6
13
  const reader = stream.getReader();
7
- while (!done) {
8
- const { value, done: _done } = await reader.read();
9
- const chunkValue = decoder.decode(value, { stream: true });
10
- if (!_done) {
11
- console.log(`[chunk ${chunk}]`);
14
+
15
+ console.log(`[stream start] ${getTime()}`);
16
+
17
+ while (!finished) {
18
+ try {
19
+ const { value, done } = await reader.read();
20
+
21
+ if (done) {
22
+ console.log(`[stream finished] total chunks: ${chunk}\n`);
23
+ finished = true;
24
+ break;
25
+ }
26
+
27
+ chunkValue = value;
28
+
29
+ // if the value is ArrayBuffer, we need to decode it
30
+ if ('byteLength' in value) {
31
+ chunkValue = decoder.decode(value, { stream: true });
32
+ } else if (typeof value !== 'string') {
33
+ chunkValue = JSON.stringify(value);
34
+ }
35
+
36
+ console.log(`[chunk ${chunk}] ${getTime()}`);
12
37
  console.log(chunkValue);
13
- }
38
+ console.log(`\n`);
14
39
 
15
- done = _done;
16
- chunk++;
40
+ finished = done;
41
+ chunk++;
42
+ } catch (e) {
43
+ finished = true;
44
+ console.error('[debugStream error]', e);
45
+ console.error('[error chunk value:]', chunkValue);
46
+ }
17
47
  }
18
48
  };