summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-01-16 16:53:19 +0300
committerGrail Finder <wohilas@gmail.com>2026-01-16 16:53:19 +0300
commiteb44b1e4b244e5a93e7d465b14df39819d8dfaba (patch)
treeabd29b6c63e198e5ce8057cd6a51a0b20b0d143f
parentf5d76eb60587564648e9f5084469a27cef5765b8 (diff)
Feat: impl attempt
-rw-r--r--bot.go106
-rw-r--r--bot_test.go318
-rw-r--r--config.example.toml2
-rw-r--r--config/config.go10
-rw-r--r--llm.go66
-rw-r--r--models/models.go14
6 files changed, 486 insertions, 30 deletions
diff --git a/bot.go b/bot.go
index 4d6da58..967c060 100644
--- a/bot.go
+++ b/bot.go
@@ -18,6 +18,7 @@ import (
"net/url"
"os"
"path"
+ "regexp"
"strconv"
"strings"
"sync"
@@ -68,6 +69,111 @@ var (
LocalModels = []string{}
)
+// parseKnownToTag extracts known_to list from content using configured tag.
+// Returns cleaned content and list of character names.
+func parseKnownToTag(content string) (string, []string) {
+ if cfg == nil || !cfg.CharSpecificContextEnabled {
+ return content, nil
+ }
+ tag := cfg.CharSpecificContextTag
+ if tag == "" {
+ tag = "__known_to_chars__"
+ }
+ // Pattern: tag + list + "__"
+ pattern := regexp.QuoteMeta(tag) + `(.*?)__`
+ re := regexp.MustCompile(pattern)
+ matches := re.FindAllStringSubmatch(content, -1)
+ if len(matches) == 0 {
+ return content, nil
+ }
+ // There may be multiple tags; we combine all.
+ var knownTo []string
+ cleaned := content
+ for _, match := range matches {
+ if len(match) < 2 {
+ continue
+ }
+ // Remove the entire matched tag from content
+ cleaned = strings.Replace(cleaned, match[0], "", 1)
+
+ list := strings.TrimSpace(match[1])
+ if list == "" {
+ continue
+ }
+ parts := strings.Split(list, ",")
+ for _, p := range parts {
+ p = strings.TrimSpace(p)
+ if p != "" {
+ knownTo = append(knownTo, p)
+ }
+ }
+ }
+ // Also remove any leftover trailing "__" that might be orphaned? Not needed.
+ return strings.TrimSpace(cleaned), knownTo
+}
+
+// processMessageTag processes a message for known_to tag and sets KnownTo field.
+// It also ensures the sender's role is included in KnownTo.
+// If KnownTo already set (e.g., from DB), preserves it unless new tag found.
+func processMessageTag(msg models.RoleMsg) models.RoleMsg {
+ if cfg == nil || !cfg.CharSpecificContextEnabled {
+ return msg
+ }
+ // If KnownTo already set, assume tag already processed (content cleaned).
+ // However, we still check for new tags (maybe added later).
+ cleaned, knownTo := parseKnownToTag(msg.Content)
+ if cleaned != msg.Content {
+ msg.Content = cleaned
+ }
+ // If tag found, replace KnownTo with new list (merge with existing?)
+ // For simplicity, if knownTo is not nil, replace.
+ if knownTo != nil {
+ msg.KnownTo = knownTo
+ }
+ // Ensure sender role is in KnownTo
+ if msg.Role != "" {
+ senderAdded := false
+ for _, k := range msg.KnownTo {
+ if k == msg.Role {
+ senderAdded = true
+ break
+ }
+ }
+ if !senderAdded {
+ msg.KnownTo = append(msg.KnownTo, msg.Role)
+ }
+ }
+ return msg
+}
+
+// filterMessagesForCharacter returns messages visible to the specified character.
+// If CharSpecificContextEnabled is false, returns all messages.
+func filterMessagesForCharacter(messages []models.RoleMsg, character string) []models.RoleMsg {
+ if cfg == nil || !cfg.CharSpecificContextEnabled || character == "" {
+ return messages
+ }
+ filtered := make([]models.RoleMsg, 0, len(messages))
+ for _, msg := range messages {
+ // If KnownTo is nil or empty, message is visible to all
+ if len(msg.KnownTo) == 0 {
+ filtered = append(filtered, msg)
+ continue
+ }
+ // Check if character is in KnownTo list
+ found := false
+ for _, k := range msg.KnownTo {
+ if k == character {
+ found = true
+ break
+ }
+ }
+ if found {
+ filtered = append(filtered, msg)
+ }
+ }
+ return filtered
+}
+
// cleanNullMessages removes messages with null or empty content to prevent API issues
func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg {
// // deletes tool calls which we don't want for now
diff --git a/bot_test.go b/bot_test.go
index d2956a9..7496175 100644
--- a/bot_test.go
+++ b/bot_test.go
@@ -286,4 +286,322 @@ func TestConvertJSONToMapStringString(t *testing.T) {
}
})
}
+}
+
+func TestParseKnownToTag(t *testing.T) {
+ tests := []struct {
+ name string
+ content string
+ enabled bool
+ tag string
+ wantCleaned string
+ wantKnownTo []string
+ }{
+ {
+ name: "feature disabled returns original",
+ content: "Hello __known_to_chars__Alice__",
+ enabled: false,
+ tag: "__known_to_chars__",
+ wantCleaned: "Hello __known_to_chars__Alice__",
+ wantKnownTo: nil,
+ },
+ {
+ name: "no tag returns original",
+ content: "Hello Alice",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Hello Alice",
+ wantKnownTo: nil,
+ },
+ {
+ name: "single tag with one char",
+ content: "Hello __known_to_chars__Alice__",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Hello",
+ wantKnownTo: []string{"Alice"},
+ },
+ {
+ name: "single tag with two chars",
+ content: "Secret __known_to_chars__Alice,Bob__ message",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Secret message",
+ wantKnownTo: []string{"Alice", "Bob"},
+ },
+ {
+ name: "tag at beginning",
+ content: "__known_to_chars__Alice__ Hello",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Hello",
+ wantKnownTo: []string{"Alice"},
+ },
+ {
+ name: "tag at end",
+ content: "Hello __known_to_chars__Alice__",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Hello",
+ wantKnownTo: []string{"Alice"},
+ },
+ {
+ name: "multiple tags",
+ content: "First __known_to_chars__Alice__ then __known_to_chars__Bob__",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "First then",
+ wantKnownTo: []string{"Alice", "Bob"},
+ },
+ {
+ name: "custom tag",
+ content: "Secret __secret__Alice,Bob__ message",
+ enabled: true,
+ tag: "__secret__",
+ wantCleaned: "Secret message",
+ wantKnownTo: []string{"Alice", "Bob"},
+ },
+ {
+ name: "empty list",
+ content: "Secret __known_to_chars____",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "Secret",
+ wantKnownTo: nil,
+ },
+ {
+ name: "whitespace around commas",
+ content: "__known_to_chars__ Alice , Bob , Carl __",
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantCleaned: "",
+ wantKnownTo: []string{"Alice", "Bob", "Carl"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Set up config
+ testCfg := &config.Config{
+ CharSpecificContextEnabled: tt.enabled,
+ CharSpecificContextTag: tt.tag,
+ }
+ cfg = testCfg
+
+ cleaned, knownTo := parseKnownToTag(tt.content)
+
+ if cleaned != tt.wantCleaned {
+ t.Errorf("parseKnownToTag() cleaned = %q, want %q", cleaned, tt.wantCleaned)
+ }
+
+ if len(knownTo) != len(tt.wantKnownTo) {
+ t.Errorf("parseKnownToTag() knownTo length = %v, want %v", len(knownTo), len(tt.wantKnownTo))
+ t.Logf("got: %v", knownTo)
+ t.Logf("want: %v", tt.wantKnownTo)
+ } else {
+ for i, got := range knownTo {
+ if got != tt.wantKnownTo[i] {
+ t.Errorf("parseKnownToTag() knownTo[%d] = %q, want %q", i, got, tt.wantKnownTo[i])
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestProcessMessageTag(t *testing.T) {
+ tests := []struct {
+ name string
+ msg models.RoleMsg
+ enabled bool
+ tag string
+ wantMsg models.RoleMsg
+ }{
+ {
+ name: "feature disabled returns unchanged",
+ msg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret __known_to_chars__Bob__",
+ },
+ enabled: false,
+ tag: "__known_to_chars__",
+ wantMsg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret __known_to_chars__Bob__",
+ KnownTo: nil,
+ },
+ },
+ {
+ name: "no tag, no knownTo",
+ msg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Hello everyone",
+ },
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantMsg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Hello everyone",
+ KnownTo: []string{"Alice"},
+ },
+ },
+ {
+ name: "tag with Bob, adds Alice automatically",
+ msg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret __known_to_chars__Bob__",
+ },
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantMsg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret",
+ KnownTo: []string{"Bob", "Alice"},
+ },
+ },
+ {
+ name: "tag already includes sender",
+ msg: models.RoleMsg{
+ Role: "Alice",
+ Content: "__known_to_chars__Alice,Bob__",
+ },
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantMsg: models.RoleMsg{
+ Role: "Alice",
+ Content: "",
+ KnownTo: []string{"Alice", "Bob"},
+ },
+ },
+ {
+ name: "knownTo already set (from DB), tag still processed",
+ msg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret __known_to_chars__Bob__",
+ KnownTo: []string{"Alice"}, // from previous processing
+ },
+ enabled: true,
+ tag: "__known_to_chars__",
+ wantMsg: models.RoleMsg{
+ Role: "Alice",
+ Content: "Secret",
+ KnownTo: []string{"Bob", "Alice"},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testCfg := &config.Config{
+ CharSpecificContextEnabled: tt.enabled,
+ CharSpecificContextTag: tt.tag,
+ }
+ cfg = testCfg
+
+ got := processMessageTag(tt.msg)
+
+ if got.Content != tt.wantMsg.Content {
+ t.Errorf("processMessageTag() content = %q, want %q", got.Content, tt.wantMsg.Content)
+ }
+
+ if len(got.KnownTo) != len(tt.wantMsg.KnownTo) {
+ t.Errorf("processMessageTag() KnownTo length = %v, want %v", len(got.KnownTo), len(tt.wantMsg.KnownTo))
+ t.Logf("got: %v", got.KnownTo)
+ t.Logf("want: %v", tt.wantMsg.KnownTo)
+ } else {
+ // order may differ; check membership
+ for _, want := range tt.wantMsg.KnownTo {
+ found := false
+ for _, gotVal := range got.KnownTo {
+ if gotVal == want {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("processMessageTag() missing KnownTo entry %q, got %v", want, got.KnownTo)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestFilterMessagesForCharacter(t *testing.T) {
+ messages := []models.RoleMsg{
+ {Role: "system", Content: "System message", KnownTo: nil}, // visible to all
+ {Role: "Alice", Content: "Hello everyone", KnownTo: nil}, // visible to all
+ {Role: "Alice", Content: "Secret for Bob", KnownTo: []string{"Alice", "Bob"}},
+ {Role: "Bob", Content: "Reply to Alice", KnownTo: []string{"Alice", "Bob"}},
+ {Role: "Alice", Content: "Private to Carl", KnownTo: []string{"Alice", "Carl"}},
+ {Role: "Carl", Content: "Hi all", KnownTo: nil}, // visible to all
+ }
+
+ tests := []struct {
+ name string
+ enabled bool
+ character string
+ wantIndices []int // indices from original messages that should be included
+ }{
+ {
+ name: "feature disabled returns all",
+ enabled: false,
+ character: "Alice",
+ wantIndices: []int{0,1,2,3,4,5},
+ },
+ {
+ name: "character empty returns all",
+ enabled: true,
+ character: "",
+ wantIndices: []int{0,1,2,3,4,5},
+ },
+ {
+ name: "Alice sees all including Carl-private",
+ enabled: true,
+ character: "Alice",
+ wantIndices: []int{0,1,2,3,4,5},
+ },
+ {
+ name: "Bob sees Alice-Bob secrets and all public",
+ enabled: true,
+ character: "Bob",
+ wantIndices: []int{0,1,2,3,5},
+ },
+ {
+ name: "Carl sees Alice-Carl secret and public",
+ enabled: true,
+ character: "Carl",
+ wantIndices: []int{0,1,4,5},
+ },
+ {
+ name: "David sees only public messages",
+ enabled: true,
+ character: "David",
+ wantIndices: []int{0,1,5},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testCfg := &config.Config{
+ CharSpecificContextEnabled: tt.enabled,
+ CharSpecificContextTag: "__known_to_chars__",
+ }
+ cfg = testCfg
+
+ got := filterMessagesForCharacter(messages, tt.character)
+
+ if len(got) != len(tt.wantIndices) {
+ t.Errorf("filterMessagesForCharacter() returned %d messages, want %d", len(got), len(tt.wantIndices))
+ t.Logf("got: %v", got)
+ return
+ }
+
+ for i, idx := range tt.wantIndices {
+ if got[i].Content != messages[idx].Content {
+ t.Errorf("filterMessagesForCharacter() message %d content = %q, want %q", i, got[i].Content, messages[idx].Content)
+ }
+ }
+ })
+ }
} \ No newline at end of file
diff --git a/config.example.toml b/config.example.toml
index 3a5401b..85b2662 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -43,3 +43,5 @@ DBPATH = "gflt.db"
FilePickerDir = "." # Directory where file picker should start
FilePickerExts = "png,jpg,jpeg,gif,webp" # Comma-separated list of allowed file extensions for file picker
EnableMouse = false # Enable mouse support in the UI
+CharSpecificContextEnabled = false
+CharSpecificContextTag = "__known_to_chars__"
diff --git a/config/config.go b/config/config.go
index 20935a2..62c8331 100644
--- a/config/config.go
+++ b/config/config.go
@@ -61,10 +61,12 @@ type Config struct {
WhisperBinaryPath string `toml:"WhisperBinaryPath"`
WhisperModelPath string `toml:"WhisperModelPath"`
STT_LANG string `toml:"STT_LANG"`
- DBPATH string `toml:"DBPATH"`
- FilePickerDir string `toml:"FilePickerDir"`
- FilePickerExts string `toml:"FilePickerExts"`
- EnableMouse bool `toml:"EnableMouse"`
+ DBPATH string `toml:"DBPATH"`
+ FilePickerDir string `toml:"FilePickerDir"`
+ FilePickerExts string `toml:"FilePickerExts"`
+ EnableMouse bool `toml:"EnableMouse"`
+ CharSpecificContextEnabled bool `toml:"CharSpecificContextEnabled"`
+ CharSpecificContextTag string `toml:"CharSpecificContextTag"`
}
func LoadConfig(fn string) (*Config, error) {
diff --git a/llm.go b/llm.go
index 5621ecf..5599d21 100644
--- a/llm.go
+++ b/llm.go
@@ -34,6 +34,24 @@ func ClearImageAttachment() {
imageAttachmentPath = ""
}
+// filterMessagesForCurrentCharacter filters messages based on char-specific context.
+// Returns filtered messages and the bot persona role (target character).
+func filterMessagesForCurrentCharacter(messages []models.RoleMsg) ([]models.RoleMsg, string) {
+ if cfg == nil || !cfg.CharSpecificContextEnabled {
+ botPersona := cfg.AssistantRole
+ if cfg.WriteNextMsgAsCompletionAgent != "" {
+ botPersona = cfg.WriteNextMsgAsCompletionAgent
+ }
+ return messages, botPersona
+ }
+ botPersona := cfg.AssistantRole
+ if cfg.WriteNextMsgAsCompletionAgent != "" {
+ botPersona = cfg.WriteNextMsgAsCompletionAgent
+ }
+ filtered := filterMessagesForCharacter(messages, botPersona)
+ return filtered, botPersona
+}
+
type ChunkParser interface {
ParseChunk([]byte) (*models.TextChunk, error)
FormMsg(msg, role string, cont bool) (io.Reader, error)
@@ -113,6 +131,7 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
}
if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
}
if !resume {
@@ -136,17 +155,14 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
// add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
}
- messages := make([]string, len(chatBody.Messages))
- for i, m := range chatBody.Messages {
+ filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
+ messages := make([]string, len(filteredMessages))
+ for i, m := range filteredMessages {
messages[i] = m.ToPrompt()
}
prompt := strings.Join(messages, "\n")
// strings builder?
if !resume {
- botPersona := cfg.AssistantRole
- if cfg.WriteNextMsgAsCompletionAgent != "" {
- botPersona = cfg.WriteNextMsgAsCompletionAgent
- }
botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart
}
@@ -270,6 +286,7 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) {
// Create a simple text message
newMsg = models.NewRoleMsg(role, msg)
}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role, "content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages))
}
@@ -291,12 +308,13 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) {
}
}
// openai /v1/chat does not support custom roles; needs to be user, assistant, system
+ filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{
- Messages: make([]models.RoleMsg, len(chatBody.Messages)),
+ Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model,
Stream: chatBody.Stream,
}
- for i, msg := range chatBody.Messages {
+ for i, msg := range filteredMessages {
if msg.Role == cfg.UserRole {
bodyCopy.Messages[i] = msg
bodyCopy.Messages[i].Role = "user"
@@ -348,6 +366,7 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
logger.Debug("formmsg deepseekercompletion", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
}
if !resume {
@@ -372,17 +391,14 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
// add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
}
- messages := make([]string, len(chatBody.Messages))
- for i, m := range chatBody.Messages {
+ filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
+ messages := make([]string, len(filteredMessages))
+ for i, m := range filteredMessages {
messages[i] = m.ToPrompt()
}
prompt := strings.Join(messages, "\n")
// strings builder?
if !resume {
- botPersona := cfg.AssistantRole
- if cfg.WriteNextMsgAsCompletionAgent != "" {
- botPersona = cfg.WriteNextMsgAsCompletionAgent
- }
botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart
}
@@ -432,6 +448,7 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
logger.Debug("formmsg deepseekerchat", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot continue
newMsg := models.RoleMsg{Role: role, Content: msg}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
}
if !resume {
@@ -451,12 +468,13 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
}
}
+ filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{
- Messages: make([]models.RoleMsg, len(chatBody.Messages)),
+ Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model,
Stream: chatBody.Stream,
}
- for i, msg := range chatBody.Messages {
+ for i, msg := range filteredMessages {
if msg.Role == cfg.UserRole || i == 1 {
bodyCopy.Messages[i] = msg
bodyCopy.Messages[i].Role = "user"
@@ -502,6 +520,7 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
logger.Debug("formmsg openroutercompletion", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
}
if !resume {
@@ -525,17 +544,14 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
// add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
}
- messages := make([]string, len(chatBody.Messages))
- for i, m := range chatBody.Messages {
+ filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
+ messages := make([]string, len(filteredMessages))
+ for i, m := range filteredMessages {
messages[i] = m.ToPrompt()
}
prompt := strings.Join(messages, "\n")
// strings builder?
if !resume {
- botPersona := cfg.AssistantRole
- if cfg.WriteNextMsgAsCompletionAgent != "" {
- botPersona = cfg.WriteNextMsgAsCompletionAgent
- }
botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart
}
@@ -619,6 +635,7 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
// Create a simple text message
newMsg = models.NewRoleMsg(role, msg)
}
+ newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg)
}
if !resume {
@@ -639,12 +656,13 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
}
}
// Create copy of chat body with standardized user role
+ filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{
- Messages: make([]models.RoleMsg, len(chatBody.Messages)),
+ Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model,
Stream: chatBody.Stream,
}
- for i, msg := range chatBody.Messages {
+ for i, msg := range filteredMessages {
bodyCopy.Messages[i] = msg
// Standardize role if it's a user role
if bodyCopy.Messages[i].Role == cfg.UserRole {
diff --git a/models/models.go b/models/models.go
index 912f72b..88ba144 100644
--- a/models/models.go
+++ b/models/models.go
@@ -93,6 +93,7 @@ type RoleMsg struct {
Content string `json:"-"`
ContentParts []interface{} `json:"-"`
ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages
+ KnownTo []string `json:"known_to,omitempty"`
hasContentParts bool // Flag to indicate which content type to marshal
}
@@ -104,10 +105,12 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
Role string `json:"role"`
Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
+ KnownTo []string `json:"known_to,omitempty"`
}{
Role: m.Role,
Content: m.ContentParts,
ToolCallID: m.ToolCallID,
+ KnownTo: m.KnownTo,
}
return json.Marshal(aux)
} else {
@@ -116,10 +119,12 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
Role string `json:"role"`
Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
+ KnownTo []string `json:"known_to,omitempty"`
}{
Role: m.Role,
Content: m.Content,
ToolCallID: m.ToolCallID,
+ KnownTo: m.KnownTo,
}
return json.Marshal(aux)
}
@@ -132,11 +137,13 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
Role string `json:"role"`
Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
+ KnownTo []string `json:"known_to,omitempty"`
}
if err := json.Unmarshal(data, &structured); err == nil && len(structured.Content) > 0 {
m.Role = structured.Role
m.ContentParts = structured.Content
m.ToolCallID = structured.ToolCallID
+ m.KnownTo = structured.KnownTo
m.hasContentParts = true
return nil
}
@@ -146,6 +153,7 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
Role string `json:"role"`
Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"`
+ KnownTo []string `json:"known_to,omitempty"`
}
if err := json.Unmarshal(data, &simple); err != nil {
return err
@@ -153,6 +161,7 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
m.Role = simple.Role
m.Content = simple.Content
m.ToolCallID = simple.ToolCallID
+ m.KnownTo = simple.KnownTo
m.hasContentParts = false
return nil
}
@@ -363,7 +372,8 @@ func (cb *ChatBody) MakeStopSlice() []string {
for _, m := range cb.Messages {
namesMap[m.Role] = struct{}{}
}
- ss := []string{"<|im_end|>"}
+ ss := make([]string, 0, 1+len(namesMap))
+ ss = append(ss, "<|im_end|>")
for k := range namesMap {
ss = append(ss, k+":\n")
}
@@ -523,7 +533,7 @@ type LCPModels struct {
}
func (lcp *LCPModels) ListModels() []string {
- resp := []string{}
+ resp := make([]string, 0, len(lcp.Data))
for _, model := range lcp.Data {
resp = append(resp, model.ID)
}