From cce43a8e36ea9703982d86abab18f5f70dcf3c02 Mon Sep 17 00:00:00 2001 From: Paulo Fidalgo Date: Thu, 16 Apr 2026 15:35:48 +0300 Subject: [PATCH 1/2] Handle nil response bodies consistently across providers Faraday JSON middleware can normalize empty HTTP bodies to nil, but ruby_llm still assumed response.body was always non-nil in several sync completion and error parsing paths. That leaked as NoMethodError from provider parsers instead of raising a RubyLLM::Error with a useful message. Centralize the sync completion invariant in Provider by adding ensure_response_body! and calling it from Provider#sync_response. Apply the same guard to Bedrock's custom signed sync path so override implementations follow the same contract. Harden provider-specific parsers that were still nil-fragile: - OpenAI::Chat#parse_completion_response - OpenRouter::Chat#parse_completion_response - Anthropic::Chat#parse_completion_response - Gemini::Chat#parse_completion_response - Bedrock::Chat#parse_completion_response - Provider#parse_error - OpenRouter#parse_error - Perplexity#parse_error Standardize nil sync completion bodies to raise: RubyLLM::Error, "Provider returned an empty response body" Add regression coverage for: - Provider#sync_response nil-body handling - Provider/OpenRouter/Perplexity parse_error nil-body handling - OpenAI/OpenRouter/Anthropic/Gemini/Bedrock chat parser nil-body handling This keeps empty-body failures inside the gem's error model and makes nil-body behavior consistent across the provider stack. --- lib/ruby_llm/provider.rb | 8 ++- lib/ruby_llm/providers/anthropic/chat.rb | 2 + lib/ruby_llm/providers/bedrock/auth.rb | 1 + lib/ruby_llm/providers/bedrock/chat.rb | 3 +- lib/ruby_llm/providers/gemini/chat.rb | 2 + lib/ruby_llm/providers/openai/chat.rb | 2 + lib/ruby_llm/providers/openrouter.rb | 2 +- lib/ruby_llm/providers/openrouter/chat.rb | 2 + lib/ruby_llm/providers/perplexity.rb | 2 +- spec/ruby_llm/provider_spec.rb | 51 +++++++++++++++++++ .../ruby_llm/providers/anthropic/chat_spec.rb | 8 +++ spec/ruby_llm/providers/bedrock/chat_spec.rb | 10 ++++ spec/ruby_llm/providers/gemini/chat_spec.rb | 13 +++++ spec/ruby_llm/providers/open_ai/chat_spec.rb | 8 +++ .../providers/open_router/chat_spec.rb | 10 ++++ .../providers/open_router/parse_error_spec.rb | 6 +++ spec/ruby_llm/providers/perplexity_spec.rb | 19 +++++++ 17 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 spec/ruby_llm/providers/perplexity_spec.rb diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 2b588c547..aa56bbb87 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -112,7 +112,7 @@ def assume_models_exist? end def parse_error(response) - return if response.body.empty? + return if response.body.nil? || response.body.empty? body = try_parse_json(response.body) case body @@ -266,7 +266,13 @@ def sync_response(connection, payload, additional_headers = {}) response = connection.post completion_url, payload do |req| req.headers = additional_headers.merge(req.headers) unless additional_headers.empty? end + ensure_response_body!(response) + parse_completion_response response end + + def ensure_response_body!(response) + raise Error.new(response, 'Provider returned an empty response body') if response.body.nil? + end end end diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index 9926fe98b..153a83f03 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -84,6 +84,8 @@ def build_output_config(schema) def parse_completion_response(response) data = response.body + raise Error.new(response, 'Provider returned an empty response body') if data.nil? + content_blocks = data['content'] || [] text_content = extract_text_content(content_blocks) diff --git a/lib/ruby_llm/providers/bedrock/auth.rb b/lib/ruby_llm/providers/bedrock/auth.rb index cc33a2217..cbf380aac 100644 --- a/lib/ruby_llm/providers/bedrock/auth.rb +++ b/lib/ruby_llm/providers/bedrock/auth.rb @@ -21,6 +21,7 @@ def signed_post(connection, url, payload, additional_headers = {}) yield req if block_given? end + ensure_response_body!(response) parse_completion_response(response) end diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index db7785ea7..87958e28c 100644 --- a/lib/ruby_llm/providers/bedrock/chat.rb +++ b/lib/ruby_llm/providers/bedrock/chat.rb @@ -45,7 +45,8 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, def parse_completion_response(response) data = response.body - return if data.nil? || data.empty? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? + return if data.empty? content_blocks = data.dig('output', 'message', 'content') || [] usage = data['usage'] || {} diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index 54fc51f72..9341bbd0c 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -107,6 +107,8 @@ def build_thought_part(thinking) def parse_completion_response(response) data = response.body + raise Error.new(response, 'Provider returned an empty response body') if data.nil? + parts = data.dig('candidates', 0, 'content', 'parts') || [] tool_calls = extract_tool_calls(data) diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index ab9552edd..d9fbe952e 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -53,6 +53,8 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema def parse_completion_response(response) data = response.body + raise Error.new(response, 'Provider returned an empty response body') if data.nil? + return if data.empty? raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message') diff --git a/lib/ruby_llm/providers/openrouter.rb b/lib/ruby_llm/providers/openrouter.rb index af2f444ad..62b6c6e01 100644 --- a/lib/ruby_llm/providers/openrouter.rb +++ b/lib/ruby_llm/providers/openrouter.rb @@ -20,7 +20,7 @@ def headers end def parse_error(response) - return if response.body.empty? + return if response.body.nil? || response.body.empty? body = try_parse_json(response.body) case body diff --git a/lib/ruby_llm/providers/openrouter/chat.rb b/lib/ruby_llm/providers/openrouter/chat.rb index 0c3622bdf..6e24e73e7 100644 --- a/lib/ruby_llm/providers/openrouter/chat.rb +++ b/lib/ruby_llm/providers/openrouter/chat.rb @@ -52,6 +52,8 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema def parse_completion_response(response) data = response.body + raise Error.new(response, 'Provider returned an empty response body') if data.nil? + return if data.empty? raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message') diff --git a/lib/ruby_llm/providers/perplexity.rb b/lib/ruby_llm/providers/perplexity.rb index 3c4fa91cc..46da62fd2 100644 --- a/lib/ruby_llm/providers/perplexity.rb +++ b/lib/ruby_llm/providers/perplexity.rb @@ -34,7 +34,7 @@ def configuration_requirements def parse_error(response) body = response.body - return if body.empty? + return if body.nil? || body.empty? # If response is HTML (Perplexity returns HTML for auth errors) if body.include?('') && body.include?('') diff --git a/spec/ruby_llm/provider_spec.rb b/spec/ruby_llm/provider_spec.rb index 4e14b5dfa..b44b6d668 100644 --- a/spec/ruby_llm/provider_spec.rb +++ b/spec/ruby_llm/provider_spec.rb @@ -3,6 +3,57 @@ require 'spec_helper' RSpec.describe RubyLLM::Provider do + describe '#sync_response' do + let(:provider_class) do + Class.new(described_class) do + def api_base + 'https://example.com' + end + + def completion_url + 'chat/completions' + end + + def parse_completion_response(_response) + :parsed + end + end + end + let(:provider) { provider_class.new(RubyLLM::Configuration.new) } + + it 'raises RubyLLM::Error for nil completion bodies' do + request = instance_double(Faraday::Request, headers: {}) + response = instance_double(Faraday::Response, body: nil) + connection = instance_double(RubyLLM::Connection) + + allow(connection).to receive(:post) + .with('chat/completions', { prompt: 'hello' }) + .and_yield(request) + .and_return(response) + + expect do + provider.send(:sync_response, connection, { prompt: 'hello' }) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + end + + describe '#parse_error' do + let(:provider_class) do + Class.new(described_class) do + def api_base + 'https://example.com' + end + end + end + let(:provider) { provider_class.new(RubyLLM::Configuration.new) } + + it 'returns nil when the response body is nil' do + response = Struct.new(:body).new(nil) + + expect(provider.parse_error(response)).to be_nil + end + end + describe '.register' do it 'registers provider configuration options on Configuration' do provider_key = :test_provider_spec diff --git a/spec/ruby_llm/providers/anthropic/chat_spec.rb b/spec/ruby_llm/providers/anthropic/chat_spec.rb index 004864050..c6b58c3a9 100644 --- a/spec/ruby_llm/providers/anthropic/chat_spec.rb +++ b/spec/ruby_llm/providers/anthropic/chat_spec.rb @@ -134,6 +134,14 @@ end describe '.parse_completion_response' do + it 'raises RubyLLM::Error for nil completion bodies' do + response = instance_double(Faraday::Response, body: nil) + + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + it 'captures cache usage metrics on the message' do response_body = { 'model' => 'claude-sonnet-4-5-20250929', diff --git a/spec/ruby_llm/providers/bedrock/chat_spec.rb b/spec/ruby_llm/providers/bedrock/chat_spec.rb index e672af2ac..7e58f388f 100644 --- a/spec/ruby_llm/providers/bedrock/chat_spec.rb +++ b/spec/ruby_llm/providers/bedrock/chat_spec.rb @@ -3,6 +3,16 @@ require 'spec_helper' RSpec.describe RubyLLM::Providers::Bedrock::Chat do + describe '.parse_completion_response' do + it 'raises RubyLLM::Error for nil completion bodies' do + response = instance_double(Faraday::Response, body: nil) + + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + end + describe '.render_tool_result_content' do it 'uses a placeholder when the tool returns no content' do result = described_class.render_tool_result_content('') diff --git a/spec/ruby_llm/providers/gemini/chat_spec.rb b/spec/ruby_llm/providers/gemini/chat_spec.rb index ea4675262..e43a5acaf 100644 --- a/spec/ruby_llm/providers/gemini/chat_spec.rb +++ b/spec/ruby_llm/providers/gemini/chat_spec.rb @@ -525,6 +525,19 @@ end describe '#parse_completion_response' do + it 'raises RubyLLM::Error for nil completion bodies' do + response = Struct.new(:body, :env).new( + nil, + Struct.new(:url).new(Struct.new(:path).new('/v1/models/gemini-2.5-flash:generateContent')) + ) + + provider = RubyLLM::Providers::Gemini.new(RubyLLM.config) + + expect do + provider.send(:parse_completion_response, response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + it 'keeps thought-only parts out of assistant content' do response = Struct.new(:body, :env).new( { diff --git a/spec/ruby_llm/providers/open_ai/chat_spec.rb b/spec/ruby_llm/providers/open_ai/chat_spec.rb index e6f681299..6e2a3f86e 100644 --- a/spec/ruby_llm/providers/open_ai/chat_spec.rb +++ b/spec/ruby_llm/providers/open_ai/chat_spec.rb @@ -4,6 +4,14 @@ RSpec.describe RubyLLM::Providers::OpenAI::Chat do describe '.parse_completion_response' do + it 'raises RubyLLM::Error for nil completion bodies' do + response = instance_double(Faraday::Response, body: nil) + + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + it 'captures cached token information when present' do response_body = { 'model' => 'gpt-4.1-nano', diff --git a/spec/ruby_llm/providers/open_router/chat_spec.rb b/spec/ruby_llm/providers/open_router/chat_spec.rb index be77c5f83..93c1f023c 100644 --- a/spec/ruby_llm/providers/open_router/chat_spec.rb +++ b/spec/ruby_llm/providers/open_router/chat_spec.rb @@ -3,6 +3,16 @@ require 'spec_helper' RSpec.describe RubyLLM::Providers::OpenRouter::Chat do + describe '.parse_completion_response' do + it 'raises RubyLLM::Error for nil completion bodies' do + response = instance_double(Faraday::Response, body: nil) + + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end + end + describe '.render_payload' do let(:model) { instance_double(RubyLLM::Model::Info, id: 'anthropic/claude-haiku-4.5') } let(:messages) { [RubyLLM::Message.new(role: :user, content: 'Hello')] } diff --git a/spec/ruby_llm/providers/open_router/parse_error_spec.rb b/spec/ruby_llm/providers/open_router/parse_error_spec.rb index 93e36d338..625e96da7 100644 --- a/spec/ruby_llm/providers/open_router/parse_error_spec.rb +++ b/spec/ruby_llm/providers/open_router/parse_error_spec.rb @@ -10,6 +10,12 @@ end describe '#parse_error' do + it 'returns nil when the response body is nil' do + response = instance_double(Faraday::Response, body: nil) + + expect(provider.parse_error(response)).to be_nil + end + it 'appends nested provider message from metadata.raw when present' do response = instance_double( Faraday::Response, diff --git a/spec/ruby_llm/providers/perplexity_spec.rb b/spec/ruby_llm/providers/perplexity_spec.rb new file mode 100644 index 000000000..7a44a16bb --- /dev/null +++ b/spec/ruby_llm/providers/perplexity_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Providers::Perplexity do + let(:provider) do + config = RubyLLM::Configuration.new + config.perplexity_api_key = 'test' + described_class.new(config) + end + + describe '#parse_error' do + it 'returns nil when the response body is nil' do + response = instance_double(Faraday::Response, body: nil) + + expect(provider.parse_error(response)).to be_nil + end + end +end From 4b94ccc4ac442948f5d8c6df09b38a5e94ff2784 Mon Sep 17 00:00:00 2001 From: Paulo Fidalgo <paulo.fidalgo.pt@gmail.com> Date: Tue, 28 Apr 2026 10:10:50 +0300 Subject: [PATCH 2/2] Handle empty completion bodies consistently Follow up to the nil response body handling by treating empty successful completion bodies as the same class of provider failure. Previously some providers raised when the body was nil, but silently returned when the parsed body was empty. Others attempted to parse an empty hash into a mostly blank message. Centralize the request-path guard in Provider#ensure_response_body! so nil and empty bodies both raise RubyLLM::Error. Align each provider parser with the same rule for direct parser calls. Keep parse_error lenient, since that path only tries to extract a better error message. --- lib/ruby_llm/provider.rb | 5 ++++- lib/ruby_llm/providers/anthropic/chat.rb | 2 +- lib/ruby_llm/providers/bedrock/chat.rb | 3 +-- lib/ruby_llm/providers/gemini/chat.rb | 2 +- lib/ruby_llm/providers/openai/chat.rb | 4 +--- lib/ruby_llm/providers/openrouter/chat.rb | 4 +--- spec/ruby_llm/provider_spec.rb | 15 ++++++++++++++ .../ruby_llm/providers/anthropic/chat_spec.rb | 14 +++++++------ spec/ruby_llm/providers/bedrock/chat_spec.rb | 12 ++++++----- spec/ruby_llm/providers/gemini/chat_spec.rb | 20 ++++++++++--------- spec/ruby_llm/providers/open_ai/chat_spec.rb | 12 ++++++----- .../providers/open_router/chat_spec.rb | 12 ++++++----- 12 files changed, 64 insertions(+), 41 deletions(-) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index aa56bbb87..8e354b0ef 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -272,7 +272,10 @@ def sync_response(connection, payload, additional_headers = {}) end def ensure_response_body!(response) - raise Error.new(response, 'Provider returned an empty response body') if response.body.nil? + body = response.body + return unless body.nil? || (body.respond_to?(:empty?) && body.empty?) + + raise Error.new(response, 'Provider returned an empty response body') end end end diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index 153a83f03..545be8314 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -84,7 +84,7 @@ def build_output_config(schema) def parse_completion_response(response) data = response.body - raise Error.new(response, 'Provider returned an empty response body') if data.nil? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? || data.empty? content_blocks = data['content'] || [] diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index 87958e28c..aff598d28 100644 --- a/lib/ruby_llm/providers/bedrock/chat.rb +++ b/lib/ruby_llm/providers/bedrock/chat.rb @@ -45,8 +45,7 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, def parse_completion_response(response) data = response.body - raise Error.new(response, 'Provider returned an empty response body') if data.nil? - return if data.empty? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? || data.empty? content_blocks = data.dig('output', 'message', 'content') || [] usage = data['usage'] || {} diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index 9341bbd0c..4b948594b 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -107,7 +107,7 @@ def build_thought_part(thinking) def parse_completion_response(response) data = response.body - raise Error.new(response, 'Provider returned an empty response body') if data.nil? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? || data.empty? parts = data.dig('candidates', 0, 'content', 'parts') || [] tool_calls = extract_tool_calls(data) diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index d9fbe952e..595feabca 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -53,9 +53,7 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema def parse_completion_response(response) data = response.body - raise Error.new(response, 'Provider returned an empty response body') if data.nil? - - return if data.empty? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? || data.empty? raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message') diff --git a/lib/ruby_llm/providers/openrouter/chat.rb b/lib/ruby_llm/providers/openrouter/chat.rb index 6e24e73e7..0cf841582 100644 --- a/lib/ruby_llm/providers/openrouter/chat.rb +++ b/lib/ruby_llm/providers/openrouter/chat.rb @@ -52,9 +52,7 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema def parse_completion_response(response) data = response.body - raise Error.new(response, 'Provider returned an empty response body') if data.nil? - - return if data.empty? + raise Error.new(response, 'Provider returned an empty response body') if data.nil? || data.empty? raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message') diff --git a/spec/ruby_llm/provider_spec.rb b/spec/ruby_llm/provider_spec.rb index b44b6d668..1ea77f0ee 100644 --- a/spec/ruby_llm/provider_spec.rb +++ b/spec/ruby_llm/provider_spec.rb @@ -35,6 +35,21 @@ def parse_completion_response(_response) provider.send(:sync_response, connection, { prompt: 'hello' }) end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') end + + it 'raises RubyLLM::Error for empty completion bodies' do + request = instance_double(Faraday::Request, headers: {}) + response = instance_double(Faraday::Response, body: {}) + connection = instance_double(RubyLLM::Connection) + + allow(connection).to receive(:post) + .with('chat/completions', { prompt: 'hello' }) + .and_yield(request) + .and_return(response) + + expect do + provider.send(:sync_response, connection, { prompt: 'hello' }) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end describe '#parse_error' do diff --git a/spec/ruby_llm/providers/anthropic/chat_spec.rb b/spec/ruby_llm/providers/anthropic/chat_spec.rb index c6b58c3a9..1312ea39c 100644 --- a/spec/ruby_llm/providers/anthropic/chat_spec.rb +++ b/spec/ruby_llm/providers/anthropic/chat_spec.rb @@ -134,12 +134,14 @@ end describe '.parse_completion_response' do - it 'raises RubyLLM::Error for nil completion bodies' do - response = instance_double(Faraday::Response, body: nil) - - expect do - described_class.parse_completion_response(response) - end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + it 'raises RubyLLM::Error for nil or empty completion bodies' do + [nil, {}].each do |body| + response = instance_double(Faraday::Response, body: body) + + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end it 'captures cache usage metrics on the message' do diff --git a/spec/ruby_llm/providers/bedrock/chat_spec.rb b/spec/ruby_llm/providers/bedrock/chat_spec.rb index 7e58f388f..5528e7028 100644 --- a/spec/ruby_llm/providers/bedrock/chat_spec.rb +++ b/spec/ruby_llm/providers/bedrock/chat_spec.rb @@ -4,12 +4,14 @@ RSpec.describe RubyLLM::Providers::Bedrock::Chat do describe '.parse_completion_response' do - it 'raises RubyLLM::Error for nil completion bodies' do - response = instance_double(Faraday::Response, body: nil) + it 'raises RubyLLM::Error for nil or empty completion bodies' do + [nil, {}].each do |body| + response = instance_double(Faraday::Response, body: body) - expect do - described_class.parse_completion_response(response) - end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end end diff --git a/spec/ruby_llm/providers/gemini/chat_spec.rb b/spec/ruby_llm/providers/gemini/chat_spec.rb index e43a5acaf..e9fdb9740 100644 --- a/spec/ruby_llm/providers/gemini/chat_spec.rb +++ b/spec/ruby_llm/providers/gemini/chat_spec.rb @@ -525,17 +525,19 @@ end describe '#parse_completion_response' do - it 'raises RubyLLM::Error for nil completion bodies' do - response = Struct.new(:body, :env).new( - nil, - Struct.new(:url).new(Struct.new(:path).new('/v1/models/gemini-2.5-flash:generateContent')) - ) - + it 'raises RubyLLM::Error for nil or empty completion bodies' do provider = RubyLLM::Providers::Gemini.new(RubyLLM.config) - expect do - provider.send(:parse_completion_response, response) - end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + [nil, {}].each do |body| + response = Struct.new(:body, :env).new( + body, + Struct.new(:url).new(Struct.new(:path).new('/v1/models/gemini-2.5-flash:generateContent')) + ) + + expect do + provider.send(:parse_completion_response, response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end it 'keeps thought-only parts out of assistant content' do diff --git a/spec/ruby_llm/providers/open_ai/chat_spec.rb b/spec/ruby_llm/providers/open_ai/chat_spec.rb index 6e2a3f86e..9b7a88bfa 100644 --- a/spec/ruby_llm/providers/open_ai/chat_spec.rb +++ b/spec/ruby_llm/providers/open_ai/chat_spec.rb @@ -4,12 +4,14 @@ RSpec.describe RubyLLM::Providers::OpenAI::Chat do describe '.parse_completion_response' do - it 'raises RubyLLM::Error for nil completion bodies' do - response = instance_double(Faraday::Response, body: nil) + it 'raises RubyLLM::Error for nil or empty completion bodies' do + [nil, {}].each do |body| + response = instance_double(Faraday::Response, body: body) - expect do - described_class.parse_completion_response(response) - end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end it 'captures cached token information when present' do diff --git a/spec/ruby_llm/providers/open_router/chat_spec.rb b/spec/ruby_llm/providers/open_router/chat_spec.rb index 93c1f023c..a205d3932 100644 --- a/spec/ruby_llm/providers/open_router/chat_spec.rb +++ b/spec/ruby_llm/providers/open_router/chat_spec.rb @@ -4,12 +4,14 @@ RSpec.describe RubyLLM::Providers::OpenRouter::Chat do describe '.parse_completion_response' do - it 'raises RubyLLM::Error for nil completion bodies' do - response = instance_double(Faraday::Response, body: nil) + it 'raises RubyLLM::Error for nil or empty completion bodies' do + [nil, {}].each do |body| + response = instance_double(Faraday::Response, body: body) - expect do - described_class.parse_completion_response(response) - end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + expect do + described_class.parse_completion_response(response) + end.to raise_error(RubyLLM::Error, 'Provider returned an empty response body') + end end end