llm.rb 11.1.0 → 11.2.0
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/CHANGELOG.md +84 -1
- data/README.md +27 -4
- data/lib/llm/a2a/transport/http.rb +9 -8
- data/lib/llm/a2a.rb +14 -7
- data/lib/llm/agent.rb +6 -3
- data/lib/llm/context.rb +20 -6
- data/lib/llm/function/array.rb +6 -0
- data/lib/llm/function.rb +26 -0
- data/lib/llm/json_adapter.rb +8 -2
- data/lib/llm/mcp/transport/http.rb +7 -5
- data/lib/llm/mcp.rb +6 -7
- data/lib/llm/provider.rb +1 -18
- data/lib/llm/providers/anthropic/files.rb +6 -6
- data/lib/llm/providers/anthropic/models.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +4 -4
- data/lib/llm/providers/bedrock/signature.rb +3 -3
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google/files.rb +5 -5
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/providers/ollama/models.rb +1 -1
- data/lib/llm/providers/ollama.rb +2 -2
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/files.rb +5 -5
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/moderations.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +3 -3
- data/lib/llm/providers/openai/vector_stores.rb +11 -11
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/skill.rb +1 -1
- data/lib/llm/tool.rb +21 -0
- data/lib/llm/transport/curb.rb +246 -0
- data/lib/llm/transport/execution.rb +1 -1
- data/lib/llm/transport/http.rb +9 -4
- data/lib/llm/transport/net_http_adapter.rb +61 -0
- data/lib/llm/transport/persistent_http.rb +10 -5
- data/lib/llm/transport/request.rb +121 -0
- data/lib/llm/transport/response/curb.rb +112 -0
- data/lib/llm/transport/response.rb +1 -0
- data/lib/llm/transport/utils.rb +42 -17
- data/lib/llm/transport.rb +17 -45
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +3 -3
- metadata +8 -4
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Transport::Curb LLM::Transport::Curb} transport is an
|
|
6
|
+
# optional adapter for libcurl via the
|
|
7
|
+
# [curb](https://github.com/taf2/curb) gem.
|
|
8
|
+
#
|
|
9
|
+
# Curb is a C extension around libcurl. It releases the GVL during
|
|
10
|
+
# I/O so other Ruby threads can run while requests are in flight. Its
|
|
11
|
+
# timeout handling is built into libcurl itself — no thread-based
|
|
12
|
+
# timeout library required. It supports HTTP/2, connection reuse, and
|
|
13
|
+
# a wider range of network protocols out of the box.
|
|
14
|
+
#
|
|
15
|
+
# Unlike the built-in Net::HTTP transports, this transport does not
|
|
16
|
+
# require any Ruby standard library HTTP client and can be used on
|
|
17
|
+
# platforms where Net::HTTP is not available or desired.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# LLM.openai(key: ENV["KEY"], transport: :curb)
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class Curb < self
|
|
24
|
+
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
25
|
+
ActiveRequest = Struct.new(:easy, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @param [String] host
|
|
29
|
+
# @param [Integer] port
|
|
30
|
+
# @param [Integer] timeout
|
|
31
|
+
# @param [Boolean] ssl
|
|
32
|
+
# @return [LLM::Transport::Curb]
|
|
33
|
+
def initialize(host:, port:, timeout:, ssl:)
|
|
34
|
+
@host = host
|
|
35
|
+
@port = port
|
|
36
|
+
@timeout = timeout
|
|
37
|
+
@ssl = ssl
|
|
38
|
+
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
39
|
+
@monitor = Monitor.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Returns the current request owner.
|
|
44
|
+
# @return [Object]
|
|
45
|
+
def request_owner
|
|
46
|
+
return Fiber.current unless defined?(::Async)
|
|
47
|
+
Async::Task.current? ? Async::Task.current : Fiber.current
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @return [Array<Class<Exception>>]
|
|
52
|
+
def interrupt_errors
|
|
53
|
+
[*INTERRUPT_ERRORS, *optional_interrupt_errors]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Interrupt an active request, if any.
|
|
58
|
+
#
|
|
59
|
+
# Sets the interrupt flag so the on_body callback can raise
|
|
60
|
+
# LLM::Interrupt on the next chunk.
|
|
61
|
+
#
|
|
62
|
+
# @param [Fiber] owner
|
|
63
|
+
# @return [nil]
|
|
64
|
+
def interrupt!(owner)
|
|
65
|
+
request_for(owner) or return
|
|
66
|
+
lock { (@interrupts ||= {})[owner] = true }
|
|
67
|
+
rescue *interrupt_errors
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Returns whether an execution owner was interrupted.
|
|
73
|
+
# @param [Fiber] owner
|
|
74
|
+
# @return [Boolean, nil]
|
|
75
|
+
def interrupted?(owner)
|
|
76
|
+
lock { @interrupts&.delete(owner) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Performs a request through curb and returns a transport response
|
|
81
|
+
# wrapper so the provider layer can stay transport-agnostic.
|
|
82
|
+
#
|
|
83
|
+
# @param [LLM::Transport::Request] request
|
|
84
|
+
# @param [Fiber] owner
|
|
85
|
+
# @param [LLM::Object, nil] stream
|
|
86
|
+
# @yieldparam [LLM::Transport::Response] response
|
|
87
|
+
# @return [Object]
|
|
88
|
+
def request(request, owner:, stream: nil, &b)
|
|
89
|
+
easy = build_easy(request)
|
|
90
|
+
set_request(ActiveRequest.new(easy:), owner)
|
|
91
|
+
if stream
|
|
92
|
+
perform_streaming(easy, owner, stream)
|
|
93
|
+
elsif b
|
|
94
|
+
res = perform_blocking(easy, owner)
|
|
95
|
+
if LLM::Transport::Response === res
|
|
96
|
+
res.success? ? b.call(res) : res
|
|
97
|
+
else
|
|
98
|
+
res
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
perform_blocking(easy, owner)
|
|
102
|
+
end
|
|
103
|
+
ensure
|
|
104
|
+
clear_request(owner)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def inspect
|
|
110
|
+
"#<#{LLM::Utils.object_id(self)}>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
attr_reader :host, :port, :timeout, :ssl, :base_uri
|
|
116
|
+
|
|
117
|
+
def build_easy(request)
|
|
118
|
+
LLM.require "curb" unless defined?(::Curl)
|
|
119
|
+
easy = ::Curl::Easy.new(request_url(request))
|
|
120
|
+
easy.timeout = timeout
|
|
121
|
+
easy.connect_timeout = timeout
|
|
122
|
+
request.headers.each { |k, v| easy.headers[k] = v }
|
|
123
|
+
easy.follow_location = true
|
|
124
|
+
easy.ssl_verify_peer = false if !ssl
|
|
125
|
+
set_body(easy, request)
|
|
126
|
+
easy
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def request_url(request)
|
|
130
|
+
path = request.path
|
|
131
|
+
return path if path.start_with?("http://", "https://")
|
|
132
|
+
scheme = ssl ? "https" : "http"
|
|
133
|
+
default_port = ssl ? 443 : 80
|
|
134
|
+
authority = port && port.to_i > 0 && port.to_i != default_port \
|
|
135
|
+
? "#{host}:#{port}" : host
|
|
136
|
+
"#{scheme}://#{authority}#{path}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def set_body(easy, request)
|
|
140
|
+
case request.method
|
|
141
|
+
when "POST"
|
|
142
|
+
easy.post_body = request.body if request.body
|
|
143
|
+
when "PUT"
|
|
144
|
+
easy.put_data = request.body if request.body
|
|
145
|
+
when "DELETE"
|
|
146
|
+
easy.delete = true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def perform_blocking(easy, owner)
|
|
151
|
+
check_interrupted(owner)
|
|
152
|
+
easy.on_body { |chunk|
|
|
153
|
+
check_interrupted(owner)
|
|
154
|
+
chunk.bytesize
|
|
155
|
+
}
|
|
156
|
+
easy.perform
|
|
157
|
+
build_response(easy)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def perform_streaming(easy, owner, stream)
|
|
161
|
+
res = nil
|
|
162
|
+
raw_body = +""
|
|
163
|
+
decoder = stream.decoder.new(stream.parser.new(stream.streamer))
|
|
164
|
+
easy.on_body do |chunk|
|
|
165
|
+
raise LLM::Interrupt, "request interrupted" if interrupted?(owner)
|
|
166
|
+
if (res ||= build_response_from_headers(easy))&.success? \
|
|
167
|
+
&& res["content-type"].to_s.include?("text/event-stream")
|
|
168
|
+
decoder << chunk
|
|
169
|
+
else
|
|
170
|
+
raw_body << chunk
|
|
171
|
+
end
|
|
172
|
+
chunk.bytesize
|
|
173
|
+
end
|
|
174
|
+
easy.perform
|
|
175
|
+
res ||= build_response(easy)
|
|
176
|
+
if raw_body.empty?
|
|
177
|
+
body = decoder.body
|
|
178
|
+
res.body = (Hash === body || Array === body) \
|
|
179
|
+
? LLM::Object.from(body) : body
|
|
180
|
+
else
|
|
181
|
+
res.body = raw_body
|
|
182
|
+
end
|
|
183
|
+
res
|
|
184
|
+
ensure
|
|
185
|
+
decoder&.free
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_response(easy)
|
|
189
|
+
LLM::Transport::Response::Curb.new(
|
|
190
|
+
easy.response_code.to_i,
|
|
191
|
+
parse_headers(easy.header_str.to_s),
|
|
192
|
+
easy.body_str.to_s
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def build_response_from_headers(easy)
|
|
197
|
+
return nil if easy.header_str.to_s.empty?
|
|
198
|
+
LLM::Transport::Response::Curb.new(
|
|
199
|
+
easy.response_code.to_i,
|
|
200
|
+
parse_headers(easy.header_str.to_s),
|
|
201
|
+
+""
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parse_headers(header_str)
|
|
206
|
+
headers = {}
|
|
207
|
+
header_str.each_line do |line|
|
|
208
|
+
line = line.strip
|
|
209
|
+
next if line.empty? || line.start_with?("HTTP/")
|
|
210
|
+
key, value = line.split(/: \s*/, 2)
|
|
211
|
+
headers[key.downcase] = value if key && value
|
|
212
|
+
end
|
|
213
|
+
headers
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def check_interrupted(owner)
|
|
217
|
+
raise LLM::Interrupt, "request interrupted" if interrupted?(owner)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def request_for(owner)
|
|
221
|
+
lock do
|
|
222
|
+
@requests ||= {}
|
|
223
|
+
@requests[owner]
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def set_request(req, owner)
|
|
228
|
+
lock do
|
|
229
|
+
@requests ||= {}
|
|
230
|
+
@requests[owner] = req
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def clear_request(owner)
|
|
235
|
+
lock { @requests&.delete(owner) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def lock(&)
|
|
239
|
+
@monitor.synchronize(&)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def optional_interrupt_errors
|
|
243
|
+
defined?(::Async::Stop) ? [Async::Stop] : []
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
data/lib/llm/transport/http.rb
CHANGED
|
@@ -11,8 +11,10 @@ class LLM::Transport
|
|
|
11
11
|
#
|
|
12
12
|
# @api private
|
|
13
13
|
class HTTP < self
|
|
14
|
+
include NetHTTPAdapter
|
|
15
|
+
|
|
14
16
|
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
15
|
-
|
|
17
|
+
ActiveRequest = Struct.new(:client, keyword_init: true)
|
|
16
18
|
|
|
17
19
|
##
|
|
18
20
|
# @param [String] host
|
|
@@ -67,15 +69,18 @@ class LLM::Transport
|
|
|
67
69
|
|
|
68
70
|
##
|
|
69
71
|
# Performs a request on the current HTTP transport.
|
|
70
|
-
#
|
|
72
|
+
# Accepts both {Net::HTTPRequest} and {LLM::Transport::Request}.
|
|
73
|
+
#
|
|
74
|
+
# @param [Net::HTTPRequest, LLM::Transport::Request] request
|
|
71
75
|
# @param [Fiber] owner
|
|
72
76
|
# @param [LLM::Object, nil] stream
|
|
73
77
|
# @yieldparam [LLM::Transport::Response] response
|
|
74
78
|
# @return [Object]
|
|
75
79
|
def request(request, owner:, stream: nil, &b)
|
|
80
|
+
http_req = resolve_request(request)
|
|
76
81
|
client = client()
|
|
77
|
-
set_request(
|
|
78
|
-
perform_request(client,
|
|
82
|
+
set_request(ActiveRequest.new(client:), owner)
|
|
83
|
+
perform_request(client, http_req, stream, &b)
|
|
79
84
|
ensure
|
|
80
85
|
clear_request(owner)
|
|
81
86
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport
|
|
4
|
+
##
|
|
5
|
+
# @api private
|
|
6
|
+
module NetHTTPAdapter
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def resolve_request(request)
|
|
10
|
+
return request if ::Net::HTTPRequest === request
|
|
11
|
+
build_net_http_request(request)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build_net_http_request(req)
|
|
15
|
+
method = req.method.downcase.to_sym
|
|
16
|
+
path = req.path
|
|
17
|
+
headers = req.headers
|
|
18
|
+
http_req = case method
|
|
19
|
+
when :get then ::Net::HTTP::Get.new(path, headers)
|
|
20
|
+
when :post then ::Net::HTTP::Post.new(path, headers)
|
|
21
|
+
when :put then ::Net::HTTP::Put.new(path, headers)
|
|
22
|
+
when :patch then ::Net::HTTP::Patch.new(path, headers)
|
|
23
|
+
when :delete then ::Net::HTTP::Delete.new(path, headers)
|
|
24
|
+
else ::Net::HTTP::GenericRequest.new(method, path, nil, headers)
|
|
25
|
+
end
|
|
26
|
+
if req.body
|
|
27
|
+
http_req.body = req.body
|
|
28
|
+
elsif req.body_stream
|
|
29
|
+
http_req.body_stream = req.body_stream
|
|
30
|
+
end
|
|
31
|
+
http_req
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def perform_request(client, request, stream, &b)
|
|
35
|
+
if stream
|
|
36
|
+
client.request(request) do |raw|
|
|
37
|
+
res = LLM::Transport::Response.from(raw)
|
|
38
|
+
if res.success?
|
|
39
|
+
parser = stream.decoder.new(stream.parser.new(stream.streamer))
|
|
40
|
+
res.read_body(parser)
|
|
41
|
+
body = parser.body
|
|
42
|
+
res.body = (Hash === body || Array === body) ? LLM::Object.from(body) : body
|
|
43
|
+
else
|
|
44
|
+
body = +""
|
|
45
|
+
res.read_body { body << _1 }
|
|
46
|
+
res.body = body
|
|
47
|
+
end
|
|
48
|
+
ensure
|
|
49
|
+
parser&.free
|
|
50
|
+
end
|
|
51
|
+
elsif b
|
|
52
|
+
client.request(request) do |raw|
|
|
53
|
+
res = LLM::Transport::Response.from(raw)
|
|
54
|
+
res.success? ? b.call(res) : res
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
LLM::Transport::Response.from(client.request(request))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -10,8 +10,10 @@ class LLM::Transport
|
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
12
|
class PersistentHTTP < self
|
|
13
|
+
include NetHTTPAdapter
|
|
14
|
+
|
|
13
15
|
INTERRUPT_ERRORS = [::IOError, ::EOFError, Errno::EBADF].freeze
|
|
14
|
-
|
|
16
|
+
ActiveRequest = Struct.new(:client, :connection, keyword_init: true)
|
|
15
17
|
@registry = {}
|
|
16
18
|
@monitor = Monitor.new
|
|
17
19
|
|
|
@@ -79,15 +81,18 @@ class LLM::Transport
|
|
|
79
81
|
|
|
80
82
|
##
|
|
81
83
|
# Performs a request on the current HTTP transport.
|
|
82
|
-
#
|
|
84
|
+
# Accepts both {Net::HTTPRequest} and {LLM::Transport::Request}.
|
|
85
|
+
#
|
|
86
|
+
# @param [Net::HTTPRequest, LLM::Transport::Request] request
|
|
83
87
|
# @param [Fiber] owner
|
|
84
88
|
# @param [LLM::Object, nil] stream
|
|
85
89
|
# @yieldparam [LLM::Transport::Response] response
|
|
86
90
|
# @return [Object]
|
|
87
91
|
def request(request, owner:, stream: nil, &b)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
http_req = resolve_request(request)
|
|
93
|
+
client.connection_for(URI.join(base_uri, http_req.path)) do |connection|
|
|
94
|
+
set_request(ActiveRequest.new(client:, connection:), owner)
|
|
95
|
+
perform_request(connection.http, http_req, stream, &b)
|
|
91
96
|
end
|
|
92
97
|
ensure
|
|
93
98
|
clear_request(owner)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Transport::Request LLM::Transport::Request} defines the
|
|
6
|
+
# normalized request interface expected by transports.
|
|
7
|
+
#
|
|
8
|
+
# Providers build request objects through this class, then hand them
|
|
9
|
+
# to a transport for execution without depending on any specific HTTP
|
|
10
|
+
# client library.
|
|
11
|
+
class Request
|
|
12
|
+
##
|
|
13
|
+
# @return [Object, nil]
|
|
14
|
+
attr_accessor :body
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @return [IO, nil]
|
|
18
|
+
attr_accessor :body_stream
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [String]
|
|
22
|
+
attr_reader :method
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @return [String]
|
|
26
|
+
attr_reader :path
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
attr_reader :headers
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @param [String] path
|
|
34
|
+
# @param [Hash, nil] headers
|
|
35
|
+
# @return [LLM::Transport::Request]
|
|
36
|
+
def self.get(path, headers = nil)
|
|
37
|
+
new("GET", path, headers)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @param [String] path
|
|
42
|
+
# @param [Hash, nil] headers
|
|
43
|
+
# @return [LLM::Transport::Request]
|
|
44
|
+
def self.post(path, headers = nil)
|
|
45
|
+
new("POST", path, headers)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @param [String] path
|
|
50
|
+
# @param [Hash, nil] headers
|
|
51
|
+
# @return [LLM::Transport::Request]
|
|
52
|
+
def self.put(path, headers = nil)
|
|
53
|
+
new("PUT", path, headers)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @param [String] path
|
|
58
|
+
# @param [Hash, nil] headers
|
|
59
|
+
# @return [LLM::Transport::Request]
|
|
60
|
+
def self.patch(path, headers = nil)
|
|
61
|
+
new("PATCH", path, headers)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# @param [String] path
|
|
66
|
+
# @param [Hash, nil] headers
|
|
67
|
+
# @return [LLM::Transport::Request]
|
|
68
|
+
def self.delete(path, headers = nil)
|
|
69
|
+
new("DELETE", path, headers)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# @param [String] method
|
|
74
|
+
# @param [String] path
|
|
75
|
+
# @param [Hash, nil] headers
|
|
76
|
+
# @return [LLM::Transport::Request]
|
|
77
|
+
def initialize(method, path, headers = nil)
|
|
78
|
+
@method = method.to_s.upcase
|
|
79
|
+
@path = path.to_s
|
|
80
|
+
@headers = {}
|
|
81
|
+
(headers || {}).each { self[_1] = _2 }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# @param [String] key
|
|
86
|
+
# @return [String, nil]
|
|
87
|
+
def [](key)
|
|
88
|
+
@headers[normalize_header(key)]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# @param [String] key
|
|
93
|
+
# @param [Object] value
|
|
94
|
+
# @return [String]
|
|
95
|
+
def []=(key, value)
|
|
96
|
+
@headers[normalize_header(key)] = value.to_s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# @yieldparam [String] key
|
|
101
|
+
# @yieldparam [String] value
|
|
102
|
+
# @return [Hash]
|
|
103
|
+
def each_header(&block)
|
|
104
|
+
@headers.each(&block)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def inspect
|
|
110
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}" \
|
|
111
|
+
" @method=#{@method} @path=#{@path}" \
|
|
112
|
+
" @headers=#{@headers.inspect}>"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def normalize_header(key)
|
|
118
|
+
key.to_s.downcase
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport::Response
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Transport::Response::Curb LLM::Transport::Response::Curb}
|
|
6
|
+
# adapts a raw status code, header hash, and body string to the
|
|
7
|
+
# {LLM::Transport::Response LLM::Transport::Response} interface.
|
|
8
|
+
#
|
|
9
|
+
# This is the response wrapper used by the
|
|
10
|
+
# {LLM::Transport::Curb LLM::Transport::Curb} transport.
|
|
11
|
+
class Curb < self
|
|
12
|
+
##
|
|
13
|
+
# @return [Integer]
|
|
14
|
+
attr_reader :code
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @return [Hash]
|
|
18
|
+
attr_reader :headers
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [String]
|
|
22
|
+
attr_accessor :body
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @param [#to_i] code
|
|
26
|
+
# @param [Hash] headers
|
|
27
|
+
# @param [String] body
|
|
28
|
+
# @return [LLM::Transport::Response::Curb]
|
|
29
|
+
def initialize(code, headers = {}, body = +"")
|
|
30
|
+
@code = code.to_i
|
|
31
|
+
@headers = {}
|
|
32
|
+
(headers || {}).each { @headers[_1.to_s.downcase] = _2.to_s }
|
|
33
|
+
@body = body
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @param [String] key
|
|
38
|
+
# @return [String, nil]
|
|
39
|
+
def [](key)
|
|
40
|
+
@headers[key.to_s.downcase]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @param [Object, nil] dest
|
|
45
|
+
# @yieldparam [String] chunk
|
|
46
|
+
# @return [void]
|
|
47
|
+
def read_body(dest = nil, &block)
|
|
48
|
+
return @body unless block_given? || dest
|
|
49
|
+
if dest
|
|
50
|
+
dest << @body.to_s
|
|
51
|
+
else
|
|
52
|
+
yield @body.to_s
|
|
53
|
+
end
|
|
54
|
+
@body
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def success?
|
|
60
|
+
code.between?(200, 299)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def ok?
|
|
66
|
+
code == 200
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def bad_request?
|
|
72
|
+
code == 400
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def unauthorized?
|
|
78
|
+
code == 401
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def forbidden?
|
|
84
|
+
code == 403
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def not_found?
|
|
90
|
+
code == 404
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def rate_limited?
|
|
96
|
+
code == 429
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def server_error?
|
|
102
|
+
code.between?(500, 599)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# @return [String]
|
|
107
|
+
def inspect
|
|
108
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)}" \
|
|
109
|
+
" @code=#{@code} @headers=#{@headers.inspect}>"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/llm/transport/utils.rb
CHANGED
|
@@ -4,32 +4,57 @@ class LLM::Transport
|
|
|
4
4
|
##
|
|
5
5
|
# Shared utility methods for HTTP-backed transports.
|
|
6
6
|
#
|
|
7
|
+
# These methods resolve the transport options accepted by providers,
|
|
8
|
+
# MCP HTTP clients, and A2A HTTP clients into concrete
|
|
9
|
+
# {LLM::Transport} instances.
|
|
10
|
+
#
|
|
7
11
|
# @api private
|
|
8
12
|
module Utils
|
|
9
13
|
extend self
|
|
10
|
-
private
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
##
|
|
16
|
+
# Resolves a transport configuration into a transport instance.
|
|
17
|
+
#
|
|
18
|
+
# Nil values use the default Net::HTTP transport, or the persistent
|
|
19
|
+
# Net::HTTP transport when `persistent` is true. Transport subclasses
|
|
20
|
+
# are instantiated with the endpoint settings, symbols are resolved
|
|
21
|
+
# through {LLM::Transport} shortcut methods, and transport instances
|
|
22
|
+
# are returned as-is.
|
|
23
|
+
#
|
|
24
|
+
# @param [String] host
|
|
25
|
+
# @param [Integer] port
|
|
26
|
+
# @param [Integer, nil] timeout
|
|
27
|
+
# @param [Boolean] ssl
|
|
28
|
+
# @param [Boolean] persistent
|
|
29
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
30
|
+
# @return [LLM::Transport]
|
|
31
|
+
def resolve_transport(host:, port:, timeout:, ssl:, persistent:, transport:)
|
|
32
|
+
if transport.nil?
|
|
33
|
+
default_transport(host:, port:, timeout:, ssl:, persistent:)
|
|
34
|
+
elsif Class === transport && transport <= LLM::Transport
|
|
35
|
+
transport.new(host:, port:, timeout:, ssl:)
|
|
36
|
+
elsif Symbol === transport
|
|
37
|
+
transport = LLM::Transport.public_send(transport)
|
|
38
|
+
transport.new(host:, port:, timeout:, ssl:)
|
|
21
39
|
else
|
|
22
40
|
transport
|
|
23
41
|
end
|
|
24
42
|
end
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# Builds the default Net::HTTP transport for an endpoint.
|
|
48
|
+
#
|
|
49
|
+
# @param [String] host
|
|
50
|
+
# @param [Integer] port
|
|
51
|
+
# @param [Integer, nil] timeout
|
|
52
|
+
# @param [Boolean] ssl
|
|
53
|
+
# @param [Boolean] persistent
|
|
54
|
+
# @return [LLM::Transport]
|
|
55
|
+
def default_transport(host:, port:, timeout:, ssl:, persistent:)
|
|
56
|
+
target = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
|
|
57
|
+
target.new(host:, port:, timeout:, ssl:)
|
|
33
58
|
end
|
|
34
59
|
end
|
|
35
60
|
end
|