snow-ai 0.3.0 → 0.3.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.
@@ -1,5 +1,4 @@
1
- import type { ChatMessage } from './chat.js';
2
- import type { ChatCompletionTool } from 'openai/resources/chat/completions';
1
+ import type { ChatMessage, ChatCompletionTool, UsageInfo } from './types.js';
3
2
  export interface AnthropicOptions {
4
3
  model: string;
5
4
  messages: ChatMessage[];
@@ -8,13 +7,6 @@ export interface AnthropicOptions {
8
7
  tools?: ChatCompletionTool[];
9
8
  sessionId?: string;
10
9
  }
11
- export interface UsageInfo {
12
- prompt_tokens: number;
13
- completion_tokens: number;
14
- total_tokens: number;
15
- cache_creation_input_tokens?: number;
16
- cache_read_input_tokens?: number;
17
- }
18
10
  export interface AnthropicStreamChunk {
19
11
  type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage';
20
12
  content?: string;
@@ -29,6 +21,18 @@ export interface AnthropicStreamChunk {
29
21
  delta?: string;
30
22
  usage?: UsageInfo;
31
23
  }
24
+ export interface AnthropicTool {
25
+ name: string;
26
+ description: string;
27
+ input_schema: any;
28
+ cache_control?: {
29
+ type: 'ephemeral';
30
+ };
31
+ }
32
+ export interface AnthropicMessageParam {
33
+ role: 'user' | 'assistant';
34
+ content: string | Array<any>;
35
+ }
32
36
  export declare function resetAnthropicClient(): void;
33
37
  /**
34
38
  * Create streaming chat completion using Anthropic API
@@ -1,45 +1,28 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
1
  import { createHash, randomUUID } from 'crypto';
3
2
  import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
4
3
  import { SYSTEM_PROMPT } from './systemPrompt.js';
5
4
  import { withRetryGenerator } from '../utils/retryUtils.js';
6
- let anthropicClient = null;
7
- function getAnthropicClient() {
8
- if (!anthropicClient) {
5
+ let anthropicConfig = null;
6
+ function getAnthropicConfig() {
7
+ if (!anthropicConfig) {
9
8
  const config = getOpenAiConfig();
10
9
  if (!config.apiKey) {
11
10
  throw new Error('Anthropic API configuration is incomplete. Please configure API key first.');
12
11
  }
13
- const clientConfig = {
14
- apiKey: config.apiKey,
15
- };
16
- if (config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1') {
17
- clientConfig.baseURL = config.baseUrl;
18
- }
19
12
  const customHeaders = getCustomHeaders();
20
- clientConfig.defaultHeaders = {
21
- 'Authorization': `Bearer ${config.apiKey}`,
22
- //'anthropic-version': '2024-09-24',
23
- ...customHeaders
24
- };
25
- // if (config.anthropicBeta) {
26
- // clientConfig.defaultHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
27
- // }
28
- // Intercept fetch to add beta parameter to URL
29
- const originalFetch = clientConfig.fetch || globalThis.fetch;
30
- clientConfig.fetch = async (url, init) => {
31
- let finalUrl = url;
32
- if (config.anthropicBeta && typeof url === 'string' && !url.includes('?beta=')) {
33
- finalUrl = url + (url.includes('?') ? '&beta=true' : '?beta=true');
34
- }
35
- return originalFetch(finalUrl, init);
13
+ anthropicConfig = {
14
+ apiKey: config.apiKey,
15
+ baseUrl: config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1'
16
+ ? config.baseUrl
17
+ : 'https://api.anthropic.com/v1',
18
+ customHeaders,
19
+ anthropicBeta: config.anthropicBeta
36
20
  };
37
- anthropicClient = new Anthropic(clientConfig);
38
21
  }
39
- return anthropicClient;
22
+ return anthropicConfig;
40
23
  }
41
24
  export function resetAnthropicClient() {
42
- anthropicClient = null;
25
+ anthropicConfig = null;
43
26
  }
44
27
  /**
45
28
  * Generate a user_id in the format: user_<hash>_account__session_<uuid>
@@ -209,16 +192,51 @@ function convertToAnthropicMessages(messages) {
209
192
  }] : undefined;
210
193
  return { system, messages: anthropicMessages };
211
194
  }
195
+ /**
196
+ * Parse Server-Sent Events (SSE) stream
197
+ */
198
+ async function* parseSSEStream(reader) {
199
+ const decoder = new TextDecoder();
200
+ let buffer = '';
201
+ while (true) {
202
+ const { done, value } = await reader.read();
203
+ if (done)
204
+ break;
205
+ buffer += decoder.decode(value, { stream: true });
206
+ const lines = buffer.split('\n');
207
+ buffer = lines.pop() || '';
208
+ for (const line of lines) {
209
+ const trimmed = line.trim();
210
+ if (!trimmed || trimmed.startsWith(':'))
211
+ continue;
212
+ if (trimmed === 'data: [DONE]') {
213
+ return;
214
+ }
215
+ if (trimmed.startsWith('event: ')) {
216
+ // Event type, will be followed by data
217
+ continue;
218
+ }
219
+ if (trimmed.startsWith('data: ')) {
220
+ const data = trimmed.slice(6);
221
+ try {
222
+ yield JSON.parse(data);
223
+ }
224
+ catch (e) {
225
+ console.error('Failed to parse SSE data:', data);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
212
231
  /**
213
232
  * Create streaming chat completion using Anthropic API
214
233
  */
215
234
  export async function* createStreamingAnthropicCompletion(options, abortSignal, onRetry) {
216
- const client = getAnthropicClient();
217
235
  yield* withRetryGenerator(async function* () {
236
+ const config = getAnthropicConfig();
218
237
  const { system, messages } = convertToAnthropicMessages(options.messages);
219
238
  const sessionId = options.sessionId || randomUUID();
220
239
  const userId = generateUserId(sessionId);
221
- const customHeaders = getCustomHeaders();
222
240
  const requestBody = {
223
241
  model: options.model,
224
242
  max_tokens: options.max_tokens || 4096,
@@ -231,15 +249,40 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
231
249
  },
232
250
  stream: true
233
251
  };
234
- const stream = await client.messages.create(requestBody, {
235
- headers: customHeaders
252
+ // Prepare headers
253
+ const headers = {
254
+ 'Content-Type': 'application/json',
255
+ 'x-api-key': config.apiKey,
256
+ 'authorization': `Bearer ${config.apiKey}`,
257
+ 'anthropic-version': '2023-06-01',
258
+ ...config.customHeaders
259
+ };
260
+ // Add beta parameter if configured
261
+ // if (config.anthropicBeta) {
262
+ // headers['anthropic-beta'] = 'prompt-caching-2024-07-31';
263
+ // }
264
+ const url = config.anthropicBeta
265
+ ? `${config.baseUrl}/messages?beta=true`
266
+ : `${config.baseUrl}/messages`;
267
+ const response = await fetch(url, {
268
+ method: 'POST',
269
+ headers,
270
+ body: JSON.stringify(requestBody),
271
+ signal: abortSignal
236
272
  });
273
+ if (!response.ok) {
274
+ const errorText = await response.text();
275
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`);
276
+ }
277
+ if (!response.body) {
278
+ throw new Error('No response body from Anthropic API');
279
+ }
237
280
  let contentBuffer = '';
238
281
  let toolCallsBuffer = new Map();
239
282
  let hasToolCalls = false;
240
283
  let usageData;
241
284
  let blockIndexToId = new Map();
242
- for await (const event of stream) {
285
+ for await (const event of parseSSEStream(response.body.getReader())) {
243
286
  if (abortSignal?.aborted) {
244
287
  return;
245
288
  }
@@ -1,24 +1,5 @@
1
- import type { ChatCompletionTool } from 'openai/resources/chat/completions';
2
- export interface ImageContent {
3
- type: 'image';
4
- data: string;
5
- mimeType: string;
6
- }
7
- export interface ChatMessage {
8
- role: 'system' | 'user' | 'assistant' | 'tool';
9
- content: string;
10
- tool_call_id?: string;
11
- tool_calls?: ToolCall[];
12
- images?: ImageContent[];
13
- }
14
- export interface ToolCall {
15
- id: string;
16
- type: 'function';
17
- function: {
18
- name: string;
19
- arguments: string;
20
- };
21
- }
1
+ import type { ChatMessage, ChatCompletionTool, ToolCall, UsageInfo, ImageContent } from './types.js';
2
+ export type { ChatMessage, ChatCompletionTool, ToolCall, UsageInfo, ImageContent };
22
3
  export interface ChatCompletionOptions {
23
4
  model: string;
24
5
  messages: ChatMessage[];
@@ -56,15 +37,19 @@ export interface ChatCompletionChunk {
56
37
  finish_reason?: string | null;
57
38
  }>;
58
39
  }
59
- export declare function resetOpenAIClient(): void;
60
- export interface UsageInfo {
61
- prompt_tokens: number;
62
- completion_tokens: number;
63
- total_tokens: number;
64
- cache_creation_input_tokens?: number;
65
- cache_read_input_tokens?: number;
66
- cached_tokens?: number;
40
+ export interface ChatCompletionMessageParam {
41
+ role: 'system' | 'user' | 'assistant' | 'tool';
42
+ content: string | Array<{
43
+ type: 'text' | 'image_url';
44
+ text?: string;
45
+ image_url?: {
46
+ url: string;
47
+ };
48
+ }>;
49
+ tool_call_id?: string;
50
+ tool_calls?: ToolCall[];
67
51
  }
52
+ export declare function resetOpenAIClient(): void;
68
53
  export interface StreamChunk {
69
54
  type: 'content' | 'tool_calls' | 'tool_call_delta' | 'reasoning_delta' | 'reasoning_started' | 'done' | 'usage';
70
55
  content?: string;
package/dist/api/chat.js CHANGED
@@ -1,4 +1,3 @@
1
- import OpenAI from 'openai';
2
1
  import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
3
2
  import { SYSTEM_PROMPT } from './systemPrompt.js';
4
3
  import { withRetryGenerator } from '../utils/retryUtils.js';
@@ -89,54 +88,98 @@ function convertToOpenAIMessages(messages, includeSystemPrompt = true) {
89
88
  }
90
89
  return result;
91
90
  }
92
- let openaiClient = null;
93
- function getOpenAIClient() {
94
- if (!openaiClient) {
91
+ let openaiConfig = null;
92
+ function getOpenAIConfig() {
93
+ if (!openaiConfig) {
95
94
  const config = getOpenAiConfig();
96
95
  if (!config.apiKey || !config.baseUrl) {
97
96
  throw new Error('OpenAI API configuration is incomplete. Please configure API settings first.');
98
97
  }
99
- // Get custom headers
100
98
  const customHeaders = getCustomHeaders();
101
- openaiClient = new OpenAI({
99
+ openaiConfig = {
102
100
  apiKey: config.apiKey,
103
- baseURL: config.baseUrl,
104
- defaultHeaders: {
105
- ...customHeaders
106
- }
107
- });
101
+ baseUrl: config.baseUrl,
102
+ customHeaders
103
+ };
108
104
  }
109
- return openaiClient;
105
+ return openaiConfig;
110
106
  }
111
107
  export function resetOpenAIClient() {
112
- openaiClient = null;
108
+ openaiConfig = null;
109
+ }
110
+ /**
111
+ * Parse Server-Sent Events (SSE) stream
112
+ */
113
+ async function* parseSSEStream(reader) {
114
+ const decoder = new TextDecoder();
115
+ let buffer = '';
116
+ while (true) {
117
+ const { done, value } = await reader.read();
118
+ if (done)
119
+ break;
120
+ buffer += decoder.decode(value, { stream: true });
121
+ const lines = buffer.split('\n');
122
+ buffer = lines.pop() || '';
123
+ for (const line of lines) {
124
+ const trimmed = line.trim();
125
+ if (!trimmed || trimmed.startsWith(':'))
126
+ continue;
127
+ if (trimmed === 'data: [DONE]') {
128
+ return;
129
+ }
130
+ if (trimmed.startsWith('data: ')) {
131
+ const data = trimmed.slice(6);
132
+ try {
133
+ yield JSON.parse(data);
134
+ }
135
+ catch (e) {
136
+ console.error('Failed to parse SSE data:', data);
137
+ }
138
+ }
139
+ }
140
+ }
113
141
  }
114
142
  /**
115
143
  * Simple streaming chat completion - only handles OpenAI interaction
116
144
  * Tool execution should be handled by the caller
117
145
  */
118
146
  export async function* createStreamingChatCompletion(options, abortSignal, onRetry) {
119
- const client = getOpenAIClient();
147
+ const config = getOpenAIConfig();
120
148
  // 使用重试包装生成器
121
149
  yield* withRetryGenerator(async function* () {
122
- const stream = await client.chat.completions.create({
150
+ const requestBody = {
123
151
  model: options.model,
124
152
  messages: convertToOpenAIMessages(options.messages),
125
153
  stream: true,
126
- stream_options: { include_usage: true }, // Request usage data in stream
154
+ stream_options: { include_usage: true },
127
155
  temperature: options.temperature || 0.7,
128
156
  max_tokens: options.max_tokens,
129
157
  tools: options.tools,
130
158
  tool_choice: options.tool_choice,
131
- }, {
132
- signal: abortSignal,
159
+ };
160
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
161
+ method: 'POST',
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ 'Authorization': `Bearer ${config.apiKey}`,
165
+ ...config.customHeaders
166
+ },
167
+ body: JSON.stringify(requestBody),
168
+ signal: abortSignal
133
169
  });
170
+ if (!response.ok) {
171
+ const errorText = await response.text();
172
+ throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`);
173
+ }
174
+ if (!response.body) {
175
+ throw new Error('No response body from OpenAI API');
176
+ }
134
177
  let contentBuffer = '';
135
178
  let toolCallsBuffer = {};
136
179
  let hasToolCalls = false;
137
180
  let usageData;
138
181
  let reasoningStarted = false; // Track if reasoning has started
139
- for await (const chunk of stream) {
182
+ for await (const chunk of parseSSEStream(response.body.getReader())) {
140
183
  if (abortSignal?.aborted) {
141
184
  return;
142
185
  }
@@ -1,19 +1,10 @@
1
- import type { ChatMessage } from './chat.js';
2
- import type { ChatCompletionTool } from 'openai/resources/chat/completions';
1
+ import type { ChatMessage, ChatCompletionTool, UsageInfo } from './types.js';
3
2
  export interface GeminiOptions {
4
3
  model: string;
5
4
  messages: ChatMessage[];
6
5
  temperature?: number;
7
6
  tools?: ChatCompletionTool[];
8
7
  }
9
- export interface UsageInfo {
10
- prompt_tokens: number;
11
- completion_tokens: number;
12
- total_tokens: number;
13
- cache_creation_input_tokens?: number;
14
- cache_read_input_tokens?: number;
15
- cached_tokens?: number;
16
- }
17
8
  export interface GeminiStreamChunk {
18
9
  type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage';
19
10
  content?: string;
@@ -1,44 +1,26 @@
1
- import { GoogleGenAI } from '@google/genai';
2
1
  import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
3
2
  import { SYSTEM_PROMPT } from './systemPrompt.js';
4
3
  import { withRetryGenerator } from '../utils/retryUtils.js';
5
- let geminiClient = null;
6
- function getGeminiClient() {
7
- if (!geminiClient) {
4
+ let geminiConfig = null;
5
+ function getGeminiConfig() {
6
+ if (!geminiConfig) {
8
7
  const config = getOpenAiConfig();
9
8
  if (!config.apiKey) {
10
9
  throw new Error('Gemini API configuration is incomplete. Please configure API key first.');
11
10
  }
12
- // Create client configuration
13
- const clientConfig = {
14
- apiKey: config.apiKey
15
- };
16
- // Get custom headers
17
11
  const customHeaders = getCustomHeaders();
18
- // Support custom baseUrl and headers for proxy servers
19
- if (config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1') {
20
- clientConfig.httpOptions = {
21
- baseUrl: config.baseUrl,
22
- headers: {
23
- 'x-goog-api-key': config.apiKey, // Gemini API requires this header
24
- ...customHeaders
25
- }
26
- };
27
- }
28
- else if (Object.keys(customHeaders).length > 0) {
29
- // If using default base URL but have custom headers
30
- clientConfig.httpOptions = {
31
- headers: {
32
- ...customHeaders
33
- }
34
- };
35
- }
36
- geminiClient = new GoogleGenAI(clientConfig);
12
+ geminiConfig = {
13
+ apiKey: config.apiKey,
14
+ baseUrl: config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1'
15
+ ? config.baseUrl
16
+ : 'https://generativelanguage.googleapis.com/v1beta',
17
+ customHeaders
18
+ };
37
19
  }
38
- return geminiClient;
20
+ return geminiConfig;
39
21
  }
40
22
  export function resetGeminiClient() {
41
- geminiClient = null;
23
+ geminiConfig = null;
42
24
  }
43
25
  /**
44
26
  * Convert OpenAI-style tools to Gemini function declarations
@@ -213,81 +195,121 @@ function convertToGeminiMessages(messages) {
213
195
  * Create streaming chat completion using Gemini API
214
196
  */
215
197
  export async function* createStreamingGeminiCompletion(options, abortSignal, onRetry) {
216
- const client = getGeminiClient();
198
+ const config = getGeminiConfig();
217
199
  // 使用重试包装生成器
218
200
  yield* withRetryGenerator(async function* () {
219
201
  const { systemInstruction, contents } = convertToGeminiMessages(options.messages);
220
- // Build request config
221
- const requestConfig = {
222
- model: options.model,
202
+ // Build request payload
203
+ const requestBody = {
223
204
  contents,
224
- config: {
225
- systemInstruction,
205
+ systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
206
+ generationConfig: {
226
207
  temperature: options.temperature ?? 0.7,
227
208
  }
228
209
  };
229
210
  // Add tools if provided
230
211
  const geminiTools = convertToolsToGemini(options.tools);
231
212
  if (geminiTools) {
232
- requestConfig.config.tools = geminiTools;
213
+ requestBody.tools = geminiTools;
214
+ }
215
+ // Extract model name from options.model (e.g., "gemini-pro" or "models/gemini-pro")
216
+ const modelName = options.model.startsWith('models/') ? options.model : `models/${options.model}`;
217
+ const url = `${config.baseUrl}/${modelName}:streamGenerateContent?key=${config.apiKey}&alt=sse`;
218
+ const response = await fetch(url, {
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ 'authorization': `Bearer ${config.apiKey}`,
223
+ ...config.customHeaders
224
+ },
225
+ body: JSON.stringify(requestBody),
226
+ signal: abortSignal
227
+ });
228
+ if (!response.ok) {
229
+ const errorText = await response.text();
230
+ throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errorText}`);
231
+ }
232
+ if (!response.body) {
233
+ throw new Error('No response body from Gemini API');
233
234
  }
234
- // Stream the response
235
- const stream = await client.models.generateContentStream(requestConfig);
236
235
  let contentBuffer = '';
237
236
  let toolCallsBuffer = [];
238
237
  let hasToolCalls = false;
239
238
  let toolCallIndex = 0;
240
239
  let totalTokens = { prompt: 0, completion: 0, total: 0 };
241
- // Save original console.warn to suppress SDK warnings
242
- const originalWarn = console.warn;
243
- console.warn = () => { }; // Suppress "there are non-text parts" warnings
244
- for await (const chunk of stream) {
240
+ // Parse SSE stream
241
+ const reader = response.body.getReader();
242
+ const decoder = new TextDecoder();
243
+ let buffer = '';
244
+ while (true) {
245
+ const { done, value } = await reader.read();
246
+ if (done)
247
+ break;
245
248
  if (abortSignal?.aborted) {
246
- console.warn = originalWarn; // Restore console.warn
247
249
  return;
248
250
  }
249
- // Process text content
250
- if (chunk.text) {
251
- contentBuffer += chunk.text;
252
- yield {
253
- type: 'content',
254
- content: chunk.text
255
- };
256
- }
257
- // Process function calls using the official API
258
- if (chunk.functionCalls && chunk.functionCalls.length > 0) {
259
- hasToolCalls = true;
260
- for (const fc of chunk.functionCalls) {
261
- if (!fc.name)
262
- continue;
263
- const toolCall = {
264
- id: `call_${toolCallIndex++}`,
265
- type: 'function',
266
- function: {
267
- name: fc.name,
268
- arguments: JSON.stringify(fc.args)
251
+ buffer += decoder.decode(value, { stream: true });
252
+ const lines = buffer.split('\n');
253
+ buffer = lines.pop() || '';
254
+ for (const line of lines) {
255
+ const trimmed = line.trim();
256
+ if (!trimmed || trimmed.startsWith(':'))
257
+ continue;
258
+ if (trimmed.startsWith('data: ')) {
259
+ const data = trimmed.slice(6);
260
+ try {
261
+ const chunk = JSON.parse(data);
262
+ // Process candidates
263
+ if (chunk.candidates && chunk.candidates.length > 0) {
264
+ const candidate = chunk.candidates[0];
265
+ if (candidate.content && candidate.content.parts) {
266
+ for (const part of candidate.content.parts) {
267
+ // Process text content
268
+ if (part.text) {
269
+ contentBuffer += part.text;
270
+ yield {
271
+ type: 'content',
272
+ content: part.text
273
+ };
274
+ }
275
+ // Process function calls
276
+ if (part.functionCall) {
277
+ hasToolCalls = true;
278
+ const fc = part.functionCall;
279
+ const toolCall = {
280
+ id: `call_${toolCallIndex++}`,
281
+ type: 'function',
282
+ function: {
283
+ name: fc.name,
284
+ arguments: JSON.stringify(fc.args || {})
285
+ }
286
+ };
287
+ toolCallsBuffer.push(toolCall);
288
+ // Yield delta for token counting
289
+ const deltaText = fc.name + JSON.stringify(fc.args || {});
290
+ yield {
291
+ type: 'tool_call_delta',
292
+ delta: deltaText
293
+ };
294
+ }
295
+ }
296
+ }
269
297
  }
270
- };
271
- toolCallsBuffer.push(toolCall);
272
- // Yield delta for token counting
273
- const deltaText = fc.name + JSON.stringify(fc.args);
274
- yield {
275
- type: 'tool_call_delta',
276
- delta: deltaText
277
- };
298
+ // Track usage info
299
+ if (chunk.usageMetadata) {
300
+ totalTokens = {
301
+ prompt: chunk.usageMetadata.promptTokenCount || 0,
302
+ completion: chunk.usageMetadata.candidatesTokenCount || 0,
303
+ total: chunk.usageMetadata.totalTokenCount || 0
304
+ };
305
+ }
306
+ }
307
+ catch (e) {
308
+ console.error('Failed to parse Gemini SSE data:', data);
309
+ }
278
310
  }
279
311
  }
280
- // Track usage info
281
- if (chunk.usageMetadata) {
282
- totalTokens = {
283
- prompt: chunk.usageMetadata.promptTokenCount || 0,
284
- completion: chunk.usageMetadata.candidatesTokenCount || 0,
285
- total: chunk.usageMetadata.totalTokenCount || 0
286
- };
287
- }
288
312
  }
289
- // Restore console.warn
290
- console.warn = originalWarn;
291
313
  // Yield tool calls if any
292
314
  if (hasToolCalls && toolCallsBuffer.length > 0) {
293
315
  yield {