mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -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>
221 lines
7.5 KiB
Go
221 lines
7.5 KiB
Go
package schema
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
)
|
|
|
|
// AnthropicSystemParam accepts system as string or array of content blocks (SDK sends array).
|
|
type AnthropicSystemParam string
|
|
|
|
// UnmarshalJSON accepts string or array of blocks with "text" field.
|
|
func (s *AnthropicSystemParam) UnmarshalJSON(data []byte) error {
|
|
var raw any
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
switch v := raw.(type) {
|
|
case string:
|
|
*s = AnthropicSystemParam(v)
|
|
return nil
|
|
case []any:
|
|
var out string
|
|
for _, block := range v {
|
|
if m, ok := block.(map[string]any); ok && m["type"] == "text" {
|
|
if t, ok := m["text"].(string); ok {
|
|
out += t
|
|
}
|
|
}
|
|
}
|
|
*s = AnthropicSystemParam(out)
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AnthropicRequest represents a request to the Anthropic Messages API
|
|
// https://docs.anthropic.com/claude/reference/messages_post
|
|
type AnthropicRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []AnthropicMessage `json:"messages"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
StopSequences []string `json:"stop_sequences,omitempty"`
|
|
Stream bool `json:"stream,omitempty"`
|
|
System AnthropicSystemParam `json:"system,omitempty"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopK *int `json:"top_k,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
Tools []AnthropicTool `json:"tools,omitempty"`
|
|
ToolChoice any `json:"tool_choice,omitempty"`
|
|
|
|
// Thinking gates extended-thinking output. We only surface thinking
|
|
// content blocks when the client explicitly opts in, matching Anthropic's
|
|
// API where thinking is off unless requested.
|
|
Thinking *AnthropicThinkingParam `json:"thinking,omitempty"`
|
|
|
|
// Internal fields for request handling
|
|
Context context.Context `json:"-"`
|
|
Cancel context.CancelFunc `json:"-"`
|
|
}
|
|
|
|
// AnthropicThinkingParam is the request-side extended-thinking toggle.
|
|
type AnthropicThinkingParam struct {
|
|
Type string `json:"type"` // "enabled" | "disabled"
|
|
BudgetTokens int `json:"budget_tokens,omitempty"`
|
|
}
|
|
|
|
// ModelName implements the LocalAIRequest interface
|
|
func (ar *AnthropicRequest) ModelName(s *string) string {
|
|
if s != nil {
|
|
ar.Model = *s
|
|
}
|
|
return ar.Model
|
|
}
|
|
|
|
// AnthropicTool represents a tool definition in the Anthropic format
|
|
type AnthropicTool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
InputSchema map[string]any `json:"input_schema"`
|
|
}
|
|
|
|
// AnthropicMessage represents a message in the Anthropic format
|
|
type AnthropicMessage struct {
|
|
Role string `json:"role"`
|
|
Content any `json:"content"`
|
|
}
|
|
|
|
// AnthropicContentBlock represents a content block in an Anthropic message
|
|
type AnthropicContentBlock struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
Source *AnthropicImageSource `json:"source,omitempty"`
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Input map[string]any `json:"input,omitempty"`
|
|
ToolUseID string `json:"tool_use_id,omitempty"`
|
|
Content any `json:"content,omitempty"`
|
|
IsError *bool `json:"is_error,omitempty"`
|
|
Thinking string `json:"thinking,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// AnthropicImageSource represents an image source in Anthropic format
|
|
type AnthropicImageSource struct {
|
|
Type string `json:"type"`
|
|
MediaType string `json:"media_type"`
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
// AnthropicResponse represents a response from the Anthropic Messages API
|
|
type AnthropicResponse struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Role string `json:"role"`
|
|
Content []AnthropicContentBlock `json:"content"`
|
|
Model string `json:"model"`
|
|
StopReason *string `json:"stop_reason"`
|
|
StopSequence *string `json:"stop_sequence,omitempty"`
|
|
Usage AnthropicUsage `json:"usage"`
|
|
}
|
|
|
|
// AnthropicUsage represents token usage in Anthropic format
|
|
type AnthropicUsage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
}
|
|
|
|
// AnthropicStreamEvent represents a streaming event from the Anthropic API
|
|
type AnthropicStreamEvent struct {
|
|
Type string `json:"type"`
|
|
Index *int `json:"index,omitempty"`
|
|
ContentBlock *AnthropicContentBlock `json:"content_block,omitempty"`
|
|
Delta *AnthropicStreamDelta `json:"delta,omitempty"`
|
|
Message *AnthropicStreamMessage `json:"message,omitempty"`
|
|
Usage *AnthropicUsage `json:"usage,omitempty"`
|
|
Error *AnthropicError `json:"error,omitempty"`
|
|
}
|
|
|
|
// AnthropicStreamDelta represents the delta in a streaming response
|
|
type AnthropicStreamDelta struct {
|
|
Type string `json:"type,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
PartialJSON string `json:"partial_json,omitempty"`
|
|
StopReason *string `json:"stop_reason,omitempty"`
|
|
StopSequence *string `json:"stop_sequence,omitempty"`
|
|
Thinking string `json:"thinking,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// AnthropicStreamMessage represents the message object in streaming events
|
|
type AnthropicStreamMessage struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Role string `json:"role"`
|
|
Content []AnthropicContentBlock `json:"content"`
|
|
Model string `json:"model"`
|
|
StopReason *string `json:"stop_reason"`
|
|
StopSequence *string `json:"stop_sequence,omitempty"`
|
|
Usage AnthropicUsage `json:"usage"`
|
|
}
|
|
|
|
// AnthropicErrorResponse represents an error response from the Anthropic API
|
|
type AnthropicErrorResponse struct {
|
|
Type string `json:"type"`
|
|
Error AnthropicError `json:"error"`
|
|
}
|
|
|
|
// AnthropicError represents an error in the Anthropic format
|
|
type AnthropicError struct {
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// GetStringContent extracts the string content from an AnthropicMessage
|
|
// Content can be either a string or an array of content blocks
|
|
func (m *AnthropicMessage) GetStringContent() string {
|
|
switch content := m.Content.(type) {
|
|
case string:
|
|
return content
|
|
case []any:
|
|
var result string
|
|
for _, block := range content {
|
|
if blockMap, ok := block.(map[string]any); ok {
|
|
if blockMap["type"] == "text" {
|
|
if text, ok := blockMap["text"].(string); ok {
|
|
result += text
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetContentBlocks extracts content blocks from an AnthropicMessage
|
|
func (m *AnthropicMessage) GetContentBlocks() []AnthropicContentBlock {
|
|
switch content := m.Content.(type) {
|
|
case string:
|
|
return []AnthropicContentBlock{{Type: "text", Text: content}}
|
|
case []any:
|
|
var blocks []AnthropicContentBlock
|
|
for _, block := range content {
|
|
if blockMap, ok := block.(map[string]any); ok {
|
|
cb := AnthropicContentBlock{}
|
|
data, err := json.Marshal(blockMap)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := json.Unmarshal(data, &cb); err != nil {
|
|
continue
|
|
}
|
|
blocks = append(blocks, cb)
|
|
}
|
|
}
|
|
return blocks
|
|
}
|
|
return nil
|
|
}
|