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 +4 -4
- data/README.md +3 -1
- data/lib/simple_inference/client.rb +101 -100
- data/lib/simple_inference/http_adapter.rb +35 -142
- data/lib/simple_inference/http_adapters/default.rb +126 -0
- data/lib/simple_inference/{http_adapter → http_adapters}/httpx.rb +43 -59
- data/lib/simple_inference/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50a8ea07e0e30771b6d42f2bacf12f12b379f1151bb44947bb5febe3cac70cf9
|
|
4
|
+
data.tar.gz: c655b141ea39e518c5cdcc30cc28bac249ef23acfae8c05f4cf852e199a98a80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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::
|
|
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 ||
|
|
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
|
-
|
|
176
|
+
streamed = false
|
|
172
177
|
|
|
173
178
|
raw_response =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
197
|
+
unless streamed
|
|
210
198
|
buffer = body_str.dup
|
|
211
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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::
|
|
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
|
|
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
|
|
51
|
-
|
|
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.
|
|
56
|
-
|
|
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.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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
|
|
137
|
-
response_headers = normalize_headers(e.response) if response_headers.empty?
|
|
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.
|
|
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
|
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.
|
|
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/
|
|
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
|