active_harness 0.3.7 → 0.3.8
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/active_harness/http/client.rb +58 -11
- data/lib/active_harness/http/streaming_client.rb +33 -1
- data/lib/active_harness.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: 5c6df0d88c07312b2546e83f5e69c58d5b3628e4ff31b0885e10a72d032a5a5e
|
|
4
|
+
data.tar.gz: f2427084db1992f1c0cdcd44c7fe6518cfaa0d348f1d3720ce9fcf78e40783c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39575f2575e8dd5d1dc90a512f1d550512d90b57d37d182c10055901c5f15f6bb4a7d425c5a362acacf0be248c237707d0e1362d71d912b2eb449340c20a9ef9
|
|
7
|
+
data.tar.gz: 41f8adbbb47d9a0e2a17505f470b8a77ffac7f275eb530ee87f3a5f60c3eaa707a6f2610f16f63fc1a73872d75cb02cfad81a0251dd831b685d02fce0f5bad85
|
|
@@ -4,35 +4,82 @@ module ActiveHarness
|
|
|
4
4
|
module Http
|
|
5
5
|
# Thin Net::HTTP wrapper — no external dependencies.
|
|
6
6
|
class Client
|
|
7
|
-
|
|
7
|
+
METHODS = {
|
|
8
|
+
get: Net::HTTP::Get,
|
|
9
|
+
post: Net::HTTP::Post,
|
|
10
|
+
put: Net::HTTP::Put,
|
|
11
|
+
patch: Net::HTTP::Patch,
|
|
12
|
+
delete: Net::HTTP::Delete
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# @param method [Symbol] :get, :post, :put, :patch or :delete
|
|
16
|
+
# @param url [URI] https (default) or http — TLS follows the URL scheme
|
|
8
17
|
# @param headers [Hash{String => String}]
|
|
9
|
-
# @param body [String]
|
|
18
|
+
# @param body [String, nil] request body (usually JSON-serialized); omit for GET/DELETE
|
|
10
19
|
# @param timeout [Integer] seconds (open + read)
|
|
11
20
|
# @return [String] raw response body; also responds to #http_status
|
|
12
21
|
# (Integer) so providers can classify errors by
|
|
13
22
|
# status without changing the return type
|
|
23
|
+
def request(method, url, headers: {}, body: nil, timeout: 30)
|
|
24
|
+
klass = METHODS[method.to_sym] or raise ArgumentError, "Unsupported HTTP method: #{method.inspect}"
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
res = build_http(url, timeout).request(build_request(klass, url, headers, body))
|
|
28
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
29
|
+
raise Errors::TimeoutError, "Request to #{url.host} timed out"
|
|
30
|
+
rescue => e
|
|
31
|
+
raise Errors::ProviderUnavailableError, "#{url.host} unreachable: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
with_http_status(res)
|
|
35
|
+
end
|
|
36
|
+
|
|
14
37
|
def post(url, headers:, body:, timeout: 30)
|
|
38
|
+
request(:post, url, headers: headers, body: body, timeout: timeout)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get(url, headers: {}, timeout: 30)
|
|
42
|
+
request(:get, url, headers: headers, timeout: timeout)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def put(url, headers:, body:, timeout: 30)
|
|
46
|
+
request(:put, url, headers: headers, body: body, timeout: timeout)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def patch(url, headers:, body:, timeout: 30)
|
|
50
|
+
request(:patch, url, headers: headers, body: body, timeout: timeout)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete(url, headers: {}, timeout: 30)
|
|
54
|
+
request(:delete, url, headers: headers, timeout: timeout)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def build_http(url, timeout)
|
|
15
60
|
http = Net::HTTP.new(url.host, url.port)
|
|
16
|
-
http.use_ssl =
|
|
61
|
+
http.use_ssl = url.scheme == "https"
|
|
17
62
|
http.open_timeout = timeout
|
|
18
63
|
http.read_timeout = timeout
|
|
64
|
+
http
|
|
65
|
+
end
|
|
19
66
|
|
|
20
|
-
|
|
67
|
+
def build_request(klass, url, headers, body)
|
|
68
|
+
req = klass.new(url)
|
|
21
69
|
headers.each { |k, v| req[k] = v }
|
|
22
|
-
req.body = body
|
|
70
|
+
req.body = body if body
|
|
71
|
+
req
|
|
72
|
+
end
|
|
23
73
|
|
|
24
|
-
|
|
74
|
+
# Returns the response body with an #http_status accessor attached.
|
|
75
|
+
def with_http_status(res)
|
|
25
76
|
body = res.body
|
|
26
77
|
return body if body.nil?
|
|
27
78
|
|
|
28
|
-
body
|
|
79
|
+
body = body.dup
|
|
29
80
|
status = res.code.to_i
|
|
30
81
|
body.define_singleton_method(:http_status) { status }
|
|
31
82
|
body
|
|
32
|
-
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
33
|
-
raise Errors::TimeoutError, "Request to #{url.host} timed out"
|
|
34
|
-
rescue => e
|
|
35
|
-
raise Errors::ProviderUnavailableError, "#{url.host} unreachable: #{e.message}"
|
|
36
83
|
end
|
|
37
84
|
end
|
|
38
85
|
end
|
|
@@ -18,7 +18,7 @@ module ActiveHarness
|
|
|
18
18
|
# @return [Hash] { content: String, usage: Hash|nil }
|
|
19
19
|
def post(url, headers:, body:, timeout: 60, on_token:, parse_chunk: nil)
|
|
20
20
|
http = Net::HTTP.new(url.host, url.port)
|
|
21
|
-
http.use_ssl =
|
|
21
|
+
http.use_ssl = url.scheme == "https"
|
|
22
22
|
http.open_timeout = timeout
|
|
23
23
|
http.read_timeout = timeout
|
|
24
24
|
|
|
@@ -31,6 +31,10 @@ module ActiveHarness
|
|
|
31
31
|
usage = {}
|
|
32
32
|
|
|
33
33
|
http.request(req) do |response|
|
|
34
|
+
# Non-2xx: the body is a plain JSON error, not SSE — read it whole and
|
|
35
|
+
# raise instead of silently returning an empty stream.
|
|
36
|
+
raise_for_status!(response.code.to_i, response.read_body) unless response.is_a?(Net::HTTPSuccess)
|
|
37
|
+
|
|
34
38
|
response.read_body do |chunk|
|
|
35
39
|
buffer += chunk
|
|
36
40
|
while (line_end = buffer.index("\n"))
|
|
@@ -55,12 +59,40 @@ module ActiveHarness
|
|
|
55
59
|
{ content: content, usage: usage.empty? ? nil : usage }
|
|
56
60
|
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
57
61
|
raise Errors::TimeoutError, "Request to #{url.host} timed out"
|
|
62
|
+
rescue Errors::ProviderError
|
|
63
|
+
raise
|
|
58
64
|
rescue => e
|
|
59
65
|
raise Errors::ProviderUnavailableError, "#{url.host} unreachable: #{e.message}"
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
private
|
|
63
69
|
|
|
70
|
+
# Maps an HTTP error status + body to the same error classes the
|
|
71
|
+
# non-streaming providers raise. The message is pulled from the common
|
|
72
|
+
# error shapes ({error:{message}}, {message}, {detail}), else the raw body.
|
|
73
|
+
def raise_for_status!(status, body)
|
|
74
|
+
msg = error_message(body)
|
|
75
|
+
code = status.to_s
|
|
76
|
+
|
|
77
|
+
case status
|
|
78
|
+
when 401, 403 then raise Errors::InvalidApiKeyError.new(msg, error_code: code)
|
|
79
|
+
when 429 then raise Errors::RateLimitError.new(msg, error_code: code)
|
|
80
|
+
when 503 then raise Errors::ProviderUnavailableError.new(msg, error_code: code)
|
|
81
|
+
when 500..599 then raise Errors::ServerError.new(msg, error_code: code)
|
|
82
|
+
else raise Errors::InvalidRequestError.new(msg, error_code: code)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def error_message(body)
|
|
87
|
+
data = JSON.parse(body.to_s)
|
|
88
|
+
msg = data.dig("error", "message") if data["error"].is_a?(Hash)
|
|
89
|
+
msg ||= data["error"] if data["error"].is_a?(String)
|
|
90
|
+
msg ||= data["message"] || data["detail"]
|
|
91
|
+
(msg.is_a?(String) ? msg : data.to_json)
|
|
92
|
+
rescue JSON::ParserError, NoMethodError, TypeError
|
|
93
|
+
body.to_s.strip.empty? ? "HTTP error with empty body" : body.to_s.strip[0, 500]
|
|
94
|
+
end
|
|
95
|
+
|
|
64
96
|
def default_chunk(parsed)
|
|
65
97
|
token = parsed.dig("choices", 0, "delta", "content")
|
|
66
98
|
raw_u = parsed["usage"]
|
data/lib/active_harness.rb
CHANGED