Skip to content

Commit e15c9e1

Browse files
committed
fix: preserve assistant text when tool calls present in Responses API conversion
When converting assistant messages with both text and tool calls to the Responses API format, the text content was silently discarded via continue. Emit the text as a separate assistant message before the function calls. Includes a regression test. Assisted-By: docker-agent
1 parent e5f7cfa commit e15c9e1

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

pkg/model/provider/openai/client.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -565,20 +565,30 @@ func convertMessagesToResponseInput(messages []chat.Message) []responses.Respons
565565
},
566566
}
567567
} else {
568-
// Assistant message with tool calls - convert to response input item with function calls
568+
// Assistant message with tool calls - emit text as a separate assistant
569+
// message before the function calls so it is not lost.
570+
if strings.TrimSpace(msg.Content) != "" {
571+
input = append(input, responses.ResponseInputItemUnionParam{
572+
OfMessage: &responses.EasyInputMessageParam{
573+
Role: responses.EasyInputMessageRoleAssistant,
574+
Content: responses.EasyInputMessageContentUnionParam{
575+
OfString: param.NewOpt(msg.Content),
576+
},
577+
},
578+
})
579+
}
569580
for _, toolCall := range msg.ToolCalls {
570581
if toolCall.Type == "function" {
571-
funcCallItem := responses.ResponseInputItemUnionParam{
582+
input = append(input, responses.ResponseInputItemUnionParam{
572583
OfFunctionCall: &responses.ResponseFunctionToolCallParam{
573584
CallID: toolCall.ID,
574585
Name: toolCall.Function.Name,
575586
Arguments: toolCall.Function.Arguments,
576587
},
577-
}
578-
input = append(input, funcCallItem)
588+
})
579589
}
580590
}
581-
continue // Don't add the assistant message itself
591+
continue
582592
}
583593

584594
case chat.MessageRoleSystem:

pkg/model/provider/openai/client_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,41 @@ func TestConvertMessagesToResponseInput_OrphanedFunctionCall(t *testing.T) {
4646
assert.Contains(t, outputIDs, "call_2")
4747
}
4848

49+
func TestConvertMessagesToResponseInput_AssistantTextWithToolCalls(t *testing.T) {
50+
// When an assistant message has both text content and tool calls,
51+
// the text must not be silently discarded.
52+
messages := []chat.Message{
53+
{Role: chat.MessageRoleUser, Content: "hello"},
54+
{
55+
Role: chat.MessageRoleAssistant,
56+
Content: "Let me search that for you.",
57+
ToolCalls: []tools.ToolCall{
58+
{ID: "call_1", Type: "function", Function: tools.FunctionCall{Name: "search", Arguments: `{"q":"test"}`}},
59+
},
60+
},
61+
{Role: chat.MessageRoleTool, Content: "result", ToolCallID: "call_1"},
62+
}
63+
64+
input := convertMessagesToResponseInput(messages)
65+
66+
// We expect: user message, assistant text message, function call, function call output.
67+
var foundAssistantText bool
68+
var foundFunctionCall bool
69+
for _, item := range input {
70+
if item.OfMessage != nil && item.OfMessage.Role == "assistant" {
71+
if item.OfMessage.Content.OfString.Valid() && item.OfMessage.Content.OfString.Value == "Let me search that for you." {
72+
foundAssistantText = true
73+
}
74+
}
75+
if item.OfFunctionCall != nil && item.OfFunctionCall.CallID == "call_1" {
76+
foundFunctionCall = true
77+
}
78+
}
79+
80+
assert.True(t, foundFunctionCall, "function call should be present")
81+
assert.True(t, foundAssistantText, "assistant text content should not be discarded when tool calls are present")
82+
}
83+
4984
func TestConvertMessagesToResponseInput_NoOrphans(t *testing.T) {
5085
// All tool calls have matching results — no placeholder needed.
5186
messages := []chat.Message{

0 commit comments

Comments
 (0)