@hunterzhu/pulse-adapters 0.4.1 → 0.4.2
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/dist/providers/normalize.js +220 -3
- package/dist/tools/filesystem.js +13 -4
- package/dist/tools/shell.js +8 -0
- package/package.json +3 -3
|
@@ -116,10 +116,31 @@ export function normalizeOpenAIResponse(response, toolNameAliases, provider = 'o
|
|
|
116
116
|
throw providerResponseError('OpenAI response must contain at least one choice');
|
|
117
117
|
const choice = providerRecord(root.choices[0], 'OpenAI choice');
|
|
118
118
|
const message = providerRecord(choice.message, 'OpenAI message');
|
|
119
|
-
const
|
|
119
|
+
const nativeToolCalls = choice.finish_reason === 'length' || message.tool_calls === undefined ? [] : normalizeOpenAIToolCalls(message.tool_calls, toolNameAliases);
|
|
120
120
|
const refusal = message.refusal == null ? undefined : requiredProviderString(message.refusal, 'OpenAI refusal');
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
let text = providerText(message.content, 'OpenAI message content');
|
|
122
|
+
let finishReason = normalizeOpenAIFinishReason(choice.finish_reason, refusal, nativeToolCalls.length > 0);
|
|
123
|
+
let toolCalls = nativeToolCalls;
|
|
124
|
+
if (provider === 'deepseek' && (finishReason === 'stop' || nativeToolCalls.length > 0)) {
|
|
125
|
+
const recovered = recoverDeepSeekDsml(text, toolNameAliases, nativeToolCalls.length === 0);
|
|
126
|
+
text = recovered.text;
|
|
127
|
+
if (nativeToolCalls.length === 0 && recovered.truncated) {
|
|
128
|
+
toolCalls = [];
|
|
129
|
+
finishReason = 'length';
|
|
130
|
+
}
|
|
131
|
+
else if (nativeToolCalls.length === 0 && recovered.calls.length > 0) {
|
|
132
|
+
toolCalls = recovered.calls.map((call, index) => ({ toolCallId: `pulse-tool-${index + 1}`, providerToolCallId: `dsml-${index + 1}`, providerToolName: call.providerToolName, providerToolArguments: call.providerToolArguments, name: call.name, input: call.input }));
|
|
133
|
+
finishReason = 'tool_calls';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (provider === 'deepseek' && nativeToolCalls.length === 0 && finishReason === 'stop' && toolCalls.length === 0) {
|
|
137
|
+
const jsonCalls = recoverDeepSeekJsonToolCalls(text, toolNameAliases);
|
|
138
|
+
if (jsonCalls !== undefined) {
|
|
139
|
+
text = '';
|
|
140
|
+
toolCalls = jsonCalls.map((call, index) => ({ toolCallId: `pulse-tool-${index + 1}`, providerToolCallId: `json-${index + 1}`, providerToolName: call.providerToolName, providerToolArguments: call.providerToolArguments, name: call.name, input: call.input }));
|
|
141
|
+
finishReason = 'tool_calls';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
123
144
|
const rawUsage = root.usage === undefined ? undefined : providerRecord(root.usage, 'OpenAI usage');
|
|
124
145
|
const promptDetails = rawUsage?.prompt_tokens_details === undefined ? undefined : providerRecord(rawUsage.prompt_tokens_details, 'OpenAI prompt token details');
|
|
125
146
|
const usage = normalizeUsage(rawUsage === undefined ? undefined : { ...rawUsage, cached_tokens: rawUsage.cached_tokens ?? promptDetails?.cached_tokens ?? rawUsage.cache_read_input_tokens, reasoning_tokens: rawUsage.completion_tokens_details?.reasoning_tokens ?? rawUsage.reasoning_tokens }, { input: 'prompt_tokens', output: 'completion_tokens', cached: 'cached_tokens', reasoning: 'reasoning_tokens' }, 'OpenAI');
|
|
@@ -127,6 +148,202 @@ export function normalizeOpenAIResponse(response, toolNameAliases, provider = 'o
|
|
|
127
148
|
const measuredUsage = usage === undefined ? undefined : { ...usage, visibleOutputChars: text.length };
|
|
128
149
|
return { text, ...(parseStructured(text) === undefined ? {} : { structured: parseStructured(text) }), toolCalls, ...(refusal === undefined ? {} : { refusal }), finishReason, ...(measuredUsage === undefined ? {} : { usage: measuredUsage }), ...(provider !== 'deepseek' || reasoningContent === undefined ? {} : { providerContinuation: { provider, reasoningContent } }) };
|
|
129
150
|
}
|
|
151
|
+
/** DeepSeek V4/V4.1 may emit DSML tool markup in content instead of tool_calls. One or more U+FF5C bars are accepted. */
|
|
152
|
+
function recoverDeepSeekDsml(text, toolNameAliases, parseCalls) {
|
|
153
|
+
const blocks = takeDsmlBlocks(text);
|
|
154
|
+
if (!blocks.changed)
|
|
155
|
+
return { text, calls: [], truncated: false };
|
|
156
|
+
if (blocks.truncated || !parseCalls)
|
|
157
|
+
return { text: blocks.text, calls: [], truncated: blocks.truncated };
|
|
158
|
+
const calls = [];
|
|
159
|
+
for (const inner of blocks.inners) {
|
|
160
|
+
const parsed = parseDsmlInvokes(inner, toolNameAliases);
|
|
161
|
+
if (parsed === undefined)
|
|
162
|
+
return { text: blocks.text, calls: [], truncated: true };
|
|
163
|
+
calls.push(...parsed);
|
|
164
|
+
}
|
|
165
|
+
return { text: blocks.text, calls, truncated: false };
|
|
166
|
+
}
|
|
167
|
+
function takeDsmlBlocks(text) {
|
|
168
|
+
const inners = [];
|
|
169
|
+
let cursor = 0;
|
|
170
|
+
let cleaned = '';
|
|
171
|
+
let changed = false;
|
|
172
|
+
while (cursor < text.length) {
|
|
173
|
+
const open = nextDsmlMatch(DSML_BLOCK_OPEN, text, cursor);
|
|
174
|
+
if (open === undefined)
|
|
175
|
+
break;
|
|
176
|
+
const close = nextDsmlMatch(DSML_BLOCK_CLOSE, text, open.end);
|
|
177
|
+
const nested = nextDsmlMatch(DSML_BLOCK_OPEN, text, open.end);
|
|
178
|
+
cleaned += text.slice(cursor, open.start);
|
|
179
|
+
changed = true;
|
|
180
|
+
if (close === undefined || (nested !== undefined && nested.start < close.start)) {
|
|
181
|
+
return { text: cleaned.trim(), inners: [], truncated: true, changed: true };
|
|
182
|
+
}
|
|
183
|
+
inners.push(text.slice(open.end, close.start));
|
|
184
|
+
cursor = close.end;
|
|
185
|
+
}
|
|
186
|
+
if (!changed)
|
|
187
|
+
return { text, inners, truncated: false, changed: false };
|
|
188
|
+
cleaned += text.slice(cursor);
|
|
189
|
+
return { text: cleaned.trim(), inners, truncated: false, changed: true };
|
|
190
|
+
}
|
|
191
|
+
function parseDsmlInvokes(inner, toolNameAliases) {
|
|
192
|
+
const opened = countDsmlTags(DSML_INVOKE_OPEN, inner);
|
|
193
|
+
if (opened !== countDsmlTags(DSML_INVOKE_CLOSE, inner))
|
|
194
|
+
return undefined;
|
|
195
|
+
const calls = [];
|
|
196
|
+
const invoke = new RegExp(DSML_INVOKE_PATTERN.source, 'g');
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = invoke.exec(inner)) !== null) {
|
|
199
|
+
const providerToolName = requiredProviderString(match[1], 'OpenAI tool name');
|
|
200
|
+
const parameters = parseDsmlParameters(match[2] ?? '');
|
|
201
|
+
if (parameters === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
calls.push({ providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input: parameters, providerToolArguments: JSON.stringify(parameters) });
|
|
204
|
+
}
|
|
205
|
+
return calls.length === opened ? calls : undefined;
|
|
206
|
+
}
|
|
207
|
+
function parseDsmlParameters(body) {
|
|
208
|
+
const opened = countDsmlTags(DSML_PARAMETER_OPEN, body);
|
|
209
|
+
if (opened !== countDsmlTags(DSML_PARAMETER_CLOSE, body))
|
|
210
|
+
return undefined;
|
|
211
|
+
const input = {};
|
|
212
|
+
const parameter = new RegExp(DSML_PARAMETER_PATTERN.source, 'g');
|
|
213
|
+
let match;
|
|
214
|
+
while ((match = parameter.exec(body)) !== null) {
|
|
215
|
+
const name = requiredProviderString(match[1], 'OpenAI tool name');
|
|
216
|
+
if (match[2] === 'true')
|
|
217
|
+
input[name] = match[3] ?? '';
|
|
218
|
+
else {
|
|
219
|
+
const parsed = parseDsmlJson(match[3] ?? '');
|
|
220
|
+
if (parsed === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
input[name] = parsed;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Object.keys(input).length === opened ? input : undefined;
|
|
226
|
+
}
|
|
227
|
+
function parseDsmlJson(value) {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(value);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** DeepSeek sometimes writes only JSON tool objects instead of tool_calls. Each object needs an explicit tool name; argument-shaped JSON and trailing prose stay text. */
|
|
236
|
+
function recoverDeepSeekJsonToolCalls(text, toolNameAliases) {
|
|
237
|
+
const leading = takeLeadingJsonValues(text.trim());
|
|
238
|
+
if (leading === undefined || leading.values.length === 0 || leading.rest.trim().length > 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
const objects = leading.values.filter((value) => !isEmptyJsonValue(value));
|
|
241
|
+
if (objects.length === 0)
|
|
242
|
+
return undefined;
|
|
243
|
+
if (objects.length === 1 && objects[0] && typeof objects[0] === 'object' && !Array.isArray(objects[0])) {
|
|
244
|
+
const batch = objects[0].tool_calls ?? objects[0].toolCalls;
|
|
245
|
+
if (Array.isArray(batch)) {
|
|
246
|
+
const calls = batch.map((item) => jsonToolCall(item, toolNameAliases));
|
|
247
|
+
return calls.every((call) => call !== undefined) && calls.length > 0 ? calls : undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const calls = objects.map((value) => jsonToolCall(value, toolNameAliases));
|
|
251
|
+
return calls.every((call) => call !== undefined) ? calls : undefined;
|
|
252
|
+
}
|
|
253
|
+
function isEmptyJsonValue(value) {
|
|
254
|
+
if (Array.isArray(value))
|
|
255
|
+
return value.length === 0;
|
|
256
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length === 0;
|
|
257
|
+
}
|
|
258
|
+
function jsonToolCall(value, toolNameAliases) {
|
|
259
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
260
|
+
return undefined;
|
|
261
|
+
const record = value;
|
|
262
|
+
const named = typeof record.tool === 'string' && record.tool.length > 0 ? record.tool : typeof record.name === 'string' && record.name.length > 0 ? record.name : undefined;
|
|
263
|
+
const providerToolName = named;
|
|
264
|
+
if (providerToolName === undefined)
|
|
265
|
+
return undefined;
|
|
266
|
+
const raw = record.arguments ?? record.input ?? record.parameters;
|
|
267
|
+
const input = raw && typeof raw === 'object' && !Array.isArray(raw) ? { ...raw } : { ...record };
|
|
268
|
+
if (raw === undefined) {
|
|
269
|
+
delete input.tool;
|
|
270
|
+
delete input.name;
|
|
271
|
+
}
|
|
272
|
+
return { providerToolName, name: toolNameAliases?.get(providerToolName) ?? providerToolName, input, providerToolArguments: JSON.stringify(input) };
|
|
273
|
+
}
|
|
274
|
+
function takeLeadingJsonValues(text) {
|
|
275
|
+
if (!text.startsWith('{') && !text.startsWith('['))
|
|
276
|
+
return undefined;
|
|
277
|
+
const values = [];
|
|
278
|
+
let cursor = 0;
|
|
279
|
+
while (cursor < text.length) {
|
|
280
|
+
while (cursor < text.length && /\s/.test(text[cursor]))
|
|
281
|
+
cursor++;
|
|
282
|
+
if (cursor >= text.length)
|
|
283
|
+
break;
|
|
284
|
+
if (text[cursor] !== '{' && text[cursor] !== '[')
|
|
285
|
+
break;
|
|
286
|
+
const end = jsonValueEnd(text, cursor);
|
|
287
|
+
if (end === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
try {
|
|
290
|
+
values.push(JSON.parse(text.slice(cursor, end)));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
cursor = end;
|
|
296
|
+
}
|
|
297
|
+
return values.length > 0 ? { values, rest: text.slice(cursor) } : undefined;
|
|
298
|
+
}
|
|
299
|
+
function jsonValueEnd(text, start) {
|
|
300
|
+
const opening = text[start];
|
|
301
|
+
if (opening !== '{' && opening !== '[')
|
|
302
|
+
return undefined;
|
|
303
|
+
const stack = [];
|
|
304
|
+
let inString = false;
|
|
305
|
+
let escaped = false;
|
|
306
|
+
for (let index = start; index < text.length; index++) {
|
|
307
|
+
const char = text[index];
|
|
308
|
+
if (inString) {
|
|
309
|
+
if (escaped)
|
|
310
|
+
escaped = false;
|
|
311
|
+
else if (char === '\\')
|
|
312
|
+
escaped = true;
|
|
313
|
+
else if (char === '"')
|
|
314
|
+
inString = false;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === '"') {
|
|
318
|
+
inString = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (char === '{' || char === '[')
|
|
322
|
+
stack.push(char === '{' ? '}' : ']');
|
|
323
|
+
else if (char === '}' || char === ']') {
|
|
324
|
+
if (stack.pop() !== char)
|
|
325
|
+
return undefined;
|
|
326
|
+
if (stack.length === 0)
|
|
327
|
+
return index + 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
function countDsmlTags(pattern, value) {
|
|
333
|
+
return value.match(new RegExp(pattern.source, 'g'))?.length ?? 0;
|
|
334
|
+
}
|
|
335
|
+
function nextDsmlMatch(pattern, value, from) {
|
|
336
|
+
const match = new RegExp(pattern.source, 'g').exec(value.slice(from));
|
|
337
|
+
return match === null ? undefined : { start: from + match.index, end: from + match.index + match[0].length };
|
|
338
|
+
}
|
|
339
|
+
const DSML_BLOCK_OPEN = /<\uFF5C+DSML\uFF5C+\s*(?:tool_calls|calls)>/;
|
|
340
|
+
const DSML_BLOCK_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*(?:tool_calls|calls)>/;
|
|
341
|
+
const DSML_INVOKE_OPEN = /<\uFF5C+DSML\uFF5C+\s*invoke\b/;
|
|
342
|
+
const DSML_INVOKE_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*invoke>/;
|
|
343
|
+
const DSML_INVOKE_PATTERN = /<\uFF5C+DSML\uFF5C+\s*invoke\s+name="([^"]+)"\s*\/?>([\s\S]*?)<\/\uFF5C+DSML\uFF5C+\s*invoke>/;
|
|
344
|
+
const DSML_PARAMETER_OPEN = /<\uFF5C+DSML\uFF5C+\s*parameter\b/;
|
|
345
|
+
const DSML_PARAMETER_CLOSE = /<\/\uFF5C+DSML\uFF5C+\s*parameter>/;
|
|
346
|
+
const DSML_PARAMETER_PATTERN = /<\uFF5C+DSML\uFF5C+\s*parameter\s+name="([^"]+)"\s+string="(true|false)"\s*>([\s\S]*?)<\/\uFF5C+DSML\uFF5C+\s*parameter>/;
|
|
130
347
|
export function normalizeAnthropicResponse(response) {
|
|
131
348
|
const root = providerRecord(response, 'Anthropic response');
|
|
132
349
|
if (!Array.isArray(root.content))
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -31,8 +31,14 @@ export class FilesystemTool {
|
|
|
31
31
|
this.root = root;
|
|
32
32
|
this.lockTimeoutMs = lockTimeoutMs;
|
|
33
33
|
}
|
|
34
|
-
safe(path) {
|
|
35
|
-
|
|
34
|
+
safe(path) {
|
|
35
|
+
const root = resolve(this.root);
|
|
36
|
+
const target = resolve(root, path);
|
|
37
|
+
const within = relative(root, target);
|
|
38
|
+
if (within.startsWith('..') || isAbsolute(within))
|
|
39
|
+
throw filesystemError('PATH_OUTSIDE_SANDBOX');
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
36
42
|
async existing(path) {
|
|
37
43
|
const target = this.safe(path);
|
|
38
44
|
const [root, resolved] = await Promise.all([realpath(this.root), realpath(target)]);
|
|
@@ -122,8 +128,11 @@ export class FilesystemTool {
|
|
|
122
128
|
await handle.close();
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
|
-
async list(path = '.', signal) {
|
|
126
|
-
|
|
131
|
+
async list(path = '.', signal) {
|
|
132
|
+
if (signal?.aborted)
|
|
133
|
+
throw filesystemError('ABORTED');
|
|
134
|
+
return readdir(await this.existing(path));
|
|
135
|
+
}
|
|
127
136
|
async write(path, content, signal) { if (signal?.aborted)
|
|
128
137
|
throw filesystemError('ABORTED'); await writeFile(await this.writable(path), content, 'utf8'); }
|
|
129
138
|
async move(source, destination, expectedHash, signal) {
|
package/dist/tools/shell.js
CHANGED
|
@@ -161,6 +161,12 @@ function shellEnvironment(env) {
|
|
|
161
161
|
const source = env ?? process.env;
|
|
162
162
|
return Object.fromEntries(Object.entries(source).filter(([key]) => !/(API_KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY|AUTHORIZATION|BEARER|CREDENTIAL|COOKIE)/i.test(key) && key !== 'NODE_OPTIONS'));
|
|
163
163
|
}
|
|
164
|
+
function shellLaunchesCommand(command, args) {
|
|
165
|
+
const base = basename(command).toLowerCase();
|
|
166
|
+
if (!['sh', 'bash', 'zsh', 'dash', 'fish', 'ksh', 'cmd', 'cmd.exe', 'powershell', 'powershell.exe', 'pwsh', 'pwsh.exe'].includes(base))
|
|
167
|
+
return false;
|
|
168
|
+
return args.some((arg) => /^(?:-lc|-c|-command|\/c)$/i.test(arg));
|
|
169
|
+
}
|
|
164
170
|
export function runShell(command, args = [], options = {}) {
|
|
165
171
|
const max = options.maxOutputBytes ?? 256 * 1024;
|
|
166
172
|
if (!Number.isFinite(max) || max < 0)
|
|
@@ -169,6 +175,8 @@ export function runShell(command, args = [], options = {}) {
|
|
|
169
175
|
return Promise.reject(shellError('INVALID_SHELL_TIMEOUT'));
|
|
170
176
|
if (!command || typeof command !== 'string' || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string'))
|
|
171
177
|
return Promise.reject(shellError('INVALID_SHELL_ARGUMENT'));
|
|
178
|
+
if (/&&|\|\||[|&;<>\n`]|\$\(/.test(command) || shellLaunchesCommand(command, args))
|
|
179
|
+
return Promise.reject(Object.assign(shellError('SHELL_SYNTAX_UNSUPPORTED'), { message: 'shell.exec does not start a shell. Pass the executable in command and each argument in args. Do not put &&, pipes, redirects, command substitution, or sh -c in command.' }));
|
|
172
180
|
try {
|
|
173
181
|
encodeSandboxCommand(command, args);
|
|
174
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
21
|
-
"@hunterzhu/pulse-runtime": "0.4.
|
|
22
|
-
"@hunterzhu/pulse-tool-sdk": "0.4.
|
|
21
|
+
"@hunterzhu/pulse-runtime": "0.4.2",
|
|
22
|
+
"@hunterzhu/pulse-tool-sdk": "0.4.2",
|
|
23
23
|
"exceljs": "4.4.0",
|
|
24
24
|
"pdfjs-dist": "4.10.38"
|
|
25
25
|
}
|