Skip to content

Commit 7d93874

Browse files
renfredxhms-jpq
andauthored
fix: text and tools use mutually exclusive issue (#855)
* fix text and tools use mutually exclusive issue * some format nits --------- Co-authored-by: dogisgreat <[email protected]>
1 parent 92e26d0 commit 7d93874

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

lib/openai/resources/responses.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,10 @@ def get_structured_output_models(parsed)
559559
in {text: {format: {type: :json_schema,
560560
schema: OpenAI::StructuredOutput::JsonSchemaConverter => model}}}
561561
parsed.dig(:text, :format).store(:schema, model.to_json_schema)
562+
else
563+
end
564+
565+
case parsed
562566
in {tools: Array => tools}
563567
# rubocop:disable Metrics/BlockLength
564568
mapped = tools.map do |tool|

test/openai/resources/responses/streaming_test.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,75 @@ def test_structured_output_parsed_in_final_response
430430
end
431431
end
432432

433+
class CalendarEvent < OpenAI::BaseModel
434+
required :name, String
435+
required :date, String
436+
required :location, String
437+
end
438+
439+
class LookupCalendar < OpenAI::BaseModel
440+
required :first_name, String
441+
required :last_name, String
442+
end
443+
444+
def test_stream_with_both_text_and_tools
445+
stub_request(:post, "http://localhost/responses")
446+
.to_return(
447+
status: 200,
448+
headers: {"Content-Type" => "text/event-stream"},
449+
body: text_and_tools_sse_response
450+
)
451+
452+
stream = @client.responses.stream(
453+
model: "gpt-4o-2024-08-06",
454+
input: [
455+
{role: :system, content: "Extract event info and look up attendees."},
456+
{role: :user, content: "Ada Lovelace is going to a conference on Friday at the Convention Center."}
457+
],
458+
text: CalendarEvent,
459+
tools: [LookupCalendar]
460+
)
461+
462+
events = stream.to_a
463+
464+
text_done = events.find { |e| e.type == :"response.output_text.done" }
465+
assert_pattern do
466+
text_done => OpenAI::Streaming::ResponseTextDoneEvent[
467+
parsed: CalendarEvent[
468+
name: "Conference",
469+
date: "Friday",
470+
location: "Convention Center"
471+
]
472+
]
473+
end
474+
475+
function_done = events.find { |e| e.type == :"response.function_call_arguments.done" }
476+
assert_equal('{"first_name":"Ada","last_name":"Lovelace"}', function_done.arguments)
477+
478+
final_response = stream.get_final_response
479+
480+
text_output = final_response.output.find { |o| o.is_a?(OpenAI::Models::Responses::ResponseOutputMessage) }
481+
text_content = text_output.content.find { |c| c[:type] == :output_text }
482+
assert_pattern do
483+
text_content[:parsed] => CalendarEvent[
484+
name: "Conference",
485+
date: "Friday",
486+
location: "Convention Center"
487+
]
488+
end
489+
490+
tool_call = final_response.output.find { |o| o.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall) }
491+
assert_pattern do
492+
tool_call => OpenAI::Models::Responses::ResponseFunctionToolCall[
493+
name: "LookupCalendar",
494+
parsed: LookupCalendar[
495+
first_name: "Ada",
496+
last_name: "Lovelace"
497+
]
498+
]
499+
end
500+
end
501+
433502
private
434503

