@agentscope-ai/agentscope 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/agent/index.d.mts +234 -0
  2. package/dist/agent/index.d.ts +234 -0
  3. package/dist/agent/index.js +1412 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/agent/index.mjs +1375 -0
  6. package/dist/agent/index.mjs.map +1 -0
  7. package/dist/base-BOx3UzOl.d.mts +41 -0
  8. package/dist/base-BoIps2RL.d.ts +41 -0
  9. package/dist/base-C7jwyH4Z.d.mts +52 -0
  10. package/dist/base-Cwi4bjze.d.ts +127 -0
  11. package/dist/base-DYlBMCy_.d.mts +127 -0
  12. package/dist/base-NX-knWOv.d.ts +52 -0
  13. package/dist/block-VsnHrllL.d.mts +48 -0
  14. package/dist/block-VsnHrllL.d.ts +48 -0
  15. package/dist/event/index.d.mts +181 -0
  16. package/dist/event/index.d.ts +181 -0
  17. package/dist/event/index.js +58 -0
  18. package/dist/event/index.js.map +1 -0
  19. package/dist/event/index.mjs +33 -0
  20. package/dist/event/index.mjs.map +1 -0
  21. package/dist/formatter/index.d.mts +187 -0
  22. package/dist/formatter/index.d.ts +187 -0
  23. package/dist/formatter/index.js +647 -0
  24. package/dist/formatter/index.js.map +1 -0
  25. package/dist/formatter/index.mjs +616 -0
  26. package/dist/formatter/index.mjs.map +1 -0
  27. package/dist/index-BTJDlKvQ.d.mts +195 -0
  28. package/dist/index-BcatlwXQ.d.ts +195 -0
  29. package/dist/index-CAxQAkiP.d.mts +21 -0
  30. package/dist/index-CAxQAkiP.d.ts +21 -0
  31. package/dist/mcp/index.d.mts +9 -0
  32. package/dist/mcp/index.d.ts +9 -0
  33. package/dist/mcp/index.js +432 -0
  34. package/dist/mcp/index.js.map +1 -0
  35. package/dist/mcp/index.mjs +408 -0
  36. package/dist/mcp/index.mjs.map +1 -0
  37. package/dist/message/index.d.mts +10 -0
  38. package/dist/message/index.d.ts +10 -0
  39. package/dist/message/index.js +67 -0
  40. package/dist/message/index.js.map +1 -0
  41. package/dist/message/index.mjs +37 -0
  42. package/dist/message/index.mjs.map +1 -0
  43. package/dist/message-CkN21KaY.d.mts +99 -0
  44. package/dist/message-CzLeTlua.d.ts +99 -0
  45. package/dist/model/index.d.mts +377 -0
  46. package/dist/model/index.d.ts +377 -0
  47. package/dist/model/index.js +1880 -0
  48. package/dist/model/index.js.map +1 -0
  49. package/dist/model/index.mjs +1849 -0
  50. package/dist/model/index.mjs.map +1 -0
  51. package/dist/storage/index.d.mts +68 -0
  52. package/dist/storage/index.d.ts +68 -0
  53. package/dist/storage/index.js +250 -0
  54. package/dist/storage/index.js.map +1 -0
  55. package/dist/storage/index.mjs +212 -0
  56. package/dist/storage/index.mjs.map +1 -0
  57. package/dist/tool/index.d.mts +311 -0
  58. package/dist/tool/index.d.ts +311 -0
  59. package/dist/tool/index.js +1494 -0
  60. package/dist/tool/index.js.map +1 -0
  61. package/dist/tool/index.mjs +1447 -0
  62. package/dist/tool/index.mjs.map +1 -0
  63. package/dist/toolkit-CEpulFi0.d.ts +99 -0
  64. package/dist/toolkit-CGEZSZPa.d.mts +99 -0
  65. package/jest.config.js +11 -0
  66. package/package.json +92 -0
  67. package/src/_utils/common.ts +104 -0
  68. package/src/_utils/index.ts +1 -0
  69. package/src/agent/agent-base.ts +0 -0
  70. package/src/agent/agent.test.ts +1028 -0
  71. package/src/agent/agent.ts +1032 -0
  72. package/src/agent/index.ts +2 -0
  73. package/src/agent/interfaces.ts +23 -0
  74. package/src/agent/test-compression.ts +72 -0
  75. package/src/event/index.ts +250 -0
  76. package/src/formatter/base.ts +133 -0
  77. package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
  78. package/src/formatter/dashscope-chat-formatter.ts +163 -0
  79. package/src/formatter/deepseek-chat-formatter.ts +130 -0
  80. package/src/formatter/index.ts +5 -0
  81. package/src/formatter/ollama-chat-formatter.ts +67 -0
  82. package/src/formatter/openai-chat-formatter.test.ts +263 -0
  83. package/src/formatter/openai-chat-formatter.ts +301 -0
  84. package/src/formatter/openai.md +767 -0
  85. package/src/mcp/base.ts +114 -0
  86. package/src/mcp/http.test.ts +303 -0
  87. package/src/mcp/http.ts +224 -0
  88. package/src/mcp/index.ts +2 -0
  89. package/src/mcp/stdio.test.ts +91 -0
  90. package/src/mcp/stdio.ts +119 -0
  91. package/src/message/block.ts +60 -0
  92. package/src/message/enums.ts +4 -0
  93. package/src/message/index.ts +12 -0
  94. package/src/message/message.test.ts +80 -0
  95. package/src/message/message.ts +131 -0
  96. package/src/model/base.ts +226 -0
  97. package/src/model/dashscope-model.test.ts +335 -0
  98. package/src/model/dashscope-model.ts +441 -0
  99. package/src/model/deepseek-model.test.ts +279 -0
  100. package/src/model/deepseek-model.ts +401 -0
  101. package/src/model/index.ts +7 -0
  102. package/src/model/ollama-model.test.ts +307 -0
  103. package/src/model/ollama-model.ts +356 -0
  104. package/src/model/openai-model.ts +327 -0
  105. package/src/model/response.ts +22 -0
  106. package/src/model/usage.ts +12 -0
  107. package/src/storage/base.ts +52 -0
  108. package/src/storage/file-system.test.ts +587 -0
  109. package/src/storage/file-system.ts +269 -0
  110. package/src/storage/index.ts +2 -0
  111. package/src/tool/base.ts +23 -0
  112. package/src/tool/bash.test.ts +174 -0
  113. package/src/tool/bash.ts +152 -0
  114. package/src/tool/edit.test.ts +83 -0
  115. package/src/tool/edit.ts +95 -0
  116. package/src/tool/glob.test.ts +63 -0
  117. package/src/tool/glob.ts +166 -0
  118. package/src/tool/grep.test.ts +74 -0
  119. package/src/tool/grep.ts +256 -0
  120. package/src/tool/index.ts +10 -0
  121. package/src/tool/read.test.ts +77 -0
  122. package/src/tool/read.ts +117 -0
  123. package/src/tool/response.ts +82 -0
  124. package/src/tool/task.test.ts +299 -0
  125. package/src/tool/task.ts +399 -0
  126. package/src/tool/toolkit.test.ts +636 -0
  127. package/src/tool/toolkit.ts +601 -0
  128. package/src/tool/write.test.ts +52 -0
  129. package/src/tool/write.ts +57 -0
  130. package/src/type/index.ts +52 -0
  131. package/tsconfig.build.json +4 -0
  132. package/tsconfig.cjs.json +11 -0
  133. package/tsconfig.esm.json +10 -0
  134. package/tsconfig.json +14 -0
  135. package/tsup.config.ts +20 -0
  136. package/typedoc.json +52 -0
