From 1ecb921068cd65a266ce5bb6b206a37ef20e55b5 Mon Sep 17 00:00:00 2001 From: Emanuel Casco Date: Thu, 7 May 2026 09:40:38 +0200 Subject: [PATCH] fix: preserve tool result follow-up text --- cmd/ocgo/main.go | 16 +++++++++++++++- cmd/ocgo/main_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/cmd/ocgo/main.go b/cmd/ocgo/main.go index 91a209c..6e9e0d6 100644 --- a/cmd/ocgo/main.go +++ b/cmd/ocgo/main.go @@ -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()}} } diff --git a/cmd/ocgo/main_test.go b/cmd/ocgo/main_test.go index a3b57ba..4183b87 100644 --- a/cmd/ocgo/main_test.go +++ b/cmd/ocgo/main_test.go @@ -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{}