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,20 +1,45 @@
1
- import OpenAI from 'openai';
2
- import { GoogleGenAI } from '@google/genai';
3
- import Anthropic from '@anthropic-ai/sdk';
4
1
  import { getOpenAiConfig, getCustomHeaders, getCustomSystemPrompt } from './apiConfig.js';
5
2
  import { SYSTEM_PROMPT } from '../api/systemPrompt.js';
6
3
  /**
7
4
  * Compression request prompt - asks AI to summarize conversation with focus on task continuity
8
5
  */
9
6
  const COMPRESSION_PROMPT = 'Please provide a concise summary of our conversation so far. Focus on: 1) The current task or goal we are working on, 2) Key decisions and approaches we have agreed upon, 3) Important context needed to continue, 4) Any pending or unfinished work. Keep it brief but ensure I can seamlessly continue assisting with the task.';
7
+ /**
8
+ * Parse Server-Sent Events (SSE) stream
9
+ */
10
+ async function* parseSSEStream(reader) {
11
+ const decoder = new TextDecoder();
12
+ let buffer = '';
13
+ while (true) {
14
+ const { done, value } = await reader.read();
15
+ if (done)
16
+ break;
17
+ buffer += decoder.decode(value, { stream: true });
18
+ const lines = buffer.split('\n');
19
+ buffer = lines.pop() || '';
20
+ for (const line of lines) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith(':'))
23
+ continue;
24
+ if (trimmed === 'data: [DONE]') {
25
+ return;
26
+ }
27
+ if (trimmed.startsWith('data: ')) {
28
+ const data = trimmed.slice(6);
29
+ try {
30
+ yield JSON.parse(data);
31
+ }
32
+ catch (e) {
33
+ console.error('Failed to parse SSE data:', data);
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
10
39
  /**
11
40
  * Compress context using OpenAI Chat Completions API
12
41
  */
13
42
  async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
14
- const client = new OpenAI({
15
- apiKey,
16
- baseURL: baseUrl,
17
- });
18
43
  const customHeaders = getCustomHeaders();
19
44
  // Build messages with system prompt support
20
45
  const messages = [];
@@ -48,17 +73,29 @@ async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversat
48
73
  stream: true,
49
74
  stream_options: { include_usage: true },
50
75
  };
51
- // Use streaming to avoid timeout
52
- const stream = (await client.chat.completions.create(requestPayload, {
53
- headers: customHeaders,
54
- }));
76
+ const response = await fetch(`${baseUrl}/chat/completions`, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'Authorization': `Bearer ${apiKey}`,
81
+ ...customHeaders
82
+ },
83
+ body: JSON.stringify(requestPayload)
84
+ });
85
+ if (!response.ok) {
86
+ const errorText = await response.text();
87
+ throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`);
88
+ }
89
+ if (!response.body) {
90
+ throw new Error('No response body from OpenAI API');
91
+ }
55
92
  let summary = '';
56
93
  let usage = {
57
94
  prompt_tokens: 0,
58
95
  completion_tokens: 0,
59
96
  total_tokens: 0,
60
97
  };
61
- for await (const chunk of stream) {
98
+ for await (const chunk of parseSSEStream(response.body.getReader())) {
62
99
  const delta = chunk.choices[0]?.delta;
63
100
  if (delta?.content) {
64
101
  summary += delta.content;
@@ -84,10 +121,6 @@ async function compressWithChatCompletions(baseUrl, apiKey, modelName, conversat
84
121
  * Compress context using OpenAI Responses API
85
122
  */
86
123
  async function compressWithResponses(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
87
- const client = new OpenAI({
88
- apiKey,
89
- baseURL: baseUrl,
90
- });
91
124
  const customHeaders = getCustomHeaders();
92
125
  // Build instructions
93
126
  const instructions = systemPrompt || SYSTEM_PROMPT;
@@ -130,17 +163,29 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
130
163
  input,
131
164
  stream: true,
132
165
  };
133
- // Use streaming to avoid timeout
134
- const stream = await client.responses.create(requestPayload, {
135
- headers: customHeaders,
166
+ const response = await fetch(`${baseUrl}/responses`, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ 'Authorization': `Bearer ${apiKey}`,
171
+ ...customHeaders
172
+ },
173
+ body: JSON.stringify(requestPayload)
136
174
  });
175
+ if (!response.ok) {
176
+ const errorText = await response.text();
177
+ throw new Error(`OpenAI Responses API error: ${response.status} ${response.statusText} - ${errorText}`);
178
+ }
179
+ if (!response.body) {
180
+ throw new Error('No response body from OpenAI Responses API');
181
+ }
137
182
  let summary = '';
138
183
  let usage = {
139
184
  prompt_tokens: 0,
140
185
  completion_tokens: 0,
141
186
  total_tokens: 0,
142
187
  };
143
- for await (const chunk of stream) {
188
+ for await (const chunk of parseSSEStream(response.body.getReader())) {
144
189
  const eventType = chunk.type;
145
190
  // Handle text content delta
146
191
  if (eventType === 'response.output_text.delta') {
@@ -150,7 +195,7 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
150
195
  }
151
196
  }
152
197
  // Handle usage info
153
- if (eventType === 'response.done') {
198
+ if (eventType === 'response.completed') {
154
199
  const response = chunk.response;
155
200
  if (response?.usage) {
156
201
  usage = {
@@ -173,26 +218,7 @@ async function compressWithResponses(baseUrl, apiKey, modelName, conversationMes
173
218
  * Compress context using Gemini API
174
219
  */
175
220
  async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
176
- const clientConfig = {
177
- apiKey,
178
- };
179
221
  const customHeaders = getCustomHeaders();
180
- // Support custom baseUrl and headers for proxy servers
181
- if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
182
- clientConfig.httpOptions = {
183
- baseUrl,
184
- headers: {
185
- 'x-goog-api-key': apiKey,
186
- ...customHeaders,
187
- },
188
- };
189
- }
190
- else if (Object.keys(customHeaders).length > 0) {
191
- clientConfig.httpOptions = {
192
- headers: customHeaders,
193
- };
194
- }
195
- const client = new GoogleGenAI(clientConfig);
196
222
  // Build system instruction
197
223
  const systemInstruction = systemPrompt || SYSTEM_PROMPT;
198
224
  // Build contents array with conversation history
@@ -220,30 +246,81 @@ async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessag
220
246
  text: COMPRESSION_PROMPT,
221
247
  }],
222
248
  });
223
- const requestConfig = {
224
- model: modelName,
225
- systemInstruction,
249
+ const requestBody = {
226
250
  contents,
251
+ systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
227
252
  };
228
- // Use streaming to avoid timeout
229
- const stream = await client.models.generateContentStream(requestConfig);
253
+ // Extract model name
254
+ const effectiveBaseUrl = baseUrl && baseUrl !== 'https://api.openai.com/v1'
255
+ ? baseUrl
256
+ : 'https://generativelanguage.googleapis.com/v1beta';
257
+ const model = modelName.startsWith('models/') ? modelName : `models/${modelName}`;
258
+ const url = `${effectiveBaseUrl}/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
259
+ const response = await fetch(url, {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Content-Type': 'application/json',
263
+ 'Authorization': `Bearer ${apiKey}`,
264
+ ...customHeaders
265
+ },
266
+ body: JSON.stringify(requestBody)
267
+ });
268
+ if (!response.ok) {
269
+ const errorText = await response.text();
270
+ throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errorText}`);
271
+ }
272
+ if (!response.body) {
273
+ throw new Error('No response body from Gemini API');
274
+ }
230
275
  let summary = '';
231
276
  let usage = {
232
277
  prompt_tokens: 0,
233
278
  completion_tokens: 0,
234
279
  total_tokens: 0,
235
280
  };
236
- for await (const chunk of stream) {
237
- if (chunk.text) {
238
- summary += chunk.text;
239
- }
240
- // Collect usage info
241
- if (chunk.usageMetadata) {
242
- usage = {
243
- prompt_tokens: chunk.usageMetadata.promptTokenCount || 0,
244
- completion_tokens: chunk.usageMetadata.candidatesTokenCount || 0,
245
- total_tokens: chunk.usageMetadata.totalTokenCount || 0,
246
- };
281
+ // Parse SSE stream
282
+ const reader = response.body.getReader();
283
+ const decoder = new TextDecoder();
284
+ let buffer = '';
285
+ while (true) {
286
+ const { done, value } = await reader.read();
287
+ if (done)
288
+ break;
289
+ buffer += decoder.decode(value, { stream: true });
290
+ const lines = buffer.split('\n');
291
+ buffer = lines.pop() || '';
292
+ for (const line of lines) {
293
+ const trimmed = line.trim();
294
+ if (!trimmed || trimmed.startsWith(':'))
295
+ continue;
296
+ if (trimmed.startsWith('data: ')) {
297
+ const data = trimmed.slice(6);
298
+ try {
299
+ const chunk = JSON.parse(data);
300
+ // Process candidates
301
+ if (chunk.candidates && chunk.candidates.length > 0) {
302
+ const candidate = chunk.candidates[0];
303
+ if (candidate.content && candidate.content.parts) {
304
+ for (const part of candidate.content.parts) {
305
+ if (part.text) {
306
+ summary += part.text;
307
+ }
308
+ }
309
+ }
310
+ }
311
+ // Collect usage info
312
+ if (chunk.usageMetadata) {
313
+ usage = {
314
+ prompt_tokens: chunk.usageMetadata.promptTokenCount || 0,
315
+ completion_tokens: chunk.usageMetadata.candidatesTokenCount || 0,
316
+ total_tokens: chunk.usageMetadata.totalTokenCount || 0,
317
+ };
318
+ }
319
+ }
320
+ catch (e) {
321
+ console.error('Failed to parse Gemini SSE data:', data);
322
+ }
323
+ }
247
324
  }
248
325
  }
249
326
  if (!summary) {
@@ -258,18 +335,7 @@ async function compressWithGemini(baseUrl, apiKey, modelName, conversationMessag
258
335
  * Compress context using Anthropic API
259
336
  */
260
337
  async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMessages, systemPrompt) {
261
- const clientConfig = {
262
- apiKey,
263
- };
264
- if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
265
- clientConfig.baseURL = baseUrl;
266
- }
267
338
  const customHeaders = getCustomHeaders();
268
- clientConfig.defaultHeaders = {
269
- 'Authorization': `Bearer ${apiKey}`,
270
- ...customHeaders,
271
- };
272
- const client = new Anthropic(clientConfig);
273
339
  // Build messages array with conversation history
274
340
  const messages = [];
275
341
  // If custom system prompt exists, add default as first user message
@@ -298,27 +364,73 @@ async function compressWithAnthropic(baseUrl, apiKey, modelName, conversationMes
298
364
  max_tokens: 4096,
299
365
  system: systemParam,
300
366
  messages,
367
+ stream: true
301
368
  };
302
- // Use streaming to avoid timeout
303
- const stream = await client.messages.stream(requestPayload);
369
+ const effectiveBaseUrl = baseUrl && baseUrl !== 'https://api.openai.com/v1'
370
+ ? baseUrl
371
+ : 'https://api.anthropic.com/v1';
372
+ const response = await fetch(`${effectiveBaseUrl}/messages`, {
373
+ method: 'POST',
374
+ headers: {
375
+ 'Content-Type': 'application/json',
376
+ 'x-api-key': apiKey,
377
+ 'authorization': `Bearer ${apiKey}`,
378
+ ...customHeaders
379
+ },
380
+ body: JSON.stringify(requestPayload)
381
+ });
382
+ if (!response.ok) {
383
+ const errorText = await response.text();
384
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`);
385
+ }
386
+ if (!response.body) {
387
+ throw new Error('No response body from Anthropic API');
388
+ }
304
389
  let summary = '';
