mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 08:25:26 +08:00
feat: suppress output of partial XML tool tag fragments in stream processing
This commit is contained in:
@@ -130,8 +130,14 @@ func flushToolSieve(state *toolStreamSieveState, toolNames []string) []toolStrea
|
||||
}
|
||||
if state.pending.Len() > 0 {
|
||||
content := state.pending.String()
|
||||
state.noteText(content)
|
||||
events = append(events, toolStreamEvent{Content: content})
|
||||
// Safety: if pending contains XML tool tag fragments (e.g. "tool_calls>"
|
||||
// from a split closing tag), swallow them instead of leaking.
|
||||
if hasOpenXMLToolTag(content) || looksLikeXMLToolTagFragment(content) {
|
||||
// Drop it — likely an incomplete tool call fragment.
|
||||
} else {
|
||||
state.noteText(content)
|
||||
events = append(events, toolStreamEvent{Content: content})
|
||||
}
|
||||
state.pending.Reset()
|
||||
}
|
||||
return events
|
||||
|
||||
@@ -104,3 +104,27 @@ func findPartialXMLToolTagStart(s string) int {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// looksLikeXMLToolTagFragment returns true if s looks like a fragment from a
|
||||
// split XML tool call tag — for example "tool_calls>" or "/tool_call>\n".
|
||||
// These fragments arise when '<' was consumed separately and the tail remains.
|
||||
func looksLikeXMLToolTagFragment(s string) bool {
|
||||
trimmed := strings.TrimSpace(s)
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(trimmed)
|
||||
// Check for closing tag tails like "tool_calls>" or "/tool_calls>"
|
||||
fragments := []string{
|
||||
"tool_calls>", "tool_call>", "/tool_calls>", "/tool_call>",
|
||||
"function_calls>", "function_call>", "/function_calls>", "/function_call>",
|
||||
"invoke>", "/invoke>", "tool_use>", "/tool_use>",
|
||||
"tool_name>", "/tool_name>", "parameters>", "/parameters>",
|
||||
}
|
||||
for _, f := range fragments {
|
||||
if strings.Contains(lower, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -79,8 +79,22 @@ function findPartialXMLToolTagStart(s) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
function looksLikeXMLToolTagFragment(s) {
|
||||
const trimmed = (s || '').trim();
|
||||
if (!trimmed) return false;
|
||||
const lower = trimmed.toLowerCase();
|
||||
const fragments = [
|
||||
'tool_calls>', 'tool_call>', '/tool_calls>', '/tool_call>',
|
||||
'function_calls>', 'function_call>', '/function_calls>', '/function_call>',
|
||||
'invoke>', '/invoke>', 'tool_use>', '/tool_use>',
|
||||
'tool_name>', '/tool_name>', 'parameters>', '/parameters>',
|
||||
];
|
||||
return fragments.some(f => lower.includes(f));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
consumeXMLToolCapture,
|
||||
hasOpenXMLToolTag,
|
||||
findPartialXMLToolTagStart,
|
||||
looksLikeXMLToolTagFragment,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
consumeXMLToolCapture: consumeXMLToolCaptureImpl,
|
||||
hasOpenXMLToolTag,
|
||||
findPartialXMLToolTagStart,
|
||||
looksLikeXMLToolTagFragment,
|
||||
} = require('./sieve-xml');
|
||||
function processToolSieveChunk(state, chunk, toolNames) {
|
||||
if (!state) {
|
||||
@@ -123,8 +124,10 @@ function flushToolSieve(state, toolNames) {
|
||||
resetIncrementalToolState(state);
|
||||
}
|
||||
if (state.pending) {
|
||||
noteText(state, state.pending);
|
||||
events.push({ type: 'text', text: state.pending });
|
||||
if (!hasOpenXMLToolTag(state.pending) && !looksLikeXMLToolTagFragment(state.pending)) {
|
||||
noteText(state, state.pending);
|
||||
events.push({ type: 'text', text: state.pending });
|
||||
}
|
||||
state.pending = '';
|
||||
}
|
||||
return events;
|
||||
|
||||
Reference in New Issue
Block a user