mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-10 16:27:58 -04:00
* feat(schema): accept reasoning_content as inbound alias for reasoning Interleaved-thinking clients (cogito, vLLM/DeepSeek-style) emit reasoning_content on assistant turns. Accept it as an inbound alias so reasoning survives the tool-result loop; canonical reasoning wins when both are present. Emission is unchanged (still reasoning). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(schema): pin interleaved reasoning+tool_calls round-trip Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(openai): pin reachedTokenBudget truncation detection Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(anthropic): add thinking and signature fields to content blocks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(anthropic): parse inbound thinking blocks into reasoning Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(anthropic): emit thinking blocks with synthetic signature on tool turns Extract buildAnthropicContentBlocks so non-streaming content assembly is unit-testable, and prepend a thinking block (with an opaque synthetic signature) before text/tool_use blocks when the request opts into thinking. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(anthropic): stream thinking_delta and signature_delta before tool_use Extract anthropicStreamSequence so the streaming block order is unit-testable, and emit content_block_start(thinking) -> thinking_delta -> signature_delta -> content_block_stop before the tool_use block sequence when thinking is enabled. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs: add interleaved thinking with tool calls guide Add a features guide describing interleaved thinking: an assistant turn carrying reasoning and tool_calls together, the reasoning-round-trip contract (including the reasoning_content inbound alias and Anthropic thinking blocks with a synthetic signature), per-backend enablement (reasoning_format for llama.cpp, reasoning_parser/tool_call_parser for vLLM/SGLang plus the vLLM auto-config hook), a worked request/response example, and known limitations. Cross-link from model-configuration, text-generation, and openai-functions. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
118 lines
3.5 KiB
Go
118 lines
3.5 KiB
Go
package schema
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/mudler/xlog"
|
|
|
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
)
|
|
|
|
type Message struct {
|
|
// The message role
|
|
Role string `json:"role,omitempty" yaml:"role"`
|
|
|
|
// The message name (used for tools calls)
|
|
Name string `json:"name,omitempty" yaml:"name"`
|
|
|
|
// The message content
|
|
Content any `json:"content" yaml:"content"`
|
|
|
|
// Staging buffers populated by the request middleware while
|
|
// decoding multimodal Content. Never serialised — strict
|
|
// providers (Anthropic) 400 on unknown message fields when the
|
|
// cloud-proxy passthrough re-marshals Message verbatim.
|
|
StringContent string `json:"-" yaml:"-"`
|
|
StringImages []string `json:"-" yaml:"-"`
|
|
StringVideos []string `json:"-" yaml:"-"`
|
|
StringAudios []string `json:"-" yaml:"-"`
|
|
|
|
// A result of a function call
|
|
FunctionCall any `json:"function_call,omitempty" yaml:"function_call,omitempty"`
|
|
|
|
ToolCalls []ToolCall `json:"tool_calls,omitempty" yaml:"tool_call,omitempty"`
|
|
|
|
ToolCallID string `json:"tool_call_id,omitempty" yaml:"tool_call_id,omitempty"`
|
|
|
|
// Reasoning content extracted from <thinking>...</thinking> tags
|
|
Reasoning *string `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON decodes Message, accepting reasoning_content as an inbound
|
|
// alias for reasoning so vLLM/DeepSeek/OpenAI-SDK style clients that emit
|
|
// reasoning_content on assistant turns round-trip through the interleaved
|
|
// thinking loop. Canonical reasoning wins when both are present.
|
|
func (m *Message) UnmarshalJSON(data []byte) error {
|
|
type messageAlias Message
|
|
aux := struct {
|
|
*messageAlias
|
|
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
|
}{messageAlias: (*messageAlias)(m)}
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
if m.Reasoning == nil && aux.ReasoningContent != nil {
|
|
m.Reasoning = aux.ReasoningContent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ToolCall struct {
|
|
Index int `json:"index"`
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
FunctionCall FunctionCall `json:"function"`
|
|
}
|
|
|
|
type FunctionCall struct {
|
|
Name string `json:"name,omitempty"`
|
|
Arguments string `json:"arguments"`
|
|
}
|
|
|
|
type Messages []Message
|
|
|
|
// MessagesToProto converts schema.Message slice to proto.Message slice
|
|
// It handles content conversion, tool_calls serialization, and optional fields
|
|
func (messages Messages) ToProto() []*proto.Message {
|
|
protoMessages := make([]*proto.Message, len(messages))
|
|
for i, message := range messages {
|
|
protoMessages[i] = &proto.Message{
|
|
Role: message.Role,
|
|
Name: message.Name, // needed by function calls
|
|
}
|
|
|
|
switch ct := message.Content.(type) {
|
|
case string:
|
|
protoMessages[i].Content = ct
|
|
case []any:
|
|
// If using the tokenizer template, in case of multimodal we want to keep the multimodal content as and return only strings here
|
|
data, _ := json.Marshal(ct)
|
|
resultData := []struct {
|
|
Text string `json:"text"`
|
|
}{}
|
|
json.Unmarshal(data, &resultData)
|
|
for _, r := range resultData {
|
|
protoMessages[i].Content += r.Text
|
|
}
|
|
}
|
|
|
|
// Serialize tool_calls to JSON string if present
|
|
if len(message.ToolCalls) > 0 {
|
|
toolCallsJSON, err := json.Marshal(message.ToolCalls)
|
|
if err != nil {
|
|
xlog.Warn("failed to marshal tool_calls to JSON", "error", err)
|
|
} else {
|
|
protoMessages[i].ToolCalls = string(toolCallsJSON)
|
|
}
|
|
}
|
|
|
|
if message.ToolCallID != "" {
|
|
protoMessages[i].ToolCallId = message.ToolCallID
|
|
}
|
|
if message.Reasoning != nil {
|
|
protoMessages[i].ReasoningContent = *message.Reasoning
|
|
}
|
|
}
|
|
return protoMessages
|
|
}
|