diff --git a/README.MD b/README.MD
index 412596e..94fa884 100644
--- a/README.MD
+++ b/README.MD
@@ -4,11 +4,14 @@
# DS2API
+
+
[](LICENSE)


[](https://github.com/CJackHwang/ds2api/releases)
[](docs/DEPLOY.md)
+
[](https://zeabur.com/templates/L4CFHP)
[](https://vercel.com/new/clone?repository-url=https://github.com/CJackHwang/ds2api)
@@ -317,9 +320,9 @@ Gemini 路由还可以使用 `x-goog-api-key`,或在没有认证头时使用 `
4. `responses` 支持并执行 `tool_choice`(`auto`/`none`/`required`/强制函数);`required` 违规时非流式返回 `422`,流式返回 `response.failed`
5. 客户端请求哪种协议,就按该协议返回工具调用(OpenAI/Claude/Gemini 各自原生结构);模型侧优先约束输出规范 XML,再由兼容层转译
-> 说明:当前版本在 parser 层仍以“尽量解析成功”为优先,未启用基于 allow-list 的工具名硬拒绝。
+> 说明:当前版本 parser 层以”尽量解析成功”为优先,所有格式合法的 XML 工具调用都会通过,不做工具名 allow-list 过滤。
>
-> 想评估“把工具调用封装成 XML 再输入模型”的方案,可参考:`docs/toolcall-semantics.md`。
+> 想评估”把工具调用封装成 XML 再输入模型”的方案,可参考:`docs/toolcall-semantics.md`。
## 本地开发抓包工具
diff --git a/README.en.md b/README.en.md
index 747993d..d267b82 100644
--- a/README.en.md
+++ b/README.en.md
@@ -4,6 +4,8 @@
# DS2API
+
+
[](LICENSE)


diff --git a/VERSION b/VERSION
index fcdb2e1..1454f6e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-4.0.0
+4.0.1
diff --git a/internal/toolstream/tool_sieve_core.go b/internal/toolstream/tool_sieve_core.go
index 2ec0914..3f77b8e 100644
--- a/internal/toolstream/tool_sieve_core.go
+++ b/internal/toolstream/tool_sieve_core.go
@@ -193,5 +193,8 @@ func consumeToolCapture(state *State, toolNames []string) (prefix string, calls
if hasOpenXMLToolTag(captured) {
return "", nil, "", false
}
- return "", nil, "", false
+ if shouldKeepBareInvokeCapture(captured) {
+ return "", nil, "", false
+ }
+ return captured, nil, "", true
}
diff --git a/internal/toolstream/tool_sieve_xml.go b/internal/toolstream/tool_sieve_xml.go
index 72cbbaa..b963981 100644
--- a/internal/toolstream/tool_sieve_xml.go
+++ b/internal/toolstream/tool_sieve_xml.go
@@ -90,6 +90,38 @@ func hasOpenXMLToolTag(captured string) bool {
return false
}
+func shouldKeepBareInvokeCapture(captured string) bool {
+ lower := strings.ToLower(captured)
+ invokeIdx := strings.Index(lower, "", invokeIdx) > invokeIdx {
+ return true
+ }
+
+ startEnd := findXMLTagEnd(captured, invokeIdx+len("", startEnd+1)
+ if invokeCloseIdx >= 0 {
+ afterClose := captured[invokeCloseIdx+len(""):]
+ return strings.TrimSpace(afterClose) == ""
+ }
+
+ trimmedLower := strings.ToLower(trimmedBody)
+ return strings.HasPrefix(trimmedLower, "' {
+ return i
+ }
+ }
+ return -1
+}
+
// findPartialXMLToolTagStart checks if the string ends with a partial canonical
// XML wrapper tag (e.g., "` as plain documentation text."
+ events := ProcessChunk(&state, chunk, []string{"read_file"})
+
+ var textContent strings.Builder
+ toolCalls := 0
+ for _, evt := range events {
+ textContent.WriteString(evt.Content)
+ toolCalls += len(evt.ToolCalls)
+ }
+
+ if toolCalls != 0 {
+ t.Fatalf("expected inline invoke prose to remain text, got %d events=%#v", toolCalls, events)
+ }
+ if textContent.String() != chunk {
+ t.Fatalf("expected inline invoke prose to stream immediately, got %q", textContent.String())
+ }
+ if state.capturing {
+ t.Fatal("expected inline invoke prose not to leave stream capture open")
+ }
+}
+
+func TestProcessToolSieveBareInvokeExampleReleasesWhenNotRepairable(t *testing.T) {
+ var state State
+ chunks := []string{
+ `Example: README.md`,
+ " then continue.",
+ }
+ var events []Event
+ for _, c := range chunks {
+ events = append(events, ProcessChunk(&state, c, []string{"read_file"})...)
+ }
+
+ var textContent strings.Builder
+ toolCalls := 0
+ for _, evt := range events {
+ textContent.WriteString(evt.Content)
+ toolCalls += len(evt.ToolCalls)
+ }
+
+ if toolCalls != 0 {
+ t.Fatalf("expected non-repairable bare invoke to remain text, got %d events=%#v", toolCalls, events)
+ }
+ if textContent.String() != strings.Join(chunks, "") {
+ t.Fatalf("expected non-repairable bare invoke to pass through, got %q", textContent.String())
+ }
+ if state.capturing {
+ t.Fatal("expected non-repairable bare invoke not to leave stream capture open")
+ }
+}
+
func TestProcessToolSieveRepairsMissingOpeningWrapperWithoutLeakingInvokeText(t *testing.T) {
var state State
chunks := []string{