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.
- package/dist/api/anthropic.d.ts +13 -9
- package/dist/api/anthropic.js +77 -34
- package/dist/api/chat.d.ts +14 -29
- package/dist/api/chat.js +62 -19
- package/dist/api/gemini.d.ts +1 -10
- package/dist/api/gemini.js +104 -82
- package/dist/api/models.js +6 -7
- package/dist/api/responses.d.ts +2 -17
- package/dist/api/responses.js +59 -17
- package/dist/api/types.d.ts +39 -0
- package/dist/api/types.js +4 -0
- package/dist/ui/pages/ConfigScreen.js +67 -49
- package/dist/ui/pages/WelcomeScreen.js +1 -1
- package/dist/utils/contextCompressor.d.ts +1 -1
- package/dist/utils/contextCompressor.js +193 -81
- package/package.json +1 -4
package/dist/api/anthropic.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { ChatMessage } from './
|
|
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
|
package/dist/api/anthropic.js
CHANGED
|
@@ -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
|
|
7
|
-
function
|
|
8
|
-
if (!
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
22
|
+
return anthropicConfig;
|
|
40
23
|
}
|
|
41
24
|
export function resetAnthropicClient() {
|
|
42
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
285
|
+
for await (const event of parseSSEStream(response.body.getReader())) {
|
|
243
286
|
if (abortSignal?.aborted) {
|
|
244
287
|
return;
|
|
245
288
|
}
|
package/dist/api/chat.d.ts
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
import type { ChatCompletionTool } from '
|
|
2
|
-
export
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
93
|
-
function
|
|
94
|
-
if (!
|
|
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
|
-
|
|
99
|
+
openaiConfig = {
|
|
102
100
|
apiKey: config.apiKey,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
});
|
|
101
|
+
baseUrl: config.baseUrl,
|
|
102
|
+
customHeaders
|
|
103
|
+
};
|
|
108
104
|
}
|
|
109
|
-
return
|
|
105
|
+
return openaiConfig;
|
|
110
106
|
}
|
|
111
107
|
export function resetOpenAIClient() {
|
|
112
|
-
|
|
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
|
|
147
|
+
const config = getOpenAIConfig();
|
|
120
148
|
// 使用重试包装生成器
|
|
121
149
|
yield* withRetryGenerator(async function* () {
|
|
122
|
-
const
|
|
150
|
+
const requestBody = {
|
|
123
151
|
model: options.model,
|
|
124
152
|
messages: convertToOpenAIMessages(options.messages),
|
|
125
153
|
stream: true,
|
|
126
|
-
stream_options: { include_usage: true },
|
|
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
|
-
|
|
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
|
|
182
|
+
for await (const chunk of parseSSEStream(response.body.getReader())) {
|
|
140
183
|
if (abortSignal?.aborted) {
|
|
141
184
|
return;
|
|
142
185
|
}
|
package/dist/api/gemini.d.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import type { ChatMessage } from './
|
|
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;
|
package/dist/api/gemini.js
CHANGED
|
@@ -1,44 +1,26 @@
|
|
|
1
|
-
import { GoogleGenAI } from '@google/genai';
|
|
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';
|
|
5
|
-
let
|
|
6
|
-
function
|
|
7
|
-
if (!
|
|
4
|
+
let geminiConfig = null;
|
|
5
|
+
function getGeminiConfig() {
|
|
6
|
+
if (!geminiConfig) {
|
|
8
7
|
const config = getOpenAiConfig();
|
|
9
8
|
if (!config.apiKey) {
|
|
10
9
|
throw new Error('Gemini API configuration is incomplete. Please configure API key first.');
|
|
11
10
|
}
|
|
12
|
-
// Create client configuration
|
|
13
|
-
const clientConfig = {
|
|
14
|
-
apiKey: config.apiKey
|
|
15
|
-
};
|
|
16
|
-
// Get custom headers
|
|
17
11
|
const customHeaders = getCustomHeaders();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
geminiClient = new GoogleGenAI(clientConfig);
|
|
12
|
+
geminiConfig = {
|
|
13
|
+
apiKey: config.apiKey,
|
|
14
|
+
baseUrl: config.baseUrl && config.baseUrl !== 'https://api.openai.com/v1'
|
|
15
|
+
? config.baseUrl
|
|
16
|
+
: 'https://generativelanguage.googleapis.com/v1beta',
|
|
17
|
+
customHeaders
|
|
18
|
+
};
|
|
37
19
|
}
|
|
38
|
-
return
|
|
20
|
+
return geminiConfig;
|
|
39
21
|
}
|
|
40
22
|
export function resetGeminiClient() {
|
|
41
|
-
|
|
23
|
+
geminiConfig = null;
|
|
42
24
|
}
|
|
43
25
|
/**
|
|
44
26
|
* Convert OpenAI-style tools to Gemini function declarations
|
|
@@ -213,81 +195,121 @@ function convertToGeminiMessages(messages) {
|
|
|
213
195
|
* Create streaming chat completion using Gemini API
|
|
214
196
|
*/
|
|
215
197
|
export async function* createStreamingGeminiCompletion(options, abortSignal, onRetry) {
|
|
216
|
-
const
|
|
198
|
+
const config = getGeminiConfig();
|
|
217
199
|
// 使用重试包装生成器
|
|
218
200
|
yield* withRetryGenerator(async function* () {
|
|
219
201
|
const { systemInstruction, contents } = convertToGeminiMessages(options.messages);
|
|
220
|
-
// Build request
|
|
221
|
-
const
|
|
222
|
-
model: options.model,
|
|
202
|
+
// Build request payload
|
|
203
|
+
const requestBody = {
|
|
223
204
|
contents,
|
|
224
|
-
|
|
225
|
-
|
|
205
|
+
systemInstruction: systemInstruction ? { parts: [{ text: systemInstruction }] } : undefined,
|
|
206
|
+
generationConfig: {
|
|
226
207
|
temperature: options.temperature ?? 0.7,
|
|
227
208
|
}
|
|
228
209
|
};
|
|
229
210
|
// Add tools if provided
|
|
230
211
|
const geminiTools = convertToolsToGemini(options.tools);
|
|
231
212
|
if (geminiTools) {
|
|
232
|
-
|
|
213
|
+
requestBody.tools = geminiTools;
|
|
214
|
+
}
|
|
215
|
+
// Extract model name from options.model (e.g., "gemini-pro" or "models/gemini-pro")
|
|
216
|
+
const modelName = options.model.startsWith('models/') ? options.model : `models/${options.model}`;
|
|
217
|
+
const url = `${config.baseUrl}/${modelName}:streamGenerateContent?key=${config.apiKey}&alt=sse`;
|
|
218
|
+
const response = await fetch(url, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'authorization': `Bearer ${config.apiKey}`,
|
|
223
|
+
...config.customHeaders
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify(requestBody),
|
|
226
|
+
signal: abortSignal
|
|
227
|
+
});
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
const errorText = await response.text();
|
|
230
|
+
throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
231
|
+
}
|
|
232
|
+
if (!response.body) {
|
|
233
|
+
throw new Error('No response body from Gemini API');
|
|
233
234
|
}
|
|
234
|
-
// Stream the response
|
|
235
|
-
const stream = await client.models.generateContentStream(requestConfig);
|
|
236
235
|
let contentBuffer = '';
|
|
237
236
|
let toolCallsBuffer = [];
|
|
238
237
|
let hasToolCalls = false;
|
|
239
238
|
let toolCallIndex = 0;
|
|
240
239
|
let totalTokens = { prompt: 0, completion: 0, total: 0 };
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
// Parse SSE stream
|
|
241
|
+
const reader = response.body.getReader();
|
|
242
|
+
const decoder = new TextDecoder();
|
|
243
|
+
let buffer = '';
|
|
244
|
+
while (true) {
|
|
245
|
+
const { done, value } = await reader.read();
|
|
246
|
+
if (done)
|
|
247
|
+
break;
|
|
245
248
|
if (abortSignal?.aborted) {
|
|
246
|
-
console.warn = originalWarn; // Restore console.warn
|
|
247
249
|
return;
|
|
248
250
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
251
|
+
buffer += decoder.decode(value, { stream: true });
|
|
252
|
+
const lines = buffer.split('\n');
|
|
253
|
+
buffer = lines.pop() || '';
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
257
|
+
continue;
|
|
258
|
+
if (trimmed.startsWith('data: ')) {
|
|
259
|
+
const data = trimmed.slice(6);
|
|
260
|
+
try {
|
|
261
|
+
const chunk = JSON.parse(data);
|
|
262
|
+
// Process candidates
|
|
263
|
+
if (chunk.candidates && chunk.candidates.length > 0) {
|
|
264
|
+
const candidate = chunk.candidates[0];
|
|
265
|
+
if (candidate.content && candidate.content.parts) {
|
|
266
|
+
for (const part of candidate.content.parts) {
|
|
267
|
+
// Process text content
|
|
268
|
+
if (part.text) {
|
|
269
|
+
contentBuffer += part.text;
|
|
270
|
+
yield {
|
|
271
|
+
type: 'content',
|
|
272
|
+
content: part.text
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// Process function calls
|
|
276
|
+
if (part.functionCall) {
|
|
277
|
+
hasToolCalls = true;
|
|
278
|
+
const fc = part.functionCall;
|
|
279
|
+
const toolCall = {
|
|
280
|
+
id: `call_${toolCallIndex++}`,
|
|
281
|
+
type: 'function',
|
|
282
|
+
function: {
|
|
283
|
+
name: fc.name,
|
|
284
|
+
arguments: JSON.stringify(fc.args || {})
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
toolCallsBuffer.push(toolCall);
|
|
288
|
+
// Yield delta for token counting
|
|
289
|
+
const deltaText = fc.name + JSON.stringify(fc.args || {});
|
|
290
|
+
yield {
|
|
291
|
+
type: 'tool_call_delta',
|
|
292
|
+
delta: deltaText
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
269
297
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
298
|
+
// Track usage info
|
|
299
|
+
if (chunk.usageMetadata) {
|
|
300
|
+
totalTokens = {
|
|
301
|
+
prompt: chunk.usageMetadata.promptTokenCount || 0,
|
|
302
|
+
completion: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
303
|
+
total: chunk.usageMetadata.totalTokenCount || 0
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
console.error('Failed to parse Gemini SSE data:', data);
|
|
309
|
+
}
|
|
278
310
|
}
|
|
279
311
|
}
|
|
280
|
-
// Track usage info
|
|
281
|
-
if (chunk.usageMetadata) {
|
|
282
|
-
totalTokens = {
|
|
283
|
-
prompt: chunk.usageMetadata.promptTokenCount || 0,
|
|
284
|
-
completion: chunk.usageMetadata.candidatesTokenCount || 0,
|
|
285
|
-
total: chunk.usageMetadata.totalTokenCount || 0
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
312
|
}
|
|
289
|
-
// Restore console.warn
|
|
290
|
-
console.warn = originalWarn;
|
|
291
313
|
// Yield tool calls if any
|
|
292
314
|
if (hasToolCalls && toolCallsBuffer.length > 0) {
|
|
293
315
|
yield {
|