snow-ai 0.3.1 → 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.
@@ -3,7 +3,7 @@ import { getOpenAiConfig, getCustomHeaders } from '../utils/apiConfig.js';
3
3
  * Fetch models from OpenAI-compatible API
4
4
  */
5
5
  async function fetchOpenAIModels(baseUrl, apiKey, customHeaders) {
6
- const url = `${baseUrl.replace(/\/$/, '')}/models`;
6
+ const url = `${baseUrl}/models`;
7
7
  const headers = {
8
8
  'Content-Type': 'application/json',
9
9
  ...customHeaders,
@@ -26,7 +26,7 @@ async function fetchOpenAIModels(baseUrl, apiKey, customHeaders) {
26
26
  */
27
27
  async function fetchGeminiModels(baseUrl, apiKey) {
28
28
  // Gemini uses API key as query parameter
29
- const url = `${baseUrl.replace(/\/$/, '')}/models?key=${apiKey}`;
29
+ const url = `${baseUrl}/models?key=${apiKey}`;
30
30
  const response = await fetch(url, {
31
31
  method: 'GET',
32
32
  headers: {
@@ -50,10 +50,9 @@ async function fetchGeminiModels(baseUrl, apiKey) {
50
50
  * Supports both Anthropic native format and OpenAI-compatible format for backward compatibility
51
51
  */
52
52
  async function fetchAnthropicModels(baseUrl, apiKey, customHeaders) {
53
- const url = `${baseUrl.replace(/\/$/, '')}/models`;
53
+ const url = `${baseUrl}/models`;
54
54
  const headers = {
55
55
  'Content-Type': 'application/json',
56
- 'anthropic-version': '2023-06-01',
57
56
  ...customHeaders,
58
57
  };
59
58
  if (apiKey) {
@@ -106,19 +105,19 @@ export async function fetchAvailableModels() {
106
105
  if (!config.apiKey) {
107
106
  throw new Error('API key is required for Gemini API');
108
107
  }
109
- models = await fetchGeminiModels(config.baseUrl.replace(/\/$/, '') + '/v1beta', config.apiKey);
108
+ models = await fetchGeminiModels(config.baseUrl.replace(/\/$/, ''), config.apiKey);
110
109
  break;
111
110
  case 'anthropic':
112
111
  if (!config.apiKey) {
113
112
  throw new Error('API key is required for Anthropic API');
114
113
  }
115
- models = await fetchAnthropicModels(config.baseUrl.replace(/\/$/, '') + '/v1', config.apiKey, customHeaders);
114
+ models = await fetchAnthropicModels(config.baseUrl.replace(/\/$/, ''), config.apiKey, customHeaders);
116
115
  break;
117
116
  case 'chat':
118
117
  case 'responses':
119
118
  default:
120
119
  // OpenAI-compatible API
121
- models = await fetchOpenAIModels(config.baseUrl, config.apiKey, customHeaders);
120
+ models = await fetchOpenAIModels(config.baseUrl.replace(/\/$/, ''), config.apiKey, customHeaders);
122
121
  break;
123
122
  }
124
123
  // Sort models alphabetically by id for better UX
@@ -1,18 +1,11 @@
1
- import type { ChatMessage, ToolCall } from './chat.js';
1
+ import type { ChatMessage, ToolCall, ChatCompletionTool, UsageInfo } from './types.js';
2
2
  export interface ResponseOptions {
3
3
  model: string;
4
4
  messages: ChatMessage[];
5
5
  stream?: boolean;
6
6
  temperature?: number;
7
7
  max_tokens?: number;
8
- tools?: Array<{
9
- type: 'function';
10
- function: {
11
- name: string;
12
- description?: string;
13
- parameters?: Record<string, any>;
14
- };
15
- }>;
8
+ tools?: ChatCompletionTool[];
16
9
  tool_choice?: 'auto' | 'none' | 'required';
17
10
  reasoning?: {
18
11
  summary?: 'auto' | 'none';
@@ -22,14 +15,6 @@ export interface ResponseOptions {
22
15
  store?: boolean;
23
16
  include?: string[];
24
17
  }
25
- export interface UsageInfo {
26
- prompt_tokens: number;
27
- completion_tokens: number;
28
- total_tokens: number;
29
- cache_creation_input_tokens?: number;
30
- cache_read_input_tokens?: number;
31
- cached_tokens?: number;
32
- }
33
18
  export interface ResponseStreamChunk {
34
19
  type: 'content' | 'tool_calls' | 'tool_call_delta' | 'reasoning_delta' | 'reasoning_started' | 'done' | 'usage';
35
20
  content?: string;
@@ -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';
@@ -52,27 +51,24 @@ function convertToolsForResponses(tools) {
52
51
  strict: false
53
52
  }));
54
53
  }
55
- let openaiClient = null;
56
- function getOpenAIClient() {
57
- if (!openaiClient) {
54
+ let openaiConfig = null;
55
+ function getOpenAIConfig() {
56
+ if (!openaiConfig) {
58
57
  const config = getOpenAiConfig();
59
58
  if (!config.apiKey || !config.baseUrl) {
60
59
  throw new Error('OpenAI API configuration is incomplete. Please configure API settings first.');
61
60
  }
62
- // Get custom headers
63
61
  const customHeaders = getCustomHeaders();
64
- openaiClient = new OpenAI({
62
+ openaiConfig = {
65
63
  apiKey: config.apiKey,
66
- baseURL: config.baseUrl,
67
- defaultHeaders: {
68
- ...customHeaders
69
- }
70
- });
64
+ baseUrl: config.baseUrl,
65
+ customHeaders
66
+ };
71
67
  }
72
- return openaiClient;
68
+ return openaiConfig;
73
69
  }
74
70
  export function resetOpenAIClient() {
75
- openaiClient = null;
71
+ openaiConfig = null;
76
72
  }
77
73
  function convertToResponseInput(messages) {
78
74
  const customSystemPrompt = getCustomSystemPrompt();
@@ -177,11 +173,43 @@ function convertToResponseInput(messages) {
177
173
  }
178
174
  return { input: result, systemInstructions };
179
175
  }
176
+ /**
177
+ * Parse Server-Sent Events (SSE) stream
178
+ */
179
+ async function* parseSSEStream(reader) {
180
+ const decoder = new TextDecoder();
181
+ let buffer = '';
182
+ while (true) {
183
+ const { done, value } = await reader.read();
184
+ if (done)
185
+ break;
186
+ buffer += decoder.decode(value, { stream: true });
187
+ const lines = buffer.split('\n');
188
+ buffer = lines.pop() || '';
189
+ for (const line of lines) {
190
+ const trimmed = line.trim();
191
+ if (!trimmed || trimmed.startsWith(':'))
192
+ continue;
193
+ if (trimmed === 'data: [DONE]') {
194
+ return;
195
+ }
196
+ if (trimmed.startsWith('data: ')) {
197
+ const data = trimmed.slice(6);
198
+ try {
199
+ yield JSON.parse(data);
200
+ }
201
+ catch (e) {
202
+ console.error('Failed to parse SSE data:', data);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
180
208
  /**
181
209
  * 使用 Responses API 创建流式响应(带自动工具调用)
182
210
  */
183
211
  export async function* createStreamingResponse(options, abortSignal, onRetry) {
184
- const client = getOpenAIClient();
212
+ const config = getOpenAIConfig();
185
213
  // 提取系统提示词和转换后的消息
186
214
  const { input: requestInput, systemInstructions } = convertToResponseInput(options.messages);
187
215
  // 使用重试包装生成器
@@ -198,15 +226,29 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
198
226
  include: options.include || ['reasoning.encrypted_content'],
199
227
  prompt_cache_key: options.prompt_cache_key,
200
228
  };
201
- const stream = await client.responses.create(requestPayload, {
202
- signal: abortSignal,
229
+ const response = await fetch(`${config.baseUrl}/responses`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ 'Authorization': `Bearer ${config.apiKey}`,
234
+ ...config.customHeaders
235
+ },
236
+ body: JSON.stringify(requestPayload),
237
+ signal: abortSignal
203
238
  });
239
+ if (!response.ok) {
240
+ const errorText = await response.text();
241
+ throw new Error(`OpenAI Responses API error: ${response.status} ${response.statusText} - ${errorText}`);
242
+ }
243
+ if (!response.body) {
244
+ throw new Error('No response body from OpenAI Responses API');
245
+ }
204
246
  let contentBuffer = '';
205
247
  let toolCallsBuffer = {};
206
248
  let hasToolCalls = false;
207
249
  let currentFunctionCallId = null;
208
250
  let usageData;
209
- for await (const chunk of stream) {
251
+ for await (const chunk of parseSSEStream(response.body.getReader())) {
210
252
  if (abortSignal?.aborted) {
211
253
  return;
212
254
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared API types for all AI providers
3
+ */
4
+ export interface ImageContent {
5
+ type: 'image';
6
+ data: string;
7
+ mimeType: string;
8
+ }
9
+ export interface ToolCall {
10
+ id: string;
11
+ type: 'function';
12
+ function: {
13
+ name: string;
14
+ arguments: string;
15
+ };
16
+ }
17
+ export interface ChatMessage {
18
+ role: 'system' | 'user' | 'assistant' | 'tool';
19
+ content: string;
20
+ tool_call_id?: string;
21
+ tool_calls?: ToolCall[];
22
+ images?: ImageContent[];
23
+ }
24
+ export interface ChatCompletionTool {
25
+ type: 'function';
26
+ function: {
27
+ name: string;
28
+ description?: string;
29
+ parameters?: Record<string, any>;
30
+ };
31
+ }
32
+ export interface UsageInfo {
33
+ prompt_tokens: number;
34
+ completion_tokens: number;
35
+ total_tokens: number;
36
+ cache_creation_input_tokens?: number;
37
+ cache_read_input_tokens?: number;
38
+ cached_tokens?: number;
39
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared API types for all AI providers
3
+ */
4
+ export {};
@@ -66,6 +66,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
66
66
  const [searchTerm, setSearchTerm] = useState('');
67
67
  const [manualInputMode, setManualInputMode] = useState(false);
68
68
  const [manualInputValue, setManualInputValue] = useState('');
69
+ const [, forceUpdate] = useState(0);
69
70
  const requestMethodOptions = [
70
71
  {
71
72
  label: 'Chat Completions - Modern chat API (GPT-4, GPT-3.5-turbo)',
@@ -392,6 +393,8 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
392
393
  key.escape) {
393
394
  setIsEditing(false);
394
395
  setSearchTerm('');
396
+ // Force re-render to clear Select component artifacts
397
+ forceUpdate(prev => prev + 1);
395
398
  return;
396
399
  }
397
400
  // Handle editing mode
@@ -620,26 +623,58 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
620
623
  activeProfile && (React.createElement(Text, { color: "cyan", dimColor: true },
621
624
  "Active Profile: ",
622
625
  activeProfile))))),
623
- React.createElement(Box, { flexDirection: "column" },
626
+ isEditing &&
627
+ (currentField === 'profile' ||
628
+ currentField === 'requestMethod' ||
629
+ currentField === 'advancedModel' ||
630
+ currentField === 'basicModel' ||
631
+ currentField === 'compactModelName') ? (React.createElement(Box, { flexDirection: "column" },
632
+ React.createElement(Text, { color: "green" },
633
+ "\u276F ",
634
+ currentField === 'profile' && 'Profile',
635
+ currentField === 'requestMethod' && 'Request Method',
636
+ currentField === 'advancedModel' && 'Advanced Model',
637
+ currentField === 'basicModel' && 'Basic Model',
638
+ currentField === 'compactModelName' && 'Compact Model',
639
+ ":"),
640
+ React.createElement(Box, { marginLeft: 3, marginTop: 1 },
641
+ currentField === 'profile' && (React.createElement(Select, { options: [
642
+ ...profiles.map(p => ({
643
+ label: `${p.displayName}${p.isActive ? ' (Active)' : ''}`,
644
+ value: p.name,
645
+ })),
646
+ {
647
+ label: chalk.green('+ New Profile'),
648
+ value: '__CREATE_NEW__',
649
+ },
650
+ {
651
+ label: chalk.red('🆇 Delete Profile'),
652
+ value: '__DELETE__',
653
+ },
654
+ ], defaultValue: activeProfile, onChange: handleProfileChange })),
655
+ currentField === 'requestMethod' && (React.createElement(Select, { options: requestMethodOptions, defaultValue: requestMethod, onChange: value => {
656
+ setRequestMethod(value);
657
+ setIsEditing(false);
658
+ } })),
659
+ (currentField === 'advancedModel' ||
660
+ currentField === 'basicModel' ||
661
+ currentField === 'compactModelName') && (React.createElement(Box, { flexDirection: "column" },
662
+ searchTerm && React.createElement(Text, { color: "cyan" },
663
+ "Filter: ",
664
+ searchTerm),
665
+ React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange })))),
666
+ React.createElement(Box, { marginTop: 1 },
667
+ React.createElement(Alert, { variant: "info" },
668
+ (currentField === 'advancedModel' ||
669
+ currentField === 'basicModel' ||
670
+ currentField === 'compactModelName') &&
671
+ 'Type to filter, ↑↓ to select, Enter to confirm, Esc to cancel',
672
+ (currentField === 'profile' || currentField === 'requestMethod') &&
673
+ '↑↓ to select, Enter to confirm, Esc to cancel')))) : (React.createElement(Box, { flexDirection: "column" },
624
674
  React.createElement(Box, { flexDirection: "column" },
625
675
  React.createElement(Text, { color: currentField === 'profile' ? 'green' : 'white' },
626
676
  currentField === 'profile' ? '❯ ' : ' ',
627
677
  "Profile:"),
628
- currentField === 'profile' && isEditing && (React.createElement(Box, { marginLeft: 3 },
629
- React.createElement(Select, { options: [
630
- ...profiles.map(p => ({
631
- label: `${p.displayName}${p.isActive ? ' (Active)' : ''}`,
632
- value: p.name,
633
- })),
634
- {
635
- label: chalk.green('+ New Profile'),
636
- value: '__CREATE_NEW__',
637
- },
638
- {
639
- label: chalk.red('🆇 Delete Profile'),
640
- value: '__DELETE__',
641
- },
642
- ], defaultValue: activeProfile, onChange: handleProfileChange }))),
643
678
  (!isEditing || currentField !== 'profile') && (React.createElement(Box, { marginLeft: 3 },
644
679
  React.createElement(Text, { color: "gray" }, profiles.find(p => p.name === activeProfile)?.displayName ||
645
680
  activeProfile)))),
@@ -663,11 +698,6 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
663
698
  React.createElement(Text, { color: currentField === 'requestMethod' ? 'green' : 'white' },
664
699
  currentField === 'requestMethod' ? '❯ ' : ' ',
665
700
  "Request Method:"),
666
- currentField === 'requestMethod' && isEditing && (React.createElement(Box, { marginLeft: 3 },
667
- React.createElement(Select, { options: requestMethodOptions, defaultValue: requestMethod, onChange: value => {
668
- setRequestMethod(value);
669
- setIsEditing(false);
670
- } }))),
671
701
  (!isEditing || currentField !== 'requestMethod') && (React.createElement(Box, { marginLeft: 3 },
672
702
  React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)
673
703
  ?.label || 'Not set')))),
@@ -683,36 +713,18 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
683
713
  React.createElement(Text, { color: currentField === 'advancedModel' ? 'green' : 'white' },
684
714
  currentField === 'advancedModel' ? '❯ ' : ' ',
685
715
  "Advanced Model:"),
686
- currentField === 'advancedModel' && isEditing && (React.createElement(Box, { marginLeft: 3 },
687
- React.createElement(Box, { flexDirection: "column" },
688
- searchTerm && React.createElement(Text, { color: "cyan" },
689
- "Filter: ",
690
- searchTerm),
691
- React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange })))),
692
716
  (!isEditing || currentField !== 'advancedModel') && (React.createElement(Box, { marginLeft: 3 },
693
717
  React.createElement(Text, { color: "gray" }, advancedModel || 'Not set')))),
694
718
  React.createElement(Box, { flexDirection: "column" },
695
719
  React.createElement(Text, { color: currentField === 'basicModel' ? 'green' : 'white' },
696
720
  currentField === 'basicModel' ? '❯ ' : ' ',
697
721
  "Basic Model:"),
698
- currentField === 'basicModel' && isEditing && (React.createElement(Box, { marginLeft: 3 },
699
- React.createElement(Box, { flexDirection: "column" },
700
- searchTerm && React.createElement(Text, { color: "cyan" },
701
- "Filter: ",
702
- searchTerm),
703
- React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange })))),
704
722
  (!isEditing || currentField !== 'basicModel') && (React.createElement(Box, { marginLeft: 3 },
705
723
  React.createElement(Text, { color: "gray" }, basicModel || 'Not set')))),
706
724
  React.createElement(Box, { flexDirection: "column" },
707
725
  React.createElement(Text, { color: currentField === 'compactModelName' ? 'green' : 'white' },
708
726
  currentField === 'compactModelName' ? '❯ ' : ' ',
709
727
  "Compact Model:"),
710
- currentField === 'compactModelName' && isEditing && (React.createElement(Box, { marginLeft: 3 },
711
- React.createElement(Box, { flexDirection: "column" },
712
- searchTerm && React.createElement(Text, { color: "cyan" },
713
- "Filter: ",
714
- searchTerm),
715
- React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange })))),
716
728
  (!isEditing || currentField !== 'compactModelName') && (React.createElement(Box, { marginLeft: 3 },
717
729
  React.createElement(Text, { color: "gray" }, compactModelName || 'Not set')))),
718
730
  React.createElement(Box, { flexDirection: "column" },
@@ -734,20 +746,21 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
734
746
  "Enter value: ",
735
747
  maxTokens))),
736
748
  (!isEditing || currentField !== 'maxTokens') && (React.createElement(Box, { marginLeft: 3 },
737
- React.createElement(Text, { color: "gray" }, maxTokens))))),
749
+ React.createElement(Text, { color: "gray" }, maxTokens)))))),
738
750
  errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
739
751
  React.createElement(Text, { color: "red", bold: true }, "Errors:"),
740
752
  errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
741
753
  "\u2022 ",
742
754
  error))))),
