snow-ai 0.2.15 → 0.2.17

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 (67) hide show
  1. package/dist/api/anthropic.d.ts +1 -1
  2. package/dist/api/anthropic.js +52 -76
  3. package/dist/api/chat.d.ts +4 -4
  4. package/dist/api/chat.js +32 -17
  5. package/dist/api/gemini.d.ts +1 -1
  6. package/dist/api/gemini.js +20 -13
  7. package/dist/api/models.d.ts +3 -0
  8. package/dist/api/models.js +101 -17
  9. package/dist/api/responses.d.ts +5 -5
  10. package/dist/api/responses.js +29 -27
  11. package/dist/app.js +4 -1
  12. package/dist/hooks/useClipboard.d.ts +4 -0
  13. package/dist/hooks/useClipboard.js +120 -0
  14. package/dist/hooks/useCommandHandler.d.ts +26 -0
  15. package/dist/hooks/useCommandHandler.js +158 -0
  16. package/dist/hooks/useCommandPanel.d.ts +16 -0
  17. package/dist/hooks/useCommandPanel.js +53 -0
  18. package/dist/hooks/useConversation.d.ts +9 -1
  19. package/dist/hooks/useConversation.js +152 -58
  20. package/dist/hooks/useFilePicker.d.ts +17 -0
  21. package/dist/hooks/useFilePicker.js +91 -0
  22. package/dist/hooks/useHistoryNavigation.d.ts +21 -0
  23. package/dist/hooks/useHistoryNavigation.js +50 -0
  24. package/dist/hooks/useInputBuffer.d.ts +6 -0
  25. package/dist/hooks/useInputBuffer.js +29 -0
  26. package/dist/hooks/useKeyboardInput.d.ts +51 -0
  27. package/dist/hooks/useKeyboardInput.js +272 -0
  28. package/dist/hooks/useSnapshotState.d.ts +12 -0
  29. package/dist/hooks/useSnapshotState.js +28 -0
  30. package/dist/hooks/useStreamingState.d.ts +24 -0
  31. package/dist/hooks/useStreamingState.js +96 -0
  32. package/dist/hooks/useVSCodeState.d.ts +8 -0
  33. package/dist/hooks/useVSCodeState.js +63 -0
  34. package/dist/mcp/filesystem.d.ts +24 -5
  35. package/dist/mcp/filesystem.js +52 -17
  36. package/dist/mcp/todo.js +4 -8
  37. package/dist/ui/components/ChatInput.js +71 -560
  38. package/dist/ui/components/DiffViewer.js +57 -30
  39. package/dist/ui/components/FileList.js +70 -26
  40. package/dist/ui/components/MessageList.d.ts +6 -0
  41. package/dist/ui/components/MessageList.js +47 -15
  42. package/dist/ui/components/ShimmerText.d.ts +9 -0
  43. package/dist/ui/components/ShimmerText.js +30 -0
  44. package/dist/ui/components/TodoTree.d.ts +1 -1
  45. package/dist/ui/components/TodoTree.js +0 -4
  46. package/dist/ui/components/ToolConfirmation.js +14 -6
  47. package/dist/ui/pages/ChatScreen.js +174 -373
  48. package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
  49. package/dist/ui/pages/CustomHeadersScreen.js +104 -0
  50. package/dist/ui/pages/WelcomeScreen.js +5 -0
  51. package/dist/utils/apiConfig.d.ts +10 -0
  52. package/dist/utils/apiConfig.js +51 -0
  53. package/dist/utils/incrementalSnapshot.d.ts +8 -0
  54. package/dist/utils/incrementalSnapshot.js +63 -0
  55. package/dist/utils/mcpToolsManager.js +6 -1
  56. package/dist/utils/retryUtils.d.ts +22 -0
  57. package/dist/utils/retryUtils.js +180 -0
  58. package/dist/utils/sessionConverter.js +80 -17
  59. package/dist/utils/sessionManager.js +35 -4
  60. package/dist/utils/textUtils.d.ts +4 -0
  61. package/dist/utils/textUtils.js +19 -0
  62. package/dist/utils/todoPreprocessor.d.ts +1 -1
  63. package/dist/utils/todoPreprocessor.js +0 -1
  64. package/dist/utils/vscodeConnection.d.ts +8 -0
  65. package/dist/utils/vscodeConnection.js +44 -0
  66. package/package.json +1 -1
  67. package/readme.md +3 -1
