updated DS compat

This commit is contained in:
Luxferre
2026-09-08 21:25:29 +03:00
parent 5291415d4e
commit e6472a77b7
2 changed files with 128 additions and 10 deletions
+21 -10
View File
@@ -835,7 +835,13 @@ type streamDelta struct {
func cleanMessagesForLLM(msgs []Message) []Message { func cleanMessagesForLLM(msgs []Message) []Message {
out := make([]Message, len(msgs)) out := make([]Message, len(msgs))
for i, m := range msgs { for i, m := range msgs {
out[i] = Message{Role: m.Role, Content: m.Content, ToolCalls: m.ToolCalls, ToolCallID: m.ToolCallID} out[i] = Message{
Role: m.Role,
Content: m.Content,
ReasoningContent: m.ReasoningContent,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
}
} }
return out return out
} }
@@ -855,17 +861,22 @@ func filterText(s string) string {
func sanitizeMessages(msgs []Message) { func sanitizeMessages(msgs []Message) {
for i := range msgs { for i := range msgs {
if msgs[i].Role == "assistant" && len(msgs[i].ToolCalls) > 0 { if msgs[i].Role == "assistant" {
for j := range msgs[i].ToolCalls { if len(msgs[i].ToolCalls) > 0 {
tc := &msgs[i].ToolCalls[j] for j := range msgs[i].ToolCalls {
tc.Function.Arguments = filterText(tc.Function.Arguments) tc := &msgs[i].ToolCalls[j]
astr := tc.Function.Arguments tc.Function.Arguments = filterText(tc.Function.Arguments)
var a map[string]any astr := tc.Function.Arguments
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil { var a map[string]any
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr}) if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
tc.Function.Arguments = string(fixed) fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
tc.Function.Arguments = string(fixed)
}
} }
} }
if msgs[i].ReasoningContent != "" {
msgs[i].ReasoningContent = filterText(msgs[i].ReasoningContent)
}
} else if msgs[i].Role == "tool" && msgs[i].Content != nil { } else if msgs[i].Role == "tool" && msgs[i].Content != nil {
msgs[i].Content = strp(filterText(*msgs[i].Content)) msgs[i].Content = strp(filterText(*msgs[i].Content))
} }
+107
View File
@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -2282,3 +2283,109 @@ func TestSOCKS5ProxySupport(t *testing.T) {
t.Fatalf("timed out waiting for SOCKS5 handshake through proxy") t.Fatalf("timed out waiting for SOCKS5 handshake through proxy")
} }
} }
func TestCleanMessagesForLLMPreservesReasoningContent(t *testing.T) {
msgs := []Message{
{Role: "user", Content: strp("hello")},
{Role: "assistant", Content: strp("done"), ReasoningContent: "thinking steps"},
{Role: "tool", ToolCallID: "tc1", Content: strp("result")},
}
cleaned := cleanMessagesForLLM(msgs)
if len(cleaned) != 3 {
t.Fatalf("expected 3 messages, got %d", len(cleaned))
}
if cleaned[1].ReasoningContent != "thinking steps" {
t.Errorf("expected ReasoningContent %q, got %q", "thinking steps", cleaned[1].ReasoningContent)
}
// Verify JSON serialization includes reasoning_content for assistant
b, err := json.Marshal(cleaned[1])
if err != nil {
t.Fatalf("marshal error: %v", err)
}
if !strings.Contains(string(b), `"reasoning_content":"thinking steps"`) {
t.Errorf("expected JSON to contain reasoning_content, got %s", string(b))
}
// Verify JSON serialization omits reasoning_content when empty
bUser, err := json.Marshal(cleaned[0])
if err != nil {
t.Fatalf("marshal error: %v", err)
}
if strings.Contains(string(bUser), "reasoning_content") {
t.Errorf("expected JSON to omit reasoning_content for empty, got %s", string(bUser))
}
}
func TestLLMPreservesReasoningContentInRequestBody(t *testing.T) {
var receivedBody []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
receivedBody, err = io.ReadAll(r.Body)
if err != nil {
t.Errorf("read body err: %v", err)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"reply"}}]}`))
}))
defer srv.Close()
cfg := defCfg
cfg.Endpoint = srv.URL
cfg.Stream = false
cfg.APIKey = "-"
msgs := []Message{
{Role: "user", Content: strp("call tool")},
{
Role: "assistant",
Content: strp(""),
ReasoningContent: "deep thoughts about tool",
ToolCalls: []ToolCall{
{
ID: "call_1",
Type: "function",
Function: struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}{Name: "shell_exec", Arguments: `{"command":"ls"}`},
},
},
},
{Role: "tool", ToolCallID: "call_1", Content: strp("file.txt\n\nexit: 0")},
}
_, _, err := llm(context.Background(), &cfg, msgs, nil)
if err != nil {
t.Fatalf("unexpected llm error: %v", err)
}
var reqPayload struct {
Messages []map[string]any `json:"messages"`
}
if err := json.Unmarshal(receivedBody, &reqPayload); err != nil {
t.Fatalf("unmarshal request payload: %v", err)
}
if len(reqPayload.Messages) != 3 {
t.Fatalf("expected 3 messages in request, got %d", len(reqPayload.Messages))
}
asstMsg := reqPayload.Messages[1]
rc, ok := asstMsg["reasoning_content"].(string)
if !ok || rc != "deep thoughts about tool" {
t.Errorf("expected assistant message reasoning_content %q, got %v", "deep thoughts about tool", asstMsg["reasoning_content"])
}
}
func TestSanitizeMessagesCleansReasoningContent(t *testing.T) {
msgs := []Message{
{Role: "assistant", ReasoningContent: "clean\x00\u200B\u00A0reasoning"},
}
sanitizeMessages(msgs)
if strings.ContainsAny(msgs[0].ReasoningContent, "\x00") || strings.Contains(msgs[0].ReasoningContent, "\u200B") {
t.Errorf("sanitizeMessages did not strip control/invisible characters from ReasoningContent: %q", msgs[0].ReasoningContent)
}
if msgs[0].ReasoningContent != "cleanreasoning" {
t.Errorf("expected 'cleanreasoning', got %q", msgs[0].ReasoningContent)
}
}