305
390
  let usage = {
306
391
  prompt_tokens: 0,
307
392
  completion_tokens: 0,
308
393
  total_tokens: 0,
309
394
  };
310
- for await (const event of stream) {
311
- if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
312
- summary += event.delta.text;
313
- }
314
- // Collect usage info from message_stop event
315
- if (event.type === 'message_stop') {
316
- const finalMessage = await stream.finalMessage();
317
- usage = {
318
- prompt_tokens: finalMessage.usage.input_tokens,
319
- completion_tokens: finalMessage.usage.output_tokens,
320
- total_tokens: finalMessage.usage.input_tokens + finalMessage.usage.output_tokens,
321
- };
395
+ // Parse Anthropic SSE stream
396
+ const reader = response.body.getReader();
397
+ const decoder = new TextDecoder();
398
+ let buffer = '';
399
+ while (true) {
400
+ const { done, value } = await reader.read();
401
+ if (done)
402
+ break;
403
+ buffer += decoder.decode(value, { stream: true });
404
+ const lines = buffer.split('\n');
405
+ buffer = lines.pop() || '';
406
+ for (const line of lines) {
407
+ const trimmed = line.trim();
408
+ if (!trimmed || trimmed.startsWith(':'))
409
+ continue;
410
+ if (trimmed.startsWith('event: ')) {
411
+ continue;
412
+ }
413
+ if (trimmed.startsWith('data: ')) {
414
+ const data = trimmed.slice(6);
415
+ try {
416
+ const event = JSON.parse(data);
417
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
418
+ summary += event.delta.text;
419
+ }
420
+ // Collect usage info from message_start event
421
+ if (event.type === 'message_start' && event.message?.usage) {
422
+ usage.prompt_tokens = event.message.usage.input_tokens || 0;
423
+ }
424
+ // Collect usage info from message_delta event
425
+ if (event.type === 'message_delta' && event.usage) {
426
+ usage.completion_tokens = event.usage.output_tokens || 0;
427
+ usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
428
+ }
429
+ }
430
+ catch (e) {
431
+ console.error('Failed to parse Anthropic SSE data:', data);
432
+ }
433
+ }
322
434
  }
323
435
  }
324
436
  if (!summary) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -39,8 +39,6 @@
39
39
  "dist"
40
40
  ],
41
41
  "dependencies": {
42
- "@anthropic-ai/sdk": "^0.65.0",
43
- "@google/genai": "^1.23.0",
44
42
  "@inkjs/ui": "^2.0.0",
45
43
  "@modelcontextprotocol/sdk": "^1.17.3",
46
44
  "chalk-template": "^1.1.2",
@@ -57,7 +55,6 @@
57
55
  "ink-text-input": "^6.0.0",
58
56
  "ink-tree-select": "^2.3.1",
59
57
  "meow": "^11.0.0",
60
- "openai": "^6.1.0",
61
58
  "puppeteer-core": "^24.25.0",
62
59
  "react": "^18.2.0",
63
60
  "string-width": "^7.2.0",