743
- React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(React.Fragment, null,
744
- React.createElement(Alert, { variant: "info" },
745
- "Editing mode:",
746
- ' ',
755
+ !(isEditing &&
756
+ (currentField === 'profile' ||
757
+ currentField === 'requestMethod' ||
747
758
  currentField === 'advancedModel' ||
748
- currentField === 'basicModel' ||
749
- currentField === 'compactModelName'
750
- ? 'Type to filter, ↑↓ to select, Enter to confirm'
751
- : 'Press Enter to save and exit editing'))) : (React.createElement(React.Fragment, null,
752
- React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, M for manual input, Ctrl+S or Esc to save"))))));
759
+ currentField === 'basicModel' ||
760
+ currentField === 'compactModelName')) && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(Alert, { variant: "info" },
761
+ "Editing mode:",
762
+ ' ',
763
+ currentField === 'maxContextTokens' || currentField === 'maxTokens'
764
+ ? 'Type to edit, Enter to save'
765
+ : 'Press Enter to save and exit editing')) : (React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, M for manual input, Ctrl+S or Esc to save"))))));
753
766
  }
@@ -92,7 +92,7 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
92
92
  }, [terminalWidth, stdout]);
93
93
  return (React.createElement(Box, { flexDirection: "column", width: terminalWidth },
94
94
  React.createElement(Static, { key: remountKey, items: [
95
- React.createElement(Box, { key: "welcome-header", flexDirection: "row", paddingLeft: 2, paddingTop: 1, paddingBottom: 1, width: terminalWidth },
95
+ React.createElement(Box, { key: "welcome-header", flexDirection: "row", paddingLeft: 2, paddingTop: 1, paddingBottom: 0, width: terminalWidth },
96
96
  React.createElement(Box, { flexDirection: "column", justifyContent: "center" },
97
97
  React.createElement(Text, { bold: true },
98
98
  React.createElement(Gradient, { name: "rainbow" }, "\u2746 SNOW AI CLI")),
@@ -1,4 +1,4 @@
1
- import type { ChatMessage } from '../api/chat.js';
1
+ import type { ChatMessage } from '../api/types.js';
2
2
  export interface CompressionResult {
3
3
  summary: string;
4
4
  usage: {