simple_inference 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccaaf770804e4ecb6f308d3e0a864f295ab30e9129a5ec7a8322d9a0dd07c89b
4
- data.tar.gz: 67f3526c92ca12c38c01d00b38e8d624655d1ac101b1314475d6a2005dc2badf
3
+ metadata.gz: 50a8ea07e0e30771b6d42f2bacf12f12b379f1151bb44947bb5febe3cac70cf9
4
+ data.tar.gz: c655b141ea39e518c5cdcc30cc28bac249ef23acfae8c05f4cf852e199a98a80
5
5
  SHA512:
6
- metadata.gz: c27972ed32eb27a5923740e5902addae2ac617c124e0e56c41905eb21c119843072e5047487dd2b1e29665e123cb22692da173bd1e48717152f05b5b7c7322e5
7
- data.tar.gz: 6219386deb601c540c27a9d309ca2356f6dc4b6604068039da3d9d527ad2a86f3ba882b9095be811dd7e8d3a0910ac9425132f5bf8333c4aa2c8f75f4eda15e5
6
+ metadata.gz: '0483fda13365abb75bde99643ada2fc95017e4883e07d823d8b86912e9553764befe7c6a92222aeca3ee05d20e8f6b1e1a23a4a74decf45383bc4cc9c055d357'
7
+ data.tar.gz: 4d115195078c198b2c2c1b4bc1307a05e337884449b19393488374cd1710efaf6186e68c7060417141e5477933ea3fafc5223896adc06c1d1063263437254d56
data/README.md CHANGED
@@ -224,6 +224,8 @@ The default HTTP adapter uses Ruby's `Net::HTTP` and is safe to use under Puma's
224
224
  - Per-client configuration only
225
225
  - Blocking IO that integrates with Ruby 3 Fiber scheduler
226
226
 
227
+ If you don't pass an adapter, `SimpleInference::Client` uses `SimpleInference::HTTPAdapters::Default` (Net::HTTP).
228
+
227
229
  For Falcon / async environments, you can keep the default adapter, or use the optional HTTPX adapter (requires the `httpx` gem):
228
230
 
229
231
  ```ruby
@@ -233,7 +235,7 @@ gem "httpx" # optional, only required when using the HTTPX adapter
233
235
  You can then use the optional HTTPX adapter shipped with this gem:
234
236
 
235
237
  ```ruby
236
- adapter = SimpleInference::HTTPAdapter::HTTPX.new(timeout: 30.0)
238
+ adapter = SimpleInference::HTTPAdapters::HTTPX.new(timeout: 30.0)
237
239
 
238
240
  SIMPLE_INFERENCE_CLIENT =
