Get streaming working

This commit is contained in:
Zach Gollwitzer
2025-03-27 08:02:29 -04:00
parent e03f88a18c
commit ce042fd250
9 changed files with 439 additions and 105 deletions

View File

@@ -1,3 +1,8 @@
# Orchestrates LLM interactions for chat conversations by:
# - Streaming generic provider responses
# - Persisting messages and tool calls
# - Broadcasting updates to chat UI
# - Handling provider errors
class Assistant
include Provided
@@ -14,42 +19,61 @@ class Assistant
end
def respond_to(message)
chat.clear_error
sleep artificial_thinking_delay
provider = get_model_provider(message.ai_model)
response = provider.chat_response(
assistant_message = AssistantMessage.new(
chat: chat,
content: "",
ai_model: message.ai_model
)
streamer = proc do |chunk|
case chunk.type
when "output_text"
stop_thinking
assistant_message.content += chunk.data
assistant_message.save!
when "function_request"
update_thinking("Analyzing your data to assist you with your question...")
when "response"
stop_thinking
assistant_message.ai_model = chunk.data.model
combined_tool_calls = chunk.data.functions.map do |tc|
ToolCall::Function.new(
provider_id: tc.id,
provider_call_id: tc.call_id,
function_name: tc.name,
function_arguments: tc.arguments,
function_result: tc.result
)
end
assistant_message.tool_calls = combined_tool_calls
assistant_message.save!
chat.update!(latest_assistant_response_id: chunk.data.id)
end
end
provider.chat_response(
message,
instructions: instructions,
available_functions: functions,
streamer: streamer
)
stop_thinking
unless response.success?
return chat.add_error(response.error)
end
Chat.transaction do
chat.clear_error
process_response_artifacts(response.data)
chat.update!(latest_assistant_response_id: response.data.id)
end
rescue => e
chat.add_error(e)
end
private
def streamer
proc do |data|
puts data
# TODO process data
end
def update_thinking(thought)
chat.broadcast_update target: "thinking-indicator", partial: "chats/thinking_indicator", locals: { chat: chat, message: thought }
end
def stop_thinking
sleep artificial_thinking_delay
chat.broadcast_remove target: "thinking-indicator"
end

View File

@@ -29,9 +29,9 @@ class Provider::Openai < Provider
processor = ChatResponseProcessor.new(
client: client,
message: message,
streamer: streamer,
instructions: instructions,
available_functions: available_functions
available_functions: available_functions,
streamer: streamer
)
processor.process

View File

@@ -10,7 +10,13 @@ class Provider::Openai::ChatResponseProcessor
def process
first_response = fetch_response(previous_response_id: previous_openai_response_id)
return first_response if first_response.functions.empty?
if first_response.functions.empty?
if streamer.present?
streamer.call(StreamChunk.new(type: "response", data: first_response))
end
return first_response
end
executed_functions = execute_pending_functions(first_response.functions)
@@ -19,6 +25,10 @@ class Provider::Openai::ChatResponseProcessor
previous_response_id: first_response.id
)
if streamer.present?
streamer.call(StreamChunk.new(type: "response", data: follow_up_response))
end
follow_up_response
end
@@ -59,24 +69,6 @@ class Provider::Openai::ChatResponseProcessor
streamer.call(StreamChunk.new(type: "output_text", data: chunk.dig("delta")))
when "response.function_call_arguments.done"
streamer.call(StreamChunk.new(type: "function_request", data: chunk.dig("arguments")))
when "response.completed"
res = chunk.dig("response")
res_output = res.dig("output")
functions_output = if executed_functions.any?
executed_functions
else
extract_pending_functions(res_output)
end
data = Response.new(
id: res.dig("id"),
messages: extract_messages(res_output),
functions: functions_output,
model: res.dig("model")
)
streamer.call(StreamChunk.new(type: "response", data: data))
end
end

View File

@@ -3,23 +3,6 @@ require "test_helper"
module LLMInterfaceTest
extend ActiveSupport::Testing::Declarative
test "provides basic chat response" do
VCR.use_cassette("#{vcr_key_prefix}/chat/basic_response") do
chat = chats(:two)
message = chat.messages.create!(
type: "UserMessage",
content: "This is a chat test. If it's working, respond with a single word: Yes",
ai_model: @subject_model
)
response = @subject.chat_response(message)
assert response.success?
assert_equal 1, response.data.messages.size
assert_includes response.data.messages.first.content, "Yes"
end
end
private
def vcr_key_prefix
@subject.class.name.demodulize.underscore

View File

