mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 19:54:03 +02:00
anthropic: fix empty inputs in content blocks (#15105)
* anthropic: fix empty inputs in content blocks
When we switched to `api.ToolCallFunctionArguments`, `omitempty` stopped
doing what we were relying on it for before. This would cause non-tool
content blocks to have an `"input": {}` field, which doesn't match our
old behavior.
* use omitzero instead
This commit is contained in:
@@ -123,7 +123,7 @@ type ContentBlock struct {
|
|||||||
// For tool_use and server_tool_use blocks
|
// For tool_use and server_tool_use blocks
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Input api.ToolCallFunctionArguments `json:"input,omitempty"`
|
Input api.ToolCallFunctionArguments `json:"input,omitzero"`
|
||||||
|
|
||||||
// For tool_result and web_search_tool_result blocks
|
// For tool_result and web_search_tool_result blocks
|
||||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||||
@@ -868,7 +868,6 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
slog.Error("failed to marshal tool arguments", "error", err, "tool_id", tc.ID)
|
slog.Error("failed to marshal tool arguments", "error", err, "tool_id", tc.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
events = append(events, StreamEvent{
|
events = append(events, StreamEvent{
|
||||||
Event: "content_block_start",
|
Event: "content_block_start",
|
||||||
Data: ContentBlockStartEvent{
|
Data: ContentBlockStartEvent{
|
||||||
@@ -878,7 +877,7 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
|||||||
Type: "tool_use",
|
Type: "tool_use",
|
||||||
ID: tc.ID,
|
ID: tc.ID,
|
||||||
Name: tc.Function.Name,
|
Name: tc.Function.Name,
|
||||||
Input: api.ToolCallFunctionArguments{},
|
Input: api.NewToolCallFunctionArguments(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1072,6 +1072,57 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContentBlockJSON_NonToolBlocksDoNotIncludeInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
block ContentBlock
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "text block",
|
||||||
|
block: ContentBlock{
|
||||||
|
Type: "text",
|
||||||
|
Text: ptr("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thinking block",
|
||||||
|
block: ContentBlock{
|
||||||
|
Type: "thinking",
|
||||||
|
Thinking: ptr("let me think"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image block",
|
||||||
|
block: ContentBlock{
|
||||||
|
Type: "image",
|
||||||
|
Source: &ImageSource{
|
||||||
|
Type: "base64",
|
||||||
|
MediaType: "image/png",
|
||||||
|
Data: testImage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
data, err := json.Marshal(tt.block)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result["input"]; ok {
|
||||||
|
t.Fatalf("unexpected input field in non-tool block JSON: %s", string(data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
||||||
t.Run("text block start includes empty text", func(t *testing.T) {
|
t.Run("text block start includes empty text", func(t *testing.T) {
|
||||||
conv := NewStreamConverter("msg_123", "test-model", 0)
|
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||||
@@ -1092,7 +1143,9 @@ func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
|||||||
// Marshal and verify the text field is present
|
// Marshal and verify the text field is present
|
||||||
data, _ := json.Marshal(start)
|
data, _ := json.Marshal(start)
|
||||||
var result map[string]any
|
var result map[string]any
|
||||||
json.Unmarshal(data, &result)
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal content_block_start JSON: %v", err)
|
||||||
|
}
|
||||||
cb := result["content_block"].(map[string]any)
|
cb := result["content_block"].(map[string]any)
|
||||||
if _, ok := cb["text"]; !ok {
|
if _, ok := cb["text"]; !ok {
|
||||||
t.Error("content_block_start for text should include 'text' field")
|
t.Error("content_block_start for text should include 'text' field")
|
||||||
@@ -1139,6 +1192,64 @@ func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
|||||||
t.Error("expected thinking content_block_start event")
|
t.Error("expected thinking content_block_start event")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("tool_use block start includes empty input object", func(t *testing.T) {
|
||||||
|
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||||
|
|
||||||
|
resp := api.ChatResponse{
|
||||||
|
Model: "test-model",
|
||||||
|
Message: api.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
ID: "call_123",
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: makeArgs("location", "Paris"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
events := conv.Process(resp)
|
||||||
|
|
||||||
|
var foundToolStart bool
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Event == "content_block_start" {
|
||||||
|
if start, ok := e.Data.(ContentBlockStartEvent); ok {
|
||||||
|
if start.ContentBlock.Type == "tool_use" {
|
||||||
|
foundToolStart = true
|
||||||
|
if start.ContentBlock.Input.Len() != 0 {
|
||||||
|
t.Errorf("expected empty input object, got len=%d", start.ContentBlock.Input.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(start)
|
||||||
|
var result map[string]any
|
||||||
|
json.Unmarshal(data, &result)
|
||||||
|
cb := result["content_block"].(map[string]any)
|
||||||
|
input, ok := cb["input"]
|
||||||
|
if !ok {
|
||||||
|
t.Error("content_block_start for tool_use should include 'input' field")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inputMap, ok := input.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("input field should be an object, got %T", input)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(inputMap) != 0 {
|
||||||
|
t.Errorf("expected empty input object in content_block_start, got %v", inputMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundToolStart {
|
||||||
|
t.Error("expected tool_use content_block_start event")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEstimateTokens_SimpleMessage(t *testing.T) {
|
func TestEstimateTokens_SimpleMessage(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user