diff --git a/components/model/ark/examples/intent_tool/intent_tool.go b/components/model/ark/examples/intent_tool/intent_tool.go index e2c3bb7e7..339ae4456 100644 --- a/components/model/ark/examples/intent_tool/intent_tool.go +++ b/components/model/ark/examples/intent_tool/intent_tool.go @@ -93,7 +93,7 @@ func textToolCall(ctx context.Context, chatModel model.ToolCallingChatModel) { }, { Role: schema.User, - Content: "My name is zhangsan, and my email is zhangsan@bytedance.com. Please recommend some suitable houses for me.", + Content: "My name is zhangsan, and my email is zhangsan@bytedance.com. Please search my salary.", }, }) diff --git a/components/model/ark/message_extra.go b/components/model/ark/message_extra.go index b1c9045b6..39938e87e 100644 --- a/components/model/ark/message_extra.go +++ b/components/model/ark/message_extra.go @@ -17,6 +17,8 @@ package ark import ( + "strings" + "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" ) @@ -24,6 +26,7 @@ import ( const ( keyOfRequestID = "ark-request-id" keyOfReasoningContent = "ark-reasoning-content" + keyOfReasoningID = "ark-reasoning-id" keyOfModelName = "ark-model-name" videoURLFPS = "ark-model-video-url-fps" keyOfContextID = "ark-context-id" @@ -32,6 +35,7 @@ const ( keyOfServiceTier = "ark-service-tier" keyOfPartial = "ark-partial" ImageSizeKey = "seedream-image-size" + keyOfOutputItemsOrder = "ark-output-items-order" ) type arkRequestID string @@ -40,8 +44,46 @@ type arkServiceTier string type arkResponseID string type arkContextID string type arkResponseCacheExpireAt int64 +type arkOutputItemsOrder string + +// outputItemType represents the type of an output item in the responses API. +type outputItemType string + +const ( + // outputItemTypeMessage represents a message output item. + outputItemTypeMessage outputItemType = "message" + // outputItemTypeReasoning represents a reasoning output item. + outputItemTypeReasoning outputItemType = "reasoning" + // outputItemTypeFunctionCall represents a function call output item. + outputItemTypeFunctionCall outputItemType = "function_call" +) func init() { + compose.RegisterStreamChunkConcatFunc(func(ts []arkOutputItemsOrder) (arkOutputItemsOrder, error) { + if len(ts) == 0 { + return "", nil + } + if len(ts) == 1 { + return ts[0], nil + } + var ret []outputItemType + var lastType outputItemType + for _, t := range ts { + if len(t) == 0 { + continue + } + itemTypes := parseOutputItemsOrder(t) + for _, it := range itemTypes { + if it != lastType { + ret = append(ret, it) + lastType = it + } + } + } + return encodeOutputItemsOrder(ret), nil + }) + schema.RegisterName[arkOutputItemsOrder]("_eino_ext_ark_output_items_order") + compose.RegisterStreamChunkConcatFunc(func(chunks []arkRequestID) (final arkRequestID, err error) { if len(chunks) == 0 { return "", nil @@ -120,6 +162,14 @@ func setReasoningContent(msg *schema.Message, reasoningContent string) { setMsgExtra(msg, keyOfReasoningContent, reasoningContent) } +func getReasoningID(msg *schema.Message) (string, bool) { + return getMsgExtraValue[string](msg, keyOfReasoningID) +} + +func setReasoningID(msg *schema.Message, id string) { + setMsgExtra(msg, keyOfReasoningID, id) +} + func GetModelName(msg *schema.Message) (string, bool) { modelName, ok := getMsgExtraValue[arkModelName](msg, keyOfModelName) if !ok { @@ -392,3 +442,54 @@ func getPartial(msg *schema.Message) bool { } return v } + +// getOutputItemsOrder returns the output items order from the message. +// This records the original order of items (message, reasoning, function_call) in the responses API output, +// so that when converting back to InputItem list, the original order can be preserved. +// Only available for ResponsesAPI. +// +// The order is stored as a comma-separated string in Extra, e.g. "message,reasoning,function_call,function_call". +func getOutputItemsOrder(msg *schema.Message) ([]outputItemType, bool) { + orderStr, ok := getMsgExtraValue[arkOutputItemsOrder](msg, keyOfOutputItemsOrder) + if !ok { + // Fallback for deserialized string type. + s, ok := getMsgExtraValue[string](msg, keyOfOutputItemsOrder) + if !ok || s == "" { + return nil, false + } + orderStr = arkOutputItemsOrder(s) + } + result := parseOutputItemsOrder(orderStr) + return result, len(result) > 0 +} + +func setOutputItemsOrder(msg *schema.Message, order []outputItemType) { + setMsgExtra(msg, keyOfOutputItemsOrder, arkOutputItemsOrder(encodeOutputItemsOrder(order))) +} + +// encodeOutputItemsOrder encodes the order entries into a comma-separated string. +// e.g. "message,reasoning,function_call,function_call" +func encodeOutputItemsOrder(order []outputItemType) arkOutputItemsOrder { + parts := make([]string, 0, len(order)) + for _, entry := range order { + parts = append(parts, string(entry)) + } + return arkOutputItemsOrder(strings.Join(parts, ",")) +} + +// parseOutputItemsOrder parses a comma-separated order string back into entries. +func parseOutputItemsOrder(s arkOutputItemsOrder) []outputItemType { + if s == "" { + return nil + } + parts := strings.Split(string(s), ",") + entries := make([]outputItemType, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + entries = append(entries, outputItemType(part)) + } + return entries +} diff --git a/components/model/ark/responses_api.go b/components/model/ark/responses_api.go index 7c07707f3..d58997f7c 100644 --- a/components/model/ark/responses_api.go +++ b/components/model/ark/responses_api.go @@ -624,19 +624,62 @@ func (cm *ResponsesAPIChatModel) populateInput(in []*schema.Message, responseReq if err != nil { return err } - if len(inputMessage.GetContent()) > 0 { - itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_InputMessage{InputMessage: inputMessage}}) - } - for _, toolCall := range msg.ToolCalls { - itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_FunctionToolCall{ - FunctionToolCall: &responses.ItemFunctionToolCall{ - Type: responses.ItemType_function_call, - CallId: toolCall.ID, - Arguments: toolCall.Function.Arguments, - Name: toolCall.Function.Name, - }, - }}) + itemsOrder, hasOrder := getOutputItemsOrder(msg) + if hasOrder { + // Reconstruct items in the original order recorded during output conversion. + toolCallIdx := 0 + for _, entry := range itemsOrder { + switch entry { + case outputItemTypeMessage: + if len(inputMessage.GetContent()) > 0 { + itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_InputMessage{InputMessage: inputMessage}}) + } + case outputItemTypeFunctionCall: + if toolCallIdx < len(msg.ToolCalls) { + toolCall := msg.ToolCalls[toolCallIdx] + itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_FunctionToolCall{ + FunctionToolCall: &responses.ItemFunctionToolCall{ + Type: responses.ItemType_function_call, + CallId: toolCall.ID, + Arguments: toolCall.Function.Arguments, + Name: toolCall.Function.Name, + }, + }}) + toolCallIdx++ + } + case outputItemTypeReasoning: + reasoning := &responses.ItemReasoning{ + Type: responses.ItemType_reasoning, + Summary: []*responses.ReasoningSummaryPart{{ + Type: responses.ContentItemType_input_text, + Text: msg.ReasoningContent, + }}, + } + if id, ok := getReasoningID(msg); ok && id != "" { + reasoning.Id = &id + } + itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_Reasoning{ + Reasoning: reasoning, + }}) + } + } + } else { + // Fallback: original behavior when no order metadata is available. + if len(inputMessage.GetContent()) > 0 { + itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_InputMessage{InputMessage: inputMessage}}) + } + + for _, toolCall := range msg.ToolCalls { + itemList = append(itemList, &responses.InputItem{Union: &responses.InputItem_FunctionToolCall{ + FunctionToolCall: &responses.ItemFunctionToolCall{ + Type: responses.ItemType_function_call, + CallId: toolCall.ID, + Arguments: toolCall.Function.Arguments, + Name: toolCall.Function.Name, + }, + }}) + } } case schema.System: inputMessage, err := cm.toArkSystemRoleItemInputMessage(msg) @@ -980,12 +1023,15 @@ func (cm *ResponsesAPIChatModel) toOutputMessage(resp *responses.ResponseObject, return nil, fmt.Errorf("received empty output from ARK") } + var itemsOrder []outputItemType + for _, item := range resp.Output { switch asItem := item.GetUnion().(type) { case *responses.OutputItem_OutputMessage: if asItem.OutputMessage == nil { continue } + itemsOrder = append(itemsOrder, outputItemTypeMessage) isMultiContent := len(asItem.OutputMessage.Content) > 1 for _, content := range asItem.OutputMessage.Content { if content.GetText() == nil { @@ -1005,6 +1051,10 @@ func (cm *ResponsesAPIChatModel) toOutputMessage(resp *responses.ResponseObject, if asItem.Reasoning == nil { continue } + itemsOrder = append(itemsOrder, outputItemTypeReasoning) + if asItem.Reasoning.Id != nil { + setReasoningID(msg, *asItem.Reasoning.Id) + } for _, s := range asItem.Reasoning.GetSummary() { if s.Text == "" { continue @@ -1020,6 +1070,7 @@ func (cm *ResponsesAPIChatModel) toOutputMessage(resp *responses.ResponseObject, if asItem.FunctionToolCall == nil { continue } + itemsOrder = append(itemsOrder, outputItemTypeFunctionCall) msg.ToolCalls = append(msg.ToolCalls, schema.ToolCall{ ID: asItem.FunctionToolCall.CallId, Type: asItem.FunctionToolCall.Type.String(), @@ -1031,6 +1082,10 @@ func (cm *ResponsesAPIChatModel) toOutputMessage(resp *responses.ResponseObject, } } + if len(itemsOrder) > 0 { + setOutputItemsOrder(msg, itemsOrder) + } + return msg, nil } @@ -1086,6 +1141,7 @@ func (cm *ResponsesAPIChatModel) toCallbackConfig(req *responses.ResponsesReques func (cm *ResponsesAPIChatModel) receivedStreamResponse(streamReader *utils.ResponsesStreamReader, config *model.Config, cacheConfig *cacheConfig, sw *schema.StreamWriter[*model.CallbackOutput]) { var itemFunctionToolCall *responses.ItemFunctionToolCall + var reasoningID *string for { event, err := streamReader.Recv() @@ -1157,6 +1213,11 @@ func (cm *ResponsesAPIChatModel) receivedStreamResponse(streamReader *utils.Resp if outputItemFuncCall, ok := ev.Item.GetItem().GetUnion().(*responses.OutputItem_FunctionToolCall); ok { itemFunctionToolCall = outputItemFuncCall.FunctionToolCall } + if outputItemReasoning, ok := ev.Item.GetItem().GetUnion().(*responses.OutputItem_Reasoning); ok { + if outputItemReasoning.Reasoning != nil { + reasoningID = outputItemReasoning.Reasoning.Id + } + } case *responses.Event_FunctionCallArguments: if ev.FunctionCallArguments == nil { @@ -1181,6 +1242,7 @@ func (cm *ResponsesAPIChatModel) receivedStreamResponse(streamReader *utils.Resp }, }, } + setOutputItemsOrder(msg, []outputItemType{outputItemTypeFunctionCall}) cm.sendCallbackOutput(sw, config, "", msg) } @@ -1194,6 +1256,11 @@ func (cm *ResponsesAPIChatModel) receivedStreamResponse(streamReader *utils.Resp ReasoningContent: delta, } setReasoningContent(msg, delta) + setOutputItemsOrder(msg, []outputItemType{outputItemTypeReasoning}) + if reasoningID != nil { + setReasoningID(msg, *reasoningID) + reasoningID = nil // only set once + } cm.sendCallbackOutput(sw, config, "", msg) case *responses.Event_Text: @@ -1204,6 +1271,7 @@ func (cm *ResponsesAPIChatModel) receivedStreamResponse(streamReader *utils.Resp Role: schema.Assistant, Content: *ev.Text.Delta, } + setOutputItemsOrder(msg, []outputItemType{outputItemTypeMessage}) cm.sendCallbackOutput(sw, config, "", msg) } diff --git a/components/model/ark/responses_api_reasoning_test.go b/components/model/ark/responses_api_reasoning_test.go new file mode 100644 index 000000000..82434985d --- /dev/null +++ b/components/model/ark/responses_api_reasoning_test.go @@ -0,0 +1,412 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ark + +import ( + "testing" + + "github.com/cloudwego/eino/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model/responses" +) + +func TestReasoningOutputConversion(t *testing.T) { + t.Run("converts reasoning with ID and Status to message", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + reasoningID := "reasoning-123" + + resp := &responses.ResponseObject{ + Usage: &responses.Usage{}, + Output: []*responses.OutputItem{ + { + Union: &responses.OutputItem_Reasoning{ + Reasoning: &responses.ItemReasoning{ + Id: &reasoningID, + Type: responses.ItemType_reasoning, + Summary: []*responses.ReasoningSummaryPart{ + { + Type: responses.ContentItemType_input_text, + Text: "This is reasoning content", + }, + }, + }, + }, + }, + }, + } + + msg, err := cm.toOutputMessage(resp, &cacheConfig{}) + require.NoError(t, err) + assert.NotNil(t, msg) + + assert.Equal(t, "This is reasoning content", msg.ReasoningContent) + + id, ok := getReasoningID(msg) + assert.True(t, ok) + assert.Equal(t, "reasoning-123", id) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{outputItemTypeReasoning}, order) + }) + + t.Run("converts multiple reasoning summaries", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + reasoningID := "reasoning-456" + + resp := &responses.ResponseObject{ + Usage: &responses.Usage{}, + Output: []*responses.OutputItem{ + { + Union: &responses.OutputItem_Reasoning{ + Reasoning: &responses.ItemReasoning{ + Id: &reasoningID, + Type: responses.ItemType_reasoning, + Summary: []*responses.ReasoningSummaryPart{ + { + Type: responses.ContentItemType_input_text, + Text: "First reasoning part", + }, + { + Type: responses.ContentItemType_input_text, + Text: "Second reasoning part", + }, + }, + }, + }, + }, + }, + } + + msg, err := cm.toOutputMessage(resp, &cacheConfig{}) + require.NoError(t, err) + assert.Equal(t, "First reasoning part\n\nSecond reasoning part", msg.ReasoningContent) + + id, ok := getReasoningID(msg) + assert.True(t, ok) + assert.Equal(t, "reasoning-456", id) + }) + + t.Run("handles reasoning without ID or Status", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + resp := &responses.ResponseObject{ + Usage: &responses.Usage{}, + Output: []*responses.OutputItem{ + { + Union: &responses.OutputItem_Reasoning{ + Reasoning: &responses.ItemReasoning{ + Type: responses.ItemType_reasoning, + Summary: []*responses.ReasoningSummaryPart{ + { + Type: responses.ContentItemType_input_text, + Text: "Reasoning without metadata", + }, + }, + }, + }, + }, + }, + } + + msg, err := cm.toOutputMessage(resp, &cacheConfig{}) + require.NoError(t, err) + assert.Equal(t, "Reasoning without metadata", msg.ReasoningContent) + + _, ok := getReasoningID(msg) + assert.False(t, ok) + }) +} + +func TestReasoningInputConversion(t *testing.T) { + t.Run("converts message with reasoning back to input item", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + msg := &schema.Message{ + Role: schema.Assistant, + ReasoningContent: "Reasoning content for input", + } + setReasoningID(msg, "reasoning-789") + setOutputItemsOrder(msg, []outputItemType{outputItemTypeReasoning}) + + req := &responses.ResponsesRequest{} + err := cm.populateInput([]*schema.Message{msg}, req) + require.NoError(t, err) + + inputList := req.Input.GetListValue() + require.NotNil(t, inputList) + require.Len(t, inputList.ListValue, 1) + + reasoningItem := inputList.ListValue[0].GetReasoning() + require.NotNil(t, reasoningItem) + assert.NotNil(t, reasoningItem.Id) + assert.Equal(t, "reasoning-789", *reasoningItem.Id) + assert.NotNil(t, reasoningItem.Status) + assert.Equal(t, responses.ItemType_reasoning, reasoningItem.Type) + require.Len(t, reasoningItem.Summary, 1) + assert.Equal(t, "Reasoning content for input", reasoningItem.Summary[0].Text) + }) + + t.Run("converts message without ID and Status metadata", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + msg := &schema.Message{ + Role: schema.Assistant, + ReasoningContent: "Simple reasoning", + } + setOutputItemsOrder(msg, []outputItemType{outputItemTypeReasoning}) + + req := &responses.ResponsesRequest{} + err := cm.populateInput([]*schema.Message{msg}, req) + require.NoError(t, err) + + inputList := req.Input.GetListValue() + require.NotNil(t, inputList) + require.Len(t, inputList.ListValue, 1) + + reasoningItem := inputList.ListValue[0].GetReasoning() + require.NotNil(t, reasoningItem) + assert.Nil(t, reasoningItem.Id) + assert.Equal(t, responses.ItemType_reasoning, reasoningItem.Type) + require.Len(t, reasoningItem.Summary, 1) + assert.Equal(t, "Simple reasoning", reasoningItem.Summary[0].Text) + }) +} + +func TestItemOrderRoundTrip(t *testing.T) { + t.Run("preserves order of message, reasoning, and function_call", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + resp := &responses.ResponseObject{ + Usage: &responses.Usage{}, + Output: []*responses.OutputItem{ + { + Union: &responses.OutputItem_Reasoning{ + Reasoning: &responses.ItemReasoning{ + Type: responses.ItemType_reasoning, + Summary: []*responses.ReasoningSummaryPart{ + {Text: "Thinking..."}, + }, + }, + }, + }, + { + Union: &responses.OutputItem_OutputMessage{ + OutputMessage: &responses.ItemOutputMessage{ + Type: responses.ItemType_message, + Content: []*responses.OutputContentItem{ + { + Union: &responses.OutputContentItem_Text{ + Text: &responses.OutputContentItemText{ + Type: responses.ContentItemType_output_text, + Text: "Here is my response", + }, + }, + }, + }, + }, + }, + }, + { + Union: &responses.OutputItem_FunctionToolCall{ + FunctionToolCall: &responses.ItemFunctionToolCall{ + Type: responses.ItemType_function_call, + CallId: "call-123", + Name: "search", + Arguments: `{"query":"test"}`, + }, + }, + }, + }, + } + + msg, err := cm.toOutputMessage(resp, &cacheConfig{}) + require.NoError(t, err) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{ + outputItemTypeReasoning, + outputItemTypeMessage, + outputItemTypeFunctionCall, + }, order) + + req := &responses.ResponsesRequest{} + err = cm.populateInput([]*schema.Message{msg}, req) + require.NoError(t, err) + + inputList := req.Input.GetListValue() + require.NotNil(t, inputList) + require.Len(t, inputList.ListValue, 3) + + assert.NotNil(t, inputList.ListValue[0].GetReasoning()) + assert.NotNil(t, inputList.ListValue[1].GetInputMessage()) + assert.NotNil(t, inputList.ListValue[2].GetFunctionToolCall()) + }) + + t.Run("preserves order with multiple function calls", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + resp := &responses.ResponseObject{ + Usage: &responses.Usage{}, + Output: []*responses.OutputItem{ + { + Union: &responses.OutputItem_OutputMessage{ + OutputMessage: &responses.ItemOutputMessage{ + Type: responses.ItemType_message, + Content: []*responses.OutputContentItem{ + { + Union: &responses.OutputContentItem_Text{ + Text: &responses.OutputContentItemText{ + Text: "Calling tools...", + }, + }, + }, + }, + }, + }, + }, + { + Union: &responses.OutputItem_FunctionToolCall{ + FunctionToolCall: &responses.ItemFunctionToolCall{ + CallId: "call-1", + Name: "tool1", + Arguments: `{}`, + }, + }, + }, + { + Union: &responses.OutputItem_FunctionToolCall{ + FunctionToolCall: &responses.ItemFunctionToolCall{ + CallId: "call-2", + Name: "tool2", + Arguments: `{}`, + }, + }, + }, + }, + } + + msg, err := cm.toOutputMessage(resp, &cacheConfig{}) + require.NoError(t, err) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{ + outputItemTypeMessage, + outputItemTypeFunctionCall, + outputItemTypeFunctionCall, + }, order) + + req := &responses.ResponsesRequest{} + err = cm.populateInput([]*schema.Message{msg}, req) + require.NoError(t, err) + + inputList := req.Input.GetListValue() + require.NotNil(t, inputList) + require.Len(t, inputList.ListValue, 3) + + assert.NotNil(t, inputList.ListValue[0].GetInputMessage()) + assert.NotNil(t, inputList.ListValue[1].GetFunctionToolCall()) + assert.Equal(t, "call-1", inputList.ListValue[1].GetFunctionToolCall().CallId) + assert.NotNil(t, inputList.ListValue[2].GetFunctionToolCall()) + assert.Equal(t, "call-2", inputList.ListValue[2].GetFunctionToolCall().CallId) + }) + + t.Run("fallback behavior without order metadata", func(t *testing.T) { + cm := &ResponsesAPIChatModel{} + + msg := &schema.Message{ + Role: schema.Assistant, + Content: "Response", + ToolCalls: []schema.ToolCall{ + { + ID: "call-1", + Type: "function", + Function: schema.FunctionCall{ + Name: "search", + Arguments: `{}`, + }, + }, + }, + } + + req := &responses.ResponsesRequest{} + err := cm.populateInput([]*schema.Message{msg}, req) + require.NoError(t, err) + + inputList := req.Input.GetListValue() + require.NotNil(t, inputList) + require.Len(t, inputList.ListValue, 2) + + assert.NotNil(t, inputList.ListValue[0].GetInputMessage()) + assert.NotNil(t, inputList.ListValue[1].GetFunctionToolCall()) + }) +} + +func TestStreamResponseItemOrder(t *testing.T) { + t.Run("stream sets item order correctly", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + Content: "text", + } + setOutputItemsOrder(msg, []outputItemType{outputItemTypeMessage}) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{outputItemTypeMessage}, order) + }) + + t.Run("reasoning stream chunk sets order", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + ReasoningContent: "thinking", + } + setReasoningContent(msg, "thinking") + setOutputItemsOrder(msg, []outputItemType{outputItemTypeReasoning}) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{outputItemTypeReasoning}, order) + + content, ok := GetReasoningContent(msg) + assert.True(t, ok) + assert.Equal(t, "thinking", content) + }) + + t.Run("function call stream chunk sets order", func(t *testing.T) { + msg := &schema.Message{ + Role: schema.Assistant, + ToolCalls: []schema.ToolCall{ + { + ID: "call-1", + Type: "function", + Function: schema.FunctionCall{ + Name: "search", + Arguments: "{}", + }, + }, + }, + } + setOutputItemsOrder(msg, []outputItemType{outputItemTypeFunctionCall}) + + order, hasOrder := getOutputItemsOrder(msg) + assert.True(t, hasOrder) + assert.Equal(t, []outputItemType{outputItemTypeFunctionCall}, order) + }) +}