llm.rb 11.1.0 → 11.3.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 +141 -12
- data/README.md +104 -69
- data/lib/llm/a2a/transport/http.rb +9 -8
- data/lib/llm/a2a.rb +14 -7
- data/lib/llm/agent.rb +31 -7
- data/lib/llm/context.rb +20 -6
- data/lib/llm/error.rb +4 -0
- 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/error_handler.rb +2 -0
- 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/error_handler.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/error_handler.rb +2 -0
- 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/error_handler.rb +2 -0
- 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/error_handler.rb +2 -0
- 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 +6 -5
- metadata +25 -8
|
@@ -17,13 +17,14 @@ class LLM::A2A
|
|
|
17
17
|
# @param [String] url The base URL of the A2A agent
|
|
18
18
|
# @param [Hash<String, String>] headers Extra HTTP headers
|
|
19
19
|
# @param [Integer, nil] timeout The timeout in seconds
|
|
20
|
-
# @param [
|
|
20
|
+
# @param [Boolean] persistent Whether to use persistent HTTP connections
|
|
21
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport Override transport
|
|
21
22
|
# @param [String] protocol_version The A2A protocol version header
|
|
22
|
-
def initialize(url:, headers: {}, timeout: nil, transport: nil, protocol_version: "1.0")
|
|
23
|
+
def initialize(url:, headers: {}, timeout: nil, persistent: false, transport: nil, protocol_version: "1.0")
|
|
23
24
|
@uri = URI.parse(url)
|
|
24
25
|
@headers = headers
|
|
25
26
|
@protocol_version = protocol_version
|
|
26
|
-
@transport = resolve_transport(@uri,
|
|
27
|
+
@transport = resolve_transport(host: @uri.host, port: uri.port, ssl: @uri.scheme == "https", timeout:, persistent:, transport:)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
@@ -31,7 +32,7 @@ class LLM::A2A
|
|
|
31
32
|
# @param [String] path The URL path
|
|
32
33
|
# @return [Hash]
|
|
33
34
|
def get(path, accept: "application/json")
|
|
34
|
-
req =
|
|
35
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept:))
|
|
35
36
|
res = transport.request(req, owner: self)
|
|
36
37
|
parse_response(res)
|
|
37
38
|
end
|
|
@@ -42,7 +43,7 @@ class LLM::A2A
|
|
|
42
43
|
# @param [Hash] body The JSON body
|
|
43
44
|
# @return [Hash]
|
|
44
45
|
def post(path, body, content_type: "application/json", accept: "application/json")
|
|
45
|
-
req =
|
|
46
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept:))
|
|
46
47
|
req.body = LLM.json.dump(body)
|
|
47
48
|
res = transport.request(req, owner: self)
|
|
48
49
|
parse_response(res)
|
|
@@ -53,7 +54,7 @@ class LLM::A2A
|
|
|
53
54
|
# @param [String] path The URL path
|
|
54
55
|
# @return [Hash]
|
|
55
56
|
def delete(path, accept: "application/json")
|
|
56
|
-
req =
|
|
57
|
+
req = LLM::Transport::Request.delete(request_path(path), headers(accept:))
|
|
57
58
|
res = transport.request(req, owner: self)
|
|
58
59
|
parse_response(res)
|
|
59
60
|
end
|
|
@@ -66,7 +67,7 @@ class LLM::A2A
|
|
|
66
67
|
# @yieldparam [LLM::Object] event A stream event
|
|
67
68
|
# @return [void]
|
|
68
69
|
def get_stream(path, &on_event)
|
|
69
|
-
req =
|
|
70
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept: "text/event-stream"))
|
|
70
71
|
stream(req, &on_event)
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -79,7 +80,7 @@ class LLM::A2A
|
|
|
79
80
|
# @yieldparam [LLM::Object] event A stream event
|
|
80
81
|
# @return [void]
|
|
81
82
|
def post_stream(path, body, content_type: "application/json", &on_event)
|
|
82
|
-
req =
|
|
83
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept: "text/event-stream"))
|
|
83
84
|
req.body = LLM.json.dump(body)
|
|
84
85
|
stream(req, &on_event)
|
|
85
86
|
end
|
data/lib/llm/a2a.rb
CHANGED
|
@@ -61,8 +61,10 @@ class LLM::A2A
|
|
|
61
61
|
# Extra HTTP headers to include in requests (e.g., Authorization)
|
|
62
62
|
# @param [Integer, nil] timeout
|
|
63
63
|
# The timeout in seconds for HTTP requests
|
|
64
|
-
# @param [
|
|
65
|
-
#
|
|
64
|
+
# @param [Boolean] persistent
|
|
65
|
+
# Whether to use persistent HTTP connections
|
|
66
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
67
|
+
# Optional override with any {LLM::Transport} instance, subclass, or shortcut
|
|
66
68
|
# @param [Symbol] binding
|
|
67
69
|
# The protocol binding to use. One of `:rest` or `:jsonrpc`
|
|
68
70
|
# @param [String] base_path
|
|
@@ -70,7 +72,7 @@ class LLM::A2A
|
|
|
70
72
|
# @param [String] protocol_version
|
|
71
73
|
# The expected A2A protocol version. Defaults to `"1.0"`.
|
|
72
74
|
# @return [LLM::A2A]
|
|
73
|
-
def self.http(url:, headers: {}, timeout: 30, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
75
|
+
def self.http(url:, headers: {}, timeout: 30, persistent: false, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
74
76
|
new(
|
|
75
77
|
binding:,
|
|
76
78
|
base_path:,
|
|
@@ -79,6 +81,7 @@ class LLM::A2A
|
|
|
79
81
|
url:,
|
|
80
82
|
headers:,
|
|
81
83
|
timeout:,
|
|
84
|
+
persistent:,
|
|
82
85
|
transport:,
|
|
83
86
|
protocol_version:
|
|
84
87
|
)
|
|
@@ -90,13 +93,15 @@ class LLM::A2A
|
|
|
90
93
|
# @param [String] url
|
|
91
94
|
# @param [Hash<String, String>] headers
|
|
92
95
|
# @param [Integer, nil] timeout
|
|
93
|
-
# @param [
|
|
96
|
+
# @param [Boolean] persistent
|
|
97
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
94
98
|
# @return [LLM::A2A]
|
|
95
|
-
def self.rest(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
99
|
+
def self.rest(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
96
100
|
http(
|
|
97
101
|
url:,
|
|
98
102
|
headers:,
|
|
99
103
|
timeout:,
|
|
104
|
+
persistent:,
|
|
100
105
|
transport:,
|
|
101
106
|
binding: :rest,
|
|
102
107
|
base_path:,
|
|
@@ -109,13 +114,15 @@ class LLM::A2A
|
|
|
109
114
|
# @param [String] url
|
|
110
115
|
# @param [Hash<String, String>] headers
|
|
111
116
|
# @param [Integer, nil] timeout
|
|
112
|
-
# @param [
|
|
117
|
+
# @param [Boolean] persistent
|
|
118
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
113
119
|
# @return [LLM::A2A]
|
|
114
|
-
def self.jsonrpc(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
120
|
+
def self.jsonrpc(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
115
121
|
http(
|
|
116
122
|
url:,
|
|
117
123
|
headers:,
|
|
118
124
|
timeout:,
|
|
125
|
+
persistent:,
|
|
119
126
|
transport:,
|
|
120
127
|
binding: :jsonrpc,
|
|
121
128
|
base_path:,
|
data/lib/llm/agent.rb
CHANGED
|
@@ -72,7 +72,11 @@ module LLM
|
|
|
72
72
|
# Returns the current skills when no argument is provided
|
|
73
73
|
def self.skills(*skills, &block)
|
|
74
74
|
return @skills if skills.empty? && !block
|
|
75
|
-
|
|
75
|
+
if skills.size == 1 and skills.grep(Symbol).any?
|
|
76
|
+
@skills = skills.first
|
|
77
|
+
else
|
|
78
|
+
@skills = block || skills.flatten
|
|
79
|
+
end
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
##
|
|
@@ -160,14 +164,31 @@ module LLM
|
|
|
160
164
|
##
|
|
161
165
|
# Set or get the tool names that require confirmation before they can run.
|
|
162
166
|
#
|
|
167
|
+
# When a single Symbol is given, it is stored as-is and resolved at
|
|
168
|
+
# initialization time by calling the method with that name on the agent
|
|
169
|
+
# instance. This allows dynamic tool confirmation lists.
|
|
170
|
+
#
|
|
171
|
+
# @example
|
|
172
|
+
# class MyAgent < LLM::Agent
|
|
173
|
+
# confirm :tools_that_need_confirmation
|
|
174
|
+
#
|
|
175
|
+
# def tools_that_need_confirmation
|
|
176
|
+
# some_condition ? %w[delete destroy] : %w[delete]
|
|
177
|
+
# end
|
|
178
|
+
# end
|
|
179
|
+
#
|
|
163
180
|
# @param [String, Symbol, Array<String, Symbol>, Proc] tool_names
|
|
164
181
|
# One or more tool names.
|
|
165
182
|
# @param [Proc] block
|
|
166
183
|
# An optional, lazy-evaluated Proc
|
|
167
|
-
# @return [Array<String>, Proc, nil]
|
|
184
|
+
# @return [Array<String>, Proc, Symbol, nil]
|
|
168
185
|
def self.confirm(*tool_names, &block)
|
|
169
186
|
return @confirm if tool_names.empty? && !block
|
|
170
|
-
|
|
187
|
+
if tool_names.size == 1 && tool_names.grep(Symbol).any?
|
|
188
|
+
@confirm = tool_names.first
|
|
189
|
+
else
|
|
190
|
+
@confirm = block || tool_names.flatten.map(&:to_s)
|
|
191
|
+
end
|
|
171
192
|
end
|
|
172
193
|
|
|
173
194
|
##
|
|
@@ -190,7 +211,7 @@ module LLM
|
|
|
190
211
|
fields_ivar = %i[tracer concurrency instructions confirm]
|
|
191
212
|
fields.each do |field|
|
|
192
213
|
resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
|
|
193
|
-
resolve_symbol = !%i[concurrency
|
|
214
|
+
resolve_symbol = !%i[concurrency].include?(field)
|
|
194
215
|
resolved = resolvable != nil ? resolve_option(self, resolvable, resolve_symbol:) : resolvable
|
|
195
216
|
resolved = [*resolved].map(&:to_s) if field == :confirm && resolved
|
|
196
217
|
if field == :model
|
|
@@ -447,10 +468,13 @@ module LLM
|
|
|
447
468
|
strategy = concurrency || :call
|
|
448
469
|
return wait(strategy) unless @confirm&.any?
|
|
449
470
|
confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
|
|
450
|
-
results = confirmables.map
|
|
451
|
-
|
|
471
|
+
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
472
|
+
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
473
|
+
if (@ctx.functions - confirmables).any?
|
|
474
|
+
[*results, *wait(strategy, except: confirmables)]
|
|
475
|
+
else
|
|
476
|
+
results
|
|
452
477
|
end
|
|
453
|
-
@ctx.functions? ? [*results, *wait(strategy)] : results
|
|
454
478
|
end
|
|
455
479
|
|
|
456
480
|
##
|
data/lib/llm/context.rb
CHANGED
|
@@ -303,15 +303,21 @@ module LLM
|
|
|
303
303
|
# without using this argument.
|
|
304
304
|
# Otherwise, this controls how pending functions are resolved directly.
|
|
305
305
|
# Use `:call` for sequential execution without spawning.
|
|
306
|
+
# @param [Array<LLM::Function>] except
|
|
307
|
+
# A list of functions to exclude from the wait
|
|
306
308
|
# @return [Array<LLM::Function::Return>]
|
|
307
|
-
def wait(strategy)
|
|
309
|
+
def wait(strategy, except: [])
|
|
308
310
|
if LLM::Stream === stream && !stream.queue.empty?
|
|
309
311
|
@queue = stream.queue
|
|
310
312
|
@queue.wait
|
|
311
313
|
else
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
314
|
+
tools = except.empty? ? functions : functions - except
|
|
315
|
+
guards = guarded_returns(tools:)
|
|
316
|
+
return guards if guards
|
|
317
|
+
@queue = tools.spawn(strategy)
|
|
318
|
+
returns = @queue.wait
|
|
319
|
+
emit_tool_returns(tools, returns)
|
|
320
|
+
returns
|
|
315
321
|
end
|
|
316
322
|
ensure
|
|
317
323
|
@queue = nil
|
|
@@ -516,10 +522,10 @@ module LLM
|
|
|
516
522
|
##
|
|
517
523
|
# Builds in-band guarded returns when the guard blocks tool work.
|
|
518
524
|
# @api private
|
|
519
|
-
def guarded_returns
|
|
525
|
+
def guarded_returns(tools:)
|
|
520
526
|
warning = guard&.call(self)
|
|
521
527
|
return unless warning
|
|
522
|
-
|
|
528
|
+
tools.map { guarded_return_for(_1, warning) }
|
|
523
529
|
end
|
|
524
530
|
|
|
525
531
|
##
|
|
@@ -568,6 +574,14 @@ module LLM
|
|
|
568
574
|
})
|
|
569
575
|
end
|
|
570
576
|
|
|
577
|
+
##
|
|
578
|
+
# Emits tool return callbacks for directly waited function work.
|
|
579
|
+
# @api private
|
|
580
|
+
def emit_tool_returns(tools, returns)
|
|
581
|
+
return unless LLM::Stream === stream
|
|
582
|
+
returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
|
|
583
|
+
end
|
|
584
|
+
|
|
571
585
|
##
|
|
572
586
|
# Closes assistant tool-call messages that do not have matching tool
|
|
573
587
|
# responses. This can happen when a turn is interrupted while a tool
|
data/lib/llm/error.rb
CHANGED
data/lib/llm/function/array.rb
CHANGED
data/lib/llm/function.rb
CHANGED
|
@@ -120,6 +120,25 @@ class LLM::Function
|
|
|
120
120
|
@arguments = LLM::Object.from(other)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
##
|
|
124
|
+
# Compares functions by tool call ID when both sides have one.
|
|
125
|
+
# @param [LLM::Function] other
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def ==(other)
|
|
128
|
+
return true if equal?(other)
|
|
129
|
+
return false unless self.class === other
|
|
130
|
+
return false unless id && other.id
|
|
131
|
+
id == other.id
|
|
132
|
+
end
|
|
133
|
+
alias_method :eql?, :==
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Returns a hash value compatible with {#==}.
|
|
137
|
+
# @return [Integer]
|
|
138
|
+
def hash
|
|
139
|
+
id ? id.hash : object_id.hash
|
|
140
|
+
end
|
|
141
|
+
|
|
123
142
|
##
|
|
124
143
|
# Returns a tracer, or nil
|
|
125
144
|
# @return [LLM::Tracer, nil]
|
|
@@ -300,6 +319,13 @@ class LLM::Function
|
|
|
300
319
|
@cancelled
|
|
301
320
|
end
|
|
302
321
|
|
|
322
|
+
##
|
|
323
|
+
# Returns true when this function is backed by a skill tool.
|
|
324
|
+
# @return [Boolean]
|
|
325
|
+
def skill?
|
|
326
|
+
@runner.respond_to?(:skill?) and @runner.skill?
|
|
327
|
+
end
|
|
328
|
+
|
|
303
329
|
##
|
|
304
330
|
# Returns true when a function has neither been called nor cancelled
|
|
305
331
|
# @return [Boolean]
|
data/lib/llm/json_adapter.rb
CHANGED
|
@@ -35,9 +35,15 @@ module LLM
|
|
|
35
35
|
class JSONAdapter::JSON < JSONAdapter
|
|
36
36
|
##
|
|
37
37
|
# @return (see JSONAdapter#dump)
|
|
38
|
-
def self.dump(obj,
|
|
38
|
+
def self.dump(obj, state = nil, **options)
|
|
39
39
|
require "json" unless defined?(::JSON)
|
|
40
|
-
::JSON
|
|
40
|
+
if ::JSON::State === state
|
|
41
|
+
::JSON.generate(obj, state)
|
|
42
|
+
elsif state
|
|
43
|
+
::JSON.dump(obj, state, **options)
|
|
44
|
+
else
|
|
45
|
+
::JSON.dump(obj, **options)
|
|
46
|
+
end
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
##
|
|
@@ -16,16 +16,18 @@ module LLM::MCP::Transport
|
|
|
16
16
|
# Extra headers to send with requests
|
|
17
17
|
# @param [Integer, nil] timeout
|
|
18
18
|
# The timeout in seconds. Defaults to nil
|
|
19
|
-
# @param [
|
|
20
|
-
#
|
|
19
|
+
# @param [Boolean] persistent
|
|
20
|
+
# Whether to use persistent HTTP connections
|
|
21
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
22
|
+
# Optional override with any {LLM::Transport} instance, subclass, or shortcut
|
|
21
23
|
# @return [LLM::MCP::Transport::HTTP]
|
|
22
|
-
def initialize(url:, headers: {}, timeout: nil, transport: nil)
|
|
24
|
+
def initialize(url:, headers: {}, timeout: nil, persistent: false, transport: nil)
|
|
23
25
|
@uri = URI.parse(url)
|
|
24
26
|
@headers = headers
|
|
25
|
-
@transport = resolve_transport(uri, transport, timeout)
|
|
26
27
|
@queue = []
|
|
27
28
|
@monitor = Monitor.new
|
|
28
29
|
@running = false
|
|
30
|
+
@transport = resolve_transport(host: uri.host, port: uri.port, ssl: uri.scheme == "https", timeout:, persistent:, transport:)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
##
|
|
@@ -62,7 +64,7 @@ module LLM::MCP::Transport
|
|
|
62
64
|
# @return [void]
|
|
63
65
|
def write(message)
|
|
64
66
|
raise LLM::MCP::Error, "MCP transport is not running" unless running?
|
|
65
|
-
req =
|
|
67
|
+
req = LLM::Transport::Request.post(uri.request_uri, headers.merge("content-type" => "application/json"))
|
|
66
68
|
req.body = LLM.json.dump(message)
|
|
67
69
|
res = transport.request(req, owner: self) { consume(_1) }
|
|
68
70
|
res = LLM::Transport::Response.from(res)
|
data/lib/llm/mcp.rb
CHANGED
|
@@ -55,9 +55,11 @@ class LLM::MCP
|
|
|
55
55
|
# The URL for the MCP HTTP endpoint
|
|
56
56
|
# @option http [Hash] :headers
|
|
57
57
|
# Extra headers for requests
|
|
58
|
-
# @option http [
|
|
59
|
-
#
|
|
60
|
-
#
|
|
58
|
+
# @option http [Boolean] :persistent
|
|
59
|
+
# Whether to use persistent HTTP connections
|
|
60
|
+
# @option http [LLM::Transport, Class, Symbol] :transport
|
|
61
|
+
# Optional override with any {LLM::Transport} instance, subclass, or
|
|
62
|
+
# shortcut, similar to {LLM::Provider}
|
|
61
63
|
# @param [Integer] timeout
|
|
62
64
|
# The maximum amount of time to wait when reading from an MCP process
|
|
63
65
|
# @return [LLM::MCP] A new MCP instance
|
|
@@ -69,10 +71,7 @@ class LLM::MCP
|
|
|
69
71
|
@command = Command.new(**stdio)
|
|
70
72
|
@transport = Transport::Stdio.new(command:)
|
|
71
73
|
elsif http
|
|
72
|
-
|
|
73
|
-
transport = http.delete(:transport)
|
|
74
|
-
transport ||= LLM::Transport::PersistentHTTP if persistent
|
|
75
|
-
@transport = Transport::HTTP.new(**http, timeout:, transport:)
|
|
74
|
+
@transport = Transport::HTTP.new(**http, timeout:)
|
|
76
75
|
else
|
|
77
76
|
raise ArgumentError, "stdio or http is required"
|
|
78
77
|
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -35,7 +35,7 @@ class LLM::Provider
|
|
|
35
35
|
@base_path = LLM::Utils.normalize_base_path(base_path)
|
|
36
36
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
37
37
|
@headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
|
|
38
|
-
@transport = resolve_transport(transport
|
|
38
|
+
@transport = LLM::Transport::Utils.resolve_transport(host:, port:, timeout:, ssl:, transport:, persistent:)
|
|
39
39
|
@monitor = Monitor.new
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -417,23 +417,6 @@ class LLM::Provider
|
|
|
417
417
|
@monitor.synchronize(&)
|
|
418
418
|
end
|
|
419
419
|
|
|
420
|
-
##
|
|
421
|
-
# @api private
|
|
422
|
-
def default_transport(persistent:)
|
|
423
|
-
transport_class = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
|
|
424
|
-
transport_class.new(host:, port:, timeout:, ssl:)
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
##
|
|
428
|
-
# @api private
|
|
429
|
-
def resolve_transport(transport, persistent:)
|
|
430
|
-
return default_transport(persistent:) if transport.nil?
|
|
431
|
-
if Class === transport && transport <= LLM::Transport
|
|
432
|
-
return transport.new(host:, port:, timeout:, ssl:)
|
|
433
|
-
end
|
|
434
|
-
transport
|
|
435
|
-
end
|
|
436
|
-
|
|
437
420
|
##
|
|
438
421
|
# @api private
|
|
439
422
|
def thread
|
|
@@ -49,6 +49,8 @@ class LLM::Anthropic
|
|
|
49
49
|
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
50
50
|
elsif res.rate_limited?
|
|
51
51
|
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
52
|
+
elsif res.not_found?
|
|
53
|
+
LLM::NotFoundError.new("Server response: not found (404)").tap { _1.response = res }
|
|
52
54
|
else
|
|
53
55
|
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
54
56
|
end
|
|
@@ -37,7 +37,7 @@ class LLM::Anthropic
|
|
|
37
37
|
# @return [LLM::Response]
|
|
38
38
|
def all(**params)
|
|
39
39
|
query = URI.encode_www_form(params)
|
|
40
|
-
req =
|
|
40
|
+
req = LLM::Transport::Request.get("/v1/files?#{query}", headers)
|
|
41
41
|
res, span, tracer = execute(request: req, operation: "request")
|
|
42
42
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
43
43
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -56,7 +56,7 @@ class LLM::Anthropic
|
|
|
56
56
|
# @return [LLM::Response]
|
|
57
57
|
def create(file:, **params)
|
|
58
58
|
multi = LLM::Multipart.new(params.merge!(file: LLM.File(file)))
|
|
59
|
-
req =
|
|
59
|
+
req = LLM::Transport::Request.post("/v1/files", headers)
|
|
60
60
|
req["content-type"] = multi.content_type
|
|
61
61
|
transport.set_body_stream(req, multi.body)
|
|
62
62
|
res, span, tracer = execute(request: req, operation: "request")
|
|
@@ -79,7 +79,7 @@ class LLM::Anthropic
|
|
|
79
79
|
def get(file:, **params)
|
|
80
80
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
81
81
|
query = URI.encode_www_form(params)
|
|
82
|
-
req =
|
|
82
|
+
req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
|
|
83
83
|
res, span, tracer = execute(request: req, operation: "request")
|
|
84
84
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
85
85
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -100,7 +100,7 @@ class LLM::Anthropic
|
|
|
100
100
|
def get_metadata(file:, **params)
|
|
101
101
|
query = URI.encode_www_form(params)
|
|
102
102
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
103
|
-
req =
|
|
103
|
+
req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
|
|
104
104
|
res, span, tracer = execute(request: req, operation: "request")
|
|
105
105
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
106
106
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -120,7 +120,7 @@ class LLM::Anthropic
|
|
|
120
120
|
# @return [LLM::Response]
|
|
121
121
|
def delete(file:)
|
|
122
122
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
123
|
-
req =
|
|
123
|
+
req = LLM::Transport::Request.delete("/v1/files/#{file_id}", headers)
|
|
124
124
|
res, span, tracer = execute(request: req, operation: "request")
|
|
125
125
|
res = LLM::Response.new(res)
|
|
126
126
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -145,7 +145,7 @@ class LLM::Anthropic
|
|
|
145
145
|
def download(file:, **params)
|
|
146
146
|
query = URI.encode_www_form(params)
|
|
147
147
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
148
|
-
req =
|
|
148
|
+
req = LLM::Transport::Request.get("/v1/files/#{file_id}/content?#{query}", headers)
|
|
149
149
|
io = StringIO.new("".b)
|
|
150
150
|
res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
151
151
|
res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
@@ -39,7 +39,7 @@ class LLM::Anthropic
|
|
|
39
39
|
# @return [LLM::Response]
|
|
40
40
|
def all(**params)
|
|
41
41
|
query = URI.encode_www_form(params)
|
|
42
|
-
req =
|
|
42
|
+
req = LLM::Transport::Request.get("/v1/models?#{query}", headers)
|
|
43
43
|
res, span, tracer = execute(request: req, operation: "request")
|
|
44
44
|
res = ResponseAdapter.adapt(res, type: :models)
|
|
45
45
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -160,7 +160,7 @@ module LLM
|
|
|
160
160
|
messages = build_complete_messages(prompt, params, role)
|
|
161
161
|
payload = adapt(messages)
|
|
162
162
|
body = LLM.json.dump(payload.merge!(params))
|
|
163
|
-
req =
|
|
163
|
+
req = LLM::Transport::Request.post("/v1/messages", headers)
|
|
164
164
|
transport.set_body_stream(req, StringIO.new(body))
|
|
165
165
|
req
|
|
166
166
|
end
|
|
@@ -53,7 +53,7 @@ class LLM::Bedrock
|
|
|
53
53
|
elsif res.rate_limited?
|
|
54
54
|
LLM::RateLimitError.new(message).tap { _1.response = res }
|
|
55
55
|
elsif res.not_found?
|
|
56
|
-
LLM::
|
|
56
|
+
LLM::NotFoundError.new("Server response: not found (404)").tap { _1.response = res }
|
|
57
57
|
else
|
|
58
58
|
LLM::Error.new(message).tap { _1.response = res }
|
|
59
59
|
end
|
|
@@ -57,13 +57,13 @@ class LLM::Bedrock
|
|
|
57
57
|
##
|
|
58
58
|
# @param [String] host
|
|
59
59
|
# @param [Hash] params
|
|
60
|
-
# @return [
|
|
60
|
+
# @return [LLM::Transport::Request]
|
|
61
61
|
def build_request(host, params)
|
|
62
62
|
path = "/foundation-models"
|
|
63
63
|
query = URI.encode_www_form(params) unless params.empty?
|
|
64
64
|
path = "#{path}?#{query}" if query && !query.empty?
|
|
65
65
|
body = ""
|
|
66
|
-
req =
|
|
66
|
+
req = LLM::Transport::Request.get(path, {"Content-Type" => "application/json", "Accept" => "application/json"})
|
|
67
67
|
req.tap { sign!(req, body, host, query) }
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -84,11 +84,11 @@ class LLM::Bedrock
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
##
|
|
87
|
-
# @param [
|
|
87
|
+
# @param [LLM::Transport::Request] req
|
|
88
88
|
# @param [String] body
|
|
89
89
|
# @param [String] host
|
|
90
90
|
# @param [String, nil] query
|
|
91
|
-
# @return [
|
|
91
|
+
# @return [LLM::Transport::Request]
|
|
92
92
|
def sign!(req, body, host = credentials.host, query = nil)
|
|
93
93
|
creds = credentials.tap { _1.host = host }
|
|
94
94
|
Signature.new(credentials: creds, method: "GET", path: "/foundation-models", query:, body:).sign!(req)
|
|
@@ -8,7 +8,7 @@ class LLM::Bedrock
|
|
|
8
8
|
# Signs HTTP requests and headers with AWS Signature V4.
|
|
9
9
|
#
|
|
10
10
|
# Returns the signed headers as a Hash through #to_h, ready to merge
|
|
11
|
-
# into
|
|
11
|
+
# into an {LLM::Transport::Request} or other HTTP client. Everything else is
|
|
12
12
|
# private.
|
|
13
13
|
#
|
|
14
14
|
# Uses only Ruby's stdlib (openssl, digest) with no external deps.
|
|
@@ -89,8 +89,8 @@ class LLM::Bedrock
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
##
|
|
92
|
-
# @param [
|
|
93
|
-
# @return [
|
|
92
|
+
# @param [LLM::Transport::Request] req
|
|
93
|
+
# @return [LLM::Transport::Request]
|
|
94
94
|
def sign!(req)
|
|
95
95
|
to_h.each { |k, v| req[k] = v }
|
|
96
96
|
req
|
|
@@ -217,7 +217,7 @@ module LLM
|
|
|
217
217
|
body = LLM.json.dump(payload)
|
|
218
218
|
path = stream ? "/model/#{model_id}/converse-stream" \
|
|
219
219
|
: "/model/#{model_id}/converse"
|
|
220
|
-
req =
|
|
220
|
+
req = LLM::Transport::Request.post(path, headers)
|
|
221
221
|
transport.set_body_stream(req, StringIO.new(body))
|
|
222
222
|
[req, messages, body]
|
|
223
223
|
end
|
|
@@ -60,6 +60,8 @@ class LLM::Google
|
|
|
60
60
|
end
|
|
61
61
|
elsif res.rate_limited?
|
|
62
62
|
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
63
|
+
elsif res.not_found?
|
|
64
|
+
LLM::NotFoundError.new("Server response: not found (404)").tap { _1.response = res }
|
|
63
65
|
else
|
|
64
66
|
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
65
67
|
end
|
|
@@ -45,7 +45,7 @@ class LLM::Google
|
|
|
45
45
|
# @return [LLM::Response]
|
|
46
46
|
def all(**params)
|
|
47
47
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
48
|
-
req =
|
|
48
|
+
req = LLM::Transport::Request.get("/v1beta/files?#{query}", headers)
|
|
49
49
|
res, span, tracer = execute(request: req, operation: "request")
|
|
50
50
|
res = ResponseAdapter.adapt(res, type: :files)
|
|
51
51
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -64,7 +64,7 @@ class LLM::Google
|
|
|
64
64
|
# @return [LLM::Response]
|
|
65
65
|
def create(file:, **params)
|
|
66
66
|
file = LLM.File(file)
|
|
67
|
-
req =
|
|
67
|
+
req = LLM::Transport::Request.post(request_upload_url(file:), {})
|
|
68
68
|
req["content-length"] = file.bytesize
|
|
69
69
|
req["X-Goog-Upload-Offset"] = 0
|
|
70
70
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
|
@@ -91,7 +91,7 @@ class LLM::Google
|
|
|
91
91
|
def get(file:, **params)
|
|
92
92
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
93
93
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
94
|
-
req =
|
|
94
|
+
req = LLM::Transport::Request.get("/v1beta/#{file_id}?#{query}", headers)
|
|
95
95
|
res, span, tracer = execute(request: req, operation: "request")
|
|
96
96
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
97
97
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -111,7 +111,7 @@ class LLM::Google
|
|
|
111
111
|
def delete(file:, **params)
|
|
112
112
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
113
113
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
114
|
-
req =
|
|
114
|
+
req = LLM::Transport::Request.delete("/v1beta/#{file_id}?#{query}", headers)
|
|
115
115
|
res, span, tracer = execute(request: req, operation: "request")
|
|
116
116
|
res = LLM::Response.new(res)
|
|
117
117
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -128,7 +128,7 @@ class LLM::Google
|
|
|
128
128
|
private
|
|
129
129
|
|
|
130
130
|
def request_upload_url(file:)
|
|
131
|
-
req =
|
|
131
|
+
req = LLM::Transport::Request.post("/upload/v1beta/files?key=#{key}", headers)
|
|
132
132
|
req["X-Goog-Upload-Protocol"] = "resumable"
|
|
133
133
|
req["X-Goog-Upload-Command"] = "start"
|
|
134
134
|
req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
|
|
@@ -40,7 +40,7 @@ class LLM::Google
|
|
|
40
40
|
# @raise (see LLM::Provider#request)
|
|
41
41
|
# @return [LLM::Response]
|
|
42
42
|
def create(prompt:, n: 1, image_size: nil, aspect_ratio: nil, person_generation: nil, model: "imagen-4.0-generate-001", **params)
|
|
43
|
-
req =
|
|
43
|
+
req = LLM::Transport::Request.post("/v1beta/models/#{model}:predict?key=#{key}", headers)
|
|
44
44
|
body = LLM.json.dump({
|
|
45
45
|
parameters: {
|
|
46
46
|
sampleCount: n,
|