@lobehub/chat 1.136.13 → 1.137.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.
Files changed (190) hide show
  1. package/.cursor/rules/add-setting-env.mdc +175 -0
  2. package/.cursor/rules/db-migrations.mdc +25 -0
  3. package/.env.example +7 -0
  4. package/CHANGELOG.md +50 -0
  5. package/Dockerfile +3 -2
  6. package/Dockerfile.database +15 -3
  7. package/Dockerfile.pglite +3 -2
  8. package/changelog/v1.json +18 -0
  9. package/docs/development/database-schema.dbml +1 -0
  10. package/docs/self-hosting/advanced/feature-flags.mdx +25 -15
  11. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +25 -15
  12. package/docs/self-hosting/environment-variables/basic.mdx +12 -0
  13. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +12 -0
  14. package/locales/ar/setting.json +8 -0
  15. package/locales/bg-BG/setting.json +8 -0
  16. package/locales/de-DE/setting.json +8 -0
  17. package/locales/en-US/setting.json +8 -0
  18. package/locales/es-ES/setting.json +8 -0
  19. package/locales/fa-IR/setting.json +8 -0
  20. package/locales/fr-FR/setting.json +8 -0
  21. package/locales/it-IT/setting.json +8 -0
  22. package/locales/ja-JP/setting.json +8 -0
  23. package/locales/ko-KR/setting.json +8 -0
  24. package/locales/nl-NL/setting.json +8 -0
  25. package/locales/pl-PL/setting.json +8 -0
  26. package/locales/pt-BR/setting.json +8 -0
  27. package/locales/ru-RU/setting.json +8 -0
  28. package/locales/tr-TR/setting.json +8 -0
  29. package/locales/vi-VN/setting.json +8 -0
  30. package/locales/zh-CN/setting.json +8 -0
  31. package/locales/zh-TW/setting.json +8 -0
  32. package/package.json +1 -1
  33. package/packages/agent-runtime/examples/tools-calling.ts +4 -3
  34. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +559 -29
  35. package/packages/agent-runtime/src/core/runtime.ts +171 -43
  36. package/packages/agent-runtime/src/types/instruction.ts +32 -6
  37. package/packages/agent-runtime/src/types/runtime.ts +2 -2
  38. package/packages/agent-runtime/src/types/state.ts +1 -8
  39. package/packages/agent-runtime/vitest.config.mts +14 -0
  40. package/packages/const/src/settings/image.ts +8 -0
  41. package/packages/const/src/settings/index.ts +3 -0
  42. package/packages/context-engine/src/__tests__/pipeline.test.ts +485 -0
  43. package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +381 -0
  44. package/packages/context-engine/src/base/__tests__/BaseProvider.test.ts +392 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +346 -0
  46. package/packages/context-engine/src/processors/__tests__/ToolCall.test.ts +552 -0
  47. package/packages/database/migrations/0038_add_image_user_settings.sql +1 -0
  48. package/packages/database/migrations/meta/0038_snapshot.json +7580 -0
  49. package/packages/database/migrations/meta/_journal.json +7 -0
  50. package/packages/database/src/core/migrations.json +6 -0
  51. package/packages/database/src/models/user.ts +3 -1
  52. package/packages/database/src/schemas/user.ts +1 -0
  53. package/packages/file-loaders/src/loaders/docx/index.test.ts +0 -1
  54. package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +30 -0
  55. package/packages/file-loaders/src/loaders/excel/index.test.ts +8 -0
  56. package/packages/file-loaders/src/loaders/pptx/index.test.ts +25 -0
  57. package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
  58. package/packages/file-loaders/vitest.config.mts +8 -0
  59. package/packages/model-runtime/CLAUDE.md +5 -0
  60. package/packages/model-runtime/docs/test-coverage.md +706 -0
  61. package/packages/model-runtime/src/core/ModelRuntime.test.ts +231 -0
  62. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +1 -1
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.test.ts +799 -0
  64. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +188 -4
  65. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +41 -10
  66. package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +439 -0
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +789 -0
  68. package/packages/model-runtime/src/core/streams/openai/responsesStream.test.ts +551 -0
  69. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +230 -0
  70. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +334 -37
  71. package/packages/model-runtime/src/providerTestUtils.ts +148 -145
  72. package/packages/model-runtime/src/providers/ai302/index.test.ts +60 -0
  73. package/packages/model-runtime/src/providers/ai302/index.ts +9 -4
  74. package/packages/model-runtime/src/providers/ai360/index.test.ts +1213 -1
  75. package/packages/model-runtime/src/providers/ai360/index.ts +9 -4
  76. package/packages/model-runtime/src/providers/aihubmix/index.test.ts +73 -0
  77. package/packages/model-runtime/src/providers/aihubmix/index.ts +6 -9
  78. package/packages/model-runtime/src/providers/akashchat/index.test.ts +433 -3
  79. package/packages/model-runtime/src/providers/akashchat/index.ts +12 -7
  80. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +183 -29
  81. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +40 -24
  82. package/packages/model-runtime/src/providers/azureai/index.test.ts +102 -0
  83. package/packages/model-runtime/src/providers/baichuan/index.test.ts +416 -26
  84. package/packages/model-runtime/src/providers/baichuan/index.ts +23 -20
  85. package/packages/model-runtime/src/providers/bedrock/index.test.ts +420 -2
  86. package/packages/model-runtime/src/providers/cerebras/index.test.ts +465 -0
  87. package/packages/model-runtime/src/providers/cerebras/index.ts +8 -3
  88. package/packages/model-runtime/src/providers/cohere/index.test.ts +1074 -1
  89. package/packages/model-runtime/src/providers/cohere/index.ts +8 -3
  90. package/packages/model-runtime/src/providers/cometapi/index.test.ts +439 -3
  91. package/packages/model-runtime/src/providers/cometapi/index.ts +8 -3
  92. package/packages/model-runtime/src/providers/deepseek/index.test.ts +116 -1
  93. package/packages/model-runtime/src/providers/deepseek/index.ts +8 -3
  94. package/packages/model-runtime/src/providers/fireworksai/index.test.ts +264 -3
  95. package/packages/model-runtime/src/providers/fireworksai/index.ts +8 -3
  96. package/packages/model-runtime/src/providers/giteeai/index.test.ts +325 -3
  97. package/packages/model-runtime/src/providers/giteeai/index.ts +23 -6
  98. package/packages/model-runtime/src/providers/github/index.test.ts +532 -3
  99. package/packages/model-runtime/src/providers/github/index.ts +8 -3
  100. package/packages/model-runtime/src/providers/groq/index.test.ts +344 -31
  101. package/packages/model-runtime/src/providers/groq/index.ts +8 -3
  102. package/packages/model-runtime/src/providers/higress/index.test.ts +142 -0
  103. package/packages/model-runtime/src/providers/higress/index.ts +8 -3
  104. package/packages/model-runtime/src/providers/huggingface/index.test.ts +612 -1
  105. package/packages/model-runtime/src/providers/huggingface/index.ts +9 -4
  106. package/packages/model-runtime/src/providers/hunyuan/index.test.ts +365 -1
  107. package/packages/model-runtime/src/providers/hunyuan/index.ts +9 -3
  108. package/packages/model-runtime/src/providers/infiniai/index.test.ts +71 -0
  109. package/packages/model-runtime/src/providers/internlm/index.test.ts +369 -2
  110. package/packages/model-runtime/src/providers/internlm/index.ts +10 -5
  111. package/packages/model-runtime/src/providers/jina/index.test.ts +164 -3
  112. package/packages/model-runtime/src/providers/jina/index.ts +8 -3
  113. package/packages/model-runtime/src/providers/lmstudio/index.test.ts +182 -3
  114. package/packages/model-runtime/src/providers/lmstudio/index.ts +8 -3
  115. package/packages/model-runtime/src/providers/mistral/index.test.ts +779 -27
  116. package/packages/model-runtime/src/providers/mistral/index.ts +8 -3
  117. package/packages/model-runtime/src/providers/modelscope/index.test.ts +232 -1
  118. package/packages/model-runtime/src/providers/modelscope/index.ts +8 -3
  119. package/packages/model-runtime/src/providers/moonshot/index.test.ts +489 -2
  120. package/packages/model-runtime/src/providers/moonshot/index.ts +8 -3
  121. package/packages/model-runtime/src/providers/nebius/index.test.ts +381 -3
  122. package/packages/model-runtime/src/providers/nebius/index.ts +8 -3
  123. package/packages/model-runtime/src/providers/newapi/index.test.ts +667 -3
  124. package/packages/model-runtime/src/providers/newapi/index.ts +6 -3
  125. package/packages/model-runtime/src/providers/nvidia/index.test.ts +168 -1
  126. package/packages/model-runtime/src/providers/nvidia/index.ts +12 -7
  127. package/packages/model-runtime/src/providers/ollama/index.test.ts +797 -1
  128. package/packages/model-runtime/src/providers/ollama/index.ts +8 -0
  129. package/packages/model-runtime/src/providers/ollamacloud/index.test.ts +411 -0
  130. package/packages/model-runtime/src/providers/ollamacloud/index.ts +8 -3
  131. package/packages/model-runtime/src/providers/openai/index.test.ts +171 -2
  132. package/packages/model-runtime/src/providers/openai/index.ts +8 -3
  133. package/packages/model-runtime/src/providers/openrouter/index.test.ts +1647 -95
  134. package/packages/model-runtime/src/providers/openrouter/index.ts +12 -7
  135. package/packages/model-runtime/src/providers/qiniu/index.test.ts +294 -1
  136. package/packages/model-runtime/src/providers/qiniu/index.ts +8 -3
  137. package/packages/model-runtime/src/providers/search1api/index.test.ts +1131 -11
  138. package/packages/model-runtime/src/providers/search1api/index.ts +10 -4
  139. package/packages/model-runtime/src/providers/sensenova/index.test.ts +1069 -1
  140. package/packages/model-runtime/src/providers/sensenova/index.ts +8 -3
  141. package/packages/model-runtime/src/providers/siliconcloud/index.test.ts +196 -0
  142. package/packages/model-runtime/src/providers/siliconcloud/index.ts +8 -3
  143. package/packages/model-runtime/src/providers/spark/index.test.ts +293 -1
  144. package/packages/model-runtime/src/providers/spark/index.ts +8 -3
  145. package/packages/model-runtime/src/providers/stepfun/index.test.ts +322 -3
  146. package/packages/model-runtime/src/providers/stepfun/index.ts +8 -3
  147. package/packages/model-runtime/src/providers/tencentcloud/index.test.ts +182 -3
  148. package/packages/model-runtime/src/providers/tencentcloud/index.ts +8 -3
  149. package/packages/model-runtime/src/providers/togetherai/index.test.ts +359 -4
  150. package/packages/model-runtime/src/providers/togetherai/index.ts +12 -5
  151. package/packages/model-runtime/src/providers/v0/index.test.ts +341 -0
  152. package/packages/model-runtime/src/providers/v0/index.ts +20 -6
  153. package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +710 -0
  154. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +19 -13
  155. package/packages/model-runtime/src/providers/vllm/index.test.ts +45 -1
  156. package/packages/model-runtime/src/providers/volcengine/index.test.ts +75 -0
  157. package/packages/model-runtime/src/providers/wenxin/index.test.ts +144 -1
  158. package/packages/model-runtime/src/providers/wenxin/index.ts +8 -3
  159. package/packages/model-runtime/src/providers/xai/index.test.ts +105 -1
  160. package/packages/model-runtime/src/providers/xinference/index.test.ts +70 -1
  161. package/packages/model-runtime/src/providers/zeroone/index.test.ts +327 -3
  162. package/packages/model-runtime/src/providers/zeroone/index.ts +23 -6
  163. package/packages/model-runtime/src/providers/zhipu/index.test.ts +908 -236
  164. package/packages/model-runtime/src/providers/zhipu/index.ts +8 -3
  165. package/packages/model-runtime/src/types/structureOutput.ts +5 -1
  166. package/packages/model-runtime/vitest.config.mts +7 -1
  167. package/packages/types/src/aiChat.ts +20 -2
  168. package/packages/types/src/serverConfig.ts +7 -1
  169. package/packages/types/src/tool/index.ts +1 -0
  170. package/packages/types/src/tool/tool.ts +33 -0
  171. package/packages/types/src/user/settings/image.ts +3 -0
  172. package/packages/types/src/user/settings/index.ts +3 -0
  173. package/src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx +3 -0
  174. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +8 -3
  175. package/src/app/[variants]/(main)/settings/image/index.tsx +74 -0
  176. package/src/components/FormInput/FormSliderWithInput.tsx +40 -0
  177. package/src/components/FormInput/index.ts +1 -0
  178. package/src/envs/image.ts +27 -0
  179. package/src/features/Conversation/Messages/Assistant/index.tsx +1 -1
  180. package/src/features/Conversation/Messages/User/index.tsx +2 -2
  181. package/src/hooks/useFetchAiImageConfig.ts +12 -17
  182. package/src/locales/default/setting.ts +8 -0
  183. package/src/server/globalConfig/index.ts +5 -0
  184. package/src/server/routers/lambda/aiChat.ts +2 -0
  185. package/src/store/global/initialState.ts +1 -0
  186. package/src/store/image/slices/generationConfig/action.test.ts +17 -0
  187. package/src/store/image/slices/generationConfig/action.ts +18 -21
  188. package/src/store/image/slices/generationConfig/initialState.ts +3 -2
  189. package/src/store/user/slices/common/action.ts +1 -0
  190. package/src/store/user/slices/settings/selectors/settings.ts +3 -0