@@ -12,6 +12,12 @@ class AssistantTest < ActiveSupport::TestCase
end
test "responds to basic prompt without tools" do
collected_chunks = []
streamer = proc do |chunk|
collected_chunks << chunk
end
@provider.expects(:chat_response).returns(
provider_success_response(
Assistant::Provideable::ChatResponse.new(

View File

@@ -6,14 +6,13 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
setup do
@subject = @openai = Provider::Openai.new(ENV.fetch("OPENAI_ACCESS_TOKEN", "test-openai-token"))
@subject_model = "gpt-4o"
@chat = chats(:two)
end
test "openai errors are automatically raised" do
VCR.use_cassette("openai/chat/error") do
chat = chats(:two)
response = @openai.chat_response(UserMessage.new(
chat: chat,
chat: @chat,
content: "Error test",
ai_model: "invalid-model-that-will-trigger-api-error"
))
@@ -23,10 +22,9 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
end
end
test "provides basic chat response 2" do
VCR.use_cassette("openai/chat/basic_response", record: :all) do
chat = chats(:two)
message = chat.messages.create!(
test "basic chat response" do
VCR.use_cassette("openai/chat/basic_response") do
message = @chat.messages.create!(
type: "UserMessage",
content: "This is a chat test. If it's working, respond with a single word: Yes",
ai_model: @subject_model
@@ -40,35 +38,40 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
end
end
test "handles chat response with tool calls" do
VCR.use_cassette("openai/chat/tool_calls", record: :all) do
class PredictableToolFunction < Assistant::Function
class << self
def expected_test_result
"$124,200"
end
test "streams basic chat response" do
VCR.use_cassette("openai/chat/basic_response") do
collected_chunks = []
def name
"get_net_worth"
end
def description
"Gets user net worth data"
end
end
def call(params = {})
self.class.expected_test_result
end
mock_streamer = proc do |chunk|
collected_chunks << chunk
end
chat = chats(:two)
initial_message = UserMessage.new(chat: chat, content: "What is my net worth?", ai_model: @subject_model)
message = @chat.messages.create!(
type: "UserMessage",
content: "This is a chat test. If it's working, respond with a single word: Yes",
ai_model: @subject_model
)
response = @openai.chat_response(
initial_message,
@subject.chat_response(message, streamer: mock_streamer)
tool_call_chunks = collected_chunks.select { |chunk| chunk.type == "function_request" }
text_chunks = collected_chunks.select { |chunk| chunk.type == "output_text" }
response_chunks = collected_chunks.select { |chunk| chunk.type == "response" }
assert_equal 1, text_chunks.size
assert_equal 1, response_chunks.size
assert_equal 0, tool_call_chunks.size
assert_equal "Yes", text_chunks.first.data
assert_equal "Yes", response_chunks.first.data.messages.first.content
end
end
test "chat response with tool calls" do
VCR.use_cassette("openai/chat/tool_calls") do
response = @subject.chat_response(
tool_call_message,
instructions: "Use the tools available to you to answer the user's question.",
available_functions: [ PredictableToolFunction.new(chat) ]
available_functions: [ PredictableToolFunction.new(@chat) ]
)
assert response.success?
@@ -77,4 +80,57 @@ class Provider::OpenaiTest < ActiveSupport::TestCase
assert_includes response.data.messages.first.content, PredictableToolFunction.expected_test_result
end
end
test "streams chat response with tool calls" do
VCR.use_cassette("openai/chat/tool_calls") do
collected_chunks = []
mock_streamer = proc do |chunk|
collected_chunks << chunk
end
@subject.chat_response(
tool_call_message,
instructions: "Use the tools available to you to answer the user's question.",
available_functions: [ PredictableToolFunction.new(@chat) ],
streamer: mock_streamer
)
text_chunks = collected_chunks.select { |chunk| chunk.type == "output_text" }
text_chunks = collected_chunks.select { |chunk| chunk.type == "output_text" }
tool_call_chunks = collected_chunks.select { |chunk| chunk.type == "function_request" }
response_chunks = collected_chunks.select { |chunk| chunk.type == "response" }
assert_equal 1, tool_call_chunks.count
assert text_chunks.count >= 1
assert_equal 1, response_chunks.count
assert_includes response_chunks.first.data.messages.first.content, PredictableToolFunction.expected_test_result
end
end
private
def tool_call_message
UserMessage.new(chat: @chat, content: "What is my net worth?", ai_model: @subject_model)
end
class PredictableToolFunction < Assistant::Function
class << self
def expected_test_result
"$124,200"
end
def name
"get_net_worth"
end
def description
"Gets user net worth data"
end
end
def call(params = {})
self.class.expected_test_result
end
end
end

View File

@@ -24,7 +24,7 @@ http_interactions:
message: OK
headers:
Date:
- Wed, 26 Mar 2025 20:38:53 GMT
- Wed, 26 Mar 2025 21:27:38 GMT
Content-Type:
- text/event-stream; charset=utf-8
Transfer-Encoding:
@@ -36,57 +36,57 @@ http_interactions:
Openai-Organization:
- "<OPENAI_ORGANIZATION_ID>"
X-Request-Id:
- req_019d20b30a7aad658a848109a7b1d9a7
- req_8fce503a4c5be145dda20867925b1622
Openai-Processing-Ms:
- '78'
- '103'
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Cf-Cache-Status:
- DYNAMIC
Set-Cookie:
- __cf_bm=8GIBHnpZz7OLpPiIZN_57apWdMo55at4QPDQ3B0S61U-1743021533-1.0.1.1-W0sXULgGh8mIqmbx8GqF3UJZ4UND3vTOJa4P8wov9R85I_G2SK631xhONoDmhVONgpt41yj08ZXVV4z7oNLWmfwMhRqB.IXZ0ODtCeELmmk;
path=/; expires=Wed, 26-Mar-25 21:08:53 GMT; domain=.api.openai.com; HttpOnly;
- __cf_bm=o5kysxtwKJs3TPoOquM0X4MkyLIaylWhRd8LhagxXck-1743024458-1.0.1.1-ol6ndVCx6dHLGnc9.YmKYwgfOBqhSZSBpIHg4STCi4OBhrgt70FYPmMptrYDvg.SoFuS5RAS_pGiNNWXHspHio3gTfJ87vIdT936GYHIDrc;
path=/; expires=Wed, 26-Mar-25 21:57:38 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=XwbFBEN.O70FMLzBgac6U3S0thWeu6FsBxqrGi7ejpM-1743021533036-0.0.1.1-604800000;
- _cfuvid=Iqk8pY6uwz2lLhdKt0PwWTdtYQUqqvS6xmP9DMVko2A-1743024458829-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
X-Content-Type-Options:
- nosniff
Server:
- cloudflare
Cf-Ray:
- 926974406901cf47-CMH
- 9269bbb21b1ecf43-CMH
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: UTF-8
string: |+
event: response.created
data: {"type":"response.created","response":{"id":"resp_67e465dcebe88192af1708aeffc86a250cda4bd56b1d6516","object":"response","created_at":1743021532,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
data: {"type":"response.created","response":{"id":"resp_67e4714ab0148192ae2cc4303794d6fc0c1a792abcdc2819","object":"response","created_at":1743024458,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_67e465dcebe88192af1708aeffc86a250cda4bd56b1d6516","object":"response","created_at":1743021532,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
data: {"type":"response.in_progress","response":{"id":"resp_67e4714ab0148192ae2cc4303794d6fc0c1a792abcdc2819","object":"response","created_at":1743024458,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","status":"in_progress","role":"assistant","content":[]}}
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","status":"in_progress","role":"assistant","content":[]}}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
data: {"type":"response.content_part.added","item_id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","output_index":0,"content_index":0,"delta":"Yes"}
data: {"type":"response.output_text.delta","item_id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","output_index":0,"content_index":0,"delta":"Yes"}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","output_index":0,"content_index":0,"text":"Yes"}
data: {"type":"response.output_text.done","item_id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","output_index":0,"content_index":0,"text":"Yes"}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Yes","annotations":[]}}
data: {"type":"response.content_part.done","item_id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Yes","annotations":[]}}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Yes","annotations":[]}]}}
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Yes","annotations":[]}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_67e465dcebe88192af1708aeffc86a250cda4bd56b1d6516","object":"response","created_at":1743021532,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"message","id":"msg_67e465dd69588192b17f5d89aae448060cda4bd56b1d6516","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Yes","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":43,"input_tokens_details":{"cached_tokens":0},"output_tokens":2,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":45},"user":null,"metadata":{}}}
data: {"type":"response.completed","response":{"id":"resp_67e4714ab0148192ae2cc4303794d6fc0c1a792abcdc2819","object":"response","created_at":1743024458,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"message","id":"msg_67e4714b1f8c8192b9b16febe8be86550c1a792abcdc2819","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Yes","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":43,"input_tokens_details":{"cached_tokens":0},"output_tokens":2,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":45},"user":null,"metadata":{}}}
recorded_at: Wed, 26 Mar 2025 20:38:53 GMT
recorded_at: Wed, 26 Mar 2025 21:27:39 GMT
recorded_with: VCR 6.3.1
...

View File

@@ -0,0 +1,72 @@
---
http_interactions:
- request:
method: post
uri: https://api.openai.com/v1/responses
body:
encoding: UTF-8
string: '{"model":"invalid-model-that-will-trigger-api-error","input":[{"role":"user","content":"Error
test"}],"instructions":null,"tools":[],"previous_response_id":null,"stream":true}'
headers:
Content-Type:
- application/json
Authorization:
- Bearer <OPENAI_ACCESS_TOKEN>
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
response:
status:
code: 400
message: Bad Request
headers:
Date:
- Wed, 26 Mar 2025 21:27:19 GMT
Content-Type:
- application/json
Content-Length:
- '207'
Connection:
- keep-alive
Openai-Version:
- '2020-10-01'
Openai-Organization:
- "<OPENAI_ORGANIZATION_ID>"
X-Request-Id:
- req_2b86e02f664e790dfa475f111402b722
Openai-Processing-Ms:
- '146'
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Cf-Cache-Status:
- DYNAMIC
Set-Cookie:
- __cf_bm=gAU0gS_ZQBfQmFkc_jKM73dhkNISbBY9FlQjGnZ6CfU-1743024439-1.0.1.1-bWRoC737.SOJPZrP90wTJLVmelTpxFqIsrunq2Lqgy4J3VvLtYBEBrqY0v4d94F5fMcm0Ju.TfQi0etmvqZtUSMRn6rvkMLmXexRcxP.1jE;
path=/; expires=Wed, 26-Mar-25 21:57:19 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=XnxX4KU80himuKAUavZYtkQasOjXJDJD.QLyMrfBSUU-1743024439792-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
X-Content-Type-Options:
- nosniff
Server:
- cloudflare
Cf-Ray:
- 9269bb3b2c14cf74-CMH
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: UTF-8
string: |-
{
"error": {
"message": "The requested model 'invalid-model-that-will-trigger-api-error' does not exist.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
recorded_at: Wed, 26 Mar 2025 21:27:19 GMT
recorded_with: VCR 6.3.1

View File

@@ -0,0 +1,201 @@
---
http_interactions:
- request:
method: post
uri: https://api.openai.com/v1/responses
body:
encoding: UTF-8
string: '{"model":"gpt-4o","input":[{"role":"user","content":"What is my net
worth?"}],"instructions":"Use the tools available to you to answer the user''s
question.","tools":[{"type":"function","name":"get_net_worth","description":"Gets
user net worth data","parameters":{"type":"object","properties":{},"required":[],"additionalProperties":false},"strict":true}],"previous_response_id":null,"stream":true}'
headers:
Content-Type:
- application/json
Authorization:
- Bearer <OPENAI_ACCESS_TOKEN>
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
response:
status:
code: 200
message: OK
headers:
Date:
- Wed, 26 Mar 2025 21:22:09 GMT
Content-Type:
- text/event-stream; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Openai-Version:
- '2020-10-01'
Openai-Organization:
- "<OPENAI_ORGANIZATION_ID>"
X-Request-Id:
- req_4f04cffbab6051b3ac301038e3796092
Openai-Processing-Ms:
- '114'
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Cf-Cache-Status:
- DYNAMIC
Set-Cookie:
- __cf_bm=F5haUlL1HA1srjwZugBxG6XWbGg.NyQBnJTTirKs5KI-1743024129-1.0.1.1-D842I3sPgDgH_KXyroq6uVivEnbWvm9WJF.L8a11GgUcULXjhweLHs0mXe6MWruf.FJe.lZj.KmX0tCqqdpKIt5JvlbHXt5D_9svedktlZY;
path=/; expires=Wed, 26-Mar-25 21:52:09 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=MmuRzsy8ebDMe6ibCEwtGp2RzcntpAmdvDlhIZtlY1s-1743024129721-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
X-Content-Type-Options:
- nosniff
Server:
- cloudflare
Cf-Ray:
- 9269b3a97f370002-ORD
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: UTF-8
string: |+
event: response.created
data: {"type":"response.created","response":{"id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","object":"response","created_at":1743024129,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Gets user net worth data","name":"get_net_worth","parameters":{"type":"object","properties":{},"required":[],"additionalProperties":false},"strict":true}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","object":"response","created_at":1743024129,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Gets user net worth data","name":"get_net_worth","parameters":{"type":"object","properties":{},"required":[],"additionalProperties":false},"strict":true}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_67e4700222008192b3a26ce30fe7ad02069d9116026394b6","call_id":"call_FtvrJsTMg7he0mTeThIqktyL","name":"get_net_worth","arguments":"","status":"in_progress"}}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","item_id":"fc_67e4700222008192b3a26ce30fe7ad02069d9116026394b6","output_index":0,"delta":"{}"}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","item_id":"fc_67e4700222008192b3a26ce30fe7ad02069d9116026394b6","output_index":0,"arguments":"{}"}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_67e4700222008192b3a26ce30fe7ad02069d9116026394b6","call_id":"call_FtvrJsTMg7he0mTeThIqktyL","name":"get_net_worth","arguments":"{}","status":"completed"}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","object":"response","created_at":1743024129,"status":"completed","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"function_call","id":"fc_67e4700222008192b3a26ce30fe7ad02069d9116026394b6","call_id":"call_FtvrJsTMg7he0mTeThIqktyL","name":"get_net_worth","arguments":"{}","status":"completed"}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"function","description":"Gets user net worth data","name":"get_net_worth","parameters":{"type":"object","properties":{},"required":[],"additionalProperties":false},"strict":true}],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":271,"input_tokens_details":{"cached_tokens":0},"output_tokens":13,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":284},"user":null,"metadata":{}}}
recorded_at: Wed, 26 Mar 2025 21:22:10 GMT
- request:
method: post
uri: https://api.openai.com/v1/responses
body:
encoding: UTF-8
string: '{"model":"gpt-4o","input":[{"role":"user","content":"What is my net
worth?"},{"type":"function_call_output","call_id":"call_FtvrJsTMg7he0mTeThIqktyL","output":"\"$124,200\""}],"instructions":"Use
the tools available to you to answer the user''s question.","tools":[],"previous_response_id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","stream":true}'
headers:
Content-Type:
- application/json
Authorization:
- Bearer <OPENAI_ACCESS_TOKEN>
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
response:
status:
code: 200
message: OK
headers:
Date:
- Wed, 26 Mar 2025 21:22:10 GMT
Content-Type:
- text/event-stream; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Openai-Version:
- '2020-10-01'
Openai-Organization:
- "<OPENAI_ORGANIZATION_ID>"
X-Request-Id:
- req_792bf572fac53f7e139b29d462933d8f
Openai-Processing-Ms:
- '148'
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Cf-Cache-Status:
- DYNAMIC
Set-Cookie:
- __cf_bm=HHguTnSUQFt9KezJAQCrQF_OHn8ZH1C4xDjXRgexdzM-1743024130-1.0.1.1-ZhqxuASVfISfGQbvvKSNy_OQiUfkeIPN2DZhors0K4cl_BzE_P5u9kbc1HkgwyW1A_6GNAenh8Fr9AkoJ0zSakdg5Dr9AU.lu5nr7adQ_60;
path=/; expires=Wed, 26-Mar-25 21:52:10 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=hX9Y33ruiC9mhYzrOoxyOh23Gy.MfQa54h9l5CllWlI-1743024130948-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
X-Content-Type-Options:
- nosniff
Server:
- cloudflare
Cf-Ray:
- 9269b3b0da83cf67-CMH
Alt-Svc:
- h3=":443"; ma=86400
body:
encoding: UTF-8
string: |+
event: response.created
data: {"type":"response.created","response":{"id":"resp_67e47002c5b48192a8202d45c6a929f8069d9116026394b6","object":"response","created_at":1743024130,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_67e47002c5b48192a8202d45c6a929f8069d9116026394b6","object":"response","created_at":1743024130,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
event: response.output_item.added
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","status":"in_progress","role":"assistant","content":[]}}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":"Your"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":" net"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":" worth"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":" is"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":" $"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":"124"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":","}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":"200"}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"delta":"."}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"text":"Your net worth is $124,200."}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Your net worth is $124,200.","annotations":[]}}
event: response.output_item.done
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Your net worth is $124,200.","annotations":[]}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_67e47002c5b48192a8202d45c6a929f8069d9116026394b6","object":"response","created_at":1743024130,"status":"completed","error":null,"incomplete_details":null,"instructions":"Use the tools available to you to answer the user's question.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"message","id":"msg_67e47003483c819290ae392b826c4910069d9116026394b6","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Your net worth is $124,200.","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":"resp_67e4700196288192b27a4effc08dc47f069d9116026394b6","reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":85,"input_tokens_details":{"cached_tokens":0},"output_tokens":10,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":95},"user":null,"metadata":{}}}
recorded_at: Wed, 26 Mar 2025 21:22:11 GMT
recorded_with: VCR 6.3.1
...