Why: Some endpoints reject consecutive system messages or a run of the same role. The product already emits several system messages in one request. Evidence: go test is covered by PrepareMessages tests for merge, strict folding, and tool pairs. Empty MessageShape leaves the transcript unchanged. Impact: Callers opt in with StreamOptions.MessageShape. Tool calls and tool results are not merged. Refs: docs/records/2026-10-04-sillytavern-review.md
61 lines
1.5 KiB
Go
61 lines
1.5 KiB
Go
package llm
|
|
|
|
// PrepareMessages 按 endpoint 整形消息。
|
|
// merge 合并相邻、且不携带工具调用的同角色消息。
|
|
// strict 在此之后把第一条之后的 system 收成 user,再合并,满足严格交替。
|
|
// 工具消息和带 tool_calls 的助手消息保持原样,避免拆开调用与结果。
|
|
func PrepareMessages(msgs []Message, shape string) []Message {
|
|
switch shape {
|
|
case "", "merge", "strict":
|
|
default:
|
|
return msgs
|
|
}
|
|
out := mergeSameRole(msgs)
|
|
if shape == "strict" {
|
|
out = foldExtraSystems(out)
|
|
out = mergeSameRole(out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mergeable(m Message) bool {
|
|
return m.Role != RoleTool && len(m.ToolCalls) == 0 && m.ToolCallID == ""
|
|
}
|
|
|
|
func mergeSameRole(msgs []Message) []Message {
|
|
if len(msgs) == 0 {
|
|
return msgs
|
|
}
|
|
out := make([]Message, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
if n := len(out); n > 0 && out[n-1].Role == m.Role && mergeable(out[n-1]) && mergeable(m) {
|
|
if out[n-1].Content != "" && m.Content != "" {
|
|
out[n-1].Content += "\n\n" + m.Content
|
|
} else {
|
|
out[n-1].Content += m.Content
|
|
}
|
|
continue
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func foldExtraSystems(msgs []Message) []Message {
|
|
seenSystem := false
|
|
out := make([]Message, 0, len(msgs))
|
|
for _, m := range msgs {
|
|
if m.Role == RoleSystem {
|
|
if seenSystem {
|
|
m.Role = RoleUser
|
|
if m.Content != "" {
|
|
m.Content = "[system]\n" + m.Content
|
|
}
|
|
}
|
|
seenSystem = true
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out
|
|
}
|