snow-ai 0.4.1 → 0.4.4

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.
@@ -41,7 +41,4 @@ export interface AnthropicMessageParam {
41
41
  content: string | Array<any>;
42
42
  }
43
43
  export declare function resetAnthropicClient(): void;
44
- /**
45
- * Create streaming chat completion using Anthropic API
46
- */
47
44
  export declare function createStreamingAnthropicCompletion(options: AnthropicOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): AsyncGenerator<AnthropicStreamChunk, void, unknown>;
@@ -251,43 +251,58 @@ function convertToAnthropicMessages(messages, includeBuiltinSystemPrompt = true)
251
251
  async function* parseSSEStream(reader) {
252
252
  const decoder = new TextDecoder();
253
253
  let buffer = '';
254
- while (true) {
255
- const { done, value } = await reader.read();
256
- if (done)
257
- break;
258
- buffer += decoder.decode(value, { stream: true });
259
- const lines = buffer.split('\n');
260
- buffer = lines.pop() || '';
261
- for (const line of lines) {
262
- const trimmed = line.trim();
263
- if (!trimmed || trimmed.startsWith(':'))
264
- continue;
265
- if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
266
- return;
267
- }
268
- // Handle both "event: " and "event:" formats
269
- if (trimmed.startsWith('event:')) {
270
- // Event type, will be followed by data
271
- continue;
254
+ try {
255
+ while (true) {
256
+ const { done, value } = await reader.read();
257
+ if (done) {
258
+ // 关键修复:检查buffer是否有残留数据
259
+ if (buffer.trim()) {
260
+ // 连接异常中断,抛出明确错误
261
+ throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
262
+ }
263
+ break; // 正常结束
272
264
  }
273
- // Handle both "data: " and "data:" formats
274
- if (trimmed.startsWith('data:')) {
275
- const data = trimmed.startsWith('data: ')
276
- ? trimmed.slice(6)
277
- : trimmed.slice(5);
278
- try {
279
- yield JSON.parse(data);
265
+ buffer += decoder.decode(value, { stream: true });
266
+ const lines = buffer.split('\n');
267
+ buffer = lines.pop() || '';
268
+ for (const line of lines) {
269
+ const trimmed = line.trim();
270
+ if (!trimmed || trimmed.startsWith(':'))
271
+ continue;
272
+ if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
273
+ return;
280
274
  }
281
- catch (e) {
282
- logger.error('Failed to parse SSE data:', data);
275
+ // Handle both "event: " and "event:" formats
276
+ if (trimmed.startsWith('event:')) {
277
+ // Event type, will be followed by data
278
+ continue;
279
+ }
280
+ // Handle both "data: " and "data:" formats
281
+ if (trimmed.startsWith('data:')) {
282
+ const data = trimmed.startsWith('data: ')
283
+ ? trimmed.slice(6)
284
+ : trimmed.slice(5);
285
+ const parseResult = parseJsonWithFix(data, {
286
+ toolName: 'SSE stream',
287
+ logWarning: false,
288
+ logError: true,
289
+ });
290
+ if (parseResult.success) {
291
+ yield parseResult.data;
292
+ }
283
293
  }
284
294
  }
285
295
  }
286
296
  }
297
+ catch (error) {
298
+ const { logger } = await import('../utils/logger.js');
299
+ logger.error('SSE stream parsing error:', {
300
+ error: error instanceof Error ? error.message : 'Unknown error',
301
+ remainingBuffer: buffer.substring(0, 200),
302
+ });
303
+ throw error;
304
+ }
287
305
  }
288
- /**
289
- * Create streaming chat completion using Anthropic API
290
- */
291
306
  export async function* createStreamingAnthropicCompletion(options, abortSignal, onRetry) {
292
307
  yield* withRetryGenerator(async function* () {
293
308
  const config = getAnthropicConfig();
package/dist/api/chat.js CHANGED
@@ -126,41 +126,57 @@ export function resetOpenAIClient() {
126
126
  async function* parseSSEStream(reader) {
127
127
  const decoder = new TextDecoder();
128
128
  let buffer = '';
129
- while (true) {
130
- const { done, value } = await reader.read();
131
- if (done)
132
- break;
133
- buffer += decoder.decode(value, { stream: true });
134
- const lines = buffer.split('\n');
135
- buffer = lines.pop() || '';
136
- for (const line of lines) {
137
- const trimmed = line.trim();
138
- if (!trimmed || trimmed.startsWith(':'))
139
- continue;
140
- if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
141
- return;
142
- }
143
- // Handle both "event: " and "event:" formats
144
- if (trimmed.startsWith('event:')) {
145
- // Event type, will be followed by data
146
- continue;
129
+ try {
130
+ while (true) {
131
+ const { done, value } = await reader.read();
132
+ if (done) {
133
+ // 关键修复:检查buffer是否有残留数据
134
+ if (buffer.trim()) {
135
+ // 连接异常中断,抛出明确错误
136
+ throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
137
+ }
138
+ break; // 正常结束
147
139
  }
148
- // Handle both "data: " and "data:" formats
149
- if (trimmed.startsWith('data:')) {
150
- const data = trimmed.startsWith('data: ')
151
- ? trimmed.slice(6)
152
- : trimmed.slice(5);
153
- const parseResult = parseJsonWithFix(data, {
154
- toolName: 'SSE stream',
155
- logWarning: false,
156
- logError: true,
157
- });
158
- if (parseResult.success) {
159
- yield parseResult.data;
140
+ buffer += decoder.decode(value, { stream: true });
141
+ const lines = buffer.split('\n');
142
+ buffer = lines.pop() || '';
143
+ for (const line of lines) {
144
+ const trimmed = line.trim();
145
+ if (!trimmed || trimmed.startsWith(':'))
146
+ continue;
147
+ if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
148
+ return;
149
+ }
150
+ // Handle both "event: " and "event:" formats
151
+ if (trimmed.startsWith('event:')) {
152
+ // Event type, will be followed by data
153
+ continue;
154
+ }
155
+ // Handle both "data: " and "data:" formats
156
+ if (trimmed.startsWith('data:')) {
157
+ const data = trimmed.startsWith('data: ')
158
+ ? trimmed.slice(6)
159
+ : trimmed.slice(5);
160
+ const parseResult = parseJsonWithFix(data, {
161
+ toolName: 'SSE stream',
162
+ logWarning: false,
163
+ logError: true,
164
+ });
165
+ if (parseResult.success) {
166
+ yield parseResult.data;
167
+ }
160
168
  }
161
169
  }
162
170
  }
163
171
  }
172
+ catch (error) {
173
+ const { logger } = await import('../utils/logger.js');
174
+ logger.error('SSE stream parsing error:', {
175
+ error: error instanceof Error ? error.message : 'Unknown error',
176
+ remainingBuffer: buffer.substring(0, 200),
177
+ });
178
+ throw error;
179
+ }
164
180
  }
165
181
  /**
166
182
  * Simple streaming chat completion - only handles OpenAI interaction
@@ -280,97 +280,113 @@ export async function* createStreamingGeminiCompletion(options, abortSignal, onR
280
280
  const reader = response.body.getReader();
281
281
  const decoder = new TextDecoder();
282
282
  let buffer = '';
283
- while (true) {
284
- const { done, value } = await reader.read();
285
- if (done)
286
- break;
287
- if (abortSignal?.aborted) {
288
- return;
289
- }
290
- buffer += decoder.decode(value, { stream: true });
291
- const lines = buffer.split('\n');
292
- buffer = lines.pop() || '';
293
- for (const line of lines) {
294
- const trimmed = line.trim();
295
- if (!trimmed || trimmed.startsWith(':'))
296
- continue;
297
- if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
298
- break;
283
+ try {
284
+ while (true) {
285
+ const { done, value } = await reader.read();
286
+ if (done) {
287
+ // 关键修复:检查buffer是否有残留数据
288
+ if (buffer.trim()) {
289
+ // 连接异常中断,抛出明确错误
290
+ throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
291
+ }
292
+ break; // 正常结束
299
293
  }
300
- // Handle both "event: " and "event:" formats
301
- if (trimmed.startsWith('event:')) {
302
- // Event type, will be followed by data
303
- continue;
294
+ if (abortSignal?.aborted) {
295
+ return;
304
296
  }
305
- // Handle both "data: " and "data:" formats
306
- if (trimmed.startsWith('data:')) {
307
- const data = trimmed.startsWith('data: ')
308
- ? trimmed.slice(6)
309
- : trimmed.slice(5);
310
- const parseResult = parseJsonWithFix(data, {
311
- toolName: 'Gemini SSE stream',
312
- logWarning: false,
313
- logError: true,
314
- });
315
- if (parseResult.success) {
316
- const chunk = parseResult.data;
317
- // Process candidates
318
- if (chunk.candidates && chunk.candidates.length > 0) {
319
- const candidate = chunk.candidates[0];
320
- if (candidate.content && candidate.content.parts) {
321
- for (const part of candidate.content.parts) {
322
- // Process thought content (Gemini thinking)
323
- // When part.thought === true, the text field contains thinking content
324
- if (part.thought === true && part.text) {
325
- thinkingTextBuffer += part.text;
326
- yield {
327
- type: 'reasoning_delta',
328
- delta: part.text,
329
- };
330
- }
331
- // Process regular text content (when thought is not true)
332
- else if (part.text) {
333
- contentBuffer += part.text;
334
- yield {
335
- type: 'content',
336
- content: part.text,
337
- };
338
- }
339
- // Process function calls
340
- if (part.functionCall) {
341
- hasToolCalls = true;
342
- const fc = part.functionCall;
343
- const toolCall = {
344
- id: `call_${toolCallIndex++}`,
345
- type: 'function',
346
- function: {
347
- name: fc.name,
348
- arguments: JSON.stringify(fc.args || {}),
349
- },
350
- };
351
- toolCallsBuffer.push(toolCall);
352
- // Yield delta for token counting
353
- const deltaText = fc.name + JSON.stringify(fc.args || {});
354
- yield {
355
- type: 'tool_call_delta',
356
- delta: deltaText,
357
- };
297
+ buffer += decoder.decode(value, { stream: true });
298
+ const lines = buffer.split('\n');
299
+ buffer = lines.pop() || '';
300
+ for (const line of lines) {
301
+ const trimmed = line.trim();
302
+ if (!trimmed || trimmed.startsWith(':'))
303
+ continue;
304
+ if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
305
+ break;
306
+ }
307
+ // Handle both "event: " and "event:" formats
308
+ if (trimmed.startsWith('event:')) {
309
+ // Event type, will be followed by data
310
+ continue;
311
+ }
312
+ // Handle both "data: " and "data:" formats
313
+ if (trimmed.startsWith('data:')) {
314
+ const data = trimmed.startsWith('data: ')
315
+ ? trimmed.slice(6)
316
+ : trimmed.slice(5);
317
+ const parseResult = parseJsonWithFix(data, {
318
+ toolName: 'Gemini SSE stream',
319
+ logWarning: false,
320
+ logError: true,
321
+ });
322
+ if (parseResult.success) {
323
+ const chunk = parseResult.data;
324
+ // Process candidates
325
+ if (chunk.candidates && chunk.candidates.length > 0) {
326
+ const candidate = chunk.candidates[0];
327
+ if (candidate.content && candidate.content.parts) {
328
+ for (const part of candidate.content.parts) {
329
+ // Process thought content (Gemini thinking)
330
+ // When part.thought === true, the text field contains thinking content
331
+ if (part.thought === true && part.text) {
332
+ thinkingTextBuffer += part.text;
333
+ yield {
334
+ type: 'reasoning_delta',
335
+ delta: part.text,
336
+ };
337
+ }
338
+ // Process regular text content (when thought is not true)
339
+ else if (part.text) {
340
+ contentBuffer += part.text;
341
+ yield {
342
+ type: 'content',
343
+ content: part.text,
344
+ };
345
+ }
346
+ // Process function calls
347
+ if (part.functionCall) {
348
+ hasToolCalls = true;
349
+ const fc = part.functionCall;
350
+ const toolCall = {
351
+ id: `call_${toolCallIndex++}`,
352
+ type: 'function',
353
+ function: {
354
+ name: fc.name,
355
+ arguments: JSON.stringify(fc.args || {}),
356
+ },
357
+ };
358
+ toolCallsBuffer.push(toolCall);
359
+ // Yield delta for token counting
360
+ const deltaText = fc.name + JSON.stringify(fc.args || {});
361
+ yield {
362
+ type: 'tool_call_delta',
363
+ delta: deltaText,
364
+ };
365
+ }
358
366
  }
359
367
  }
360
368
  }
361
- }
362
- // Track usage info
363
- if (chunk.usageMetadata) {
364
- totalTokens = {
365
- prompt: chunk.usageMetadata.promptTokenCount || 0,
366
- completion: chunk.usageMetadata.candidatesTokenCount || 0,
367
- total: chunk.usageMetadata.totalTokenCount || 0,
368
- };
369
+ // Track usage info
370
+ if (chunk.usageMetadata) {
371
+ totalTokens = {
372
+ prompt: chunk.usageMetadata.promptTokenCount || 0,
373
+ completion: chunk.usageMetadata.candidatesTokenCount || 0,
374
+ total: chunk.usageMetadata.totalTokenCount || 0,
375
+ };
376
+ }
369
377
  }
370
378
  }
371
379
  }
372
380
  }
373
381
  }
382
+ catch (error) {
383
+ const { logger } = await import('../utils/logger.js');
384
+ logger.error('Gemini SSE stream parsing error:', {
385
+ error: error instanceof Error ? error.message : 'Unknown error',
386
+ remainingBuffer: buffer.substring(0, 200),
387
+ });
388
+ throw error;
389
+ }
374
390
  // Yield tool calls if any
375
391
  if (hasToolCalls && toolCallsBuffer.length > 0) {
376
392
  yield {
@@ -207,41 +207,57 @@ function convertToResponseInput(messages, includeBuiltinSystemPrompt = true) {
207
207
  async function* parseSSEStream(reader) {
208
208
  const decoder = new TextDecoder();
209
209
  let buffer = '';
210
- while (true) {
211
- const { done, value } = await reader.read();
212
- if (done)
213
- break;
214
- buffer += decoder.decode(value, { stream: true });
215
- const lines = buffer.split('\n');
216
- buffer = lines.pop() || '';
217
- for (const line of lines) {
218
- const trimmed = line.trim();
219
- if (!trimmed || trimmed.startsWith(':'))
220
- continue;
221
- if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
222
- return;
223
- }
224
- // Handle both "event: " and "event:" formats
225
- if (trimmed.startsWith('event:')) {
226
- // Event type, will be followed by data
227
- continue;
210
+ try {
211
+ while (true) {
212
+ const { done, value } = await reader.read();
213
+ if (done) {
214
+ // 关键修复:检查buffer是否有残留数据
215
+ if (buffer.trim()) {
216
+ // 连接异常中断,抛出明确错误
217
+ throw new Error(`Stream terminated unexpectedly with incomplete data: ${buffer.substring(0, 100)}...`);
218
+ }
219
+ break; // 正常结束
228
220
  }
229
- // Handle both "data: " and "data:" formats
230
- if (trimmed.startsWith('data:')) {
231
- const data = trimmed.startsWith('data: ')
232
- ? trimmed.slice(6)
233
- : trimmed.slice(5);
234
- const parseResult = parseJsonWithFix(data, {
235
- toolName: 'Responses API SSE stream',
236
- logWarning: false,
237
- logError: true,
238
- });
239
- if (parseResult.success) {
240
- yield parseResult.data;
221
+ buffer += decoder.decode(value, { stream: true });
222
+ const lines = buffer.split('\n');
223
+ buffer = lines.pop() || '';
224
+ for (const line of lines) {
225
+ const trimmed = line.trim();
226
+ if (!trimmed || trimmed.startsWith(':'))
227
+ continue;
228
+ if (trimmed === 'data: [DONE]' || trimmed === 'data:[DONE]') {
229
+ return;
230
+ }
231
+ // Handle both "event: " and "event:" formats
232
+ if (trimmed.startsWith('event:')) {
233
+ // Event type, will be followed by data
234
+ continue;
235
+ }
236
+ // Handle both "data: " and "data:" formats
237
+ if (trimmed.startsWith('data:')) {
238
+ const data = trimmed.startsWith('data: ')
239
+ ? trimmed.slice(6)
240
+ : trimmed.slice(5);
241
+ const parseResult = parseJsonWithFix(data, {
242
+ toolName: 'Responses API SSE stream',
243
+ logWarning: false,
244
+ logError: true,
245
+ });
246
+ if (parseResult.success) {
247
+ yield parseResult.data;
248
+ }
241
249
  }
242
250
  }
243
251
  }
244
252
  }
253
+ catch (error) {
254
+ const { logger } = await import('../utils/logger.js');
255
+ logger.error('Responses API SSE stream parsing error:', {
256
+ error: error instanceof Error ? error.message : 'Unknown error',
257
+ remainingBuffer: buffer.substring(0, 200),
258
+ });
259
+ throw error;
260
+ }
245
261
  }
246
262
  /**
247
263
  * 使用 Responses API 创建流式响应(带自动工具调用)
@@ -1041,9 +1041,12 @@ export async function handleConversationWithTools(options) {
1041
1041
  freeEncoder();
1042
1042
  }
1043
1043
  catch (error) {
1044
- freeEncoder();
1045
1044
  throw error;
1046
1045
  }
1046
+ finally {
1047
+ // ✅ 确保总是释放encoder资源,避免资源泄漏
1048
+ freeEncoder();
1049
+ }
1047
1050
  // Return the accumulated usage data
1048
1051
  return { usage: accumulatedUsage };
1049
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -56,6 +56,7 @@
56
56
  "ink-spinner": "^5.0.0",
57
57
  "ink-text-input": "^6.0.0",
58
58
  "meow": "^11.0.0",
59
+ "prettier": "^2.8.7",
59
60
  "puppeteer-core": "^24.25.0",
60
61
  "react": "^18.2.0",
61
62
  "string-width": "^7.2.0",
@@ -75,7 +76,6 @@
75
76
  "eslint-plugin-react": "^7.32.2",
76
77
  "eslint-plugin-react-hooks": "^4.6.0",
77
78
  "ink-testing-library": "^3.0.0",
78
- "prettier": "^2.8.7",
79
79
  "ts-node": "^10.9.1",
80
80
  "typescript": "^5.0.3",
81
81
  "xo": "^0.53.1"