gateway: a plugin's model that writes its tool call into the text is answered as a built-in's is, proven (#823, Dazzle-sys: Trae CN's DeepSeek V4.1 Flash in a group ended the session in Pi) — TestPluginTextWrittenCall serves a plugin provider (trae-cn) whose Chat stream writes <tool_call>{"name":"bash",…}</|DSML|parameter></|DSML|invoke across two chunks, as the screenshot had it: a Chat client gets a tool_calls delta with the command intact and finish_reason tool_calls, an Anthropic Messages client a tool_use, neither any DSML. It fails on bbc89f9e^ (before v0.1.909's text-call reading) and passes now. The trae plugin reads these forms itself from 0.1.11. go vet and go test -tags nogui ./internal/gateway/ under a temp HOME pass.

This commit is contained in:
yetone committed 2026-10-05 03:25:31 +08:00
1 parent 7bfe3137bd
commit 878a8489f0
1 file changed
+74
+74
View File
@@ -0,0 +1,74 @@
package gateway
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// A plugin's model that writes its tool call into the text is answered as
// a built-in's is (#823, Dazzle-sys: Trae CN's DeepSeek V4.1 Flash in a
// group, in Pi): the plugin's text goes through the same reading, so the
// agent gets the call, whether it speaks Chat or Anthropic Messages.
func TestPluginTextWrittenCall(t *testing.T) {
// as the screenshot had it: Hermes' block, closed with DeepSeek's tags
text := "<tool_call>{\"name\":\"bash\",\"arguments\":{\"command\":\"cd /e/Project && echo \\\"### $spec\\\"; done\"}}</|DSML|parameter>\n</|DSML|invoke"
pid := besideFake(t, "trae-cn", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
for _, part := range []string{text[:40], text[40:]} {
b, _ := json.Marshal(map[string]any{"id": "c1", "object": "chat.completion.chunk", "choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": part}, "finish_reason": nil}}})
io.WriteString(w, "data: "+string(b)+"\n\n")
}
io.WriteString(w, `data: {"id":"c1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`+"\n\ndata: [DONE]\n\n")
}))
h := New().Handler()
model := pid + "/fake-1"
tool := `{"type":"function","function":{"name":"bash","parameters":{"type":"object","properties":{"command":{"type":"string"}}}}}`
t.Run("chat", func(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(`{"model":"`+model+`","stream":true,"tools":[`+tool+`],"messages":[{"role":"user","content":"go on"}]}`))
req.Header.Set("Authorization", "Bearer magpie")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
body := rec.Body.String()
if rec.Code != 200 || strings.Contains(body, "DSML") || strings.Contains(body, "tool_call>") ||
!strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
t.Fatalf("%d %s", rec.Code, body)
}
var args string
for _, line := range strings.Split(body, "\n") {
var c struct {
Choices []struct {
Delta struct {
ToolCalls []struct {
Function struct{ Name, Arguments string }
} `json:"tool_calls"`
}
}
}
if json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &c) == nil && len(c.Choices) > 0 {
for _, tc := range c.Choices[0].Delta.ToolCalls {
args += tc.Function.Arguments
}
}
}
var a map[string]string
if json.Unmarshal([]byte(args), &a) != nil || a["command"] != `cd /e/Project && echo "### $spec"; done` {
t.Fatalf("arguments %q", args)
}
})
t.Run("messages", func(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/messages", bytes.NewReader([]byte(`{"model":"`+model+`","stream":true,"max_tokens":100,"tools":[{"name":"bash","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":"go on"}]}`)))
req.Header.Set("x-api-key", "magpie")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
body := rec.Body.String()
if rec.Code != 200 || strings.Contains(body, "DSML") || !strings.Contains(body, `"tool_use"`) {
t.Fatalf("%d %s", rec.Code, body)
}
})
}