snow-ai 0.3.1 → 0.3.3

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.
@@ -0,0 +1,31 @@
1
+ export declare class SummaryAgent {
2
+ private modelName;
3
+ private requestMethod;
4
+ private initialized;
5
+ /**
6
+ * Initialize the summary agent with current configuration
7
+ * @returns true if initialized successfully, false otherwise
8
+ */
9
+ private initialize;
10
+ /**
11
+ * Check if summary agent is available
12
+ */
13
+ isAvailable(): Promise<boolean>;
14
+ /**
15
+ * Call the basic model with the same routing as main flow
16
+ * Uses streaming APIs and intercepts to assemble complete response
17
+ * This ensures 100% consistency with main flow routing
18
+ * @param messages - Chat messages
19
+ * @param abortSignal - Optional abort signal to cancel the request
20
+ */
21
+ private callBasicModel;
22
+ /**
23
+ * Generate a concise summary from the first user message
24
+ *
25
+ * @param userMessage - The first user message in the conversation
26
+ * @param abortSignal - Optional abort signal to cancel generation
27
+ * @returns A concise summary (10-20 words) suitable for session title
28
+ */
29
+ generateSummary(userMessage: string, abortSignal?: AbortSignal): Promise<string>;
30
+ }
31
+ export declare const summaryAgent: SummaryAgent;
@@ -0,0 +1,256 @@
1
+ import { getOpenAiConfig, getCustomSystemPrompt } from '../utils/apiConfig.js';
2
+ import { logger } from '../utils/logger.js';
3
+ import { createStreamingChatCompletion } from '../api/chat.js';
4
+ import { createStreamingResponse } from '../api/responses.js';
5
+ import { createStreamingGeminiCompletion } from '../api/gemini.js';
6
+ import { createStreamingAnthropicCompletion } from '../api/anthropic.js';
7
+ export class SummaryAgent {
8
+ constructor() {
9
+ Object.defineProperty(this, "modelName", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: ''
14
+ });
15
+ Object.defineProperty(this, "requestMethod", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: 'chat'
20
+ });
21
+ Object.defineProperty(this, "initialized", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: false
26
+ });
27
+ }
28
+ /**
29
+ * Initialize the summary agent with current configuration
30
+ * @returns true if initialized successfully, false otherwise
31
+ */
32
+ async initialize() {
33
+ try {
34
+ const config = getOpenAiConfig();
35
+ // Check if basic model is configured
36
+ if (!config.basicModel) {
37
+ return false;
38
+ }
39
+ this.modelName = config.basicModel;
40
+ this.requestMethod = config.requestMethod; // Follow main flow's request method
41
+ this.initialized = true;
42
+ return true;
43
+ }
44
+ catch (error) {
45
+ logger.warn('Failed to initialize summary agent:', error);
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Check if summary agent is available
51
+ */
52
+ async isAvailable() {
53
+ if (!this.initialized) {
54
+ return await this.initialize();
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * Call the basic model with the same routing as main flow
60
+ * Uses streaming APIs and intercepts to assemble complete response
61
+ * This ensures 100% consistency with main flow routing
62
+ * @param messages - Chat messages
63
+ * @param abortSignal - Optional abort signal to cancel the request
64
+ */
65
+ async callBasicModel(messages, abortSignal) {
66
+ const config = getOpenAiConfig();
67
+ if (!config.basicModel) {
68
+ throw new Error('Basic model not configured');
69
+ }
70
+ // Get custom system prompt if configured
71
+ const customSystemPrompt = getCustomSystemPrompt();
72
+ // If custom system prompt exists, prepend it to messages
73
+ // This ensures summary agent respects user's custom system configuration
74
+ let processedMessages = messages;
75
+ if (customSystemPrompt) {
76
+ processedMessages = [
77
+ {
78
+ role: 'system',
79
+ content: customSystemPrompt,
80
+ },
81
+ ...messages,
82
+ ];
83
+ }
84
+ // Temporarily override advancedModel with basicModel
85
+ const originalAdvancedModel = config.advancedModel;
86
+ try {
87
+ // Override config to use basicModel
88
+ config.advancedModel = config.basicModel;
89
+ let streamGenerator;
90
+ // Route to appropriate streaming API based on request method (follows main flow exactly)
91
+ switch (this.requestMethod) {
92
+ case 'anthropic':
93
+ streamGenerator = createStreamingAnthropicCompletion({
94
+ model: this.modelName,
95
+ messages: processedMessages,
96
+ max_tokens: 1024, // Summaries are short
97
+ }, abortSignal);
98
+ break;
99
+ case 'gemini':
100
+ streamGenerator = createStreamingGeminiCompletion({
101
+ model: this.modelName,
102
+ messages: processedMessages,
103
+ }, abortSignal);
104
+ break;
105
+ case 'responses':
106
+ streamGenerator = createStreamingResponse({
107
+ model: this.modelName,
108
+ messages: processedMessages,
109
+ stream: true,
110
+ }, abortSignal);
111
+ break;
112
+ case 'chat':
113
+ default:
114
+ streamGenerator = createStreamingChatCompletion({
115
+ model: this.modelName,
116
+ messages: processedMessages,
117
+ stream: true,
118
+ }, abortSignal);
119
+ break;
120
+ }
121
+ // Intercept streaming response and assemble complete content
122
+ let completeContent = '';
123
+ let chunkCount = 0;
124
+ try {
125
+ for await (const chunk of streamGenerator) {
126
+ chunkCount++;
127
+ // Check abort signal
128
+ if (abortSignal?.aborted) {
129
+ throw new Error('Request aborted');
130
+ }
131
+ // Handle different chunk formats based on request method
132
+ if (this.requestMethod === 'chat') {
133
+ // Chat API uses standard OpenAI format: {choices: [{delta: {content}}]}
134
+ if (chunk.choices && chunk.choices[0]?.delta?.content) {
135
+ completeContent += chunk.choices[0].delta.content;
136
+ }
137
+ }
138
+ else {
139
+ // Responses, Gemini, and Anthropic APIs all use: {type: 'content', content: string}
140
+ if (chunk.type === 'content' && chunk.content) {
141
+ completeContent += chunk.content;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ catch (streamError) {
147
+ // Log streaming error with details
148
+ if (streamError instanceof Error) {
149
+ logger.error('Summary agent: Streaming error:', {
150
+ error: streamError.message,
151
+ stack: streamError.stack,
152
+ name: streamError.name,
153
+ chunkCount,
154
+ contentLength: completeContent.length,
155
+ });
156
+ }
157
+ else {
158
+ logger.error('Summary agent: Unknown streaming error:', {
159
+ error: streamError,
160
+ chunkCount,
161
+ contentLength: completeContent.length,
162
+ });
163
+ }
164
+ throw streamError;
165
+ }
166
+ return completeContent;
167
+ }
168
+ catch (error) {
169
+ // Log detailed error from API call setup or streaming
170
+ if (error instanceof Error) {
171
+ logger.error('Summary agent: API call failed:', {
172
+ error: error.message,
173
+ stack: error.stack,
174
+ name: error.name,
175
+ requestMethod: this.requestMethod,
176
+ modelName: this.modelName,
177
+ });
178
+ }
179
+ else {
180
+ logger.error('Summary agent: Unknown API error:', {
181
+ error,
182
+ requestMethod: this.requestMethod,
183
+ modelName: this.modelName,
184
+ });
185
+ }
186
+ throw error;
187
+ }
188
+ finally {
189
+ // Restore original config
190
+ config.advancedModel = originalAdvancedModel;
191
+ }
192
+ }
193
+ /**
194
+ * Generate a concise summary from the first user message
195
+ *
196
+ * @param userMessage - The first user message in the conversation
197
+ * @param abortSignal - Optional abort signal to cancel generation
198
+ * @returns A concise summary (10-20 words) suitable for session title
199
+ */
200
+ async generateSummary(userMessage, abortSignal) {
201
+ const available = await this.isAvailable();
202
+ if (!available) {
203
+ // If summary agent is not available, return a truncated version of the message
204
+ return userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : '');
205
+ }
206
+ try {
207
+ const summaryPrompt = `Generate a concise summary (10-20 words) for the following user message. The summary should capture the main topic or intent.
208
+
209
+ User Message: ${userMessage}
210
+
211
+ Instructions:
212
+ 1. Keep it under 20 words
213
+ 2. Focus on the main topic or question
214
+ 3. Use clear, simple language
215
+ 4. Do not include quotes or special formatting
216
+ 5. Make it suitable as a conversation title
217
+
218
+ Summary:`;
219
+ const messages = [
220
+ {
221
+ role: 'user',
222
+ content: summaryPrompt,
223
+ },
224
+ ];
225
+ const summary = await this.callBasicModel(messages, abortSignal);
226
+ if (!summary || summary.trim().length === 0) {
227
+ logger.warn('Summary agent returned empty response, using truncated message');
228
+ return (userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : ''));
229
+ }
230
+ // Clean up the summary (remove quotes, trim whitespace)
231
+ const cleanedSummary = summary
232
+ .trim()
233
+ .replace(/^["']|["']$/g, '') // Remove leading/trailing quotes
234
+ .replace(/\n/g, ' ') // Replace newlines with spaces
235
+ .slice(0, 100); // Limit to 100 characters max
236
+ return cleanedSummary;
237
+ }
238
+ catch (error) {
239
+ // Log detailed error information
240
+ if (error instanceof Error) {
241
+ logger.warn('Summary agent generation failed, using truncated message:', {
242
+ error: error.message,
243
+ stack: error.stack,
244
+ name: error.name,
245
+ });
246
+ }
247
+ else {
248
+ logger.warn('Summary agent generation failed with unknown error:', error);
249
+ }
250
+ // Fallback to truncated message
251
+ return userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : '');
252
+ }
253
+ }
254
+ }
255
+ // Export singleton instance
256
+ export const summaryAgent = new SummaryAgent();
@@ -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;