Files
LocalAI/core/schema/message_test.go
Ettore Di Giacinto 02cc8cbcaa feat(llama.cpp): consolidate options and respect tokenizer template when enabled (#7120)
* feat(llama.cpp): expose env vars as options for consistency

This allows to configure everything in the YAML file of the model rather
than have global configurations

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(llama.cpp): respect usetokenizertemplate and use llama.cpp templating system to process messages

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* WIP

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Detect template exists if use tokenizer template is enabled

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Better recognization of chat

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixes to support tool calls while using templates from tokenizer

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Fixups

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Drop template guessing, fix passing tools to tokenizer

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Extract grammar and other options from chat template, add schema struct

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* WIP

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* WIP

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Automatically set use_jinja

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Cleanups, identify by default gguf models for chat

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* Update docs

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2025-11-07 21:23:50 +01:00

266 lines
6.5 KiB
Go

package schema_test
import (
"encoding/json"
. "github.com/mudler/LocalAI/core/schema"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("LLM tests", func() {
Context("ToProtoMessages conversion", func() {
It("should convert basic message with string content", func() {
messages := Messages{
{
Role: "user",
Content: "Hello, world!",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("user"))
Expect(protoMessages[0].Content).To(Equal("Hello, world!"))
Expect(protoMessages[0].Name).To(BeEmpty())
Expect(protoMessages[0].ToolCalls).To(BeEmpty())
})
It("should convert message with nil content to empty string", func() {
messages := Messages{
{
Role: "assistant",
Content: nil,
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("assistant"))
Expect(protoMessages[0].Content).To(Equal(""))
})
It("should convert message with array content (multimodal)", func() {
messages := Messages{
{
Role: "user",
Content: []interface{}{
map[string]interface{}{
"type": "text",
"text": "Hello",
},
map[string]interface{}{
"type": "text",
"text": " World",
},
},
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("user"))
Expect(protoMessages[0].Content).To(Equal("Hello World"))
})
It("should convert message with tool_calls", func() {
messages := Messages{
{
Role: "assistant",
Content: "I'll call a function",
ToolCalls: []ToolCall{
{
Index: 0,
ID: "call_123",
Type: "function",
FunctionCall: FunctionCall{
Name: "get_weather",
Arguments: `{"location": "San Francisco"}`,
},
},
},
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("assistant"))
Expect(protoMessages[0].Content).To(Equal("I'll call a function"))
Expect(protoMessages[0].ToolCalls).NotTo(BeEmpty())
// Verify tool_calls JSON is valid
var toolCalls []ToolCall
err := json.Unmarshal([]byte(protoMessages[0].ToolCalls), &toolCalls)
Expect(err).NotTo(HaveOccurred())
Expect(toolCalls).To(HaveLen(1))
Expect(toolCalls[0].ID).To(Equal("call_123"))
Expect(toolCalls[0].FunctionCall.Name).To(Equal("get_weather"))
})
It("should convert message with name field", func() {
messages := Messages{
{
Role: "tool",
Content: "Function result",
Name: "get_weather",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("tool"))
Expect(protoMessages[0].Content).To(Equal("Function result"))
Expect(protoMessages[0].Name).To(Equal("get_weather"))
})
It("should convert message with tool_calls and nil content", func() {
messages := Messages{
{
Role: "assistant",
Content: nil,
ToolCalls: []ToolCall{
{
Index: 0,
ID: "call_456",
Type: "function",
FunctionCall: FunctionCall{
Name: "search",
Arguments: `{"query": "test"}`,
},
},
},
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("assistant"))
Expect(protoMessages[0].Content).To(Equal(""))
Expect(protoMessages[0].ToolCalls).NotTo(BeEmpty())
var toolCalls []ToolCall
err := json.Unmarshal([]byte(protoMessages[0].ToolCalls), &toolCalls)
Expect(err).NotTo(HaveOccurred())
Expect(toolCalls).To(HaveLen(1))
Expect(toolCalls[0].FunctionCall.Name).To(Equal("search"))
})
It("should convert multiple messages", func() {
messages := Messages{
{
Role: "user",
Content: "Hello",
},
{
Role: "assistant",
Content: "Hi there!",
},
{
Role: "user",
Content: "How are you?",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(3))
Expect(protoMessages[0].Role).To(Equal("user"))
Expect(protoMessages[0].Content).To(Equal("Hello"))
Expect(protoMessages[1].Role).To(Equal("assistant"))
Expect(protoMessages[1].Content).To(Equal("Hi there!"))
Expect(protoMessages[2].Role).To(Equal("user"))
Expect(protoMessages[2].Content).To(Equal("How are you?"))
})
It("should handle empty messages slice", func() {
messages := Messages{}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(0))
})
It("should handle message with all optional fields", func() {
messages := Messages{
{
Role: "assistant",
Content: "I'll help you",
Name: "test_tool",
ToolCalls: []ToolCall{
{
Index: 0,
ID: "call_789",
Type: "function",
FunctionCall: FunctionCall{
Name: "test_function",
Arguments: `{"param": "value"}`,
},
},
},
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("assistant"))
Expect(protoMessages[0].Content).To(Equal("I'll help you"))
Expect(protoMessages[0].Name).To(Equal("test_tool"))
Expect(protoMessages[0].ToolCalls).NotTo(BeEmpty())
var toolCalls []ToolCall
err := json.Unmarshal([]byte(protoMessages[0].ToolCalls), &toolCalls)
Expect(err).NotTo(HaveOccurred())
Expect(toolCalls).To(HaveLen(1))
})
It("should handle message with empty string content", func() {
messages := Messages{
{
Role: "user",
Content: "",
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("user"))
Expect(protoMessages[0].Content).To(Equal(""))
})
It("should handle message with array content containing non-text parts", func() {
messages := Messages{
{
Role: "user",
Content: []interface{}{
map[string]interface{}{
"type": "text",
"text": "Hello",
},
map[string]interface{}{
"type": "image",
"url": "https://example.com/image.jpg",
},
},
},
}
protoMessages := messages.ToProto()
Expect(protoMessages).To(HaveLen(1))
Expect(protoMessages[0].Role).To(Equal("user"))
// Should only extract text parts
Expect(protoMessages[0].Content).To(Equal("Hello"))
})
})
})