From a22138a45ec1e164e8a16885ea8f16ed96c0826c Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Thu, 19 Oct 2023 18:03:33 -0400 Subject: [PATCH 01/11] Raise errors on HTTP errors or JSON parse errors --- lib/openai/http.rb | 18 ++++---- ...urbo_streamed_chat_with_error_response.yml | 42 +++++++++++++++++++ spec/openai/client/chat_spec.rb | 22 ++++++++++ spec/openai/client/finetunes_spec.rb | 4 +- spec/openai/client/http_spec.rb | 24 +---------- 5 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 spec/fixtures/cassettes/gpt-3_5-turbo_streamed_chat_with_error_response.yml diff --git a/lib/openai/http.rb b/lib/openai/http.rb index b00c0207..26b2f156 100644 --- a/lib/openai/http.rb +++ b/lib/openai/http.rb @@ -59,25 +59,21 @@ def to_json_stream(user_proc:) proc do |chunk, _bytes, env| if env && env.status != 200 - emit_json(json: chunk, user_proc: user_proc) - else - parser.feed(chunk) do |_type, data| - emit_json(json: data, user_proc: user_proc) unless data == "[DONE]" - end + raise_error = Faraday::Response::RaiseError.new + raise_error.on_complete(env.merge(body: JSON.parse(chunk))) end - end - end - def emit_json(json:, user_proc:) - user_proc.call(JSON.parse(json)) - rescue JSON::ParserError - # Ignore invalid JSON. + parser.feed(chunk) do |_type, data| + user_proc.call(JSON.parse(data)) unless data == "[DONE]" + end + end end def conn(multipart: false) Faraday.new do |f| f.options[:timeout] = @request_timeout f.request(:multipart) if multipart + f.response :raise_error end end diff --git a/spec/fixtures/cassettes/gpt-3_5-turbo_streamed_chat_with_error_response.yml b/spec/fixtures/cassettes/gpt-3_5-turbo_streamed_chat_with_error_response.yml new file mode 100644 index 00000000..2bb10218 --- /dev/null +++ b/spec/fixtures/cassettes/gpt-3_5-turbo_streamed_chat_with_error_response.yml @@ -0,0 +1,42 @@ +--- +http_interactions: + - request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"Hello!"}],"stream":true}' + headers: + Content-Type: + - application/json + Authorization: + - Bearer + 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: + - Mon, 14 Aug 2023 15:02:13 GMT + Content-Type: + - application/json + body: + encoding: UTF-8 + string: |+ + { + "error": { + "message": "Test error", + "type": "test_error", + "param": null, + "code": "test" + } + } + recorded_at: Mon, 14 Aug 2023 15:02:13 GMT + +recorded_with: VCR 6.1.0 diff --git a/spec/openai/client/chat_spec.rb b/spec/openai/client/chat_spec.rb index 4dc2f701..272b28da 100644 --- a/spec/openai/client/chat_spec.rb +++ b/spec/openai/client/chat_spec.rb @@ -72,6 +72,28 @@ def call(chunk) end end end + + context "with an error response" do + let(:cassette) { "#{model} streamed chat with error response".downcase } + + it "raises an HTTP error" do + VCR.use_cassette(cassette) do + response + rescue Faraday::BadRequestError => e + expect(e.response).to include(status: 400) + expect(e.response[:body]).to eq({ + "error" => { + "message" => "Test error", + "type" => "test_error", + "param" => nil, + "code" => "test" + } + }) + else + raise "Expected to raise Faraday::BadRequestError" + end + end + end end end diff --git a/spec/openai/client/finetunes_spec.rb b/spec/openai/client/finetunes_spec.rb index 6eaccfcb..d5b7c768 100644 --- a/spec/openai/client/finetunes_spec.rb +++ b/spec/openai/client/finetunes_spec.rb @@ -81,9 +81,9 @@ # It takes too long to fine-tune a model so we can delete it when running the test suite # against the live API. Instead, we just check that the API returns an error. - it "returns an error" do + it "raises an error" do VCR.use_cassette(cassette) do - expect(response.dig("error", "message")).to include("does not exist") + expect { response }.to raise_error(Faraday::ResourceNotFound) end end diff --git a/spec/openai/client/http_spec.rb b/spec/openai/client/http_spec.rb index d12b027a..2eaa1937 100644 --- a/spec/openai/client/http_spec.rb +++ b/spec/openai/client/http_spec.rb @@ -144,32 +144,12 @@ CHUNK end - it "does not raise an error" do + it "raise an error" do expect(user_proc).to receive(:call).with(JSON.parse('{"foo": "bar"}')) expect do stream.call(chunk) - end.not_to raise_error - end - end - - context "wehn called with string containing Obie's invalid JSON" do - let(:chunk) do - <<~CHUNK - data: { "foo": "bar" } - - data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":123,"model":"gpt-4","choices":[{"index":0,"delta":{"role":"assistant","content":null,"fu - - # - CHUNK - end - - it "does not raise an error" do - expect(user_proc).to receive(:call).with(JSON.parse('{"foo": "bar"}')) - - expect do - stream.call(chunk) - end.not_to raise_error + end.to raise_error(JSON::ParserError) end end From a4d467db31ba8fc10121554bce0ef7286c7d6b10 Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Thu, 19 Oct 2023 20:17:04 -0400 Subject: [PATCH 02/11] Remove completion test in finetunes suite --- .../finetune_completions_i_love_mondays.yml | 175 ------------------ ...tune_completions_i_love_mondays_create.yml | 125 ------------- spec/openai/client/finetunes_spec.rb | 20 -- 3 files changed, 320 deletions(-) delete mode 100644 spec/fixtures/cassettes/finetune_completions_i_love_mondays.yml delete mode 100644 spec/fixtures/cassettes/finetune_completions_i_love_mondays_create.yml diff --git a/spec/fixtures/cassettes/finetune_completions_i_love_mondays.yml b/spec/fixtures/cassettes/finetune_completions_i_love_mondays.yml deleted file mode 100644 index fb0b2b0a..00000000 --- a/spec/fixtures/cassettes/finetune_completions_i_love_mondays.yml +++ /dev/null @@ -1,175 +0,0 @@ ---- -http_interactions: -- request: - method: post - uri: https://api.openai.com/v1/completions - body: - encoding: UTF-8 - string: '{"model":"ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52","prompt":"I - love Mondays"}' - headers: - Content-Type: - - application/json - Authorization: - - Bearer - 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 Apr 2023 10:09:28 GMT - Content-Type: - - application/json - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Access-Control-Allow-Origin: - - "*" - Cache-Control: - - no-cache, must-revalidate - Openai-Model: - - ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52 - Openai-Organization: - - user-jxm65ijkzc1qrfhc0ij8moic - Openai-Processing-Ms: - - '505' - Openai-Version: - - '2020-10-01' - Strict-Transport-Security: - - max-age=15724800; includeSubDomains - X-Ratelimit-Limit-Requests: - - '3000' - X-Ratelimit-Limit-Tokens: - - '250000' - X-Ratelimit-Remaining-Requests: - - '2999' - X-Ratelimit-Remaining-Tokens: - - '249983' - X-Ratelimit-Reset-Requests: - - 20ms - X-Ratelimit-Reset-Tokens: - - 3ms - X-Request-Id: - - b051193af1c4fbf86b789b6103134d70 - Cf-Cache-Status: - - DYNAMIC - Server: - - cloudflare - Cf-Ray: - - 7bde07c2ae3d4ad1-CGK - Alt-Svc: - - h3=":443"; ma=86400, h3-29=":443"; ma=86400 - body: - encoding: ASCII-8BIT - string: '{"id":"cmpl-79WNTFdjpCCVZljOg9XhP1TacLq7t","object":"text_completion","created":1682503767,"model":"ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52","choices":[{"text":" - without kids and I love one out of every four days of being held in a","index":0,"logprobs":null,"finish_reason":"length"}],"usage":{"prompt_tokens":3,"completion_tokens":16,"total_tokens":19}} - - ' - recorded_at: Wed, 26 Apr 2023 10:09:28 GMT -- request: - method: post - uri: https://api.openai.com/v1/completions - body: - encoding: UTF-8 - string: '{"model":"ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52","prompt":"I - love Mondays"}' - headers: - Content-Type: - - application/json - Authorization: - - Bearer - Test: - - X-Default - 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: - - Mon, 14 Aug 2023 15:02:53 GMT - Content-Type: - - application/json - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Access-Control-Allow-Origin: - - "*" - Cache-Control: - - no-cache, must-revalidate - Openai-Model: - - ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52 - Openai-Organization: - - user-jxm65ijkzc1qrfhc0ij8moic - Openai-Processing-Ms: - - '116' - Openai-Version: - - '2020-10-01' - Strict-Transport-Security: - - max-age=15724800; includeSubDomains - X-Ratelimit-Limit-Requests: - - '3000' - X-Ratelimit-Limit-Tokens: - - '250000' - X-Ratelimit-Limit-Tokens-Usage-Based: - - '250000' - X-Ratelimit-Remaining-Requests: - - '2999' - X-Ratelimit-Remaining-Tokens: - - '249983' - X-Ratelimit-Remaining-Tokens-Usage-Based: - - '249983' - X-Ratelimit-Reset-Requests: - - 20ms - X-Ratelimit-Reset-Tokens: - - 3ms - X-Ratelimit-Reset-Tokens-Usage-Based: - - 3ms - X-Request-Id: - - 55b2a15f27c0ffd867978c0b447a95e9 - Cf-Cache-Status: - - DYNAMIC - Server: - - cloudflare - Cf-Ray: - - 7f6a14d46b634136-LHR - Alt-Svc: - - h3=":443"; ma=86400 - body: - encoding: ASCII-8BIT - string: | - { - "id": "cmpl-7nTNl18qWMK7AUe0FD6dMbceH4Jk7", - "object": "text_completion", - "created": 1692025373, - "model": "ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52", - "choices": [ - { - "text": ". It screams every Thursday to come. I love the Sunday blues, too.\"", - "index": 0, - "logprobs": null, - "finish_reason": "length" - } - ], - "usage": { - "prompt_tokens": 3, - "completion_tokens": 16, - "total_tokens": 19 - } - } - recorded_at: Mon, 14 Aug 2023 15:02:53 GMT -recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/cassettes/finetune_completions_i_love_mondays_create.yml b/spec/fixtures/cassettes/finetune_completions_i_love_mondays_create.yml deleted file mode 100644 index aa39f278..00000000 --- a/spec/fixtures/cassettes/finetune_completions_i_love_mondays_create.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -http_interactions: -- request: - method: post - uri: https://api.openai.com/v1/fine-tunes - body: - encoding: UTF-8 - string: '{"training_file":"file-M1RDS8Ezce8dgwbBReH7rZjI","model":"ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52"}' - headers: - Content-Type: - - application/json - Authorization: - - Bearer - 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 Apr 2023 10:09:27 GMT - Content-Type: - - application/json - Content-Length: - - '330' - Connection: - - keep-alive - Access-Control-Allow-Origin: - - "*" - Openai-Version: - - '2020-10-01' - X-Request-Id: - - 34830a91068469e97ce329304c20005e - Openai-Processing-Ms: - - '46' - Strict-Transport-Security: - - max-age=15724800; includeSubDomains - Cf-Cache-Status: - - DYNAMIC - Server: - - cloudflare - Cf-Ray: - - 7bde07bfbf4ebeac-CGK - Alt-Svc: - - h3=":443"; ma=86400, h3-29=":443"; ma=86400 - body: - encoding: UTF-8 - string: | - { - "error": { - "message": "Invalid base model: ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52 (we don't support fine-tuning from this model. However, we will support fine-tuning from all new models fine-tuned from this point onwards.)", - "type": "invalid_request_error", - "param": null, - "code": null - } - } - recorded_at: Wed, 26 Apr 2023 10:09:27 GMT -- request: - method: post - uri: https://api.openai.com/v1/fine-tunes - body: - encoding: UTF-8 - string: '{"training_file":"file-vdxufGUKXK9TYCB9M1itajjD","model":"ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52"}' - headers: - Content-Type: - - application/json - Authorization: - - Bearer - Test: - - X-Default - 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: - - Mon, 14 Aug 2023 15:02:52 GMT - Content-Type: - - application/json - Content-Length: - - '330' - Connection: - - keep-alive - Access-Control-Allow-Origin: - - "*" - Openai-Version: - - '2020-10-01' - X-Request-Id: - - 72e691fc5f995b39d3841e8788dfac8a - Openai-Processing-Ms: - - '36' - Strict-Transport-Security: - - max-age=15724800; includeSubDomains - Cf-Cache-Status: - - DYNAMIC - Server: - - cloudflare - Cf-Ray: - - 7f6a14d27b4c7773-LHR - Alt-Svc: - - h3=":443"; ma=86400 - body: - encoding: UTF-8 - string: | - { - "error": { - "message": "Invalid base model: ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52 (we don't support fine-tuning from this model. However, we will support fine-tuning from all new models fine-tuned from this point onwards.)", - "type": "invalid_request_error", - "param": null, - "code": null - } - } - recorded_at: Mon, 14 Aug 2023 15:02:52 GMT -recorded_with: VCR 6.1.0 diff --git a/spec/openai/client/finetunes_spec.rb b/spec/openai/client/finetunes_spec.rb index d5b7c768..7289e75d 100644 --- a/spec/openai/client/finetunes_spec.rb +++ b/spec/openai/client/finetunes_spec.rb @@ -95,25 +95,5 @@ end end end - - describe "#completions" do - let(:prompt) { "I love Mondays" } - let(:cassette) { "finetune completions #{prompt}".downcase } - let(:model) { "ada:ft-user-jxm65ijkzc1qrfhc0ij8moic-2021-12-11-20-11-52" } - let(:response) do - OpenAI::Client.new.completions( - parameters: { - model: model, - prompt: prompt - } - ) - end - - it "succeeds" do - VCR.use_cassette(cassette) do - expect(response["model"]).to eq(model) - end - end - end end end From dfe0376853535cf338b989ab23c99525c609d2aa Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Thu, 19 Oct 2023 20:53:34 -0400 Subject: [PATCH 03/11] Update OpenAI error handling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14fb3975..9be492d2 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ puts response.dig("choices", 0, "message", "content") [Quick guide to streaming ChatGPT with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) -You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of text chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will pass that to your proc as a Hash. +You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of completion chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will raise an error. ```ruby client.chat( From 7739fc68764c32c0dd67cf255ce6bd8cd28b8143 Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Thu, 19 Oct 2023 20:53:55 -0400 Subject: [PATCH 04/11] Update bit about completion token counting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9be492d2..4e9d4ec1 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ client.chat( # => "Anna is a young woman in her mid-twenties, with wavy chestnut hair that falls to her shoulders..." ``` -Note: the API docs state that token usage is included in the streamed chat chunk objects, but this doesn't currently appear to be the case. To count tokens while streaming, try `OpenAI.rough_token_count` or [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby). +Note: OpenAPI currently does not report token usage for streaming responses. To count tokens while streaming, try `OpenAI.rough_token_count` or [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby). Also, each call to the stream proc corresponds to a single token, so you can count the number of calls to the proc to get the completion token count. ### Functions From 2b854398839c7d857a32706012a72ca6c6951a1a Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Thu, 19 Oct 2023 20:56:22 -0400 Subject: [PATCH 05/11] GPT is the model, ChatGPT is a specific chat app --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4e9d4ec1..a1ea88d5 100644 --- a/README.md +++ b/README.md @@ -161,9 +161,9 @@ client.models.retrieve(id: "text-ada-001") - text-babbage-001 - text-curie-001 -### ChatGPT +### GPT -ChatGPT is a model that can be used to generate text in a conversational style. You can use it to [generate a response](https://platform.openai.com/docs/api-reference/chat/create) to a sequence of [messages](https://platform.openai.com/docs/guides/chat/introduction): +GPT is a model that can be used to generate text in a conversational style. You can use it to [generate a response](https://platform.openai.com/docs/api-reference/chat/create) to a sequence of [messages](https://platform.openai.com/docs/guides/chat/introduction): ```ruby response = client.chat( @@ -176,9 +176,9 @@ puts response.dig("choices", 0, "message", "content") # => "Hello! How may I assist you today?" ``` -### Streaming ChatGPT +### Streaming GPT -[Quick guide to streaming ChatGPT with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) +[Quick guide to streaming GPT with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of completion chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will raise an error. From 812cbea8e5773c6a87aa828f05367644fadf77d0 Mon Sep 17 00:00:00 2001 From: Alex Rudall Date: Tue, 31 Oct 2023 12:13:19 +0000 Subject: [PATCH 06/11] Tweak README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a1ea88d5..1ed93479 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ client.models.retrieve(id: "text-ada-001") - text-babbage-001 - text-curie-001 -### GPT +### Chat GPT is a model that can be used to generate text in a conversational style. You can use it to [generate a response](https://platform.openai.com/docs/api-reference/chat/create) to a sequence of [messages](https://platform.openai.com/docs/guides/chat/introduction): @@ -176,9 +176,9 @@ puts response.dig("choices", 0, "message", "content") # => "Hello! How may I assist you today?" ``` -### Streaming GPT +### Streaming Chat -[Quick guide to streaming GPT with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) +[Quick guide to streaming Chat with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of completion chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will raise an error. @@ -195,7 +195,7 @@ client.chat( # => "Anna is a young woman in her mid-twenties, with wavy chestnut hair that falls to her shoulders..." ``` -Note: OpenAPI currently does not report token usage for streaming responses. To count tokens while streaming, try `OpenAI.rough_token_count` or [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby). Also, each call to the stream proc corresponds to a single token, so you can count the number of calls to the proc to get the completion token count. +Note: OpenAPI currently does not report token usage for streaming responses. To count tokens while streaming, try `OpenAI.rough_token_count` or [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby). We think that each call to the stream proc corresponds to a single token, so you can also try counting the number of calls to the proc to get the completion token count. ### Functions From 8db163922bdc42c0aaa38122347676119c6e9293 Mon Sep 17 00:00:00 2001 From: Alex Rudall Date: Tue, 31 Oct 2023 12:42:34 +0000 Subject: [PATCH 07/11] Fix couple of errors in spec --- spec/openai/client/http_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/openai/client/http_spec.rb b/spec/openai/client/http_spec.rb index 2eaa1937..f8f3a536 100644 --- a/spec/openai/client/http_spec.rb +++ b/spec/openai/client/http_spec.rb @@ -155,7 +155,13 @@ context "when OpenAI returns an HTTP error" do let(:chunk) { "{\"error\":{\"message\":\"A bad thing has happened!\"}}" } - let(:env) { Faraday::Env.new(status: 500) } + let(:env) do + Faraday::Env.new( + status: 500, + url: URI::HTTPS.build(host: "api.openai.com"), + request: Faraday::RequestOptions.new + ) + end it "does not raise an error and calls the user proc with the error parsed as JSON" do expect(user_proc).to receive(:call).with( From bf29da49ec35c186a09ef5782e6b93834eed5b09 Mon Sep 17 00:00:00 2001 From: Ates Goral Date: Wed, 1 Nov 2023 10:10:07 -0400 Subject: [PATCH 08/11] Remove HTTP error case from http spec --- spec/openai/client/http_spec.rb | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/spec/openai/client/http_spec.rb b/spec/openai/client/http_spec.rb index f8f3a536..01f7ae7f 100644 --- a/spec/openai/client/http_spec.rb +++ b/spec/openai/client/http_spec.rb @@ -153,31 +153,6 @@ end end - context "when OpenAI returns an HTTP error" do - let(:chunk) { "{\"error\":{\"message\":\"A bad thing has happened!\"}}" } - let(:env) do - Faraday::Env.new( - status: 500, - url: URI::HTTPS.build(host: "api.openai.com"), - request: Faraday::RequestOptions.new - ) - end - - it "does not raise an error and calls the user proc with the error parsed as JSON" do - expect(user_proc).to receive(:call).with( - { - "error" => { - "message" => "A bad thing has happened!" - } - } - ) - - expect do - stream.call(chunk, 0, env) - end.not_to raise_error - end - end - context "when called with JSON split across chunks" do it "calls the user proc with the data parsed as JSON" do expect(user_proc).to receive(:call).with(JSON.parse('{ "foo": "bar" }')) From 038a81fee3a70d70d051568973964bfd7e0bc42e Mon Sep 17 00:00:00 2001 From: Alex Rudall Date: Sat, 4 Nov 2023 13:46:10 +0000 Subject: [PATCH 09/11] Tweak docs --- README.md | 2 +- lib/openai/http.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1ed93479..793b9f0c 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ puts response.dig("choices", 0, "message", "content") [Quick guide to streaming Chat with Rails 7 and Hotwire](https://gist.github.com/alexrudall/cb5ee1e109353ef358adb4e66631799d) -You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of completion chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will raise an error. +You can stream from the API in realtime, which can be much faster and used to create a more engaging user experience. Pass a [Proc](https://ruby-doc.org/core-2.6/Proc.html) (or any object with a `#call` method) to the `stream` parameter to receive the stream of completion chunks as they are generated. Each time one or more chunks is received, the proc will be called once with each chunk, parsed as a Hash. If OpenAI returns an error, `ruby-openai` will raise a Faraday error. ```ruby client.chat( diff --git a/lib/openai/http.rb b/lib/openai/http.rb index 26b2f156..bc06de9d 100644 --- a/lib/openai/http.rb +++ b/lib/openai/http.rb @@ -50,8 +50,6 @@ def to_json(string) # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could # be a data object or an error object as described in the OpenAI API documentation. # - # If the JSON object for a given data or error message is invalid, it is ignored. - # # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk. # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON. def to_json_stream(user_proc:) From 61970cd90c870e01e1af210d180034619844e35e Mon Sep 17 00:00:00 2001 From: Alex Rudall Date: Sun, 5 Nov 2023 23:47:22 +0000 Subject: [PATCH 10/11] Add spec for GET error --- .../http_get_with_error_response.yml | 41 +++++++++++++++++++ spec/openai/client/http_spec.rb | 16 ++++++++ 2 files changed, 57 insertions(+) create mode 100644 spec/fixtures/cassettes/http_get_with_error_response.yml diff --git a/spec/fixtures/cassettes/http_get_with_error_response.yml b/spec/fixtures/cassettes/http_get_with_error_response.yml new file mode 100644 index 00000000..bfc3f5a8 --- /dev/null +++ b/spec/fixtures/cassettes/http_get_with_error_response.yml @@ -0,0 +1,41 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.openai.com/v1/models/text-ada-001 + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Authorization: + - Bearer + 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: + - Mon, 14 Aug 2023 15:02:13 GMT + Content-Type: + - application/json + body: + encoding: UTF-8 + string: |+ + { + "error": { + "message": "Test error", + "type": "test_error", + "param": null, + "code": "test" + } + } + recorded_at: Mon, 14 Aug 2023 15:02:13 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/openai/client/http_spec.rb b/spec/openai/client/http_spec.rb index 01f7ae7f..5149ed5f 100644 --- a/spec/openai/client/http_spec.rb +++ b/spec/openai/client/http_spec.rb @@ -96,6 +96,22 @@ end end + describe ".get" do + context "with an error response" do + let(:cassette) { "http get with error response".downcase } + + it "raises an HTTP error" do + VCR.use_cassette(cassette) do + OpenAI::Client.new.models.retrieve(id: "text-ada-001") + rescue Faraday::Error => e + expect(e.response).to include(status: 400) + else + raise "Expected to raise Faraday::BadRequestError" + end + end + end + end + describe ".to_json_stream" do context "with a proc" do let(:user_proc) { proc { |x| x } } From 37df302f38896334fe465adf89ed910343ed24ed Mon Sep 17 00:00:00 2001 From: Alex Rudall Date: Sun, 5 Nov 2023 23:54:07 +0000 Subject: [PATCH 11/11] Add Errors section to the README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 793b9f0c..ab370a31 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,18 @@ puts response["text"] # => "Transcription of the text" ``` +#### Errors + +HTTP errors can be caught like this: + +``` + begin + OpenAI::Client.new.models.retrieve(id: "text-ada-001") + rescue Faraday::Error => e + raise "Got a Faraday error: #{e}" + end +``` + ## Development After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.