@@ -1,313 +1,985 @@
1
1
  // @vitest-environment node
2
- import { ChatStreamCallbacks, LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
3
- import { OpenAI } from 'openai';
4
- import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { testProvider } from '../../providerTestUtils';
6
+ import { LobeZhipuAI, params } from './index';
7
+
8
+ testProvider({
9
+ provider: 'zhipu',
10
+ defaultBaseURL: 'https://open.bigmodel.cn/api/paas/v4',
11
+ chatModel: 'glm-4',
12
+ Runtime: LobeZhipuAI,
13
+ chatDebugEnv: 'DEBUG_ZHIPU_CHAT_COMPLETION',
14
+ test: {
15
+ skipAPICall: true, // Skip because Zhipu has custom handlePayload that normalizes temperature
16
+ },
17
+ });
18
+
19
+ // Mock the console.error to avoid polluting test output
20
+ vi.spyOn(console, 'error').mockImplementation(() => {});
21
+
22
+ let instance: LobeOpenAICompatibleRuntime;
23
+
24
+ beforeEach(() => {
25
+ instance = new LobeZhipuAI({ apiKey: 'test' });
26
+
27
+ // Mock chat.completions.create
28
+ vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
29
+ new ReadableStream() as any,
30
+ );
31
+ });
5
32
 
