diff --git a/config.example.toml b/config.example.toml
index c9cbd3b..f9a12e2 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -44,7 +44,9 @@ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTM
# 本站点配额耗尽时自动切换到其他站点的同款模型。
cross_site_failover = true
# 站点对单次回复有约 1k token 的硬上限,大参数的工具调用 JSON 会被截断。
-# 自动向上游追加「续写」轮次把 JSON 拼完整;0 = 关闭。
+# 自动向上游追加「续写」轮次把 JSON 拼完整;若最终仍不闭合,会在截断处
+# 强行补全闭合,返回内容写到哪算哪的部分工具调用,由客户端下一轮继续写。
+# 0 = 关闭续写(每次截断都直接强行补全返回)。
continue_rounds = 20
[turnstile]
diff --git a/internal/openai/continue_test.go b/internal/openai/continue_test.go
index fc385db..111952c 100644
--- a/internal/openai/continue_test.go
+++ b/internal/openai/continue_test.go
@@ -335,3 +335,43 @@ func TestTryParseToolCallsShortCloseSequence(t *testing.T) {
t.Fatalf("content = %q, want html", obj.Content)
}
}
+
+func TestTryParseToolCallsLeakedQuoteCutoff(t *testing.T) {
+ raw := `{"tool_calls": [{"id": "call_001", "type": "function", "function": {"name": "write", "arguments": "{\"filePath\": \"tihu.html\", \"content\": \"
canvas scene leak "and then cut`
+ _, calls, ok := TryParseToolCalls(raw)
+ if !ok {
+ t.Fatal("TryParseToolCalls ok = false, want force-closed tool call")
+ }
+ args := calls[0].Function.Arguments
+ if !json.Valid([]byte(args)) {
+ t.Fatalf("args invalid after force close: %s", args)
+ }
+ var inner struct {
+ FilePath string `json:"filePath"`
+ Content string `json:"content"`
+ }
+ if err := json.Unmarshal([]byte(args), &inner); err != nil {
+ t.Fatal(err)
+ }
+ if inner.FilePath != "tihu.html" {
+ t.Fatalf("filePath = %q", inner.FilePath)
+ }
+ if !strings.Contains(inner.Content, "canvas scene leak") || !strings.Contains(inner.Content, `"and then cut`) {
+ t.Fatalf("content lost pre-cut text: %q", inner.Content)
+ }
+}
+
+func TestTryParseToolCallsPlainCutoffReturnsPartial(t *testing.T) {
+ raw := `{"tool_calls": [{"id": "c", "type": "function", "function": {"name": "write", "arguments": "{\"filePath\": \"a.html\", \"content\": \"