mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-20 07:57:43 +08:00
fix: preserve partial-update fields for current_input_file and thinking_injection, expand DSML space-separator aliases
- Guard current_input_file.enabled / thinking_injection.{enabled,prompt} with hasNestedSettingsKey so partial updates don't overwrite omitted fields
- Expand DSML alias support to tolerate space-separated tags (e.g. <|dsml invoke>) alongside pipe-separated forms
- Sync Go sieve, Node sieve, toolcall parser, and tests for all new DSML variants
- Update API.md and toolcall-semantics.md with expanded alias coverage
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -554,3 +554,64 @@ func TestSieve_ChineseReviewSamplePreservesInlineDSMLMention(t *testing.T) {
|
||||
t.Fatalf("真实工具块不应泄漏到正文, got %q", text.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSieve_ToleratesDSMLSpaceSeparatorTypo(t *testing.T) {
|
||||
var state State
|
||||
chunks := []string{
|
||||
"准备读取文件。\n",
|
||||
"<|DSML tool_calls>\n",
|
||||
"<|DSML invoke name=\"Read\">\n",
|
||||
"<|DSML parameter name=\"file_path\"><![CDATA[/tmp/input.txt]]></|DSML parameter>\n",
|
||||
"</|DSML invoke>\n",
|
||||
"</|DSML tool_calls>",
|
||||
}
|
||||
var events []Event
|
||||
for _, c := range chunks {
|
||||
events = append(events, ProcessChunk(&state, c, []string{"Read"})...)
|
||||
}
|
||||
events = append(events, Flush(&state, []string{"Read"})...)
|
||||
|
||||
var text strings.Builder
|
||||
var filePath string
|
||||
callCount := 0
|
||||
for _, e := range events {
|
||||
text.WriteString(e.Content)
|
||||
for _, call := range e.ToolCalls {
|
||||
callCount++
|
||||
filePath, _ = call.Input["file_path"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if callCount != 1 {
|
||||
t.Fatalf("应解析出 1 个工具调用,got %d, text=%q", callCount, text.String())
|
||||
}
|
||||
if filePath != "/tmp/input.txt" {
|
||||
t.Fatalf("应解析出 file_path,got %q", filePath)
|
||||
}
|
||||
if !strings.Contains(text.String(), "准备读取文件") {
|
||||
t.Fatalf("前置正文应保留, got %q", text.String())
|
||||
}
|
||||
if strings.Contains(text.String(), "<|DSML invoke") {
|
||||
t.Fatalf("真实工具块不应泄漏到正文, got %q", text.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSieve_DSMLSpaceLookalikeTagNameStaysText(t *testing.T) {
|
||||
var state State
|
||||
input := "<|DSML tool_calls_extra><|DSML invoke name=\"Read\"><|DSML parameter name=\"file_path\">/tmp/input.txt</|DSML parameter></|DSML invoke></|DSML tool_calls_extra>"
|
||||
events := ProcessChunk(&state, input, []string{"Read"})
|
||||
events = append(events, Flush(&state, []string{"Read"})...)
|
||||
|
||||
var text strings.Builder
|
||||
callCount := 0
|
||||
for _, e := range events {
|
||||
text.WriteString(e.Content)
|
||||
callCount += len(e.ToolCalls)
|
||||
}
|
||||
if callCount != 0 {
|
||||
t.Fatalf("相似标签名不应触发工具调用,got %d", callCount)
|
||||
}
|
||||
if text.String() != input {
|
||||
t.Fatalf("相似标签名应作为正文透传, got %q", text.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +99,10 @@ func consumeXMLToolCapture(captured string, toolNames []string) (prefix string,
|
||||
// hasOpenXMLToolTag returns true if captured text contains an XML tool opening tag
|
||||
// whose SPECIFIC closing tag has not appeared yet.
|
||||
func hasOpenXMLToolTag(captured string) bool {
|
||||
lower := strings.ToLower(captured)
|
||||
for _, pair := range xmlToolCallTagPairs {
|
||||
openIdx := strings.Index(lower, pair.open)
|
||||
openIdx := findXMLOpenOutsideCDATA(captured, pair.open, 0)
|
||||
if openIdx >= 0 {
|
||||
if findXMLCloseOutsideCDATA(captured, pair.close, openIdx+len(pair.open)) < 0 {
|
||||
if findMatchingXMLToolWrapperClose(captured, pair.open, pair.close, openIdx) < 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -117,17 +116,25 @@ func shouldKeepBareInvokeCapture(captured string) bool {
|
||||
if invokeIdx < 0 || containsAnyToolCallWrapper(lower) {
|
||||
return false
|
||||
}
|
||||
wrapperClose := "</tool_calls>"
|
||||
invokeOpenLen := len("<invoke")
|
||||
invokeClose := "</invoke>"
|
||||
parameterOpen := "<parameter"
|
||||
if dsml {
|
||||
wrapperClose = "</|dsml|tool_calls>"
|
||||
invokeOpenLen = len("<|dsml|invoke")
|
||||
invokeClose = "</|dsml|invoke>"
|
||||
parameterOpen = "<|dsml|parameter"
|
||||
}
|
||||
if findXMLCloseOutsideCDATA(captured, wrapperClose, invokeIdx) > invokeIdx {
|
||||
if dsml && strings.HasPrefix(lower[invokeIdx:], "<|dsml invoke") {
|
||||
invokeOpenLen = len("<|dsml invoke")
|
||||
parameterOpen = "<|dsml parameter"
|
||||
}
|
||||
if dsml && strings.HasPrefix(lower[invokeIdx:], "<dsml|invoke") {
|
||||
invokeOpenLen = len("<dsml|invoke")
|
||||
parameterOpen = "<dsml|parameter"
|
||||
}
|
||||
if dsml && strings.HasPrefix(lower[invokeIdx:], "<dsml invoke") {
|
||||
invokeOpenLen = len("<dsml invoke")
|
||||
parameterOpen = "<dsml parameter"
|
||||
}
|
||||
if findAnyXMLCloseOutsideCDATA(captured, possibleWrapperCloseTags(dsml), invokeIdx) > invokeIdx {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -141,9 +148,15 @@ func shouldKeepBareInvokeCapture(captured string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
invokeCloseIdx := findXMLCloseOutsideCDATA(captured, invokeClose, startEnd+1)
|
||||
invokeCloseIdx := findAnyXMLCloseOutsideCDATA(captured, possibleInvokeCloseTags(dsml), startEnd+1)
|
||||
if invokeCloseIdx >= 0 {
|
||||
afterClose := captured[invokeCloseIdx+len(invokeClose):]
|
||||
afterClose := captured[invokeCloseIdx:]
|
||||
for _, closeTag := range possibleInvokeCloseTags(dsml) {
|
||||
if strings.HasPrefix(strings.ToLower(afterClose), closeTag) {
|
||||
afterClose = afterClose[len(closeTag):]
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(afterClose) == ""
|
||||
}
|
||||
|
||||
@@ -156,15 +169,42 @@ func shouldKeepBareInvokeCapture(captured string) bool {
|
||||
func containsAnyToolCallWrapper(lower string) bool {
|
||||
return strings.Contains(lower, "<tool_calls") ||
|
||||
strings.Contains(lower, "<|dsml|tool_calls") ||
|
||||
strings.Contains(lower, "<|dsml tool_calls") ||
|
||||
strings.Contains(lower, "<dsml|tool_calls") ||
|
||||
strings.Contains(lower, "<dsml tool_calls") ||
|
||||
strings.Contains(lower, "<|tool_calls") ||
|
||||
strings.Contains(lower, "<|tool_calls")
|
||||
}
|
||||
|
||||
func possibleWrapperCloseTags(dsml bool) []string {
|
||||
if !dsml {
|
||||
return []string{"</tool_calls>"}
|
||||
}
|
||||
return []string{"</|dsml|tool_calls>", "</|dsml tool_calls>", "</dsml|tool_calls>", "</dsml tool_calls>", "</|tool_calls>", "</|tool_calls>"}
|
||||
}
|
||||
|
||||
func possibleInvokeCloseTags(dsml bool) []string {
|
||||
if !dsml {
|
||||
return []string{"</invoke>"}
|
||||
}
|
||||
return []string{"</|dsml|invoke>", "</|dsml invoke>", "</dsml|invoke>", "</dsml invoke>", "</|invoke>", "</|invoke>"}
|
||||
}
|
||||
|
||||
func findAnyXMLCloseOutsideCDATA(s string, closeTags []string, start int) int {
|
||||
best := -1
|
||||
for _, closeTag := range closeTags {
|
||||
idx := findXMLCloseOutsideCDATA(s, closeTag, start)
|
||||
if idx >= 0 && (best < 0 || idx < best) {
|
||||
best = idx
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func firstInvokeIndex(lower string) (int, bool) {
|
||||
xmlIdx := strings.Index(lower, "<invoke")
|
||||
// Check all DSML-like invoke prefixes.
|
||||
dsmlPrefixes := []string{"<|dsml|invoke", "<dsml|invoke", "<|invoke", "<|invoke"}
|
||||
dsmlPrefixes := []string{"<|dsml|invoke", "<|dsml invoke", "<dsml|invoke", "<dsml invoke", "<|invoke", "<|invoke"}
|
||||
dsmlIdx := -1
|
||||
for _, prefix := range dsmlPrefixes {
|
||||
idx := strings.Index(lower, prefix)
|
||||
|
||||
@@ -5,11 +5,13 @@ import "regexp"
|
||||
// --- XML tool call support for the streaming sieve ---
|
||||
|
||||
//nolint:unused // kept as explicit tag inventory for future XML sieve refinements.
|
||||
var xmlToolCallClosingTags = []string{"</tool_calls>", "</|dsml|tool_calls>", "</dsml|tool_calls>", "</|tool_calls>", "</|tool_calls>"}
|
||||
var xmlToolCallClosingTags = []string{"</tool_calls>", "</|dsml|tool_calls>", "</|dsml tool_calls>", "</dsml|tool_calls>", "</dsml tool_calls>", "</|tool_calls>", "</|tool_calls>"}
|
||||
var xmlToolCallOpeningTags = []string{
|
||||
"<tool_calls", "<invoke",
|
||||
"<|dsml|tool_calls", "<|dsml|invoke",
|
||||
"<|dsml tool_calls", "<|dsml invoke",
|
||||
"<dsml|tool_calls", "<dsml|invoke",
|
||||
"<dsml tool_calls", "<dsml invoke",
|
||||
"<|tool_calls", "<|invoke",
|
||||
"<|tool_calls", "<|invoke",
|
||||
}
|
||||
@@ -18,7 +20,9 @@ var xmlToolCallOpeningTags = []string{
|
||||
// Order matters: longer/wrapper tags must be checked first.
|
||||
var xmlToolCallTagPairs = []struct{ open, close string }{
|
||||
{"<|dsml|tool_calls", "</|dsml|tool_calls>"},
|
||||
{"<|dsml tool_calls", "</|dsml tool_calls>"},
|
||||
{"<dsml|tool_calls", "</dsml|tool_calls>"},
|
||||
{"<dsml tool_calls", "</dsml tool_calls>"},
|
||||
{"<|tool_calls", "</|tool_calls>"},
|
||||
{"<|tool_calls", "</|tool_calls>"},
|
||||
{"<tool_calls", "</tool_calls>"},
|
||||
@@ -33,8 +37,12 @@ var xmlToolCallBlockPattern = regexp.MustCompile(`(?is)((?:<tool_calls\b|<\|dsml
|
||||
var xmlToolTagsToDetect = []string{
|
||||
"<|dsml|tool_calls>", "<|dsml|tool_calls\n", "<|dsml|tool_calls ",
|
||||
"<|dsml|invoke ", "<|dsml|invoke\n", "<|dsml|invoke\t", "<|dsml|invoke\r",
|
||||
"<|dsml tool_calls>", "<|dsml tool_calls\n", "<|dsml tool_calls ",
|
||||
"<|dsml invoke ", "<|dsml invoke\n", "<|dsml invoke\t", "<|dsml invoke\r",
|
||||
"<dsml|tool_calls>", "<dsml|tool_calls\n", "<dsml|tool_calls ",
|
||||
"<dsml|invoke ", "<dsml|invoke\n", "<dsml|invoke\t", "<dsml|invoke\r",
|
||||
"<dsml tool_calls>", "<dsml tool_calls\n", "<dsml tool_calls ",
|
||||
"<dsml invoke ", "<dsml invoke\n", "<dsml invoke\t", "<dsml invoke\r",
|
||||
"<|tool_calls>", "<|tool_calls\n", "<|tool_calls ",
|
||||
"<|invoke ", "<|invoke\n", "<|invoke\t", "<|invoke\r",
|
||||
"<|tool_calls>", "<|tool_calls\n", "<|tool_calls ",
|
||||
|
||||
Reference in New Issue
Block a user