@@ -33,4 +33,4 @@ export declare function resetAnthropicClient(): void;
33
33
  /**
34
34
  * Create streaming chat completion using Anthropic API
35
35
  */
36
- export declare function createStreamingAnthropicCompletion(options: AnthropicOptions, abortSignal?: AbortSignal): AsyncGenerator<AnthropicStreamChunk, void, unknown>;
36
+ export declare function createStreamingAnthropicCompletion(options: AnthropicOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): AsyncGenerator<AnthropicStreamChunk, void, unknown>;
@@ -1,7 +1,8 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { createHash, randomUUID } from 'crypto';
3
- import { getOpenAiConfig, getCustomSystemPrompt } from '../utils/apiConfig.js';
3
+ import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
4
4
  import { SYSTEM_PROMPT } from './systemPrompt.js';
5
+ import { withRetryGenerator } from '../utils/retryUtils.js';
5
6
  let anthropicClient = null;
6
7
  function getAnthropicClient() {
7
8
  if (!anthropicClient) {
@@ -12,17 +13,26 @@ function getAnthropicClient() {
12
13
  const clientConfig = {
13
14
  apiKey: config.apiKey,
14
15
  };
15
- // Support custom baseUrl for proxy servers
16
16
  if (config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1') {
17
17
  clientConfig.baseURL = config.baseUrl;
18
18
  }
19
- // If Anthropic Beta is enabled, add default query parameter
20
- if (config.anthropicBeta) {
21
- clientConfig.defaultQuery = { beta: 'true' };
22
- }
23
- // Add Authorization header for enhanced compatibility
19
+ const customHeaders = getCustomHeaders();
24
20
  clientConfig.defaultHeaders = {
25
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);
26
36
  };
27
37
  anthropicClient = new Anthropic(clientConfig);
28
38
  }
@@ -63,7 +73,6 @@ function convertToolsToAnthropic(tools) {
63
73
  }
64
74
  throw new Error('Invalid tool format');
65
75
  });
