Skip to content

Commit 770939b

Browse files
authored
Merge pull request #2438 from dgageot/fix-openai-issue
fix: preserve assistant text when tool calls present in Responses API conversion
2 parents 5bf07db + e15c9e1 commit 770939b

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)