mirror of
https://github.com/ollama/ollama.git
synced 2026-01-12 00:06:57 +08:00
* api: add Anthropic Messages API compatibility layer Add middleware to support the Anthropic Messages API format at /v1/messages. This enables tools like Claude Code to work with Ollama local and cloud models through the Anthropic API interface.
779 lines
20 KiB
Go
779 lines
20 KiB
Go
package anthropic
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
)
|
|
|
|
// Error types matching Anthropic API
|
|
type Error struct {
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type ErrorResponse struct {
|
|
Type string `json:"type"` // always "error"
|
|
Error Error `json:"error"`
|
|
RequestID string `json:"request_id,omitempty"`
|
|
}
|
|
|
|
// NewError creates a new ErrorResponse with the appropriate error type based on HTTP status code
|
|
func NewError(code int, message string) ErrorResponse {
|
|
var etype string
|
|
switch code {
|
|
case http.StatusBadRequest:
|
|
etype = "invalid_request_error"
|
|
case http.StatusUnauthorized:
|
|
etype = "authentication_error"
|
|
case http.StatusForbidden:
|
|
etype = "permission_error"
|
|
case http.StatusNotFound:
|
|
etype = "not_found_error"
|
|
case http.StatusTooManyRequests:
|
|
etype = "rate_limit_error"
|
|
case http.StatusServiceUnavailable, 529:
|
|
etype = "overloaded_error"
|
|
default:
|
|
etype = "api_error"
|
|
}
|
|
|
|
return ErrorResponse{
|
|
Type: "error",
|
|
Error: Error{Type: etype, Message: message},
|
|
RequestID: generateID("req"),
|
|
}
|
|
}
|
|
|
|
// Request types
|
|
|
|
// MessagesRequest represents an Anthropic Messages API request
|
|
type MessagesRequest struct {
|
|
Model string `json:"model"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
Messages []MessageParam `json:"messages"`
|
|
System any `json:"system,omitempty"` // string or []ContentBlock
|
|
Stream bool `json:"stream,omitempty"`
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
TopK *int `json:"top_k,omitempty"`
|
|
StopSequences []string `json:"stop_sequences,omitempty"`
|
|
Tools []Tool `json:"tools,omitempty"`
|
|
ToolChoice *ToolChoice `json:"tool_choice,omitempty"`
|
|
Thinking *ThinkingConfig `json:"thinking,omitempty"`
|
|
Metadata *Metadata `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// MessageParam represents a message in the request
|
|
type MessageParam struct {
|
|
Role string `json:"role"` // "user" or "assistant"
|
|
Content any `json:"content"` // string or []ContentBlock
|
|
}
|
|
|
|
// ContentBlock represents a content block in a message.
|
|
// Text and Thinking use pointers so they serialize as the field being present (even if empty)
|
|
// only when set, which is required for SDK streaming accumulation.
|
|
type ContentBlock struct {
|
|
Type string `json:"type"` // text, image, tool_use, tool_result, thinking
|
|
|
|
// For text blocks - pointer so field only appears when set (SDK requires it for accumulation)
|
|
Text *string `json:"text,omitempty"`
|
|
|
|
// For image blocks
|
|
Source *ImageSource `json:"source,omitempty"`
|
|
|
|
// For tool_use blocks
|
|
ID string `json:"id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Input any `json:"input,omitempty"`
|
|
|
|
// For tool_result blocks
|
|
ToolUseID string `json:"tool_use_id,omitempty"`
|
|
Content any `json:"content,omitempty"` // string or []ContentBlock
|
|
IsError bool `json:"is_error,omitempty"`
|
|
|
|
// For thinking blocks - pointer so field only appears when set (SDK requires it for accumulation)
|
|
Thinking *string `json:"thinking,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// ImageSource represents the source of an image
|
|
type ImageSource struct {
|
|
Type string `json:"type"` // "base64" or "url"
|
|
MediaType string `json:"media_type,omitempty"`
|
|
Data string `json:"data,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
// Tool represents a tool definition
|
|
type Tool struct {
|
|
Type string `json:"type,omitempty"` // "custom" for user-defined tools
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
InputSchema json.RawMessage `json:"input_schema,omitempty"`
|
|
}
|
|
|
|
// ToolChoice controls how the model uses tools
|
|
type ToolChoice struct {
|
|
Type string `json:"type"` // "auto", "any", "tool", "none"
|
|
Name string `json:"name,omitempty"`
|
|
DisableParallelToolUse bool `json:"disable_parallel_tool_use,omitempty"`
|
|
}
|
|
|
|
// ThinkingConfig controls extended thinking
|
|
type ThinkingConfig struct {
|
|
Type string `json:"type"` // "enabled" or "disabled"
|
|
BudgetTokens int `json:"budget_tokens,omitempty"`
|
|
}
|
|
|
|
// Metadata for the request
|
|
type Metadata struct {
|
|
UserID string `json:"user_id,omitempty"`
|
|
}
|
|
|
|
// Response types
|
|
|
|
// MessagesResponse represents an Anthropic Messages API response
|
|
type MessagesResponse struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"` // "message"
|
|
Role string `json:"role"` // "assistant"
|
|
Model string `json:"model"`
|
|
Content []ContentBlock `json:"content"`
|
|
StopReason string `json:"stop_reason,omitempty"`
|
|
StopSequence string `json:"stop_sequence,omitempty"`
|
|
Usage Usage `json:"usage"`
|
|
}
|
|
|
|
// Usage contains token usage information
|
|
type Usage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
}
|
|
|
|
// Streaming event types
|
|
|
|
// MessageStartEvent is sent at the start of streaming
|
|
type MessageStartEvent struct {
|
|
Type string `json:"type"` // "message_start"
|
|
Message MessagesResponse `json:"message"`
|
|
}
|
|
|
|
// ContentBlockStartEvent signals the start of a content block
|
|
type ContentBlockStartEvent struct {
|
|
Type string `json:"type"` // "content_block_start"
|
|
Index int `json:"index"`
|
|
ContentBlock ContentBlock `json:"content_block"`
|
|
}
|
|
|
|
// ContentBlockDeltaEvent contains incremental content updates
|
|
type ContentBlockDeltaEvent struct {
|
|
Type string `json:"type"` // "content_block_delta"
|
|
Index int `json:"index"`
|
|
Delta Delta `json:"delta"`
|
|
}
|
|
|
|
// Delta represents an incremental update
|
|
type Delta struct {
|
|
Type string `json:"type"` // "text_delta", "input_json_delta", "thinking_delta", "signature_delta"
|
|
Text string `json:"text,omitempty"`
|
|
PartialJSON string `json:"partial_json,omitempty"`
|
|
Thinking string `json:"thinking,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
}
|
|
|
|
// ContentBlockStopEvent signals the end of a content block
|
|
type ContentBlockStopEvent struct {
|
|
Type string `json:"type"` // "content_block_stop"
|
|
Index int `json:"index"`
|
|
}
|
|
|
|
// MessageDeltaEvent contains updates to the message
|
|
type MessageDeltaEvent struct {
|
|
Type string `json:"type"` // "message_delta"
|
|
Delta MessageDelta `json:"delta"`
|
|
Usage DeltaUsage `json:"usage"`
|
|
}
|
|
|
|
// MessageDelta contains stop information
|
|
type MessageDelta struct {
|
|
StopReason string `json:"stop_reason,omitempty"`
|
|
StopSequence string `json:"stop_sequence,omitempty"`
|
|
}
|
|
|
|
// DeltaUsage contains cumulative token usage
|
|
type DeltaUsage struct {
|
|
OutputTokens int `json:"output_tokens"`
|
|
}
|
|
|
|
// MessageStopEvent signals the end of the message
|
|
type MessageStopEvent struct {
|
|
Type string `json:"type"` // "message_stop"
|
|
}
|
|
|
|
// PingEvent is a keepalive event
|
|
type PingEvent struct {
|
|
Type string `json:"type"` // "ping"
|
|
}
|
|
|
|
// StreamErrorEvent is an error during streaming
|
|
type StreamErrorEvent struct {
|
|
Type string `json:"type"` // "error"
|
|
Error Error `json:"error"`
|
|
}
|
|
|
|
// FromMessagesRequest converts an Anthropic MessagesRequest to an Ollama api.ChatRequest
|
|
func FromMessagesRequest(r MessagesRequest) (*api.ChatRequest, error) {
|
|
var messages []api.Message
|
|
|
|
if r.System != nil {
|
|
switch sys := r.System.(type) {
|
|
case string:
|
|
if sys != "" {
|
|
messages = append(messages, api.Message{Role: "system", Content: sys})
|
|
}
|
|
case []any:
|
|
// System can be an array of content blocks
|
|
var content strings.Builder
|
|
for _, block := range sys {
|
|
if blockMap, ok := block.(map[string]any); ok {
|
|
if blockMap["type"] == "text" {
|
|
if text, ok := blockMap["text"].(string); ok {
|
|
content.WriteString(text)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if content.Len() > 0 {
|
|
messages = append(messages, api.Message{Role: "system", Content: content.String()})
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, msg := range r.Messages {
|
|
converted, err := convertMessage(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
messages = append(messages, converted...)
|
|
}
|
|
|
|
options := make(map[string]any)
|
|
|
|
options["num_predict"] = r.MaxTokens
|
|
|
|
if r.Temperature != nil {
|
|
options["temperature"] = *r.Temperature
|
|
}
|
|
|
|
if r.TopP != nil {
|
|
options["top_p"] = *r.TopP
|
|
}
|
|
|
|
if r.TopK != nil {
|
|
options["top_k"] = *r.TopK
|
|
}
|
|
|
|
if len(r.StopSequences) > 0 {
|
|
options["stop"] = r.StopSequences
|
|
}
|
|
|
|
var tools api.Tools
|
|
for _, t := range r.Tools {
|
|
tool, err := convertTool(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tools = append(tools, tool)
|
|
}
|
|
|
|
var think *api.ThinkValue
|
|
if r.Thinking != nil && r.Thinking.Type == "enabled" {
|
|
think = &api.ThinkValue{Value: true}
|
|
}
|
|
|
|
stream := r.Stream
|
|
|
|
return &api.ChatRequest{
|
|
Model: r.Model,
|
|
Messages: messages,
|
|
Options: options,
|
|
Stream: &stream,
|
|
Tools: tools,
|
|
Think: think,
|
|
}, nil
|
|
}
|
|
|
|
// convertMessage converts an Anthropic MessageParam to Ollama api.Message(s)
|
|
func convertMessage(msg MessageParam) ([]api.Message, error) {
|
|
var messages []api.Message
|
|
role := strings.ToLower(msg.Role)
|
|
|
|
switch content := msg.Content.(type) {
|
|
case string:
|
|
messages = append(messages, api.Message{Role: role, Content: content})
|
|
|
|
case []any:
|
|
var textContent strings.Builder
|
|
var images []api.ImageData
|
|
var toolCalls []api.ToolCall
|
|
var thinking string
|
|
var toolResults []api.Message
|
|
|
|
for _, block := range content {
|
|
blockMap, ok := block.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("invalid content block format")
|
|
}
|
|
|
|
blockType, _ := blockMap["type"].(string)
|
|
|
|
switch blockType {
|
|
case "text":
|
|
if text, ok := blockMap["text"].(string); ok {
|
|
textContent.WriteString(text)
|
|
}
|
|
|
|
case "image":
|
|
source, ok := blockMap["source"].(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("invalid image source")
|
|
}
|
|
|
|
sourceType, _ := source["type"].(string)
|
|
if sourceType == "base64" {
|
|
data, _ := source["data"].(string)
|
|
decoded, err := base64.StdEncoding.DecodeString(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid base64 image data: %w", err)
|
|
}
|
|
images = append(images, decoded)
|
|
} else {
|
|
return nil, fmt.Errorf("invalid image source type: %s. Only base64 images are supported.", sourceType)
|
|
}
|
|
// URL images would need to be fetched - skip for now
|
|
|
|
case "tool_use":
|
|
id, ok := blockMap["id"].(string)
|
|
if !ok {
|
|
return nil, errors.New("tool_use block missing required 'id' field")
|
|
}
|
|
name, ok := blockMap["name"].(string)
|
|
if !ok {
|
|
return nil, errors.New("tool_use block missing required 'name' field")
|
|
}
|
|
tc := api.ToolCall{
|
|
ID: id,
|
|
Function: api.ToolCallFunction{
|
|
Name: name,
|
|
},
|
|
}
|
|
if input, ok := blockMap["input"].(map[string]any); ok {
|
|
tc.Function.Arguments = mapToArgs(input)
|
|
}
|
|
toolCalls = append(toolCalls, tc)
|
|
|
|
case "tool_result":
|
|
toolUseID, _ := blockMap["tool_use_id"].(string)
|
|
var resultContent string
|
|
|
|
switch c := blockMap["content"].(type) {
|
|
case string:
|
|
resultContent = c
|
|
case []any:
|
|
for _, cb := range c {
|
|
if cbMap, ok := cb.(map[string]any); ok {
|
|
if cbMap["type"] == "text" {
|
|
if text, ok := cbMap["text"].(string); ok {
|
|
resultContent += text
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
toolResults = append(toolResults, api.Message{
|
|
Role: "tool",
|
|
Content: resultContent,
|
|
ToolCallID: toolUseID,
|
|
})
|
|
|
|
case "thinking":
|
|
if t, ok := blockMap["thinking"].(string); ok {
|
|
thinking = t
|
|
}
|
|
}
|
|
}
|
|
|
|
if textContent.Len() > 0 || len(images) > 0 || len(toolCalls) > 0 || thinking != "" {
|
|
m := api.Message{
|
|
Role: role,
|
|
Content: textContent.String(),
|
|
Images: images,
|
|
ToolCalls: toolCalls,
|
|
Thinking: thinking,
|
|
}
|
|
messages = append(messages, m)
|
|
}
|
|
|
|
// Add tool results as separate messages
|
|
messages = append(messages, toolResults...)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("invalid message content type: %T", content)
|
|
}
|
|
|
|
return messages, nil
|
|
}
|
|
|
|
// convertTool converts an Anthropic Tool to an Ollama api.Tool
|
|
func convertTool(t Tool) (api.Tool, error) {
|
|
var params api.ToolFunctionParameters
|
|
if len(t.InputSchema) > 0 {
|
|
if err := json.Unmarshal(t.InputSchema, ¶ms); err != nil {
|
|
return api.Tool{}, fmt.Errorf("invalid input_schema for tool %q: %w", t.Name, err)
|
|
}
|
|
}
|
|
|
|
return api.Tool{
|
|
Type: "function",
|
|
Function: api.ToolFunction{
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
Parameters: params,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// ToMessagesResponse converts an Ollama api.ChatResponse to an Anthropic MessagesResponse
|
|
func ToMessagesResponse(id string, r api.ChatResponse) MessagesResponse {
|
|
var content []ContentBlock
|
|
|
|
if r.Message.Thinking != "" {
|
|
content = append(content, ContentBlock{
|
|
Type: "thinking",
|
|
Thinking: ptr(r.Message.Thinking),
|
|
})
|
|
}
|
|
|
|
if r.Message.Content != "" {
|
|
content = append(content, ContentBlock{
|
|
Type: "text",
|
|
Text: ptr(r.Message.Content),
|
|
})
|
|
}
|
|
|
|
for _, tc := range r.Message.ToolCalls {
|
|
content = append(content, ContentBlock{
|
|
Type: "tool_use",
|
|
ID: tc.ID,
|
|
Name: tc.Function.Name,
|
|
Input: tc.Function.Arguments,
|
|
})
|
|
}
|
|
|
|
stopReason := mapStopReason(r.DoneReason, len(r.Message.ToolCalls) > 0)
|
|
|
|
return MessagesResponse{
|
|
ID: id,
|
|
Type: "message",
|
|
Role: "assistant",
|
|
Model: r.Model,
|
|
Content: content,
|
|
StopReason: stopReason,
|
|
Usage: Usage{
|
|
InputTokens: r.Metrics.PromptEvalCount,
|
|
OutputTokens: r.Metrics.EvalCount,
|
|
},
|
|
}
|
|
}
|
|
|
|
// mapStopReason converts Ollama done_reason to Anthropic stop_reason
|
|
func mapStopReason(reason string, hasToolCalls bool) string {
|
|
if hasToolCalls {
|
|
return "tool_use"
|
|
}
|
|
|
|
switch reason {
|
|
case "stop":
|
|
return "end_turn"
|
|
case "length":
|
|
return "max_tokens"
|
|
default:
|
|
if reason != "" {
|
|
return "stop_sequence"
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// StreamConverter manages state for converting Ollama streaming responses to Anthropic format
|
|
type StreamConverter struct {
|
|
ID string
|
|
Model string
|
|
firstWrite bool
|
|
contentIndex int
|
|
inputTokens int
|
|
outputTokens int
|
|
thinkingStarted bool
|
|
thinkingDone bool
|
|
textStarted bool
|
|
toolCallsSent map[string]bool
|
|
}
|
|
|
|
func NewStreamConverter(id, model string) *StreamConverter {
|
|
return &StreamConverter{
|
|
ID: id,
|
|
Model: model,
|
|
firstWrite: true,
|
|
toolCallsSent: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// StreamEvent represents a streaming event to be sent to the client
|
|
type StreamEvent struct {
|
|
Event string
|
|
Data any
|
|
}
|
|
|
|
// Process converts an Ollama ChatResponse to Anthropic streaming events
|
|
func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|
var events []StreamEvent
|
|
|
|
if c.firstWrite {
|
|
c.firstWrite = false
|
|
c.inputTokens = r.Metrics.PromptEvalCount
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "message_start",
|
|
Data: MessageStartEvent{
|
|
Type: "message_start",
|
|
Message: MessagesResponse{
|
|
ID: c.ID,
|
|
Type: "message",
|
|
Role: "assistant",
|
|
Model: c.Model,
|
|
Content: []ContentBlock{},
|
|
Usage: Usage{
|
|
InputTokens: c.inputTokens,
|
|
OutputTokens: 0,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if r.Message.Thinking != "" && !c.thinkingDone {
|
|
if !c.thinkingStarted {
|
|
c.thinkingStarted = true
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_start",
|
|
Data: ContentBlockStartEvent{
|
|
Type: "content_block_start",
|
|
Index: c.contentIndex,
|
|
ContentBlock: ContentBlock{
|
|
Type: "thinking",
|
|
Thinking: ptr(""),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_delta",
|
|
Data: ContentBlockDeltaEvent{
|
|
Type: "content_block_delta",
|
|
Index: c.contentIndex,
|
|
Delta: Delta{
|
|
Type: "thinking_delta",
|
|
Thinking: r.Message.Thinking,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if r.Message.Content != "" {
|
|
if c.thinkingStarted && !c.thinkingDone {
|
|
c.thinkingDone = true
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_stop",
|
|
Data: ContentBlockStopEvent{
|
|
Type: "content_block_stop",
|
|
Index: c.contentIndex,
|
|
},
|
|
})
|
|
c.contentIndex++
|
|
}
|
|
|
|
if !c.textStarted {
|
|
c.textStarted = true
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_start",
|
|
Data: ContentBlockStartEvent{
|
|
Type: "content_block_start",
|
|
Index: c.contentIndex,
|
|
ContentBlock: ContentBlock{
|
|
Type: "text",
|
|
Text: ptr(""),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_delta",
|
|
Data: ContentBlockDeltaEvent{
|
|
Type: "content_block_delta",
|
|
Index: c.contentIndex,
|
|
Delta: Delta{
|
|
Type: "text_delta",
|
|
Text: r.Message.Content,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
for _, tc := range r.Message.ToolCalls {
|
|
if c.toolCallsSent[tc.ID] {
|
|
continue
|
|
}
|
|
|
|
if c.textStarted {
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_stop",
|
|
Data: ContentBlockStopEvent{
|
|
Type: "content_block_stop",
|
|
Index: c.contentIndex,
|
|
},
|
|
})
|
|
c.contentIndex++
|
|
c.textStarted = false
|
|
}
|
|
|
|
argsJSON, err := json.Marshal(tc.Function.Arguments)
|
|
if err != nil {
|
|
slog.Error("failed to marshal tool arguments", "error", err, "tool_id", tc.ID)
|
|
continue
|
|
}
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_start",
|
|
Data: ContentBlockStartEvent{
|
|
Type: "content_block_start",
|
|
Index: c.contentIndex,
|
|
ContentBlock: ContentBlock{
|
|
Type: "tool_use",
|
|
ID: tc.ID,
|
|
Name: tc.Function.Name,
|
|
Input: map[string]any{},
|
|
},
|
|
},
|
|
})
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_delta",
|
|
Data: ContentBlockDeltaEvent{
|
|
Type: "content_block_delta",
|
|
Index: c.contentIndex,
|
|
Delta: Delta{
|
|
Type: "input_json_delta",
|
|
PartialJSON: string(argsJSON),
|
|
},
|
|
},
|
|
})
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_stop",
|
|
Data: ContentBlockStopEvent{
|
|
Type: "content_block_stop",
|
|
Index: c.contentIndex,
|
|
},
|
|
})
|
|
|
|
c.toolCallsSent[tc.ID] = true
|
|
c.contentIndex++
|
|
}
|
|
|
|
if r.Done {
|
|
if c.textStarted {
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_stop",
|
|
Data: ContentBlockStopEvent{
|
|
Type: "content_block_stop",
|
|
Index: c.contentIndex,
|
|
},
|
|
})
|
|
} else if c.thinkingStarted && !c.thinkingDone {
|
|
events = append(events, StreamEvent{
|
|
Event: "content_block_stop",
|
|
Data: ContentBlockStopEvent{
|
|
Type: "content_block_stop",
|
|
Index: c.contentIndex,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.outputTokens = r.Metrics.EvalCount
|
|
stopReason := mapStopReason(r.DoneReason, len(c.toolCallsSent) > 0)
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "message_delta",
|
|
Data: MessageDeltaEvent{
|
|
Type: "message_delta",
|
|
Delta: MessageDelta{
|
|
StopReason: stopReason,
|
|
},
|
|
Usage: DeltaUsage{
|
|
OutputTokens: c.outputTokens,
|
|
},
|
|
},
|
|
})
|
|
|
|
events = append(events, StreamEvent{
|
|
Event: "message_stop",
|
|
Data: MessageStopEvent{
|
|
Type: "message_stop",
|
|
},
|
|
})
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
// generateID generates a unique ID with the given prefix using crypto/rand
|
|
func generateID(prefix string) string {
|
|
b := make([]byte, 12)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// Fallback to time-based ID if crypto/rand fails
|
|
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
|
|
}
|
|
return fmt.Sprintf("%s_%x", prefix, b)
|
|
}
|
|
|
|
// GenerateMessageID generates a unique message ID
|
|
func GenerateMessageID() string {
|
|
return generateID("msg")
|
|
}
|
|
|
|
// ptr returns a pointer to the given string value
|
|
func ptr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// mapToArgs converts a map to ToolCallFunctionArguments
|
|
func mapToArgs(m map[string]any) api.ToolCallFunctionArguments {
|
|
args := api.NewToolCallFunctionArguments()
|
|
for k, v := range m {
|
|
args.Set(k, v)
|
|
}
|
|
return args
|
|
}
|