@lobehub/chat 1.34.6 → 1.35.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.35.0](https://github.com/lobehub/lobe-chat/compare/v1.34.6...v1.35.0)
6
+
7
+ <sup>Released on **2024-12-01**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Support ollama tools use.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Support ollama tools use, closes [#3327](https://github.com/lobehub/lobe-chat/issues/3327) ([72d8835](https://github.com/lobehub/lobe-chat/commit/72d8835))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.34.6](https://github.com/lobehub/lobe-chat/compare/v1.34.5...v1.34.6)
6
31
 
7
32
  <sup>Released on **2024-12-01**</sup>
package/README.md CHANGED
@@ -285,14 +285,14 @@ Our marketplace is not just a showcase platform but also a collaborative space.
285
285
 
286
286
  <!-- AGENT LIST -->
287
287
 
288
- | Recent Submits | Description |
289
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
290
- | [Backend Development Assistant](https://chat-preview.lobehub.com/market?agent=backend-assistant)<br/><sup>By **[zeno980](https://github.com/zeno980)** on **2024-11-26**</sup> | Skilled in backend development tasks<br/>`backend-development` `ai-technology` `web-applications` `spring` `sql` |
291
- | [Bilingual Dictionary Expert](https://chat-preview.lobehub.com/market?agent=english-chinese-dictionary-expert)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-11-26**</sup> | Expert in bilingual English-Chinese vocabulary translation and analysis<br/>`translation` `language-learning` `vocabulary` `dictionary` |
292
- | [SSC Incremental](https://chat-preview.lobehub.com/market?agent=great-for-analysis-coding-and-rubber-ducking)<br/><sup>By **[Base03](https://github.com/Base03)** on **2024-11-26**</sup> | Claude minus the Reddit<br/>`technology` `analysis` `software` `ai` `research` |
293
- | [Interviewer's Assistant](https://chat-preview.lobehub.com/market?agent=interviewer-assistant)<br/><sup>By **[xandertang](https://github.com/Dr-T)** on **2024-11-26**</sup> | Proficient in designing and evaluating interview questions for product managers, generating interview questions based on resume interpretation results.<br/>`interview` `resume` `recruitment` `efficiency` |
294
-
295
- > 📊 Total agents: [<kbd>**446**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
288
+ | Recent Submits | Description |
289
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
290
+ | [AI Assistant for Course Content and Teaching Guidelines](https://chat-preview.lobehub.com/market?agent=course-prep-teaching-guide-ai)<br/><sup>By **[HNaga](https://github.com/HNaga)** on **2024-11-29**</sup> | This AI assistant is designed to help educators and instructors prepare comprehensive course content and provide practical teaching guidelines. It leverages advanced NLP capabilities to generate lesson plans, suggest engaging teaching strategies, and offer insights into educational best practices.<br/>`education` `teaching` `course-design` `content-creation` `ai-assistance` `curriculum-development` `instructional-design` |
291
+ | [Backend Development Assistant](https://chat-preview.lobehub.com/market?agent=backend-assistant)<br/><sup>By **[zeno980](https://github.com/zeno980)** on **2024-11-26**</sup> | Skilled in backend development tasks<br/>`backend-development` `ai-technology` `web-applications` `spring` `sql` |
292
+ | [Bilingual Dictionary Expert](https://chat-preview.lobehub.com/market?agent=english-chinese-dictionary-expert)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-11-26**</sup> | Expert in bilingual English-Chinese vocabulary translation and analysis<br/>`translation` `language-learning` `vocabulary` `dictionary` |
293
+ | [SSC Incremental](https://chat-preview.lobehub.com/market?agent=great-for-analysis-coding-and-rubber-ducking)<br/><sup>By **[Base03](https://github.com/Base03)** on **2024-11-26**</sup> | Claude minus the Reddit<br/>`technology` `analysis` `software` `ai` `research` |
294
+
295
+ > 📊 Total agents: [<kbd>**447**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
296
296
 
297
297
  <!-- AGENT LIST -->
298
298
 
package/README.zh-CN.md CHANGED
@@ -274,14 +274,14 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
274
274
 
275
275
  <!-- AGENT LIST -->
276
276
 
277
- | 最近新增 | 助手说明 |
278
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
279
- | [后端开发助手](https://chat-preview.lobehub.com/market?agent=backend-assistant)<br/><sup>By **[zeno980](https://github.com/zeno980)** on **2024-11-26**</sup> | 擅长后端开发任务<br/>`后端开发` `ai技术` `web应用` `spring` `sql` |
280
- | [双语词典专家](https://chat-preview.lobehub.com/market?agent=english-chinese-dictionary-expert)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-11-26**</sup> | 双语英语 - 中文词汇翻译和分析专家<br/>`翻译` `语言学习` `词汇` `词典` |
281
- | [SSC 增量](https://chat-preview.lobehub.com/market?agent=great-for-analysis-coding-and-rubber-ducking)<br/><sup>By **[Base03](https://github.com/Base03)** on **2024-11-26**</sup> | Claude 减去 Reddit<br/>`技术` `分析` `软件` `人工智能` `研究` |
282
- | [面试官助手](https://chat-preview.lobehub.com/market?agent=interviewer-assistant)<br/><sup>By **[xandertang](https://github.com/Dr-T)** on **2024-11-26**</sup> | Proficient in designing and evaluating interview questions for product managers, generating interview questions based on resume interpretation results.<br/>`面试` `简历` `招聘` `效率` |
283
-
284
- > 📊 Total agents: [<kbd>**446**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
277
+ | 最近新增 | 助手说明 |
278
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
279
+ | [课程内容和教学指南的 AI 助手](https://chat-preview.lobehub.com/market?agent=course-prep-teaching-guide-ai)<br/><sup>By **[HNaga](https://github.com/HNaga)** on **2024-11-29**</sup> | 这个 AI 助手旨在帮助教育工作者和讲师准备全面的课程内容并提供实用的教学指南。它利用先进的自然语言处理能力生成课程计划,建议引人入胜的教学策略,并提供教育最佳实践的见解。<br/>`教育` `教学` `课程设计` `内容创作` `人工智能助手` `课程开发` `教学设计` |
280
+ | [后端开发助手](https://chat-preview.lobehub.com/market?agent=backend-assistant)<br/><sup>By **[zeno980](https://github.com/zeno980)** on **2024-11-26**</sup> | 擅长后端开发任务<br/>`后端开发` `ai技术` `web应用` `spring` `sql` |
281
+ | [双语词典专家](https://chat-preview.lobehub.com/market?agent=english-chinese-dictionary-expert)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-11-26**</sup> | 双语英语 - 中文词汇翻译和分析专家<br/>`翻译` `语言学习` `词汇` `词典` |
282
+ | [SSC 增量](https://chat-preview.lobehub.com/market?agent=great-for-analysis-coding-and-rubber-ducking)<br/><sup>By **[Base03](https://github.com/Base03)** on **2024-11-26**</sup> | Claude 减去 Reddit<br/>`技术` `分析` `软件` `人工智能` `研究` |
283
+
284
+ > 📊 Total agents: [<kbd>**447**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
285
285
 
286
286
  <!-- AGENT LIST -->
287
287
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Support ollama tools use."
6
+ ]
7
+ },
8
+ "date": "2024-12-01",
9
+ "version": "1.35.0"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
@@ -80,5 +80,34 @@ We have integrated several free/open-source data analytics services in LobeChat
80
80
  - Default: `https://analytics.umami.is/script.js`
81
81
  - Example: `https://umami.your-site.com/script.js`
82
82
 
83
- [posthog-analytics-url]: https://posthog.com
84
- [umami-analytics-url]: https://umami.is
83
+ ## Langfuse Observability
84
+
85
+ [Langfuse](https://langfuse.com/) is an [open-source](https://github.com/langfuse/langfuse) LLM Observability platform. By enabling the Langfuse integration, you can trace your chat data with Langfuse to develop, monitor, and evaluate the use of your LobeChat.
86
+
87
+ ### `ENABLE_LANGFUSE`
88
+
89
+ - Type: Required
90
+ - Description: Determines if Langfuse analytics is enabled.
91
+ - Default: `1`
92
+ - Example: `1`
93
+
94
+ ### `LANGFUSE_SECRET_KEY`
95
+
96
+ - Type: Required
97
+ - Description: Langfuse API secret key. Can be created by signing up for [Langfuse Cloud](https://cloud.langfuse.com) or by self-hosting Langfuse.
98
+ - Default: \`\`
99
+ - Example: `sk-lf-...`
100
+
101
+ ### `LANGFUSE_PUBLIC_KEY`
102
+
103
+ - Type: Required
104
+ - Description: Langfuse API public key. Can be created by signing up for [Langfuse Cloud](https://cloud.langfuse.com) or by self-hosting Langfuse.
105
+ - Default: \`\`
106
+ - Example: `pk-lf-...`
107
+
108
+ ### `LANGFUSE_HOST`
109
+
110
+ - Type: Required
111
+ - Description: Langfuse host address. Use `https://us.cloud.langfuse.com` if your Langfuse project is in the US data region.
112
+ - Default: `https://cloud.langfuse.com`
113
+ - Example: `https://cloud.langfuse.com`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.34.6",
3
+ "version": "1.35.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -181,7 +181,7 @@
181
181
  "numeral": "^2.0.6",
182
182
  "nuqs": "^1.20.0",
183
183
  "officeparser": "^4.2.0",
184
- "ollama": "^0.5.9",
184
+ "ollama": "^0.5.10",
185
185
  "openai": "^4.68.1",
186
186
  "openapi-fetch": "^0.9.8",
187
187
  "partial-json": "^0.1.7",
@@ -1,6 +1,5 @@
1
1
  import { ModelProviderCard } from '@/types/llm';
2
2
 
3
- // ref: https://ollama.com/library
4
3
  const Ollama: ModelProviderCard = {
5
4
  chatModels: [
6
5
  {
@@ -8,6 +7,7 @@ const Ollama: ModelProviderCard = {
8
7
  'Llama 3.1 是 Meta 推出的领先模型,支持高达 405B 参数,可应用于复杂对话、多语言翻译和数据分析领域。',
9
8
  displayName: 'Llama 3.1 8B',
10
9
  enabled: true,
10
+ functionCall: true,
11
11
  id: 'llama3.1',
12
12
  tokens: 128_000,
13
13
  },
@@ -221,28 +221,29 @@ const Ollama: ModelProviderCard = {
221
221
  tokens: 128_000,
222
222
  },
223
223
  {
224
- description: 'Qwen2 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
225
- displayName: 'Qwen2 0.5B',
226
- id: 'qwen2:0.5b',
224
+ description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
225
+ displayName: 'Qwen2.5 0.5B',
226
+ id: 'qwen2.5:0.5b',
227
227
  tokens: 128_000,
228
228
  },
229
229
  {
230
- description: 'Qwen2 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
231
- displayName: 'Qwen2 1.5B',
232
- id: 'qwen2:1.5b',
230
+ description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
231
+ displayName: 'Qwen2.5 1.5B',
232
+ id: 'qwen2.5:1.5b',
233
233
  tokens: 128_000,
234
234
  },
235
235
  {
236
- description: 'Qwen2 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
237
- displayName: 'Qwen2 7B',
236
+ description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
237
+ displayName: 'Qwen2.5 7B',
238
238
  enabled: true,
239
- id: 'qwen2',
239
+ functionCall: true,
240
+ id: 'qwen2.5',
240
241
  tokens: 128_000,
241
242
  },
242
243
  {
243
- description: 'Qwen2 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
244
- displayName: 'Qwen2 72B',
245
- id: 'qwen2:72b',
244
+ description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
245
+ displayName: 'Qwen2.5 72B',
246
+ id: 'qwen2.5:72b',
246
247
  tokens: 128_000,
247
248
  },
248
249
  {
@@ -1,4 +1,4 @@
1
- import { Ollama } from 'ollama/browser';
1
+ import { Ollama, Tool } from 'ollama/browser';
2
2
  import { ClientOptions } from 'openai';
3
3
 
4
4
  import { OpenAIChatMessage } from '@/libs/agent-runtime';
@@ -8,8 +8,9 @@ import { LobeRuntimeAI } from '../BaseAI';
8
8
  import { AgentRuntimeErrorType } from '../error';
9
9
  import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
10
10
  import { AgentRuntimeError } from '../utils/createError';
11
+ import { debugStream } from '../utils/debugStream';
11
12
  import { StreamingResponse } from '../utils/response';
12
- import { OllamaStream } from '../utils/streams';
13
+ import { OllamaStream, convertIterableToStream } from '../utils/streams';
13
14
  import { parseDataUri } from '../utils/uriParser';
14
15
  import { OllamaMessage } from './type';
15
16
 
@@ -45,23 +46,38 @@ export class LobeOllamaAI implements LobeRuntimeAI {
45
46
  options: {
46
47
  frequency_penalty: payload.frequency_penalty,
47
48
  presence_penalty: payload.presence_penalty,
48
- temperature:
49
- payload.temperature !== undefined
50
- ? payload.temperature / 2
51
- : undefined,
49
+ temperature: payload.temperature !== undefined ? payload.temperature / 2 : undefined,
52
50
  top_p: payload.top_p,
53
51
  },
54
52
  stream: true,
53
+ tools: payload.tools as Tool[],
55
54
  });
56
55
 
57
- return StreamingResponse(OllamaStream(response, options?.callback), {
56
+ const stream = convertIterableToStream(response);
57
+ const [prod, debug] = stream.tee();
58
+
59
+ if (process.env.DEBUG_OLLAMA_CHAT_COMPLETION === '1') {
60
+ debugStream(debug).catch(console.error);
61
+ }
62
+
63
+ return StreamingResponse(OllamaStream(prod, options?.callback), {
58
64
  headers: options?.headers,
59
65
  });
60
66
  } catch (error) {
61
- const e = error as { message: string; name: string; status_code: number };
67
+ const e = error as {
68
+ error: any;
69
+ message: string;
70
+ name: string;
71
+ status_code: number;
72
+ };
62
73
 
63
74
  throw AgentRuntimeError.chat({
64
- error: { message: e.message, name: e.name, status_code: e.status_code },
75
+ error: {
76
+ ...e.error,
77
+ message: String(e.error?.message || e.message),
78
+ name: e.name,
79
+ status_code: e.status_code,
80
+ },
65
81
  errorType: AgentRuntimeErrorType.OllamaBizError,
66
82
  provider: ModelProvider.Ollama,
67
83
  });
@@ -6,61 +6,145 @@ import * as uuidModule from '@/utils/uuid';
6
6
  import { OllamaStream } from './ollama';
7
7
 
8
8
  describe('OllamaStream', () => {
9
- it('should transform Ollama stream to protocol stream', async () => {
10
- vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
9
+ describe('should transform Ollama stream to protocol stream', () => {
10
+ it('text', async () => {
11
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
12
+
13
+ const mockOllamaStream = new ReadableStream<ChatResponse>({
14
+ start(controller) {
15
+ controller.enqueue({ message: { content: 'Hello' }, done: false } as ChatResponse);
16
+ controller.enqueue({ message: { content: ' world!' }, done: false } as ChatResponse);
17
+ controller.enqueue({ message: { content: '' }, done: true } as ChatResponse);
18
+
19
+ controller.close();
20
+ },
21
+ });
22
+
23
+ const onStartMock = vi.fn();
24
+ const onTextMock = vi.fn();
25
+ const onTokenMock = vi.fn();
26
+ const onCompletionMock = vi.fn();
27
+
28
+ const protocolStream = OllamaStream(mockOllamaStream, {
29
+ onStart: onStartMock,
30
+ onText: onTextMock,
31
+ onToken: onTokenMock,
32
+ onCompletion: onCompletionMock,
33
+ });
34
+
35
+ const decoder = new TextDecoder();
36
+ const chunks = [];
11
37
 
12
- const mockOllamaStream: AsyncIterable<ChatResponse> = {
13
38
  // @ts-ignore
14
- async *[Symbol.asyncIterator]() {
15
- yield { message: { content: 'Hello' }, done: false };
16
- yield { message: { content: ' world!' }, done: false };
17
- yield { message: { content: '' }, done: true };
18
- },
19
- };
20
-
21
- const onStartMock = vi.fn();
22
- const onTextMock = vi.fn();
23
- const onTokenMock = vi.fn();
24
- const onCompletionMock = vi.fn();
25
-
26
- const protocolStream = OllamaStream(mockOllamaStream, {
27
- onStart: onStartMock,
28
- onText: onTextMock,
29
- onToken: onTokenMock,
30
- onCompletion: onCompletionMock,
39
+ for await (const chunk of protocolStream) {
40
+ chunks.push(decoder.decode(chunk, { stream: true }));
41
+ }
42
+
43
+ expect(chunks).toEqual([
44
+ 'id: chat_1\n',
45
+ 'event: text\n',
46
+ `data: "Hello"\n\n`,
47
+ 'id: chat_1\n',
48
+ 'event: text\n',
49
+ `data: " world!"\n\n`,
50
+ 'id: chat_1\n',
51
+ 'event: stop\n',
52
+ `data: "finished"\n\n`,
53
+ ]);
54
+
55
+ expect(onStartMock).toHaveBeenCalledTimes(1);
56
+ expect(onTextMock).toHaveBeenNthCalledWith(1, '"Hello"');
57
+ expect(onTextMock).toHaveBeenNthCalledWith(2, '" world!"');
58
+ expect(onTokenMock).toHaveBeenCalledTimes(2);
59
+ expect(onCompletionMock).toHaveBeenCalledTimes(1);
31
60
  });
32
61
 
33
- const decoder = new TextDecoder();
34
- const chunks = [];
62
+ it('tools use', async () => {
63
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
35
64
 
36
- // @ts-ignore
37
- for await (const chunk of protocolStream) {
38
- chunks.push(decoder.decode(chunk, { stream: true }));
39
- }
65
+ const mockOllamaStream = new ReadableStream<ChatResponse>({
66
+ start(controller) {
67
+ controller.enqueue({
68
+ model: 'qwen2.5',
69
+ created_at: new Date('2024-12-01T03:34:55.166692Z'),
70
+ message: {
71
+ role: 'assistant',
72
+ content: '',
73
+ tool_calls: [
74
+ {
75
+ function: {
76
+ name: 'realtime-weather____fetchCurrentWeather',
77
+ arguments: { city: '杭州' },
78
+ },
79
+ },
80
+ ],
81
+ },
82
+ done: false,
83
+ } as unknown as ChatResponse);
84
+ controller.enqueue({
85
+ model: 'qwen2.5',
86
+ created_at: '2024-12-01T03:34:55.2133Z',
87
+ message: { role: 'assistant', content: '' },
88
+ done_reason: 'stop',
89
+ done: true,
90
+ total_duration: 1122415333,
91
+ load_duration: 26178333,
92
+ prompt_eval_count: 221,
93
+ prompt_eval_duration: 507000000,
94
+ eval_count: 26,
95
+ eval_duration: 583000000,
96
+ } as unknown as ChatResponse);
97
+
98
+ controller.close();
99
+ },
100
+ });
101
+ const onStartMock = vi.fn();
102
+ const onTextMock = vi.fn();
103
+ const onTokenMock = vi.fn();
104
+ const onToolCall = vi.fn();
105
+ const onCompletionMock = vi.fn();
40
106
 
41
- expect(chunks).toEqual([
42
- 'id: chat_1\n',
43
- 'event: text\n',
44
- `data: "Hello"\n\n`,
45
- 'id: chat_1\n',
46
- 'event: text\n',
47
- `data: " world!"\n\n`,
48
- 'id: chat_1\n',
49
- 'event: stop\n',
50
- `data: "finished"\n\n`,
51
- ]);
52
-
53
- expect(onStartMock).toHaveBeenCalledTimes(1);
54
- expect(onTextMock).toHaveBeenNthCalledWith(1, '"Hello"');
55
- expect(onTextMock).toHaveBeenNthCalledWith(2, '" world!"');
56
- expect(onTokenMock).toHaveBeenCalledTimes(2);
57
- expect(onCompletionMock).toHaveBeenCalledTimes(1);
107
+ const protocolStream = OllamaStream(mockOllamaStream, {
108
+ onStart: onStartMock,
109
+ onText: onTextMock,
110
+ onToken: onTokenMock,
111
+ onCompletion: onCompletionMock,
112
+ onToolCall,
113
+ });
114
+
115
+ const decoder = new TextDecoder();
116
+ const chunks = [];
117
+
118
+ // @ts-ignore
119
+ for await (const chunk of protocolStream) {
120
+ chunks.push(decoder.decode(chunk, { stream: true }));
121
+ }
122
+
123
+ expect(chunks).toEqual(
124
+ [
125
+ 'id: chat_1',
126
+ 'event: tool_calls',
127
+ `data: [{"function":{"arguments":"{\\"city\\":\\"杭州\\"}","name":"realtime-weather____fetchCurrentWeather"},"id":"realtime-weather____fetchCurrentWeather_0","index":0,"type":"function"}]\n`,
128
+ 'id: chat_1',
129
+ 'event: stop',
130
+ `data: "finished"\n`,
131
+ ].map((i) => `${i}\n`),
132
+ );
133
+
134
+ expect(onTextMock).toHaveBeenCalledTimes(0);
135
+ expect(onStartMock).toHaveBeenCalledTimes(1);
136
+ expect(onToolCall).toHaveBeenCalledTimes(1);
137
+ expect(onTokenMock).toHaveBeenCalledTimes(0);
138
+ expect(onCompletionMock).toHaveBeenCalledTimes(1);
139
+ });
58
140
  });
59
141
 
60
142
  it('should handle empty stream', async () => {
61
- const mockOllamaStream = {
62
- async *[Symbol.asyncIterator]() {},
63
- };
143
+ const mockOllamaStream = new ReadableStream<ChatResponse>({
144
+ start(controller) {
145
+ controller.close();
146
+ },
147
+ });
64
148
 
65
149
  const protocolStream = OllamaStream(mockOllamaStream);
66
150
 
@@ -6,27 +6,42 @@ import { nanoid } from '@/utils/uuid';
6
6
  import {
7
7
  StreamProtocolChunk,
8
8
  StreamStack,
9
- convertIterableToStream,
10
9
  createCallbacksTransformer,
11
10
  createSSEProtocolTransformer,
11
+ generateToolCallId,
12
12
  } from './protocol';
13
13
 
14
14
  const transformOllamaStream = (chunk: ChatResponse, stack: StreamStack): StreamProtocolChunk => {
15
15
  // maybe need another structure to add support for multiple choices
16
- if (chunk.done) {
16
+ if (chunk.done && !chunk.message.content) {
17
17
  return { data: 'finished', id: stack.id, type: 'stop' };
18
18
  }
19
19
 
20
+ if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) {
21
+ return {
22
+ data: chunk.message.tool_calls.map((value, index) => ({
23
+ function: {
24
+ arguments: JSON.stringify(value.function?.arguments) ?? '{}',
25
+ name: value.function?.name ?? null,
26
+ },
27
+ id: generateToolCallId(index, value.function?.name),
28
+ index: index,
29
+ type: 'function',
30
+ })),
31
+ id: stack.id,
32
+ type: 'tool_calls',
33
+ };
34
+ }
20
35
  return { data: chunk.message.content, id: stack.id, type: 'text' };
21
36
  };
22
37
 
23
38
  export const OllamaStream = (
24
- res: AsyncIterable<ChatResponse>,
39
+ res: ReadableStream<ChatResponse>,
25
40
  cb?: ChatStreamCallbacks,
26
41
  ): ReadableStream<string> => {
27
42
  const streamStack: StreamStack = { id: 'chat_' + nanoid() };
28
43
 
29
- return convertIterableToStream(res)
44
+ return res
30
45
  .pipeThrough(createSSEProtocolTransformer(transformOllamaStream, streamStack))
31
46
  .pipeThrough(createCallbacksTransformer(cb));
32
47
  };
@@ -134,6 +134,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
134
134
  const runtime = await initAgentRuntimeWithUserPayload(ModelProvider.Ollama, jwtPayload);
135
135
  expect(runtime).toBeInstanceOf(AgentRuntime);
136
136
  expect(runtime['_runtime']).toBeInstanceOf(LobeOllamaAI);
137
+ expect(runtime['_runtime']['baseURL']).toEqual(jwtPayload.endpoint);
137
138
  });
138
139
 
139
140
  it('Perplexity AI provider: with apikey', async () => {
@@ -391,7 +392,7 @@ describe('initAgentRuntimeWithUserPayload method', () => {
391
392
  // endpoint 不存在,应返回 DEFAULT_BASE_URL
392
393
  expect(runtime['_runtime'].baseURL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1');
393
394
  });
394
-
395
+
395
396
  it('Unknown Provider', async () => {
396
397
  const jwtPayload = {};
397
398
  const runtime = await initAgentRuntimeWithUserPayload('unknown', jwtPayload);
@@ -33,7 +33,7 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
33
33
  default: {
34
34
  let upperProvider = provider.toUpperCase();
35
35
 
36
- if (!( `${upperProvider}_API_KEY` in llmConfig)) {
36
+ if (!(`${upperProvider}_API_KEY` in llmConfig)) {
37
37
  upperProvider = ModelProvider.OpenAI.toUpperCase(); // Use OpenAI options as default
38
38
  }
39
39
 
@@ -43,6 +43,12 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
43
43
  return baseURL ? { apiKey, baseURL } : { apiKey };
44
44
  }
45
45
 
46
+ case ModelProvider.Ollama: {
47
+ const baseURL = payload?.endpoint || process.env.OLLAMA_PROXY_URL;
48
+
49
+ return { baseURL };
50
+ }
51
+
46
52
  case ModelProvider.Azure: {
47
53
  const { AZURE_API_KEY, AZURE_API_VERSION, AZURE_ENDPOINT } = llmConfig;
48
54
  const apikey = apiKeyManager.pick(payload?.apiKey || AZURE_API_KEY);