From c353f7bd3f8eef0b0cb2a33ab5aafb87f2b17eb1 Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 5 Oct 2026 05:09:24 +0800 Subject: [PATCH] =?UTF-8?q?gateway:=20a=20reply=20that=20is=20only=20a=20=20and=20DeepSeek's=20closing=20tags=20says=20nothi?= =?UTF-8?q?ng,=20and=20a=20=20wrapping=20DeepSeek's=20own=20i?= =?UTF-8?q?nvoke=20is=20the=20call=20(Dazzle-sys,=20#823=20again:=20Trae?= =?UTF-8?q?=20CN's=20DeepSeek=20V4.1=20Flash=20in=20Pi=20showed=20=20=20=20=20was=20read=20only=20for=20a=20J?= =?UTF-8?q?SON=20object,=20so=20the=20held=20text=20went=20to=20the=20agen?= =?UTF-8?q?t=20as=20it=20was,=20and=20the=20model=20read=20it=20back.=20A?= =?UTF-8?q?=20=20whose=20content=20starts=20with=20a=20tag,?= =?UTF-8?q?=20or=20is=20nothing,=20now=20has=20its=20opener=20dropped=20an?= =?UTF-8?q?d=20what=20follows=20read=20as=20it=20is:=20a=20DSML=20invoke?= =?UTF-8?q?=20in=20it=20is=20the=20call;=20held=20text=20that=20is=20tags?= =?UTF-8?q?=20and=20nothing=20else=20(a=20tag=20cut=20short=20at=20its=20e?= =?UTF-8?q?nd,=20?= =?UTF-8?q?=20y)=20goes=20on=20whole.=20trae=200.1.13=20does=20the=20same?= =?UTF-8?q?=20in=20the=20plugin.=20TestTextWrittenTagsOnly=20fails=20on=20?= =?UTF-8?q?the=20old=20code=20with=20the=20screenshot's=20text;=20go=20vet?= =?UTF-8?q?,=20builds=20(darwin,=20GOOS=3Dlinux=20nogui,=20GOOS=3Dwindows)?= =?UTF-8?q?=20and=20go=20test=20-tags=20nogui=20-count=3D1=20./...=20under?= =?UTF-8?q?=20a=20temp=20HOME=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/textcalls.go | 39 ++++++++++++++++++++++++++---- internal/gateway/textcalls_test.go | 23 ++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/internal/gateway/textcalls.go b/internal/gateway/textcalls.go index f41efd82..38863d74 100644 --- a/internal/gateway/textcalls.go +++ b/internal/gateway/textcalls.go @@ -114,6 +114,13 @@ func parseTextCalls(s string, names map[string]bool) (calls []writtenCall, rest switch { case h >= 0 && (d == nil || h < d[0]): left.WriteString(s[:h]) + if t := strings.TrimLeft(s[h+len(""):], " \t\r\n"); t == "" || strings.HasPrefix(t, "<") { + // no JSON after it: DeepSeek's own tags, an invoke or the + // closes of one (#823), or nothing; the opener is dropped + // and what follows read as it is + s = t + continue + } c, n, good := hermesCall(s[h+len(""):], names) if !good { return nil, "", false @@ -134,7 +141,25 @@ func parseTextCalls(s string, names map[string]bool) (calls []writtenCall, rest } } rest = strings.TrimSpace(textCallJunk.ReplaceAllString(left.String(), "")) - return calls, rest, len(calls) > 0 + rest = strings.TrimSpace(strings.TrimSuffix(rest, cutMark(rest))) + // tags and nothing else (#823, Dazzle-sys: + // 0 || rest == "" +} + +// cutMark is the end of s that is a mark's beginning cut short (), "" when there's none; a lone < may be text. +func cutMark(s string) string { + for _, m := range append([]string{"", "= 2; n-- { + if strings.HasSuffix(s, m[:n]) { + return m[:n] + } + } + } + return "" } // hermesCall reads the JSON object a opens, and says how much @@ -320,7 +345,9 @@ func (t *textCallSee) event(ev Event) { t.see(Event{Kind: KToolStart, ID: textCallID(), Name: c.Name}) t.see(Event{Kind: KToolArgs, Text: string(c.Args)}) } - ev.Stop = "tool" + if len(calls) > 0 { + ev.Stop = "tool" + } } } t.release() @@ -455,9 +482,11 @@ func (t *textCallTidy) line(line []byte) []byte { "function": map[string]any{"name": c.Name, "arguments": string(c.Args)}} } pre = append(pre, t.release()...) - pre = append(pre, t.chunk(map[string]any{"tool_calls": tcs}, nil)...) - ch["finish_reason"] = "tool_calls" - changed = true + if len(tcs) > 0 { + pre = append(pre, t.chunk(map[string]any{"tool_calls": tcs}, nil)...) + ch["finish_reason"] = "tool_calls" + changed = true + } } else { pre = append(pre, t.release()...) } diff --git a/internal/gateway/textcalls_test.go b/internal/gateway/textcalls_test.go index 988860c8..16dbad38 100644 --- a/internal/gateway/textcalls_test.go +++ b/internal/gateway/textcalls_test.go @@ -185,3 +185,26 @@ func TestTextWrittenDSMLCallTranslated(t *testing.T) { } } } + +// #823 again (Dazzle-sys, Trae CN's DeepSeek V4.1 Flash in Pi): a reply +// that was a with only DeepSeek's closing tags after it, cut +// short, reached the agent as that text. Tags and nothing else are +// dropped; a wrapping DeepSeek's own invoke is the call. +func TestTextWrittenTagsOnly(t *testing.T) { + body := `{"model":"a/m","stream":true,"messages":[{"role":"user","content":"hi"}],` + textCallTools + `}` + text, calls, finish, raw := relayedChat(t, body, + chunkOf("\n\n"), chunkOf("\n\n<|DSML|invoke name=\"bash\">\n<|DSML|parameter name=\"command\" string=\"true\">ls\n\n"), stopChunk) + if text != "看一下。" || calls["bash"] != `{"command":"ls"}` || finish != "tool_calls" || strings.Contains(raw, "DSML") || strings.Contains(raw, "tool_call>") { + t.Fatalf("wrapped invoke: text %q calls %v finish %q\n%s", text, calls, finish, raw) + } + // text with a tag-like end stays + text, _, _, _ = relayedChat(t, body, chunkOf("x y"), stopChunk) + if text != "x y" { + t.Fatalf("lookalike: %q", text) + } +}