codemini-cli 0.3.4 → 0.3.6
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 +20 -18
- package/package.json +6 -6
- package/souls/anime.md +12 -9
- package/souls/caveman.md +6 -6
- package/souls/ceo.md +10 -9
- package/souls/default.md +1 -1
- package/souls/pirate.md +6 -6
- package/souls/playful.md +7 -7
- package/souls/professional.md +1 -1
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +167 -49
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +720 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +93 -11
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +90 -25
- package/src/core/shell-profile.js +26 -6
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +216 -405
- package/src/tui/chat-app.js +490 -146
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getEffectivePolicy } from './shell-profile.js';
|
|
3
3
|
|
|
4
|
+
const SHELL_KEYWORDS = new Set([
|
|
5
|
+
'if',
|
|
6
|
+
'then',
|
|
7
|
+
'elif',
|
|
8
|
+
'else',
|
|
9
|
+
'fi',
|
|
10
|
+
'for',
|
|
11
|
+
'while',
|
|
12
|
+
'until',
|
|
13
|
+
'do',
|
|
14
|
+
'done',
|
|
15
|
+
'case',
|
|
16
|
+
'esac',
|
|
17
|
+
'in',
|
|
18
|
+
'function',
|
|
19
|
+
'time',
|
|
20
|
+
'{',
|
|
21
|
+
'}'
|
|
22
|
+
]);
|
|
23
|
+
|
|
4
24
|
function firstToken(command) {
|
|
5
25
|
const m = String(command || '').trim().match(/^"([^"]+)"|^'([^']+)'|^(\S+)/);
|
|
6
26
|
const raw = (m && (m[1] || m[2] || m[3])) || '';
|
|
@@ -51,6 +71,11 @@ function splitCommandSegments(command) {
|
|
|
51
71
|
continue;
|
|
52
72
|
}
|
|
53
73
|
|
|
74
|
+
if (ch === '&' && text[i - 1] === '>') {
|
|
75
|
+
current += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
if (ch === '|' || ch === ';' || ch === '&') {
|
|
55
80
|
if (current.trim()) segments.push(current.trim());
|
|
56
81
|
current = '';
|
|
@@ -157,6 +182,30 @@ function suggestionForToken(token, config) {
|
|
|
157
182
|
return 'Prefer structured tools like read, edit, write, grep, glob, and list first. If you need shell fallback, use allowed shell commands for search and local context such as rg, find, grep, sed, cat, or ls.';
|
|
158
183
|
}
|
|
159
184
|
|
|
185
|
+
function validateCdSegment(command, workspaceRoot) {
|
|
186
|
+
const tokens = tokenizeTopLevel(command);
|
|
187
|
+
if (tokens.length === 1) {
|
|
188
|
+
return { allowed: false, reason: 'cd requires a target path in safe mode' };
|
|
189
|
+
}
|
|
190
|
+
if (tokens.length !== 2) {
|
|
191
|
+
return { allowed: false, reason: 'cd only supports a single target path in safe mode' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rawTarget = String(tokens[1] || '').trim();
|
|
195
|
+
if (!rawTarget || rawTarget.startsWith('-')) {
|
|
196
|
+
return { allowed: false, reason: 'cd target is not allowed in safe mode' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
200
|
+
const resolvedTarget = path.resolve(resolvedRoot, rawTarget);
|
|
201
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
202
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
203
|
+
return { allowed: false, reason: `cd escapes workspace: ${rawTarget}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { allowed: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
160
209
|
export function evaluateCommandPolicy(command, config, workspaceRoot = process.cwd()) {
|
|
161
210
|
const policy = getEffectivePolicy(config);
|
|
162
211
|
const cmd = String(command || '').trim();
|
|
@@ -181,6 +230,13 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
181
230
|
const inspectedTokens = collectCommandTokens(cmd);
|
|
182
231
|
const allowlist = Array.isArray(policy.command_allowlist) ? policy.command_allowlist : [];
|
|
183
232
|
for (const item of inspectedTokens) {
|
|
233
|
+
if (SHELL_KEYWORDS.has(item.token)) continue;
|
|
234
|
+
if (item.token === 'cd') {
|
|
235
|
+
const cdCheck = validateCdSegment(item.raw, workspaceRoot);
|
|
236
|
+
if (!cdCheck.allowed) {
|
|
237
|
+
return { allowed: false, reason: cdCheck.reason, suggestion: suggestionForToken(item.token, config) };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
184
240
|
if (includesAny(item.token, policy.blocked_commands)) {
|
|
185
241
|
return { allowed: false, reason: `blocked command: ${item.token}`, suggestion: suggestionForToken(item.token, config) };
|
|
186
242
|
}
|
package/src/core/config-store.js
CHANGED
|
@@ -44,8 +44,6 @@ const DEFAULT_CONFIG = {
|
|
|
44
44
|
'edit',
|
|
45
45
|
'write',
|
|
46
46
|
'run',
|
|
47
|
-
'patch',
|
|
48
|
-
'generate_diff',
|
|
49
47
|
'list_background_tasks',
|
|
50
48
|
'get_background_task',
|
|
51
49
|
'stop_background_task'
|
|
@@ -151,7 +149,6 @@ function normalizePolicyLists(config) {
|
|
|
151
149
|
'edit',
|
|
152
150
|
'write',
|
|
153
151
|
'run',
|
|
154
|
-
'generate_diff',
|
|
155
152
|
'list_background_tasks',
|
|
156
153
|
'get_background_task',
|
|
157
154
|
'stop_background_task',
|
package/src/core/crypto-utils.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 共享加密工具函数。
|
|
3
|
-
*
|
|
3
|
+
* 统一使用 sha256 作为默认哈希算法,避免在各模块中重复定义。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 向后兼容别名,内部已迁移到 sha256。
|
|
10
|
+
* @deprecated 请使用 sha256() 替代。
|
|
11
|
+
*/
|
|
8
12
|
export function sha1(input) {
|
|
9
|
-
return
|
|
13
|
+
return sha256(input);
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function sha256(input) {
|
package/src/core/memory-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { sha256 } from './crypto-utils.js';
|
|
4
4
|
import { getMemoryDir, getProjectMemoryDir } from './paths.js';
|
|
5
5
|
import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export function getProjectMemoryKey(workspaceRoot = process.cwd(), projectAlias
|
|
|
23
23
|
if (alias) return slugify(alias);
|
|
24
24
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
25
25
|
const base = path.basename(root);
|
|
26
|
-
return `${slugify(base)}-${
|
|
26
|
+
return `${slugify(base)}-${sha256(root).slice(0, 10)}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function ensureScope(scope) {
|
|
@@ -63,7 +63,7 @@ function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
|
63
63
|
const now = nowIso();
|
|
64
64
|
const content = normalizeMemoryText(item?.content || '');
|
|
65
65
|
return {
|
|
66
|
-
id: String(item?.id || `mem_${
|
|
66
|
+
id: String(item?.id || `mem_${sha256(`${scope}:${projectKey}:${content}:${now}:${Math.random()}`).slice(0, 12)}`),
|
|
67
67
|
scope,
|
|
68
68
|
projectKey: projectKey || undefined,
|
|
69
69
|
kind: String(item?.kind || 'note').trim() || 'note',
|
|
@@ -2,8 +2,9 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
|
|
4
4
|
import { INDEX_SKIP_DIRS as SKIP_DIRS, SOURCE_EXTENSIONS, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
5
|
-
import {
|
|
5
|
+
import { sha256 } from './crypto-utils.js';
|
|
6
6
|
import { BoundedCache } from './bounded-cache.js';
|
|
7
|
+
import { trimInline, normalizeRelativePath, escapeRegex } from './string-utils.js';
|
|
7
8
|
|
|
8
9
|
const PROJECT_MARKER_FILES = new Set([
|
|
9
10
|
'package.json',
|
|
@@ -31,11 +32,7 @@ function clipList(values, max = 32) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function rel(cwd, filePath) {
|
|
34
|
-
return path.relative(cwd, filePath)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeRelativePath(value) {
|
|
38
|
-
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+/, '');
|
|
35
|
+
return normalizeRelativePath(path.relative(cwd, filePath));
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
async function safeStat(filePath) {
|
|
@@ -58,13 +55,6 @@ function tokenizeQuery(text) {
|
|
|
58
55
|
return [...new Set(String(text || '').toLowerCase().match(/[a-z0-9_./-]+/g) || [])].filter(Boolean);
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
function trimInline(value, max = 240) {
|
|
62
|
-
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
63
|
-
if (!text) return '';
|
|
64
|
-
if (text.length <= max) return text;
|
|
65
|
-
return `${text.slice(0, max - 3)}...`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
function trimMultiline(value, max = 1800) {
|
|
69
59
|
const text = String(value || '').trim();
|
|
70
60
|
if (!text) return '';
|
|
@@ -77,10 +67,6 @@ async function writeJson(filePath, value) {
|
|
|
77
67
|
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
function escapeRegex(value) {
|
|
81
|
-
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
70
|
function gitignorePatternToRegex(pattern) {
|
|
85
71
|
const normalized = normalizeRelativePath(pattern);
|
|
86
72
|
let regexBody = '';
|
|
@@ -282,7 +268,7 @@ function buildFileEntry(relativePath, content, stat) {
|
|
|
282
268
|
return {
|
|
283
269
|
file: relativePath,
|
|
284
270
|
language: LANGUAGE_BY_EXT[ext] || 'text',
|
|
285
|
-
hash:
|
|
271
|
+
hash: sha256(content),
|
|
286
272
|
size: Number(stat?.size || content.length || 0),
|
|
287
273
|
mtimeMs: Number(stat?.mtimeMs || 0),
|
|
288
274
|
imports,
|
|
@@ -318,14 +318,27 @@ export async function createChatCompletionStream({
|
|
|
318
318
|
onTextDelta,
|
|
319
319
|
onToolCallDelta,
|
|
320
320
|
timeoutMs = 90000,
|
|
321
|
-
maxTokens = 4096
|
|
321
|
+
maxTokens = 4096,
|
|
322
|
+
signal: externalSignal
|
|
322
323
|
}) {
|
|
324
|
+
// 合并超时信号与外部中止信号
|
|
325
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
const onAbort = () => controller.abort();
|
|
328
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
329
|
+
if (externalSignal) {
|
|
330
|
+
if (externalSignal.aborted) {
|
|
331
|
+
controller.abort();
|
|
332
|
+
} else {
|
|
333
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
323
336
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true, maxTokens });
|
|
324
337
|
const response = await fetch(buildMessagesUrl(baseUrl), {
|
|
325
338
|
method: 'POST',
|
|
326
339
|
headers: createHeaders(apiKey),
|
|
327
340
|
body: JSON.stringify(payload),
|
|
328
|
-
signal:
|
|
341
|
+
signal: controller.signal
|
|
329
342
|
});
|
|
330
343
|
|
|
331
344
|
if (!response.ok || !response.body) {
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
|
|
3
|
+
function extractTextContent(content) {
|
|
4
|
+
if (typeof content === 'string') return content;
|
|
5
|
+
if (Array.isArray(content)) {
|
|
6
|
+
return content
|
|
7
|
+
.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
return '';
|
|
11
|
+
})
|
|
12
|
+
.join('');
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
18
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
19
|
+
if (argumentsValue == null) return '{}';
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(argumentsValue);
|
|
22
|
+
} catch {
|
|
23
|
+
return '{}';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tryParseJsonObject(raw) {
|
|
28
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
if (typeof raw !== 'string') return {};
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeAssistantContentBlocks(message) {
|
|
42
|
+
if (Array.isArray(message?.content) && message.content.length > 0) {
|
|
43
|
+
return message.content.map((block) => ({ ...block }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentBlocks = [];
|
|
47
|
+
const text = extractTextContent(message?.content);
|
|
48
|
+
if (text) {
|
|
49
|
+
contentBlocks.push({ type: 'text', text });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(message?.tool_calls)) {
|
|
53
|
+
for (const toolCall of message.tool_calls) {
|
|
54
|
+
const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
|
|
55
|
+
if (!name) continue;
|
|
56
|
+
contentBlocks.push({
|
|
57
|
+
type: 'tool_use',
|
|
58
|
+
id: String(toolCall?.id || ''),
|
|
59
|
+
name,
|
|
60
|
+
input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return contentBlocks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeMessages(messages) {
|
|
69
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
70
|
+
const systemParts = [];
|
|
71
|
+
const out = [];
|
|
72
|
+
|
|
73
|
+
for (const message of source) {
|
|
74
|
+
if (!message || typeof message !== 'object') continue;
|
|
75
|
+
if (message.role === 'system') {
|
|
76
|
+
const text = extractTextContent(message.content);
|
|
77
|
+
if (text) systemParts.push(text);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (message.role === 'tool') {
|
|
82
|
+
out.push({
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: 'tool_result',
|
|
87
|
+
tool_use_id: String(message.tool_call_id || ''),
|
|
88
|
+
content: extractTextContent(message.content)
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (message.role === 'assistant') {
|
|
96
|
+
out.push({
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
content: normalizeAssistantContentBlocks(message)
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
out.push({
|
|
104
|
+
role: message.role,
|
|
105
|
+
content: [{ type: 'text', text: extractTextContent(message.content) }]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
system: systemParts.join('\n\n').trim() || undefined,
|
|
111
|
+
messages: out
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeTools(tools) {
|
|
116
|
+
const source = Array.isArray(tools) ? tools : [];
|
|
117
|
+
return source
|
|
118
|
+
.map((tool) => {
|
|
119
|
+
const fn = tool?.function || {};
|
|
120
|
+
const name = String(fn.name || '').trim();
|
|
121
|
+
if (!name) return null;
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
...(fn.description ? { description: String(fn.description) } : {}),
|
|
125
|
+
input_schema: fn.parameters && typeof fn.parameters === 'object' ? fn.parameters : { type: 'object' }
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildPayload({ model, temperature, messages, tools, stream = false, maxTokens = 4096 }) {
|
|
132
|
+
const normalized = normalizeMessages(messages);
|
|
133
|
+
const payload = {
|
|
134
|
+
model,
|
|
135
|
+
max_tokens: maxTokens,
|
|
136
|
+
temperature,
|
|
137
|
+
messages: normalized.messages
|
|
138
|
+
};
|
|
139
|
+
if (normalized.system) payload.system = normalized.system;
|
|
140
|
+
if (stream) payload.stream = true;
|
|
141
|
+
|
|
142
|
+
const normalizedTools = normalizeTools(tools);
|
|
143
|
+
if (normalizedTools.length > 0) {
|
|
144
|
+
payload.tools = normalizedTools;
|
|
145
|
+
payload.tool_choice = { type: 'auto' };
|
|
146
|
+
}
|
|
147
|
+
return payload;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasTrailingToolContext(messages) {
|
|
151
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
152
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
153
|
+
const message = source[index];
|
|
154
|
+
if (!message || typeof message !== 'object') continue;
|
|
155
|
+
if (message.role === 'tool') return true;
|
|
156
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildAssistantMessage(content) {
|
|
162
|
+
return {
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
content
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractAssistantResult(data, messages) {
|
|
169
|
+
const content = Array.isArray(data?.content) ? data.content.map((block) => ({ ...block })) : [];
|
|
170
|
+
const text = content
|
|
171
|
+
.filter((block) => block?.type === 'text')
|
|
172
|
+
.map((block) => block.text || '')
|
|
173
|
+
.join('');
|
|
174
|
+
const toolCalls = content
|
|
175
|
+
.filter((block) => block?.type === 'tool_use')
|
|
176
|
+
.map((block) => ({
|
|
177
|
+
id: String(block.id || ''),
|
|
178
|
+
name: String(block.name || ''),
|
|
179
|
+
arguments: normalizeIncomingToolCallArguments(block.input)
|
|
180
|
+
}))
|
|
181
|
+
.filter((toolCall) => toolCall.name);
|
|
182
|
+
const normalizedText = String(text || '').trim();
|
|
183
|
+
|
|
184
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
185
|
+
if (hasTrailingToolContext(messages)) {
|
|
186
|
+
return {
|
|
187
|
+
text: '',
|
|
188
|
+
toolCalls: [],
|
|
189
|
+
usage: data?.usage || null,
|
|
190
|
+
incomplete: true,
|
|
191
|
+
content
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
throw new Error('Anthropic gateway returned empty assistant response');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
text,
|
|
199
|
+
toolCalls,
|
|
200
|
+
usage: data?.usage || null,
|
|
201
|
+
content,
|
|
202
|
+
assistantMessage: buildAssistantMessage(content)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function mergeUsage(current, next) {
|
|
207
|
+
if (!next || typeof next !== 'object') return current;
|
|
208
|
+
return {
|
|
209
|
+
...(current || {}),
|
|
210
|
+
...next
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function emptyToolCall(index) {
|
|
215
|
+
return {
|
|
216
|
+
index,
|
|
217
|
+
id: '',
|
|
218
|
+
name: '',
|
|
219
|
+
arguments: ''
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, contentBlocksByIndex) {
|
|
224
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
225
|
+
.sort((a, b) => a[0] - b[0])
|
|
226
|
+
.map(([, tc], i) => ({
|
|
227
|
+
id: tc.id || `tc-${i + 1}`,
|
|
228
|
+
name: tc.name,
|
|
229
|
+
arguments: tc.arguments || '{}'
|
|
230
|
+
}))
|
|
231
|
+
.filter((tc) => tc.name);
|
|
232
|
+
const normalizedText = String(text || '').trim();
|
|
233
|
+
const content = Array.from(contentBlocksByIndex.entries())
|
|
234
|
+
.sort((a, b) => a[0] - b[0])
|
|
235
|
+
.map(([, block]) => {
|
|
236
|
+
if (block.type === 'tool_use') {
|
|
237
|
+
return {
|
|
238
|
+
type: 'tool_use',
|
|
239
|
+
id: block.id,
|
|
240
|
+
name: block.name,
|
|
241
|
+
input: tryParseJsonObject(block.arguments)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (block.type === 'thinking') {
|
|
245
|
+
return {
|
|
246
|
+
type: 'thinking',
|
|
247
|
+
thinking: block.thinking || '',
|
|
248
|
+
...(block.signature ? { signature: block.signature } : {})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: block.text || ''
|
|
254
|
+
};
|
|
255
|
+
})
|
|
256
|
+
.filter((block) => {
|
|
257
|
+
if (block.type === 'tool_use') return Boolean(block.name);
|
|
258
|
+
if (block.type === 'thinking') return Boolean(block.thinking);
|
|
259
|
+
return Boolean(block.text);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
263
|
+
if (hasTrailingToolContext(messages)) {
|
|
264
|
+
return {
|
|
265
|
+
text: '',
|
|
266
|
+
toolCalls: [],
|
|
267
|
+
usage,
|
|
268
|
+
incomplete: true,
|
|
269
|
+
content: []
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
throw new Error('Anthropic gateway stream returned empty assistant response');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
text,
|
|
277
|
+
toolCalls,
|
|
278
|
+
usage,
|
|
279
|
+
incomplete: false,
|
|
280
|
+
content,
|
|
281
|
+
assistantMessage: buildAssistantMessage(content)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createClient({ baseUrl, apiKey, timeoutMs = 90000, maxRetries = 2 }) {
|
|
286
|
+
return new Anthropic({
|
|
287
|
+
apiKey,
|
|
288
|
+
baseURL: String(baseUrl || '').replace(/\/$/, ''),
|
|
289
|
+
timeout: timeoutMs,
|
|
290
|
+
maxRetries
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function inferEventType(event) {
|
|
295
|
+
const explicit = String(event?.type || event?.event || '').trim();
|
|
296
|
+
if (explicit) return explicit;
|
|
297
|
+
if (event?.content_block && typeof event?.index === 'number') return 'content_block_start';
|
|
298
|
+
if (event?.delta && typeof event?.index === 'number') return 'content_block_delta';
|
|
299
|
+
if (event?.message) return 'message_start';
|
|
300
|
+
if (event?.usage) return 'message_delta';
|
|
301
|
+
if (event && typeof event === 'object' && Object.keys(event).length === 0) return 'message_stop';
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function createChatCompletion({
|
|
306
|
+
baseUrl,
|
|
307
|
+
apiKey,
|
|
308
|
+
model,
|
|
309
|
+
messages,
|
|
310
|
+
temperature = 0.2,
|
|
311
|
+
tools,
|
|
312
|
+
timeoutMs = 90000,
|
|
313
|
+
maxTokens = 4096,
|
|
314
|
+
maxRetries = 2
|
|
315
|
+
}) {
|
|
316
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
317
|
+
const response = await client.messages.create(buildPayload({ model, temperature, messages, tools, maxTokens }));
|
|
318
|
+
return extractAssistantResult(response, messages);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function createChatCompletionStream({
|
|
322
|
+
baseUrl,
|
|
323
|
+
apiKey,
|
|
324
|
+
model,
|
|
325
|
+
messages,
|
|
326
|
+
temperature = 0.2,
|
|
327
|
+
tools,
|
|
328
|
+
onTextDelta,
|
|
329
|
+
onToolCallDelta,
|
|
330
|
+
timeoutMs = 90000,
|
|
331
|
+
maxTokens = 4096,
|
|
332
|
+
maxRetries = 2
|
|
333
|
+
}) {
|
|
334
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
335
|
+
const stream = await client.messages.create(buildPayload({ model, temperature, messages, tools, stream: true, maxTokens }));
|
|
336
|
+
|
|
337
|
+
let text = '';
|
|
338
|
+
let usage = null;
|
|
339
|
+
const toolCallsByIndex = new Map();
|
|
340
|
+
const contentBlocksByIndex = new Map();
|
|
341
|
+
|
|
342
|
+
for await (const event of stream) {
|
|
343
|
+
const eventType = inferEventType(event);
|
|
344
|
+
usage = mergeUsage(usage, event?.usage);
|
|
345
|
+
usage = mergeUsage(usage, event?.message?.usage);
|
|
346
|
+
|
|
347
|
+
if (eventType === 'content_block_start') {
|
|
348
|
+
const index = Number(event?.index ?? 0);
|
|
349
|
+
const contentBlock = event?.content_block || {};
|
|
350
|
+
if (contentBlock.type === 'tool_use') {
|
|
351
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
352
|
+
current.id = String(contentBlock.id || current.id || '');
|
|
353
|
+
current.name = String(contentBlock.name || current.name || '');
|
|
354
|
+
const initialInput = contentBlock.input && Object.keys(contentBlock.input).length > 0
|
|
355
|
+
? normalizeIncomingToolCallArguments(contentBlock.input)
|
|
356
|
+
: '';
|
|
357
|
+
current.arguments = current.arguments || initialInput;
|
|
358
|
+
toolCallsByIndex.set(index, current);
|
|
359
|
+
contentBlocksByIndex.set(index, {
|
|
360
|
+
type: 'tool_use',
|
|
361
|
+
id: current.id,
|
|
362
|
+
name: current.name,
|
|
363
|
+
arguments: current.arguments
|
|
364
|
+
});
|
|
365
|
+
} else if (contentBlock.type === 'thinking') {
|
|
366
|
+
contentBlocksByIndex.set(index, {
|
|
367
|
+
type: 'thinking',
|
|
368
|
+
thinking: String(contentBlock.thinking || ''),
|
|
369
|
+
signature: String(contentBlock.signature || '')
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
contentBlocksByIndex.set(index, {
|
|
373
|
+
type: 'text',
|
|
374
|
+
text: String(contentBlock.text || '')
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (eventType === 'content_block_delta') {
|
|
381
|
+
const index = Number(event?.index ?? 0);
|
|
382
|
+
const delta = event?.delta || {};
|
|
383
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
384
|
+
text += delta.text;
|
|
385
|
+
const current = contentBlocksByIndex.get(index) || { type: 'text', text: '' };
|
|
386
|
+
current.text = `${current.text || ''}${delta.text}`;
|
|
387
|
+
contentBlocksByIndex.set(index, current);
|
|
388
|
+
if (onTextDelta) onTextDelta(delta.text);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (delta.type === 'thinking_delta' && delta.thinking) {
|
|
393
|
+
const current = contentBlocksByIndex.get(index) || { type: 'thinking', thinking: '', signature: '' };
|
|
394
|
+
current.thinking = `${current.thinking || ''}${delta.thinking}`;
|
|
395
|
+
contentBlocksByIndex.set(index, current);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (delta.type === 'signature_delta') {
|
|
400
|
+
const current = contentBlocksByIndex.get(index) || { type: 'thinking', thinking: '', signature: '' };
|
|
401
|
+
current.signature = `${current.signature || ''}${String(delta.signature || '')}`;
|
|
402
|
+
contentBlocksByIndex.set(index, current);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (delta.type === 'input_json_delta') {
|
|
407
|
+
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
408
|
+
current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
|
|
409
|
+
toolCallsByIndex.set(index, current);
|
|
410
|
+
contentBlocksByIndex.set(index, {
|
|
411
|
+
type: 'tool_use',
|
|
412
|
+
id: current.id,
|
|
413
|
+
name: current.name,
|
|
414
|
+
arguments: current.arguments
|
|
415
|
+
});
|
|
416
|
+
if (onToolCallDelta) {
|
|
417
|
+
onToolCallDelta({
|
|
418
|
+
index,
|
|
419
|
+
id: current.id || `tc-${index + 1}`,
|
|
420
|
+
name: current.name,
|
|
421
|
+
arguments: current.arguments || '{}'
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (eventType === 'message_delta') {
|
|
429
|
+
usage = mergeUsage(usage, event?.delta?.usage);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (eventType === 'message_stop') {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages, contentBlocksByIndex);
|
|
439
|
+
}
|