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.
Files changed (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -77
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -289
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -548
  39. package/src/core/project-instructions.js +98 -98
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. 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
+ }