Skip to content

Commit c02665a

Browse files
authored
Merge pull request alexrudall#248 from rmontgomery429/faraday-json-parsing
Improved stream parsing
2 parents ef4348b + cbbdd1b commit c02665a

File tree

3 files changed

+97
-10
lines changed

3 files changed

+97
-10
lines changed

lib/openai/client.rb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,21 @@ def self.to_json(string)
9393
JSON.parse(string.gsub("}\n{", "},{").prepend("[").concat("]"))
9494
end
9595

96+
# Given a proc, returns a proc that can be used to iterate over a JSON stream of chunks.
97+
# For each chunk, the proc is called giving it the JSON object. The JSON object could be a
98+
# data object or an error object as described in the OpenAI API documentation.
99+
#
100+
# If the JSON object for a given data or error message is invalid, it is ignored.
101+
#
102+
# @param user_proc [Proc] The proc to call for each JSON object in the chunk.
103+
# @return [Proc] A proc that can be used to iterate over the JSON stream.
96104
private_class_method def self.to_json_stream(user_proc:)
97-
proc do |chunk, bytesize|
98-
# Clean up response string of chunks.
99-
chunk = chunk.gsub("\n\ndata: [DONE]\n\n", "").gsub("\n\ndata:", ",").gsub("data: ", "")
100-
101-
# Turn it into JSON.
102-
chunk = to_json(chunk)
103-
104-
# Pass an array of JSONified chunk(s) to the user's Proc.
105-
user_proc.call([chunk].flatten, bytesize)
105+
proc do |chunk, _|
106+
chunk.scan(/(?:data|error): (\{.*\})/i).flatten.each do |data|
107+
user_proc.call(JSON.parse(data))
108+
rescue JSON::ParserError
109+
# Ignore invalid JSON.
110+
end
106111
end
107112
end
108113

spec/openai/client/chat_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
it "succeeds" do
3636
VCR.use_cassette(cassette) do
3737
response
38-
expect(chunks.dig(0, 0, "choices", 0, "index")).to eq(0)
38+
expect(chunks.dig(0, "choices", 0, "index")).to eq(0)
3939
end
4040
end
4141
end

spec/openai/client/client_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,88 @@
2525
end
2626
end
2727

28+
describe ".to_json_stream" do
29+
context "with a proc" do
30+
let(:user_proc) { proc { |x| x } }
31+
let(:stream) { OpenAI::Client.send(:to_json_stream, user_proc: user_proc) }
32+
33+
it "returns a proc" do
34+
expect(stream).to be_a(Proc)
35+
end
36+
37+
context "when called with a string containing a single JSON object" do
38+
it "calls the user proc with the data parsed as JSON" do
39+
expect(user_proc).to receive(:call).with(JSON.parse('{"foo": "bar"}'))
40+
stream.call('data: { "foo": "bar" }')
41+
end
42+
end
43+
44+
context "when called with string containing more than one JSON object" do
45+
it "calls the user proc for each data parsed as JSON" do
46+
expect(user_proc).to receive(:call).with(JSON.parse('{"foo": "bar"}'))
47+
expect(user_proc).to receive(:call).with(JSON.parse('{"baz": "qud"}'))
48+
49+
stream.call(<<-CHUNK)
50+
data: { "foo": "bar" }
51+
52+
data: { "baz": "qud" }
53+
54+
data: [DONE]
55+
56+
CHUNK
57+
end
58+
end
59+
60+
context "when called with a string that does not even resemble a JSON object" do
61+
let(:bad_examples) { ["", "foo", "data: ", "data: foo"] }
62+
63+
it "does not call the user proc" do
64+
bad_examples.each do |chunk|
65+
expect(user_proc).to_not receive(:call)
66+
stream.call(chunk)
67+
end
68+
end
69+
end
70+
71+
context "when called with a string containing that looks like a JSON object but is invalid" do
72+
let(:chunk) do
73+
<<-CHUNK
74+
data: { "foo": "bar" }
75+
data: { BAD ]:-> JSON }
76+
CHUNK
77+
end
78+
79+
it "does not raise an error" do
80+
expect(user_proc).to receive(:call).with(JSON.parse('{"foo": "bar"}'))
81+
82+
expect do
83+
stream.call(chunk)
84+
end.to_not raise_error(JSON::ParserError)
85+
end
86+
end
87+
88+
context "when called with a string containing an error" do
89+
let(:chunk) do
90+
<<-CHUNK
91+
data: { "foo": "bar" }
92+
error: { "message": "A bad thing has happened!" }
93+
CHUNK
94+
end
95+
96+
it "does not raise an error" do
97+
expect(user_proc).to receive(:call).with(JSON.parse('{ "foo": "bar" }'))
98+
expect(user_proc).to receive(:call).with(
99+
JSON.parse('{ "message": "A bad thing has happened!" }')
100+
)
101+
102+
expect do
103+
stream.call(chunk)
104+
end.to_not raise_error(JSON::ParserError)
105+
end
106+
end
107+
end
108+
end
109+
28110
describe ".to_json" do
29111
context "with a jsonl string" do
30112
let(:body) { "{\"prompt\":\":)\"}\n{\"prompt\":\":(\"}\n" }

0 commit comments

Comments
 (0)