codemini-cli 0.5.10 → 0.5.11
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/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
|
@@ -1,514 +1,514 @@
|
|
|
1
|
-
function extractTextContent(content) {
|
|
2
|
-
if (typeof content === 'string') return content;
|
|
3
|
-
if (Array.isArray(content)) {
|
|
4
|
-
return content
|
|
5
|
-
.map((part) => {
|
|
6
|
-
if (typeof part === 'string') return part;
|
|
7
|
-
if (part?.type === 'text') return part.text || '';
|
|
8
|
-
return '';
|
|
9
|
-
})
|
|
10
|
-
.join('');
|
|
11
|
-
}
|
|
12
|
-
return '';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function cloneAnthropicContentBlock(block) {
|
|
16
|
-
if (!block || typeof block !== 'object') return null;
|
|
17
|
-
if (block.type === 'thinking') {
|
|
18
|
-
const thinking = String(block.thinking || block.text || '');
|
|
19
|
-
if (!thinking) return null;
|
|
20
|
-
return {
|
|
21
|
-
type: 'thinking',
|
|
22
|
-
thinking,
|
|
23
|
-
...(block.signature ? { signature: String(block.signature) } : {})
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
if (block.type === 'redacted_thinking') {
|
|
27
|
-
const data = block.data != null ? String(block.data) : '';
|
|
28
|
-
if (!data) return null;
|
|
29
|
-
return { type: 'redacted_thinking', data };
|
|
30
|
-
}
|
|
31
|
-
if (block.type === 'text') {
|
|
32
|
-
const text = String(block.text || '');
|
|
33
|
-
return text ? { type: 'text', text } : null;
|
|
34
|
-
}
|
|
35
|
-
if (block.type === 'tool_use') {
|
|
36
|
-
const name = String(block.name || '').trim();
|
|
37
|
-
if (!name) return null;
|
|
38
|
-
return {
|
|
39
|
-
type: 'tool_use',
|
|
40
|
-
id: String(block.id || ''),
|
|
41
|
-
name,
|
|
42
|
-
input: block.input && typeof block.input === 'object' && !Array.isArray(block.input) ? block.input : {}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function extractThinkingBlocks(message) {
|
|
49
|
-
const source = [
|
|
50
|
-
...(Array.isArray(message?.reasoning_details) ? message.reasoning_details : []),
|
|
51
|
-
...(Array.isArray(message?.content) ? message.content : [])
|
|
52
|
-
];
|
|
53
|
-
return source
|
|
54
|
-
.filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
|
|
55
|
-
.map(cloneAnthropicContentBlock)
|
|
56
|
-
.filter(Boolean);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function buildAssistantMessage({ text = '', toolCalls = [], thinkingBlocks = [] }) {
|
|
60
|
-
const assistantMessage = {
|
|
61
|
-
role: 'assistant',
|
|
62
|
-
content: text
|
|
63
|
-
};
|
|
64
|
-
const reasoningDetails = Array.isArray(thinkingBlocks)
|
|
65
|
-
? thinkingBlocks.map(cloneAnthropicContentBlock).filter(Boolean)
|
|
66
|
-
: [];
|
|
67
|
-
if (reasoningDetails.length > 0) assistantMessage.reasoning_details = reasoningDetails;
|
|
68
|
-
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
69
|
-
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
70
|
-
id: tc.id,
|
|
71
|
-
type: 'function',
|
|
72
|
-
function: {
|
|
73
|
-
name: tc.name,
|
|
74
|
-
arguments: tc.arguments || '{}'
|
|
75
|
-
}
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
return assistantMessage;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
82
|
-
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
83
|
-
if (argumentsValue == null) return '{}';
|
|
84
|
-
try {
|
|
85
|
-
return JSON.stringify(argumentsValue);
|
|
86
|
-
} catch {
|
|
87
|
-
return '{}';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function tryParseJsonObject(raw) {
|
|
92
|
-
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
93
|
-
return raw;
|
|
94
|
-
}
|
|
95
|
-
if (typeof raw !== 'string') return {};
|
|
96
|
-
try {
|
|
97
|
-
const parsed = JSON.parse(raw);
|
|
98
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
99
|
-
return parsed;
|
|
100
|
-
}
|
|
101
|
-
} catch {}
|
|
102
|
-
return {};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizeMessages(messages) {
|
|
106
|
-
const source = Array.isArray(messages) ? messages : [];
|
|
107
|
-
const systemParts = [];
|
|
108
|
-
const out = [];
|
|
109
|
-
|
|
110
|
-
for (let i = 0; i < source.length; i += 1) {
|
|
111
|
-
const message = source[i];
|
|
112
|
-
if (!message || typeof message !== 'object') continue;
|
|
113
|
-
if (message.role === 'system') {
|
|
114
|
-
const text = extractTextContent(message.content);
|
|
115
|
-
if (text) systemParts.push(text);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (message.role === 'tool') {
|
|
120
|
-
const toolResults = [];
|
|
121
|
-
while (i < source.length) {
|
|
122
|
-
const toolMessage = source[i];
|
|
123
|
-
if (!toolMessage || typeof toolMessage !== 'object' || toolMessage.role !== 'tool') break;
|
|
124
|
-
toolResults.push({
|
|
125
|
-
type: 'tool_result',
|
|
126
|
-
tool_use_id: String(toolMessage.tool_call_id || ''),
|
|
127
|
-
content: extractTextContent(toolMessage.content)
|
|
128
|
-
});
|
|
129
|
-
i += 1;
|
|
130
|
-
}
|
|
131
|
-
i -= 1;
|
|
132
|
-
out.push({
|
|
133
|
-
role: 'user',
|
|
134
|
-
content: toolResults
|
|
135
|
-
});
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const contentBlocks = message.role === 'assistant' ? extractThinkingBlocks(message) : [];
|
|
140
|
-
const text = extractTextContent(message.content);
|
|
141
|
-
if (text) {
|
|
142
|
-
contentBlocks.push({ type: 'text', text });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const hasContentToolUse = Array.isArray(message.content)
|
|
146
|
-
&& message.content.some((block) => block?.type === 'tool_use');
|
|
147
|
-
if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
148
|
-
if (!hasContentToolUse) {
|
|
149
|
-
for (const toolCall of message.tool_calls) {
|
|
150
|
-
const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
|
|
151
|
-
if (!name) continue;
|
|
152
|
-
contentBlocks.push({
|
|
153
|
-
type: 'tool_use',
|
|
154
|
-
id: String(toolCall?.id || ''),
|
|
155
|
-
name,
|
|
156
|
-
input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
out.push({
|
|
163
|
-
role: message.role,
|
|
164
|
-
content: contentBlocks
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
system: systemParts.join('\n\n').trim() || undefined,
|
|
170
|
-
messages: out
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function normalizeTools(tools) {
|
|
175
|
-
const source = Array.isArray(tools) ? tools : [];
|
|
176
|
-
return source
|
|
177
|
-
.map((tool) => {
|
|
178
|
-
const fn = tool?.function || {};
|
|
179
|
-
const name = String(fn.name || '').trim();
|
|
180
|
-
if (!name) return null;
|
|
181
|
-
return {
|
|
182
|
-
name,
|
|
183
|
-
...(fn.description ? { description: String(fn.description) } : {}),
|
|
184
|
-
input_schema: fn.parameters && typeof fn.parameters === 'object' ? fn.parameters : { type: 'object' }
|
|
185
|
-
};
|
|
186
|
-
})
|
|
187
|
-
.filter(Boolean);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function buildPayload({ model, temperature, messages, tools, stream = false, maxTokens = 4096 }) {
|
|
191
|
-
const normalized = normalizeMessages(messages);
|
|
192
|
-
const payload = {
|
|
193
|
-
model,
|
|
194
|
-
max_tokens: maxTokens,
|
|
195
|
-
temperature,
|
|
196
|
-
messages: normalized.messages
|
|
197
|
-
};
|
|
198
|
-
if (normalized.system) payload.system = normalized.system;
|
|
199
|
-
if (stream) payload.stream = true;
|
|
200
|
-
|
|
201
|
-
const normalizedTools = normalizeTools(tools);
|
|
202
|
-
if (normalizedTools.length > 0) {
|
|
203
|
-
payload.tools = normalizedTools;
|
|
204
|
-
payload.tool_choice = { type: 'auto' };
|
|
205
|
-
}
|
|
206
|
-
return payload;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function hasTrailingToolContext(messages) {
|
|
210
|
-
const source = Array.isArray(messages) ? messages : [];
|
|
211
|
-
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
212
|
-
const message = source[index];
|
|
213
|
-
if (!message || typeof message !== 'object') continue;
|
|
214
|
-
if (message.role === 'tool') return true;
|
|
215
|
-
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
216
|
-
}
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function extractAssistantResult(data, messages) {
|
|
221
|
-
const content = Array.isArray(data?.content) ? data.content : [];
|
|
222
|
-
const thinkingBlocks = content
|
|
223
|
-
.filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
|
|
224
|
-
.map(cloneAnthropicContentBlock)
|
|
225
|
-
.filter(Boolean);
|
|
226
|
-
const text = content
|
|
227
|
-
.filter((block) => block?.type === 'text')
|
|
228
|
-
.map((block) => block.text || '')
|
|
229
|
-
.join('');
|
|
230
|
-
const toolCalls = content
|
|
231
|
-
.filter((block) => block?.type === 'tool_use')
|
|
232
|
-
.map((block) => ({
|
|
233
|
-
id: String(block.id || ''),
|
|
234
|
-
name: String(block.name || ''),
|
|
235
|
-
arguments: normalizeIncomingToolCallArguments(block.input)
|
|
236
|
-
}))
|
|
237
|
-
.filter((toolCall) => toolCall.name);
|
|
238
|
-
const normalizedText = String(text || '').trim();
|
|
239
|
-
|
|
240
|
-
if (!normalizedText && toolCalls.length === 0) {
|
|
241
|
-
if (hasTrailingToolContext(messages)) {
|
|
242
|
-
return {
|
|
243
|
-
text: '',
|
|
244
|
-
toolCalls: [],
|
|
245
|
-
usage: data?.usage || null,
|
|
246
|
-
incomplete: true,
|
|
247
|
-
content,
|
|
248
|
-
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
throw new Error('Anthropic gateway returned empty assistant response');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
text,
|
|
256
|
-
toolCalls,
|
|
257
|
-
usage: data?.usage || null,
|
|
258
|
-
content,
|
|
259
|
-
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function createHeaders(apiKey) {
|
|
264
|
-
return {
|
|
265
|
-
'content-type': 'application/json',
|
|
266
|
-
'x-api-key': apiKey,
|
|
267
|
-
'anthropic-version': '2023-06-01'
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function buildMessagesUrl(baseUrl) {
|
|
272
|
-
return `${String(baseUrl || '').replace(/\/$/, '')}/v1/messages`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function parseJsonResponse(response) {
|
|
276
|
-
if (!response.ok) {
|
|
277
|
-
const text = await response.text().catch(() => '');
|
|
278
|
-
throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
|
|
279
|
-
}
|
|
280
|
-
return response.json();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function mergeUsage(current, next) {
|
|
284
|
-
if (!next || typeof next !== 'object') return current;
|
|
285
|
-
return {
|
|
286
|
-
...(current || {}),
|
|
287
|
-
...next
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function emptyToolCall(index) {
|
|
292
|
-
return {
|
|
293
|
-
index,
|
|
294
|
-
id: '',
|
|
295
|
-
name: '',
|
|
296
|
-
arguments: ''
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks = []) {
|
|
301
|
-
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
302
|
-
.sort((a, b) => a[0] - b[0])
|
|
303
|
-
.map(([, tc], i) => ({
|
|
304
|
-
id: tc.id || `tc-${i + 1}`,
|
|
305
|
-
name: tc.name,
|
|
306
|
-
arguments: tc.arguments || '{}'
|
|
307
|
-
}))
|
|
308
|
-
.filter((tc) => tc.name);
|
|
309
|
-
const normalizedText = String(text || '').trim();
|
|
310
|
-
const content = [];
|
|
311
|
-
for (const block of thinkingBlocks) {
|
|
312
|
-
const cloned = cloneAnthropicContentBlock(block);
|
|
313
|
-
if (cloned) content.push(cloned);
|
|
314
|
-
}
|
|
315
|
-
if (text) content.push({ type: 'text', text });
|
|
316
|
-
for (const toolCall of toolCalls) {
|
|
317
|
-
content.push({
|
|
318
|
-
type: 'tool_use',
|
|
319
|
-
id: toolCall.id,
|
|
320
|
-
name: toolCall.name,
|
|
321
|
-
input: tryParseJsonObject(toolCall.arguments)
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!normalizedText && toolCalls.length === 0) {
|
|
326
|
-
if (hasTrailingToolContext(messages)) {
|
|
327
|
-
return {
|
|
328
|
-
text: '',
|
|
329
|
-
toolCalls: [],
|
|
330
|
-
usage,
|
|
331
|
-
incomplete: true,
|
|
332
|
-
content: [],
|
|
333
|
-
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
throw new Error('Anthropic gateway stream returned empty assistant response');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
text,
|
|
341
|
-
toolCalls,
|
|
342
|
-
usage,
|
|
343
|
-
incomplete: false,
|
|
344
|
-
content,
|
|
345
|
-
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function* iterateSseEvents(stream) {
|
|
350
|
-
const decoder = new TextDecoder();
|
|
351
|
-
let buffer = '';
|
|
352
|
-
|
|
353
|
-
for await (const chunk of stream) {
|
|
354
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
355
|
-
while (buffer.includes('\n\n')) {
|
|
356
|
-
const boundary = buffer.indexOf('\n\n');
|
|
357
|
-
const rawEvent = buffer.slice(0, boundary);
|
|
358
|
-
buffer = buffer.slice(boundary + 2);
|
|
359
|
-
const lines = rawEvent.split('\n');
|
|
360
|
-
let event = 'message';
|
|
361
|
-
const dataLines = [];
|
|
362
|
-
for (const line of lines) {
|
|
363
|
-
if (line.startsWith('event:')) {
|
|
364
|
-
event = line.slice(6).trim();
|
|
365
|
-
} else if (line.startsWith('data:')) {
|
|
366
|
-
dataLines.push(line.slice(5).trimStart());
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const dataText = dataLines.join('\n');
|
|
370
|
-
if (!dataText || dataText === '[DONE]') continue;
|
|
371
|
-
yield {
|
|
372
|
-
event,
|
|
373
|
-
data: JSON.parse(dataText)
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export async function createChatCompletion({
|
|
380
|
-
baseUrl,
|
|
381
|
-
apiKey,
|
|
382
|
-
model,
|
|
383
|
-
messages,
|
|
384
|
-
temperature = 0.2,
|
|
385
|
-
tools,
|
|
386
|
-
timeoutMs = 1800000,
|
|
387
|
-
maxTokens = 4096
|
|
388
|
-
}) {
|
|
389
|
-
const payload = buildPayload({ model, temperature, messages, tools, maxTokens });
|
|
390
|
-
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
391
|
-
method: 'POST',
|
|
392
|
-
headers: createHeaders(apiKey),
|
|
393
|
-
body: JSON.stringify(payload),
|
|
394
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
395
|
-
});
|
|
396
|
-
const data = await parseJsonResponse(response);
|
|
397
|
-
return extractAssistantResult(data, messages);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export async function createChatCompletionStream({
|
|
401
|
-
baseUrl,
|
|
402
|
-
apiKey,
|
|
403
|
-
model,
|
|
404
|
-
messages,
|
|
405
|
-
temperature = 0.2,
|
|
406
|
-
tools,
|
|
407
|
-
onTextDelta,
|
|
408
|
-
onToolCallDelta,
|
|
409
|
-
timeoutMs = 1800000,
|
|
410
|
-
maxTokens = 4096,
|
|
411
|
-
signal: externalSignal
|
|
412
|
-
}) {
|
|
413
|
-
// 合并超时信号与外部中止信号
|
|
414
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
415
|
-
const controller = new AbortController();
|
|
416
|
-
const onAbort = () => controller.abort();
|
|
417
|
-
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
418
|
-
if (externalSignal) {
|
|
419
|
-
if (externalSignal.aborted) {
|
|
420
|
-
controller.abort();
|
|
421
|
-
} else {
|
|
422
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const payload = buildPayload({ model, temperature, messages, tools, stream: true, maxTokens });
|
|
426
|
-
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
427
|
-
method: 'POST',
|
|
428
|
-
headers: createHeaders(apiKey),
|
|
429
|
-
body: JSON.stringify(payload),
|
|
430
|
-
signal: controller.signal
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
if (!response.ok || !response.body) {
|
|
434
|
-
const text = await response.text().catch(() => '');
|
|
435
|
-
throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
let text = '';
|
|
439
|
-
let usage = null;
|
|
440
|
-
const toolCallsByIndex = new Map();
|
|
441
|
-
const thinkingBlocksByIndex = new Map();
|
|
442
|
-
|
|
443
|
-
for await (const chunk of iterateSseEvents(response.body)) {
|
|
444
|
-
usage = mergeUsage(usage, chunk?.data?.usage);
|
|
445
|
-
usage = mergeUsage(usage, chunk?.data?.message?.usage);
|
|
446
|
-
|
|
447
|
-
if (chunk.event === 'content_block_start') {
|
|
448
|
-
const index = Number(chunk?.data?.index ?? 0);
|
|
449
|
-
const contentBlock = chunk?.data?.content_block || {};
|
|
450
|
-
if (contentBlock.type === 'tool_use') {
|
|
451
|
-
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
452
|
-
current.id = String(contentBlock.id || current.id || '');
|
|
453
|
-
current.name = String(contentBlock.name || current.name || '');
|
|
454
|
-
const initialInput = contentBlock.input && Object.keys(contentBlock.input).length > 0
|
|
455
|
-
? normalizeIncomingToolCallArguments(contentBlock.input)
|
|
456
|
-
: '';
|
|
457
|
-
current.arguments = current.arguments || initialInput;
|
|
458
|
-
toolCallsByIndex.set(index, current);
|
|
459
|
-
} else if (contentBlock.type === 'thinking' || contentBlock.type === 'redacted_thinking') {
|
|
460
|
-
const current = cloneAnthropicContentBlock(contentBlock) || { type: contentBlock.type };
|
|
461
|
-
if (current.type === 'thinking' && current.thinking == null) current.thinking = '';
|
|
462
|
-
thinkingBlocksByIndex.set(index, current);
|
|
463
|
-
}
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (chunk.event !== 'content_block_delta') {
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const index = Number(chunk?.data?.index ?? 0);
|
|
472
|
-
const delta = chunk?.data?.delta || {};
|
|
473
|
-
if (delta.type === 'text_delta' && delta.text) {
|
|
474
|
-
text += delta.text;
|
|
475
|
-
if (onTextDelta) onTextDelta(delta.text);
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (delta.type === 'thinking_delta') {
|
|
480
|
-
const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
|
|
481
|
-
current.thinking = `${current.thinking || ''}${String(delta.thinking || '')}`;
|
|
482
|
-
thinkingBlocksByIndex.set(index, current);
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (delta.type === 'signature_delta') {
|
|
487
|
-
const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
|
|
488
|
-
current.signature = String(delta.signature || '');
|
|
489
|
-
thinkingBlocksByIndex.set(index, current);
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (delta.type === 'input_json_delta') {
|
|
494
|
-
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
495
|
-
current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
|
|
496
|
-
toolCallsByIndex.set(index, current);
|
|
497
|
-
if (onToolCallDelta) {
|
|
498
|
-
onToolCallDelta({
|
|
499
|
-
index,
|
|
500
|
-
id: current.id || `tc-${index + 1}`,
|
|
501
|
-
name: current.name,
|
|
502
|
-
arguments: current.arguments || '{}'
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const thinkingBlocks = Array.from(thinkingBlocksByIndex.entries())
|
|
509
|
-
.sort((a, b) => a[0] - b[0])
|
|
510
|
-
.map(([, block]) => cloneAnthropicContentBlock(block))
|
|
511
|
-
.filter(Boolean);
|
|
512
|
-
|
|
513
|
-
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks);
|
|
514
|
-
}
|
|
1
|
+
function extractTextContent(content) {
|
|
2
|
+
if (typeof content === 'string') return content;
|
|
3
|
+
if (Array.isArray(content)) {
|
|
4
|
+
return content
|
|
5
|
+
.map((part) => {
|
|
6
|
+
if (typeof part === 'string') return part;
|
|
7
|
+
if (part?.type === 'text') return part.text || '';
|
|
8
|
+
return '';
|
|
9
|
+
})
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cloneAnthropicContentBlock(block) {
|
|
16
|
+
if (!block || typeof block !== 'object') return null;
|
|
17
|
+
if (block.type === 'thinking') {
|
|
18
|
+
const thinking = String(block.thinking || block.text || '');
|
|
19
|
+
if (!thinking) return null;
|
|
20
|
+
return {
|
|
21
|
+
type: 'thinking',
|
|
22
|
+
thinking,
|
|
23
|
+
...(block.signature ? { signature: String(block.signature) } : {})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (block.type === 'redacted_thinking') {
|
|
27
|
+
const data = block.data != null ? String(block.data) : '';
|
|
28
|
+
if (!data) return null;
|
|
29
|
+
return { type: 'redacted_thinking', data };
|
|
30
|
+
}
|
|
31
|
+
if (block.type === 'text') {
|
|
32
|
+
const text = String(block.text || '');
|
|
33
|
+
return text ? { type: 'text', text } : null;
|
|
34
|
+
}
|
|
35
|
+
if (block.type === 'tool_use') {
|
|
36
|
+
const name = String(block.name || '').trim();
|
|
37
|
+
if (!name) return null;
|
|
38
|
+
return {
|
|
39
|
+
type: 'tool_use',
|
|
40
|
+
id: String(block.id || ''),
|
|
41
|
+
name,
|
|
42
|
+
input: block.input && typeof block.input === 'object' && !Array.isArray(block.input) ? block.input : {}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractThinkingBlocks(message) {
|
|
49
|
+
const source = [
|
|
50
|
+
...(Array.isArray(message?.reasoning_details) ? message.reasoning_details : []),
|
|
51
|
+
...(Array.isArray(message?.content) ? message.content : [])
|
|
52
|
+
];
|
|
53
|
+
return source
|
|
54
|
+
.filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
|
|
55
|
+
.map(cloneAnthropicContentBlock)
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildAssistantMessage({ text = '', toolCalls = [], thinkingBlocks = [] }) {
|
|
60
|
+
const assistantMessage = {
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
content: text
|
|
63
|
+
};
|
|
64
|
+
const reasoningDetails = Array.isArray(thinkingBlocks)
|
|
65
|
+
? thinkingBlocks.map(cloneAnthropicContentBlock).filter(Boolean)
|
|
66
|
+
: [];
|
|
67
|
+
if (reasoningDetails.length > 0) assistantMessage.reasoning_details = reasoningDetails;
|
|
68
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
69
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
70
|
+
id: tc.id,
|
|
71
|
+
type: 'function',
|
|
72
|
+
function: {
|
|
73
|
+
name: tc.name,
|
|
74
|
+
arguments: tc.arguments || '{}'
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return assistantMessage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
82
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
83
|
+
if (argumentsValue == null) return '{}';
|
|
84
|
+
try {
|
|
85
|
+
return JSON.stringify(argumentsValue);
|
|
86
|
+
} catch {
|
|
87
|
+
return '{}';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tryParseJsonObject(raw) {
|
|
92
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
if (typeof raw !== 'string') return {};
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeMessages(messages) {
|
|
106
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
107
|
+
const systemParts = [];
|
|
108
|
+
const out = [];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
111
|
+
const message = source[i];
|
|
112
|
+
if (!message || typeof message !== 'object') continue;
|
|
113
|
+
if (message.role === 'system') {
|
|
114
|
+
const text = extractTextContent(message.content);
|
|
115
|
+
if (text) systemParts.push(text);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (message.role === 'tool') {
|
|
120
|
+
const toolResults = [];
|
|
121
|
+
while (i < source.length) {
|
|
122
|
+
const toolMessage = source[i];
|
|
123
|
+
if (!toolMessage || typeof toolMessage !== 'object' || toolMessage.role !== 'tool') break;
|
|
124
|
+
toolResults.push({
|
|
125
|
+
type: 'tool_result',
|
|
126
|
+
tool_use_id: String(toolMessage.tool_call_id || ''),
|
|
127
|
+
content: extractTextContent(toolMessage.content)
|
|
128
|
+
});
|
|
129
|
+
i += 1;
|
|
130
|
+
}
|
|
131
|
+
i -= 1;
|
|
132
|
+
out.push({
|
|
133
|
+
role: 'user',
|
|
134
|
+
content: toolResults
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const contentBlocks = message.role === 'assistant' ? extractThinkingBlocks(message) : [];
|
|
140
|
+
const text = extractTextContent(message.content);
|
|
141
|
+
if (text) {
|
|
142
|
+
contentBlocks.push({ type: 'text', text });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hasContentToolUse = Array.isArray(message.content)
|
|
146
|
+
&& message.content.some((block) => block?.type === 'tool_use');
|
|
147
|
+
if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
148
|
+
if (!hasContentToolUse) {
|
|
149
|
+
for (const toolCall of message.tool_calls) {
|
|
150
|
+
const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
|
|
151
|
+
if (!name) continue;
|
|
152
|
+
contentBlocks.push({
|
|
153
|
+
type: 'tool_use',
|
|
154
|
+
id: String(toolCall?.id || ''),
|
|
155
|
+
name,
|
|
156
|
+
input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
out.push({
|
|
163
|
+
role: message.role,
|
|
164
|
+
content: contentBlocks
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
system: systemParts.join('\n\n').trim() || undefined,
|
|
170
|
+
messages: out
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeTools(tools) {
|
|
175
|
+
const source = Array.isArray(tools) ? tools : [];
|
|
176
|
+
return source
|
|
177
|
+
.map((tool) => {
|
|
178
|
+
const fn = tool?.function || {};
|
|
179
|
+
const name = String(fn.name || '').trim();
|
|
180
|
+
if (!name) return null;
|
|
181
|
+
return {
|
|
182
|
+
name,
|
|
183
|
+
...(fn.description ? { description: String(fn.description) } : {}),
|
|
184
|
+
input_schema: fn.parameters && typeof fn.parameters === 'object' ? fn.parameters : { type: 'object' }
|
|
185
|
+
};
|
|
186
|
+
})
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildPayload({ model, temperature, messages, tools, stream = false, maxTokens = 4096 }) {
|
|
191
|
+
const normalized = normalizeMessages(messages);
|
|
192
|
+
const payload = {
|
|
193
|
+
model,
|
|
194
|
+
max_tokens: maxTokens,
|
|
195
|
+
temperature,
|
|
196
|
+
messages: normalized.messages
|
|
197
|
+
};
|
|
198
|
+
if (normalized.system) payload.system = normalized.system;
|
|
199
|
+
if (stream) payload.stream = true;
|
|
200
|
+
|
|
201
|
+
const normalizedTools = normalizeTools(tools);
|
|
202
|
+
if (normalizedTools.length > 0) {
|
|
203
|
+
payload.tools = normalizedTools;
|
|
204
|
+
payload.tool_choice = { type: 'auto' };
|
|
205
|
+
}
|
|
206
|
+
return payload;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasTrailingToolContext(messages) {
|
|
210
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
211
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
212
|
+
const message = source[index];
|
|
213
|
+
if (!message || typeof message !== 'object') continue;
|
|
214
|
+
if (message.role === 'tool') return true;
|
|
215
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractAssistantResult(data, messages) {
|
|
221
|
+
const content = Array.isArray(data?.content) ? data.content : [];
|
|
222
|
+
const thinkingBlocks = content
|
|
223
|
+
.filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
|
|
224
|
+
.map(cloneAnthropicContentBlock)
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
const text = content
|
|
227
|
+
.filter((block) => block?.type === 'text')
|
|
228
|
+
.map((block) => block.text || '')
|
|
229
|
+
.join('');
|
|
230
|
+
const toolCalls = content
|
|
231
|
+
.filter((block) => block?.type === 'tool_use')
|
|
232
|
+
.map((block) => ({
|
|
233
|
+
id: String(block.id || ''),
|
|
234
|
+
name: String(block.name || ''),
|
|
235
|
+
arguments: normalizeIncomingToolCallArguments(block.input)
|
|
236
|
+
}))
|
|
237
|
+
.filter((toolCall) => toolCall.name);
|
|
238
|
+
const normalizedText = String(text || '').trim();
|
|
239
|
+
|
|
240
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
241
|
+
if (hasTrailingToolContext(messages)) {
|
|
242
|
+
return {
|
|
243
|
+
text: '',
|
|
244
|
+
toolCalls: [],
|
|
245
|
+
usage: data?.usage || null,
|
|
246
|
+
incomplete: true,
|
|
247
|
+
content,
|
|
248
|
+
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
throw new Error('Anthropic gateway returned empty assistant response');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
text,
|
|
256
|
+
toolCalls,
|
|
257
|
+
usage: data?.usage || null,
|
|
258
|
+
content,
|
|
259
|
+
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createHeaders(apiKey) {
|
|
264
|
+
return {
|
|
265
|
+
'content-type': 'application/json',
|
|
266
|
+
'x-api-key': apiKey,
|
|
267
|
+
'anthropic-version': '2023-06-01'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildMessagesUrl(baseUrl) {
|
|
272
|
+
return `${String(baseUrl || '').replace(/\/$/, '')}/v1/messages`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function parseJsonResponse(response) {
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
const text = await response.text().catch(() => '');
|
|
278
|
+
throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
|
|
279
|
+
}
|
|
280
|
+
return response.json();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function mergeUsage(current, next) {
|
|
284
|
+
if (!next || typeof next !== 'object') return current;
|
|
285
|
+
return {
|
|
286
|
+
...(current || {}),
|
|
287
|
+
...next
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function emptyToolCall(index) {
|
|
292
|
+
return {
|
|
293
|
+
index,
|
|
294
|
+
id: '',
|
|
295
|
+
name: '',
|
|
296
|
+
arguments: ''
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks = []) {
|
|
301
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
302
|
+
.sort((a, b) => a[0] - b[0])
|
|
303
|
+
.map(([, tc], i) => ({
|
|
304
|
+
id: tc.id || `tc-${i + 1}`,
|
|
305
|
+
name: tc.name,
|
|
306
|
+
arguments: tc.arguments || '{}'
|
|
307
|
+
}))
|
|
308
|
+
.filter((tc) => tc.name);
|
|
309
|
+
const normalizedText = String(text || '').trim();
|
|
310
|
+
const content = [];
|
|
311
|
+
for (const block of thinkingBlocks) {
|
|
312
|
+
const cloned = cloneAnthropicContentBlock(block);
|
|
313
|
+
if (cloned) content.push(cloned);
|
|
314
|
+
}
|
|
315
|
+
if (text) content.push({ type: 'text', text });
|
|
316
|
+
for (const toolCall of toolCalls) {
|
|
317
|
+
content.push({
|
|
318
|
+
type: 'tool_use',
|
|
319
|
+
id: toolCall.id,
|
|
320
|
+
name: toolCall.name,
|
|
321
|
+
input: tryParseJsonObject(toolCall.arguments)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
326
|
+
if (hasTrailingToolContext(messages)) {
|
|
327
|
+
return {
|
|
328
|
+
text: '',
|
|
329
|
+
toolCalls: [],
|
|
330
|
+
usage,
|
|
331
|
+
incomplete: true,
|
|
332
|
+
content: [],
|
|
333
|
+
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new Error('Anthropic gateway stream returned empty assistant response');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
text,
|
|
341
|
+
toolCalls,
|
|
342
|
+
usage,
|
|
343
|
+
incomplete: false,
|
|
344
|
+
content,
|
|
345
|
+
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function* iterateSseEvents(stream) {
|
|
350
|
+
const decoder = new TextDecoder();
|
|
351
|
+
let buffer = '';
|
|
352
|
+
|
|
353
|
+
for await (const chunk of stream) {
|
|
354
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
355
|
+
while (buffer.includes('\n\n')) {
|
|
356
|
+
const boundary = buffer.indexOf('\n\n');
|
|
357
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
358
|
+
buffer = buffer.slice(boundary + 2);
|
|
359
|
+
const lines = rawEvent.split('\n');
|
|
360
|
+
let event = 'message';
|
|
361
|
+
const dataLines = [];
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
if (line.startsWith('event:')) {
|
|
364
|
+
event = line.slice(6).trim();
|
|
365
|
+
} else if (line.startsWith('data:')) {
|
|
366
|
+
dataLines.push(line.slice(5).trimStart());
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const dataText = dataLines.join('\n');
|
|
370
|
+
if (!dataText || dataText === '[DONE]') continue;
|
|
371
|
+
yield {
|
|
372
|
+
event,
|
|
373
|
+
data: JSON.parse(dataText)
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function createChatCompletion({
|
|
380
|
+
baseUrl,
|
|
381
|
+
apiKey,
|
|
382
|
+
model,
|
|
383
|
+
messages,
|
|
384
|
+
temperature = 0.2,
|
|
385
|
+
tools,
|
|
386
|
+
timeoutMs = 1800000,
|
|
387
|
+
maxTokens = 4096
|
|
388
|
+
}) {
|
|
389
|
+
const payload = buildPayload({ model, temperature, messages, tools, maxTokens });
|
|
390
|
+
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
391
|
+
method: 'POST',
|
|
392
|
+
headers: createHeaders(apiKey),
|
|
393
|
+
body: JSON.stringify(payload),
|
|
394
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
395
|
+
});
|
|
396
|
+
const data = await parseJsonResponse(response);
|
|
397
|
+
return extractAssistantResult(data, messages);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function createChatCompletionStream({
|
|
401
|
+
baseUrl,
|
|
402
|
+
apiKey,
|
|
403
|
+
model,
|
|
404
|
+
messages,
|
|
405
|
+
temperature = 0.2,
|
|
406
|
+
tools,
|
|
407
|
+
onTextDelta,
|
|
408
|
+
onToolCallDelta,
|
|
409
|
+
timeoutMs = 1800000,
|
|
410
|
+
maxTokens = 4096,
|
|
411
|
+
signal: externalSignal
|
|
412
|
+
}) {
|
|
413
|
+
// 合并超时信号与外部中止信号
|
|
414
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const onAbort = () => controller.abort();
|
|
417
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
418
|
+
if (externalSignal) {
|
|
419
|
+
if (externalSignal.aborted) {
|
|
420
|
+
controller.abort();
|
|
421
|
+
} else {
|
|
422
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const payload = buildPayload({ model, temperature, messages, tools, stream: true, maxTokens });
|
|
426
|
+
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: createHeaders(apiKey),
|
|
429
|
+
body: JSON.stringify(payload),
|
|
430
|
+
signal: controller.signal
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!response.ok || !response.body) {
|
|
434
|
+
const text = await response.text().catch(() => '');
|
|
435
|
+
throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let text = '';
|
|
439
|
+
let usage = null;
|
|
440
|
+
const toolCallsByIndex = new Map();
|
|
441
|
+
const thinkingBlocksByIndex = new Map();
|
|
442
|
+
|
|
443
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
444
|
+
usage = mergeUsage(usage, chunk?.data?.usage);
|
|
445
|
+
usage = mergeUsage(usage, chunk?.data?.message?.usage);
|
|
446
|
+
|
|
447
|
+
if (chunk.event === 'content_block_start') {
|
|
448
|
+
const index = Number(chunk?.data?.index ?? 0);
|
|
449
|
+
const contentBlock = chunk?.data?.content_block || {};
|
|
450
|
+
if (contentBlock.type === 'tool_use') {
|
|
451
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
452
|
+
current.id = String(contentBlock.id || current.id || '');
|
|
453
|
+
current.name = String(contentBlock.name || current.name || '');
|
|
454
|
+
const initialInput = contentBlock.input && Object.keys(contentBlock.input).length > 0
|
|
455
|
+
? normalizeIncomingToolCallArguments(contentBlock.input)
|
|
456
|
+
: '';
|
|
457
|
+
current.arguments = current.arguments || initialInput;
|
|
458
|
+
toolCallsByIndex.set(index, current);
|
|
459
|
+
} else if (contentBlock.type === 'thinking' || contentBlock.type === 'redacted_thinking') {
|
|
460
|
+
const current = cloneAnthropicContentBlock(contentBlock) || { type: contentBlock.type };
|
|
461
|
+
if (current.type === 'thinking' && current.thinking == null) current.thinking = '';
|
|
462
|
+
thinkingBlocksByIndex.set(index, current);
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (chunk.event !== 'content_block_delta') {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const index = Number(chunk?.data?.index ?? 0);
|
|
472
|
+
const delta = chunk?.data?.delta || {};
|
|
473
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
474
|
+
text += delta.text;
|
|
475
|
+
if (onTextDelta) onTextDelta(delta.text);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (delta.type === 'thinking_delta') {
|
|
480
|
+
const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
|
|
481
|
+
current.thinking = `${current.thinking || ''}${String(delta.thinking || '')}`;
|
|
482
|
+
thinkingBlocksByIndex.set(index, current);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (delta.type === 'signature_delta') {
|
|
487
|
+
const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
|
|
488
|
+
current.signature = String(delta.signature || '');
|
|
489
|
+
thinkingBlocksByIndex.set(index, current);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (delta.type === 'input_json_delta') {
|
|
494
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
495
|
+
current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
|
|
496
|
+
toolCallsByIndex.set(index, current);
|
|
497
|
+
if (onToolCallDelta) {
|
|
498
|
+
onToolCallDelta({
|
|
499
|
+
index,
|
|
500
|
+
id: current.id || `tc-${index + 1}`,
|
|
501
|
+
name: current.name,
|
|
502
|
+
arguments: current.arguments || '{}'
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const thinkingBlocks = Array.from(thinkingBlocksByIndex.entries())
|
|
509
|
+
.sort((a, b) => a[0] - b[0])
|
|
510
|
+
.map(([, block]) => cloneAnthropicContentBlock(block))
|
|
511
|
+
.filter(Boolean);
|
|
512
|
+
|
|
513
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks);
|
|
514
|
+
}
|