6
- import * as debugStreamModule from '../../utils/debugStream';
7
- import { LobeZhipuAI } from './index';
33
+ afterEach(() => {
34
+ vi.clearAllMocks();
35
+ });
8
36
 
9
- const bizErrorType = 'ProviderBizError';
10
- const invalidErrorType = 'InvalidProviderAPIKey';
37
+ describe('LobeZhipuAI - custom features', () => {
38
+ describe('Debug Configuration', () => {
39
+ it('should disable debug by default', () => {
40
+ delete process.env.DEBUG_ZHIPU_CHAT_COMPLETION;
41
+ const result = params.debug.chatCompletion();
42
+ expect(result).toBe(false);
43
+ });
11
44
 
12
- describe('LobeZhipuAI', () => {
13
- afterEach(() => {
14
- vi.restoreAllMocks();
45
+ it('should enable debug when env is set', () => {
46
+ process.env.DEBUG_ZHIPU_CHAT_COMPLETION = '1';
47
+ const result = params.debug.chatCompletion();
48
+ expect(result).toBe(true);
49
+ delete process.env.DEBUG_ZHIPU_CHAT_COMPLETION;
50
+ });
15
51
  });
16
52
 
17
- describe('chat', () => {
18
- let instance: LobeOpenAICompatibleRuntime;
53
+ describe('handlePayload', () => {
54
+ describe('Web Search Feature', () => {
55
+ it('should add web_search tool when enabledSearch is true', async () => {
56
+ await instance.chat({
57
+ enabledSearch: true,
58
+ messages: [{ content: 'Hello', role: 'user' }],
59
+ model: 'glm-4',
60
+ temperature: 0,
61
+ });
19
62
 
20
- beforeEach(async () => {
21
- instance = new LobeZhipuAI({
22
- apiKey: 'test_api_key',
63
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
64
+ expect.objectContaining({
65
+ tools: expect.arrayContaining([
66
+ expect.objectContaining({
67
+ type: 'web_search',
68
+ web_search: expect.objectContaining({
69
+ enable: true,
70
+ result_sequence: 'before',
71
+ search_engine: 'search_std',
72
+ search_result: true,
73
+ }),
74
+ }),
75
+ ]),
76
+ }),
77
+ expect.anything(),
78
+ );
23
79
  });
24
80
 
25
- // Mock chat.completions.create
26
- vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
27
- new ReadableStream() as any,
28
- );
29
- });
81
+ it('should use custom search engine from env', async () => {
82
+ process.env.ZHIPU_SEARCH_ENGINE = 'search_pro';
30
83
 
31
- it('should return a StreamingTextResponse on successful API call', async () => {
32
- const result = await instance.chat({
33
- messages: [{ content: 'Hello', role: 'user' }],
34
- model: 'glm-4',
35
- temperature: 0,
84
+ const payload = params.chatCompletion.handlePayload({
85
+ enabledSearch: true,
86
+ messages: [{ content: 'Hello', role: 'user' }],
87
+ model: 'glm-4',
88
+ });
89
+
90
+ expect(payload.tools).toContainEqual(
91
+ expect.objectContaining({
92
+ type: 'web_search',
93
+ web_search: expect.objectContaining({
94
+ search_engine: 'search_pro',
95
+ }),
96
+ }),
97
+ );
98
+
99
+ delete process.env.ZHIPU_SEARCH_ENGINE;
100
+ });
101
+
102
+ it('should merge web_search with existing tools', async () => {
103
+ const existingTool = {
104
+ type: 'function' as const,
105
+ function: { name: 'test_tool', parameters: {} },
106
+ };
107
+
108
+ await instance.chat({
109
+ enabledSearch: true,
110
+ messages: [{ content: 'Hello', role: 'user' }],
111
+ model: 'glm-4',
112
+ temperature: 0,
113
+ tools: [existingTool],
114
+ });
115
+
116
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
117
+ expect.objectContaining({
118
+ tools: expect.arrayContaining([
119
+ existingTool,
120
+ expect.objectContaining({ type: 'web_search' }),
121
+ ]),
122
+ }),
123
+ expect.anything(),
124
+ );
125
+ });
126
+
127
+ it('should not add web_search tool when enabledSearch is false', async () => {
128
+ await instance.chat({
129
+ enabledSearch: false,
130
+ messages: [{ content: 'Hello', role: 'user' }],
131
+ model: 'glm-4',
132
+ temperature: 0,
133
+ });
134
+
135
+ const callArgs = (instance['client'].chat.completions.create as any).mock.calls[0][0];
136
+ expect(callArgs.tools).toBeUndefined();
137
+ });
138
+
139
+ it('should use existing tools without web_search when enabledSearch is not set', async () => {
140
+ const existingTool = {
141
+ type: 'function' as const,
142
+ function: { name: 'test_tool', parameters: {} },
143
+ };
144
+
145
+ await instance.chat({
146
+ messages: [{ content: 'Hello', role: 'user' }],
147
+ model: 'glm-4',
148
+ temperature: 0,
149
+ tools: [existingTool],
150
+ });
151
+
152
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ tools: [existingTool],
155
+ }),
156
+ expect.anything(),
157
+ );
36
158
  });
37
- expect(result).toBeInstanceOf(Response);
38
159
  });
39
160
 
40
- it('should handle callback and headers correctly', async () => {
41
- // 模拟 chat.completions.create 方法返回一个可读流
42
- const mockCreateMethod = vi
43
- .spyOn(instance['client'].chat.completions, 'create')
44
- .mockResolvedValue(
45
- new ReadableStream({
46
- start(controller) {
47
- controller.enqueue({
48
- id: 'chatcmpl-8xDx5AETP8mESQN7UB30GxTN2H1SO',
49
- object: 'chat.completion.chunk',
50
- created: 1709125675,
51
- model: 'gpt-3.5-turbo-0125',
52
- system_fingerprint: 'fp_86156a94a0',
53
- choices: [
54
- { index: 0, delta: { content: 'hello' }, logprobs: null, finish_reason: null },
55
- ],
56
- });
57
- controller.close();
58
- },
59
- }) as any,
161
+ describe('Model-specific max_tokens constraints', () => {
162
+ it('should limit max_tokens to 1024 for glm-4v models', async () => {
163
+ await instance.chat({
164
+ max_tokens: 2000,
165
+ messages: [{ content: 'Hello', role: 'user' }],
166
+ model: 'glm-4v',
167
+ temperature: 0.5,
168
+ });
169
+
170
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
171
+ expect.objectContaining({
172
+ max_tokens: 1024,
173
+ }),
174
+ expect.anything(),
60
175
  );
176
+ });
61
177
 
62
- // 准备 callback headers
63
- const mockCallback: ChatStreamCallbacks = {
64
- onStart: vi.fn(),
65
- onText: vi.fn(),
66
- };
67
- const mockHeaders = { 'Custom-Header': 'TestValue' };
178
+ it('should limit max_tokens to 15300 for glm-zero-preview model', async () => {
179
+ await instance.chat({
180
+ max_tokens: 20000,
181
+ messages: [{ content: 'Hello', role: 'user' }],
182
+ model: 'glm-zero-preview',
183
+ temperature: 0.5,
184
+ });
68
185
 
69
- // 执行测试
70
- const result = await instance.chat(
71
- {
186
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ max_tokens: 15_300,
189
+ }),
190
+ expect.anything(),
191
+ );
192
+ });
193
+
194
+ it('should allow max_tokens lower than constraint for glm-4v', async () => {
195
+ await instance.chat({
196
+ max_tokens: 512,
197
+ messages: [{ content: 'Hello', role: 'user' }],
198
+ model: 'glm-4v',
199
+ temperature: 0.5,
200
+ });
201
+
202
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
203
+ expect.objectContaining({
204
+ max_tokens: 512,
205
+ }),
206
+ expect.anything(),
207
+ );
208
+ });
209
+
210
+ it('should not limit max_tokens for other models', async () => {
211
+ await instance.chat({
212
+ max_tokens: 4000,
213
+ messages: [{ content: 'Hello', role: 'user' }],
214
+ model: 'glm-4',
215
+ temperature: 0.5,
216
+ });
217
+
218
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
219
+ expect.objectContaining({
220
+ max_tokens: 4000,
221
+ }),
222
+ expect.anything(),
223
+ );
224
+ });
225
+ });
226
+
227
+ describe('glm-4-alltools temperature and top_p constraints', () => {
228
+ it('should clamp temperature to [0.01, 0.99] for glm-4-alltools', async () => {
229
+ await instance.chat({
72
230
  messages: [{ content: 'Hello', role: 'user' }],
73
- model: 'text-davinci-003',
231
+ model: 'glm-4-alltools',
74
232
  temperature: 0,
75
- },
76
- { callback: mockCallback, headers: mockHeaders },
77
- );
233
+ });
234
+
235
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
236
+ expect.objectContaining({
237
+ temperature: 0.01,
238
+ }),
239
+ expect.anything(),
240
+ );
241
+ });
242
+
243
+ it('should clamp high temperature to 0.99 for glm-4-alltools', async () => {
244
+ await instance.chat({
245
+ messages: [{ content: 'Hello', role: 'user' }],
246
+ model: 'glm-4-alltools',
247
+ temperature: 2.0, // Will be normalized to 1.0, then clamped to 0.99
248
+ });
78
249
 
79
- // 验证 callback 被调用
80
- await result.text(); // 确保流被消费
81
- expect(mockCallback.onStart).toHaveBeenCalled();
82
- expect(mockCallback.onText).toHaveBeenCalledWith('hello');
250
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
251
+ expect.objectContaining({
252
+ temperature: 0.99,
253
+ }),
254
+ expect.anything(),
255
+ );
256
+ });
257
+
258
+ it('should clamp top_p to [0.01, 0.99] for glm-4-alltools', async () => {
259
+ await instance.chat({
260
+ messages: [{ content: 'Hello', role: 'user' }],
261
+ model: 'glm-4-alltools',
262
+ temperature: 0.5,
263
+ top_p: 0,
264
+ });
83
265
 
84
- // 验证 headers 被正确传递
85
- expect(result.headers.get('Custom-Header')).toEqual('TestValue');
266
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
267
+ expect.objectContaining({
268
+ top_p: 0.01,
269
+ }),
270
+ expect.anything(),
271
+ );
272
+ });
86
273
 
87
- // 清理
88
- mockCreateMethod.mockRestore();
274
+ it('should clamp high top_p to 0.99 for glm-4-alltools', async () => {
275
+ await instance.chat({
276
+ messages: [{ content: 'Hello', role: 'user' }],
277
+ model: 'glm-4-alltools',
278
+ temperature: 0.5,
279
+ top_p: 1,
280
+ });
281
+
282
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
283
+ expect.objectContaining({
284
+ top_p: 0.99,
285
+ }),
286
+ expect.anything(),
287
+ );
288
+ });
289
+
290
+ it('should normalize and preserve temperature in range for glm-4-alltools', async () => {
291
+ await instance.chat({
292
+ messages: [{ content: 'Hello', role: 'user' }],
293
+ model: 'glm-4-alltools',
294
+ temperature: 1.0, // Will be normalized to 0.5
295
+ });
296
+
297
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
298
+ expect.objectContaining({
299
+ temperature: 0.5,
300
+ }),
301
+ expect.anything(),
302
+ );
303
+ });
89
304
  });
90
305
 
91
- it('should transform messages correctly', async () => {
92
- const spyOn = vi.spyOn(instance['client'].chat.completions, 'create');
306
+ describe('Temperature normalization', () => {
307
+ it('should normalize temperature by dividing by 2', async () => {
308
+ await instance.chat({
309
+ messages: [{ content: 'Hello', role: 'user' }],
310
+ model: 'glm-4',
311
+ temperature: 1.0,
312
+ });
93
313
 
94
- await instance.chat({
95
- messages: [
96
- { content: 'Hello', role: 'user' },
97
- { content: [{ type: 'text', text: 'Hello again' }], role: 'user' },
98
- ],
99
- model: 'glm-4',
100
- temperature: 1.6,
101
- top_p: 1,
314
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
315
+ expect.objectContaining({
316
+ temperature: 0.5,
317
+ }),
318
+ expect.anything(),
319
+ );
102
320
  });
103
321
 
104
- const calledWithParams = spyOn.mock.calls[0][0];
322
+ it('should normalize high temperature', async () => {
323
+ await instance.chat({
324
+ messages: [{ content: 'Hello', role: 'user' }],
325
+ model: 'glm-4',
326
+ temperature: 1.6,
327
+ });
105
328
 
106
- expect(calledWithParams.messages[1].content).toEqual([{ type: 'text', text: 'Hello again' }]);
107
- expect(calledWithParams.temperature).toBe(0.8); // temperature should be divided by two
108
- expect(calledWithParams.top_p).toEqual(1);
329
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
330
+ expect.objectContaining({
331
+ temperature: 0.8,
332
+ }),
333
+ expect.anything(),
334
+ );
335
+ });
336
+
337
+ it('should handle temperature 0 correctly', async () => {
338
+ await instance.chat({
339
+ messages: [{ content: 'Hello', role: 'user' }],
340
+ model: 'glm-4',
341
+ temperature: 0,
342
+ });
343
+
344
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
345
+ expect.objectContaining({
346
+ temperature: 0,
347
+ }),
348
+ expect.anything(),
349
+ );
350
+ });
109
351
  });
110
352
 
111
- it('should pass arameters correctly', async () => {
112
- const spyOn = vi.spyOn(instance['client'].chat.completions, 'create');
353
+ describe('Thinking mode for GLM-4.5 models', () => {
354
+ it('should include thinking type for glm-4.5 models', async () => {
355
+ await instance.chat({
356
+ messages: [{ content: 'Hello', role: 'user' }],
357
+ model: 'glm-4.5',
358
+ temperature: 0.5,
359
+ thinking: { type: 'enabled', budget_tokens: 1000 },
360
+ });
361
+
362
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
363
+ expect.objectContaining({
364
+ thinking: { type: 'enabled' },
365
+ }),
366
+ expect.anything(),
367
+ );
368
+ });
369
+
370
+ it('should include thinking for glm-4.5-turbo', async () => {
371
+ await instance.chat({
372
+ messages: [{ content: 'Hello', role: 'user' }],
373
+ model: 'glm-4.5-turbo',
374
+ temperature: 0.5,
375
+ thinking: { type: 'disabled', budget_tokens: 0 },
376
+ });
377
+
378
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
379
+ expect.objectContaining({
380
+ thinking: { type: 'disabled' },
381
+ }),
382
+ expect.anything(),
383
+ );
384
+ });
385
+
386
+ it('should not include thinking for non-4.5 models', async () => {
387
+ await instance.chat({
388
+ messages: [{ content: 'Hello', role: 'user' }],
389
+ model: 'glm-4',
390
+ temperature: 0.5,
391
+ thinking: { type: 'enabled', budget_tokens: 1000 },
392
+ });
113
393
 
114
- await instance.chat({
115
- messages: [
116
- { content: 'Hello', role: 'user' },
117
- { content: [{ type: 'text', text: 'Hello again' }], role: 'user' },
118
- ],
119
- model: 'glm-4-alltools',
120
- temperature: 0,
121
- top_p: 1,
394
+ const callArgs = (instance['client'].chat.completions.create as any).mock.calls[0][0];
395
+ expect(callArgs.thinking).toBeUndefined();
122
396
  });
123
397
 
124
- const calledWithParams = spyOn.mock.calls[0][0];
398
+ it('should handle undefined thinking gracefully for 4.5 models', async () => {
399
+ await instance.chat({
400
+ messages: [{ content: 'Hello', role: 'user' }],
401
+ model: 'glm-4.5',
402
+ temperature: 0.5,
403
+ });
125
404
 
126
- expect(calledWithParams.messages[1].content).toEqual([{ type: 'text', text: 'Hello again' }]);
127
- expect(calledWithParams.temperature).toBe(0.01);
128
- expect(calledWithParams.top_p).toEqual(0.99);
405
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
406
+ expect.objectContaining({
407
+ thinking: { type: undefined },
408
+ }),
409
+ expect.anything(),
410
+ );
411
+ });
129
412
  });
130
413
 
131
- describe('Error', () => {
132
- it('should return ZhipuAIBizError with an openai error response when OpenAI.APIError is thrown', async () => {
133
- // Arrange
134
- const apiError = new OpenAI.APIError(
135
- 400,
136
- {
137
- status: 400,
138
- error: {
139
- message: 'Bad Request',
140
- },
141
- },
142
- 'Error message',
143
- {},
414
+ describe('Stream parameter', () => {
415
+ it('should always set stream to true', async () => {
416
+ await instance.chat({
417
+ messages: [{ content: 'Hello', role: 'user' }],
418
+ model: 'glm-4',
419
+ temperature: 0.5,
420
+ });
421
+
422
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
423
+ expect.objectContaining({
424
+ stream: true,
425
+ }),
426
+ expect.anything(),
144
427
  );
428
+ });
429
+
430
+ it('should override stream parameter to true', async () => {
431
+ await instance.chat({
432
+ messages: [{ content: 'Hello', role: 'user' }],
433
+ model: 'glm-4',
434
+ stream: false,
435
+ temperature: 0.5,
436
+ });
145
437
 
146
- vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
438
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
439
+ expect.objectContaining({
440
+ stream: true,
441
+ }),
442
+ expect.anything(),
443
+ );
444
+ });
445
+ });
446
+
447
+ describe('Preserve other payload properties', () => {
448
+ it('should preserve all other properties', async () => {
449
+ await instance.chat({
450
+ frequency_penalty: 0.5,
451
+ max_tokens: 100,
452
+ messages: [{ content: 'Hello', role: 'user' }],
453
+ model: 'glm-4',
454
+ presence_penalty: 0.3,
455
+ temperature: 0.5,
456
+ top_p: 0.9,
457
+ });
147
458
 
148
- // Act
149
- try {
150
- await instance.chat({
459
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
460
+ expect.objectContaining({
461
+ frequency_penalty: 0.5,
462
+ max_tokens: 100,
151
463
  messages: [{ content: 'Hello', role: 'user' }],
152
- model: 'text-davinci-003',
153
- temperature: 0,
154
- });
155
- } catch (e) {
156
- expect(e).toEqual({
157
- endpoint: 'https://open.bigmodel.cn/api/paas/v4',
158
- error: {
159
- error: { message: 'Bad Request' },
160
- status: 400,
161
- },
162
- errorType: bizErrorType,
163
- provider: 'zhipu',
164
- });
165
- }
464
+ model: 'glm-4',
465
+ presence_penalty: 0.3,
466
+ temperature: 0.25, // Normalized from 0.5
467
+ top_p: 0.9,
468
+ }),
469
+ expect.anything(),
470
+ );
166
471
  });
472
+ });
473
+ });
167
474
 
168
- it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => {
169
- try {
170
- new LobeZhipuAI({ apiKey: '' });
171
- } catch (e) {
172
- expect(e).toEqual({ errorType: invalidErrorType });
475
+ describe('handleStream', () => {
476
+ describe('Tool calls index fixing for GLM-4.5', () => {
477
+ it('should fix negative tool_calls index to positive', async () => {
478
+ const mockStream = new ReadableStream({
479
+ start(controller) {
480
+ controller.enqueue({
481
+ choices: [
482
+ {
483
+ delta: {
484
+ tool_calls: [
485
+ { index: -1, id: 'call_1', function: { name: 'tool1', arguments: '{}' } },
486
+ { index: -1, id: 'call_2', function: { name: 'tool2', arguments: '{}' } },
487
+ ],
488
+ },
489
+ finish_reason: null,
490
+ index: 0,
491
+ },
492
+ ],
493
+ created: 1234567890,
494
+ id: 'chatcmpl-123',
495
+ model: 'glm-4.5',
496
+ object: 'chat.completion.chunk',
497
+ });
498
+ controller.close();
499
+ },
500
+ });
501
+
502
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
503
+
504
+ const result = await instance.chat({
505
+ messages: [{ content: 'Hello', role: 'user' }],
506
+ model: 'glm-4.5',
507
+ temperature: 0.5,
508
+ });
509
+
510
+ // Read the stream to trigger the transform
511
+ const reader = result.body?.getReader();
512
+ const chunks = [];
513
+ if (reader) {
514
+ let done = false;
515
+ while (!done) {
516
+ const { value, done: isDone } = await reader.read();
517
+ done = isDone;
518
+ if (value) {
519
+ chunks.push(new TextDecoder().decode(value));
520
+ }
521
+ }
173
522
  }
523
+
524
+ // The transform should have fixed the negative indices
525
+ expect(chunks).toBeDefined();
174
526
  });
175
527
 
176
- it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
177
- // Arrange
178
- const errorInfo = {
179
- stack: 'abc',
180
- cause: {
181
- message: 'api is undefined',
528
+ it('should preserve positive tool_calls index', async () => {
529
+ const mockStream = new ReadableStream({
530
+ start(controller) {
531
+ controller.enqueue({
532
+ choices: [
533
+ {
534
+ delta: {
535
+ tool_calls: [
536
+ { index: 0, id: 'call_1', function: { name: 'tool1', arguments: '{}' } },
537
+ { index: 1, id: 'call_2', function: { name: 'tool2', arguments: '{}' } },
538
+ ],
539
+ },
540
+ finish_reason: null,
541
+ index: 0,
542
+ },
543
+ ],
544
+ created: 1234567890,
545
+ id: 'chatcmpl-123',
546
+ model: 'glm-4',
547
+ object: 'chat.completion.chunk',
548
+ });
549
+ controller.close();
182
550
  },
183
- };
184
- const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
551
+ });
185
552
 
186
- vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
553
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
187
554
 
188
- // Act
189
- try {
190
- await instance.chat({
191
- messages: [{ content: 'Hello', role: 'user' }],
192
- model: 'text-davinci-003',
193
- temperature: 0.2,
194
- });
195
- } catch (e) {
196
- expect(e).toEqual({
197
- endpoint: 'https://open.bigmodel.cn/api/paas/v4',
198
- error: {
199
- cause: { message: 'api is undefined' },
200
- stack: 'abc',
201
- },
202
- errorType: bizErrorType,
203
- provider: 'zhipu',
204
- });
555
+ const result = await instance.chat({
556
+ messages: [{ content: 'Hello', role: 'user' }],
557
+ model: 'glm-4',
558
+ temperature: 0.5,
559
+ });
560
+
561
+ // Read the stream
562
+ const reader = result.body?.getReader();
563
+ if (reader) {
564
+ let done = false;
565
+ while (!done) {
566
+ const { value, done: isDone } = await reader.read();
567
+ done = isDone;
568
+ }
205
569
  }
570
+
571
+ expect(result).toBeDefined();
206
572
  });
207
573
 
208
- it('should return OpenAIBizError with an cause response with desensitize Url', async () => {
209
- // Arrange
210
- const errorInfo = {
211
- stack: 'abc',
212
- cause: { message: 'api is undefined' },
213
- };
214
- const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
574
+ it('should handle chunks without tool_calls', async () => {
575
+ const mockStream = new ReadableStream({
576
+ start(controller) {
577
+ controller.enqueue({
578
+ choices: [
579
+ {
580
+ delta: {
581
+ content: 'Hello',
582
+ },
583
+ finish_reason: null,
584
+ index: 0,
585
+ },
586
+ ],
587
+ created: 1234567890,
588
+ id: 'chatcmpl-123',
589
+ model: 'glm-4',
590
+ object: 'chat.completion.chunk',
591
+ });
592
+ controller.close();
593
+ },
594
+ });
215
595
 
216
- instance = new LobeZhipuAI({
217
- apiKey: 'test',
596
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
218
597
 
219
- baseURL: 'https://abc.com/v2',
598
+ const result = await instance.chat({
599
+ messages: [{ content: 'Hello', role: 'user' }],
600
+ model: 'glm-4',
601
+ temperature: 0.5,
220
602
  });
221
603
 
222
- vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
604
+ expect(result).toBeInstanceOf(Response);
605
+ });
223
606
 
224
- // Act
225
- try {
226
- await instance.chat({
227
- messages: [{ content: 'Hello', role: 'user' }],
228
- model: 'gpt-3.5-turbo',
229
- temperature: 0,
230
- });
231
- } catch (e) {
232
- expect(e).toEqual({
233
- endpoint: 'https://***.com/v2',
234
- error: {
235
- cause: { message: 'api is undefined' },
236
- stack: 'abc',
237
- },
238
- errorType: bizErrorType,
239
- provider: 'zhipu',
240
- });
241
- }
607
+ it('should handle chunks without choices', async () => {
608
+ const mockStream = new ReadableStream({
609
+ start(controller) {
610
+ controller.enqueue({
611
+ created: 1234567890,
612
+ id: 'chatcmpl-123',
613
+ model: 'glm-4',
614
+ object: 'chat.completion.chunk',
615
+ });
616
+ controller.close();
617
+ },
618
+ });
619
+
620
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
621
+
622
+ const result = await instance.chat({
623
+ messages: [{ content: 'Hello', role: 'user' }],
624
+ model: 'glm-4',
625
+ temperature: 0.5,
626
+ });
627
+
628
+ expect(result).toBeInstanceOf(Response);
242
629
  });
243
630
 
244
- it('should return AgentRuntimeError for non-OpenAI errors', async () => {
245
- // Arrange
246
- const genericError = new Error('Generic Error');
631
+ it('should handle empty tool_calls array', async () => {
632
+ const mockStream = new ReadableStream({
633
+ start(controller) {
634
+ controller.enqueue({
635
+ choices: [
636
+ {
637
+ delta: {
638
+ tool_calls: [],
639
+ },
640
+ finish_reason: null,
641
+ index: 0,
642
+ },
643
+ ],
644
+ created: 1234567890,
645
+ id: 'chatcmpl-123',
646
+ model: 'glm-4',
647
+ object: 'chat.completion.chunk',
648
+ });
649
+ controller.close();
650
+ },
651
+ });
247
652
 
248
- vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
653
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
249
654
 
250
- // Act
251
- try {
252
- await instance.chat({
253
- messages: [{ content: 'Hello', role: 'user' }],
254
- model: 'text-davinci-003',
255
- temperature: 0,
256
- });
257
- } catch (e) {
258
- expect(e).toEqual({
259
- endpoint: 'https://open.bigmodel.cn/api/paas/v4',
260
- errorType: 'AgentRuntimeError',
261
- provider: 'zhipu',
262
- error: {
263
- name: genericError.name,
264
- cause: genericError.cause,
265
- message: genericError.message,
266
- },
267
- });
268
- }
655
+ const result = await instance.chat({
656
+ messages: [{ content: 'Hello', role: 'user' }],
657
+ model: 'glm-4',
658
+ temperature: 0.5,
659
+ });
660
+
661
+ expect(result).toBeInstanceOf(Response);
269
662
  });
270
- });
271
663
 
272
- describe('DEBUG', () => {
273
- it('should call debugStream and return StreamingTextResponse when DEBUG_OPENAI_CHAT_COMPLETION is 1', async () => {
274
- // Arrange
275
- const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
276
- const mockDebugStream = new ReadableStream({
664
+ it('should handle mixed tool_calls indices', async () => {
665
+ const mockStream = new ReadableStream({
277
666
  start(controller) {
278
- controller.enqueue('Debug stream content');
667
+ controller.enqueue({
668
+ choices: [
669
+ {
670
+ delta: {
671
+ tool_calls: [
672
+ { index: 0, id: 'call_1', function: { name: 'tool1', arguments: '{}' } },
673
+ { index: -1, id: 'call_2', function: { name: 'tool2', arguments: '{}' } },
674
+ { index: 2, id: 'call_3', function: { name: 'tool3', arguments: '{}' } },
675
+ ],
676
+ },
677
+ finish_reason: null,
678
+ index: 0,
679
+ },
680
+ ],
681
+ created: 1234567890,
682
+ id: 'chatcmpl-123',
683
+ model: 'glm-4.5',
684
+ object: 'chat.completion.chunk',
685
+ });
279
686
  controller.close();
280
687
  },
281
- }) as any;
282
- mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
688
+ });
283
689
 
284
- // 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
285
- (instance['client'].chat.completions.create as Mock).mockResolvedValue({
286
- tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
690
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
691
+
692
+ const result = await instance.chat({
693
+ messages: [{ content: 'Hello', role: 'user' }],
694
+ model: 'glm-4.5',
695
+ temperature: 0.5,
287
696
  });
288
697
 
289
- // 保存原始环境变量值
290
- const originalDebugValue = process.env.DEBUG_ZHIPU_CHAT_COMPLETION;
698
+ // Read the stream to trigger the transform
699
+ const reader = result.body?.getReader();
700
+ if (reader) {
701
+ let done = false;
702
+ while (!done) {
703
+ const { value, done: isDone } = await reader.read();
704
+ done = isDone;
705
+ }
706
+ }
291
707
 
292
- // 模拟环境变量
293
- process.env.DEBUG_ZHIPU_CHAT_COMPLETION = '1';
294
- vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
708
+ expect(result).toBeDefined();
709
+ });
295
710
 
296
- // 执行测试
297
- // 运行你的测试函数,确保它会在条件满足时调用 debugStream
298
- // 假设的测试函数调用,你可能需要根据实际情况调整
299
- await instance.chat({
711
+ it('should handle multiple chunks with tool_calls', async () => {
712
+ const mockStream = new ReadableStream({
713
+ start(controller) {
714
+ // First chunk with tool_call
715
+ controller.enqueue({
716
+ choices: [
717
+ {
718
+ delta: {
719
+ tool_calls: [{ index: -1, id: 'call_1', function: { name: 'tool1' } }],
720
+ },
721
+ finish_reason: null,
722
+ index: 0,
723
+ },
724
+ ],
725
+ created: 1234567890,
726
+ id: 'chatcmpl-123',
727
+ model: 'glm-4.5',
728
+ object: 'chat.completion.chunk',
729
+ });
730
+ // Second chunk with arguments
731
+ controller.enqueue({
732
+ choices: [
733
+ {
734
+ delta: {
735
+ tool_calls: [{ index: -1, function: { arguments: '{"a":1}' } }],
736
+ },
737
+ finish_reason: null,
738
+ index: 0,
739
+ },
740
+ ],
741
+ created: 1234567890,
742
+ id: 'chatcmpl-123',
743
+ model: 'glm-4.5',
744
+ object: 'chat.completion.chunk',
745
+ });
746
+ controller.close();
747
+ },
748
+ });
749
+
750
+ (instance['client'].chat.completions.create as any).mockResolvedValue(mockStream);
751
+
752
+ const result = await instance.chat({
300
753
  messages: [{ content: 'Hello', role: 'user' }],
301
- model: 'text-davinci-003',
302
- temperature: 0,
754
+ model: 'glm-4.5',
755
+ temperature: 0.5,
303
756
  });
304
757
 
305
- // 验证 debugStream 被调用
306
- expect(debugStreamModule.debugStream).toHaveBeenCalled();
758
+ // Read the stream
759
+ const reader = result.body?.getReader();
760
+ if (reader) {
761
+ let done = false;
762
+ while (!done) {
763
+ const { value, done: isDone } = await reader.read();
764
+ done = isDone;
765
+ }
766
+ }
767
+
768
+ expect(result).toBeDefined();
769
+ });
770
+ });
771
+ });
772
+
773
+ describe('exports', () => {
774
+ it('should export params object', () => {
775
+ expect(params).toBeDefined();
776
+ expect(params.provider).toBe('zhipu');
777
+ expect(params.baseURL).toBe('https://open.bigmodel.cn/api/paas/v4');
778
+ expect(params.chatCompletion).toBeDefined();
779
+ expect(params.debug).toBeDefined();
780
+ expect(params.models).toBeDefined();
781
+ });
782
+
783
+ it('should export chatCompletion configuration with handlePayload', () => {
784
+ expect(params.chatCompletion.handlePayload).toBeDefined();
785
+ expect(typeof params.chatCompletion.handlePayload).toBe('function');
786
+ });
787
+
788
+ it('should export chatCompletion configuration with handleStream', () => {
789
+ expect(params.chatCompletion.handleStream).toBeDefined();
790
+ expect(typeof params.chatCompletion.handleStream).toBe('function');
791
+ });
792
+
793
+ it('should export debug configuration', () => {
794
+ expect(params.debug.chatCompletion).toBeDefined();
795
+ expect(typeof params.debug.chatCompletion).toBe('function');
796
+ });
797
+
798
+ it('should export models function', () => {
799
+ expect(params.models).toBeDefined();
800
+ expect(typeof params.models).toBe('function');
801
+ });
802
+ });
803
+
804
+ describe('models', () => {
805
+ const mockFetch = vi.fn();
806
+ const originalFetch = global.fetch;
807
+
808
+ beforeEach(() => {
809
+ global.fetch = mockFetch;
810
+ vi.clearAllMocks();
811
+ });
812
+
813
+ afterEach(() => {
814
+ global.fetch = originalFetch;
815
+ });
816
+
817
+ it('should fetch and process models with correct headers', async () => {
818
+ mockFetch.mockResolvedValue({
819
+ json: async () => ({
820
+ rows: [
821
+ {
822
+ description: 'GLM-4 model',
823
+ modelCode: 'glm-4',
824
+ modelName: 'GLM-4',
825
+ },
826
+ {
827
+ description: 'GLM-4V model',
828
+ modelCode: 'glm-4v',
829
+ modelName: 'GLM-4V',
830
+ },
831
+ ],
832
+ }),
833
+ });
834
+
835
+ const mockClient = { apiKey: 'test_api_key' };
836
+ await params.models({ client: mockClient as any });
837
+
838
+ expect(mockFetch).toHaveBeenCalledWith(
839
+ 'https://open.bigmodel.cn/api/fine-tuning/model_center/list?pageSize=100&pageNum=1',
840
+ {
841
+ headers: {
842
+ 'Authorization': 'Bearer test_api_key',
843
+ 'Bigmodel-Organization': 'lobehub',
844
+ 'Bigmodel-Project': 'lobechat',
845
+ },
846
+ method: 'GET',
847
+ },
848
+ );
849
+ });
850
+
851
+ it('should process model list correctly', async () => {
852
+ mockFetch.mockResolvedValue({
853
+ json: async () => ({
854
+ rows: [
855
+ {
856
+ description: 'GLM-4 model',
857
+ modelCode: 'glm-4',
858
+ modelName: 'GLM-4',
859
+ },
860
+ ],
861
+ }),
862
+ });
863
+
864
+ const mockClient = { apiKey: 'test_api_key' };
865
+ const models = await params.models({ client: mockClient as any });
866
+
867
+ expect(models).toBeDefined();
868
+ expect(Array.isArray(models)).toBe(true);
869
+ });
870
+
871
+ it('should transform modelCode to id and modelName to displayName', async () => {
872
+ mockFetch.mockResolvedValue({
873
+ json: async () => ({
874
+ rows: [
875
+ {
876
+ description: 'Test model',
877
+ modelCode: 'test-model-code',
878
+ modelName: 'Test Model Name',
879
+ },
880
+ ],
881
+ }),
882
+ });
883
+
884
+ const mockClient = { apiKey: 'test_api_key' };
885
+ const models = await params.models({ client: mockClient as any });
886
+
887
+ // processModelList will merge with LOBE_DEFAULT_MODEL_LIST
888
+ // Check that fetch was called and data was processed
889
+ expect(mockFetch).toHaveBeenCalled();
890
+ expect(models).toBeDefined();
891
+ });
892
+
893
+ it('should handle empty model list', async () => {
894
+ mockFetch.mockResolvedValue({
895
+ json: async () => ({
896
+ rows: [],
897
+ }),
898
+ });
899
+
900
+ const mockClient = { apiKey: 'test_api_key' };
901
+ const models = await params.models({ client: mockClient as any });
902
+
903
+ expect(models).toEqual([]);
904
+ });
905
+
906
+ it('should handle multiple models', async () => {
907
+ mockFetch.mockResolvedValue({
908
+ json: async () => ({
909
+ rows: [
910
+ {
911
+ description: 'GLM-4 model',
912
+ modelCode: 'glm-4',
913
+ modelName: 'GLM-4',
914
+ },
915
+ {
916
+ description: 'GLM-4V model',
917
+ modelCode: 'glm-4v',
918
+ modelName: 'GLM-4V',
919
+ },
920
+ {
921
+ description: 'GLM-4-Air model',
922
+ modelCode: 'glm-4-air',
923
+ modelName: 'GLM-4-Air',
924
+ },
925
+ ],
926
+ }),
927
+ });
928
+
929
+ const mockClient = { apiKey: 'test_api_key' };
930
+ const models = await params.models({ client: mockClient as any });
931
+
932
+ expect(models).toBeDefined();
933
+ expect(Array.isArray(models)).toBe(true);
934
+ });
307
935
 
308
- // 恢复原始环境变量值
309
- process.env.DEBUG_ZHIPU_CHAT_COMPLETION = originalDebugValue;
936
+ it('should include description in model data', async () => {
937
+ mockFetch.mockResolvedValue({
938
+ json: async () => ({
939
+ rows: [
940
+ {
941
+ description: 'This is a test description',
942
+ modelCode: 'glm-4',
943
+ modelName: 'GLM-4',
944
+ },
945
+ ],
946
+ }),
310
947
  });
948
+
949
+ const mockClient = { apiKey: 'test_api_key' };
950
+ await params.models({ client: mockClient as any });
951
+
952
+ // Verify the API was called correctly
953
+ expect(mockFetch).toHaveBeenCalledWith(
954
+ expect.any(String),
955
+ expect.objectContaining({
956
+ headers: expect.objectContaining({
957
+ Authorization: 'Bearer test_api_key',
958
+ }),
959
+ }),
960
+ );
961
+ });
962
+
963
+ it('should handle API errors gracefully', async () => {
964
+ mockFetch.mockRejectedValue(new Error('API Error'));
965
+
966
+ const mockClient = { apiKey: 'test_api_key' };
967
+
968
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow('API Error');
969
+ });
970
+
971
+ it('should use correct API endpoint', async () => {
972
+ mockFetch.mockResolvedValue({
973
+ json: async () => ({ rows: [] }),
974
+ });
975
+
976
+ const mockClient = { apiKey: 'test_api_key' };
977
+ await params.models({ client: mockClient as any });
978
+
979
+ expect(mockFetch).toHaveBeenCalledWith(
980
+ 'https://open.bigmodel.cn/api/fine-tuning/model_center/list?pageSize=100&pageNum=1',
981
+ expect.anything(),
982
+ );
311
983
  });
312
984
  });
313
985
  });