435504
def function_tool_params
@@ -742,4 +811,51 @@ def error_sse_response
742811
743812
SSE
744813
end
814+
815+
def text_and_tools_sse_response
816+
<<~SSE
817+
event: response.created
818+
data: {"type":"response.created","sequence_number":1,"response":{"id":"resp_stream_001","object":"realtime.response","status":"in_progress","output":[],"usage":null}}
819+
820+
event: response.output_item.added
821+
data: {"type":"response.output_item.added","sequence_number":2,"response_id":"resp_stream_001","output_index":0,"item":{"id":"msg_001","object":"realtime.item","type":"message","status":"in_progress","role":"assistant","content":[]}}
822+
823+
event: response.content_part.added
824+
data: {"type":"response.content_part.added","sequence_number":3,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"content_index":0,"part":{"type":"output_text","text":""}}
825+
826+
event: response.output_text.delta
827+
data: {"type":"response.output_text.delta","sequence_number":4,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"content_index":0,"delta":"{\\"name\\":\\"Conference\\","}
828+
829+
event: response.output_text.delta
830+
data: {"type":"response.output_text.delta","sequence_number":5,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"content_index":0,"delta":"\\"date\\":\\"Friday\\",\\"location\\":\\"Convention Center\\"}"}
831+
832+
event: response.output_text.done
833+
data: {"type":"response.output_text.done","sequence_number":6,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"content_index":0,"text":"{\\"name\\":\\"Conference\\",\\"date\\":\\"Friday\\",\\"location\\":\\"Convention Center\\"}"}
834+
835+
event: response.content_part.done
836+
data: {"type":"response.content_part.done","sequence_number":7,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"content_index":0,"part":{"type":"output_text","text":"{\\"name\\":\\"Conference\\",\\"date\\":\\"Friday\\",\\"location\\":\\"Convention Center\\"}"}}
837+
838+
event: response.output_item.done
839+
data: {"type":"response.output_item.done","sequence_number":8,"response_id":"resp_stream_001","item_id":"msg_001","output_index":0,"item":{"id":"msg_001","object":"realtime.item","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"{\\"name\\":\\"Conference\\",\\"date\\":\\"Friday\\",\\"location\\":\\"Convention Center\\"}"}]}}
840+
841+
event: response.output_item.added
842+
data: {"type":"response.output_item.added","sequence_number":9,"response_id":"resp_stream_001","output_index":1,"item":{"id":"call_001","object":"realtime.item","type":"function_call","status":"in_progress","name":"LookupCalendar","arguments":"","call_id":"call_001"}}
843+
844+
event: response.function_call_arguments.delta
845+
data: {"type":"response.function_call_arguments.delta","sequence_number":10,"item_id":"call_001","output_index":1,"delta":"{\\"first_name\\":\\"Ada\\","}
846+
847+
event: response.function_call_arguments.delta
848+
data: {"type":"response.function_call_arguments.delta","sequence_number":11,"item_id":"call_001","output_index":1,"delta":"\\"last_name\\":\\"Lovelace\\"}"}
849+
850+
event: response.function_call_arguments.done
851+
data: {"type":"response.function_call_arguments.done","sequence_number":12,"item_id":"call_001","output_index":1,"arguments":"{\\"first_name\\":\\"Ada\\",\\"last_name\\":\\"Lovelace\\"}"}
852+
853+
event: response.output_item.done
854+
data: {"type":"response.output_item.done","sequence_number":13,"response_id":"resp_stream_001","item_id":"call_001","output_index":1,"item":{"id":"call_001","object":"realtime.item","type":"function_call","status":"completed","name":"LookupCalendar","arguments":"{\\"first_name\\":\\"Ada\\",\\"last_name\\":\\"Lovelace\\"}","call_id":"call_001"}}
855+
856+
event: response.completed
857+
data: {"type":"response.completed","sequence_number":14,"response":{"id":"resp_stream_001","object":"realtime.response","status":"completed","output":[{"id":"msg_001","object":"realtime.item","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"{\\"name\\":\\"Conference\\",\\"date\\":\\"Friday\\",\\"location\\":\\"Convention Center\\"}"}]},{"id":"call_001","object":"realtime.item","type":"function_call","status":"completed","name":"LookupCalendar","arguments":"{\\"first_name\\":\\"Ada\\",\\"last_name\\":\\"Lovelace\\"}","call_id":"call_001"}],"usage":{"total_tokens":50,"input_tokens":30,"output_tokens":20}}}
858+
859+
SSE
860+
end
745861
end

0 commit comments

Comments
 (0)