66
- // Add cache_control to the last tool for prompt caching
67
76
  if (convertedTools.length > 0) {
68
77
  const lastTool = convertedTools[convertedTools.length - 1];
69
78
  lastTool.cache_control = { type: 'ephemeral' };
@@ -73,23 +82,17 @@ function convertToolsToAnthropic(tools) {
73
82
  /**
74
83
  * Convert our ChatMessage format to Anthropic's message format
75
84
  * Adds cache_control to system prompt and last user message for prompt caching
76
- * Logic:
77
- * 1. If custom system prompt exists: use custom as system, prepend default as first user message
78
- * 2. If no custom system prompt: use default as system
79
85
  */
80
86
  function convertToAnthropicMessages(messages) {
81
87
  const customSystemPrompt = getCustomSystemPrompt();
82
88
  let systemContent;
83
89
  const anthropicMessages = [];
84
90
  for (const msg of messages) {
85
- // Extract system message
86
91
  if (msg.role === 'system') {
87
92
  systemContent = msg.content;
88
93
  continue;
89
94
  }
90
- // Handle tool result messages
91
95
  if (msg.role === 'tool' && msg.tool_call_id) {
92
- // Anthropic expects tool results as user messages with tool_result content
93
96
  anthropicMessages.push({
94
97
  role: 'user',
95
98
  content: [{
@@ -100,19 +103,15 @@ function convertToAnthropicMessages(messages) {
100
103
  });
101
104
  continue;
102
105
  }
103
- // Handle user messages with images
104
106
  if (msg.role === 'user' && msg.images && msg.images.length > 0) {
105
107
  const content = [];
106
- // Add text content
107
108
  if (msg.content) {
108
109
  content.push({
109
110
  type: 'text',
110
111
  text: msg.content
111
112
  });
112
113
  }
113
- // Add images
114
114
  for (const image of msg.images) {
115
- // Extract base64 data and mime type
116
115
  const base64Match = image.data.match(/^data:([^;]+);base64,(.+)$/);
117
116
  if (base64Match) {
118
117
  content.push({
@@ -131,17 +130,14 @@ function convertToAnthropicMessages(messages) {
131
130
  });
132
131
  continue;
133
132
  }
134
- // Handle assistant messages with tool calls
135
133
  if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
136
134
  const content = [];
137
- // Add text content if present
138
135
  if (msg.content) {
139
136
  content.push({
140
137
  type: 'text',
141
138
  text: msg.content
142
139
  });
143
140
  }
144
- // Add tool uses
145
141
  for (const toolCall of msg.tool_calls) {
146
142
  content.push({
147
143
  type: 'tool_use',
@@ -156,7 +152,6 @@ function convertToAnthropicMessages(messages) {
156
152
  });
157
153
  continue;
158
154
  }
159
- // Handle regular text messages
160
155
  if (msg.role === 'user' || msg.role === 'assistant') {
161
156
  anthropicMessages.push({
162
157
  role: msg.role,
@@ -164,25 +159,33 @@ function convertToAnthropicMessages(messages) {
164
159
  });
165
160
  }
166
161
  }
167
- // 如果配置了自定义系统提示词
168
162
  if (customSystemPrompt) {
169
- // 自定义系统提示词作为 system,默认系统提示词作为第一条用户消息
170
163
  systemContent = customSystemPrompt;
171
164
  anthropicMessages.unshift({
172
165
  role: 'user',
173
- content: SYSTEM_PROMPT
166
+ content: [{
167
+ type: 'text',
168
+ text: SYSTEM_PROMPT,
169
+ cache_control: { type: 'ephemeral' }
170
+ }]
174
171
  });
175
172
  }
176
173
  else if (!systemContent) {
177
- // 没有自定义系统提示词,默认系统提示词作为 system
178
174
  systemContent = SYSTEM_PROMPT;
179
175
  }
180
- // Add cache_control to last user message for prompt caching
181
- if (anthropicMessages.length > 0) {
182
- const lastMessageIndex = anthropicMessages.length - 1;
183
- const lastMessage = anthropicMessages[lastMessageIndex];
176
+ let lastUserMessageIndex = -1;
177
+ for (let i = anthropicMessages.length - 1; i >= 0; i--) {
178
+ if (anthropicMessages[i]?.role === 'user') {
179
+ if (customSystemPrompt && i === 0) {
180
+ continue;
181
+ }
182
+ lastUserMessageIndex = i;
183
+ break;
184
+ }
185
+ }
186
+ if (lastUserMessageIndex >= 0) {
187
+ const lastMessage = anthropicMessages[lastUserMessageIndex];
184
188
  if (lastMessage && lastMessage.role === 'user') {
185
- // Convert content to array format if it's a string
186
189
  if (typeof lastMessage.content === 'string') {
187
190
  lastMessage.content = [{
188
191
  type: 'text',
@@ -191,7 +194,6 @@ function convertToAnthropicMessages(messages) {
191
194
  }];
192
195
  }
193
196
  else if (Array.isArray(lastMessage.content)) {
194
- // Add cache_control to last content block
195
197
  const lastContentIndex = lastMessage.content.length - 1;
196
198
  if (lastContentIndex >= 0) {
197
199
  const lastContent = lastMessage.content[lastContentIndex];
@@ -200,7 +202,6 @@ function convertToAnthropicMessages(messages) {
200
202
  }
201
203
  }
202
204
  }
203
- // Format system prompt with cache_control (only if we have a system prompt)
204
205
  const system = systemContent ? [{
205
206
  type: 'text',
206
207
  text: systemContent,
@@ -211,14 +212,13 @@ function convertToAnthropicMessages(messages) {
211
212
  /**
212
213
  * Create streaming chat completion using Anthropic API
213
214
  */
214
- export async function* createStreamingAnthropicCompletion(options, abortSignal) {
215
+ export async function* createStreamingAnthropicCompletion(options, abortSignal, onRetry) {
215
216
  const client = getAnthropicClient();
216
- try {
217
+ yield* withRetryGenerator(async function* () {
217
218
  const { system, messages } = convertToAnthropicMessages(options.messages);
218
- // Generate user_id with session tracking if sessionId is provided
219
219
  const sessionId = options.sessionId || randomUUID();
220
220
  const userId = generateUserId(sessionId);
221
- // Prepare request body for logging
221
+ const customHeaders = getCustomHeaders();
222
222
  const requestBody = {
223
223
  model: options.model,
224
224
  max_tokens: options.max_tokens || 4096,
@@ -231,33 +231,32 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
231
231
  },
232
232
  stream: true
233
233
  };
234
- // Create streaming request
235
- const stream = await client.messages.create(requestBody);
234
+ const stream = await client.messages.create(requestBody, {
235
+ headers: customHeaders
236
+ });
236
237
  let contentBuffer = '';
237
238
  let toolCallsBuffer = new Map();
238
239
  let hasToolCalls = false;
239
240
  let usageData;
240
- let currentToolUseId = null; // Track current tool use block ID
241
+ let blockIndexToId = new Map();
241
242
  for await (const event of stream) {
242
243
  if (abortSignal?.aborted) {
243
244
  return;
244
245
  }
245
- // Handle different event types
246
246
  if (event.type === 'content_block_start') {
247
247
  const block = event.content_block;
248
- // Handle tool use blocks
249
248
  if (block.type === 'tool_use') {
250
249
  hasToolCalls = true;
251
- currentToolUseId = block.id; // Store current tool use ID
250
+ const blockIndex = event.index;
251
+ blockIndexToId.set(blockIndex, block.id);
252
252
  toolCallsBuffer.set(block.id, {
253
253
  id: block.id,
254
254
  type: 'function',
255
255
  function: {
256
256
  name: block.name,
257
- arguments: '{}' // Initialize with empty object instead of empty string
257
+ arguments: '{}'
258
258
  }
259
259
  });
260
- // Yield delta for token counting
261
260
  yield {
262
261
  type: 'tool_call_delta',
263
262
  delta: block.name
@@ -266,7 +265,6 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
266
265
  }
267
266
  else if (event.type === 'content_block_delta') {
268
267
  const delta = event.delta;
269
- // Handle text content
270
268
  if (delta.type === 'text_delta') {
271
269
  const text = delta.text;
272
270
  contentBuffer += text;
@@ -275,21 +273,19 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
275
273
  content: text
276
274
  };
277
275
  }
278
- // Handle tool input deltas
279
276
  if (delta.type === 'input_json_delta') {
280
277
  const jsonDelta = delta.partial_json;
281
- // Use currentToolUseId instead of event.index
282
- if (currentToolUseId) {
283
- const toolCall = toolCallsBuffer.get(currentToolUseId);
278
+ const blockIndex = event.index;
279
+ const toolId = blockIndexToId.get(blockIndex);
280
+ if (toolId) {
281
+ const toolCall = toolCallsBuffer.get(toolId);
284
282
  if (toolCall) {
285
- // If this is the first delta and arguments is still '{}', replace it
286
283
  if (toolCall.function.arguments === '{}') {
287
284
  toolCall.function.arguments = jsonDelta;
288
285
  }
289
286
  else {
290
287
  toolCall.function.arguments += jsonDelta;
291
288
  }
292
- // Yield delta for token counting
293
289
  yield {
294
290
  type: 'tool_call_delta',
295
291
  delta: jsonDelta
@@ -298,12 +294,7 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
298
294
  }
299
295
  }
300
296
  }
301
- else if (event.type === 'content_block_stop') {
302
- // Reset current tool use ID when block ends
303
- currentToolUseId = null;
304
- }
305
297
  else if (event.type === 'message_start') {
306
- // Capture initial usage data (including cache metrics)
307
298
  if (event.message.usage) {
308
299
  usageData = {
309
300
  prompt_tokens: event.message.usage.input_tokens || 0,
@@ -315,7 +306,6 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
315
306
  }
316
307
  }
317
308
  else if (event.type === 'message_delta') {
318
- // Update usage data with final token counts (including cache metrics)
319
309
  if (event.usage) {
320
310
  if (!usageData) {
321
311
  usageData = {
@@ -326,7 +316,6 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
326
316
  }
327
317
  usageData.completion_tokens = event.usage.output_tokens || 0;
328
318
  usageData.total_tokens = usageData.prompt_tokens + usageData.completion_tokens;
329
- // Update cache metrics if present
330
319
  if (event.usage.cache_creation_input_tokens !== undefined) {
331
320
  usageData.cache_creation_input_tokens = event.usage.cache_creation_input_tokens;
332
321
  }
@@ -336,17 +325,12 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
336
325
  }
337
326
  }
338
327
  }
339
- // Yield tool calls if any (only after stream completes)
340
328
  if (hasToolCalls && toolCallsBuffer.size > 0) {
341
- // Validate that all tool call arguments are complete valid JSON
342
329
  const toolCalls = Array.from(toolCallsBuffer.values());
343
330
  for (const toolCall of toolCalls) {
344
331
  try {
345
- // Validate JSON completeness
346
- // Empty string should be treated as empty object
347
332
  const args = toolCall.function.arguments.trim() || '{}';
348
333
  JSON.parse(args);
349
- // Update with normalized version
350
334
  toolCall.function.arguments = args;
351
335
  }
352
336
  catch (e) {
@@ -359,25 +343,17 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal)
359
343
  tool_calls: toolCalls
360
344
  };
361
345
  }
362
- // Yield usage information if available
363
346
  if (usageData) {
364
347
  yield {
365
348
  type: 'usage',
366
349
  usage: usageData
367
350
  };
368
351
  }
369
- // Signal completion
370
352
  yield {
371
353
  type: 'done'
372
354
  };
373
- }
374
- catch (error) {
375
- if (abortSignal?.aborted) {
376
- return;
377
- }
378
- if (error instanceof Error) {
379
- throw new Error(`Anthropic streaming completion failed: ${error.message}`);
380
- }
381
- throw new Error('Anthropic streaming completion failed: Unknown error');
382
- }
355
+ }, {
356
+ abortSignal,
357
+ onRetry
358
+ });
383
359
  }
@@ -60,11 +60,11 @@ export declare function resetOpenAIClient(): void;
60
60
  /**
61
61
  * Create chat completion with automatic function calling support
62
62
  */
63
- export declare function createChatCompletionWithTools(options: ChatCompletionOptions, maxToolRounds?: number): Promise<{
63
+ export declare function createChatCompletionWithTools(options: ChatCompletionOptions, maxToolRounds?: number, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): Promise<{
64
64
  content: string;
65
65
  toolCalls: ToolCall[];
66
66
  }>;
67
- export declare function createChatCompletion(options: ChatCompletionOptions): Promise<string>;
67
+ export declare function createChatCompletion(options: ChatCompletionOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): Promise<string>;
68
68
  export interface UsageInfo {
69
69
  prompt_tokens: number;
70
70
  completion_tokens: number;
@@ -74,7 +74,7 @@ export interface UsageInfo {
74
74
  cached_tokens?: number;
75
75
  }
76
76
  export interface StreamChunk {
77
- type: 'content' | 'tool_calls' | 'tool_call_delta' | 'reasoning_delta' | 'done' | 'usage';
77
+ type: 'content' | 'tool_calls' | 'tool_call_delta' | 'reasoning_delta' | 'reasoning_started' | 'done' | 'usage';
78
78
  content?: string;
79
79
  tool_calls?: Array<{
80
80
  id: string;
@@ -91,5 +91,5 @@ export interface StreamChunk {
91
91
  * Simple streaming chat completion - only handles OpenAI interaction
92
92
  * Tool execution should be handled by the caller
93
93
  */
94
- export declare function createStreamingChatCompletion(options: ChatCompletionOptions, abortSignal?: AbortSignal): AsyncGenerator<StreamChunk, void, unknown>;
94
+ export declare function createStreamingChatCompletion(options: ChatCompletionOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): AsyncGenerator<StreamChunk, void, unknown>;
95
95
  export declare function validateChatOptions(options: ChatCompletionOptions): string[];
package/dist/api/chat.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import OpenAI from 'openai';
2
- import { getOpenAiConfig, getCustomSystemPrompt } from '../utils/apiConfig.js';
2
+ import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
3
3
  import { executeMCPTool } from '../utils/mcpToolsManager.js';
4
4
  import { SYSTEM_PROMPT } from './systemPrompt.js';
5
+ import { withRetry, withRetryGenerator } from '../utils/retryUtils.js';
5
6
  /**
6
7
  * Convert our ChatMessage format to OpenAI's ChatCompletionMessageParam format
7
8
  * Automatically prepends system prompt if not present
@@ -96,9 +97,14 @@ function getOpenAIClient() {
96
97
  if (!config.apiKey || !config.baseUrl) {
97
98
  throw new Error('OpenAI API configuration is incomplete. Please configure API settings first.');
98
99
  }
100
+ // Get custom headers
101
+ const customHeaders = getCustomHeaders();
99
102
  openaiClient = new OpenAI({
100
103
  apiKey: config.apiKey,
101
104
  baseURL: config.baseUrl,
105
+ defaultHeaders: {
106
+ ...customHeaders
107
+ }
102
108
  });
103
109
  }
104
110
  return openaiClient;
@@ -109,14 +115,14 @@ export function resetOpenAIClient() {
109
115
  /**
110
116
  * Create chat completion with automatic function calling support
111
117
  */
112
- export async function createChatCompletionWithTools(options, maxToolRounds = 5) {
118
+ export async function createChatCompletionWithTools(options, maxToolRounds = 5, abortSignal, onRetry) {
113
119
  const client = getOpenAIClient();
114
120
  let messages = [...options.messages];
115
121
  let allToolCalls = [];
116
122
  let rounds = 0;
117
123
  try {
118
124
  while (rounds < maxToolRounds) {
119
- const response = await client.chat.completions.create({
125
+ const response = await withRetry(() => client.chat.completions.create({
120
126
  model: options.model,
121
127
  messages: convertToOpenAIMessages(messages),
122
128
  stream: false,
@@ -124,6 +130,9 @@ export async function createChatCompletionWithTools(options, maxToolRounds = 5)
124
130
  max_tokens: options.max_tokens,
125
131
  tools: options.tools,
126
132
  tool_choice: options.tool_choice,
133
+ }), {
134
+ abortSignal,
135
+ onRetry
127
136
  });
128
137
  const message = response.choices[0]?.message;
129
138
  if (!message) {
@@ -179,12 +188,12 @@ export async function createChatCompletionWithTools(options, maxToolRounds = 5)
179
188
  throw new Error('Chat completion with tools failed: Unknown error');
180
189
  }
181
190
  }
182
- export async function createChatCompletion(options) {
191
+ export async function createChatCompletion(options, abortSignal, onRetry) {
183
192
  const client = getOpenAIClient();
184
193
  let messages = [...options.messages];
185
194
  try {
186
195
  while (true) {
187
- const response = await client.chat.completions.create({
196
+ const response = await withRetry(() => client.chat.completions.create({
188
197
  model: options.model,
189
198
  messages: convertToOpenAIMessages(messages),
190
199
  stream: false,
@@ -192,6 +201,9 @@ export async function createChatCompletion(options) {
192
201
  max_tokens: options.max_tokens,
193
202
  tools: options.tools,
194
203
  tool_choice: options.tool_choice,
204
+ }), {
205
+ abortSignal,
206
+ onRetry
195
207
  });
196
208
  const message = response.choices[0]?.message;
197
209
  if (!message) {
@@ -246,9 +258,10 @@ export async function createChatCompletion(options) {
246
258
  * Simple streaming chat completion - only handles OpenAI interaction
247
259
  * Tool execution should be handled by the caller
248
260
  */
249
- export async function* createStreamingChatCompletion(options, abortSignal) {
261
+ export async function* createStreamingChatCompletion(options, abortSignal, onRetry) {
250
262
  const client = getOpenAIClient();
251
- try {
263
+ // 使用重试包装生成器
264
+ yield* withRetryGenerator(async function* () {
252
265
  const stream = await client.chat.completions.create({
253
266
  model: options.model,
254
267
  messages: convertToOpenAIMessages(options.messages),
@@ -265,6 +278,7 @@ export async function* createStreamingChatCompletion(options, abortSignal) {
265
278
  let toolCallsBuffer = {};
266
279
  let hasToolCalls = false;
267
280
  let usageData;
281
+ let reasoningStarted = false; // Track if reasoning has started
268
282
  for await (const chunk of stream) {
269
283
  if (abortSignal?.aborted) {
270
284
  return;
@@ -298,6 +312,13 @@ export async function* createStreamingChatCompletion(options, abortSignal) {
298
312
  // Note: reasoning_content is NOT included in the response, only counted for tokens
299
313
  const reasoningContent = choice.delta?.reasoning_content;
300
314
  if (reasoningContent) {
315
+ // Emit reasoning_started event on first reasoning content
316
+ if (!reasoningStarted) {
317
+ reasoningStarted = true;
318
+ yield {
319
+ type: 'reasoning_started'
320
+ };
321
+ }
301
322
  yield {
302
323
  type: 'reasoning_delta',
303
324
  delta: reasoningContent
@@ -363,16 +384,10 @@ export async function* createStreamingChatCompletion(options, abortSignal) {
363
384
  yield {
364
385
  type: 'done'
365
386
  };
366
- }
367
- catch (error) {
368
- if (error instanceof Error && error.name === 'AbortError') {
369
- return;
370
- }
371
- if (error instanceof Error) {
372
- throw new Error(`Streaming chat completion failed: ${error.message}`);
373
- }
374
- throw new Error('Streaming chat completion failed: Unknown error');
375
- }
387
+ }, {
388
+ abortSignal,
389
+ onRetry
390
+ });
376
391
  }
377
392
  export function validateChatOptions(options) {
378
393
  const errors = [];
@@ -32,4 +32,4 @@ export declare function resetGeminiClient(): void;
32
32
  /**
33
33
  * Create streaming chat completion using Gemini API
34
34
  */
35
- export declare function createStreamingGeminiCompletion(options: GeminiOptions, abortSignal?: AbortSignal): AsyncGenerator<GeminiStreamChunk, void, unknown>;
35
+ export declare function createStreamingGeminiCompletion(options: GeminiOptions, abortSignal?: AbortSignal, onRetry?: (error: Error, attempt: number, nextDelay: number) => void): AsyncGenerator<GeminiStreamChunk, void, unknown>;
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI } from '@google/genai';
2
- import { getOpenAiConfig, getCustomSystemPrompt } from '../utils/apiConfig.js';
2
+ import { getOpenAiConfig, getCustomSystemPrompt, getCustomHeaders } from '../utils/apiConfig.js';
3
3
  import { SYSTEM_PROMPT } from './systemPrompt.js';
4
+ import { withRetryGenerator } from '../utils/retryUtils.js';
4
5
  let geminiClient = null;
5
6
  function getGeminiClient() {
6
7
  if (!geminiClient) {
@@ -12,12 +13,23 @@ function getGeminiClient() {
12
13
  const clientConfig = {
13
14
  apiKey: config.apiKey
14
15
  };
16
+ // Get custom headers
17
+ const customHeaders = getCustomHeaders();
15
18
  // Support custom baseUrl and headers for proxy servers
16
19
  if (config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1') {
17
20
  clientConfig.httpOptions = {
18
21
  baseUrl: config.baseUrl,
19
22
  headers: {
20
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
21
33
  }
22
34
  };
23
35
  }
@@ -200,9 +212,10 @@ function convertToGeminiMessages(messages) {
200
212
  /**
201
213
  * Create streaming chat completion using Gemini API
202
214
  */
203
- export async function* createStreamingGeminiCompletion(options, abortSignal) {
215
+ export async function* createStreamingGeminiCompletion(options, abortSignal, onRetry) {
204
216
  const client = getGeminiClient();
205
- try {
217
+ // 使用重试包装生成器
218
+ yield* withRetryGenerator(async function* () {
206
219
  const { systemInstruction, contents } = convertToGeminiMessages(options.messages);
207
220
  // Build request config
208
221
  const requestConfig = {
@@ -297,14 +310,8 @@ export async function* createStreamingGeminiCompletion(options, abortSignal) {
297
310
  yield {
298
311
  type: 'done'
299
312
  };
300
- }
301
- catch (error) {
302
- if (abortSignal?.aborted) {
303
- return;
304
- }
305
- if (error instanceof Error) {
306
- throw new Error(`Gemini streaming completion failed: ${error.message}`);
307
- }
308
- throw new Error('Gemini streaming completion failed: Unknown error');
309
- }
313
+ }, {
314
+ abortSignal,
315
+ onRetry
316
+ });
310
317
  }
@@ -8,5 +8,8 @@ export interface ModelsResponse {
8
8
  object: string;
9
9
  data: Model[];
10
10
  }
11
+ /**
12
+ * Fetch available models based on configured request method
13
+ */
11
14
  export declare function fetchAvailableModels(): Promise<Model[]>;
12
15
  export declare function filterModels(models: Model[], searchTerm: string): Model[];