@@ -151,9 +151,17 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
151151
152152 "response.completed" ->
153153 usage_data = get_in ( data , [ "response" , "usage" ] )
154+ response_id = get_in ( data , [ "response" , "id" ] )
154155
155156 meta = % { terminal?: true , finish_reason: :stop }
156157
158+ meta =
159+ if response_id do
160+ Map . put ( meta , :response_id , response_id )
161+ else
162+ meta
163+ end
164+
157165 meta =
158166 if usage_data do
159167 raw_usage = % {
@@ -214,13 +222,15 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
214222 opts_map = if is_map ( opts ) , do: opts , else: Map . new ( opts )
215223 provider_opts = opts_map [ :provider_options ] || [ ]
216224
217- previous_response_id = provider_opts [ :previous_response_id ]
225+ previous_response_id =
226+ provider_opts [ :previous_response_id ] ||
227+ extract_previous_response_id_from_context ( context )
218228
219- input =
220- Enum . flat_map ( context . messages , fn msg ->
229+ { input , tool_messages } =
230+ Enum . reduce ( context . messages , { [ ] , [ ] } , fn msg , { input_acc , tool_acc } ->
221231 case msg . role do
222232 :tool ->
223- [ ]
233+ { input_acc , [ msg | tool_acc ] }
224234
225235 _ ->
226236 content =
@@ -232,17 +242,33 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
232242 end )
233243
234244 if content == [ ] and msg . tool_calls == nil do
235- [ ]
245+ { input_acc , tool_acc }
236246 else
237247 if msg . role == :assistant and msg . tool_calls != nil and msg . tool_calls != [ ] do
238- [ ]
248+ { input_acc , tool_acc }
239249 else
240- [ % { "role" => Atom . to_string ( msg . role ) , "content" => content } ]
250+ { input_acc ++ [ % { "role" => Atom . to_string ( msg . role ) , "content" => content } ] ,
251+ tool_acc }
241252 end
242253 end
243254 end
244255 end )
245256
257+ tool_outputs_from_context = extract_tool_outputs_from_messages ( Enum . reverse ( tool_messages ) )
258+
259+ tool_outputs =
260+ case provider_opts [ :tool_outputs ] do
261+ nil -> tool_outputs_from_context
262+ [ ] -> tool_outputs_from_context
263+ explicit_outputs -> explicit_outputs
264+ end
265+
266+ input =
267+ case tool_outputs do
268+ [ ] -> input
269+ outputs -> input ++ encode_tool_outputs ( outputs )
270+ end
271+
246272 max_output_tokens =
247273 opts_map [ :max_output_tokens ] ||
248274 opts_map [ :max_completion_tokens ] ||
@@ -381,6 +407,58 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
381407 defp maybe_put_string ( map , _key , nil ) , do: map
382408 defp maybe_put_string ( map , key , value ) , do: Map . put ( map , key , value )
383409
410+ defp extract_previous_response_id_from_context ( context ) do
411+ context . messages
412+ |> Enum . reverse ( )
413+ |> Enum . find_value ( fn msg ->
414+ case msg do
415+ % { role: :assistant , tool_calls: tool_calls , metadata: % { response_id: id } }
416+ when not is_nil ( tool_calls ) and tool_calls != [ ] ->
417+ id
418+
419+ _ ->
420+ nil
421+ end
422+ end )
423+ end
424+
425+ defp extract_tool_outputs_from_messages ( tool_messages ) do
426+ Enum . map ( tool_messages , fn msg ->
427+ output_text =
428+ msg . content
429+ |> Enum . find_value ( fn part ->
430+ if part . type == :text , do: part . text
431+ end ) || ""
432+
433+ % {
434+ call_id: msg . tool_call_id ,
435+ output: output_text
436+ }
437+ end )
438+ end
439+
440+ defp encode_tool_outputs ( outputs ) when is_list ( outputs ) do
441+ Enum . map ( outputs , fn output ->
442+ call_id = output [ :call_id ] || output [ "call_id" ]
443+ raw_output = output [ :output ] || output [ "output" ]
444+
445+ output_string =
446+ cond do
447+ is_binary ( raw_output ) -> raw_output
448+ is_map ( raw_output ) or is_list ( raw_output ) -> Jason . encode! ( raw_output )
449+ true -> to_string ( raw_output )
450+ end
451+
452+ % {
453+ "type" => "function_call_output" ,
454+ "call_id" => call_id ,
455+ "output" => output_string
456+ }
457+ end )
458+ end
459+
460+ defp encode_tool_outputs ( _ ) , do: [ ]
461+
384462 defp encode_tools_if_any ( request ) do
385463 case request . options [ :tools ] do
386464 nil -> nil
@@ -564,7 +642,8 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
564642 msg = % ReqLLM.Message {
565643 role: :assistant ,
566644 content: content_parts ,
567- tool_calls: if ( tool_calls != [ ] , do: tool_calls )
645+ tool_calls: if ( tool_calls != [ ] , do: tool_calls ) ,
646+ metadata: % { response_id: body [ "id" ] }
568647 }
569648
570649 response = % ReqLLM.Response {
@@ -670,13 +749,27 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
670749 segments
671750 |> Enum . filter ( & ( & 1 [ "type" ] == "function_call" ) )
672751 |> Enum . map ( fn seg ->
673- args_json = seg [ "arguments" ] || "{}"
674- id = seg [ "call_id" ]
752+ args_json = normalize_arguments_json ( seg [ "arguments" ] )
753+ id = seg [ "call_id" ] || seg [ "id" ]
675754 name = seg [ "name" ] || "unknown"
676755 ReqLLM.ToolCall . new ( id , name , args_json )
677756 end )
678757 end
679758
759+ defp normalize_arguments_json ( nil ) , do: "{}"
760+ defp normalize_arguments_json ( "" ) , do: "{}"
761+
762+ defp normalize_arguments_json ( json ) when is_binary ( json ) do
763+ trimmed = String . trim ( json )
764+
765+ case Jason . decode ( trimmed ) do
766+ { :ok , _ } -> trimmed
767+ { :error , _ } -> trimmed
768+ end
769+ end
770+
771+ defp normalize_arguments_json ( _ ) , do: "{}"
772+
680773 defp build_content_parts ( text , thinking ) do
681774 parts = [ ]
682775
@@ -733,4 +826,147 @@ defmodule ReqLLM.Providers.OpenAI.ResponsesAPI do
733826 defp normalize_finish_reason ( "tool_calls" ) , do: :tool_calls
734827 defp normalize_finish_reason ( "content_filter" ) , do: :content_filter
735828 defp normalize_finish_reason ( _ ) , do: :error
829+
830+ @ doc false
831+ def build_responses_body_from_chunks ( chunks , model ) do
832+ state =
833+ Enum . reduce (
834+ chunks ,
835+ % {
836+ text: "" ,
837+ reasoning: "" ,
838+ tool_calls: % { } ,
839+ tool_call_order: [ ] ,
840+ usage: nil ,
841+ finish_reason: nil ,
842+ response_id: nil
843+ } ,
844+ & accumulate_chunk_to_state / 2
845+ )
846+
847+ output_segments = [ ]
848+
849+ output_segments =
850+ if state . reasoning == "" do
851+ output_segments
852+ else
853+ [
854+ % {
855+ "type" => "reasoning" ,
856+ "content" => [ % { "type" => "text" , "text" => state . reasoning } ]
857+ }
858+ | output_segments
859+ ]
860+ end
861+
862+ tool_segments =
863+ Enum . map ( state . tool_call_order , fn key ->
864+ tc = state . tool_calls [ key ]
865+
866+ % {
867+ "type" => "function_call" ,
868+ "id" => tc . id || "call_#{ key } " ,
869+ "name" => tc . name || "unknown" ,
870+ "arguments" => tc . arguments || "{}"
871+ }
872+ end )
873+
874+ output_segments = output_segments ++ tool_segments
875+
876+ response_id = state . response_id || "resp_stream_#{ System . unique_integer ( [ :positive ] ) } "
877+
878+ body = % {
879+ "id" => response_id ,
880+ "model" => model ,
881+ "status" => if ( state . finish_reason == :stop , do: "completed" , else: "incomplete" ) ,
882+ "output" => output_segments
883+ }
884+
885+ body =
886+ if state . text == "" do
887+ body
888+ else
889+ Map . put ( body , "output_text" , state . text )
890+ end
891+
892+ body =
893+ if state . usage do
894+ Map . put ( body , "usage" , state . usage )
895+ else
896+ body
897+ end
898+
899+ body
900+ end
901+
902+ defp accumulate_chunk_to_state ( % ReqLLM.StreamChunk { type: :content , text: text } , state ) do
903+ % { state | text: state . text <> text }
904+ end
905+
906+ defp accumulate_chunk_to_state ( % ReqLLM.StreamChunk { type: :thinking , text: text } , state ) do
907+ % { state | reasoning: state . reasoning <> text }
908+ end
909+
910+ defp accumulate_chunk_to_state ( % ReqLLM.StreamChunk { type: :tool_call } = chunk , state ) do
911+ # Get tool call ID from metadata
912+ tool_id = chunk . metadata [ :id ] || chunk . metadata [ :call_id ]
913+ key = chunk . metadata [ :index ] || tool_id || 0
914+
915+ existing = Map . get ( state . tool_calls , key , % { } )
916+
917+ updated = % {
918+ id: tool_id || existing [ :id ] ,
919+ name: chunk . name || existing [ :name ] ,
920+ arguments: merge_tool_arguments ( existing [ :arguments ] , chunk . arguments )
921+ }
922+
923+ order =
924+ if key in state . tool_call_order ,
925+ do: state . tool_call_order ,
926+ else: state . tool_call_order ++ [ key ]
927+
928+ % { state | tool_calls: Map . put ( state . tool_calls , key , updated ) , tool_call_order: order }
929+ end
930+
931+ defp accumulate_chunk_to_state ( % ReqLLM.StreamChunk { type: :meta , metadata: meta } , state ) do
932+ state
933+ |> maybe_put_usage ( meta [ :usage ] )
934+ |> maybe_put_finish ( meta [ :finish_reason ] )
935+ |> maybe_put_response_id ( meta [ :response_id ] )
936+ end
937+
938+ defp accumulate_chunk_to_state ( _chunk , state ) , do: state
939+
940+ defp merge_tool_arguments ( nil , new ) , do: new
941+ defp merge_tool_arguments ( existing , nil ) , do: existing
942+
943+ defp merge_tool_arguments ( existing , new ) when is_binary ( existing ) and is_binary ( new ) do
944+ existing <> new
945+ end
946+
947+ defp merge_tool_arguments ( existing , new ) when is_map ( new ) do
948+ merge_tool_arguments ( existing , Jason . encode! ( new ) )
949+ end
950+
951+ defp merge_tool_arguments ( existing , _new ) , do: existing
952+
953+ defp maybe_put_usage ( state , nil ) , do: state
954+
955+ defp maybe_put_usage ( state , usage ) do
956+ normalized =
957+ Map . update (
958+ usage ,
959+ :reasoning_tokens ,
960+ usage [ :reasoning ] || usage [ :thinking_tokens ] || 0 ,
961+ & & 1
962+ )
963+
964+ % { state | usage: normalized }
965+ end
966+
967+ defp maybe_put_finish ( state , nil ) , do: state
968+ defp maybe_put_finish ( state , reason ) , do: % { state | finish_reason: reason }
969+
970+ defp maybe_put_response_id ( state , nil ) , do: state
971+ defp maybe_put_response_id ( state , id ) , do: % { state | response_id: id }
736972end
0 commit comments