simple_inference 0.1.0 → 0.1.1
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/lib/simple_inference/client.rb +110 -14
- data/lib/simple_inference/http_adapter/httpx.rb +142 -11
- data/lib/simple_inference/http_adapter.rb +30 -2
- data/lib/simple_inference/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccaaf770804e4ecb6f308d3e0a864f295ab30e9129a5ec7a8322d9a0dd07c89b
|
|
4
|
+
data.tar.gz: 67f3526c92ca12c38c01d00b38e8d624655d1ac101b1314475d6a2005dc2badf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c27972ed32eb27a5923740e5902addae2ac617c124e0e56c41905eb21c119843072e5047487dd2b1e29665e123cb22692da173bd1e48717152f05b5b7c7322e5
|
|
7
|
+
data.tar.gz: 6219386deb601c540c27a9d309ca2356f6dc4b6604068039da3d9d527ad2a86f3ba882b9095be811dd7e8d3a0910ac9425132f5bf8333c4aa2c8f75f4eda15e5
|
|
@@ -426,6 +426,7 @@ module SimpleInference
|
|
|
426
426
|
end
|
|
427
427
|
|
|
428
428
|
body, headers = build_multipart_body(io, filename, form_fields)
|
|
429
|
+
headers["Transfer-Encoding"] = "chunked" if body.respond_to?(:size) && body.size.nil?
|
|
429
430
|
|
|
430
431
|
request_env = {
|
|
431
432
|
method: :post,
|
|
@@ -481,26 +482,115 @@ module SimpleInference
|
|
|
481
482
|
"Content-Type" => "multipart/form-data; boundary=#{boundary}",
|
|
482
483
|
}
|
|
483
484
|
|
|
484
|
-
|
|
485
|
-
|
|
485
|
+
parts = []
|
|
486
486
|
fields.each do |name, value|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
487
|
+
parts << "--#{boundary}\r\n".b
|
|
488
|
+
parts << %(Content-Disposition: form-data; name="#{name}"\r\n\r\n).b
|
|
489
|
+
parts << value.to_s.b
|
|
490
|
+
parts << "\r\n".b
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
parts << "--#{boundary}\r\n".b
|
|
494
|
+
parts << %(Content-Disposition: form-data; name="file"; filename="#{filename}"\r\n).b
|
|
495
|
+
parts << "Content-Type: application/octet-stream\r\n\r\n".b
|
|
496
|
+
parts << io
|
|
497
|
+
parts << "\r\n--#{boundary}--\r\n".b
|
|
498
|
+
|
|
499
|
+
[MultipartStream.new(parts), headers]
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
class MultipartStream
|
|
503
|
+
def initialize(parts)
|
|
504
|
+
@parts = parts
|
|
505
|
+
@part_index = 0
|
|
506
|
+
@string_offset = 0
|
|
507
|
+
@size = nil
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def read(length = nil, outbuf = nil)
|
|
511
|
+
return "".b if length.nil? && eof?
|
|
512
|
+
return nil if eof?
|
|
513
|
+
|
|
514
|
+
out = outbuf ? outbuf.replace("".b) : +"".b
|
|
515
|
+
|
|
516
|
+
if length.nil?
|
|
517
|
+
while (chunk = read(16_384))
|
|
518
|
+
out << chunk
|
|
519
|
+
end
|
|
520
|
+
return out
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
while out.bytesize < length && !eof?
|
|
524
|
+
part = @parts.fetch(@part_index)
|
|
525
|
+
|
|
526
|
+
if part.is_a?(String)
|
|
527
|
+
remaining = part.bytesize - @string_offset
|
|
528
|
+
if remaining <= 0
|
|
529
|
+
advance_part!
|
|
530
|
+
next
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
take = [length - out.bytesize, remaining].min
|
|
534
|
+
out << part.byteslice(@string_offset, take)
|
|
535
|
+
@string_offset += take
|
|
536
|
+
|
|
537
|
+
advance_part! if @string_offset >= part.bytesize
|
|
538
|
+
else
|
|
539
|
+
chunk = part.read(length - out.bytesize)
|
|
540
|
+
if chunk.nil? || chunk.empty?
|
|
541
|
+
advance_part!
|
|
542
|
+
next
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
out << chunk
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
return nil if out.empty? && eof?
|
|
550
|
+
|
|
551
|
+
out
|
|
491
552
|
end
|
|
492
553
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
554
|
+
def size
|
|
555
|
+
@size ||= compute_size
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
private
|
|
496
559
|
|
|
497
|
-
|
|
498
|
-
|
|
560
|
+
def eof?
|
|
561
|
+
@part_index >= @parts.length
|
|
499
562
|
end
|
|
500
563
|
|
|
501
|
-
|
|
564
|
+
def advance_part!
|
|
565
|
+
@part_index += 1
|
|
566
|
+
@string_offset = 0
|
|
567
|
+
end
|
|
502
568
|
|
|
503
|
-
|
|
569
|
+
def compute_size
|
|
570
|
+
total = 0
|
|
571
|
+
|
|
572
|
+
@parts.each do |part|
|
|
573
|
+
if part.is_a?(String)
|
|
574
|
+
total += part.bytesize
|
|
575
|
+
next
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
return nil unless part.respond_to?(:size)
|
|
579
|
+
|
|
580
|
+
part_size = part.size
|
|
581
|
+
if part.respond_to?(:pos)
|
|
582
|
+
begin
|
|
583
|
+
part_size -= part.pos
|
|
584
|
+
rescue StandardError
|
|
585
|
+
# ignore pos errors
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
total += part_size
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
total
|
|
593
|
+
end
|
|
504
594
|
end
|
|
505
595
|
|
|
506
596
|
def handle_response(request_env, expect_json:, raise_on_http_error:)
|
|
@@ -523,7 +613,13 @@ module SimpleInference
|
|
|
523
613
|
|
|
524
614
|
begin
|
|
525
615
|
error_body = JSON.parse(body)
|
|
526
|
-
|
|
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
|
|
527
623
|
rescue JSON::ParserError
|
|
528
624
|
# fall back to generic message
|
|
529
625
|
end
|
|
@@ -11,13 +11,10 @@ end
|
|
|
11
11
|
module SimpleInference
|
|
12
12
|
module HTTPAdapter
|
|
13
13
|
# Fiber-friendly HTTP adapter built on HTTPX.
|
|
14
|
-
#
|
|
15
|
-
# NOTE: This adapter intentionally does NOT implement `#call_stream`.
|
|
16
|
-
# Streaming consumers will still work via the SDK's full-body SSE parsing
|
|
17
|
-
# fallback path (see SimpleInference::Client#handle_stream_response).
|
|
18
14
|
class HTTPX
|
|
19
|
-
def initialize(timeout: nil)
|
|
15
|
+
def initialize(timeout: nil, client: ::HTTPX)
|
|
20
16
|
@timeout = timeout
|
|
17
|
+
@client = client
|
|
21
18
|
end
|
|
22
19
|
|
|
23
20
|
def call(request)
|
|
@@ -26,7 +23,7 @@ module SimpleInference
|
|
|
26
23
|
headers = request[:headers] || {}
|
|
27
24
|
body = request[:body]
|
|
28
25
|
|
|
29
|
-
client =
|
|
26
|
+
client = @client
|
|
30
27
|
|
|
31
28
|
# Mirror the SDK's timeout semantics:
|
|
32
29
|
# - `:timeout` is the overall request deadline (maps to HTTPX `request_timeout`)
|
|
@@ -47,26 +44,160 @@ module SimpleInference
|
|
|
47
44
|
response = client.request(method, url, headers: headers, body: body)
|
|
48
45
|
|
|
49
46
|
# HTTPX may return an error response object instead of raising.
|
|
47
|
+
#
|
|
48
|
+
# NOTE: Some error response objects do not expose the normal response API
|
|
49
|
+
# (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")
|
|
53
|
+
end
|
|
54
|
+
|
|
50
55
|
if response.respond_to?(:status) && response.status.to_i == 0
|
|
51
56
|
err = response.respond_to?(:error) ? response.error : nil
|
|
52
57
|
raise Errors::ConnectionError, (err ? err.message : "HTTPX request failed")
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
response_headers =
|
|
56
|
-
response.headers.to_h.each_with_object({}) do |(k, v), out|
|
|
57
|
-
out[k.to_s] = v.is_a?(Array) ? v.join(", ") : v.to_s
|
|
58
|
-
end
|
|
60
|
+
response_headers = normalize_headers(response)
|
|
59
61
|
|
|
60
62
|
{
|
|
61
63
|
status: response.status.to_i,
|
|
62
64
|
headers: response_headers,
|
|
63
|
-
body: response.body.to_s,
|
|
65
|
+
body: response.respond_to?(:body) ? response.body.to_s : "",
|
|
64
66
|
}
|
|
65
67
|
rescue ::HTTPX::TimeoutError => e
|
|
66
68
|
raise Errors::TimeoutError, e.message
|
|
67
69
|
rescue ::HTTPX::Error, IOError, SystemCallError => e
|
|
68
70
|
raise Errors::ConnectionError, e.message
|
|
69
71
|
end
|
|
72
|
+
|
|
73
|
+
def call_stream(request)
|
|
74
|
+
return call(request) unless block_given?
|
|
75
|
+
|
|
76
|
+
method = request.fetch(:method).to_s.downcase.to_sym
|
|
77
|
+
url = request.fetch(:url)
|
|
78
|
+
headers = request[:headers] || {}
|
|
79
|
+
body = request[:body]
|
|
80
|
+
|
|
81
|
+
client = @client
|
|
82
|
+
|
|
83
|
+
# Mirror the SDK's timeout semantics:
|
|
84
|
+
# - `:timeout` is the overall request deadline (maps to HTTPX `request_timeout`)
|
|
85
|
+
# - `:open_timeout` and `:read_timeout` override connect/read deadlines
|
|
86
|
+
timeout = request[:timeout] || @timeout
|
|
87
|
+
open_timeout = request[:open_timeout] || timeout
|
|
88
|
+
read_timeout = request[:read_timeout] || timeout
|
|
89
|
+
|
|
90
|
+
# The HTTPX stream plugin defaults to `read_timeout: Infinity`; align with the
|
|
91
|
+
# non-streaming defaults unless the caller explicitly overrides.
|
|
92
|
+
read_timeout ||= 60
|
|
93
|
+
|
|
94
|
+
timeout_opts = {}
|
|
95
|
+
timeout_opts[:request_timeout] = timeout.to_f if timeout
|
|
96
|
+
timeout_opts[:connect_timeout] = open_timeout.to_f if open_timeout
|
|
97
|
+
timeout_opts[:read_timeout] = read_timeout.to_f if read_timeout
|
|
98
|
+
|
|
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?
|
|
113
|
+
|
|
114
|
+
streaming = nil
|
|
115
|
+
response_headers = {}
|
|
116
|
+
status = nil
|
|
117
|
+
full_body = +"".b
|
|
118
|
+
|
|
119
|
+
stream_response = client.request(method, url, headers: headers, body: body, stream: true)
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
stream_response.each do |chunk|
|
|
123
|
+
status ||= stream_response.respond_to?(:status) ? stream_response.status.to_i : nil
|
|
124
|
+
response_headers = normalize_headers(stream_response) if response_headers.empty?
|
|
125
|
+
streaming = streamable_sse?(status, response_headers) if streaming.nil?
|
|
126
|
+
|
|
127
|
+
if streaming
|
|
128
|
+
yield chunk
|
|
129
|
+
else
|
|
130
|
+
full_body << chunk.to_s
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
rescue ::HTTPX::HTTPError => e
|
|
134
|
+
# HTTPX's stream plugin raises for non-2xx. Swallow it and let the SDK
|
|
135
|
+
# 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)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
status ||= stream_response.respond_to?(:status) ? stream_response.status.to_i : 0
|
|
141
|
+
response_headers = normalize_headers(stream_response) if response_headers.empty?
|
|
142
|
+
|
|
143
|
+
if streamable_sse?(status, response_headers)
|
|
144
|
+
{ status: status, headers: response_headers, body: nil }
|
|
145
|
+
else
|
|
146
|
+
{ status: status, headers: response_headers, body: full_body.to_s }
|
|
147
|
+
end
|
|
148
|
+
rescue ::HTTPX::TimeoutError => e
|
|
149
|
+
raise Errors::TimeoutError, e.message
|
|
150
|
+
rescue ::HTTPX::Error, IOError, SystemCallError => e
|
|
151
|
+
raise Errors::ConnectionError, e.message
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def normalize_headers(response)
|
|
157
|
+
return {} unless response.respond_to?(:headers)
|
|
158
|
+
|
|
159
|
+
response.headers.to_h.each_with_object({}) do |(k, v), out|
|
|
160
|
+
out[k.to_s] = v.is_a?(Array) ? v.join(", ") : v.to_s
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def streamable_sse?(status, headers)
|
|
165
|
+
return false unless status.to_i >= 200 && status.to_i < 300
|
|
166
|
+
|
|
167
|
+
content_type =
|
|
168
|
+
headers.each_with_object({}) do |(k, v), out|
|
|
169
|
+
out[k.to_s.downcase] = v
|
|
170
|
+
end["content-type"].to_s
|
|
171
|
+
|
|
172
|
+
content_type.include?("text/event-stream")
|
|
173
|
+
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
|
|
70
201
|
end
|
|
71
202
|
end
|
|
72
203
|
end
|
|
@@ -34,7 +34,21 @@ module SimpleInference
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
body = request[:body]
|
|
37
|
-
|
|
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
|
|
38
52
|
|
|
39
53
|
response = http.request(req)
|
|
40
54
|
|
|
@@ -75,7 +89,21 @@ module SimpleInference
|
|
|
75
89
|
end
|
|
76
90
|
|
|
77
91
|
body = request[:body]
|
|
78
|
-
|
|
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
|
|
79
107
|
|
|
80
108
|
status = nil
|
|
81
109
|
response_headers = {}
|