@@ -0,0 +1,307 @@
1
+ import { OllamaChatModel } from './ollama-model';
2
+ import { ChatResponse } from './response';
3
+ import { createMsg } from '../message';
4
+
5
+ // Mock fetch for streaming responses
6
+ global.fetch = jest.fn();
7
+
8
+ describe('OllamaChatModel', () => {
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ test('Test stream generation with delta output', async () => {
14
+ // Mock streaming response with multiple chunks (NDJSON format)
15
+ // Ollama returns newline-delimited JSON
16
+ const mockStreamChunks = [
17
+ '{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"Let me"},"done":false}\n',
18
+ '{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":" check the weather"},"done":false}\n',
19
+ '{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"get_current_weather","arguments":{"location":"Beijing"}}}]},"done":false}\n',
20
+ '{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":100,"eval_count":50}\n',
21
+ ];
22
+
23
+ const mockReadableStream = new ReadableStream({
24
+ start(controller) {
25
+ mockStreamChunks.forEach(chunk =>
26
+ controller.enqueue(new TextEncoder().encode(chunk))
27
+ );
28
+ controller.close();
29
+ },
30
+ });
31
+
32
+ (global.fetch as jest.Mock).mockResolvedValue({
33
+ ok: true,
34
+ body: mockReadableStream,
35
+ });
36
+
37
+ const model = new OllamaChatModel({
38
+ modelName: 'qwen3:1.7b',
39
+ stream: true,
40
+ host: 'http://localhost:11434',
41
+ });
42
+
43
+ const res = await model.call({
44
+ messages: [
45
+ createMsg({
46
+ name: 'user',
47
+ role: 'user',
48
+ content: [
49
+ { type: 'text', text: "How's the weather today?", id: crypto.randomUUID() },
50
+ ],
51
+ }),
52
+ ],
53
+ tools: [
54
+ {
55
+ type: 'function',
56
+ function: {
57
+ name: 'get_current_weather',
58
+ description: 'Get the current weather in a given location',
59
+ parameters: {
60
+ type: 'object',
61
+ properties: {
62
+ location: {
63
+ type: 'string',
64
+ description: 'The city and state, e.g. San Francisco, CA',
65
+ },
66
+ },
67
+ required: ['location'],
68
+ },
69
+ },
70
+ },
71
+ ],
72
+ });
73
+
74
+ const generator = res as AsyncGenerator<ChatResponse, ChatResponse>;
75
+ let completeResponse: ChatResponse | undefined;
76
+ const yieldedChunks: ChatResponse[] = [];
77
+
78
+ // Manual iteration to capture both yielded and returned values
79
+ while (true) {
80
+ const result = await generator.next();
81
+ if (result.done) {
82
+ completeResponse = result.value;
83
+ break;
84
+ }
85
+ yieldedChunks.push(result.value);
86
+ }
87
+
88
+ // Verify we received multiple yielded chunks
89
+ expect(yieldedChunks.length).toBeGreaterThan(0);
90
+
91
+ // Verify the final complete response has correct structure
92
+ expect(completeResponse.content.length).toBe(2);
93
+
94
+ // Check text block - should be complete after accumulation
95
+ const textBlock = completeResponse.content.find(b => b.type === 'text');
96
+ expect(textBlock).toBeDefined();
97
+ expect(textBlock).toMatchObject({
98
+ type: 'text',
99
+ text: 'Let me check the weather',
100
+ });
101
+
102
+ // Check tool_call block
103
+ const toolCallBlock = completeResponse.content.find(b => b.type === 'tool_call');
104
+ expect(toolCallBlock).toBeDefined();
105
+ expect(toolCallBlock).toMatchObject({
106
+ type: 'tool_call',
107
+ name: 'get_current_weather',
108
+ input: '{"location":"Beijing"}',
109
+ });
110
+
111
+ // Verify usage
112
+ expect(completeResponse.usage).toBeDefined();
113
+ expect(completeResponse.usage?.inputTokens).toBe(100);
114
+ expect(completeResponse.usage?.outputTokens).toBe(50);
115
+ }, 10000);
116
+
117
+ test('Test non-streaming generation', async () => {
118
+ // Mock non-streaming response
119
+ const mockResponse = {
120
+ model: 'qwen3:8b',
121
+ created_at: '2024-01-01T00:00:00Z',
122
+ message: {
123
+ role: 'assistant',
124
+ content: '你好!我是一个AI助手。',
125
+ },
126
+ done: true,
127
+ prompt_eval_count: 50,
128
+ eval_count: 30,
129
+ };
130
+
131
+ (global.fetch as jest.Mock).mockResolvedValue({
132
+ ok: true,
133
+ json: async () => mockResponse,
134
+ });
135
+
136
+ const model = new OllamaChatModel({
137
+ modelName: 'qwen3:8b',
138
+ stream: false,
139
+ host: 'http://localhost:11434',
140
+ });
141
+
142
+ const res = await model.call({
143
+ messages: [
144
+ createMsg({
145
+ name: 'user',
146
+ role: 'user',
147
+ content: [
148
+ { type: 'text', text: '你好,请简单介绍一下自己', id: crypto.randomUUID() },
149
+ ],
150
+ }),
151
+ ],
152
+ });
153
+
154
+ const completeResponse = res as ChatResponse;
155
+
156
+ // Verify complete response structure
157
+ expect(completeResponse.content.length).toBe(1);
158
+
159
+ // Check text block
160
+ const textBlock = completeResponse.content.find(b => b.type === 'text');
161
+ expect(textBlock).toBeDefined();
162
+ expect(textBlock).toMatchObject({
163
+ type: 'text',
164
+ text: '你好!我是一个AI助手。',
165
+ });
166
+
167
+ // Verify usage
168
+ expect(completeResponse.usage).toBeDefined();
169
+ expect(completeResponse.usage?.inputTokens).toBe(50);
170
+ expect(completeResponse.usage?.outputTokens).toBe(30);
171
+ }, 10000);
172
+
173
+ test('Test with thinking enabled', async () => {
174
+ // Mock streaming response with thinking
175
+ const mockStreamChunks = [
176
+ '{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","thinking":"计算"},"done":false}\n',
177
+ '{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","thinking":" 123 * 456"},"done":false}\n',
178
+ '{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"答案是 56088"},"done":false}\n',
179
+ '{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":80,"eval_count":40}\n',
180
+ ];
181
+
182
+ const mockReadableStream = new ReadableStream({
183
+ start(controller) {
184
+ mockStreamChunks.forEach(chunk =>
185
+ controller.enqueue(new TextEncoder().encode(chunk))
186
+ );
187
+ controller.close();
188
+ },
189
+ });
190
+
191
+ (global.fetch as jest.Mock).mockResolvedValue({
192
+ ok: true,
193
+ body: mockReadableStream,
194
+ });
195
+
196
+ const model = new OllamaChatModel({
197
+ modelName: 'qwen3:8b',
198
+ stream: true,
199
+ thinkingConfig: {
200
+ enableThinking: true,
201
+ },
202
+ host: 'http://localhost:11434',
203
+ });
204
+
205
+ const res = await model.call({
206
+ messages: [
207
+ createMsg({
208
+ name: 'user',
209
+ role: 'user',
210
+ content: [
211
+ {
212
+ type: 'text',
213
+ text: '计算 123 * 456 等于多少?',
214
+ id: crypto.randomUUID(),
215
+ },
216
+ ],
217
+ }),
218
+ ],
219
+ });
220
+
221
+ const generator = res as AsyncGenerator<ChatResponse, ChatResponse>;
222
+ let completeResponse: ChatResponse | undefined;
223
+ const yieldedChunks: ChatResponse[] = [];
224
+
225
+ while (true) {
226
+ const result = await generator.next();
227
+ if (result.done) {
228
+ completeResponse = result.value;
229
+ break;
230
+ }
231
+ yieldedChunks.push(result.value);
232
+ }
233
+
234
+ // Verify we received multiple yielded chunks
235
+ expect(yieldedChunks.length).toBeGreaterThan(0);
236
+
237
+ // Verify the final complete response has correct structure
238
+ expect(completeResponse.content.length).toBe(2);
239
+
240
+ // Check thinking block
241
+ const thinkingBlock = completeResponse.content.find(b => b.type === 'thinking');
242
+ expect(thinkingBlock).toBeDefined();
243
+ expect(thinkingBlock).toMatchObject({
244
+ type: 'thinking',
245
+ thinking: '计算 123 * 456',
246
+ });
247
+
248
+ // Check text block
249
+ const textBlock = completeResponse.content.find(b => b.type === 'text');
250
+ expect(textBlock).toBeDefined();
251
+ expect(textBlock).toMatchObject({
252
+ type: 'text',
253
+ text: '答案是 56088',
254
+ });
255
+
256
+ // Verify usage
257
+ expect(completeResponse.usage).toBeDefined();
258
+ expect(completeResponse.usage?.inputTokens).toBe(80);
259
+ expect(completeResponse.usage?.outputTokens).toBe(40);
260
+ }, 10000);
261
+
262
+ test('Test formatToolChoice function', () => {
263
+ const model = new OllamaChatModel({
264
+ modelName: 'qwen3:1.7b',
265
+ host: 'http://localhost:11434',
266
+ });
267
+
268
+ // Ollama's _formatToolChoice always returns undefined
269
+ expect(model._formatToolChoice('auto')).toBeUndefined();
270
+ expect(model._formatToolChoice('none')).toBeUndefined();
271
+ expect(model._formatToolChoice('my_function')).toBeUndefined();
272
+ expect(model._formatToolChoice(undefined)).toBeUndefined();
273
+ });
274
+
275
+ test('Test formatToolSchemas function', () => {
276
+ const model = new OllamaChatModel({
277
+ modelName: 'qwen3:1.7b',
278
+ host: 'http://localhost:11434',
279
+ });
280
+
281
+ const toolSchemas = [
282
+ {
283
+ type: 'function' as const,
284
+ function: {
285
+ name: 'get_current_weather',
286
+ description: 'Get the current weather in a given location',
287
+ parameters: {
288
+ type: 'object' as const,
289
+ properties: {
290
+ location: {
291
+ type: 'string',
292
+ description: 'The city and state, e.g. San Francisco, CA',
293
+ },
294
+ },
295
+ required: ['location'],
296
+ },
297
+ },
298
+ },
299
+ ];
300
+
301
+ // Test with tool schemas
302
+ expect(model._formatToolSchemas(toolSchemas)).toEqual(toolSchemas);
303
+
304
+ // Test with undefined (should return empty array)
305
+ expect(model._formatToolSchemas(undefined)).toEqual([]);
306
+ });
307
+ });
@@ -0,0 +1,356 @@
1
+ import { Ollama, ChatResponse as OllamaChatResponse, AbortableAsyncIterator } from 'ollama';
2
+
3
+ import { ChatModelBase, ChatModelOptions, ChatModelRequestOptions } from './base';
4
+ import { ChatResponse } from './response';
5
+ import { TextBlock, ThinkingBlock, ToolCallBlock } from '../message';
6
+ import { ToolChoice, ToolSchema } from '../type';
7
+ import { ChatUsage } from './usage';
8
+ import { OllamaChatFormatter } from '../formatter';
9
+
10
+ interface OllamaThinkingConfig {
11
+ /**
12
+ * Whether to enable thinking or not.
13
+ */
14
+ enableThinking: boolean;
15
+
16
+ /**
17
+ * Thinking level for Ollama models (high, medium, low).
18
+ * Only applicable when enableThinking is true.
19
+ */
20
+ thinkingLevel?: 'high' | 'medium' | 'low';
21
+ }
22
+
23
+ interface OllamaChatModelOptions extends ChatModelOptions {
24
+ /**
25
+ * Additional parameters to pass to the Ollama API (e.g., temperature).
26
+ */
27
+ options?: Record<string, unknown>;
28
+
29
+ /**
30
+ * Duration to keep the model loaded in memory (e.g., "5m", "1h").
31
+ */
32
+ keepAlive?: string;
33
+
34
+ /**
35
+ * Thinking configuration for Ollama models.
36
+ */
37
+ thinkingConfig?: OllamaThinkingConfig;
38
+
39
+ /**
40
+ * The host address of the Ollama server.
41
+ */
42
+ host?: string;
43
+
44
+ /**
45
+ * Extra keyword arguments to initialize the Ollama client.
46
+ */
47
+ clientKwargs?: Record<string, unknown>;
48
+
49
+ /**
50
+ * Extra keyword arguments used in Ollama API generation.
51
+ */
52
+ generateKwargs?: Record<string, unknown>;
53
+ }
54
+
55
+ /**
56
+ * The Ollama chat model class in AgentScope.
57
+ */
58
+ export class OllamaChatModel extends ChatModelBase {
59
+ protected client: Ollama;
60
+ protected options?: Record<string, unknown>;
61
+ protected keepAlive: string;
62
+ protected thinkingConfig: OllamaThinkingConfig;
63
+ protected generateKwargs: Record<string, unknown>;
64
+
65
+ /**
66
+ * Initializes a new instance of the OllamaChatModel class.
67
+ * @param root0
68
+ * @param root0.modelName
69
+ * @param root0.stream
70
+ * @param root0.options
71
+ * @param root0.keepAlive
72
+ * @param root0.thinkingConfig
73
+ * @param root0.host
74
+ * @param root0.maxRetries
75
+ * @param root0.fallbackModelName
76
+ * @param root0.clientKwargs
77
+ * @param root0.generateKwargs
78
+ * @param root0.formatter
79
+ */
80
+ constructor({
81
+ modelName,
82
+ stream = true,
83
+ options,
84
+ keepAlive = '5m',
85
+ thinkingConfig,
86
+ host,
87
+ maxRetries = 0,
88
+ fallbackModelName,
89
+ clientKwargs,
90
+ generateKwargs,
91
+ formatter,
92
+ }: OllamaChatModelOptions) {
93
+ // If no formatter is provided, create a default OllamaChatFormatter
94
+ const defaultFormatter = formatter || new OllamaChatFormatter();
95
+ super({
96
+ modelName,
97
+ stream,
98
+ maxRetries,
99
+ fallbackModelName,
100
+ formatter: defaultFormatter,
101
+ } as ChatModelOptions);
102
+
103
+ this.options = options;
104
+ this.keepAlive = keepAlive;
105
+ this.thinkingConfig = thinkingConfig || {
106
+ enableThinking: false,
107
+ };
108
+ this.generateKwargs = generateKwargs || {};
109
+
110
+ // Initialize Ollama client
111
+ this.client = new Ollama({
112
+ host: host,
113
+ ...clientKwargs,
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Calls the Ollama API with the given parameters.
119
+ * @param modelName
120
+ * @param options
121
+ * @returns A promise that resolves to either a ChatResponse or an AsyncGenerator of ChatResponses.
122
+ */
123
+ async _callAPI(
124
+ modelName: string,
125
+ options: ChatModelRequestOptions<Record<string, unknown>>
126
+ ): Promise<ChatResponse | AsyncGenerator<ChatResponse, ChatResponse>> {
127
+ const kwargs: Record<string, unknown> = {
128
+ model: modelName,
129
+ messages: options.messages,
130
+ stream: this.stream,
131
+ options: this.options,
132
+ keep_alive: this.keepAlive,
133
+ ...this.generateKwargs,
134
+ };
135
+
136
+ if (this.thinkingConfig.enableThinking) {
137
+ // If thinkingLevel is specified, use it; otherwise use true
138
+ kwargs.think = this.thinkingConfig.thinkingLevel || true;
139
+ } else {
140
+ kwargs.think = false;
141
+ }
142
+
143
+ if (options.tools) {
144
+ kwargs.tools = this._formatToolSchemas(options.tools);
145
+ }
146
+
147
+ if (options.toolChoice) {
148
+ console.warn('Ollama does not support tool_choice yet, ignored.');
149
+ }
150
+
151
+ const startTime = Date.now();
152
+
153
+ if (this.stream) {
154
+ const response = (await this.client.chat({
155
+ ...kwargs,
156
+ stream: true,
157
+ } as Parameters<
158
+ typeof this.client.chat
159
+ >[0])) as unknown as AbortableAsyncIterator<OllamaChatResponse>;
160
+ return this._parseOllamaStreamResponse(response, startTime);
161
+ }
162
+
163
+ const response = (await this.client.chat({
164
+ ...kwargs,
165
+ stream: false,
166
+ } as Parameters<typeof this.client.chat>[0])) as unknown as OllamaChatResponse;
167
+ return this._parseOllamaResponse(response, startTime);
168
+ }
169
+
170
+ /**
171
+ * Parse Ollama streaming response.
172
+ * @param stream
173
+ * @param startTime
174
+ * @returns An async generator that yields delta ChatResponse objects and returns the complete ChatResponse.
175
+ */
176
+ async *_parseOllamaStreamResponse(
177
+ stream: AbortableAsyncIterator<OllamaChatResponse>,
178
+ startTime: number
179
+ ): AsyncGenerator<ChatResponse, ChatResponse> {
180
+ let accText = '';
181
+ let accThinking = '';
182
+ const toolCalls: Map<string, ToolCallBlock> = new Map();
183
+ let lastUsage: ChatUsage | null = null;
184
+
185
+ for await (const chunk of stream) {
186
+ const msg = chunk.message;
187
+
188
+ // Delta data for this chunk
189
+ let deltaText = '';
190
+ let deltaThinking = '';
191
+ const deltaToolCalls: Map<string, ToolCallBlock> = new Map();
192
+
193
+ // Accumulate text and thinking
194
+ if (msg.thinking) {
195
+ deltaThinking = msg.thinking;
196
+ accThinking += msg.thinking;
197
+ }
198
+ if (msg.content) {
199
+ deltaText = msg.content;
200
+ accText += msg.content;
201
+ }
202
+
203
+ // Handle tool calls
204
+ if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
205
+ for (let idx = 0; idx < msg.tool_calls.length; idx++) {
206
+ const toolCall = msg.tool_calls[idx];
207
+ const func = toolCall.function;
208
+ const toolId = `${idx}_${func.name}`;
209
+
210
+ const toolCallBlock = {
211
+ type: 'tool_call' as const,
212
+ id: toolId,
213
+ name: func.name,
214
+ input: JSON.stringify(func.arguments),
215
+ };
216
+
217
+ toolCalls.set(toolId, toolCallBlock);
218
+ deltaToolCalls.set(toolId, toolCallBlock);
219
+ }
220
+ }
221
+
222
+ // Calculate usage
223
+ const currentTime = (Date.now() - startTime) / 1000;
224
+ lastUsage = {
225
+ type: 'chat_usage',
226
+ inputTokens: chunk.prompt_eval_count || 0,
227
+ outputTokens: chunk.eval_count || 0,
228
+ time: currentTime,
229
+ };
230
+
231
+ // Yield delta response
232
+ const deltaBlocks = this._buildContentBlocks(deltaText, deltaThinking, deltaToolCalls);
233
+ yield {
234
+ type: 'chat',
235
+ id: crypto.randomUUID(),
236
+ createdAt: new Date().toISOString(),
237
+ content: deltaBlocks,
238
+ usage: lastUsage,
239
+ } as ChatResponse;
240
+ }
241
+
242
+ // Return complete response
243
+ const blocks = this._buildContentBlocks(accText, accThinking, toolCalls);
244
+ return {
245
+ type: 'chat',
246
+ id: crypto.randomUUID(),
247
+ createdAt: new Date().toISOString(),
248
+ content: blocks,
249
+ usage: lastUsage,
250
+ } as ChatResponse;
251
+ }
252
+
253
+ /**
254
+ * Parse Ollama non-streaming response.
255
+ * @param response
256
+ * @param startTime
257
+ * @returns A ChatResponse object containing the content blocks and usage.
258
+ */
259
+ _parseOllamaResponse(response: OllamaChatResponse, startTime: number): ChatResponse {
260
+ const blocks: Array<TextBlock | ThinkingBlock | ToolCallBlock> = [];
261
+
262
+ if (response.message.thinking) {
263
+ blocks.push({
264
+ id: crypto.randomUUID(),
265
+ type: 'thinking',
266
+ thinking: response.message.thinking,
267
+ });
268
+ }
269
+
270
+ if (response.message.content) {
271
+ blocks.push({
272
+ id: crypto.randomUUID(),
273
+ type: 'text',
274
+ text: response.message.content,
275
+ });
276
+ }
277
+
278
+ // Handle tool calls
279
+ if (response.message.tool_calls && Array.isArray(response.message.tool_calls)) {
280
+ for (let idx = 0; idx < response.message.tool_calls.length; idx++) {
281
+ const toolCall = response.message.tool_calls[idx];
282
+ blocks.push({
283
+ type: 'tool_call',
284
+ id: `${idx}_${toolCall.function.name}`,
285
+ name: toolCall.function.name,
286
+ input: JSON.stringify(toolCall.function.arguments),
287
+ });
288
+ }
289
+ }
290
+
291
+ const usage =
292
+ response.prompt_eval_count !== undefined && response.eval_count !== undefined
293
+ ? {
294
+ type: 'chat_usage',
295
+ inputTokens: response.prompt_eval_count || 0,
296
+ outputTokens: response.eval_count || 0,
297
+ time: (Date.now() - startTime) / 1000,
298
+ }
299
+ : undefined;
300
+
301
+ return {
302
+ type: 'chat',
303
+ id: crypto.randomUUID(),
304
+ createdAt: new Date().toISOString(),
305
+ content: blocks,
306
+ usage,
307
+ } as ChatResponse;
308
+ }
309
+
310
+ /**
311
+ * Build content blocks from accumulated data.
312
+ * @param text
313
+ * @param thinking
314
+ * @param toolCalls
315
+ * @returns An array of content blocks.
316
+ */
317
+ _buildContentBlocks(
318
+ text: string,
319
+ thinking: string,
320
+ toolCalls: Map<string, ToolCallBlock>
321
+ ): Array<TextBlock | ThinkingBlock | ToolCallBlock> {
322
+ const blocks: Array<TextBlock | ThinkingBlock | ToolCallBlock> = [];
323
+
324
+ if (thinking) {
325
+ blocks.push({ id: crypto.randomUUID(), type: 'thinking', thinking });
326
+ }
327
+
328
+ if (text) {
329
+ blocks.push({ id: crypto.randomUUID(), type: 'text', text });
330
+ }
331
+
332
+ toolCalls.forEach(toolCall => {
333
+ blocks.push(toolCall);
334
+ });
335
+
336
+ return blocks;
337
+ }
338
+
339
+ /**
340
+ * Format tool choice parameter (not supported by Ollama).
341
+ * @param _toolChoice
342
+ * @returns undefined as Ollama does not support tool choice.
343
+ */
344
+ _formatToolChoice(_toolChoice?: ToolChoice): unknown {
345
+ return undefined;
346
+ }
347
+
348
+ /**
349
+ * Format tool schemas for Ollama API (no special formatting needed).
350
+ * @param tools
351
+ * @returns The same array of tool schemas, or an empty array if undefined.
352
+ */
353
+ _formatToolSchemas(tools: ToolSchema[] | undefined): ToolSchema[] {
354
+ return tools || [];
355
+ }
356
+ }