codemini-cli 0.5.7 → 0.5.9
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 +30 -0
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-Dp1CwQdI.js → highlighted-body-OFNGDK62-HgeDi9HJ.js} +1 -1
- package/codemini-web/dist/assets/index-BSdIdn3L.css +2 -0
- package/codemini-web/dist/assets/{index-Bvd2jj3t.js → index-C4tKT3v4.js} +95 -93
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-CDgkkDBg.js +1 -0
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +550 -549
- package/codemini-web/server.js +314 -187
- package/package.json +67 -67
- package/skills/codemini.skills.json +40 -0
- package/src/commands/skill.js +16 -5
- package/src/core/chat-runtime.js +93 -14
- package/src/core/command-loader.js +120 -25
- package/src/core/command-policy.js +34 -10
- package/src/core/config-store.js +11 -0
- package/src/core/provider/anthropic.js +137 -24
- package/src/core/tools.js +114 -65
- package/codemini-web/dist/assets/index-Csjkc1MY.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DSVp--w4.js +0 -1
|
@@ -11,6 +11,8 @@ import { readSkillRegistry } from './skill-registry.js';
|
|
|
11
11
|
|
|
12
12
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const BUNDLED_SKILLS_DIR = path.resolve(MODULE_DIR, '..', '..', 'skills');
|
|
14
|
+
const SKILL_CATALOG_FILE = 'codemini.skills.json';
|
|
15
|
+
const FRONTMATTER_READ_BYTES = 16 * 1024;
|
|
14
16
|
|
|
15
17
|
function parseArrayText(value) {
|
|
16
18
|
const inner = value.slice(1, -1).trim();
|
|
@@ -46,6 +48,79 @@ function parseFrontmatter(raw) {
|
|
|
46
48
|
return { metadata, content };
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function readFrontmatterMetadata(filePath) {
|
|
52
|
+
let fd;
|
|
53
|
+
try {
|
|
54
|
+
fd = fs.openSync(filePath, 'r');
|
|
55
|
+
const buffer = Buffer.alloc(FRONTMATTER_READ_BYTES);
|
|
56
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
57
|
+
const raw = buffer.subarray(0, bytesRead).toString('utf8');
|
|
58
|
+
if (!raw.startsWith('---\n')) return {};
|
|
59
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
60
|
+
if (end === -1) return {};
|
|
61
|
+
return parseFrontmatter(raw.slice(0, end + 5)).metadata;
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
} finally {
|
|
65
|
+
if (fd !== undefined) {
|
|
66
|
+
try { fs.closeSync(fd); } catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readSkillCatalog(baseDir) {
|
|
72
|
+
const catalogPath = path.join(baseDir, SKILL_CATALOG_FILE);
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
|
75
|
+
return parsed && typeof parsed === 'object' && parsed.skills && typeof parsed.skills === 'object'
|
|
76
|
+
? parsed.skills
|
|
77
|
+
: {};
|
|
78
|
+
} catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeStringArray(value) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
const single = String(value || '').trim();
|
|
88
|
+
return single ? [single] : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function catalogMetadata(catalog, name) {
|
|
92
|
+
const entry = catalog?.[name];
|
|
93
|
+
if (!entry || typeof entry !== 'object') return {};
|
|
94
|
+
return {
|
|
95
|
+
...(entry.description ? { description: String(entry.description) } : {}),
|
|
96
|
+
...(entry.mode ? { mode: String(entry.mode) } : {}),
|
|
97
|
+
...(entry.enabled !== undefined ? { enabled: entry.enabled !== false } : {}),
|
|
98
|
+
...(entry.priority !== undefined ? { priority: Number(entry.priority) } : {}),
|
|
99
|
+
triggers: normalizeStringArray(entry.triggers)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function commandWithContent(command, parsedContent) {
|
|
104
|
+
if (parsedContent !== undefined) {
|
|
105
|
+
return { ...command, content: parsedContent };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let cached;
|
|
109
|
+
let loaded = false;
|
|
110
|
+
return Object.defineProperty({ ...command }, 'content', {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
get() {
|
|
114
|
+
if (!loaded) {
|
|
115
|
+
const raw = fs.readFileSync(command.path, 'utf8');
|
|
116
|
+
cached = parseFrontmatter(raw).content;
|
|
117
|
+
loaded = true;
|
|
118
|
+
}
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
49
124
|
function safeEntries(dir) {
|
|
50
125
|
try {
|
|
51
126
|
return fs.readdirSync(dir);
|
|
@@ -104,77 +179,95 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
|
104
179
|
|
|
105
180
|
function loadLegacySkillsFromDir(baseDir, source, out) {
|
|
106
181
|
if (!fs.existsSync(baseDir)) return;
|
|
182
|
+
const catalog = readSkillCatalog(baseDir);
|
|
107
183
|
for (const entry of safeEntries(baseDir)) {
|
|
108
184
|
if (!isSafeEntry(entry)) continue;
|
|
109
185
|
const full = path.join(baseDir, entry);
|
|
110
186
|
const stat = fs.statSync(full);
|
|
111
187
|
if (!stat.isDirectory()) continue;
|
|
188
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
112
189
|
const skillFile = path.join(full, 'SKILL.md');
|
|
113
190
|
if (!fs.existsSync(skillFile)) continue;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
setCommand(out, entry, {
|
|
191
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
192
|
+
setCommand(out, entry, commandWithContent({
|
|
117
193
|
name: entry,
|
|
118
194
|
source: `${source}-skill`,
|
|
119
195
|
path: skillFile,
|
|
120
196
|
metadata: {
|
|
121
|
-
|
|
197
|
+
...frontmatter,
|
|
198
|
+
...catalogMeta,
|
|
199
|
+
description: catalogMeta.description || frontmatter.description || 'Legacy skill',
|
|
122
200
|
type: 'skill'
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
});
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
126
203
|
}
|
|
127
204
|
}
|
|
128
205
|
|
|
129
206
|
function loadBundledSkillsFromDir(baseDir, out) {
|
|
130
207
|
if (!fs.existsSync(baseDir)) return;
|
|
208
|
+
const catalog = readSkillCatalog(baseDir);
|
|
131
209
|
for (const entry of safeEntries(baseDir)) {
|
|
132
210
|
if (!isSafeEntry(entry)) continue;
|
|
133
211
|
const full = path.join(baseDir, entry);
|
|
134
212
|
const stat = fs.statSync(full);
|
|
135
213
|
if (!stat.isDirectory()) continue;
|
|
214
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
136
215
|
const skillFile = path.join(full, 'SKILL.md');
|
|
137
216
|
if (!fs.existsSync(skillFile)) continue;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
setCommand(out, entry, {
|
|
217
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
218
|
+
setCommand(out, entry, commandWithContent({
|
|
141
219
|
name: entry,
|
|
142
220
|
source: 'bundled-skill',
|
|
143
221
|
path: skillFile,
|
|
144
222
|
metadata: {
|
|
145
|
-
...
|
|
223
|
+
...frontmatter,
|
|
224
|
+
...catalogMeta,
|
|
146
225
|
type: 'skill',
|
|
147
|
-
version:
|
|
148
|
-
description:
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
version: frontmatter.version || '0.1.0',
|
|
227
|
+
description: catalogMeta.description || frontmatter.description || 'Bundled skill'
|
|
228
|
+
}
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function applySkillCatalogPatches(baseDir, out) {
|
|
234
|
+
const catalog = readSkillCatalog(baseDir);
|
|
235
|
+
for (const name of Object.keys(catalog)) {
|
|
236
|
+
const existing = out.get(name);
|
|
237
|
+
if (!existing || existing.metadata?.type !== 'skill') continue;
|
|
238
|
+
const meta = catalogMetadata(catalog, name);
|
|
239
|
+
existing.metadata = {
|
|
240
|
+
...existing.metadata,
|
|
241
|
+
...meta,
|
|
242
|
+
description: meta.description || existing.metadata.description || ''
|
|
243
|
+
};
|
|
152
244
|
}
|
|
153
245
|
}
|
|
154
246
|
|
|
155
247
|
function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
156
248
|
if (!registry || !Array.isArray(registry.skills)) return;
|
|
249
|
+
const catalog = readSkillCatalog(baseDir);
|
|
157
250
|
for (const skill of registry.skills) {
|
|
158
251
|
if (skill.enabled === false) continue;
|
|
159
252
|
const name = skill.name;
|
|
160
253
|
if (out.has(name)) continue;
|
|
254
|
+
const catalogMeta = catalogMetadata(catalog, name);
|
|
161
255
|
const entry = skill.entryFile || 'SKILL.md';
|
|
162
256
|
const full = path.join(baseDir, name, entry);
|
|
163
257
|
if (!fs.existsSync(full)) continue;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
setCommand(out, name, {
|
|
258
|
+
const frontmatter = readFrontmatterMetadata(full);
|
|
259
|
+
setCommand(out, name, commandWithContent({
|
|
167
260
|
name,
|
|
168
261
|
source: 'registry-skill',
|
|
169
262
|
path: full,
|
|
170
263
|
metadata: {
|
|
171
|
-
...
|
|
264
|
+
...frontmatter,
|
|
265
|
+
...catalogMeta,
|
|
172
266
|
type: 'skill',
|
|
173
|
-
version: skill.version ||
|
|
174
|
-
description: skill.description ||
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
});
|
|
267
|
+
version: skill.version || frontmatter.version || '0.0.0',
|
|
268
|
+
description: catalogMeta.description || skill.description || frontmatter.description || 'Installed skill'
|
|
269
|
+
}
|
|
270
|
+
}));
|
|
178
271
|
}
|
|
179
272
|
}
|
|
180
273
|
|
|
@@ -201,10 +294,12 @@ export async function loadCommandsAndSkills(cwd = process.cwd()) {
|
|
|
201
294
|
const commands = new Map();
|
|
202
295
|
|
|
203
296
|
loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
|
|
297
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
204
298
|
loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
|
|
205
299
|
loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
|
|
206
300
|
loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
|
|
207
301
|
loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
|
|
302
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
208
303
|
const registry = await readSkillRegistry();
|
|
209
304
|
loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
|
|
210
305
|
|
|
@@ -196,7 +196,25 @@ function suggestionForToken(token, config) {
|
|
|
196
196
|
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.';
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
function
|
|
199
|
+
function allowedPathRoots(workspaceRoot, config = {}) {
|
|
200
|
+
return [
|
|
201
|
+
workspaceRoot,
|
|
202
|
+
...(Array.isArray(config?.policy?.allowed_paths) ? config.policy.allowed_paths : [])
|
|
203
|
+
]
|
|
204
|
+
.map((item) => String(item || '').trim())
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map((item) => path.resolve(item));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isWithinAnyRoot(candidatePath, roots = []) {
|
|
210
|
+
const resolvedCandidate = path.resolve(candidatePath);
|
|
211
|
+
return roots.some((root) => {
|
|
212
|
+
const relative = path.relative(root, resolvedCandidate);
|
|
213
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateCdSegment(command, workspaceRoot, config = {}) {
|
|
200
218
|
const tokens = tokenizeTopLevel(command);
|
|
201
219
|
if (tokens.length === 1) {
|
|
202
220
|
return { allowed: false, reason: 'cd requires a target path in safe mode' };
|
|
@@ -210,11 +228,9 @@ function validateCdSegment(command, workspaceRoot) {
|
|
|
210
228
|
return { allowed: false, reason: 'cd target is not allowed in safe mode' };
|
|
211
229
|
}
|
|
212
230
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
217
|
-
return { allowed: false, reason: `cd escapes workspace: ${rawTarget}` };
|
|
231
|
+
const resolvedTarget = path.resolve(path.resolve(workspaceRoot), rawTarget);
|
|
232
|
+
if (!isWithinAnyRoot(resolvedTarget, allowedPathRoots(workspaceRoot, config))) {
|
|
233
|
+
return { allowed: false, reason: `cd escapes workspace or allowed paths: ${rawTarget}` };
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
return { allowed: true };
|
|
@@ -246,7 +262,7 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
246
262
|
for (const item of inspectedTokens) {
|
|
247
263
|
if (SHELL_KEYWORDS.has(item.token)) continue;
|
|
248
264
|
if (item.token === 'cd') {
|
|
249
|
-
const cdCheck = validateCdSegment(item.raw, workspaceRoot);
|
|
265
|
+
const cdCheck = validateCdSegment(item.raw, workspaceRoot, config);
|
|
250
266
|
if (!cdCheck.allowed) {
|
|
251
267
|
return { allowed: false, reason: cdCheck.reason, suggestion: suggestionForToken(item.token, config) };
|
|
252
268
|
}
|
|
@@ -263,11 +279,19 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
const
|
|
282
|
+
const allowedLower = allowedPathRoots(workspaceRoot, config).map((item) => item.toLowerCase().replace(/\//g, '\\'));
|
|
267
283
|
const windowsAbsPath = lower.match(/[a-z]:\\[^\s'"]+/g) || [];
|
|
268
284
|
for (const p of windowsAbsPath) {
|
|
269
|
-
if (!p.startsWith(
|
|
270
|
-
return { allowed: false, reason: `absolute path outside workspace: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
285
|
+
if (!allowedLower.some((root) => p === root || p.startsWith(`${root}\\`))) {
|
|
286
|
+
return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const posixAbsPath = cmd.match(/(?<![:/\w])\/(?!\/)[^\s'"]+/g) || [];
|
|
291
|
+
const allowedResolved = allowedPathRoots(workspaceRoot, config);
|
|
292
|
+
for (const p of posixAbsPath) {
|
|
293
|
+
if (!isWithinAnyRoot(p, allowedResolved)) {
|
|
294
|
+
return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
|
package/src/core/config-store.js
CHANGED
|
@@ -89,6 +89,7 @@ const DEFAULT_CONFIG = {
|
|
|
89
89
|
policy: {
|
|
90
90
|
safe_mode: true,
|
|
91
91
|
allow_dangerous_commands: false,
|
|
92
|
+
allowed_paths: [],
|
|
92
93
|
command_allowlist: [],
|
|
93
94
|
blocked_commands: [],
|
|
94
95
|
blocked_path_patterns: [],
|
|
@@ -195,6 +196,9 @@ function normalizePolicyLists(config) {
|
|
|
195
196
|
next.policy.command_allowlist = uniqueStrings(
|
|
196
197
|
Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
|
|
197
198
|
);
|
|
199
|
+
next.policy.allowed_paths = uniqueStrings(
|
|
200
|
+
Array.isArray(next.policy.allowed_paths) ? next.policy.allowed_paths : []
|
|
201
|
+
);
|
|
198
202
|
next.policy.blocked_commands = uniqueStrings(
|
|
199
203
|
Array.isArray(next.policy.blocked_commands) ? next.policy.blocked_commands : []
|
|
200
204
|
);
|
|
@@ -212,6 +216,13 @@ function parseValue(input) {
|
|
|
212
216
|
if (input === 'true') return true;
|
|
213
217
|
if (input === 'false') return false;
|
|
214
218
|
if (input === 'null') return null;
|
|
219
|
+
if ((input.startsWith('[') && input.endsWith(']')) || (input.startsWith('{') && input.endsWith('}'))) {
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(input);
|
|
222
|
+
} catch {
|
|
223
|
+
return input;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
215
226
|
if (!Number.isNaN(Number(input)) && input.trim() !== '') return Number(input);
|
|
216
227
|
return input;
|
|
217
228
|
}
|
|
@@ -12,6 +12,72 @@ function extractTextContent(content) {
|
|
|
12
12
|
return '';
|
|
13
13
|
}
|
|
14
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
|
+
|
|
15
81
|
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
16
82
|
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
17
83
|
if (argumentsValue == null) return '{}';
|
|
@@ -41,7 +107,8 @@ function normalizeMessages(messages) {
|
|
|
41
107
|
const systemParts = [];
|
|
42
108
|
const out = [];
|
|
43
109
|
|
|
44
|
-
for (
|
|
110
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
111
|
+
const message = source[i];
|
|
45
112
|
if (!message || typeof message !== 'object') continue;
|
|
46
113
|
if (message.role === 'system') {
|
|
47
114
|
const text = extractTextContent(message.content);
|
|
@@ -50,35 +117,45 @@ function normalizeMessages(messages) {
|
|
|
50
117
|
}
|
|
51
118
|
|
|
52
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;
|
|
53
132
|
out.push({
|
|
54
133
|
role: 'user',
|
|
55
|
-
content:
|
|
56
|
-
{
|
|
57
|
-
type: 'tool_result',
|
|
58
|
-
tool_use_id: String(message.tool_call_id || ''),
|
|
59
|
-
content: extractTextContent(message.content)
|
|
60
|
-
}
|
|
61
|
-
]
|
|
134
|
+
content: toolResults
|
|
62
135
|
});
|
|
63
136
|
continue;
|
|
64
137
|
}
|
|
65
138
|
|
|
66
|
-
const contentBlocks = [];
|
|
139
|
+
const contentBlocks = message.role === 'assistant' ? extractThinkingBlocks(message) : [];
|
|
67
140
|
const text = extractTextContent(message.content);
|
|
68
141
|
if (text) {
|
|
69
142
|
contentBlocks.push({ type: 'text', text });
|
|
70
143
|
}
|
|
71
144
|
|
|
145
|
+
const hasContentToolUse = Array.isArray(message.content)
|
|
146
|
+
&& message.content.some((block) => block?.type === 'tool_use');
|
|
72
147
|
if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
159
|
}
|
|
83
160
|
}
|
|
84
161
|
|
|
@@ -142,6 +219,10 @@ function hasTrailingToolContext(messages) {
|
|
|
142
219
|
|
|
143
220
|
function extractAssistantResult(data, messages) {
|
|
144
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);
|
|
145
226
|
const text = content
|
|
146
227
|
.filter((block) => block?.type === 'text')
|
|
147
228
|
.map((block) => block.text || '')
|
|
@@ -163,7 +244,8 @@ function extractAssistantResult(data, messages) {
|
|
|
163
244
|
toolCalls: [],
|
|
164
245
|
usage: data?.usage || null,
|
|
165
246
|
incomplete: true,
|
|
166
|
-
content
|
|
247
|
+
content,
|
|
248
|
+
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
167
249
|
};
|
|
168
250
|
}
|
|
169
251
|
throw new Error('Anthropic gateway returned empty assistant response');
|
|
@@ -173,7 +255,8 @@ function extractAssistantResult(data, messages) {
|
|
|
173
255
|
text,
|
|
174
256
|
toolCalls,
|
|
175
257
|
usage: data?.usage || null,
|
|
176
|
-
content
|
|
258
|
+
content,
|
|
259
|
+
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
177
260
|
};
|
|
178
261
|
}
|
|
179
262
|
|
|
@@ -214,7 +297,7 @@ function emptyToolCall(index) {
|
|
|
214
297
|
};
|
|
215
298
|
}
|
|
216
299
|
|
|
217
|
-
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
300
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks = []) {
|
|
218
301
|
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
219
302
|
.sort((a, b) => a[0] - b[0])
|
|
220
303
|
.map(([, tc], i) => ({
|
|
@@ -225,6 +308,10 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
|
225
308
|
.filter((tc) => tc.name);
|
|
226
309
|
const normalizedText = String(text || '').trim();
|
|
227
310
|
const content = [];
|
|
311
|
+
for (const block of thinkingBlocks) {
|
|
312
|
+
const cloned = cloneAnthropicContentBlock(block);
|
|
313
|
+
if (cloned) content.push(cloned);
|
|
314
|
+
}
|
|
228
315
|
if (text) content.push({ type: 'text', text });
|
|
229
316
|
for (const toolCall of toolCalls) {
|
|
230
317
|
content.push({
|
|
@@ -242,7 +329,8 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
|
242
329
|
toolCalls: [],
|
|
243
330
|
usage,
|
|
244
331
|
incomplete: true,
|
|
245
|
-
content: []
|
|
332
|
+
content: [],
|
|
333
|
+
assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
|
|
246
334
|
};
|
|
247
335
|
}
|
|
248
336
|
throw new Error('Anthropic gateway stream returned empty assistant response');
|
|
@@ -253,7 +341,8 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
|
253
341
|
toolCalls,
|
|
254
342
|
usage,
|
|
255
343
|
incomplete: false,
|
|
256
|
-
content
|
|
344
|
+
content,
|
|
345
|
+
assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
|
|
257
346
|
};
|
|
258
347
|
}
|
|
259
348
|
|
|
@@ -349,6 +438,7 @@ export async function createChatCompletionStream({
|
|
|
349
438
|
let text = '';
|
|
350
439
|
let usage = null;
|
|
351
440
|
const toolCallsByIndex = new Map();
|
|
441
|
+
const thinkingBlocksByIndex = new Map();
|
|
352
442
|
|
|
353
443
|
for await (const chunk of iterateSseEvents(response.body)) {
|
|
354
444
|
usage = mergeUsage(usage, chunk?.data?.usage);
|
|
@@ -366,6 +456,10 @@ export async function createChatCompletionStream({
|
|
|
366
456
|
: '';
|
|
367
457
|
current.arguments = current.arguments || initialInput;
|
|
368
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);
|
|
369
463
|
}
|
|
370
464
|
continue;
|
|
371
465
|
}
|
|
@@ -382,6 +476,20 @@ export async function createChatCompletionStream({
|
|
|
382
476
|
continue;
|
|
383
477
|
}
|
|
384
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
|
+
|
|
385
493
|
if (delta.type === 'input_json_delta') {
|
|
386
494
|
const current = toolCallsByIndex.get(index) || emptyToolCall(index);
|
|
387
495
|
current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
|
|
@@ -397,5 +505,10 @@ export async function createChatCompletionStream({
|
|
|
397
505
|
}
|
|
398
506
|
}
|
|
399
507
|
|
|
400
|
-
|
|
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);
|
|
401
514
|
}
|