239
241
  SimpleInference::Client.new(
@@ -12,7 +12,12 @@ module SimpleInference
12
12
 
13
13
  def initialize(options = {})
14
14
  @config = Config.new(options || {})
15
- @adapter = @config.adapter || HTTPAdapter::Default.new
15
+ @adapter = @config.adapter || HTTPAdapters::Default.new
16
+
17
+ unless @adapter.is_a?(HTTPAdapter)
18
+ raise Errors::ConfigurationError,
19
+ "adapter must be an instance of SimpleInference::HTTPAdapter (got #{@adapter.class})"
20
+ end
16
21
  end
17
22
 
18
23
  # POST /v1/chat/completions
@@ -168,32 +173,15 @@ module SimpleInference
168
173
  def handle_stream_response(request_env, raise_on_http_error:, &on_event)
169
174
  sse_buffer = +""
170
175
  sse_done = false
171
- used_streaming_adapter = false
176
+ streamed = false
172
177
 
173
178
  raw_response =
174
- if @adapter.respond_to?(:call_stream)
175
- used_streaming_adapter = true
176
- @adapter.call_stream(request_env) do |chunk|
177
- next if sse_done
178
-
179
- sse_buffer << chunk.to_s
180
- extract_sse_blocks!(sse_buffer).each do |block|
181
- data = sse_data_from_block(block)
182
- next if data.nil?
183
-
184
- payload = data.strip
185
- next if payload.empty?
186
- if payload == "[DONE]"
187
- sse_done = true
188
- sse_buffer.clear
189
- break
190
- end
191
-
192
- on_event&.call(parse_json_event(payload))
193
- end
194
- end
195
- else
196
- @adapter.call(request_env)
179
+ @adapter.call_stream(request_env) do |chunk|
180
+ streamed = true
181
+ next if sse_done
182
+
183
+ sse_buffer << chunk.to_s
184
+ sse_done = consume_sse_buffer!(sse_buffer, &on_event) || sse_done
197
185
  end
198
186
 
199
187
  status = raw_response[:status]
@@ -206,18 +194,9 @@ module SimpleInference
206
194
  # Streaming case.
207
195
  if status >= 200 && status < 300 && content_type.include?("text/event-stream")
208
196
  # If we couldn't stream incrementally, best-effort parse the full SSE body.
209
- unless used_streaming_adapter
197
+ unless streamed
210
198
  buffer = body_str.dup
211
- extract_sse_blocks!(buffer).each do |block|
212
- data = sse_data_from_block(block)
213
- next if data.nil?
214
-
215
- payload = data.strip
216
- next if payload.empty?
217
- break if payload == "[DONE]"
218
-
219
- on_event&.call(parse_json_event(payload))
220
- end
199
+ consume_sse_buffer!(buffer, &on_event)
221
200
  end
222
201
 
223
202
  return {
@@ -231,39 +210,14 @@ module SimpleInference
231
210
  should_parse_json = content_type.include?("json")
232
211
  parsed_body = should_parse_json ? parse_json(body_str) : body_str
233
212
 
234
- raise_on =
235
- if raise_on_http_error.nil?
236
- config.raise_on_error
237
- else
238
- !!raise_on_http_error
239
- end
240
-
241
- if raise_on && (status < 200 || status >= 300)
242
- # Do not raise for the known "streaming unsupported" case; the caller will
243
- # perform a non-streaming retry fallback.
244
- unless streaming_unsupported_error?(status, parsed_body)
245
- message = "HTTP #{status}"
246
- begin
247
- error_body = JSON.parse(body_str)
248
- error_field = error_body["error"]
249
- message =
250
- if error_field.is_a?(Hash)
251
- error_field["message"] || error_body["message"] || message
252
- else
253
- error_field || error_body["message"] || message
254
- end
255
- rescue JSON::ParserError
256
- # fall back to generic message
257
- end
258
-
259
- raise Errors::HTTPError.new(
260
- message,
261
- status: status,
262
- headers: headers,
263
- body: body_str
264
- )
265
- end
266
- end
213
+ maybe_raise_http_error(
214
+ status: status,
215
+ headers: headers,
216
+ body_str: body_str,
217
+ raise_on_http_error: raise_on_http_error,
218
+ ignore_streaming_unsupported: true,
219
+ parsed_body: parsed_body
220
+ )
267
221
 
268
222
  {
269
223
  status: status,
@@ -294,6 +248,27 @@ module SimpleInference
294
248
  blocks
295
249
  end
296
250
 
251
+ def consume_sse_buffer!(buffer, &on_event)
252
+ done = false
253
+
254
+ extract_sse_blocks!(buffer).each do |block|
255
+ data = sse_data_from_block(block)
256
+ next if data.nil?
257
+
258
+ payload = data.strip
259
+ next if payload.empty?
260
+ if payload == "[DONE]"
261
+ done = true
262
+ buffer.clear
263
+ break
264
+ end
265
+
266
+ on_event&.call(parse_json_event(payload))
267
+ end
268
+
269
+ done
270
+ end
271
+
297
272
  def sse_data_from_block(block)
298
273
  return nil if block.nil? || block.empty?
299
274
 
@@ -600,37 +575,12 @@ module SimpleInference
600
575
  headers = (response[:headers] || {}).transform_keys { |k| k.to_s.downcase }
601
576
  body = response[:body].to_s
602
577
 
603
- # Decide whether to raise on HTTP errors
604
- raise_on =
605
- if raise_on_http_error.nil?
606
- config.raise_on_error
607
- else
608
- !!raise_on_http_error
609
- end
610
-
611
- if raise_on && (status < 200 || status >= 300)
612
- message = "HTTP #{status}"
613
-
614
- begin
615
- error_body = JSON.parse(body)
616
- error_field = error_body["error"]
617
- message =
618
- if error_field.is_a?(Hash)
619
- error_field["message"] || error_body["message"] || message
620
- else
621
- error_field || error_body["message"] || message
622
- end
623
- rescue JSON::ParserError
624
- # fall back to generic message
625
- end
626
-
627
- raise Errors::HTTPError.new(
628
- message,
629
- status: status,
630
- headers: headers,
631
- body: body
632
- )
633
- end
578
+ maybe_raise_http_error(
579
+ status: status,
580
+ headers: headers,
581
+ body_str: body,
582
+ raise_on_http_error: raise_on_http_error
583
+ )
634
584
 
635
585
  should_parse_json =
636
586
  if expect_json.nil?
@@ -665,5 +615,56 @@ module SimpleInference
665
615
  rescue JSON::ParserError => e
666
616
  raise Errors::DecodeError, "Failed to parse JSON response: #{e.message}"
667
617
  end
618
+
619
+ def raise_on_http_error?(raise_on_http_error)
620
+ raise_on_http_error.nil? ? config.raise_on_error : !!raise_on_http_error
621
+ end
622
+
623
+ def http_error_message(status, body_str, parsed_body: nil)
624
+ message = "HTTP #{status}"
625
+
626
+ error_body =
627
+ if parsed_body.is_a?(Hash)
628
+ parsed_body
629
+ else
630
+ begin
631
+ JSON.parse(body_str)
632
+ rescue JSON::ParserError
633
+ nil
634
+ end
635
+ end
636
+
637
+ return message unless error_body.is_a?(Hash)
638
+
639
+ error_field = error_body["error"]
640
+ if error_field.is_a?(Hash)
641
+ error_field["message"] || error_body["message"] || message
642
+ else
643
+ error_field || error_body["message"] || message
644
+ end
645
+ end
646
+
647
+ def maybe_raise_http_error(
648
+ status:,
649
+ headers:,
650
+ body_str:,
651
+ raise_on_http_error:,
652
+ ignore_streaming_unsupported: false,
653
+ parsed_body: nil
654
+ )
655
+ return unless raise_on_http_error?(raise_on_http_error)
656
+ return unless status < 200 || status >= 300
657
+
658
+ # Do not raise for the known "streaming unsupported" case; the caller will
659
+ # perform a non-streaming retry fallback.
660
+ return if ignore_streaming_unsupported && streaming_unsupported_error?(status, parsed_body)
661
+
662
+ raise Errors::HTTPError.new(
663
+ http_error_message(status, body_str, parsed_body: parsed_body),
664
+ status: status,
665
+ headers: headers,
666
+ body: body_str
667
+ )
668
+ end
668
669
  end
669
670
  end
@@ -1,151 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "uri"
5
-
6
3
  module SimpleInference
7
- module HTTPAdapter
8
- # Optional adapters are lazily loaded so the SDK has no hard runtime deps.
9
- autoload :HTTPX, "simple_inference/http_adapter/httpx"
10
-
11
- # Default synchronous HTTP adapter built on Net::HTTP.
12
- # It is compatible with Ruby 3 Fiber scheduler and keeps the interface
13
- # minimal so it can be swapped out for custom adapters (HTTPX, async-http, etc.).
14
- class Default
15
- def call(request)
16
- uri = URI.parse(request.fetch(:url))
17
-
18
- http = Net::HTTP.new(uri.host, uri.port)
19
- http.use_ssl = uri.scheme == "https"
20
-
21
- timeout = request[:timeout]
22
- open_timeout = request[:open_timeout] || timeout
23
- read_timeout = request[:read_timeout] || timeout
24
-
25
- http.open_timeout = open_timeout if open_timeout
26
- http.read_timeout = read_timeout if read_timeout
27
-
28
- klass = http_class_for(request[:method])
29
- req = klass.new(uri.request_uri)
30
-
31
- headers = request[:headers] || {}
32
- headers.each do |key, value|
33
- req[key.to_s] = value
34
- end
35
-
36
- body = request[:body]
37
- if body
38
- if body.is_a?(String)
39
- req.body = body
40
- elsif body.respond_to?(:read)
41
- req.body_stream = body
42
- if body.respond_to?(:size) && (size = body.size)
43
- req.content_length = size
44
- req.delete("Transfer-Encoding")
45
- else
46
- req["Transfer-Encoding"] = "chunked"
47
- end
48
- else
49
- req.body = body.to_s
50
- end
51
- end
52
-
53
- response = http.request(req)
54
-
55
- {
56
- status: Integer(response.code),
57
- headers: response.each_header.to_h,
58
- body: response.body.to_s,
59
- }
60
- end
61
-
62
- # Streaming-capable request helper.
63
- #
64
- # When the response is `text/event-stream` (and 2xx), it yields raw body chunks
65
- # as they arrive via the given block, and returns a response hash with `body: nil`.
66
- #
67
- # For non-streaming responses, it behaves like `#call` and returns the full body.
68
- def call_stream(request)
69
- return call(request) unless block_given?
70
-
71
- uri = URI.parse(request.fetch(:url))
72
-
73
- http = Net::HTTP.new(uri.host, uri.port)
74
- http.use_ssl = uri.scheme == "https"
75
-
76
- timeout = request[:timeout]
77
- open_timeout = request[:open_timeout] || timeout
78
- read_timeout = request[:read_timeout] || timeout
79
-
80
- http.open_timeout = open_timeout if open_timeout
81
- http.read_timeout = read_timeout if read_timeout
82
-
83
- klass = http_class_for(request[:method])
84
- req = klass.new(uri.request_uri)
85
-
86
- headers = request[:headers] || {}
87
- headers.each do |key, value|
88
- req[key.to_s] = value
89
- end
90
-
91
- body = request[:body]
92
- if body
93
- if body.is_a?(String)
94
- req.body = body
95
- elsif body.respond_to?(:read)
96
- req.body_stream = body
97
- if body.respond_to?(:size) && (size = body.size)
98
- req.content_length = size
99
- req.delete("Transfer-Encoding")
100
- else
101
- req["Transfer-Encoding"] = "chunked"
102
- end
103
- else
104
- req.body = body.to_s
105
- end
106
- end
107
-
108
- status = nil
109
- response_headers = {}
110
- response_body = +""
111
-
112
- http.request(req) do |response|
113
- status = Integer(response.code)
114
- response_headers = response.each_header.to_h
115
-
116
- headers_lc = response_headers.transform_keys { |k| k.to_s.downcase }
117
- content_type = headers_lc["content-type"]
118
-
119
- if status >= 200 && status < 300 && content_type&.include?("text/event-stream")
120
- response.read_body do |chunk|
121
- yield chunk
122
- end
123
- response_body = nil
124
- else
125
- response_body = response.body.to_s
126
- end
127
- end
4
+ # Base class for HTTP adapters.
5
+ #
6
+ # Concrete adapters must implement `#call` and may override `#call_stream`
7
+ # for incremental streaming.
8
+ class HTTPAdapter
9
+ def call(_request)
10
+ raise NotImplementedError, "#{self.class} must implement #call"
11
+ end
128
12
 
129
- {
130
- status: Integer(status),
131
- headers: response_headers,
132
- body: response_body,
133
- }
13
+ # Streaming-capable request helper.
14
+ #
15
+ # When the response is `text/event-stream` (and 2xx), it yields raw body chunks
16
+ # as they arrive via the given block, and returns a response hash with `body: nil`.
17
+ #
18
+ # For non-streaming responses, it behaves like `#call` and returns the full body.
19
+ def call_stream(request)
20
+ return call(request) unless block_given?
21
+
22
+ response = call(request)
23
+
24
+ status = response[:status].to_i
25
+ headers = response[:headers] || {}
26
+ content_type =
27
+ headers.each_with_object({}) do |(k, v), out|
28
+ out[k.to_s.downcase] = v
29
+ end["content-type"].to_s
30
+
31
+ if status >= 200 && status < 300 && content_type.include?("text/event-stream")
32
+ yield response[:body].to_s
33
+ return { status: status, headers: headers, body: nil }
134
34
  end
135
35
 
136
- private
137
-
138
- def http_class_for(method)
139
- case method.to_s.upcase
140
- when "GET" then Net::HTTP::Get
141
- when "POST" then Net::HTTP::Post
142
- when "PUT" then Net::HTTP::Put
143
- when "PATCH" then Net::HTTP::Patch
144
- when "DELETE" then Net::HTTP::Delete
145
- else
146
- raise ArgumentError, "Unsupported HTTP method: #{method.inspect}"
147
- end
148
- end
36
+ response
149
37
  end
150
38
  end
39
+
40
+ module HTTPAdapters
41
+ autoload :Default, "simple_inference/http_adapters/default"
42
+ autoload :HTTPX, "simple_inference/http_adapters/httpx"
43
+ end
151
44
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module SimpleInference
7
+ module HTTPAdapters
8
+ # Default synchronous HTTP adapter built on Net::HTTP.
9
+ # It is compatible with Ruby 3 Fiber scheduler and keeps the interface
10
+ # minimal so it can be swapped out for custom adapters (HTTPX, async-http, etc.).
11
+ class Default < HTTPAdapter
12
+ def call(request)
13
+ http, req = build_request(request)
14
+
15
+ response = http.request(req)
16
+
17
+ {
18
+ status: Integer(response.code),
19
+ headers: response.each_header.to_h,
20
+ body: response.body.to_s,
21
+ }
22
+ end
23
+
24
+ # Streaming-capable request helper.
25
+ #
26
+ # When the response is `text/event-stream` (and 2xx), it yields raw body chunks
27
+ # as they arrive via the given block, and returns a response hash with `body: nil`.
28
+ #
29
+ # For non-streaming responses, it behaves like `#call` and returns the full body.
30
+ def call_stream(request)
31
+ return call(request) unless block_given?
32
+
33
+ http, req = build_request(request)
34
+
35
+ status = nil
36
+ response_headers = {}
37
+ response_body = +""
38
+
39
+ http.request(req) do |response|
40
+ status = Integer(response.code)
41
+ response_headers = response.each_header.to_h
42
+
43
+ headers_lc = response_headers.transform_keys { |k| k.to_s.downcase }
44
+ content_type = headers_lc["content-type"]
45
+
46
+ if status >= 200 && status < 300 && content_type&.include?("text/event-stream")
47
+ response.read_body do |chunk|
48
+ yield chunk
49
+ end
50
+ response_body = nil
51
+ else
52
+ response_body = response.body.to_s
53
+ end
54
+ end
55
+
56
+ {
57
+ status: Integer(status),
58
+ headers: response_headers,
59
+ body: response_body,
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def build_request(request)
66
+ uri = URI.parse(request.fetch(:url))
67
+
68
+ http = Net::HTTP.new(uri.host, uri.port)
69
+ http.use_ssl = uri.scheme == "https"
70
+
71
+ apply_timeouts(http, request)
72
+
73
+ req = http_class_for(request[:method]).new(uri.request_uri)
74
+ apply_headers(req, request[:headers] || {})
75
+ apply_body(req, request[:body])
76
+
77
+ [http, req]
78
+ end
79
+
80
+ def apply_timeouts(http, request)
81
+ timeout = request[:timeout]
82
+ open_timeout = request[:open_timeout] || timeout
83
+ read_timeout = request[:read_timeout] || timeout
84
+
85
+ http.open_timeout = open_timeout if open_timeout
86
+ http.read_timeout = read_timeout if read_timeout
87
+ end
88
+
89
+ def apply_headers(req, headers)
90
+ headers.each do |key, value|
91
+ req[key.to_s] = value
92
+ end
93
+ end
94
+
95
+ def apply_body(req, body)
96
+ return unless body
97
+
98
+ if body.is_a?(String)
99
+ req.body = body
100
+ elsif body.respond_to?(:read)
101
+ req.body_stream = body
102
+ if body.respond_to?(:size) && (size = body.size)
103
+ req.content_length = size
104
+ req.delete("Transfer-Encoding")
105
+ else
106
+ req["Transfer-Encoding"] = "chunked"
107
+ end
108
+ else
109
+ req.body = body.to_s
110
+ end
111
+ end
112
+
113
+ def http_class_for(method)
114
+ case method.to_s.upcase
115
+ when "GET" then Net::HTTP::Get
116
+ when "POST" then Net::HTTP::Post
117
+ when "PUT" then Net::HTTP::Put
118
+ when "PATCH" then Net::HTTP::Patch
119
+ when "DELETE" then Net::HTTP::Delete
120
+ else
121
+ raise ArgumentError, "Unsupported HTTP method: #{method.inspect}"
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -4,17 +4,33 @@ begin
4
4
  require "httpx"
5
5
  rescue LoadError => e
6
6
  raise LoadError,
7
- "httpx gem is required for SimpleInference::HTTPAdapter::HTTPX (add `gem \"httpx\"`)",
7
+ "httpx gem is required for SimpleInference::HTTPAdapters::HTTPX (add `gem \"httpx\"`)",
8
8
  cause: e
9
9
  end
10
10
 
11
+ # HTTPX's stream plugin expects all request objects to respond to `#stream`,
12
+ # however some internal requests (e.g. proxy CONNECT) don't include the plugin's
13
+ # RequestMethods. Add a harmless accessor at the base class level.
14
+ unless ::HTTPX::Request.method_defined?(:stream) && ::HTTPX::Request.method_defined?(:stream=)
15
+ ::HTTPX::Request.class_eval do
16
+ attr_accessor :stream
17
+ end
18
+ end
19
+
11
20
  module SimpleInference
12
- module HTTPAdapter
21
+ module HTTPAdapters
13
22
  # Fiber-friendly HTTP adapter built on HTTPX.
14
- class HTTPX
23
+ class HTTPX < HTTPAdapter
15
24
  def initialize(timeout: nil, client: ::HTTPX)
16
25
  @timeout = timeout
26
+
27
+ unless client == ::HTTPX || client.is_a?(::HTTPX::Session)
28
+ raise ArgumentError,
29
+ "client must be ::HTTPX or an instance of ::HTTPX::Session (got #{client.class})"
30
+ end
31
+
17
32
  @client = client
33
+ @stream_client = client.plugin(:stream)
18
34
  end
19
35
 
20
36
  def call(request)
@@ -47,14 +63,12 @@ module SimpleInference
47
63
  #
48
64
  # NOTE: Some error response objects do not expose the normal response API
49
65
  # (e.g. no `#headers`), so we must handle them explicitly.
50
- if defined?(::HTTPX::ErrorResponse) && response.is_a?(::HTTPX::ErrorResponse)
51
- err = response.respond_to?(:error) ? response.error : nil
52
- raise Errors::ConnectionError, (err ? err.message : "HTTPX request failed")
66
+ if response.is_a?(::HTTPX::ErrorResponse)
67
+ raise Errors::ConnectionError, (response.error&.message || "HTTPX request failed")
53
68
  end
54
69
 
55
- if response.respond_to?(:status) && response.status.to_i == 0
56
- err = response.respond_to?(:error) ? response.error : nil
57
- raise Errors::ConnectionError, (err ? err.message : "HTTPX request failed")
70
+ if response.status.to_i == 0
71
+ raise Errors::ConnectionError, "HTTPX request failed"
58
72
  end
59
73
 
60
74
  response_headers = normalize_headers(response)
@@ -62,7 +76,7 @@ module SimpleInference
62
76
  {
63
77
  status: response.status.to_i,
64
78
  headers: response_headers,
65
- body: response.respond_to?(:body) ? response.body.to_s : "",
79
+ body: response.body.to_s,
66
80
  }
67
81
  rescue ::HTTPX::TimeoutError => e
68
82
  raise Errors::TimeoutError, e.message
@@ -78,8 +92,6 @@ module SimpleInference
78
92
  headers = request[:headers] || {}
79
93
  body = request[:body]
80
94
 
81
- client = @client
82
-
83
95
  # Mirror the SDK's timeout semantics:
84
96
  # - `:timeout` is the overall request deadline (maps to HTTPX `request_timeout`)
85
97
  # - `:open_timeout` and `:read_timeout` override connect/read deadlines
@@ -96,20 +108,13 @@ module SimpleInference
96
108
  timeout_opts[:connect_timeout] = open_timeout.to_f if open_timeout
97
109
  timeout_opts[:read_timeout] = read_timeout.to_f if read_timeout
98
110
 
99
- unless client.respond_to?(:plugin)
100
- return call_stream_via_full_body(
101
- method: method,
102
- url: url,
103
- headers: headers,
104
- body: body,
105
- timeout: timeout,
106
- open_timeout: open_timeout,
107
- read_timeout: read_timeout
108
- ) { |chunk| yield chunk }
109
- end
110
-
111
- client = client.plugin(:stream)
112
- client = client.with(timeout: timeout_opts) unless timeout_opts.empty?
111
+ stream_client = @stream_client
112
+ client =
113
+ if timeout_opts.empty?
114
+ stream_client.with(timeout: {})
115
+ else
116
+ stream_client.with(timeout: timeout_opts)
117
+ end
113
118
 
114
119
  streaming = nil
115
120
  response_headers = {}
@@ -118,9 +123,17 @@ module SimpleInference
118
123
 
119
124
  stream_response = client.request(method, url, headers: headers, body: body, stream: true)
120
125
 
126
+ if stream_response.is_a?(::HTTPX::ErrorResponse)
127
+ raise Errors::ConnectionError, (stream_response.error&.message || "HTTPX request failed")
128
+ end
129
+
130
+ if stream_response.status.to_i == 0
131
+ raise Errors::ConnectionError, "HTTPX request failed"
132
+ end
133
+
121
134
  begin
122
135
  stream_response.each do |chunk|
123
- status ||= stream_response.respond_to?(:status) ? stream_response.status.to_i : nil
136
+ status ||= stream_response.status.to_i
124
137
  response_headers = normalize_headers(stream_response) if response_headers.empty?
125
138
  streaming = streamable_sse?(status, response_headers) if streaming.nil?
126
139
 
@@ -133,11 +146,11 @@ module SimpleInference
133
146
  rescue ::HTTPX::HTTPError => e
134
147
  # HTTPX's stream plugin raises for non-2xx. Swallow it and let the SDK
135
148
  # raise `Errors::HTTPError` based on status.
136
- status ||= e.response.status.to_i if e.respond_to?(:response) && e.response.respond_to?(:status)
137
- response_headers = normalize_headers(e.response) if response_headers.empty? && e.respond_to?(:response)
149
+ status ||= e.response.status.to_i
150
+ response_headers = normalize_headers(e.response) if response_headers.empty?
138
151
  end
139
152
 
140
- status ||= stream_response.respond_to?(:status) ? stream_response.status.to_i : 0
153
+ status ||= stream_response.status.to_i
141
154
  response_headers = normalize_headers(stream_response) if response_headers.empty?
142
155
 
143
156
  if streamable_sse?(status, response_headers)
@@ -154,8 +167,6 @@ module SimpleInference
154
167
  private
155
168
 
156
169
  def normalize_headers(response)
157
- return {} unless response.respond_to?(:headers)
158
-
159
170
  response.headers.to_h.each_with_object({}) do |(k, v), out|
160
171
  out[k.to_s] = v.is_a?(Array) ? v.join(", ") : v.to_s
161
172
  end
@@ -171,33 +182,6 @@ module SimpleInference
171
182
 
172
183
  content_type.include?("text/event-stream")
173
184
  end
174
-
175
- def call_stream_via_full_body(method:, url:, headers:, body:, timeout:, open_timeout:, read_timeout:)
176
- response =
177
- call(
178
- {
179
- method: method,
180
- url: url,
181
- headers: headers,
182
- body: body,
183
- timeout: timeout,
184
- open_timeout: open_timeout,
185
- read_timeout: read_timeout,
186
- }
187
- )
188
-
189
- content_type =
190
- (response[:headers] || {}).each_with_object({}) do |(k, v), out|
191
- out[k.to_s.downcase] = v
192
- end["content-type"].to_s
193
-
194
- if response[:status].to_i >= 200 && response[:status].to_i < 300 && content_type.include?("text/event-stream")
195
- yield response[:body].to_s
196
- return { status: response[:status].to_i, headers: response[:headers] || {}, body: nil }
197
- end
198
-
199
- response
200
- end
201
185
  end
202
186
  end
203
187
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleInference
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_inference
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - jasl
@@ -25,7 +25,8 @@ files:
25
25
  - lib/simple_inference/config.rb
26
26
  - lib/simple_inference/errors.rb
27
27
  - lib/simple_inference/http_adapter.rb
28
- - lib/simple_inference/http_adapter/httpx.rb
28
+ - lib/simple_inference/http_adapters/default.rb
29
+ - lib/simple_inference/http_adapters/httpx.rb
29
30
  - lib/simple_inference/version.rb
30
31
  - sig/simple_inference.rbs
31
32
  homepage: https://github.com/jasl/simple_inference_server/tree/main/sdks/ruby