mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-14 13:15:07 +08:00
fix: align tool call protocol and thinking controls
This commit is contained in:
@@ -8,7 +8,7 @@ const {
|
||||
stripFencedCodeBlocks,
|
||||
} = require('./parse_payload');
|
||||
|
||||
const TOOL_MARKUP_PREFIXES = ['<tools', '<tool_call'];
|
||||
const TOOL_MARKUP_PREFIXES = ['<tool_calls'];
|
||||
|
||||
function extractToolNames(tools) {
|
||||
if (!Array.isArray(tools) || tools.length === 0) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const TOOLS_WRAPPER_PATTERN = /<tools\b[^>]*>([\s\S]*?)<\/tools>/gi;
|
||||
const TOOL_CALL_MARKUP_BLOCK_PATTERN = /<(?:[a-z0-9_:-]+:)?tool_call\b[^>]*>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?tool_call>/gi;
|
||||
const TOOL_CALL_CANONICAL_BODY_PATTERN = /^\s*<(?:[a-z0-9_:-]+:)?tool_name\b[^>]*>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?tool_name>\s*<(?:[a-z0-9_:-]+:)?param\b[^>]*>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?param>\s*$/i;
|
||||
const TOOLS_WRAPPER_PATTERN = /<tool_calls\b[^>]*>([\s\S]*?)<\/tool_calls>/gi;
|
||||
const TOOL_CALL_MARKUP_BLOCK_PATTERN = /<(?:[a-z0-9_:-]+:)?invoke\b([^>]*)>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?invoke>/gi;
|
||||
const PARAMETER_BLOCK_PATTERN = /<(?:[a-z0-9_:-]+:)?parameter\b([^>]*)>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?parameter>/gi;
|
||||
const TOOL_CALL_MARKUP_KV_PATTERN = /<(?:[a-z0-9_:-]+:)?([a-z0-9_.-]+)\b[^>]*>([\s\S]*?)<\/(?:[a-z0-9_:-]+:)?\1>/gi;
|
||||
const CDATA_PATTERN = /^<!\[CDATA\[([\s\S]*?)]]>$/i;
|
||||
const XML_ATTR_PATTERN = /\b([a-z0-9_:-]+)\s*=\s*("([^"]*)"|'([^']*)')/gi;
|
||||
|
||||
const {
|
||||
toStringSafe,
|
||||
@@ -27,7 +28,7 @@ function parseMarkupToolCalls(text) {
|
||||
for (const wrapper of raw.matchAll(TOOLS_WRAPPER_PATTERN)) {
|
||||
const body = toStringSafe(wrapper[1]);
|
||||
for (const block of body.matchAll(TOOL_CALL_MARKUP_BLOCK_PATTERN)) {
|
||||
const parsed = parseMarkupSingleToolCall(toStringSafe(block[1]).trim());
|
||||
const parsed = parseMarkupSingleToolCall(block);
|
||||
if (parsed) {
|
||||
out.push(parsed);
|
||||
}
|
||||
@@ -36,33 +37,43 @@ function parseMarkupToolCalls(text) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseMarkupSingleToolCall(inner) {
|
||||
// Try inline JSON parse for the inner content.
|
||||
function parseMarkupSingleToolCall(block) {
|
||||
const attrs = parseTagAttributes(block[1]);
|
||||
const name = toStringSafe(attrs.name).trim();
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
const inner = toStringSafe(block[2]).trim();
|
||||
|
||||
if (inner) {
|
||||
try {
|
||||
const decoded = JSON.parse(inner);
|
||||
if (decoded && typeof decoded === 'object' && !Array.isArray(decoded) && decoded.name) {
|
||||
if (decoded && typeof decoded === 'object' && !Array.isArray(decoded)) {
|
||||
return {
|
||||
name: toStringSafe(decoded.name),
|
||||
input: decoded.input && typeof decoded.input === 'object' && !Array.isArray(decoded.input) ? decoded.input : {},
|
||||
name,
|
||||
input: decoded.input && typeof decoded.input === 'object' && !Array.isArray(decoded.input)
|
||||
? decoded.input
|
||||
: decoded.parameters && typeof decoded.parameters === 'object' && !Array.isArray(decoded.parameters)
|
||||
? decoded.parameters
|
||||
: {},
|
||||
};
|
||||
}
|
||||
} catch (_err) {
|
||||
// Not JSON, continue with markup parsing.
|
||||
}
|
||||
}
|
||||
|
||||
const match = inner.match(TOOL_CALL_CANONICAL_BODY_PATTERN);
|
||||
if (!match || match.length < 3) {
|
||||
const input = {};
|
||||
for (const match of inner.matchAll(PARAMETER_BLOCK_PATTERN)) {
|
||||
const parameterAttrs = parseTagAttributes(match[1]);
|
||||
const paramName = toStringSafe(parameterAttrs.name).trim();
|
||||
if (!paramName) {
|
||||
continue;
|
||||
}
|
||||
appendMarkupValue(input, paramName, parseMarkupValue(match[2]));
|
||||
}
|
||||
if (Object.keys(input).length === 0 && inner.trim() !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = extractRawTagValue(match[1]).trim();
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const input = parseMarkupInput(match[2]);
|
||||
return { name, input };
|
||||
}
|
||||
|
||||
@@ -124,11 +135,14 @@ function parseMarkupValue(raw) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch (_err) {
|
||||
return s;
|
||||
if (s.startsWith('{') || s.startsWith('[')) {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch (_err) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractRawTagValue(inner) {
|
||||
@@ -158,6 +172,22 @@ function unescapeHtml(safe) {
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function parseTagAttributes(raw) {
|
||||
const source = toStringSafe(raw);
|
||||
const out = {};
|
||||
if (!source) {
|
||||
return out;
|
||||
}
|
||||
for (const match of source.matchAll(XML_ATTR_PATTERN)) {
|
||||
const key = toStringSafe(match[1]).trim().toLowerCase();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
out[key] = match[3] || match[4] || '';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseToolCallInput(v) {
|
||||
if (v == null) {
|
||||
return {};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
'use strict';
|
||||
const { parseToolCalls } = require('./parse');
|
||||
|
||||
// Tag pairs ordered longest-first: wrapper tags checked before inner tags.
|
||||
// XML wrapper tag pair used by the streaming sieve.
|
||||
const XML_TOOL_TAG_PAIRS = [
|
||||
{ open: '<tools', close: '</tools>' },
|
||||
{ open: '<tool_call', close: '</tool_call>' },
|
||||
{ open: '<tool_calls', close: '</tool_calls>' },
|
||||
];
|
||||
|
||||
const XML_TOOL_OPENING_TAGS = XML_TOOL_TAG_PAIRS.map(p => p.open);
|
||||
|
||||
function consumeXMLToolCapture(captured, toolNames, trimWrappingJSONFence) {
|
||||
const lower = captured.toLowerCase();
|
||||
// Find the FIRST matching open/close pair, preferring wrapper tags.
|
||||
// Find the FIRST matching open/close pair for the canonical wrapper.
|
||||
for (const pair of XML_TOOL_TAG_PAIRS) {
|
||||
const openIdx = lower.indexOf(pair.open);
|
||||
if (openIdx < 0) {
|
||||
@@ -21,7 +20,7 @@ function consumeXMLToolCapture(captured, toolNames, trimWrappingJSONFence) {
|
||||
const closeIdx = lower.lastIndexOf(pair.close);
|
||||
if (closeIdx < openIdx) {
|
||||
// Opening tag present but specific closing tag hasn't arrived.
|
||||
// Return not-ready — do NOT fall through to inner pairs.
|
||||
// Return not-ready so buffering continues until the wrapper closes.
|
||||
return { ready: false, prefix: '', calls: [], suffix: '' };
|
||||
}
|
||||
const closeEnd = closeIdx + pair.close.length;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const XML_TOOL_SEGMENT_TAGS = [
|
||||
'<tools>', '<tools\n', '<tools ', '<tool_call>', '<tool_call\n', '<tool_call ',
|
||||
'<tool_calls>', '<tool_calls\n', '<tool_calls ',
|
||||
];
|
||||
|
||||
const XML_TOOL_OPENING_TAGS = [
|
||||
'<tools', '<tool_call',
|
||||
'<tool_calls',
|
||||
];
|
||||
|
||||
const XML_TOOL_CLOSING_TAGS = [
|
||||
'</tools>', '</tool_call>',
|
||||
'</tool_calls>',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user