fix: preserve tool result follow-up text

This commit is contained in:
Emanuel Casco 2026-05-07 09:40:38 +02:00
parent 6e004b8eff
commit 1ecb921068
2 changed files with 44 additions and 1 deletions

View file

@ -534,6 +534,13 @@ func cachedReasoningContent(calls []OAIToolCall) string {
return reasoning
}
}
if len(calls) > 0 {
// Moonshot/Kimi rejects follow-up assistant tool-call messages when
// thinking is enabled unless reasoning_content is present. Some
// OpenAI-compatible streams omit reasoning_content on the initial tool
// call, so provide a minimal placeholder for replayed tool-call history.
return "Tool call requested."
}
return ""
}
@ -615,7 +622,14 @@ func contentToOpenAI(m AMessage) []OAIMessage {
return []OAIMessage{msg}
}
if len(toolMsgs) > 0 {
return toolMsgs
out := append([]OAIMessage{}, toolMsgs...)
if userText := strings.TrimSpace(text.String()); userText != "" {
// Anthropic can send a user's next text in the same content array as
// tool_result blocks. Preserve that text as the next user message;
// dropping it makes the model answer the previous tool result again.
out = append(out, OAIMessage{Role: m.Role, Content: userText})
}
return out
}
return []OAIMessage{{Role: m.Role, Content: text.String()}}
}

View file

@ -112,11 +112,40 @@ func TestResponsesInputFunctionCallUsesCallID(t *testing.T) {
if messages[0].ToolCalls[0].ID != "call_123" {
t.Fatalf("tool call ID should match call_id for follow-up tool output: %+v", messages[0].ToolCalls[0])
}
if messages[0].ReasoningContent == "" {
t.Fatalf("assistant tool call history should include fallback reasoning_content: %+v", messages[0])
}
if messages[1].ToolCallID != "call_123" {
t.Fatalf("bad tool output ID: %+v", messages[1])
}
}
func TestAnthropicToolUseHistoryIncludesFallbackReasoning(t *testing.T) {
messages := contentToOpenAI(AMessage{Role: "assistant", Content: []byte(`[{"type":"tool_use","id":"call_123","name":"Bash","input":{"command":"pwd"}}]`)})
if len(messages) != 1 {
t.Fatalf("got %d messages", len(messages))
}
if messages[0].Role != "assistant" || len(messages[0].ToolCalls) != 1 {
t.Fatalf("bad tool call conversion: %+v", messages[0])
}
if messages[0].ReasoningContent == "" {
t.Fatalf("assistant tool call history should include fallback reasoning_content: %+v", messages[0])
}
}
func TestAnthropicToolResultPreservesFollowingUserText(t *testing.T) {
messages := contentToOpenAI(AMessage{Role: "user", Content: []byte(`[{"type":"tool_result","tool_use_id":"call_123","content":"09:33:16"},{"type":"text","text":"https://figma.example/design what's going on here?"}]`)})
if len(messages) != 2 {
t.Fatalf("got %d messages: %+v", len(messages), messages)
}
if messages[0].Role != "tool" || messages[0].ToolCallID != "call_123" || messages[0].Content != "09:33:16" {
t.Fatalf("bad tool result conversion: %+v", messages[0])
}
if messages[1].Role != "user" || !strings.Contains(messages[1].Content, "figma.example") {
t.Fatalf("following user text was not preserved: %+v", messages[1])
}
}
func TestStreamAnthropicForwardsToolCalls(t *testing.T) {
reasoningContentCache.Lock()
reasoningContentCache.byCallID = map[string]string{}