@absolutejs/ai 0.0.1
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/README.md +6 -0
- package/dist/ai/client/index.js +682 -0
- package/dist/ai/client/index.js.map +15 -0
- package/dist/ai/index.js +3317 -0
- package/dist/ai/index.js.map +28 -0
- package/dist/ai/providers/anthropic.js +371 -0
- package/dist/ai/providers/anthropic.js.map +10 -0
- package/dist/ai/providers/gemini.js +260 -0
- package/dist/ai/providers/gemini.js.map +10 -0
- package/dist/ai/providers/ollama.js +221 -0
- package/dist/ai/providers/ollama.js.map +10 -0
- package/dist/ai/providers/openai.js +322 -0
- package/dist/ai/providers/openai.js.map +10 -0
- package/dist/ai/providers/openaiCompatible.js +360 -0
- package/dist/ai/providers/openaiCompatible.js.map +11 -0
- package/dist/ai/providers/openaiResponses.js +381 -0
- package/dist/ai/providers/openaiResponses.js.map +10 -0
- package/dist/src/ai/client/actions.d.ts +186 -0
- package/dist/src/ai/client/connection.d.ts +9 -0
- package/dist/src/ai/client/createAIStream.d.ts +12 -0
- package/dist/src/ai/client/index.d.ts +4 -0
- package/dist/src/ai/client/messageStore.d.ts +12 -0
- package/dist/src/ai/conversationManager.d.ts +19 -0
- package/dist/src/ai/htmxRenderers.d.ts +3 -0
- package/dist/src/ai/index.d.ts +19 -0
- package/dist/src/ai/memoryStore.d.ts +2 -0
- package/dist/src/ai/protocol.d.ts +4 -0
- package/dist/src/ai/providers/anthropic.d.ts +3 -0
- package/dist/src/ai/providers/gemini.d.ts +8 -0
- package/dist/src/ai/providers/ollama.d.ts +6 -0
- package/dist/src/ai/providers/openai.d.ts +7 -0
- package/dist/src/ai/providers/openaiCompatible.d.ts +30 -0
- package/dist/src/ai/providers/openaiResponses.d.ts +33 -0
- package/dist/src/ai/streamAI.d.ts +2 -0
- package/dist/src/ai/streamAIToSSE.d.ts +6 -0
- package/dist/src/constants.d.ts +60 -0
- package/dist/src/plugins/aiChat.d.ts +239 -0
- package/dist/types/ai.d.ts +4773 -0
- package/dist/types/anthropic.d.ts +20 -0
- package/dist/types/session.d.ts +16 -0
- package/dist/types/typeGuards.d.ts +3 -0
- package/package.json +59 -0
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,3317 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/ai/providers/openai.ts
|
|
3
|
+
var DEFAULT_BASE_URL = "https://api.openai.com";
|
|
4
|
+
var SSE_DATA_PREFIX_LENGTH = 6;
|
|
5
|
+
var DONE_SENTINEL = "[DONE]";
|
|
6
|
+
var NOT_FOUND = -1;
|
|
7
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
8
|
+
var isRecordArray = (value) => Array.isArray(value) && value.length > 0 && isRecord(value[0]);
|
|
9
|
+
var hasArrayContent = (msg) => typeof msg.content !== "string" && Array.isArray(msg.content);
|
|
10
|
+
var buildToolMessages = (blocks) => {
|
|
11
|
+
const toolUseBlocks = blocks.filter((block) => block.type === "tool_use");
|
|
12
|
+
const toolResultBlocks = blocks.filter((block) => block.type === "tool_result");
|
|
13
|
+
const messages = [];
|
|
14
|
+
if (toolUseBlocks.length > 0) {
|
|
15
|
+
messages.push({
|
|
16
|
+
content: null,
|
|
17
|
+
role: "assistant",
|
|
18
|
+
tool_calls: toolUseBlocks.map((block) => ({
|
|
19
|
+
function: {
|
|
20
|
+
arguments: typeof block.input === "string" ? block.input : JSON.stringify(block.input),
|
|
21
|
+
name: block.name
|
|
22
|
+
},
|
|
23
|
+
id: block.id,
|
|
24
|
+
type: "function"
|
|
25
|
+
}))
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const result of toolResultBlocks) {
|
|
29
|
+
messages.push({
|
|
30
|
+
content: typeof result.content === "string" ? result.content : "",
|
|
31
|
+
role: "tool",
|
|
32
|
+
tool_call_id: result.tool_use_id
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return messages;
|
|
36
|
+
};
|
|
37
|
+
var processMessageAtIndex = (result, msg, idx) => {
|
|
38
|
+
if (!hasArrayContent(msg)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const hasToolBlocks = msg.content.some((block) => block.type === "tool_use" || block.type === "tool_result");
|
|
42
|
+
if (!hasToolBlocks) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const toolMessages = buildToolMessages(msg.content);
|
|
46
|
+
result.splice(idx, 1, ...toolMessages);
|
|
47
|
+
};
|
|
48
|
+
var convertSingleMessage = (result, msg, idx) => {
|
|
49
|
+
if (!msg) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
processMessageAtIndex(result, msg, idx);
|
|
53
|
+
};
|
|
54
|
+
var convertToolResultMessages = (messages, params) => {
|
|
55
|
+
const result = [...messages];
|
|
56
|
+
for (let idx = 0;idx < params.messages.length; idx++) {
|
|
57
|
+
convertSingleMessage(result, params.messages[idx], idx);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
var mapToolDefinitions = (tools) => tools.map((tool) => ({
|
|
62
|
+
function: {
|
|
63
|
+
description: tool.description,
|
|
64
|
+
name: tool.name,
|
|
65
|
+
parameters: tool.input_schema
|
|
66
|
+
},
|
|
67
|
+
type: "function"
|
|
68
|
+
}));
|
|
69
|
+
var mapContentBlockToOpenAI = (block) => {
|
|
70
|
+
if (block.type === "image") {
|
|
71
|
+
return {
|
|
72
|
+
image_url: {
|
|
73
|
+
url: `data:${block.source.media_type};base64,${block.source.data}`
|
|
74
|
+
},
|
|
75
|
+
type: "image_url"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (block.type === "document") {
|
|
79
|
+
return {
|
|
80
|
+
file: {
|
|
81
|
+
file_data: `data:${block.source.media_type};base64,${block.source.data}`,
|
|
82
|
+
filename: block.name ?? "document.pdf"
|
|
83
|
+
},
|
|
84
|
+
type: "file"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (block.type === "text") {
|
|
88
|
+
return { text: block.content, type: "text" };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
var mapOpenAIContent = (msg) => {
|
|
93
|
+
if (typeof msg.content === "string") {
|
|
94
|
+
return msg.content;
|
|
95
|
+
}
|
|
96
|
+
const hasMedia = msg.content.some((block) => block.type === "image" || block.type === "document");
|
|
97
|
+
if (!hasMedia) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return msg.content.map(mapContentBlockToOpenAI).filter((mapped) => mapped !== null);
|
|
101
|
+
};
|
|
102
|
+
var buildRequestBody = (params) => {
|
|
103
|
+
const messages = convertToolResultMessages(params.messages.map((msg) => ({
|
|
104
|
+
content: mapOpenAIContent(msg),
|
|
105
|
+
role: msg.role
|
|
106
|
+
})), params);
|
|
107
|
+
const body = {
|
|
108
|
+
messages,
|
|
109
|
+
model: params.model,
|
|
110
|
+
stream: true,
|
|
111
|
+
stream_options: { include_usage: true }
|
|
112
|
+
};
|
|
113
|
+
if (params.tools && params.tools.length > 0) {
|
|
114
|
+
body.tools = mapToolDefinitions(params.tools);
|
|
115
|
+
}
|
|
116
|
+
return body;
|
|
117
|
+
};
|
|
118
|
+
var parseToolInput = (rawArguments) => {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(rawArguments);
|
|
121
|
+
} catch {
|
|
122
|
+
return rawArguments;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var flushPendingToolCalls = function* (pendingToolCalls) {
|
|
126
|
+
for (const [, tool] of pendingToolCalls) {
|
|
127
|
+
const input = parseToolInput(tool.arguments);
|
|
128
|
+
yield {
|
|
129
|
+
id: tool.id,
|
|
130
|
+
input,
|
|
131
|
+
name: tool.name,
|
|
132
|
+
type: "tool_use"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
pendingToolCalls.clear();
|
|
136
|
+
};
|
|
137
|
+
var extractUsage = (parsedUsage) => ({
|
|
138
|
+
inputTokens: parsedUsage.prompt_tokens ?? 0,
|
|
139
|
+
outputTokens: parsedUsage.completion_tokens ?? 0
|
|
140
|
+
});
|
|
141
|
+
var resolveToolCallIndex = (toolCall) => {
|
|
142
|
+
const raw = typeof toolCall.index === "number" ? toolCall.index : NOT_FOUND;
|
|
143
|
+
return raw < 0 ? undefined : raw;
|
|
144
|
+
};
|
|
145
|
+
var initPendingToolCall = (toolCall, func, index, pendingToolCalls) => {
|
|
146
|
+
if (pendingToolCalls.has(index)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const toolId = typeof toolCall.id === "string" ? toolCall.id : "";
|
|
150
|
+
const toolName = func && typeof func.name === "string" ? func.name : "";
|
|
151
|
+
pendingToolCalls.set(index, {
|
|
152
|
+
arguments: "",
|
|
153
|
+
id: toolId,
|
|
154
|
+
name: toolName
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
var updatePendingToolCall = (toolCall, func, pending) => {
|
|
158
|
+
if (typeof toolCall.id === "string") {
|
|
159
|
+
pending.id = toolCall.id;
|
|
160
|
+
}
|
|
161
|
+
if (func && typeof func.name === "string") {
|
|
162
|
+
pending.name = func.name;
|
|
163
|
+
}
|
|
164
|
+
if (func && typeof func.arguments === "string") {
|
|
165
|
+
pending.arguments += func.arguments;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var processToolCallDelta = (toolCall, pendingToolCalls) => {
|
|
169
|
+
const index = resolveToolCallIndex(toolCall);
|
|
170
|
+
if (index === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const func = isRecord(toolCall.function) ? toolCall.function : null;
|
|
174
|
+
initPendingToolCall(toolCall, func, index, pendingToolCalls);
|
|
175
|
+
const pending = pendingToolCalls.get(index);
|
|
176
|
+
if (!pending) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
updatePendingToolCall(toolCall, func, pending);
|
|
180
|
+
};
|
|
181
|
+
var processToolCallDeltas = (toolCalls, pendingToolCalls) => {
|
|
182
|
+
for (const toolCall of toolCalls) {
|
|
183
|
+
processToolCallDelta(toolCall, pendingToolCalls);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var processDelta = function* (delta, pendingToolCalls) {
|
|
187
|
+
if (typeof delta.content === "string") {
|
|
188
|
+
yield { content: delta.content, type: "text" };
|
|
189
|
+
}
|
|
190
|
+
if (isRecordArray(delta.tool_calls)) {
|
|
191
|
+
processToolCallDeltas(delta.tool_calls, pendingToolCalls);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var processChoice = function* (choice, pendingToolCalls) {
|
|
195
|
+
const delta = isRecord(choice.delta) ? choice.delta : null;
|
|
196
|
+
if (delta) {
|
|
197
|
+
yield* processDelta(delta, pendingToolCalls);
|
|
198
|
+
}
|
|
199
|
+
if (choice.finish_reason === "tool_calls") {
|
|
200
|
+
yield* flushPendingToolCalls(pendingToolCalls);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var narrowUsageRecord = (parsed) => {
|
|
204
|
+
if (!isRecord(parsed.usage)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const { usage } = parsed;
|
|
208
|
+
const promptTokens = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
|
|
209
|
+
const completionTokens = typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0;
|
|
210
|
+
return extractUsage({
|
|
211
|
+
completion_tokens: completionTokens,
|
|
212
|
+
prompt_tokens: promptTokens
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
var processSSELine = function* (line, pendingToolCalls, currentUsage) {
|
|
216
|
+
const trimmed = line.trim();
|
|
217
|
+
if (!trimmed || !trimmed.startsWith("data: ")) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const data = trimmed.slice(SSE_DATA_PREFIX_LENGTH);
|
|
221
|
+
if (data === DONE_SENTINEL) {
|
|
222
|
+
yield* flushPendingToolCalls(pendingToolCalls);
|
|
223
|
+
yield { type: "done", usage: currentUsage };
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let parsed;
|
|
227
|
+
try {
|
|
228
|
+
parsed = JSON.parse(data);
|
|
229
|
+
} catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const usageUpdate = narrowUsageRecord(parsed);
|
|
233
|
+
if (usageUpdate) {
|
|
234
|
+
yield { type: "usage_update", usage: usageUpdate };
|
|
235
|
+
}
|
|
236
|
+
const { choices } = parsed;
|
|
237
|
+
if (!isRecordArray(choices)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const [firstChoice] = choices;
|
|
241
|
+
if (!firstChoice) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
yield* processChoice(firstChoice, pendingToolCalls);
|
|
245
|
+
};
|
|
246
|
+
var isUsageUpdate = (chunk) => chunk.type === "usage_update";
|
|
247
|
+
var collectYieldableChunks = (line, pendingToolCalls, usageRef) => {
|
|
248
|
+
const allChunks = Array.from(processSSELine(line, pendingToolCalls, usageRef.current));
|
|
249
|
+
const usageChunks = allChunks.filter(isUsageUpdate);
|
|
250
|
+
const lastUsage = usageChunks.at(NOT_FOUND);
|
|
251
|
+
if (lastUsage) {
|
|
252
|
+
usageRef.current = lastUsage.usage;
|
|
253
|
+
}
|
|
254
|
+
return allChunks.filter((chunk) => !isUsageUpdate(chunk));
|
|
255
|
+
};
|
|
256
|
+
var processSSELines = function* (lines, pendingToolCalls, usageRef) {
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
yield* collectYieldableChunks(line, pendingToolCalls, usageRef);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var processStreamValue = (value, decoder, state) => {
|
|
262
|
+
state.buffer += decoder.decode(value, { stream: true });
|
|
263
|
+
const lines = state.buffer.split(`
|
|
264
|
+
`);
|
|
265
|
+
state.buffer = lines.pop() ?? "";
|
|
266
|
+
return lines;
|
|
267
|
+
};
|
|
268
|
+
var drainReader = async function* (reader, decoder, state, signal) {
|
|
269
|
+
for (let result = await reader.read();!result.done && !signal?.aborted; result = await reader.read()) {
|
|
270
|
+
const lines = processStreamValue(result.value, decoder, state);
|
|
271
|
+
yield* processSSELines(lines, state.pendingToolCalls, state.usageRef);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var parseSSEStream = async function* (body, signal) {
|
|
275
|
+
const reader = body.getReader();
|
|
276
|
+
const decoder = new TextDecoder;
|
|
277
|
+
const state = {
|
|
278
|
+
buffer: "",
|
|
279
|
+
pendingToolCalls: new Map,
|
|
280
|
+
usageRef: { current: undefined }
|
|
281
|
+
};
|
|
282
|
+
try {
|
|
283
|
+
yield* drainReader(reader, decoder, state, signal);
|
|
284
|
+
yield { type: "done", usage: state.usageRef.current };
|
|
285
|
+
} finally {
|
|
286
|
+
reader.releaseLock();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var fetchOpenAIStream = async function* (baseUrl, apiKey, body, signal) {
|
|
290
|
+
const response = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
291
|
+
body: JSON.stringify(body),
|
|
292
|
+
headers: {
|
|
293
|
+
Authorization: `Bearer ${apiKey}`,
|
|
294
|
+
"Content-Type": "application/json"
|
|
295
|
+
},
|
|
296
|
+
method: "POST",
|
|
297
|
+
signal
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const errorText = await response.text();
|
|
301
|
+
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
|
302
|
+
}
|
|
303
|
+
if (!response.body) {
|
|
304
|
+
throw new Error("OpenAI API returned no response body");
|
|
305
|
+
}
|
|
306
|
+
yield* parseSSEStream(response.body, signal);
|
|
307
|
+
};
|
|
308
|
+
var openai = (config) => {
|
|
309
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
310
|
+
return {
|
|
311
|
+
stream: (params) => {
|
|
312
|
+
const body = buildRequestBody(params);
|
|
313
|
+
return fetchOpenAIStream(baseUrl, config.apiKey, body, params.signal);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/ai/providers/openaiCompatible.ts
|
|
319
|
+
var alibaba = (config) => openaiCompatible({
|
|
320
|
+
apiKey: config.apiKey,
|
|
321
|
+
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode"
|
|
322
|
+
});
|
|
323
|
+
var deepseek = (config) => openaiCompatible({
|
|
324
|
+
apiKey: config.apiKey,
|
|
325
|
+
baseUrl: "https://api.deepseek.com"
|
|
326
|
+
});
|
|
327
|
+
var google = (config) => openaiCompatible({
|
|
328
|
+
apiKey: config.apiKey,
|
|
329
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
330
|
+
});
|
|
331
|
+
var meta = (config) => openaiCompatible({
|
|
332
|
+
apiKey: config.apiKey,
|
|
333
|
+
baseUrl: "https://api.llama.com/compat/v1"
|
|
334
|
+
});
|
|
335
|
+
var mistralai = (config) => openaiCompatible({
|
|
336
|
+
apiKey: config.apiKey,
|
|
337
|
+
baseUrl: "https://api.mistral.ai"
|
|
338
|
+
});
|
|
339
|
+
var moonshot = (config) => openaiCompatible({
|
|
340
|
+
apiKey: config.apiKey,
|
|
341
|
+
baseUrl: "https://api.moonshot.ai"
|
|
342
|
+
});
|
|
343
|
+
var openaiCompatible = (config) => openai({ apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
344
|
+
var xai = (config) => openaiCompatible({
|
|
345
|
+
apiKey: config.apiKey,
|
|
346
|
+
baseUrl: "https://api.x.ai"
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// src/ai/providers/openaiResponses.ts
|
|
350
|
+
var DEFAULT_BASE_URL2 = "https://api.openai.com";
|
|
351
|
+
var EVENT_PREFIX_LENGTH = 7;
|
|
352
|
+
var DATA_PREFIX_LENGTH = 6;
|
|
353
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
354
|
+
var isRecordArray2 = (value) => Array.isArray(value) && value.length > 0 && isRecord2(value[0]);
|
|
355
|
+
var mapBlockToResponsesFormat = (block) => {
|
|
356
|
+
if (block.type === "text") {
|
|
357
|
+
return { text: block.content, type: "input_text" };
|
|
358
|
+
}
|
|
359
|
+
if (block.type === "image") {
|
|
360
|
+
return {
|
|
361
|
+
image_url: {
|
|
362
|
+
url: `data:${block.source.media_type};base64,${block.source.data}`
|
|
363
|
+
},
|
|
364
|
+
type: "input_image"
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (block.type === "document") {
|
|
368
|
+
return {
|
|
369
|
+
file: {
|
|
370
|
+
file_data: `data:${block.source.media_type};base64,${block.source.data}`,
|
|
371
|
+
filename: block.name ?? "document.pdf"
|
|
372
|
+
},
|
|
373
|
+
type: "input_file"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
};
|
|
378
|
+
var mapContentToResponsesFormat = (content) => {
|
|
379
|
+
if (typeof content === "string") {
|
|
380
|
+
return content;
|
|
381
|
+
}
|
|
382
|
+
const parts = content.map(mapBlockToResponsesFormat).filter((mapped) => mapped !== null);
|
|
383
|
+
return parts.length > 0 ? parts : "";
|
|
384
|
+
};
|
|
385
|
+
var hasToolBlocks = (content) => content.some((block) => block.type === "tool_use" || block.type === "tool_result");
|
|
386
|
+
var convertToolBlock = (block) => {
|
|
387
|
+
if (block.type === "tool_use") {
|
|
388
|
+
return {
|
|
389
|
+
arguments: typeof block.input === "string" ? block.input : JSON.stringify(block.input),
|
|
390
|
+
call_id: block.id,
|
|
391
|
+
name: block.name,
|
|
392
|
+
type: "function_call"
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (block.type === "tool_result") {
|
|
396
|
+
return {
|
|
397
|
+
call_id: block.tool_use_id,
|
|
398
|
+
output: typeof block.content === "string" ? block.content : "",
|
|
399
|
+
type: "function_call_output"
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
};
|
|
404
|
+
var convertToolBlocks = (content) => content.map(convertToolBlock).filter((converted) => converted !== null);
|
|
405
|
+
var convertMessage = (msg) => {
|
|
406
|
+
if (typeof msg.content !== "string" && Array.isArray(msg.content) && hasToolBlocks(msg.content)) {
|
|
407
|
+
return convertToolBlocks(msg.content);
|
|
408
|
+
}
|
|
409
|
+
const content = mapContentToResponsesFormat(msg.content);
|
|
410
|
+
return [
|
|
411
|
+
{
|
|
412
|
+
content,
|
|
413
|
+
role: msg.role === "system" ? "developer" : msg.role,
|
|
414
|
+
type: "message"
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
};
|
|
418
|
+
var buildInput = (messages) => {
|
|
419
|
+
const input = [];
|
|
420
|
+
for (const msg of messages) {
|
|
421
|
+
input.push(...convertMessage(msg));
|
|
422
|
+
}
|
|
423
|
+
return input;
|
|
424
|
+
};
|
|
425
|
+
var mapToolDefinition = (tool) => ({
|
|
426
|
+
description: tool.description,
|
|
427
|
+
name: tool.name,
|
|
428
|
+
parameters: tool.input_schema,
|
|
429
|
+
type: "function"
|
|
430
|
+
});
|
|
431
|
+
var buildTools = (tools, isImageModel) => {
|
|
432
|
+
const mapped = tools ? tools.map(mapToolDefinition) : [];
|
|
433
|
+
const result = [...mapped];
|
|
434
|
+
if (isImageModel) {
|
|
435
|
+
result.push({ type: "image_generation" });
|
|
436
|
+
}
|
|
437
|
+
return result.length > 0 ? result : undefined;
|
|
438
|
+
};
|
|
439
|
+
var buildRequestBody2 = (params, isImageModel) => {
|
|
440
|
+
const body = {
|
|
441
|
+
input: buildInput(params.messages),
|
|
442
|
+
model: params.model,
|
|
443
|
+
stream: true
|
|
444
|
+
};
|
|
445
|
+
if (params.systemPrompt) {
|
|
446
|
+
body.instructions = params.systemPrompt;
|
|
447
|
+
}
|
|
448
|
+
const tools = buildTools(params.tools, isImageModel);
|
|
449
|
+
if (tools) {
|
|
450
|
+
body.tools = tools;
|
|
451
|
+
}
|
|
452
|
+
if (params.thinking) {
|
|
453
|
+
body.reasoning = {
|
|
454
|
+
effort: "high",
|
|
455
|
+
summary: "auto"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return body;
|
|
459
|
+
};
|
|
460
|
+
var parseJSON = (data) => {
|
|
461
|
+
try {
|
|
462
|
+
return JSON.parse(data);
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var parseToolInput2 = (rawArguments) => {
|
|
468
|
+
try {
|
|
469
|
+
return JSON.parse(rawArguments);
|
|
470
|
+
} catch {
|
|
471
|
+
return rawArguments;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var extractUsage2 = (response) => {
|
|
475
|
+
if (!isRecord2(response.usage)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const { usage } = response;
|
|
479
|
+
return {
|
|
480
|
+
inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
|
|
481
|
+
outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
var extractMimeFormat = (mimeType) => {
|
|
485
|
+
if (typeof mimeType !== "string") {
|
|
486
|
+
return "png";
|
|
487
|
+
}
|
|
488
|
+
if (mimeType.includes("jpeg"))
|
|
489
|
+
return "jpeg";
|
|
490
|
+
if (mimeType.includes("webp"))
|
|
491
|
+
return "webp";
|
|
492
|
+
return "png";
|
|
493
|
+
};
|
|
494
|
+
var processTextDelta = function* (parsed) {
|
|
495
|
+
if (typeof parsed.delta === "string") {
|
|
496
|
+
yield { content: parsed.delta, type: "text" };
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var processPartialImage = function* (parsed) {
|
|
500
|
+
const itemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
501
|
+
const b64 = typeof parsed.partial_image_b64 === "string" ? parsed.partial_image_b64 : undefined;
|
|
502
|
+
if (b64) {
|
|
503
|
+
yield {
|
|
504
|
+
data: b64,
|
|
505
|
+
format: "png",
|
|
506
|
+
imageId: itemId,
|
|
507
|
+
isPartial: true,
|
|
508
|
+
type: "image"
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var processFunctionCallArgumentsDelta = (parsed, pendingCalls) => {
|
|
513
|
+
const itemId = typeof parsed.item_id === "string" ? parsed.item_id : "";
|
|
514
|
+
const callId = typeof parsed.call_id === "string" ? parsed.call_id : "";
|
|
515
|
+
const delta = typeof parsed.arguments_delta === "string" ? parsed.arguments_delta : "";
|
|
516
|
+
const existing = pendingCalls.get(itemId);
|
|
517
|
+
if (existing) {
|
|
518
|
+
existing.arguments += delta;
|
|
519
|
+
} else {
|
|
520
|
+
pendingCalls.set(itemId, {
|
|
521
|
+
arguments: delta,
|
|
522
|
+
callId,
|
|
523
|
+
name: ""
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
var processFunctionCallArgumentsDone = function* (parsed, pendingCalls) {
|
|
528
|
+
const itemId = typeof parsed.item_id === "string" ? parsed.item_id : "";
|
|
529
|
+
const callId = typeof parsed.call_id === "string" ? parsed.call_id : "";
|
|
530
|
+
const fullArgs = typeof parsed.arguments === "string" ? parsed.arguments : "";
|
|
531
|
+
const pending = pendingCalls.get(itemId);
|
|
532
|
+
const name = pending?.name ?? "";
|
|
533
|
+
const args = fullArgs || pending?.arguments || "";
|
|
534
|
+
pendingCalls.delete(itemId);
|
|
535
|
+
yield {
|
|
536
|
+
id: callId || pending?.callId || itemId,
|
|
537
|
+
input: parseToolInput2(args),
|
|
538
|
+
name,
|
|
539
|
+
type: "tool_use"
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
var processOutputItemAdded = (parsed, pendingCalls) => {
|
|
543
|
+
if (!isRecord2(parsed.item)) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const { item } = parsed;
|
|
547
|
+
const itemId = typeof item.id === "string" ? item.id : "";
|
|
548
|
+
const itemType = typeof item.type === "string" ? item.type : "";
|
|
549
|
+
if (itemType !== "function_call") {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
553
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
554
|
+
pendingCalls.set(itemId, {
|
|
555
|
+
arguments: "",
|
|
556
|
+
callId,
|
|
557
|
+
name
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
var isCompletedImageGeneration = (item) => item.type === "image_generation_call" && item.status === "completed" && typeof item.result === "string" && item.result !== "";
|
|
561
|
+
var buildImageChunk = (item) => ({
|
|
562
|
+
data: typeof item.result === "string" ? item.result : "",
|
|
563
|
+
format: extractMimeFormat(item.output_format),
|
|
564
|
+
imageId: typeof item.id === "string" ? item.id : undefined,
|
|
565
|
+
isPartial: false,
|
|
566
|
+
revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : undefined,
|
|
567
|
+
type: "image"
|
|
568
|
+
});
|
|
569
|
+
var extractImageFromOutput = function* (output) {
|
|
570
|
+
const completedImages = output.filter(isCompletedImageGeneration);
|
|
571
|
+
for (const item of completedImages) {
|
|
572
|
+
yield buildImageChunk(item);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var processCompleted = function* (parsed) {
|
|
576
|
+
if (!isRecord2(parsed.response)) {
|
|
577
|
+
yield { type: "done", usage: undefined };
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const { response } = parsed;
|
|
581
|
+
const usage = extractUsage2(response);
|
|
582
|
+
if (isRecordArray2(response.output)) {
|
|
583
|
+
yield* extractImageFromOutput(response.output);
|
|
584
|
+
}
|
|
585
|
+
yield { type: "done", usage };
|
|
586
|
+
};
|
|
587
|
+
var processSSEEvent = function* (eventType, parsed, pendingCalls) {
|
|
588
|
+
switch (eventType) {
|
|
589
|
+
case "response.reasoning_summary_text.delta": {
|
|
590
|
+
const delta = typeof parsed.delta === "string" ? parsed.delta : "";
|
|
591
|
+
if (!delta)
|
|
592
|
+
break;
|
|
593
|
+
yield {
|
|
594
|
+
content: delta,
|
|
595
|
+
type: "thinking"
|
|
596
|
+
};
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case "response.output_text.delta":
|
|
600
|
+
yield* processTextDelta(parsed);
|
|
601
|
+
break;
|
|
602
|
+
case "response.image_generation_call.partial_image":
|
|
603
|
+
yield* processPartialImage(parsed);
|
|
604
|
+
break;
|
|
605
|
+
case "response.output_item.added":
|
|
606
|
+
processOutputItemAdded(parsed, pendingCalls);
|
|
607
|
+
break;
|
|
608
|
+
case "response.function_call_arguments.delta":
|
|
609
|
+
processFunctionCallArgumentsDelta(parsed, pendingCalls);
|
|
610
|
+
break;
|
|
611
|
+
case "response.function_call_arguments.done":
|
|
612
|
+
yield* processFunctionCallArgumentsDone(parsed, pendingCalls);
|
|
613
|
+
break;
|
|
614
|
+
case "response.completed":
|
|
615
|
+
yield* processCompleted(parsed);
|
|
616
|
+
break;
|
|
617
|
+
case "response.failed":
|
|
618
|
+
case "response.incomplete": {
|
|
619
|
+
const respObj = isRecord2(parsed.response) ? parsed.response : parsed;
|
|
620
|
+
const errMsg = isRecord2(respObj.error) && typeof respObj.error.message === "string" ? respObj.error.message : `OpenAI Responses API: ${eventType}`;
|
|
621
|
+
throw new Error(errMsg);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
var flushSSEBuffer = function* (state) {
|
|
626
|
+
if (!state.currentEvent || !state.buffer) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const parsed = parseJSON(state.buffer);
|
|
630
|
+
if (parsed) {
|
|
631
|
+
yield* processSSEEvent(state.currentEvent, parsed, state.pendingCalls);
|
|
632
|
+
}
|
|
633
|
+
state.currentEvent = "";
|
|
634
|
+
state.buffer = "";
|
|
635
|
+
};
|
|
636
|
+
var parseSSELine = (trimmed, state) => {
|
|
637
|
+
if (trimmed.startsWith("event: ")) {
|
|
638
|
+
state.currentEvent = trimmed.slice(EVENT_PREFIX_LENGTH);
|
|
639
|
+
} else if (trimmed.startsWith("data: ")) {
|
|
640
|
+
state.buffer = trimmed.slice(DATA_PREFIX_LENGTH);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
var processSSELine2 = function* (line, state) {
|
|
644
|
+
const trimmed = line.trim();
|
|
645
|
+
if (trimmed) {
|
|
646
|
+
parseSSELine(trimmed, state);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
yield* flushSSEBuffer(state);
|
|
650
|
+
};
|
|
651
|
+
var processSSELines2 = function* (lines, state) {
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
yield* processSSELine2(line, state);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
var drainReader2 = async function* (reader, decoder, state, signal) {
|
|
657
|
+
let textBuffer = "";
|
|
658
|
+
for (let result = await reader.read();!result.done && !signal?.aborted; result = await reader.read()) {
|
|
659
|
+
textBuffer += decoder.decode(result.value, { stream: true });
|
|
660
|
+
const lines = textBuffer.split(`
|
|
661
|
+
`);
|
|
662
|
+
textBuffer = lines.pop() ?? "";
|
|
663
|
+
yield* processSSELines2(lines, state);
|
|
664
|
+
}
|
|
665
|
+
if (textBuffer.trim()) {
|
|
666
|
+
yield* processSSELines2([textBuffer, ""], state);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var parseSSEStream2 = async function* (body, signal) {
|
|
670
|
+
const reader = body.getReader();
|
|
671
|
+
const decoder = new TextDecoder;
|
|
672
|
+
const state = {
|
|
673
|
+
buffer: "",
|
|
674
|
+
currentEvent: "",
|
|
675
|
+
pendingCalls: new Map,
|
|
676
|
+
usage: undefined
|
|
677
|
+
};
|
|
678
|
+
try {
|
|
679
|
+
yield* drainReader2(reader, decoder, state, signal);
|
|
680
|
+
} finally {
|
|
681
|
+
reader.releaseLock();
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
var fetchResponsesStream = async function* (baseUrl, apiKey, body, signal) {
|
|
685
|
+
const response = await fetch(`${baseUrl}/v1/responses`, {
|
|
686
|
+
body: JSON.stringify(body),
|
|
687
|
+
headers: {
|
|
688
|
+
Authorization: `Bearer ${apiKey}`,
|
|
689
|
+
"Content-Type": "application/json"
|
|
690
|
+
},
|
|
691
|
+
method: "POST",
|
|
692
|
+
signal
|
|
693
|
+
});
|
|
694
|
+
if (!response.ok) {
|
|
695
|
+
const errorText = await response.text();
|
|
696
|
+
throw new Error(`OpenAI Responses API error ${response.status}: ${errorText}`);
|
|
697
|
+
}
|
|
698
|
+
if (!response.body) {
|
|
699
|
+
throw new Error("OpenAI Responses API returned no response body");
|
|
700
|
+
}
|
|
701
|
+
yield* parseSSEStream2(response.body, signal);
|
|
702
|
+
};
|
|
703
|
+
var resolveImageModels = (imageModels) => {
|
|
704
|
+
if (!imageModels) {
|
|
705
|
+
return new Set;
|
|
706
|
+
}
|
|
707
|
+
if (imageModels instanceof Set) {
|
|
708
|
+
return imageModels;
|
|
709
|
+
}
|
|
710
|
+
return new Set(imageModels);
|
|
711
|
+
};
|
|
712
|
+
var openaiResponses = (config) => {
|
|
713
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL2;
|
|
714
|
+
const imageModels = resolveImageModels(config.imageModels);
|
|
715
|
+
return {
|
|
716
|
+
stream: (params) => {
|
|
717
|
+
const isImageModel = imageModels.has(params.model);
|
|
718
|
+
const body = buildRequestBody2(params, isImageModel);
|
|
719
|
+
return fetchResponsesStream(baseUrl, config.apiKey, body, params.signal);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/ai/providers/gemini.ts
|
|
725
|
+
var DEFAULT_BASE_URL3 = "https://generativelanguage.googleapis.com";
|
|
726
|
+
var SSE_DATA_PREFIX_LENGTH2 = 6;
|
|
727
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null;
|
|
728
|
+
var isRecordArray3 = (value) => Array.isArray(value) && value.length > 0 && isRecord3(value[0]);
|
|
729
|
+
var mapRole = (role) => {
|
|
730
|
+
if (role === "assistant" || role === "system")
|
|
731
|
+
return "model";
|
|
732
|
+
return "user";
|
|
733
|
+
};
|
|
734
|
+
var mapContentBlock = (block) => {
|
|
735
|
+
switch (block.type) {
|
|
736
|
+
case "text":
|
|
737
|
+
return { text: block.content };
|
|
738
|
+
case "image":
|
|
739
|
+
return {
|
|
740
|
+
inlineData: {
|
|
741
|
+
data: block.source.data,
|
|
742
|
+
mimeType: block.source.media_type
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
case "document":
|
|
746
|
+
return {
|
|
747
|
+
inlineData: {
|
|
748
|
+
data: block.source.data,
|
|
749
|
+
mimeType: block.source.media_type
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
case "tool_use":
|
|
753
|
+
return {
|
|
754
|
+
functionCall: {
|
|
755
|
+
args: typeof block.input === "string" ? JSON.parse(block.input) : block.input,
|
|
756
|
+
name: block.name
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
case "tool_result":
|
|
760
|
+
return {
|
|
761
|
+
functionResponse: {
|
|
762
|
+
name: block.tool_use_id,
|
|
763
|
+
response: { result: block.content }
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
default:
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var convertMessageContent = (content) => {
|
|
771
|
+
if (typeof content === "string") {
|
|
772
|
+
return [{ text: content }];
|
|
773
|
+
}
|
|
774
|
+
return content.map(mapContentBlock).filter((mapped) => mapped !== null);
|
|
775
|
+
};
|
|
776
|
+
var hasFunctionResponse = (content) => content.some((block) => block.type === "tool_result");
|
|
777
|
+
var convertSingleMessage2 = (msg) => {
|
|
778
|
+
if (typeof msg.content !== "string" && hasFunctionResponse(msg.content)) {
|
|
779
|
+
return { parts: convertMessageContent(msg.content), role: "user" };
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
parts: convertMessageContent(msg.content),
|
|
783
|
+
role: mapRole(msg.role)
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
var convertMessages = (messages) => messages.map(convertSingleMessage2);
|
|
787
|
+
var mapToolDefinitions2 = (tools) => tools.map((tool) => ({
|
|
788
|
+
description: tool.description,
|
|
789
|
+
name: tool.name,
|
|
790
|
+
parameters: tool.input_schema
|
|
791
|
+
}));
|
|
792
|
+
var buildRequestBody3 = (params, isImageModel) => {
|
|
793
|
+
const body = {
|
|
794
|
+
contents: convertMessages(params.messages)
|
|
795
|
+
};
|
|
796
|
+
if (isImageModel) {
|
|
797
|
+
body.generationConfig = {
|
|
798
|
+
responseModalities: ["TEXT", "IMAGE"]
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (params.systemPrompt) {
|
|
802
|
+
body.systemInstruction = {
|
|
803
|
+
parts: [{ text: params.systemPrompt }]
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (params.tools && params.tools.length > 0) {
|
|
807
|
+
body.tools = [
|
|
808
|
+
{ functionDeclarations: mapToolDefinitions2(params.tools) }
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
return body;
|
|
812
|
+
};
|
|
813
|
+
var extractMimeFormat2 = (mimeType) => {
|
|
814
|
+
if (typeof mimeType !== "string") {
|
|
815
|
+
return "png";
|
|
816
|
+
}
|
|
817
|
+
if (mimeType.includes("jpeg") || mimeType.includes("jpg"))
|
|
818
|
+
return "jpeg";
|
|
819
|
+
if (mimeType.includes("webp"))
|
|
820
|
+
return "webp";
|
|
821
|
+
return "png";
|
|
822
|
+
};
|
|
823
|
+
var processTextPart = function* (part) {
|
|
824
|
+
if (typeof part.text === "string" && part.text) {
|
|
825
|
+
yield { content: part.text, type: "text" };
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var processInlineDataPart = function* (inlineData) {
|
|
829
|
+
const data = typeof inlineData.data === "string" ? inlineData.data : "";
|
|
830
|
+
const { mimeType } = inlineData;
|
|
831
|
+
if (!data) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
yield {
|
|
835
|
+
data,
|
|
836
|
+
format: extractMimeFormat2(mimeType),
|
|
837
|
+
isPartial: false,
|
|
838
|
+
type: "image"
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
var processFunctionCallPart = function* (functionCall) {
|
|
842
|
+
const name = typeof functionCall.name === "string" ? functionCall.name : "";
|
|
843
|
+
const args = functionCall.args ?? {};
|
|
844
|
+
yield {
|
|
845
|
+
id: `gemini-${name}-${Date.now()}`,
|
|
846
|
+
input: args,
|
|
847
|
+
name,
|
|
848
|
+
type: "tool_use"
|
|
849
|
+
};
|
|
850
|
+
};
|
|
851
|
+
var processPart = function* (part) {
|
|
852
|
+
if ("text" in part) {
|
|
853
|
+
yield* processTextPart(part);
|
|
854
|
+
}
|
|
855
|
+
if (isRecord3(part.inlineData)) {
|
|
856
|
+
yield* processInlineDataPart(part.inlineData);
|
|
857
|
+
}
|
|
858
|
+
if (isRecord3(part.functionCall)) {
|
|
859
|
+
yield* processFunctionCallPart(part.functionCall);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
var extractUsage3 = (parsed) => {
|
|
863
|
+
if (!isRecord3(parsed.usageMetadata)) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const { usageMetadata } = parsed;
|
|
867
|
+
return {
|
|
868
|
+
inputTokens: typeof usageMetadata.promptTokenCount === "number" ? usageMetadata.promptTokenCount : 0,
|
|
869
|
+
outputTokens: typeof usageMetadata.candidatesTokenCount === "number" ? usageMetadata.candidatesTokenCount : 0
|
|
870
|
+
};
|
|
871
|
+
};
|
|
872
|
+
var processChunk = function* (parsed, state) {
|
|
873
|
+
const usage = extractUsage3(parsed);
|
|
874
|
+
if (usage) {
|
|
875
|
+
state.usage = usage;
|
|
876
|
+
}
|
|
877
|
+
if (!isRecordArray3(parsed.candidates)) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const [candidate] = parsed.candidates;
|
|
881
|
+
if (!candidate || !isRecord3(candidate.content)) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const { content } = candidate;
|
|
885
|
+
if (!isRecordArray3(content.parts)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
for (const part of content.parts) {
|
|
889
|
+
yield* processPart(part);
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
var processSSELine3 = function* (line, state) {
|
|
893
|
+
const trimmed = line.trim();
|
|
894
|
+
if (!trimmed || !trimmed.startsWith("data: ")) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const data = trimmed.slice(SSE_DATA_PREFIX_LENGTH2);
|
|
898
|
+
let parsed;
|
|
899
|
+
try {
|
|
900
|
+
parsed = JSON.parse(data);
|
|
901
|
+
} catch {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
yield* processChunk(parsed, state);
|
|
905
|
+
};
|
|
906
|
+
var processSSEBuffer = function* (lines, state) {
|
|
907
|
+
for (const line of lines) {
|
|
908
|
+
yield* processSSELine3(line, state);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
var drainReader3 = async function* (reader, decoder, state, signal) {
|
|
912
|
+
let textBuffer = "";
|
|
913
|
+
for (let result = await reader.read();!result.done && !signal?.aborted; result = await reader.read()) {
|
|
914
|
+
textBuffer += decoder.decode(result.value, { stream: true });
|
|
915
|
+
const lines = textBuffer.split(`
|
|
916
|
+
`);
|
|
917
|
+
textBuffer = lines.pop() ?? "";
|
|
918
|
+
yield* processSSEBuffer(lines, state);
|
|
919
|
+
}
|
|
920
|
+
if (textBuffer.trim()) {
|
|
921
|
+
yield* processSSELine3(textBuffer, state);
|
|
922
|
+
}
|
|
923
|
+
yield { type: "done", usage: state.usage };
|
|
924
|
+
};
|
|
925
|
+
var parseSSEStream3 = async function* (body, signal) {
|
|
926
|
+
const reader = body.getReader();
|
|
927
|
+
const decoder = new TextDecoder;
|
|
928
|
+
const state = {
|
|
929
|
+
buffer: "",
|
|
930
|
+
usage: undefined
|
|
931
|
+
};
|
|
932
|
+
try {
|
|
933
|
+
yield* drainReader3(reader, decoder, state, signal);
|
|
934
|
+
} finally {
|
|
935
|
+
reader.releaseLock();
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
var fetchGeminiStream = async function* (baseUrl, apiKey, model, body, signal) {
|
|
939
|
+
const url = `${baseUrl}/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`;
|
|
940
|
+
const response = await fetch(url, {
|
|
941
|
+
body: JSON.stringify(body),
|
|
942
|
+
headers: {
|
|
943
|
+
"Content-Type": "application/json"
|
|
944
|
+
},
|
|
945
|
+
method: "POST",
|
|
946
|
+
signal
|
|
947
|
+
});
|
|
948
|
+
if (!response.ok) {
|
|
949
|
+
const errorText = await response.text();
|
|
950
|
+
throw new Error(`Gemini API error ${response.status}: ${errorText}`);
|
|
951
|
+
}
|
|
952
|
+
if (!response.body) {
|
|
953
|
+
throw new Error("Gemini API returned no response body");
|
|
954
|
+
}
|
|
955
|
+
yield* parseSSEStream3(response.body, signal);
|
|
956
|
+
};
|
|
957
|
+
var resolveImageModels2 = (raw) => {
|
|
958
|
+
if (!raw) {
|
|
959
|
+
return new Set;
|
|
960
|
+
}
|
|
961
|
+
if (raw instanceof Set) {
|
|
962
|
+
return raw;
|
|
963
|
+
}
|
|
964
|
+
return new Set(raw);
|
|
965
|
+
};
|
|
966
|
+
var gemini = (config) => {
|
|
967
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL3;
|
|
968
|
+
const imageModels = resolveImageModels2(config.imageModels);
|
|
969
|
+
return {
|
|
970
|
+
stream: (params) => {
|
|
971
|
+
const isImageModel = imageModels.has(params.model);
|
|
972
|
+
const body = buildRequestBody3(params, isImageModel);
|
|
973
|
+
return fetchGeminiStream(baseUrl, config.apiKey, params.model, body, params.signal);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/ai/providers/anthropic.ts
|
|
979
|
+
var DEFAULT_BASE_URL4 = "https://api.anthropic.com";
|
|
980
|
+
var API_VERSION = "2023-06-01";
|
|
981
|
+
var MAX_TOKENS = 8192;
|
|
982
|
+
var EVENT_PREFIX_LENGTH2 = 7;
|
|
983
|
+
var DATA_PREFIX_LENGTH2 = 6;
|
|
984
|
+
var EMPTY_CHUNKS = [];
|
|
985
|
+
var isRecord4 = (val) => typeof val === "object" && val !== null;
|
|
986
|
+
var mapContentBlock2 = (block) => {
|
|
987
|
+
if (block.type === "thinking") {
|
|
988
|
+
return {
|
|
989
|
+
signature: block.signature,
|
|
990
|
+
thinking: block.thinking,
|
|
991
|
+
type: "thinking"
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (block.type === "image") {
|
|
995
|
+
return {
|
|
996
|
+
source: block.source,
|
|
997
|
+
type: "image"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
if (block.type === "document") {
|
|
1001
|
+
return {
|
|
1002
|
+
source: block.source,
|
|
1003
|
+
type: "document"
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
if (block.type === "tool_result") {
|
|
1007
|
+
return {
|
|
1008
|
+
content: block.content,
|
|
1009
|
+
tool_use_id: block.tool_use_id,
|
|
1010
|
+
type: "tool_result"
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (block.type === "tool_use") {
|
|
1014
|
+
return {
|
|
1015
|
+
id: block.id,
|
|
1016
|
+
input: block.input,
|
|
1017
|
+
name: block.name,
|
|
1018
|
+
type: "tool_use"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return { text: block.content, type: "text" };
|
|
1022
|
+
};
|
|
1023
|
+
var mapMessage = (msg) => ({
|
|
1024
|
+
content: typeof msg.content === "string" ? msg.content : msg.content.map(mapContentBlock2),
|
|
1025
|
+
role: msg.role === "system" ? "user" : msg.role
|
|
1026
|
+
});
|
|
1027
|
+
var mapToolDefinition2 = (tool) => ({
|
|
1028
|
+
description: tool.description,
|
|
1029
|
+
input_schema: tool.input_schema,
|
|
1030
|
+
name: tool.name
|
|
1031
|
+
});
|
|
1032
|
+
var buildRequestBody4 = (params) => {
|
|
1033
|
+
const messages = params.messages.filter((msg) => msg.role !== "system").map(mapMessage);
|
|
1034
|
+
const body = {
|
|
1035
|
+
max_tokens: MAX_TOKENS,
|
|
1036
|
+
messages,
|
|
1037
|
+
model: params.model,
|
|
1038
|
+
stream: true
|
|
1039
|
+
};
|
|
1040
|
+
if (params.systemPrompt) {
|
|
1041
|
+
body.system = params.systemPrompt;
|
|
1042
|
+
}
|
|
1043
|
+
if (params.tools && params.tools.length > 0) {
|
|
1044
|
+
body.tools = params.tools.map(mapToolDefinition2);
|
|
1045
|
+
}
|
|
1046
|
+
if (params.thinking) {
|
|
1047
|
+
body.thinking = params.thinking;
|
|
1048
|
+
body.max_tokens = Math.max(MAX_TOKENS, params.thinking.budget_tokens + MAX_TOKENS);
|
|
1049
|
+
}
|
|
1050
|
+
return body;
|
|
1051
|
+
};
|
|
1052
|
+
var classifyLine = (line) => {
|
|
1053
|
+
if (line.startsWith("event: ")) {
|
|
1054
|
+
return {
|
|
1055
|
+
field: "event",
|
|
1056
|
+
value: line.slice(EVENT_PREFIX_LENGTH2)
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
if (line.startsWith("data: ")) {
|
|
1060
|
+
return {
|
|
1061
|
+
field: "data",
|
|
1062
|
+
value: line.slice(DATA_PREFIX_LENGTH2)
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return;
|
|
1066
|
+
};
|
|
1067
|
+
var applyClassified = (acc, classified) => {
|
|
1068
|
+
if (!classified) {
|
|
1069
|
+
return acc;
|
|
1070
|
+
}
|
|
1071
|
+
if (classified.field === "event") {
|
|
1072
|
+
return { eventData: acc.eventData, eventType: classified.value };
|
|
1073
|
+
}
|
|
1074
|
+
return { eventData: classified.value, eventType: acc.eventType };
|
|
1075
|
+
};
|
|
1076
|
+
var parseEventLines = (event) => event.split(`
|
|
1077
|
+
`).reduce((acc, line) => applyClassified(acc, classifyLine(line)), {
|
|
1078
|
+
eventData: "",
|
|
1079
|
+
eventType: ""
|
|
1080
|
+
});
|
|
1081
|
+
var safeParse = (text) => {
|
|
1082
|
+
try {
|
|
1083
|
+
const result = JSON.parse(text);
|
|
1084
|
+
return result;
|
|
1085
|
+
} catch {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
var tryParseJson = (text) => {
|
|
1090
|
+
const result = safeParse(text);
|
|
1091
|
+
if (isRecord4(result)) {
|
|
1092
|
+
return result;
|
|
1093
|
+
}
|
|
1094
|
+
return;
|
|
1095
|
+
};
|
|
1096
|
+
var getRecord = (obj, key) => {
|
|
1097
|
+
const val = obj[key];
|
|
1098
|
+
if (isRecord4(val)) {
|
|
1099
|
+
return val;
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
};
|
|
1103
|
+
var getString = (obj, key) => {
|
|
1104
|
+
const val = obj[key];
|
|
1105
|
+
if (typeof val === "string") {
|
|
1106
|
+
return val;
|
|
1107
|
+
}
|
|
1108
|
+
return "";
|
|
1109
|
+
};
|
|
1110
|
+
var getNumber = (obj, key) => {
|
|
1111
|
+
const val = obj[key];
|
|
1112
|
+
if (typeof val === "number") {
|
|
1113
|
+
return val;
|
|
1114
|
+
}
|
|
1115
|
+
return 0;
|
|
1116
|
+
};
|
|
1117
|
+
var handleContentBlockStart = (parsed, state) => {
|
|
1118
|
+
const block = getRecord(parsed, "content_block");
|
|
1119
|
+
if (block && block.type === "tool_use") {
|
|
1120
|
+
state.currentToolId = getString(block, "id");
|
|
1121
|
+
state.currentToolName = getString(block, "name");
|
|
1122
|
+
state.toolInputJson = "";
|
|
1123
|
+
state.isThinkingBlock = false;
|
|
1124
|
+
} else if (block && block.type === "thinking") {
|
|
1125
|
+
state.isThinkingBlock = true;
|
|
1126
|
+
state.thinkingSignature = "";
|
|
1127
|
+
} else {
|
|
1128
|
+
state.isThinkingBlock = false;
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
var handleContentBlockDelta = (parsed, state) => {
|
|
1132
|
+
const delta = getRecord(parsed, "delta");
|
|
1133
|
+
if (!delta) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (delta.type === "thinking_delta") {
|
|
1137
|
+
return {
|
|
1138
|
+
content: getString(delta, "thinking"),
|
|
1139
|
+
type: "thinking"
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
if (delta.type === "text_delta") {
|
|
1143
|
+
return {
|
|
1144
|
+
content: getString(delta, "text"),
|
|
1145
|
+
type: "text"
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (delta.type === "input_json_delta") {
|
|
1149
|
+
state.toolInputJson += getString(delta, "partial_json");
|
|
1150
|
+
}
|
|
1151
|
+
if (delta.type === "signature_delta") {
|
|
1152
|
+
state.thinkingSignature += getString(delta, "signature");
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
};
|
|
1156
|
+
var handleContentBlockStop = (state) => {
|
|
1157
|
+
if (state.isThinkingBlock && state.thinkingSignature) {
|
|
1158
|
+
state.isThinkingBlock = false;
|
|
1159
|
+
const signature = state.thinkingSignature;
|
|
1160
|
+
state.thinkingSignature = "";
|
|
1161
|
+
return {
|
|
1162
|
+
content: "",
|
|
1163
|
+
signature,
|
|
1164
|
+
type: "thinking"
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (!state.currentToolId) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const input = tryParseJson(state.toolInputJson) ?? state.toolInputJson;
|
|
1171
|
+
const chunk = {
|
|
1172
|
+
id: state.currentToolId,
|
|
1173
|
+
input,
|
|
1174
|
+
name: state.currentToolName,
|
|
1175
|
+
type: "tool_use"
|
|
1176
|
+
};
|
|
1177
|
+
state.currentToolId = "";
|
|
1178
|
+
state.currentToolName = "";
|
|
1179
|
+
state.toolInputJson = "";
|
|
1180
|
+
return chunk;
|
|
1181
|
+
};
|
|
1182
|
+
var extractUsage4 = (usageRecord, existingUsage) => {
|
|
1183
|
+
if (!usageRecord) {
|
|
1184
|
+
return existingUsage;
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
inputTokens: getNumber(usageRecord, "input_tokens") || existingUsage?.inputTokens || 0,
|
|
1188
|
+
outputTokens: getNumber(usageRecord, "output_tokens") || existingUsage?.outputTokens || 0
|
|
1189
|
+
};
|
|
1190
|
+
};
|
|
1191
|
+
var handleMessageDelta = (parsed, state) => {
|
|
1192
|
+
const deltaUsage = getRecord(parsed, "usage");
|
|
1193
|
+
state.usage = extractUsage4(deltaUsage, state.usage);
|
|
1194
|
+
};
|
|
1195
|
+
var handleMessageStart = (parsed, state) => {
|
|
1196
|
+
const message = getRecord(parsed, "message");
|
|
1197
|
+
if (!message) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const startUsage = getRecord(message, "usage");
|
|
1201
|
+
state.usage = extractUsage4(startUsage, state.usage);
|
|
1202
|
+
};
|
|
1203
|
+
var handleError = (parsed) => {
|
|
1204
|
+
const error = getRecord(parsed, "error");
|
|
1205
|
+
const errorMessage = error ? getString(error, "message") : "";
|
|
1206
|
+
throw new Error(errorMessage || "Anthropic API error");
|
|
1207
|
+
};
|
|
1208
|
+
var processEvent = (eventType, parsed, state) => {
|
|
1209
|
+
switch (eventType) {
|
|
1210
|
+
case "content_block_start": {
|
|
1211
|
+
handleContentBlockStart(parsed, state);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
case "content_block_delta": {
|
|
1215
|
+
return handleContentBlockDelta(parsed, state);
|
|
1216
|
+
}
|
|
1217
|
+
case "content_block_stop": {
|
|
1218
|
+
return handleContentBlockStop(state);
|
|
1219
|
+
}
|
|
1220
|
+
case "message_delta": {
|
|
1221
|
+
handleMessageDelta(parsed, state);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
case "message_start": {
|
|
1225
|
+
handleMessageStart(parsed, state);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
case "message_stop": {
|
|
1229
|
+
return { type: "done", usage: state.usage };
|
|
1230
|
+
}
|
|
1231
|
+
case "error": {
|
|
1232
|
+
handleError(parsed);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
default: {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
var processSingleEvent = (event, state) => {
|
|
1241
|
+
if (!event.trim()) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const { eventData, eventType } = parseEventLines(event);
|
|
1245
|
+
if (!eventData) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const parsed = tryParseJson(eventData);
|
|
1249
|
+
if (!parsed) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
return processEvent(eventType, parsed, state);
|
|
1253
|
+
};
|
|
1254
|
+
var collectChunk = (event, state) => {
|
|
1255
|
+
const chunk = processSingleEvent(event, state);
|
|
1256
|
+
return chunk ? [chunk] : [];
|
|
1257
|
+
};
|
|
1258
|
+
var processBufferedEvents = (eventsText, state) => {
|
|
1259
|
+
const events = eventsText.split(`
|
|
1260
|
+
|
|
1261
|
+
`);
|
|
1262
|
+
state.buffer = events.pop() ?? "";
|
|
1263
|
+
return events.flatMap((event) => collectChunk(event, state));
|
|
1264
|
+
};
|
|
1265
|
+
var readNextChunks = async (reader, decoder, state, signal) => {
|
|
1266
|
+
if (signal?.aborted) {
|
|
1267
|
+
return { chunks: EMPTY_CHUNKS, done: true };
|
|
1268
|
+
}
|
|
1269
|
+
const { done, value } = await reader.read();
|
|
1270
|
+
if (done) {
|
|
1271
|
+
return { chunks: EMPTY_CHUNKS, done: true };
|
|
1272
|
+
}
|
|
1273
|
+
const rawText = state.buffer + decoder.decode(value, { stream: true });
|
|
1274
|
+
const chunks = processBufferedEvents(rawText, state);
|
|
1275
|
+
return { chunks, done: false };
|
|
1276
|
+
};
|
|
1277
|
+
var findDoneChunk = (chunks) => chunks.findIndex((c) => c.type === "done");
|
|
1278
|
+
var sseStreamLoop = async (reader, decoder, state, signal) => {
|
|
1279
|
+
const result = await readNextChunks(reader, decoder, state, signal);
|
|
1280
|
+
if (result.done) {
|
|
1281
|
+
return { chunks: result.chunks, finished: true };
|
|
1282
|
+
}
|
|
1283
|
+
const doneIdx = findDoneChunk(result.chunks);
|
|
1284
|
+
if (doneIdx >= 0) {
|
|
1285
|
+
return { chunks: result.chunks.slice(0, doneIdx + 1), finished: true };
|
|
1286
|
+
}
|
|
1287
|
+
return { chunks: result.chunks, finished: false };
|
|
1288
|
+
};
|
|
1289
|
+
async function* streamChunks(reader, decoder, state, signal) {
|
|
1290
|
+
let finished = false;
|
|
1291
|
+
while (!finished) {
|
|
1292
|
+
const result = await sseStreamLoop(reader, decoder, state, signal);
|
|
1293
|
+
({ finished } = result);
|
|
1294
|
+
yield* result.chunks;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async function* parseSSEStream4(body, signal) {
|
|
1298
|
+
const reader = body.getReader();
|
|
1299
|
+
const decoder = new TextDecoder;
|
|
1300
|
+
const state = {
|
|
1301
|
+
buffer: "",
|
|
1302
|
+
currentToolId: "",
|
|
1303
|
+
currentToolName: "",
|
|
1304
|
+
isThinkingBlock: false,
|
|
1305
|
+
thinkingSignature: "",
|
|
1306
|
+
toolInputJson: "",
|
|
1307
|
+
usage: undefined
|
|
1308
|
+
};
|
|
1309
|
+
try {
|
|
1310
|
+
yield* streamChunks(reader, decoder, state, signal);
|
|
1311
|
+
} finally {
|
|
1312
|
+
reader.releaseLock();
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
var fetchAndStream = async function* (baseUrl, config, params) {
|
|
1316
|
+
const body = buildRequestBody4(params);
|
|
1317
|
+
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
1318
|
+
body: JSON.stringify(body),
|
|
1319
|
+
headers: {
|
|
1320
|
+
"anthropic-version": API_VERSION,
|
|
1321
|
+
"Content-Type": "application/json",
|
|
1322
|
+
"x-api-key": config.apiKey
|
|
1323
|
+
},
|
|
1324
|
+
method: "POST",
|
|
1325
|
+
signal: params.signal
|
|
1326
|
+
});
|
|
1327
|
+
if (!response.ok) {
|
|
1328
|
+
const errorText = await response.text();
|
|
1329
|
+
throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
|
|
1330
|
+
}
|
|
1331
|
+
if (!response.body) {
|
|
1332
|
+
throw new Error("Anthropic API returned no response body");
|
|
1333
|
+
}
|
|
1334
|
+
yield* parseSSEStream4(response.body, params.signal);
|
|
1335
|
+
};
|
|
1336
|
+
var anthropic = (config) => {
|
|
1337
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL4;
|
|
1338
|
+
return {
|
|
1339
|
+
stream: (params) => fetchAndStream(baseUrl, config, params)
|
|
1340
|
+
};
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/ai/providers/ollama.ts
|
|
1344
|
+
var DEFAULT_BASE_URL5 = "http://localhost:11434";
|
|
1345
|
+
var ZERO_TOKENS = 0;
|
|
1346
|
+
var DONE_CHUNK = { type: "done" };
|
|
1347
|
+
var mapToolDefinitions3 = (tools) => tools.map((tool) => ({
|
|
1348
|
+
function: {
|
|
1349
|
+
description: tool.description,
|
|
1350
|
+
name: tool.name,
|
|
1351
|
+
parameters: tool.input_schema
|
|
1352
|
+
},
|
|
1353
|
+
type: "function"
|
|
1354
|
+
}));
|
|
1355
|
+
var convertMessage2 = (msg) => {
|
|
1356
|
+
if (typeof msg.content === "string") {
|
|
1357
|
+
return [{ content: msg.content, role: msg.role }];
|
|
1358
|
+
}
|
|
1359
|
+
const results = [];
|
|
1360
|
+
const toolUseBlocks = msg.content.filter((block) => block.type === "tool_use");
|
|
1361
|
+
const toolResultBlocks = msg.content.filter((block) => block.type === "tool_result");
|
|
1362
|
+
if (toolUseBlocks.length > 0) {
|
|
1363
|
+
results.push({
|
|
1364
|
+
content: "",
|
|
1365
|
+
role: "assistant",
|
|
1366
|
+
tool_calls: toolUseBlocks.map((block) => ({
|
|
1367
|
+
function: {
|
|
1368
|
+
arguments: typeof block.input === "object" && block.input !== null ? { ...block.input } : {},
|
|
1369
|
+
name: block.name
|
|
1370
|
+
}
|
|
1371
|
+
}))
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
for (const block of toolResultBlocks) {
|
|
1375
|
+
results.push({
|
|
1376
|
+
content: typeof block.content === "string" ? block.content : "",
|
|
1377
|
+
role: "tool"
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return results;
|
|
1381
|
+
};
|
|
1382
|
+
var buildRequestBody5 = (params) => {
|
|
1383
|
+
const messages = params.messages.flatMap(convertMessage2);
|
|
1384
|
+
if (params.systemPrompt) {
|
|
1385
|
+
messages.unshift({ content: params.systemPrompt, role: "system" });
|
|
1386
|
+
}
|
|
1387
|
+
const body = {
|
|
1388
|
+
messages,
|
|
1389
|
+
model: params.model,
|
|
1390
|
+
stream: true
|
|
1391
|
+
};
|
|
1392
|
+
if (params.tools && params.tools.length > 0) {
|
|
1393
|
+
body.tools = mapToolDefinitions3(params.tools);
|
|
1394
|
+
}
|
|
1395
|
+
return body;
|
|
1396
|
+
};
|
|
1397
|
+
var isRecord5 = (val) => val !== null && typeof val === "object" && !Array.isArray(val);
|
|
1398
|
+
var tryParseJSON = (text) => {
|
|
1399
|
+
try {
|
|
1400
|
+
const result = JSON.parse(text);
|
|
1401
|
+
if (isRecord5(result)) {
|
|
1402
|
+
return result;
|
|
1403
|
+
}
|
|
1404
|
+
return null;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
var buildDoneChunk = (parsed) => {
|
|
1410
|
+
const promptEvalCount = typeof parsed.prompt_eval_count === "number" ? parsed.prompt_eval_count : undefined;
|
|
1411
|
+
const evalCount = typeof parsed.eval_count === "number" ? parsed.eval_count : undefined;
|
|
1412
|
+
const totalDuration = typeof parsed.total_duration === "number" ? parsed.total_duration : undefined;
|
|
1413
|
+
const hasTokenCounts = promptEvalCount !== undefined || evalCount !== undefined;
|
|
1414
|
+
if (hasTokenCounts) {
|
|
1415
|
+
return {
|
|
1416
|
+
type: "done",
|
|
1417
|
+
usage: {
|
|
1418
|
+
inputTokens: promptEvalCount ?? ZERO_TOKENS,
|
|
1419
|
+
outputTokens: evalCount ?? ZERO_TOKENS
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
if (totalDuration !== undefined) {
|
|
1424
|
+
return {
|
|
1425
|
+
type: "done",
|
|
1426
|
+
usage: { inputTokens: ZERO_TOKENS, outputTokens: ZERO_TOKENS }
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
return { type: "done" };
|
|
1430
|
+
};
|
|
1431
|
+
var buildTextChunk = (content) => ({
|
|
1432
|
+
content,
|
|
1433
|
+
type: "text"
|
|
1434
|
+
});
|
|
1435
|
+
var extractToolCalls = (message) => {
|
|
1436
|
+
const toolCalls = message.tool_calls;
|
|
1437
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
return toolCalls.filter(isRecord5).filter((call) => isRecord5(call.function)).map((call) => {
|
|
1441
|
+
const func = call.function;
|
|
1442
|
+
if (!isRecord5(func)) {
|
|
1443
|
+
return {
|
|
1444
|
+
id: crypto.randomUUID(),
|
|
1445
|
+
input: {},
|
|
1446
|
+
name: "",
|
|
1447
|
+
type: "tool_use"
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
id: crypto.randomUUID(),
|
|
1452
|
+
input: func.arguments ?? {},
|
|
1453
|
+
name: typeof func.name === "string" ? func.name : "",
|
|
1454
|
+
type: "tool_use"
|
|
1455
|
+
};
|
|
1456
|
+
});
|
|
1457
|
+
};
|
|
1458
|
+
var extractChunks = (parsed) => {
|
|
1459
|
+
const { message } = parsed;
|
|
1460
|
+
if (!isRecord5(message)) {
|
|
1461
|
+
return [];
|
|
1462
|
+
}
|
|
1463
|
+
const toolChunks = extractToolCalls(message);
|
|
1464
|
+
if (toolChunks.length > 0) {
|
|
1465
|
+
return toolChunks;
|
|
1466
|
+
}
|
|
1467
|
+
const { content } = message;
|
|
1468
|
+
if (typeof content !== "string" || !content) {
|
|
1469
|
+
return [];
|
|
1470
|
+
}
|
|
1471
|
+
return [buildTextChunk(content)];
|
|
1472
|
+
};
|
|
1473
|
+
var processLine = (line) => {
|
|
1474
|
+
const trimmed = line.trim();
|
|
1475
|
+
if (!trimmed) {
|
|
1476
|
+
return [];
|
|
1477
|
+
}
|
|
1478
|
+
const parsed = tryParseJSON(trimmed);
|
|
1479
|
+
if (!parsed) {
|
|
1480
|
+
return [];
|
|
1481
|
+
}
|
|
1482
|
+
if (parsed.done === true) {
|
|
1483
|
+
return [buildDoneChunk(parsed)];
|
|
1484
|
+
}
|
|
1485
|
+
return extractChunks(parsed);
|
|
1486
|
+
};
|
|
1487
|
+
var processBufferedLines = (lines) => lines.flatMap(processLine);
|
|
1488
|
+
var readStreamChunks = async (reader, decoder, buffer, signal) => {
|
|
1489
|
+
const emptyChunks = [];
|
|
1490
|
+
if (signal?.aborted) {
|
|
1491
|
+
return {
|
|
1492
|
+
allChunks: emptyChunks,
|
|
1493
|
+
currentBuffer: buffer,
|
|
1494
|
+
finished: true
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const result = await reader.read();
|
|
1498
|
+
const { done, value } = result;
|
|
1499
|
+
if (done) {
|
|
1500
|
+
return {
|
|
1501
|
+
allChunks: emptyChunks,
|
|
1502
|
+
currentBuffer: buffer,
|
|
1503
|
+
finished: true
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
const currentBuffer = buffer + decoder.decode(value, { stream: true });
|
|
1507
|
+
const lines = currentBuffer.split(`
|
|
1508
|
+
`);
|
|
1509
|
+
const remainder = lines.pop() ?? "";
|
|
1510
|
+
const allChunks = processBufferedLines(lines);
|
|
1511
|
+
const finished = allChunks.some((c) => c.type === "done");
|
|
1512
|
+
return { allChunks, currentBuffer: remainder, finished };
|
|
1513
|
+
};
|
|
1514
|
+
var parseNDJSONStream = async function* (body, signal) {
|
|
1515
|
+
const reader = body.getReader();
|
|
1516
|
+
try {
|
|
1517
|
+
yield* parseNDJSONStreamInner(reader, signal);
|
|
1518
|
+
} finally {
|
|
1519
|
+
reader.releaseLock();
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
var parseNDJSONStreamInner = async function* (reader, signal) {
|
|
1523
|
+
const decoder = new TextDecoder;
|
|
1524
|
+
let buffer = "";
|
|
1525
|
+
let done = false;
|
|
1526
|
+
while (!done) {
|
|
1527
|
+
const result = await readStreamChunks(reader, decoder, buffer, signal);
|
|
1528
|
+
buffer = result.currentBuffer;
|
|
1529
|
+
done = result.finished;
|
|
1530
|
+
yield* result.allChunks;
|
|
1531
|
+
}
|
|
1532
|
+
yield DONE_CHUNK;
|
|
1533
|
+
};
|
|
1534
|
+
var fetchAndStream2 = async function* (baseUrl, params) {
|
|
1535
|
+
const requestBody = buildRequestBody5(params);
|
|
1536
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
1537
|
+
body: JSON.stringify(requestBody),
|
|
1538
|
+
headers: { "Content-Type": "application/json" },
|
|
1539
|
+
method: "POST",
|
|
1540
|
+
signal: params.signal
|
|
1541
|
+
});
|
|
1542
|
+
if (!response.ok) {
|
|
1543
|
+
const errorText = await response.text();
|
|
1544
|
+
throw new Error(`Ollama API error ${response.status}: ${errorText}`);
|
|
1545
|
+
}
|
|
1546
|
+
if (!response.body) {
|
|
1547
|
+
throw new Error("Ollama API returned no response body");
|
|
1548
|
+
}
|
|
1549
|
+
yield* parseNDJSONStream(response.body, params.signal);
|
|
1550
|
+
};
|
|
1551
|
+
var ollama = (config = {}) => {
|
|
1552
|
+
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL5;
|
|
1553
|
+
return {
|
|
1554
|
+
stream: (params) => fetchAndStream2(baseUrl, params)
|
|
1555
|
+
};
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
// src/plugins/aiChat.ts
|
|
1559
|
+
import { Elysia } from "elysia";
|
|
1560
|
+
|
|
1561
|
+
// src/ai/memoryStore.ts
|
|
1562
|
+
var createMemoryStore = () => {
|
|
1563
|
+
const conversations = new Map;
|
|
1564
|
+
const get = async (id) => conversations.get(id);
|
|
1565
|
+
const getOrCreate = async (id) => {
|
|
1566
|
+
let conversation = conversations.get(id);
|
|
1567
|
+
if (!conversation) {
|
|
1568
|
+
conversation = { createdAt: Date.now(), id, messages: [] };
|
|
1569
|
+
conversations.set(id, conversation);
|
|
1570
|
+
}
|
|
1571
|
+
return conversation;
|
|
1572
|
+
};
|
|
1573
|
+
const set = async (id, conversation) => {
|
|
1574
|
+
conversations.set(id, conversation);
|
|
1575
|
+
};
|
|
1576
|
+
const list = async () => Array.from(conversations.values()).map((conv) => ({
|
|
1577
|
+
createdAt: conv.createdAt,
|
|
1578
|
+
id: conv.id,
|
|
1579
|
+
lastMessageAt: conv.lastMessageAt,
|
|
1580
|
+
messageCount: conv.messages.length,
|
|
1581
|
+
title: conv.title ?? "Untitled"
|
|
1582
|
+
})).sort((first, second) => (second.lastMessageAt ?? second.createdAt) - (first.lastMessageAt ?? first.createdAt));
|
|
1583
|
+
const remove = async (id) => {
|
|
1584
|
+
conversations.delete(id);
|
|
1585
|
+
};
|
|
1586
|
+
return { get, getOrCreate, list, remove, set };
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// types/typeGuards.ts
|
|
1590
|
+
var isValidAIClientMessage = (data) => {
|
|
1591
|
+
if (!data || typeof data !== "object") {
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
if (!("type" in data) || typeof data.type !== "string") {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
switch (data.type) {
|
|
1598
|
+
case "message":
|
|
1599
|
+
return "content" in data && typeof data.content === "string";
|
|
1600
|
+
case "cancel":
|
|
1601
|
+
return "conversationId" in data && typeof data.conversationId === "string";
|
|
1602
|
+
case "branch":
|
|
1603
|
+
return "messageId" in data && typeof data.messageId === "string" && "content" in data && typeof data.content === "string" && "conversationId" in data && typeof data.conversationId === "string";
|
|
1604
|
+
default:
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
var isValidAIServerMessage = (data) => {
|
|
1609
|
+
if (!data || typeof data !== "object") {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
if (!("type" in data) || typeof data.type !== "string") {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
switch (data.type) {
|
|
1616
|
+
case "chunk":
|
|
1617
|
+
case "thinking":
|
|
1618
|
+
return "content" in data && typeof data.content === "string" && "messageId" in data && "conversationId" in data;
|
|
1619
|
+
case "tool_status":
|
|
1620
|
+
return "name" in data && "status" in data && "messageId" in data && "conversationId" in data;
|
|
1621
|
+
case "image":
|
|
1622
|
+
return "data" in data && typeof data.data === "string" && "format" in data && typeof data.format === "string" && "isPartial" in data && typeof data.isPartial === "boolean" && "messageId" in data && "conversationId" in data;
|
|
1623
|
+
case "complete":
|
|
1624
|
+
return "messageId" in data && "conversationId" in data;
|
|
1625
|
+
case "rag_retrieved":
|
|
1626
|
+
return "conversationId" in data && "messageId" in data && "sources" in data && Array.isArray(data.sources);
|
|
1627
|
+
case "error":
|
|
1628
|
+
return "message" in data && typeof data.message === "string";
|
|
1629
|
+
default:
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
// src/ai/protocol.ts
|
|
1635
|
+
var generateId = () => crypto.randomUUID();
|
|
1636
|
+
var parseAIMessage = (raw) => {
|
|
1637
|
+
if (raw === null || raw === undefined) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
let text;
|
|
1641
|
+
if (typeof raw === "string") {
|
|
1642
|
+
text = raw;
|
|
1643
|
+
} else if (raw instanceof ArrayBuffer) {
|
|
1644
|
+
text = new TextDecoder().decode(raw);
|
|
1645
|
+
} else if (ArrayBuffer.isView(raw)) {
|
|
1646
|
+
text = new TextDecoder().decode(raw);
|
|
1647
|
+
} else if (typeof raw === "object") {
|
|
1648
|
+
if (isValidAIClientMessage(raw)) {
|
|
1649
|
+
return raw;
|
|
1650
|
+
}
|
|
1651
|
+
return null;
|
|
1652
|
+
} else {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const parsed = JSON.parse(text);
|
|
1657
|
+
if (isValidAIClientMessage(parsed)) {
|
|
1658
|
+
return parsed;
|
|
1659
|
+
}
|
|
1660
|
+
return null;
|
|
1661
|
+
} catch {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
var serializeAIMessage = (message) => JSON.stringify(message);
|
|
1666
|
+
|
|
1667
|
+
// src/ai/streamAI.ts
|
|
1668
|
+
var WS_OPEN = 1;
|
|
1669
|
+
var BACKPRESSURE_THRESHOLD = 1048576;
|
|
1670
|
+
var BACKPRESSURE_DELAY = 10;
|
|
1671
|
+
var DEFAULT_MAX_TURNS = 10;
|
|
1672
|
+
var DEFAULT_THINKING_BUDGET = 1e4;
|
|
1673
|
+
var INITIAL_TURN = 0;
|
|
1674
|
+
var delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1675
|
+
var checkBackpressure = async (socket) => {
|
|
1676
|
+
if (!("raw" in socket)) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const { raw } = socket;
|
|
1680
|
+
if (raw && typeof raw === "object" && "bufferedAmount" in raw && typeof raw.bufferedAmount === "number" && raw.bufferedAmount > BACKPRESSURE_THRESHOLD) {
|
|
1681
|
+
await delay(BACKPRESSURE_DELAY);
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
var sendMessage = async (socket, message) => {
|
|
1685
|
+
if (socket.readyState !== WS_OPEN) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
await checkBackpressure(socket);
|
|
1689
|
+
socket.send(serializeAIMessage(message));
|
|
1690
|
+
return true;
|
|
1691
|
+
};
|
|
1692
|
+
var buildToolDefinitions = (tools) => Object.entries(tools).map(([name, def]) => ({
|
|
1693
|
+
description: def.description,
|
|
1694
|
+
input_schema: def.input,
|
|
1695
|
+
name
|
|
1696
|
+
}));
|
|
1697
|
+
var extractTextContent = (chunk, onChunk) => {
|
|
1698
|
+
if (!onChunk) {
|
|
1699
|
+
return chunk.content;
|
|
1700
|
+
}
|
|
1701
|
+
const transformed = onChunk(chunk);
|
|
1702
|
+
if (transformed && typeof transformed === "object" && "content" in transformed) {
|
|
1703
|
+
return transformed.content;
|
|
1704
|
+
}
|
|
1705
|
+
return chunk.content;
|
|
1706
|
+
};
|
|
1707
|
+
var sendImageMessage = async (socket, chunk, messageId, conversationId) => sendMessage(socket, {
|
|
1708
|
+
conversationId,
|
|
1709
|
+
data: chunk.data,
|
|
1710
|
+
format: chunk.format,
|
|
1711
|
+
imageId: chunk.imageId,
|
|
1712
|
+
isPartial: chunk.isPartial,
|
|
1713
|
+
messageId,
|
|
1714
|
+
revisedPrompt: chunk.revisedPrompt,
|
|
1715
|
+
type: "image"
|
|
1716
|
+
});
|
|
1717
|
+
var sendToolRunning = async (socket, toolName, toolInput, messageId, conversationId) => sendMessage(socket, {
|
|
1718
|
+
conversationId,
|
|
1719
|
+
input: toolInput,
|
|
1720
|
+
messageId,
|
|
1721
|
+
name: toolName,
|
|
1722
|
+
status: "running",
|
|
1723
|
+
type: "tool_status"
|
|
1724
|
+
});
|
|
1725
|
+
var sendToolComplete = async (socket, toolName, result, messageId, conversationId) => sendMessage(socket, {
|
|
1726
|
+
conversationId,
|
|
1727
|
+
messageId,
|
|
1728
|
+
name: toolName,
|
|
1729
|
+
result,
|
|
1730
|
+
status: "complete",
|
|
1731
|
+
type: "tool_status"
|
|
1732
|
+
});
|
|
1733
|
+
var executeTool = async (options, toolName, toolInput) => {
|
|
1734
|
+
const toolDef = options.tools?.[toolName];
|
|
1735
|
+
if (!toolDef) {
|
|
1736
|
+
return `Error: unknown tool "${toolName}"`;
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
return await toolDef.handler(toolInput);
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
var serializeToolCall = (name, input) => `${name}:${JSON.stringify(input)}`;
|
|
1745
|
+
var handleToolChunkText = (chunk, state, options, socket, messageId, conversationId) => {
|
|
1746
|
+
const textContent = extractTextContent(chunk, options.onChunk);
|
|
1747
|
+
state.currentFullResponse += textContent;
|
|
1748
|
+
sendMessage(socket, {
|
|
1749
|
+
content: textContent,
|
|
1750
|
+
conversationId,
|
|
1751
|
+
messageId,
|
|
1752
|
+
type: "chunk"
|
|
1753
|
+
});
|
|
1754
|
+
};
|
|
1755
|
+
var handleToolChunkToolUse = (chunk, state) => {
|
|
1756
|
+
state.pendingToolCalls.push({
|
|
1757
|
+
id: chunk.id,
|
|
1758
|
+
input: chunk.input,
|
|
1759
|
+
name: chunk.name
|
|
1760
|
+
});
|
|
1761
|
+
};
|
|
1762
|
+
var flushThinking = (state) => {
|
|
1763
|
+
if (state.currentThinking) {
|
|
1764
|
+
state.contentBlocks.push({
|
|
1765
|
+
signature: state.currentThinking.signature || undefined,
|
|
1766
|
+
thinking: state.currentThinking.text,
|
|
1767
|
+
type: "thinking"
|
|
1768
|
+
});
|
|
1769
|
+
state.currentThinking = null;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
var processToolChunk = (chunk, state, options, socket, messageId, conversationId) => {
|
|
1773
|
+
let hitAnotherTool = false;
|
|
1774
|
+
switch (chunk.type) {
|
|
1775
|
+
case "thinking":
|
|
1776
|
+
if (chunk.content) {
|
|
1777
|
+
sendMessage(socket, {
|
|
1778
|
+
content: chunk.content,
|
|
1779
|
+
conversationId,
|
|
1780
|
+
messageId,
|
|
1781
|
+
type: "thinking"
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
if (!state.currentThinking) {
|
|
1785
|
+
state.currentThinking = { signature: "", text: "" };
|
|
1786
|
+
}
|
|
1787
|
+
state.currentThinking.text += chunk.content;
|
|
1788
|
+
if (chunk.signature) {
|
|
1789
|
+
state.currentThinking.signature = chunk.signature;
|
|
1790
|
+
}
|
|
1791
|
+
break;
|
|
1792
|
+
case "text":
|
|
1793
|
+
flushThinking(state);
|
|
1794
|
+
handleToolChunkText(chunk, state, options, socket, messageId, conversationId);
|
|
1795
|
+
state.contentBlocks.push({
|
|
1796
|
+
content: chunk.content,
|
|
1797
|
+
type: "text"
|
|
1798
|
+
});
|
|
1799
|
+
break;
|
|
1800
|
+
case "image":
|
|
1801
|
+
sendImageMessage(socket, chunk, messageId, conversationId);
|
|
1802
|
+
options.onImage?.({
|
|
1803
|
+
data: chunk.data,
|
|
1804
|
+
format: chunk.format,
|
|
1805
|
+
imageId: chunk.imageId,
|
|
1806
|
+
isPartial: chunk.isPartial,
|
|
1807
|
+
revisedPrompt: chunk.revisedPrompt
|
|
1808
|
+
});
|
|
1809
|
+
break;
|
|
1810
|
+
case "tool_use":
|
|
1811
|
+
flushThinking(state);
|
|
1812
|
+
handleToolChunkToolUse(chunk, state);
|
|
1813
|
+
state.contentBlocks.push({
|
|
1814
|
+
id: chunk.id,
|
|
1815
|
+
input: typeof chunk.input === "object" && chunk.input !== null ? chunk.input : {},
|
|
1816
|
+
name: chunk.name,
|
|
1817
|
+
type: "tool_use"
|
|
1818
|
+
});
|
|
1819
|
+
hitAnotherTool = true;
|
|
1820
|
+
break;
|
|
1821
|
+
case "done":
|
|
1822
|
+
flushThinking(state);
|
|
1823
|
+
state.currentUsage = chunk.usage;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
return hitAnotherTool;
|
|
1827
|
+
};
|
|
1828
|
+
var processToolTurn = async (socket, options, state, messageId, conversationId, signal) => {
|
|
1829
|
+
const toolCalls = [...state.pendingToolCalls];
|
|
1830
|
+
state.pendingToolCalls = [];
|
|
1831
|
+
state.currentMessages.push({
|
|
1832
|
+
content: state.contentBlocks,
|
|
1833
|
+
role: "assistant"
|
|
1834
|
+
});
|
|
1835
|
+
state.contentBlocks = [];
|
|
1836
|
+
const toolResultBlocks = [];
|
|
1837
|
+
for (const toolCall of toolCalls) {
|
|
1838
|
+
await sendToolRunning(socket, toolCall.name, toolCall.input, messageId, conversationId);
|
|
1839
|
+
const result = await executeTool(options, toolCall.name, toolCall.input);
|
|
1840
|
+
await sendToolComplete(socket, toolCall.name, result, messageId, conversationId);
|
|
1841
|
+
options.onToolUse?.(toolCall.name, toolCall.input, result);
|
|
1842
|
+
toolResultBlocks.push({
|
|
1843
|
+
content: result,
|
|
1844
|
+
tool_use_id: toolCall.id,
|
|
1845
|
+
type: "tool_result"
|
|
1846
|
+
});
|
|
1847
|
+
state.executedToolKeys.add(serializeToolCall(toolCall.name, toolCall.input));
|
|
1848
|
+
}
|
|
1849
|
+
state.currentMessages.push({
|
|
1850
|
+
content: toolResultBlocks,
|
|
1851
|
+
role: "user"
|
|
1852
|
+
});
|
|
1853
|
+
const toolDefs = options.tools ? buildToolDefinitions(options.tools) : undefined;
|
|
1854
|
+
const thinkingConfig = options.thinking ? {
|
|
1855
|
+
budget_tokens: typeof options.thinking === "object" ? options.thinking.budgetTokens : DEFAULT_THINKING_BUDGET,
|
|
1856
|
+
type: "enabled"
|
|
1857
|
+
} : undefined;
|
|
1858
|
+
const stream = options.provider.stream({
|
|
1859
|
+
messages: state.currentMessages,
|
|
1860
|
+
model: options.model,
|
|
1861
|
+
signal,
|
|
1862
|
+
systemPrompt: options.systemPrompt,
|
|
1863
|
+
thinking: thinkingConfig,
|
|
1864
|
+
tools: toolDefs
|
|
1865
|
+
});
|
|
1866
|
+
await consumeToolStream(stream, state, options, socket, messageId, conversationId, signal);
|
|
1867
|
+
};
|
|
1868
|
+
var consumeToolStream = async (stream, state, options, socket, messageId, conversationId, signal) => {
|
|
1869
|
+
let hitAnotherTool = false;
|
|
1870
|
+
for await (const chunk of stream) {
|
|
1871
|
+
if (signal.aborted)
|
|
1872
|
+
break;
|
|
1873
|
+
const isToolHit = processToolChunk(chunk, state, options, socket, messageId, conversationId);
|
|
1874
|
+
if (isToolHit)
|
|
1875
|
+
hitAnotherTool = true;
|
|
1876
|
+
}
|
|
1877
|
+
return hitAnotherTool;
|
|
1878
|
+
};
|
|
1879
|
+
var shouldContinueToolLoop = (state, maxTurns, signal) => state.pendingToolCalls.length > 0 && state.currentTurn < maxTurns && !signal.aborted;
|
|
1880
|
+
var areAllRepeatedToolCalls = (state) => state.pendingToolCalls.every((toolCall) => state.executedToolKeys.has(serializeToolCall(toolCall.name, toolCall.input)));
|
|
1881
|
+
var buildToolLoopResult = (state) => ({
|
|
1882
|
+
fullResponse: state.currentFullResponse,
|
|
1883
|
+
usage: state.currentUsage
|
|
1884
|
+
});
|
|
1885
|
+
var executeToolLoop = async (socket, options, messages, initialToolCalls, initialContentBlocks, messageId, conversationId, signal, fullResponse, turn) => {
|
|
1886
|
+
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
1887
|
+
const state = {
|
|
1888
|
+
contentBlocks: initialContentBlocks,
|
|
1889
|
+
currentFullResponse: fullResponse,
|
|
1890
|
+
currentMessages: [...messages],
|
|
1891
|
+
currentThinking: null,
|
|
1892
|
+
currentTurn: turn,
|
|
1893
|
+
currentUsage: undefined,
|
|
1894
|
+
executedToolKeys: new Set,
|
|
1895
|
+
pendingToolCalls: initialToolCalls
|
|
1896
|
+
};
|
|
1897
|
+
while (shouldContinueToolLoop(state, maxTurns, signal)) {
|
|
1898
|
+
if (areAllRepeatedToolCalls(state))
|
|
1899
|
+
break;
|
|
1900
|
+
await processToolTurn(socket, options, state, messageId, conversationId, signal);
|
|
1901
|
+
state.currentTurn++;
|
|
1902
|
+
}
|
|
1903
|
+
return buildToolLoopResult(state);
|
|
1904
|
+
};
|
|
1905
|
+
var sendComplete = async (socket, messageId, conversationId, usage, durationMs, model, completeMeta) => sendMessage(socket, {
|
|
1906
|
+
conversationId,
|
|
1907
|
+
durationMs,
|
|
1908
|
+
messageId,
|
|
1909
|
+
model,
|
|
1910
|
+
sources: completeMeta?.sources,
|
|
1911
|
+
type: "complete",
|
|
1912
|
+
usage
|
|
1913
|
+
});
|
|
1914
|
+
var sendError = async (socket, err, messageId, conversationId) => sendMessage(socket, {
|
|
1915
|
+
conversationId,
|
|
1916
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1917
|
+
messageId,
|
|
1918
|
+
type: "error"
|
|
1919
|
+
});
|
|
1920
|
+
var handleTextChunk = async (chunk, options, socket, messageId, conversationId) => {
|
|
1921
|
+
const textContent = extractTextContent(chunk, options.onChunk);
|
|
1922
|
+
await sendMessage(socket, {
|
|
1923
|
+
content: textContent,
|
|
1924
|
+
conversationId,
|
|
1925
|
+
messageId,
|
|
1926
|
+
type: "chunk"
|
|
1927
|
+
});
|
|
1928
|
+
return textContent;
|
|
1929
|
+
};
|
|
1930
|
+
var handleToolCalls = async (socket, options, messages, toolCalls, contentBlocks, messageId, conversationId, signal, fullResponse, startTime) => {
|
|
1931
|
+
const toolResult = await executeToolLoop(socket, options, messages, toolCalls, contentBlocks, messageId, conversationId, signal, fullResponse, INITIAL_TURN);
|
|
1932
|
+
await sendComplete(socket, messageId, conversationId, toolResult.usage, Date.now() - startTime, options.model, options.completeMeta);
|
|
1933
|
+
options.onComplete?.(toolResult.fullResponse, toolResult.usage, options.completeMeta);
|
|
1934
|
+
return toolResult;
|
|
1935
|
+
};
|
|
1936
|
+
var processStreamTextChunk = async (chunk, options, socket, messageId, conversationId) => {
|
|
1937
|
+
const textContent = await handleTextChunk(chunk, options, socket, messageId, conversationId);
|
|
1938
|
+
return textContent;
|
|
1939
|
+
};
|
|
1940
|
+
var processStream = async (socket, options, messages, messageId, conversationId, signal, startTime) => {
|
|
1941
|
+
const toolDefs = options.tools ? buildToolDefinitions(options.tools) : undefined;
|
|
1942
|
+
const thinkingConfig = options.thinking ? {
|
|
1943
|
+
budget_tokens: typeof options.thinking === "object" ? options.thinking.budgetTokens : DEFAULT_THINKING_BUDGET,
|
|
1944
|
+
type: "enabled"
|
|
1945
|
+
} : undefined;
|
|
1946
|
+
const stream = options.provider.stream({
|
|
1947
|
+
messages,
|
|
1948
|
+
model: options.model,
|
|
1949
|
+
signal,
|
|
1950
|
+
systemPrompt: options.systemPrompt,
|
|
1951
|
+
thinking: thinkingConfig,
|
|
1952
|
+
tools: toolDefs
|
|
1953
|
+
});
|
|
1954
|
+
const result = await consumeStream(stream, options, socket, messages, messageId, conversationId, signal);
|
|
1955
|
+
if (result.pendingToolCalls.length > 0) {
|
|
1956
|
+
await handleToolCalls(socket, options, messages, result.pendingToolCalls, result.contentBlocks, messageId, conversationId, signal, result.fullResponse, startTime);
|
|
1957
|
+
} else {
|
|
1958
|
+
await sendComplete(socket, messageId, conversationId, result.usage, Date.now() - startTime, options.model, options.completeMeta);
|
|
1959
|
+
options.onComplete?.(result.fullResponse, result.usage, options.completeMeta);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
var flushStreamThinking = (state) => {
|
|
1963
|
+
if (state.currentThinking) {
|
|
1964
|
+
state.contentBlocks.push({
|
|
1965
|
+
signature: state.currentThinking.signature || undefined,
|
|
1966
|
+
thinking: state.currentThinking.text,
|
|
1967
|
+
type: "thinking"
|
|
1968
|
+
});
|
|
1969
|
+
state.currentThinking = null;
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
var consumeStreamChunk = async (chunk, options, socket, state, messageId, conversationId) => {
|
|
1973
|
+
switch (chunk.type) {
|
|
1974
|
+
case "thinking":
|
|
1975
|
+
if (chunk.content) {
|
|
1976
|
+
await sendMessage(socket, {
|
|
1977
|
+
content: chunk.content,
|
|
1978
|
+
conversationId,
|
|
1979
|
+
messageId,
|
|
1980
|
+
type: "thinking"
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
if (!state.currentThinking) {
|
|
1984
|
+
state.currentThinking = { signature: "", text: "" };
|
|
1985
|
+
}
|
|
1986
|
+
state.currentThinking.text += chunk.content;
|
|
1987
|
+
if (chunk.signature) {
|
|
1988
|
+
state.currentThinking.signature = chunk.signature;
|
|
1989
|
+
}
|
|
1990
|
+
break;
|
|
1991
|
+
case "text":
|
|
1992
|
+
flushStreamThinking(state);
|
|
1993
|
+
state.fullResponse += await processStreamTextChunk(chunk, options, socket, messageId, conversationId);
|
|
1994
|
+
state.contentBlocks.push({ content: chunk.content, type: "text" });
|
|
1995
|
+
break;
|
|
1996
|
+
case "image":
|
|
1997
|
+
await sendImageMessage(socket, chunk, messageId, conversationId);
|
|
1998
|
+
options.onImage?.({
|
|
1999
|
+
data: chunk.data,
|
|
2000
|
+
format: chunk.format,
|
|
2001
|
+
imageId: chunk.imageId,
|
|
2002
|
+
isPartial: chunk.isPartial,
|
|
2003
|
+
revisedPrompt: chunk.revisedPrompt
|
|
2004
|
+
});
|
|
2005
|
+
break;
|
|
2006
|
+
case "tool_use":
|
|
2007
|
+
flushStreamThinking(state);
|
|
2008
|
+
state.pendingToolCalls.push({
|
|
2009
|
+
id: chunk.id,
|
|
2010
|
+
input: chunk.input,
|
|
2011
|
+
name: chunk.name
|
|
2012
|
+
});
|
|
2013
|
+
state.contentBlocks.push({
|
|
2014
|
+
id: chunk.id,
|
|
2015
|
+
input: typeof chunk.input === "object" && chunk.input !== null ? chunk.input : {},
|
|
2016
|
+
name: chunk.name,
|
|
2017
|
+
type: "tool_use"
|
|
2018
|
+
});
|
|
2019
|
+
break;
|
|
2020
|
+
case "done":
|
|
2021
|
+
flushStreamThinking(state);
|
|
2022
|
+
state.usage = chunk.usage;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
var consumeStream = async (stream, options, socket, messages, messageId, conversationId, signal) => {
|
|
2027
|
+
const state = {
|
|
2028
|
+
contentBlocks: [],
|
|
2029
|
+
currentThinking: null,
|
|
2030
|
+
fullResponse: "",
|
|
2031
|
+
pendingToolCalls: [],
|
|
2032
|
+
usage: undefined
|
|
2033
|
+
};
|
|
2034
|
+
for await (const chunk of stream) {
|
|
2035
|
+
if (signal.aborted)
|
|
2036
|
+
break;
|
|
2037
|
+
await consumeStreamChunk(chunk, options, socket, state, messageId, conversationId);
|
|
2038
|
+
}
|
|
2039
|
+
return state;
|
|
2040
|
+
};
|
|
2041
|
+
var streamAI = async (socket, conversationId, messageId, options) => {
|
|
2042
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
2043
|
+
const startTime = Date.now();
|
|
2044
|
+
const messages = options.messages ? [...options.messages] : [];
|
|
2045
|
+
try {
|
|
2046
|
+
await processStream(socket, options, messages, messageId, conversationId, signal, startTime);
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
await handleStreamError(socket, err, messageId, conversationId, signal, startTime);
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
var handleStreamError = async (socket, err, messageId, conversationId, signal, startTime) => {
|
|
2052
|
+
if (signal.aborted) {
|
|
2053
|
+
await sendComplete(socket, messageId, conversationId, undefined, Date.now() - startTime);
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
await sendError(socket, err, messageId, conversationId);
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
// src/ai/streamAIToSSE.ts
|
|
2060
|
+
var DEFAULT_MAX_TURNS2 = 10;
|
|
2061
|
+
var DEFAULT_THINKING_BUDGET2 = 1e4;
|
|
2062
|
+
var buildToolDefinitions2 = (tools) => Object.entries(tools).map(([name, def]) => ({
|
|
2063
|
+
description: def.description,
|
|
2064
|
+
input_schema: def.input,
|
|
2065
|
+
name
|
|
2066
|
+
}));
|
|
2067
|
+
var executeTool2 = async (options, toolName, toolInput) => {
|
|
2068
|
+
const toolDef = options.tools?.[toolName];
|
|
2069
|
+
if (!toolDef) {
|
|
2070
|
+
return `Error: unknown tool "${toolName}"`;
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
return await toolDef.handler(toolInput);
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
var buildThinkingConfig = (options) => options.thinking ? {
|
|
2079
|
+
budget_tokens: typeof options.thinking === "object" ? options.thinking.budgetTokens : DEFAULT_THINKING_BUDGET2,
|
|
2080
|
+
type: "enabled"
|
|
2081
|
+
} : undefined;
|
|
2082
|
+
var serializeToolCall2 = (name, input) => `${name}:${JSON.stringify(input)}`;
|
|
2083
|
+
var streamAIToSSE = async function* (conversationId, messageId, options, renderers) {
|
|
2084
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
2085
|
+
const startTime = Date.now();
|
|
2086
|
+
const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS2;
|
|
2087
|
+
const messages = options.messages ? [...options.messages] : [];
|
|
2088
|
+
try {
|
|
2089
|
+
yield* streamTurns(options, renderers, messages, signal, startTime, maxTurns);
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
if (signal.aborted)
|
|
2092
|
+
return;
|
|
2093
|
+
yield {
|
|
2094
|
+
data: renderers.error(err instanceof Error ? err.message : String(err)),
|
|
2095
|
+
event: "status"
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
var flushThinking2 = (thinking, contentBlocks) => {
|
|
2100
|
+
contentBlocks.push({
|
|
2101
|
+
signature: thinking.signature || undefined,
|
|
2102
|
+
thinking: thinking.text,
|
|
2103
|
+
type: "thinking"
|
|
2104
|
+
});
|
|
2105
|
+
};
|
|
2106
|
+
var yieldCompletion = (renderers, options, fullResponse, usage, startTime) => {
|
|
2107
|
+
const durationMs = Date.now() - startTime;
|
|
2108
|
+
options.onComplete?.(fullResponse, usage);
|
|
2109
|
+
return {
|
|
2110
|
+
data: renderers.complete(usage, durationMs, options.model),
|
|
2111
|
+
event: "status"
|
|
2112
|
+
};
|
|
2113
|
+
};
|
|
2114
|
+
var processThinkingChunk = function* (content, signature, chunkState, renderers) {
|
|
2115
|
+
chunkState.currentThinking ??= { signature: "", text: "" };
|
|
2116
|
+
chunkState.currentThinking.text += content;
|
|
2117
|
+
chunkState.currentThinking.signature = signature ?? chunkState.currentThinking.signature;
|
|
2118
|
+
yield {
|
|
2119
|
+
data: renderers.thinking(chunkState.currentThinking.text),
|
|
2120
|
+
event: "thinking"
|
|
2121
|
+
};
|
|
2122
|
+
};
|
|
2123
|
+
var maybeFlushThinking = (chunkState) => {
|
|
2124
|
+
if (!chunkState.currentThinking)
|
|
2125
|
+
return;
|
|
2126
|
+
flushThinking2(chunkState.currentThinking, chunkState.contentBlocks);
|
|
2127
|
+
chunkState.currentThinking = null;
|
|
2128
|
+
};
|
|
2129
|
+
var processTextChunk = function* (content, chunkState, renderers, fullResponse) {
|
|
2130
|
+
maybeFlushThinking(chunkState);
|
|
2131
|
+
chunkState.contentBlocks.push({
|
|
2132
|
+
content,
|
|
2133
|
+
type: "text"
|
|
2134
|
+
});
|
|
2135
|
+
yield {
|
|
2136
|
+
data: renderers.chunk(content, fullResponse + content),
|
|
2137
|
+
event: "content"
|
|
2138
|
+
};
|
|
2139
|
+
};
|
|
2140
|
+
var processImageChunk = function* (chunk, renderers, options) {
|
|
2141
|
+
yield {
|
|
2142
|
+
data: renderers.image(chunk.data, chunk.format, chunk.revisedPrompt),
|
|
2143
|
+
event: "images"
|
|
2144
|
+
};
|
|
2145
|
+
options.onImage?.({
|
|
2146
|
+
data: chunk.data,
|
|
2147
|
+
format: chunk.format,
|
|
2148
|
+
imageId: chunk.imageId,
|
|
2149
|
+
isPartial: chunk.isPartial,
|
|
2150
|
+
revisedPrompt: chunk.revisedPrompt
|
|
2151
|
+
});
|
|
2152
|
+
};
|
|
2153
|
+
var processToolUseChunk = (chunk, chunkState) => {
|
|
2154
|
+
maybeFlushThinking(chunkState);
|
|
2155
|
+
chunkState.pendingToolCalls.push({
|
|
2156
|
+
id: chunk.id,
|
|
2157
|
+
input: chunk.input,
|
|
2158
|
+
name: chunk.name
|
|
2159
|
+
});
|
|
2160
|
+
chunkState.contentBlocks.push({
|
|
2161
|
+
id: chunk.id,
|
|
2162
|
+
input: typeof chunk.input === "object" && chunk.input !== null ? chunk.input : {},
|
|
2163
|
+
name: chunk.name,
|
|
2164
|
+
type: "tool_use"
|
|
2165
|
+
});
|
|
2166
|
+
};
|
|
2167
|
+
var processChunk2 = function* (chunk, chunkState, renderers, options, fullResponse) {
|
|
2168
|
+
switch (chunk.type) {
|
|
2169
|
+
case "thinking":
|
|
2170
|
+
yield* processThinkingChunk(chunk.content, chunk.signature, chunkState, renderers);
|
|
2171
|
+
break;
|
|
2172
|
+
case "text":
|
|
2173
|
+
yield* processTextChunk(chunk.content, chunkState, renderers, fullResponse);
|
|
2174
|
+
break;
|
|
2175
|
+
case "image":
|
|
2176
|
+
yield* processImageChunk(chunk, renderers, options);
|
|
2177
|
+
break;
|
|
2178
|
+
case "tool_use":
|
|
2179
|
+
processToolUseChunk(chunk, chunkState);
|
|
2180
|
+
break;
|
|
2181
|
+
case "done":
|
|
2182
|
+
maybeFlushThinking(chunkState);
|
|
2183
|
+
chunkState.usage = chunk.usage;
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
var executeToolCalls = async function* (pendingToolCalls, options, renderers, turnState) {
|
|
2188
|
+
const toolResultBlocks = [];
|
|
2189
|
+
for (const toolCall of pendingToolCalls) {
|
|
2190
|
+
turnState.allToolsHtml += renderers.toolRunning(toolCall.name, toolCall.input);
|
|
2191
|
+
yield { data: turnState.allToolsHtml, event: "tools" };
|
|
2192
|
+
const result = await executeTool2(options, toolCall.name, toolCall.input);
|
|
2193
|
+
turnState.allToolsHtml = turnState.allToolsHtml.replace(renderers.toolRunning(toolCall.name, toolCall.input), renderers.toolComplete(toolCall.name, result));
|
|
2194
|
+
yield { data: turnState.allToolsHtml, event: "tools" };
|
|
2195
|
+
options.onToolUse?.(toolCall.name, toolCall.input, result);
|
|
2196
|
+
toolResultBlocks.push({
|
|
2197
|
+
content: result,
|
|
2198
|
+
tool_use_id: toolCall.id,
|
|
2199
|
+
type: "tool_result"
|
|
2200
|
+
});
|
|
2201
|
+
turnState.executedToolKeys.add(serializeToolCall2(toolCall.name, toolCall.input));
|
|
2202
|
+
}
|
|
2203
|
+
return toolResultBlocks;
|
|
2204
|
+
};
|
|
2205
|
+
var consumeStream2 = async function* (stream, chunkState, renderers, options, turnState, signal) {
|
|
2206
|
+
for await (const chunk of stream) {
|
|
2207
|
+
if (signal.aborted)
|
|
2208
|
+
break;
|
|
2209
|
+
const prevResponse = turnState.fullResponse;
|
|
2210
|
+
yield* processChunk2(chunk, chunkState, renderers, options, turnState.fullResponse);
|
|
2211
|
+
if (chunk.type !== "text")
|
|
2212
|
+
continue;
|
|
2213
|
+
turnState.fullResponse = prevResponse + chunk.content;
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
var shouldStopToolLoop = (chunkState, turnState, signal) => {
|
|
2217
|
+
if (chunkState.pendingToolCalls.length === 0 || signal.aborted) {
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
return chunkState.pendingToolCalls.every((toolCall) => turnState.executedToolKeys.has(serializeToolCall2(toolCall.name, toolCall.input)));
|
|
2221
|
+
};
|
|
2222
|
+
var processTurn = async function* (chunkState, options, renderers, turnState) {
|
|
2223
|
+
turnState.currentMessages.push({
|
|
2224
|
+
content: chunkState.contentBlocks,
|
|
2225
|
+
role: "assistant"
|
|
2226
|
+
});
|
|
2227
|
+
const toolResults = yield* executeToolCalls(chunkState.pendingToolCalls, options, renderers, turnState);
|
|
2228
|
+
turnState.currentMessages.push({
|
|
2229
|
+
content: toolResults,
|
|
2230
|
+
role: "user"
|
|
2231
|
+
});
|
|
2232
|
+
};
|
|
2233
|
+
var streamTurns = async function* (options, renderers, messages, signal, startTime, maxTurns) {
|
|
2234
|
+
const turnState = {
|
|
2235
|
+
allToolsHtml: "",
|
|
2236
|
+
currentMessages: [...messages],
|
|
2237
|
+
executedToolKeys: new Set,
|
|
2238
|
+
fullResponse: "",
|
|
2239
|
+
turn: 0
|
|
2240
|
+
};
|
|
2241
|
+
const toolDefs = options.tools ? buildToolDefinitions2(options.tools) : undefined;
|
|
2242
|
+
const thinkingConfig = buildThinkingConfig(options);
|
|
2243
|
+
for (;turnState.turn <= maxTurns && !signal.aborted; turnState.turn++) {
|
|
2244
|
+
const chunkState = {
|
|
2245
|
+
contentBlocks: [],
|
|
2246
|
+
currentThinking: null,
|
|
2247
|
+
pendingToolCalls: [],
|
|
2248
|
+
usage: undefined
|
|
2249
|
+
};
|
|
2250
|
+
const stream = options.provider.stream({
|
|
2251
|
+
messages: turnState.currentMessages,
|
|
2252
|
+
model: options.model,
|
|
2253
|
+
signal,
|
|
2254
|
+
systemPrompt: options.systemPrompt,
|
|
2255
|
+
thinking: thinkingConfig,
|
|
2256
|
+
tools: toolDefs
|
|
2257
|
+
});
|
|
2258
|
+
yield* consumeStream2(stream, chunkState, renderers, options, turnState, signal);
|
|
2259
|
+
if (shouldStopToolLoop(chunkState, turnState, signal)) {
|
|
2260
|
+
return void (yield yieldCompletion(renderers, options, turnState.fullResponse, chunkState.usage, startTime));
|
|
2261
|
+
}
|
|
2262
|
+
yield* processTurn(chunkState, options, renderers, turnState);
|
|
2263
|
+
}
|
|
2264
|
+
return;
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
// src/constants.ts
|
|
2268
|
+
var EXCLUDE_LAST_OFFSET = -1;
|
|
2269
|
+
var HOURS_IN_DAY = 24;
|
|
2270
|
+
var MILLISECONDS_IN_A_SECOND = 1000;
|
|
2271
|
+
var MINUTES_IN_AN_HOUR = 60;
|
|
2272
|
+
var SECONDS_IN_A_MINUTE = 60;
|
|
2273
|
+
var MILLISECONDS_IN_A_MINUTE = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
|
|
2274
|
+
var MILLISECONDS_IN_A_DAY = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE * MINUTES_IN_AN_HOUR * HOURS_IN_DAY;
|
|
2275
|
+
var TWO_THIRDS = 2 / 3;
|
|
2276
|
+
|
|
2277
|
+
// src/ai/htmxRenderers.ts
|
|
2278
|
+
var escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2279
|
+
var defaultChunk = (_text, fullContent) => `<div class="ai-content">${escapeHtml(fullContent)}</div>`;
|
|
2280
|
+
var defaultMessageStart = ({
|
|
2281
|
+
cancelUrl,
|
|
2282
|
+
content,
|
|
2283
|
+
messageId,
|
|
2284
|
+
sseUrl
|
|
2285
|
+
}) => `<div id="msg-${messageId}" class="message user">` + `<div>${escapeHtml(content)}</div>` + `</div>` + `<div id="response-${messageId}" ` + `hx-ext="sse" ` + `sse-connect="${escapeHtml(sseUrl)}" ` + `hx-swap="innerHTML">` + `<div id="sse-retrieval-${messageId}" sse-swap="retrieval" hx-swap="innerHTML"></div>` + `<div id="sse-sources-${messageId}" sse-swap="sources" hx-swap="innerHTML"></div>` + `<div id="sse-content-${messageId}" sse-swap="content" hx-swap="innerHTML"></div>` + `<div id="sse-thinking-${messageId}" sse-swap="thinking" hx-swap="innerHTML"></div>` + `<div id="sse-tools-${messageId}" sse-swap="tools" hx-swap="innerHTML"></div>` + `<div id="sse-images-${messageId}" sse-swap="images" hx-swap="innerHTML"></div>` + `<div id="sse-status-${messageId}" sse-swap="status" hx-swap="innerHTML">` + `<button type="button" class="ai-cancel-button" hx-post="${escapeHtml(cancelUrl)}" hx-target="#sse-status-${messageId}" hx-swap="innerHTML">Cancel</button>` + `</div>` + `</div>`;
|
|
2286
|
+
var defaultThinking = (text) => `<details class="ai-thinking"><summary>Thinking</summary><p>${escapeHtml(text)}</p></details>`;
|
|
2287
|
+
var defaultToolRunning = (name, _input) => `<div class="ai-tool running"><span class="tool-name">${escapeHtml(name)}</span> <span class="tool-status">Running...</span></div>`;
|
|
2288
|
+
var defaultToolComplete = (name, result) => `<details class="ai-tool complete"><summary>${escapeHtml(name)}</summary><pre>${escapeHtml(result)}</pre></details>`;
|
|
2289
|
+
var defaultImage = (data, format, revisedPrompt) => `<figure class="ai-image">` + `<img src="data:image/${escapeHtml(format)};base64,${data}" alt="${revisedPrompt ? escapeHtml(revisedPrompt) : "Generated image"}" />${revisedPrompt ? `<figcaption>${escapeHtml(revisedPrompt)}</figcaption>` : ""}</figure>`;
|
|
2290
|
+
var defaultComplete = (usage, durationMs, model) => {
|
|
2291
|
+
const parts = [];
|
|
2292
|
+
if (model) {
|
|
2293
|
+
parts.push(escapeHtml(model));
|
|
2294
|
+
}
|
|
2295
|
+
if (usage) {
|
|
2296
|
+
parts.push(`${usage.inputTokens}in / ${usage.outputTokens}out`);
|
|
2297
|
+
}
|
|
2298
|
+
if (durationMs !== undefined) {
|
|
2299
|
+
const seconds = (durationMs / MILLISECONDS_IN_A_SECOND).toFixed(1);
|
|
2300
|
+
parts.push(`${seconds}s`);
|
|
2301
|
+
}
|
|
2302
|
+
return parts.length > 0 ? `<div class="ai-usage">${parts.join(" \xB7 ")}</div>` : "";
|
|
2303
|
+
};
|
|
2304
|
+
var defaultError = (message) => `<div class="ai-error">${escapeHtml(message)}</div>`;
|
|
2305
|
+
var defaultCanceled = () => `<div class="ai-canceled">Canceled.</div>`;
|
|
2306
|
+
var defaultRAGRetrieving = () => `<div class="ai-retrieving">Retrieving sources...</div>`;
|
|
2307
|
+
var renderTraceSummary = (trace) => {
|
|
2308
|
+
if (!trace) {
|
|
2309
|
+
return "";
|
|
2310
|
+
}
|
|
2311
|
+
return `<div class="ai-trace-summary">` + `Mode: ${escapeHtml(trace.mode)} \xB7 Final: ${trace.resultCounts.final} \xB7 Vector: ${trace.resultCounts.vector} \xB7 Lexical: ${trace.resultCounts.lexical}` + `</div>`;
|
|
2312
|
+
};
|
|
2313
|
+
var defaultRAGRetrieved = (sources, input) => sources.length === 0 ? "" : `<div class="ai-sources">` + `<h4>Citations</h4>` + renderTraceSummary(input?.trace) + `<ul>` + `${sources.map((source) => `<li>[${source.chunkId}] ${escapeHtml(source.text)}${source.source ? ` (${escapeHtml(source.source)})` : ""}</li>`).join("")}` + `</ul>` + `</div>`;
|
|
2314
|
+
var resolveRenderers = (custom) => ({
|
|
2315
|
+
chunk: custom?.chunk ?? defaultChunk,
|
|
2316
|
+
messageStart: custom?.messageStart ?? defaultMessageStart,
|
|
2317
|
+
complete: custom?.complete ?? defaultComplete,
|
|
2318
|
+
canceled: custom?.canceled ?? defaultCanceled,
|
|
2319
|
+
error: custom?.error ?? defaultError,
|
|
2320
|
+
image: custom?.image ?? defaultImage,
|
|
2321
|
+
ragRetrieving: custom?.ragRetrieving ?? defaultRAGRetrieving,
|
|
2322
|
+
ragRetrieved: custom?.ragRetrieved ?? defaultRAGRetrieved,
|
|
2323
|
+
thinking: custom?.thinking ?? defaultThinking,
|
|
2324
|
+
toolComplete: custom?.toolComplete ?? defaultToolComplete,
|
|
2325
|
+
toolRunning: custom?.toolRunning ?? defaultToolRunning
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
// src/plugins/aiChat.ts
|
|
2329
|
+
var DEFAULT_PATH = "/chat";
|
|
2330
|
+
var MAX_PREFIX_LEN = 12;
|
|
2331
|
+
var TITLE_MAX_LENGTH = 80;
|
|
2332
|
+
var NOT_FOUND2 = -1;
|
|
2333
|
+
var defaultParseProvider = (content) => {
|
|
2334
|
+
const colonIdx = content.indexOf(":");
|
|
2335
|
+
const hasPrefix = colonIdx > 0 && colonIdx < MAX_PREFIX_LEN;
|
|
2336
|
+
return {
|
|
2337
|
+
content: hasPrefix ? content.slice(colonIdx + 1) : content,
|
|
2338
|
+
providerName: hasPrefix ? content.slice(0, colonIdx) : "anthropic"
|
|
2339
|
+
};
|
|
2340
|
+
};
|
|
2341
|
+
var appendMessage = (conversation, message) => {
|
|
2342
|
+
conversation.messages.push(message);
|
|
2343
|
+
conversation.lastMessageAt = Date.now();
|
|
2344
|
+
if (!conversation.title && message.role === "user") {
|
|
2345
|
+
conversation.title = message.content.slice(0, TITLE_MAX_LENGTH);
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
var getHistory = (conversation) => conversation.messages.map((msg) => ({
|
|
2349
|
+
content: msg.content,
|
|
2350
|
+
role: msg.role
|
|
2351
|
+
}));
|
|
2352
|
+
var branchConversation = (source, fromMessageId) => {
|
|
2353
|
+
const cutoffIndex = source.messages.findIndex((msg) => msg.id === fromMessageId);
|
|
2354
|
+
if (cutoffIndex === NOT_FOUND2) {
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
const newId = generateId();
|
|
2358
|
+
const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: newId }));
|
|
2359
|
+
const newConversation = {
|
|
2360
|
+
createdAt: Date.now(),
|
|
2361
|
+
id: newId,
|
|
2362
|
+
messages: branchedMessages
|
|
2363
|
+
};
|
|
2364
|
+
return newConversation;
|
|
2365
|
+
};
|
|
2366
|
+
var buildUserMessage = (content, attachments) => {
|
|
2367
|
+
if (attachments && attachments.length > 0) {
|
|
2368
|
+
return {
|
|
2369
|
+
content: [
|
|
2370
|
+
...attachments.map((att) => {
|
|
2371
|
+
if (att.media_type === "application/pdf") {
|
|
2372
|
+
return {
|
|
2373
|
+
name: att.name,
|
|
2374
|
+
source: {
|
|
2375
|
+
data: att.data,
|
|
2376
|
+
media_type: att.media_type,
|
|
2377
|
+
type: "base64"
|
|
2378
|
+
},
|
|
2379
|
+
type: "document"
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
return {
|
|
2383
|
+
source: {
|
|
2384
|
+
data: att.data,
|
|
2385
|
+
media_type: att.media_type,
|
|
2386
|
+
type: "base64"
|
|
2387
|
+
},
|
|
2388
|
+
type: "image"
|
|
2389
|
+
};
|
|
2390
|
+
}),
|
|
2391
|
+
{ content, type: "text" }
|
|
2392
|
+
],
|
|
2393
|
+
role: "user"
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
return { content, role: "user" };
|
|
2397
|
+
};
|
|
2398
|
+
var resolveModel = (config, parsed) => {
|
|
2399
|
+
if (parsed.model) {
|
|
2400
|
+
return parsed.model;
|
|
2401
|
+
}
|
|
2402
|
+
if (typeof config.model === "string") {
|
|
2403
|
+
return config.model;
|
|
2404
|
+
}
|
|
2405
|
+
if (typeof config.model === "function") {
|
|
2406
|
+
return config.model(parsed.providerName);
|
|
2407
|
+
}
|
|
2408
|
+
return parsed.providerName;
|
|
2409
|
+
};
|
|
2410
|
+
var resolveTools = (config, providerName, model) => typeof config.tools === "function" ? config.tools(providerName, model) : config.tools;
|
|
2411
|
+
var resolveThinking = (config, providerName, model) => typeof config.thinking === "function" ? config.thinking(providerName, model) : config.thinking;
|
|
2412
|
+
var aiChat = (config) => {
|
|
2413
|
+
const path = config.path ?? DEFAULT_PATH;
|
|
2414
|
+
const store = config.store ?? createMemoryStore();
|
|
2415
|
+
const parseProvider = config.parseProvider ?? defaultParseProvider;
|
|
2416
|
+
const abortControllers = new Map;
|
|
2417
|
+
const handleCancel = (conversationId) => {
|
|
2418
|
+
const controller = abortControllers.get(conversationId);
|
|
2419
|
+
if (controller) {
|
|
2420
|
+
controller.abort();
|
|
2421
|
+
abortControllers.delete(conversationId);
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
const handleBranch = async (ws, messageId, conversationId) => {
|
|
2425
|
+
const source = await store.get(conversationId);
|
|
2426
|
+
if (!source) {
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const newConv = branchConversation(source, messageId);
|
|
2430
|
+
if (newConv) {
|
|
2431
|
+
await store.set(newConv.id, newConv);
|
|
2432
|
+
ws.send(JSON.stringify({ conversationId: newConv.id, type: "branched" }));
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
const handleUserMessage = async (ws, rawContent, rawConversationId, attachments) => {
|
|
2436
|
+
const conversationId = rawConversationId ?? generateId();
|
|
2437
|
+
const messageId = generateId();
|
|
2438
|
+
const parsed = parseProvider(rawContent);
|
|
2439
|
+
const { content, providerName } = parsed;
|
|
2440
|
+
const conversation = await store.getOrCreate(conversationId);
|
|
2441
|
+
const history = getHistory(conversation);
|
|
2442
|
+
const controller = new AbortController;
|
|
2443
|
+
abortControllers.set(conversationId, controller);
|
|
2444
|
+
appendMessage(conversation, {
|
|
2445
|
+
attachments,
|
|
2446
|
+
content,
|
|
2447
|
+
conversationId,
|
|
2448
|
+
id: messageId,
|
|
2449
|
+
role: "user",
|
|
2450
|
+
timestamp: Date.now()
|
|
2451
|
+
});
|
|
2452
|
+
await store.set(conversationId, conversation);
|
|
2453
|
+
const model = resolveModel(config, parsed);
|
|
2454
|
+
const userMessage = buildUserMessage(content, attachments);
|
|
2455
|
+
await streamAI(ws, conversationId, messageId, {
|
|
2456
|
+
maxTurns: config.maxTurns,
|
|
2457
|
+
messages: [...history, userMessage],
|
|
2458
|
+
model,
|
|
2459
|
+
provider: config.provider(providerName),
|
|
2460
|
+
signal: controller.signal,
|
|
2461
|
+
systemPrompt: config.systemPrompt,
|
|
2462
|
+
thinking: resolveThinking(config, providerName, model),
|
|
2463
|
+
tools: resolveTools(config, providerName, model),
|
|
2464
|
+
onComplete: async (fullResponse, usage) => {
|
|
2465
|
+
const conv = await store.get(conversationId);
|
|
2466
|
+
if (conv) {
|
|
2467
|
+
appendMessage(conv, {
|
|
2468
|
+
content: fullResponse,
|
|
2469
|
+
conversationId,
|
|
2470
|
+
id: generateId(),
|
|
2471
|
+
role: "assistant",
|
|
2472
|
+
timestamp: Date.now()
|
|
2473
|
+
});
|
|
2474
|
+
await store.set(conversationId, conv);
|
|
2475
|
+
}
|
|
2476
|
+
abortControllers.delete(conversationId);
|
|
2477
|
+
config.onComplete?.(conversationId, fullResponse, usage);
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
};
|
|
2481
|
+
const htmxRoutes = () => {
|
|
2482
|
+
if (!config.htmx) {
|
|
2483
|
+
return new Elysia;
|
|
2484
|
+
}
|
|
2485
|
+
const renderers = resolveRenderers(typeof config.htmx === "object" ? config.htmx.render : undefined);
|
|
2486
|
+
return new Elysia().post(`${path}/message`, async ({ body }) => {
|
|
2487
|
+
const requestBody = body && typeof body === "object" ? body : {};
|
|
2488
|
+
const rawContent = "content" in requestBody ? String(requestBody.content) : undefined;
|
|
2489
|
+
const rawConvId = "conversationId" in requestBody ? String(requestBody.conversationId) : undefined;
|
|
2490
|
+
const rawAttachmentsValue = "attachments" in requestBody ? requestBody.attachments : undefined;
|
|
2491
|
+
const rawAttachments = Array.isArray(rawAttachmentsValue) ? rawAttachmentsValue : undefined;
|
|
2492
|
+
if (!rawContent) {
|
|
2493
|
+
return new Response("Missing content", { status: 400 });
|
|
2494
|
+
}
|
|
2495
|
+
const conversationId = rawConvId || generateId();
|
|
2496
|
+
const messageId = generateId();
|
|
2497
|
+
const parsed = parseProvider(rawContent);
|
|
2498
|
+
const { content } = parsed;
|
|
2499
|
+
const conversation = await store.getOrCreate(conversationId);
|
|
2500
|
+
appendMessage(conversation, {
|
|
2501
|
+
attachments: rawAttachments,
|
|
2502
|
+
content,
|
|
2503
|
+
conversationId,
|
|
2504
|
+
id: messageId,
|
|
2505
|
+
role: "user",
|
|
2506
|
+
timestamp: Date.now()
|
|
2507
|
+
});
|
|
2508
|
+
await store.set(conversationId, conversation);
|
|
2509
|
+
const sseUrl = `${path}/sse/${conversationId}/${messageId}`;
|
|
2510
|
+
return new Response(`<div id="msg-${messageId}" class="message user">` + `<div>${content}</div>` + `</div>` + `<div id="response-${messageId}" ` + `hx-ext="sse" ` + `sse-connect="${sseUrl}" ` + `hx-swap="innerHTML">` + `<div id="sse-content-${messageId}" sse-swap="content" hx-swap="innerHTML"></div>` + `<div id="sse-thinking-${messageId}" sse-swap="thinking" hx-swap="innerHTML"></div>` + `<div id="sse-tools-${messageId}" sse-swap="tools" hx-swap="innerHTML"></div>` + `<div id="sse-images-${messageId}" sse-swap="images" hx-swap="innerHTML"></div>` + `<div id="sse-status-${messageId}" sse-swap="status" hx-swap="innerHTML"></div>` + `</div>`, { headers: { "Content-Type": "text/html" } });
|
|
2511
|
+
}).get(`${path}/sse/:conversationId/:messageId`, async function* ({ params }) {
|
|
2512
|
+
const { conversationId, messageId } = params;
|
|
2513
|
+
const conversation = await store.get(conversationId);
|
|
2514
|
+
if (!conversation) {
|
|
2515
|
+
yield {
|
|
2516
|
+
data: renderers.error("Conversation not found"),
|
|
2517
|
+
event: "status"
|
|
2518
|
+
};
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
const parsed = parseProvider(conversation.messages.at(EXCLUDE_LAST_OFFSET)?.content ?? "");
|
|
2522
|
+
const { providerName } = parsed;
|
|
2523
|
+
const model = resolveModel(config, parsed);
|
|
2524
|
+
const controller = new AbortController;
|
|
2525
|
+
abortControllers.set(conversationId, controller);
|
|
2526
|
+
const history = getHistory(conversation);
|
|
2527
|
+
const lastMsg = conversation.messages.at(EXCLUDE_LAST_OFFSET);
|
|
2528
|
+
const userMessage = buildUserMessage(lastMsg?.content ?? "", lastMsg?.attachments);
|
|
2529
|
+
const sseStream = streamAIToSSE(conversationId, messageId, {
|
|
2530
|
+
maxTurns: config.maxTurns,
|
|
2531
|
+
messages: [
|
|
2532
|
+
...history.slice(0, EXCLUDE_LAST_OFFSET),
|
|
2533
|
+
userMessage
|
|
2534
|
+
],
|
|
2535
|
+
model,
|
|
2536
|
+
provider: config.provider(providerName),
|
|
2537
|
+
signal: controller.signal,
|
|
2538
|
+
systemPrompt: config.systemPrompt,
|
|
2539
|
+
thinking: resolveThinking(config, providerName, model),
|
|
2540
|
+
tools: resolveTools(config, providerName, model)
|
|
2541
|
+
}, renderers);
|
|
2542
|
+
for await (const event of sseStream) {
|
|
2543
|
+
yield event;
|
|
2544
|
+
}
|
|
2545
|
+
const conv = await store.get(conversationId);
|
|
2546
|
+
if (conv) {
|
|
2547
|
+
await store.set(conversationId, conv);
|
|
2548
|
+
}
|
|
2549
|
+
abortControllers.delete(conversationId);
|
|
2550
|
+
}).get(`${path}/history/:conversationId`, async ({ params }) => {
|
|
2551
|
+
const conv = await store.get(params.conversationId);
|
|
2552
|
+
if (!conv) {
|
|
2553
|
+
return new Response("", { status: 404 });
|
|
2554
|
+
}
|
|
2555
|
+
const html = conv.messages.map((msg) => `<div class="message ${msg.role}"><div>${msg.content}</div></div>`).join("");
|
|
2556
|
+
return new Response(html, {
|
|
2557
|
+
headers: { "Content-Type": "text/html" }
|
|
2558
|
+
});
|
|
2559
|
+
}).get(`${path}/conversations/list`, async () => {
|
|
2560
|
+
const convos = await store.list();
|
|
2561
|
+
const html = convos.map((c) => `<div class="conversation-item" ` + `hx-get="${path}/history/${c.id}" ` + `hx-target="#messages" ` + `hx-swap="innerHTML">` + `<span class="title">${c.title}</span>` + `<span class="count">${c.messageCount} messages</span>` + `</div>`).join("");
|
|
2562
|
+
return new Response(html || '<div class="empty">No conversations</div>', { headers: { "Content-Type": "text/html" } });
|
|
2563
|
+
});
|
|
2564
|
+
};
|
|
2565
|
+
return new Elysia().ws(path, {
|
|
2566
|
+
message: async (ws, raw) => {
|
|
2567
|
+
const msg = parseAIMessage(raw);
|
|
2568
|
+
if (!msg) {
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (msg.type === "cancel" && msg.conversationId) {
|
|
2572
|
+
handleCancel(msg.conversationId);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (msg.type === "branch") {
|
|
2576
|
+
await handleBranch(ws, msg.messageId, msg.conversationId);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (msg.type === "message") {
|
|
2580
|
+
await handleUserMessage(ws, msg.content, msg.conversationId, msg.attachments);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}).get(`${path}/conversations`, () => store.list()).get(`${path}/conversations/:id`, async ({ params }) => {
|
|
2584
|
+
const conv = await store.get(params.id);
|
|
2585
|
+
if (!conv) {
|
|
2586
|
+
return new Response("Not found", { status: 404 });
|
|
2587
|
+
}
|
|
2588
|
+
return {
|
|
2589
|
+
id: conv.id,
|
|
2590
|
+
messages: conv.messages,
|
|
2591
|
+
title: conv.title ?? "Untitled"
|
|
2592
|
+
};
|
|
2593
|
+
}).delete(`${path}/conversations/:id`, async ({ params }) => {
|
|
2594
|
+
await store.remove(params.id);
|
|
2595
|
+
return { ok: true };
|
|
2596
|
+
}).use(htmxRoutes());
|
|
2597
|
+
};
|
|
2598
|
+
// src/ai/conversationManager.ts
|
|
2599
|
+
var NOT_FOUND3 = -1;
|
|
2600
|
+
var TITLE_MAX_LENGTH2 = 80;
|
|
2601
|
+
var createConversationManager = () => {
|
|
2602
|
+
const conversations = new Map;
|
|
2603
|
+
const getOrCreate = (conversationId) => {
|
|
2604
|
+
const id = conversationId ?? generateId();
|
|
2605
|
+
let conversation = conversations.get(id);
|
|
2606
|
+
if (!conversation) {
|
|
2607
|
+
conversation = { createdAt: Date.now(), id, messages: [] };
|
|
2608
|
+
conversations.set(id, conversation);
|
|
2609
|
+
}
|
|
2610
|
+
return conversation;
|
|
2611
|
+
};
|
|
2612
|
+
const appendMessage2 = (conversationId, message) => {
|
|
2613
|
+
const conversation = getOrCreate(conversationId);
|
|
2614
|
+
conversation.messages.push(message);
|
|
2615
|
+
conversation.lastMessageAt = Date.now();
|
|
2616
|
+
if (!conversation.title && message.role === "user") {
|
|
2617
|
+
conversation.title = message.content.slice(0, TITLE_MAX_LENGTH2);
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
const branch = (fromMessageId, sourceConversationId) => {
|
|
2621
|
+
const source = conversations.get(sourceConversationId);
|
|
2622
|
+
if (!source) {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
const cutoffIndex = source.messages.findIndex((msg) => msg.id === fromMessageId);
|
|
2626
|
+
if (cutoffIndex === NOT_FOUND3) {
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
const newId = generateId();
|
|
2630
|
+
const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: newId }));
|
|
2631
|
+
const newConversation = {
|
|
2632
|
+
createdAt: Date.now(),
|
|
2633
|
+
id: newId,
|
|
2634
|
+
messages: branchedMessages
|
|
2635
|
+
};
|
|
2636
|
+
conversations.set(newId, newConversation);
|
|
2637
|
+
return newId;
|
|
2638
|
+
};
|
|
2639
|
+
const emptyHistory = [];
|
|
2640
|
+
const getHistory2 = (conversationId) => {
|
|
2641
|
+
const conversation = conversations.get(conversationId);
|
|
2642
|
+
if (!conversation) {
|
|
2643
|
+
return emptyHistory;
|
|
2644
|
+
}
|
|
2645
|
+
const history = conversation.messages.map((msg) => ({
|
|
2646
|
+
content: msg.content,
|
|
2647
|
+
role: msg.role
|
|
2648
|
+
}));
|
|
2649
|
+
return history;
|
|
2650
|
+
};
|
|
2651
|
+
const getAbortController = (conversationId) => {
|
|
2652
|
+
const conversation = getOrCreate(conversationId);
|
|
2653
|
+
const controller = new AbortController;
|
|
2654
|
+
conversation.activeStreamAbort = controller;
|
|
2655
|
+
return controller;
|
|
2656
|
+
};
|
|
2657
|
+
const abort = (conversationId) => {
|
|
2658
|
+
const conversation = conversations.get(conversationId);
|
|
2659
|
+
if (conversation?.activeStreamAbort) {
|
|
2660
|
+
conversation.activeStreamAbort.abort();
|
|
2661
|
+
conversation.activeStreamAbort = undefined;
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
const get = (conversationId) => conversations.get(conversationId);
|
|
2665
|
+
const remove = (conversationId) => conversations.delete(conversationId);
|
|
2666
|
+
const list = () => Array.from(conversations.values()).map((conv) => ({
|
|
2667
|
+
createdAt: conv.createdAt,
|
|
2668
|
+
id: conv.id,
|
|
2669
|
+
lastMessageAt: conv.lastMessageAt,
|
|
2670
|
+
messageCount: conv.messages.length,
|
|
2671
|
+
title: conv.title ?? "Untitled"
|
|
2672
|
+
})).sort((first, second) => (second.lastMessageAt ?? second.createdAt) - (first.lastMessageAt ?? first.createdAt));
|
|
2673
|
+
const getMessages = (conversationId) => {
|
|
2674
|
+
const conversation = conversations.get(conversationId);
|
|
2675
|
+
if (!conversation) {
|
|
2676
|
+
return [];
|
|
2677
|
+
}
|
|
2678
|
+
return conversation.messages;
|
|
2679
|
+
};
|
|
2680
|
+
return {
|
|
2681
|
+
abort,
|
|
2682
|
+
appendMessage: appendMessage2,
|
|
2683
|
+
branch,
|
|
2684
|
+
get,
|
|
2685
|
+
getAbortController,
|
|
2686
|
+
getHistory: getHistory2,
|
|
2687
|
+
getMessages,
|
|
2688
|
+
getOrCreate,
|
|
2689
|
+
list,
|
|
2690
|
+
remove
|
|
2691
|
+
};
|
|
2692
|
+
};
|
|
2693
|
+
// src/ai/client/actions.ts
|
|
2694
|
+
var serverMessageToAction = (message) => {
|
|
2695
|
+
switch (message.type) {
|
|
2696
|
+
case "chunk":
|
|
2697
|
+
return {
|
|
2698
|
+
content: message.content,
|
|
2699
|
+
conversationId: message.conversationId,
|
|
2700
|
+
messageId: message.messageId,
|
|
2701
|
+
type: "chunk"
|
|
2702
|
+
};
|
|
2703
|
+
case "thinking":
|
|
2704
|
+
return {
|
|
2705
|
+
content: message.content,
|
|
2706
|
+
conversationId: message.conversationId,
|
|
2707
|
+
messageId: message.messageId,
|
|
2708
|
+
type: "thinking"
|
|
2709
|
+
};
|
|
2710
|
+
case "tool_status":
|
|
2711
|
+
return {
|
|
2712
|
+
conversationId: message.conversationId,
|
|
2713
|
+
input: message.input,
|
|
2714
|
+
messageId: message.messageId,
|
|
2715
|
+
name: message.name,
|
|
2716
|
+
result: message.result,
|
|
2717
|
+
status: message.status,
|
|
2718
|
+
type: "tool_status"
|
|
2719
|
+
};
|
|
2720
|
+
case "image":
|
|
2721
|
+
return {
|
|
2722
|
+
conversationId: message.conversationId,
|
|
2723
|
+
data: message.data,
|
|
2724
|
+
format: message.format,
|
|
2725
|
+
imageId: message.imageId,
|
|
2726
|
+
isPartial: message.isPartial,
|
|
2727
|
+
messageId: message.messageId,
|
|
2728
|
+
revisedPrompt: message.revisedPrompt,
|
|
2729
|
+
type: "image"
|
|
2730
|
+
};
|
|
2731
|
+
case "complete":
|
|
2732
|
+
return {
|
|
2733
|
+
conversationId: message.conversationId,
|
|
2734
|
+
durationMs: message.durationMs,
|
|
2735
|
+
messageId: message.messageId,
|
|
2736
|
+
model: message.model,
|
|
2737
|
+
sources: message.sources,
|
|
2738
|
+
type: "complete",
|
|
2739
|
+
usage: message.usage
|
|
2740
|
+
};
|
|
2741
|
+
case "rag_retrieving":
|
|
2742
|
+
return {
|
|
2743
|
+
conversationId: message.conversationId,
|
|
2744
|
+
messageId: message.messageId,
|
|
2745
|
+
retrievalStartedAt: message.retrievalStartedAt,
|
|
2746
|
+
type: "rag_retrieving"
|
|
2747
|
+
};
|
|
2748
|
+
case "rag_retrieved":
|
|
2749
|
+
return {
|
|
2750
|
+
conversationId: message.conversationId,
|
|
2751
|
+
messageId: message.messageId,
|
|
2752
|
+
retrievalDurationMs: message.retrievalDurationMs,
|
|
2753
|
+
retrievalStartedAt: message.retrievalStartedAt,
|
|
2754
|
+
retrievedAt: message.retrievedAt,
|
|
2755
|
+
sources: message.sources,
|
|
2756
|
+
trace: message.trace,
|
|
2757
|
+
type: "rag_retrieved"
|
|
2758
|
+
};
|
|
2759
|
+
case "error":
|
|
2760
|
+
return { message: message.message, type: "error" };
|
|
2761
|
+
default:
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
// src/ai/client/connection.ts
|
|
2766
|
+
var WS_OPEN2 = 1;
|
|
2767
|
+
var WS_NORMAL_CLOSURE = 1000;
|
|
2768
|
+
var WS_CLOSED = 3;
|
|
2769
|
+
var DEFAULT_PING_INTERVAL = 30000;
|
|
2770
|
+
var RECONNECT_INITIAL_DELAY = 500;
|
|
2771
|
+
var RECONNECT_POLL_INTERVAL = 300;
|
|
2772
|
+
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 60;
|
|
2773
|
+
var noop = () => {};
|
|
2774
|
+
var noopUnsubscribe = () => noop;
|
|
2775
|
+
var NOOP_CONNECTION = {
|
|
2776
|
+
close: noop,
|
|
2777
|
+
send: noop,
|
|
2778
|
+
subscribe: noopUnsubscribe,
|
|
2779
|
+
getReadyState: () => WS_CLOSED
|
|
2780
|
+
};
|
|
2781
|
+
var buildWsUrl = (path) => {
|
|
2782
|
+
const { hostname, port, protocol } = window.location;
|
|
2783
|
+
const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
|
|
2784
|
+
const portSuffix = port ? `:${port}` : "";
|
|
2785
|
+
return `${wsProtocol}//${hostname}${portSuffix}${path}`;
|
|
2786
|
+
};
|
|
2787
|
+
var parseServerMessage = (event) => {
|
|
2788
|
+
let data;
|
|
2789
|
+
try {
|
|
2790
|
+
data = JSON.parse(String(event.data));
|
|
2791
|
+
} catch {
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
if (data && typeof data === "object" && "type" in data && data.type === "pong") {
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
if (!isValidAIServerMessage(data)) {
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
return data;
|
|
2801
|
+
};
|
|
2802
|
+
var createAIConnection = (path, options = {}) => {
|
|
2803
|
+
if (typeof window === "undefined") {
|
|
2804
|
+
return NOOP_CONNECTION;
|
|
2805
|
+
}
|
|
2806
|
+
const shouldReconnect = options.reconnect !== false;
|
|
2807
|
+
const pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
2808
|
+
const maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
2809
|
+
const listeners = new Set;
|
|
2810
|
+
const connState = {
|
|
2811
|
+
isConnected: false,
|
|
2812
|
+
pendingMessages: [],
|
|
2813
|
+
pingInterval: null,
|
|
2814
|
+
reconnectAttempts: 0,
|
|
2815
|
+
reconnectTimeout: null,
|
|
2816
|
+
ws: null
|
|
2817
|
+
};
|
|
2818
|
+
const flushPendingMessages = () => {
|
|
2819
|
+
if (connState.ws?.readyState !== WS_OPEN2) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
while (connState.pendingMessages.length > 0) {
|
|
2823
|
+
const next = connState.pendingMessages.shift();
|
|
2824
|
+
if (typeof next === "string") {
|
|
2825
|
+
connState.ws.send(next);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
const clearTimers = () => {
|
|
2830
|
+
if (connState.pingInterval) {
|
|
2831
|
+
clearInterval(connState.pingInterval);
|
|
2832
|
+
connState.pingInterval = null;
|
|
2833
|
+
}
|
|
2834
|
+
if (connState.reconnectTimeout) {
|
|
2835
|
+
clearTimeout(connState.reconnectTimeout);
|
|
2836
|
+
connState.reconnectTimeout = null;
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
const scheduleReconnect = () => {
|
|
2840
|
+
connState.reconnectAttempts++;
|
|
2841
|
+
const delay2 = connState.reconnectAttempts === 1 ? RECONNECT_INITIAL_DELAY : RECONNECT_POLL_INTERVAL;
|
|
2842
|
+
connState.reconnectTimeout = setTimeout(() => {
|
|
2843
|
+
if (connState.reconnectAttempts > maxReconnectAttempts) {
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
connect();
|
|
2847
|
+
}, delay2);
|
|
2848
|
+
};
|
|
2849
|
+
const connect = () => {
|
|
2850
|
+
const url = buildWsUrl(path);
|
|
2851
|
+
const wsInstance = new WebSocket(url, options.protocols);
|
|
2852
|
+
wsInstance.onopen = () => {
|
|
2853
|
+
connState.isConnected = true;
|
|
2854
|
+
connState.reconnectAttempts = 0;
|
|
2855
|
+
flushPendingMessages();
|
|
2856
|
+
connState.pingInterval = setInterval(() => {
|
|
2857
|
+
if (wsInstance.readyState === WS_OPEN2 && connState.isConnected) {
|
|
2858
|
+
wsInstance.send(JSON.stringify({ type: "ping" }));
|
|
2859
|
+
}
|
|
2860
|
+
}, pingInterval);
|
|
2861
|
+
};
|
|
2862
|
+
wsInstance.onmessage = (event) => {
|
|
2863
|
+
const message = parseServerMessage(event);
|
|
2864
|
+
if (!message) {
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
listeners.forEach((listener) => listener(message));
|
|
2868
|
+
};
|
|
2869
|
+
wsInstance.onclose = (event) => {
|
|
2870
|
+
connState.isConnected = false;
|
|
2871
|
+
clearTimers();
|
|
2872
|
+
const shouldAttemptReconnect = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && connState.reconnectAttempts < maxReconnectAttempts;
|
|
2873
|
+
if (shouldAttemptReconnect) {
|
|
2874
|
+
scheduleReconnect();
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
wsInstance.onerror = () => {};
|
|
2878
|
+
connState.ws = wsInstance;
|
|
2879
|
+
};
|
|
2880
|
+
const send = (msg) => {
|
|
2881
|
+
const serialized = JSON.stringify(msg);
|
|
2882
|
+
if (connState.ws?.readyState === WS_OPEN2) {
|
|
2883
|
+
connState.ws.send(serialized);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
connState.pendingMessages.push(serialized);
|
|
2887
|
+
};
|
|
2888
|
+
const subscribe = (callback) => {
|
|
2889
|
+
listeners.add(callback);
|
|
2890
|
+
return () => {
|
|
2891
|
+
listeners.delete(callback);
|
|
2892
|
+
};
|
|
2893
|
+
};
|
|
2894
|
+
const close = () => {
|
|
2895
|
+
clearTimers();
|
|
2896
|
+
if (connState.ws) {
|
|
2897
|
+
connState.ws.close(WS_NORMAL_CLOSURE);
|
|
2898
|
+
connState.ws = null;
|
|
2899
|
+
}
|
|
2900
|
+
connState.isConnected = false;
|
|
2901
|
+
listeners.clear();
|
|
2902
|
+
};
|
|
2903
|
+
const getReadyState = () => connState.ws?.readyState ?? WS_CLOSED;
|
|
2904
|
+
connect();
|
|
2905
|
+
return { close, getReadyState, send, subscribe };
|
|
2906
|
+
};
|
|
2907
|
+
// src/ai/client/messageStore.ts
|
|
2908
|
+
var EMPTY_STATE = {
|
|
2909
|
+
activeConversationId: null,
|
|
2910
|
+
conversations: new Map,
|
|
2911
|
+
error: null,
|
|
2912
|
+
isStreaming: false
|
|
2913
|
+
};
|
|
2914
|
+
var initialActiveConversationId = null;
|
|
2915
|
+
var initialError = null;
|
|
2916
|
+
var freshState = () => ({
|
|
2917
|
+
activeConversationId: initialActiveConversationId,
|
|
2918
|
+
conversations: new Map,
|
|
2919
|
+
error: initialError,
|
|
2920
|
+
isStreaming: false
|
|
2921
|
+
});
|
|
2922
|
+
var findAssistantMessage = (conversation, messageId) => conversation.messages.find((msg) => msg.id === messageId && msg.role === "assistant");
|
|
2923
|
+
var getOrCreate = (state, conversationId) => {
|
|
2924
|
+
let conversation = state.conversations.get(conversationId);
|
|
2925
|
+
if (!conversation) {
|
|
2926
|
+
conversation = {
|
|
2927
|
+
createdAt: Date.now(),
|
|
2928
|
+
id: conversationId,
|
|
2929
|
+
messages: []
|
|
2930
|
+
};
|
|
2931
|
+
state.conversations.set(conversationId, conversation);
|
|
2932
|
+
}
|
|
2933
|
+
return conversation;
|
|
2934
|
+
};
|
|
2935
|
+
var handleSend = (state, action) => {
|
|
2936
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
2937
|
+
const message = {
|
|
2938
|
+
attachments: action.attachments,
|
|
2939
|
+
content: action.content,
|
|
2940
|
+
conversationId: action.conversationId,
|
|
2941
|
+
id: action.messageId,
|
|
2942
|
+
role: "user",
|
|
2943
|
+
timestamp: Date.now()
|
|
2944
|
+
};
|
|
2945
|
+
conversation.messages = [...conversation.messages, message];
|
|
2946
|
+
state.activeConversationId = action.conversationId;
|
|
2947
|
+
state.error = null;
|
|
2948
|
+
state.isStreaming = true;
|
|
2949
|
+
};
|
|
2950
|
+
var handleChunk = (state, action) => {
|
|
2951
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
2952
|
+
const existingIdx = conversation.messages.findIndex((msg) => msg.id === action.messageId && msg.role === "assistant");
|
|
2953
|
+
if (existingIdx >= 0) {
|
|
2954
|
+
const prevContent = conversation.messages[existingIdx]?.content ?? "";
|
|
2955
|
+
conversation.messages = conversation.messages.map((msg, idx) => idx === existingIdx ? { ...msg, content: prevContent + action.content } : msg);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
const message = {
|
|
2959
|
+
content: action.content,
|
|
2960
|
+
conversationId: action.conversationId,
|
|
2961
|
+
id: action.messageId,
|
|
2962
|
+
isStreaming: true,
|
|
2963
|
+
role: "assistant",
|
|
2964
|
+
timestamp: Date.now()
|
|
2965
|
+
};
|
|
2966
|
+
conversation.messages = [...conversation.messages, message];
|
|
2967
|
+
};
|
|
2968
|
+
var handleThinking = (state, action) => {
|
|
2969
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
2970
|
+
const existingIdx = conversation.messages.findIndex((msg) => msg.id === action.messageId && msg.role === "assistant");
|
|
2971
|
+
if (existingIdx >= 0) {
|
|
2972
|
+
const prevThinking = conversation.messages[existingIdx]?.thinking ?? "";
|
|
2973
|
+
conversation.messages = conversation.messages.map((msg, idx) => idx === existingIdx ? { ...msg, thinking: prevThinking + action.content } : msg);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
const message = {
|
|
2977
|
+
content: "",
|
|
2978
|
+
conversationId: action.conversationId,
|
|
2979
|
+
id: action.messageId,
|
|
2980
|
+
isStreaming: true,
|
|
2981
|
+
role: "assistant",
|
|
2982
|
+
thinking: action.content,
|
|
2983
|
+
timestamp: Date.now()
|
|
2984
|
+
};
|
|
2985
|
+
conversation.messages = [...conversation.messages, message];
|
|
2986
|
+
};
|
|
2987
|
+
var upsertToolCall = (message, toolCall) => {
|
|
2988
|
+
if (!message.toolCalls) {
|
|
2989
|
+
message.toolCalls = [toolCall];
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const existingIdx = message.toolCalls.findIndex((existing) => existing.name === toolCall.name);
|
|
2993
|
+
if (existingIdx >= 0) {
|
|
2994
|
+
message.toolCalls[existingIdx] = toolCall;
|
|
2995
|
+
} else {
|
|
2996
|
+
message.toolCalls = [...message.toolCalls, toolCall];
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
var getOrCreateAssistantMessage = (conversation, messageId, conversationId) => {
|
|
3000
|
+
const existing = findAssistantMessage(conversation, messageId);
|
|
3001
|
+
if (existing) {
|
|
3002
|
+
return existing;
|
|
3003
|
+
}
|
|
3004
|
+
const message = {
|
|
3005
|
+
content: "",
|
|
3006
|
+
conversationId,
|
|
3007
|
+
id: messageId,
|
|
3008
|
+
isStreaming: true,
|
|
3009
|
+
role: "assistant",
|
|
3010
|
+
timestamp: Date.now()
|
|
3011
|
+
};
|
|
3012
|
+
conversation.messages = [...conversation.messages, message];
|
|
3013
|
+
return message;
|
|
3014
|
+
};
|
|
3015
|
+
var handleRAGRetrieved = (state, action) => {
|
|
3016
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
3017
|
+
const message = getOrCreateAssistantMessage(conversation, action.messageId, action.conversationId);
|
|
3018
|
+
message.sources = action.sources;
|
|
3019
|
+
message.retrievalStartedAt = action.retrievalStartedAt ?? message.retrievalStartedAt;
|
|
3020
|
+
message.retrievedAt = action.retrievedAt;
|
|
3021
|
+
message.retrievalDurationMs = action.retrievalDurationMs;
|
|
3022
|
+
message.retrievalTrace = action.trace;
|
|
3023
|
+
conversation.messages = [...conversation.messages];
|
|
3024
|
+
};
|
|
3025
|
+
var handleRAGRetrieving = (state, action) => {
|
|
3026
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
3027
|
+
const message = getOrCreateAssistantMessage(conversation, action.messageId, action.conversationId);
|
|
3028
|
+
message.retrievalStartedAt = action.retrievalStartedAt;
|
|
3029
|
+
conversation.messages = [...conversation.messages];
|
|
3030
|
+
};
|
|
3031
|
+
var handleToolStatus = (state, action) => {
|
|
3032
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
3033
|
+
const message = getOrCreateAssistantMessage(conversation, action.messageId, action.conversationId);
|
|
3034
|
+
const toolCall = {
|
|
3035
|
+
id: action.messageId,
|
|
3036
|
+
input: action.input,
|
|
3037
|
+
name: action.name,
|
|
3038
|
+
result: action.status === "complete" ? action.result ?? "" : undefined
|
|
3039
|
+
};
|
|
3040
|
+
upsertToolCall(message, toolCall);
|
|
3041
|
+
conversation.messages = [...conversation.messages];
|
|
3042
|
+
};
|
|
3043
|
+
var markMessageComplete = (conversation, messageId, usage, durationMs, model, sources) => {
|
|
3044
|
+
conversation.messages = conversation.messages.map((msg) => msg.id === messageId && msg.role === "assistant" ? {
|
|
3045
|
+
...msg,
|
|
3046
|
+
durationMs,
|
|
3047
|
+
isStreaming: false,
|
|
3048
|
+
model,
|
|
3049
|
+
sources: sources ?? msg.sources,
|
|
3050
|
+
usage
|
|
3051
|
+
} : msg);
|
|
3052
|
+
};
|
|
3053
|
+
var NOT_FOUND4 = -1;
|
|
3054
|
+
var findExistingImageById = (images, imageId) => {
|
|
3055
|
+
if (!imageId)
|
|
3056
|
+
return NOT_FOUND4;
|
|
3057
|
+
return images.findIndex((img) => img.imageId === imageId);
|
|
3058
|
+
};
|
|
3059
|
+
var findReplaceablePartialIndex = (images) => {
|
|
3060
|
+
const lastIdx = images.length - 1;
|
|
3061
|
+
if (lastIdx >= 0 && images[lastIdx]?.isPartial) {
|
|
3062
|
+
return lastIdx;
|
|
3063
|
+
}
|
|
3064
|
+
return NOT_FOUND4;
|
|
3065
|
+
};
|
|
3066
|
+
var upsertImage = (message, imageData) => {
|
|
3067
|
+
if (!message.images) {
|
|
3068
|
+
message.images = [imageData];
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
const existingIdx = findExistingImageById(message.images, imageData.imageId);
|
|
3072
|
+
if (existingIdx >= 0) {
|
|
3073
|
+
message.images[existingIdx] = imageData;
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
const replaceableIdx = findReplaceablePartialIndex(message.images);
|
|
3077
|
+
if (replaceableIdx >= 0) {
|
|
3078
|
+
message.images[replaceableIdx] = imageData;
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
message.images = [...message.images, imageData];
|
|
3082
|
+
};
|
|
3083
|
+
var handleImage = (state, action) => {
|
|
3084
|
+
const conversation = getOrCreate(state, action.conversationId);
|
|
3085
|
+
const message = getOrCreateAssistantMessage(conversation, action.messageId, action.conversationId);
|
|
3086
|
+
upsertImage(message, {
|
|
3087
|
+
data: action.data,
|
|
3088
|
+
format: action.format,
|
|
3089
|
+
imageId: action.imageId,
|
|
3090
|
+
isPartial: action.isPartial,
|
|
3091
|
+
revisedPrompt: action.revisedPrompt
|
|
3092
|
+
});
|
|
3093
|
+
conversation.messages = [...conversation.messages];
|
|
3094
|
+
};
|
|
3095
|
+
var handleComplete = (state, action) => {
|
|
3096
|
+
const conversation = state.conversations.get(action.conversationId);
|
|
3097
|
+
if (conversation) {
|
|
3098
|
+
markMessageComplete(conversation, action.messageId, action.usage, action.durationMs, action.model, action.sources);
|
|
3099
|
+
}
|
|
3100
|
+
state.isStreaming = false;
|
|
3101
|
+
};
|
|
3102
|
+
var markConversationStreamsComplete = (conversation) => {
|
|
3103
|
+
const streamingMessages = conversation.messages.filter((msg) => msg.isStreaming);
|
|
3104
|
+
if (streamingMessages.length === 0) {
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
for (const msg of streamingMessages) {
|
|
3108
|
+
msg.isStreaming = false;
|
|
3109
|
+
}
|
|
3110
|
+
conversation.messages = [...conversation.messages];
|
|
3111
|
+
};
|
|
3112
|
+
var markAllStreamsComplete = (state) => {
|
|
3113
|
+
for (const [, conversation] of state.conversations) {
|
|
3114
|
+
markConversationStreamsComplete(conversation);
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
var handleBranch = (state, action) => {
|
|
3118
|
+
const source = state.conversations.get(action.oldConversationId);
|
|
3119
|
+
if (!source) {
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
const cutoffIndex = source.messages.findIndex((msg) => msg.id === action.fromMessageId);
|
|
3123
|
+
if (cutoffIndex < 0) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
const branchedMessages = source.messages.slice(0, cutoffIndex + 1).map((msg) => ({ ...msg, conversationId: action.newConversationId }));
|
|
3127
|
+
const newConversation = {
|
|
3128
|
+
createdAt: Date.now(),
|
|
3129
|
+
id: action.newConversationId,
|
|
3130
|
+
messages: branchedMessages
|
|
3131
|
+
};
|
|
3132
|
+
state.conversations.set(action.newConversationId, newConversation);
|
|
3133
|
+
state.activeConversationId = action.newConversationId;
|
|
3134
|
+
};
|
|
3135
|
+
var applyAction = (state, action) => {
|
|
3136
|
+
switch (action.type) {
|
|
3137
|
+
case "send":
|
|
3138
|
+
handleSend(state, action);
|
|
3139
|
+
break;
|
|
3140
|
+
case "chunk":
|
|
3141
|
+
handleChunk(state, action);
|
|
3142
|
+
break;
|
|
3143
|
+
case "thinking":
|
|
3144
|
+
handleThinking(state, action);
|
|
3145
|
+
break;
|
|
3146
|
+
case "tool_status":
|
|
3147
|
+
handleToolStatus(state, action);
|
|
3148
|
+
break;
|
|
3149
|
+
case "image":
|
|
3150
|
+
handleImage(state, action);
|
|
3151
|
+
break;
|
|
3152
|
+
case "complete":
|
|
3153
|
+
handleComplete(state, action);
|
|
3154
|
+
break;
|
|
3155
|
+
case "error":
|
|
3156
|
+
state.error = action.message;
|
|
3157
|
+
state.isStreaming = false;
|
|
3158
|
+
break;
|
|
3159
|
+
case "rag_retrieving":
|
|
3160
|
+
handleRAGRetrieving(state, action);
|
|
3161
|
+
break;
|
|
3162
|
+
case "rag_retrieved":
|
|
3163
|
+
handleRAGRetrieved(state, action);
|
|
3164
|
+
break;
|
|
3165
|
+
case "cancel":
|
|
3166
|
+
state.isStreaming = false;
|
|
3167
|
+
markAllStreamsComplete(state);
|
|
3168
|
+
break;
|
|
3169
|
+
case "branch":
|
|
3170
|
+
handleBranch(state, action);
|
|
3171
|
+
break;
|
|
3172
|
+
case "set_conversation":
|
|
3173
|
+
state.activeConversationId = action.conversationId;
|
|
3174
|
+
break;
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
var createAIMessageStore = () => {
|
|
3178
|
+
let state = freshState();
|
|
3179
|
+
const subscribers = new Set;
|
|
3180
|
+
return {
|
|
3181
|
+
dispatch: (action) => {
|
|
3182
|
+
applyAction(state, action);
|
|
3183
|
+
state = { ...state, conversations: new Map(state.conversations) };
|
|
3184
|
+
subscribers.forEach((callback) => callback());
|
|
3185
|
+
},
|
|
3186
|
+
getServerSnapshot: () => EMPTY_STATE,
|
|
3187
|
+
getSnapshot: () => state,
|
|
3188
|
+
subscribe: (callback) => {
|
|
3189
|
+
subscribers.add(callback);
|
|
3190
|
+
return () => {
|
|
3191
|
+
subscribers.delete(callback);
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
};
|
|
3195
|
+
};
|
|
3196
|
+
|
|
3197
|
+
// src/ai/client/createAIStream.ts
|
|
3198
|
+
var createAIStream = (path, conversationId) => {
|
|
3199
|
+
const connection = createAIConnection(path);
|
|
3200
|
+
const store = createAIMessageStore();
|
|
3201
|
+
const listeners = new Set;
|
|
3202
|
+
let currentError = null;
|
|
3203
|
+
let currentIsStreaming = false;
|
|
3204
|
+
let currentMessages = [];
|
|
3205
|
+
let activeConversationId = conversationId ?? null;
|
|
3206
|
+
const syncState = () => {
|
|
3207
|
+
const snapshot = store.getSnapshot();
|
|
3208
|
+
const convId = activeConversationId ?? snapshot.activeConversationId;
|
|
3209
|
+
const conversation = convId ? snapshot.conversations.get(convId) : undefined;
|
|
3210
|
+
activeConversationId = convId ?? snapshot.activeConversationId;
|
|
3211
|
+
currentError = snapshot.error;
|
|
3212
|
+
currentIsStreaming = snapshot.isStreaming;
|
|
3213
|
+
currentMessages = conversation?.messages ?? [];
|
|
3214
|
+
listeners.forEach((listener) => listener());
|
|
3215
|
+
};
|
|
3216
|
+
const unsubscribeStore = store.subscribe(syncState);
|
|
3217
|
+
const unsubscribeConnection = connection.subscribe((message) => {
|
|
3218
|
+
const action = serverMessageToAction(message);
|
|
3219
|
+
if (action) {
|
|
3220
|
+
store.dispatch(action);
|
|
3221
|
+
}
|
|
3222
|
+
});
|
|
3223
|
+
const branch = (messageId, content) => {
|
|
3224
|
+
if (activeConversationId) {
|
|
3225
|
+
connection.send({
|
|
3226
|
+
content,
|
|
3227
|
+
conversationId: activeConversationId,
|
|
3228
|
+
messageId,
|
|
3229
|
+
type: "branch"
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
const cancel = () => {
|
|
3234
|
+
if (activeConversationId) {
|
|
3235
|
+
store.dispatch({ type: "cancel" });
|
|
3236
|
+
connection.send({
|
|
3237
|
+
conversationId: activeConversationId,
|
|
3238
|
+
type: "cancel"
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
const destroy = () => {
|
|
3243
|
+
unsubscribeStore();
|
|
3244
|
+
unsubscribeConnection();
|
|
3245
|
+
connection.close();
|
|
3246
|
+
listeners.clear();
|
|
3247
|
+
};
|
|
3248
|
+
const send = (content, attachments) => {
|
|
3249
|
+
const convId = activeConversationId ?? generateId();
|
|
3250
|
+
const msgId = generateId();
|
|
3251
|
+
store.dispatch({
|
|
3252
|
+
attachments,
|
|
3253
|
+
content,
|
|
3254
|
+
conversationId: convId,
|
|
3255
|
+
messageId: msgId,
|
|
3256
|
+
type: "send"
|
|
3257
|
+
});
|
|
3258
|
+
connection.send({
|
|
3259
|
+
attachments,
|
|
3260
|
+
content,
|
|
3261
|
+
conversationId: convId,
|
|
3262
|
+
type: "message"
|
|
3263
|
+
});
|
|
3264
|
+
};
|
|
3265
|
+
const subscribe = (callback) => {
|
|
3266
|
+
listeners.add(callback);
|
|
3267
|
+
return () => {
|
|
3268
|
+
listeners.delete(callback);
|
|
3269
|
+
};
|
|
3270
|
+
};
|
|
3271
|
+
return {
|
|
3272
|
+
branch,
|
|
3273
|
+
cancel,
|
|
3274
|
+
destroy,
|
|
3275
|
+
send,
|
|
3276
|
+
subscribe,
|
|
3277
|
+
get error() {
|
|
3278
|
+
return currentError;
|
|
3279
|
+
},
|
|
3280
|
+
get isStreaming() {
|
|
3281
|
+
return currentIsStreaming;
|
|
3282
|
+
},
|
|
3283
|
+
get messages() {
|
|
3284
|
+
return currentMessages;
|
|
3285
|
+
}
|
|
3286
|
+
};
|
|
3287
|
+
};
|
|
3288
|
+
export {
|
|
3289
|
+
xai,
|
|
3290
|
+
streamAIToSSE,
|
|
3291
|
+
streamAI,
|
|
3292
|
+
serverMessageToAction,
|
|
3293
|
+
serializeAIMessage,
|
|
3294
|
+
resolveRenderers,
|
|
3295
|
+
parseAIMessage,
|
|
3296
|
+
openaiResponses,
|
|
3297
|
+
openaiCompatible,
|
|
3298
|
+
openai,
|
|
3299
|
+
ollama,
|
|
3300
|
+
moonshot,
|
|
3301
|
+
mistralai,
|
|
3302
|
+
meta,
|
|
3303
|
+
google,
|
|
3304
|
+
generateId,
|
|
3305
|
+
gemini,
|
|
3306
|
+
deepseek,
|
|
3307
|
+
createMemoryStore,
|
|
3308
|
+
createConversationManager,
|
|
3309
|
+
createAIStream,
|
|
3310
|
+
createAIConnection,
|
|
3311
|
+
anthropic,
|
|
3312
|
+
alibaba,
|
|
3313
|
+
aiChat
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
//# debugId=165B0DAA4823A12464756E2164756E21
|
|
3317
|
+
//# sourceMappingURL=index.js.map
|