llm.rb 7.0.0 → 8.1.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 +151 -1
- data/README.md +45 -25
- data/data/bedrock.json +2948 -0
- data/data/deepseek.json +8 -8
- data/data/openai.json +39 -2
- data/data/xai.json +35 -0
- data/data/zai.json +1 -1
- data/lib/llm/active_record/acts_as_agent.rb +2 -6
- data/lib/llm/active_record/acts_as_llm.rb +4 -82
- data/lib/llm/active_record.rb +80 -2
- data/lib/llm/agent.rb +9 -4
- data/lib/llm/error.rb +4 -0
- data/lib/llm/function/array.rb +7 -3
- data/lib/llm/function/fiber_group.rb +9 -3
- data/lib/llm/function/fork/job.rb +67 -0
- data/lib/llm/function/fork/task.rb +76 -0
- data/lib/llm/function/fork.rb +8 -0
- data/lib/llm/function/fork_group.rb +36 -0
- data/lib/llm/function/ractor/task.rb +13 -3
- data/lib/llm/function/task.rb +10 -2
- data/lib/llm/function.rb +24 -11
- data/lib/llm/mcp/command.rb +1 -1
- data/lib/llm/mcp/transport/http.rb +2 -2
- data/lib/llm/mcp.rb +7 -4
- data/lib/llm/object/kernel.rb +8 -2
- data/lib/llm/object.rb +75 -21
- data/lib/llm/{mcp/pipe.rb → pipe.rb} +9 -8
- data/lib/llm/provider/transport/http/execution.rb +1 -1
- data/lib/llm/provider/transport/http.rb +1 -1
- data/lib/llm/provider.rb +7 -0
- data/lib/llm/providers/bedrock/error_handler.rb +80 -0
- data/lib/llm/providers/bedrock/models.rb +109 -0
- data/lib/llm/providers/bedrock/request_adapter/completion.rb +153 -0
- data/lib/llm/providers/bedrock/request_adapter.rb +95 -0
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +143 -0
- data/lib/llm/providers/bedrock/response_adapter/models.rb +34 -0
- data/lib/llm/providers/bedrock/response_adapter.rb +40 -0
- data/lib/llm/providers/bedrock/signature.rb +166 -0
- data/lib/llm/providers/bedrock/stream_decoder.rb +140 -0
- data/lib/llm/providers/bedrock/stream_parser.rb +201 -0
- data/lib/llm/providers/bedrock.rb +272 -0
- data/lib/llm/stream/queue.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +27 -1
- data/llm.gemspec +2 -1
- metadata +33 -3
|
@@ -19,9 +19,19 @@ class LLM::Function
|
|
|
19
19
|
# @param [Object, nil] span
|
|
20
20
|
# @return [LLM::Function::Ractor::Task]
|
|
21
21
|
def initialize(runner_class, id, name, arguments, tracer: nil, span: nil)
|
|
22
|
+
@runner_class = runner_class
|
|
23
|
+
@id = id
|
|
24
|
+
@name = name
|
|
25
|
+
@arguments = arguments
|
|
22
26
|
@tracer = tracer
|
|
23
27
|
@span = span
|
|
24
|
-
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [LLM::Function::Ractor::Task]
|
|
32
|
+
def spawn
|
|
33
|
+
@mailbox = Ractor::Mailbox.new(build_task)
|
|
34
|
+
self
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
##
|
|
@@ -49,8 +59,8 @@ class LLM::Function
|
|
|
49
59
|
|
|
50
60
|
private
|
|
51
61
|
|
|
52
|
-
def build_task
|
|
53
|
-
::Ractor.new(runner_class, id, name, arguments) do |runner_class, id, name, arguments|
|
|
62
|
+
def build_task
|
|
63
|
+
::Ractor.new(@runner_class, @id, @name, @arguments) do |runner_class, id, name, arguments|
|
|
54
64
|
LLM::Function::Ractor::Job.new(::Ractor.current, runner_class, id, name, arguments).call
|
|
55
65
|
end
|
|
56
66
|
end
|
data/lib/llm/function/task.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
class LLM::Function
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Function::Task} class wraps a single concurrent function call and
|
|
6
|
-
# provides a small, uniform interface across threads, fibers,
|
|
6
|
+
# provides a small, uniform interface across threads, scheduler-backed fibers,
|
|
7
|
+
# and async tasks.
|
|
7
8
|
class Task
|
|
8
9
|
##
|
|
9
10
|
# @return [Object]
|
|
@@ -32,6 +33,7 @@ class LLM::Function
|
|
|
32
33
|
##
|
|
33
34
|
# @return [nil]
|
|
34
35
|
def interrupt!
|
|
36
|
+
task.interrupt! if task.respond_to?(:interrupt!)
|
|
35
37
|
function&.interrupt!
|
|
36
38
|
nil
|
|
37
39
|
end
|
|
@@ -43,12 +45,18 @@ class LLM::Function
|
|
|
43
45
|
if Thread === task
|
|
44
46
|
task.value
|
|
45
47
|
elsif Fiber === task
|
|
46
|
-
|
|
48
|
+
fiber.alive? ? scheduler.run : nil
|
|
47
49
|
task.value
|
|
48
50
|
else
|
|
49
51
|
task.wait
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
alias_method :value, :wait
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scheduler
|
|
59
|
+
Fiber.scheduler
|
|
60
|
+
end
|
|
53
61
|
end
|
|
54
62
|
end
|
data/lib/llm/function.rb
CHANGED
|
@@ -36,6 +36,8 @@ class LLM::Function
|
|
|
36
36
|
require_relative "function/thread_group"
|
|
37
37
|
require_relative "function/fiber_group"
|
|
38
38
|
require_relative "function/task_group"
|
|
39
|
+
require_relative "function/fork"
|
|
40
|
+
require_relative "function/fork_group"
|
|
39
41
|
require_relative "function/ractor"
|
|
40
42
|
require_relative "function/ractor_group"
|
|
41
43
|
|
|
@@ -209,7 +211,9 @@ class LLM::Function
|
|
|
209
211
|
# Controls concurrency strategy:
|
|
210
212
|
# - `:thread`: Use threads
|
|
211
213
|
# - `:task`: Use async tasks (requires async gem)
|
|
212
|
-
# - `:
|
|
214
|
+
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
215
|
+
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
216
|
+
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
213
217
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
|
214
218
|
#
|
|
215
219
|
# @return [LLM::Function::Task]
|
|
@@ -217,25 +221,26 @@ class LLM::Function
|
|
|
217
221
|
def spawn(strategy)
|
|
218
222
|
task = case strategy
|
|
219
223
|
when :task
|
|
220
|
-
require "async" unless defined?(::Async)
|
|
224
|
+
LLM.require "async" unless defined?(::Async)
|
|
221
225
|
Async { call! }
|
|
222
226
|
when :thread
|
|
223
227
|
Thread.new { call! }
|
|
224
228
|
when :fiber
|
|
225
|
-
Fiber.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
raise ArgumentError, "Fiber concurrency requires Fiber.scheduler" unless Fiber.scheduler
|
|
230
|
+
Fiber.schedule { call! }
|
|
231
|
+
when :fork
|
|
232
|
+
LLM.require "xchan" unless defined?(::Chan::UNIXSocket)
|
|
233
|
+
span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
|
|
234
|
+
Fork::Task.new(self, tracer: @tracer, span:).spawn
|
|
230
235
|
when :ractor
|
|
231
236
|
raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
|
|
232
237
|
if @runner.respond_to?(:skill?) && @runner.skill?
|
|
233
238
|
raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
|
|
234
239
|
end
|
|
235
240
|
span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
|
|
236
|
-
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:)
|
|
241
|
+
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
|
|
237
242
|
else
|
|
238
|
-
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
|
|
243
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, :fork, or :ractor"
|
|
239
244
|
end
|
|
240
245
|
Task.new(task, self)
|
|
241
246
|
ensure
|
|
@@ -306,6 +311,15 @@ class LLM::Function
|
|
|
306
311
|
end
|
|
307
312
|
end
|
|
308
313
|
|
|
314
|
+
##
|
|
315
|
+
# Returns the bound function runner instance.
|
|
316
|
+
# @return [Object]
|
|
317
|
+
def runner
|
|
318
|
+
runner = Class === @runner ? @runner.new : @runner
|
|
319
|
+
runner.tracer = @tracer if runner.respond_to?(:tracer=)
|
|
320
|
+
runner
|
|
321
|
+
end
|
|
322
|
+
|
|
309
323
|
private
|
|
310
324
|
|
|
311
325
|
def format_openai(provider)
|
|
@@ -331,8 +345,7 @@ class LLM::Function
|
|
|
331
345
|
# @return [LLM::Function::Return]
|
|
332
346
|
# Returns a Return object with either the function result or error information.
|
|
333
347
|
def call_function
|
|
334
|
-
runner =
|
|
335
|
-
runner.tracer = @tracer if runner.respond_to?(:tracer=)
|
|
348
|
+
runner = self.runner
|
|
336
349
|
kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
|
|
337
350
|
Return.new(id, name, runner.call(**kwargs))
|
|
338
351
|
rescue => ex
|
data/lib/llm/mcp/command.rb
CHANGED
|
@@ -34,7 +34,7 @@ class LLM::MCP
|
|
|
34
34
|
# @return [void]
|
|
35
35
|
def start
|
|
36
36
|
raise LLM::MCP::Error, "MCP command is already running" if alive?
|
|
37
|
-
@stdout, @stderr, @stdin = 3.times.map { Pipe.new }
|
|
37
|
+
@stdout, @stderr, @stdin = 3.times.map { LLM::Pipe.new }
|
|
38
38
|
@buffers.clear
|
|
39
39
|
@pid = Process.spawn(env.to_h, *argv, {chdir: cwd, out: stdout.w, err: stderr.w, in: stdin.r}.compact)
|
|
40
40
|
[stdin.close_reader, [stdout, stderr].each(&:close_writer)]
|
|
@@ -104,12 +104,12 @@ module LLM::MCP::Transport
|
|
|
104
104
|
# Configures the transport to use a persistent HTTP connection pool
|
|
105
105
|
# via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
|
|
106
106
|
# @example
|
|
107
|
-
# mcp = LLM.
|
|
107
|
+
# mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
|
|
108
108
|
# # do something with 'mcp'
|
|
109
109
|
# @return [LLM::MCP::Transport::HTTP]
|
|
110
110
|
def persist!
|
|
111
111
|
LLM.lock(:mcp) do
|
|
112
|
-
require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
|
|
112
|
+
LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
|
|
113
113
|
unless LLM::MCP.clients.key?(key)
|
|
114
114
|
http = Net::HTTP::Persistent.new(name: self.class.name)
|
|
115
115
|
http.read_timeout = timeout
|
data/lib/llm/mcp.rb
CHANGED
|
@@ -19,17 +19,18 @@ class LLM::MCP
|
|
|
19
19
|
require_relative "mcp/mailbox"
|
|
20
20
|
require_relative "mcp/router"
|
|
21
21
|
require_relative "mcp/rpc"
|
|
22
|
-
require_relative "mcp/pipe"
|
|
23
22
|
require_relative "mcp/transport/http"
|
|
24
23
|
require_relative "mcp/transport/stdio"
|
|
25
24
|
|
|
26
25
|
include RPC
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
@clients = {}
|
|
29
28
|
|
|
30
29
|
##
|
|
31
30
|
# @api private
|
|
32
|
-
def self.clients
|
|
31
|
+
def self.clients
|
|
32
|
+
@clients
|
|
33
|
+
end
|
|
33
34
|
|
|
34
35
|
##
|
|
35
36
|
# Builds an MCP client that uses the stdio transport.
|
|
@@ -80,7 +81,9 @@ class LLM::MCP
|
|
|
80
81
|
@command = Command.new(**stdio)
|
|
81
82
|
@transport = Transport::Stdio.new(command:)
|
|
82
83
|
elsif http
|
|
84
|
+
persistent = http.delete(:persistent)
|
|
83
85
|
@transport = Transport::HTTP.new(**http, timeout:)
|
|
86
|
+
@transport.persistent if persistent
|
|
84
87
|
else
|
|
85
88
|
raise ArgumentError, "stdio or http is required"
|
|
86
89
|
end
|
|
@@ -122,7 +125,7 @@ class LLM::MCP
|
|
|
122
125
|
# Configures an HTTP MCP transport to use a persistent connection pool
|
|
123
126
|
# via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
|
|
124
127
|
# @example
|
|
125
|
-
# mcp = LLM.
|
|
128
|
+
# mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
|
|
126
129
|
# # do something with 'mcp'
|
|
127
130
|
# @return [LLM::MCP]
|
|
128
131
|
def persist!
|
data/lib/llm/object/kernel.rb
CHANGED
|
@@ -4,6 +4,8 @@ class LLM::Object
|
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
6
|
module Kernel
|
|
7
|
+
TypeError = ::TypeError
|
|
8
|
+
|
|
7
9
|
def tap(...)
|
|
8
10
|
::Kernel.instance_method(:tap).bind(self).call(...)
|
|
9
11
|
end
|
|
@@ -26,11 +28,15 @@ class LLM::Object
|
|
|
26
28
|
alias_method :is_a?, :kind_of?
|
|
27
29
|
|
|
28
30
|
def respond_to?(m, include_private = false)
|
|
29
|
-
!!key(m) || self.class.method_defined?(m)
|
|
31
|
+
!!SINGLETON.key(@h, m) || self.class.method_defined?(m)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def respond_to_missing?(m, include_private = false)
|
|
33
|
-
!!key(m)
|
|
35
|
+
!!SINGLETON.key(@h, m)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def raise(...)
|
|
39
|
+
::Kernel.raise(...)
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def object_id
|
data/lib/llm/object.rb
CHANGED
|
@@ -8,6 +8,37 @@ class LLM::Object < BasicObject
|
|
|
8
8
|
require_relative "object/builder"
|
|
9
9
|
require_relative "object/kernel"
|
|
10
10
|
|
|
11
|
+
SINGLETON = self
|
|
12
|
+
UNDEFINED = ::Object.new.freeze
|
|
13
|
+
LLM = ::LLM
|
|
14
|
+
private_constant :SINGLETON, :UNDEFINED, :LLM
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @api private
|
|
18
|
+
# @param [Hash] h
|
|
19
|
+
# @param [#to_s, #to_sym] k
|
|
20
|
+
# @return [String, Symbol, nil]
|
|
21
|
+
def self.key(h, k)
|
|
22
|
+
return nil if k.nil?
|
|
23
|
+
if h.key?(k.to_s)
|
|
24
|
+
k.to_s
|
|
25
|
+
elsif h.key?(k.to_sym)
|
|
26
|
+
k.to_sym
|
|
27
|
+
else
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @api private
|
|
34
|
+
# @param [Hash] h
|
|
35
|
+
# @param [#to_s, #to_sym] k
|
|
36
|
+
# @return [Object, nil]
|
|
37
|
+
def self.get(h, k)
|
|
38
|
+
name = key(h, k)
|
|
39
|
+
h[name] if name
|
|
40
|
+
end
|
|
41
|
+
|
|
11
42
|
extend Builder
|
|
12
43
|
include Kernel
|
|
13
44
|
include ::Enumerable
|
|
@@ -29,11 +60,19 @@ class LLM::Object < BasicObject
|
|
|
29
60
|
@h.each(&)
|
|
30
61
|
end
|
|
31
62
|
|
|
63
|
+
##
|
|
64
|
+
# In-place transform of values with a block.
|
|
65
|
+
# @yieldparam [Object] v
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def transform_values!(&)
|
|
68
|
+
@h.transform_values!(&)
|
|
69
|
+
end
|
|
70
|
+
|
|
32
71
|
##
|
|
33
72
|
# @param [Symbol, #to_sym] k
|
|
34
73
|
# @return [Object]
|
|
35
74
|
def [](k)
|
|
36
|
-
@h[key(k)]
|
|
75
|
+
@h[SINGLETON.key(@h, k)]
|
|
37
76
|
end
|
|
38
77
|
|
|
39
78
|
##
|
|
@@ -47,7 +86,7 @@ class LLM::Object < BasicObject
|
|
|
47
86
|
##
|
|
48
87
|
# @return [String]
|
|
49
88
|
def to_json(...)
|
|
50
|
-
|
|
89
|
+
LLM.json.dump(to_h, ...)
|
|
51
90
|
end
|
|
52
91
|
|
|
53
92
|
##
|
|
@@ -83,16 +122,39 @@ class LLM::Object < BasicObject
|
|
|
83
122
|
##
|
|
84
123
|
# @param [String, Symbol] k
|
|
85
124
|
# @return [Boolean]
|
|
86
|
-
def key?(k)
|
|
87
|
-
@h
|
|
125
|
+
def key?(k = UNDEFINED)
|
|
126
|
+
return SINGLETON.get(@h, :key?) if k.equal?(UNDEFINED)
|
|
127
|
+
@h.key?(SINGLETON.key(@h, k))
|
|
88
128
|
end
|
|
89
129
|
alias_method :has_key?, :key?
|
|
90
130
|
|
|
91
131
|
##
|
|
92
132
|
# @param [String, Symbol] k
|
|
93
133
|
# @return [Object]
|
|
94
|
-
def fetch(k, *args, &b)
|
|
95
|
-
@h
|
|
134
|
+
def fetch(k = UNDEFINED, *args, &b)
|
|
135
|
+
return SINGLETON.get(@h, :fetch) if k.equal?(UNDEFINED)
|
|
136
|
+
@h.fetch(SINGLETON.key(@h, k), *args, &b)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# @param [Hash, to_h] other
|
|
141
|
+
# The hash to merge
|
|
142
|
+
# @return [LLM::Object]
|
|
143
|
+
# Returns a new LLM::Object
|
|
144
|
+
def merge(other = UNDEFINED)
|
|
145
|
+
return SINGLETON.get(@h, :merge) if other.equal?(UNDEFINED)
|
|
146
|
+
other = ::Hash.try_convert(other)
|
|
147
|
+
raise TypeError, "#{other} cannot be coerced into a Hash" unless other
|
|
148
|
+
SINGLETON.from @h.merge(other)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
# @param [#to_s, #to_sym] k
|
|
153
|
+
# The key name
|
|
154
|
+
# @return [void]
|
|
155
|
+
def delete(k = UNDEFINED)
|
|
156
|
+
return SINGLETON.get(@h, :delete) if k.equal?(UNDEFINED)
|
|
157
|
+
@h.delete(SINGLETON.key(@h, k))
|
|
96
158
|
end
|
|
97
159
|
|
|
98
160
|
##
|
|
@@ -110,14 +172,16 @@ class LLM::Object < BasicObject
|
|
|
110
172
|
|
|
111
173
|
##
|
|
112
174
|
# @return [Object, nil]
|
|
113
|
-
def dig(
|
|
114
|
-
@h
|
|
175
|
+
def dig(*args)
|
|
176
|
+
return SINGLETON.get(@h, :dig) if args.empty?
|
|
177
|
+
@h.dig(*args)
|
|
115
178
|
end
|
|
116
179
|
|
|
117
180
|
##
|
|
118
181
|
# @return [Hash]
|
|
119
|
-
def slice(
|
|
120
|
-
@h
|
|
182
|
+
def slice(*args)
|
|
183
|
+
return SINGLETON.get(@h, :slice) if args.empty?
|
|
184
|
+
@h.slice(*args)
|
|
121
185
|
end
|
|
122
186
|
|
|
123
187
|
private
|
|
@@ -125,20 +189,10 @@ class LLM::Object < BasicObject
|
|
|
125
189
|
def method_missing(m, *args, &b)
|
|
126
190
|
if m.to_s.end_with?("=")
|
|
127
191
|
self[m[0..-2]] = args.first
|
|
128
|
-
elsif k = key(m)
|
|
192
|
+
elsif k = SINGLETON.key(@h, m)
|
|
129
193
|
@h[k]
|
|
130
194
|
else
|
|
131
195
|
nil
|
|
132
196
|
end
|
|
133
197
|
end
|
|
134
|
-
|
|
135
|
-
def key(k)
|
|
136
|
-
if @h.key?(k.to_s)
|
|
137
|
-
k.to_s
|
|
138
|
-
elsif @h.key?(k.to_sym)
|
|
139
|
-
k.to_sym
|
|
140
|
-
else
|
|
141
|
-
nil
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
198
|
end
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module LLM
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# the stdin, stdout, and stderr streams of an MCP process through
|
|
9
|
-
# one small interface.
|
|
5
|
+
# The {LLM::Pipe LLM::Pipe} class wraps a pair of IO objects created by
|
|
6
|
+
# {IO.pipe}. It is used by llm.rb internals to manage process and stream
|
|
7
|
+
# communication through one small interface.
|
|
10
8
|
class Pipe
|
|
11
9
|
##
|
|
12
10
|
# @return [IO]
|
|
@@ -20,9 +18,12 @@ class LLM::MCP
|
|
|
20
18
|
|
|
21
19
|
##
|
|
22
20
|
# Returns a new pipe.
|
|
23
|
-
# @
|
|
24
|
-
|
|
21
|
+
# @param [Boolean] binmode
|
|
22
|
+
# Whether both ends of the pipe should be switched to binary mode
|
|
23
|
+
# @return [LLM::Pipe]
|
|
24
|
+
def initialize(binmode: false)
|
|
25
25
|
@r, @w = IO.pipe
|
|
26
|
+
[@r, @w].each(&:binmode) if binmode
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
##
|
|
@@ -92,7 +92,7 @@ module LLM::Provider::Transport
|
|
|
92
92
|
if stream
|
|
93
93
|
http.request(request) do |res|
|
|
94
94
|
if Net::HTTPSuccess === res
|
|
95
|
-
parser =
|
|
95
|
+
parser = stream_decoder.new(stream_parser.new(stream))
|
|
96
96
|
res.read_body(parser)
|
|
97
97
|
body = parser.body
|
|
98
98
|
res.body = (Hash === body || Array === body) ? LLM::Object.from(body) : body
|
data/lib/llm/provider.rb
CHANGED
|
@@ -399,6 +399,13 @@ class LLM::Provider
|
|
|
399
399
|
raise NotImplementedError
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
+
##
|
|
403
|
+
# @return [Class]
|
|
404
|
+
# Returns the class responsible for decoding streamed response bodies
|
|
405
|
+
def stream_decoder
|
|
406
|
+
LLM::Provider::Transport::HTTP::StreamDecoder
|
|
407
|
+
end
|
|
408
|
+
|
|
402
409
|
##
|
|
403
410
|
# Resolves tools to their function representations
|
|
404
411
|
# @param [Array<LLM::Function, LLM::Tool>] tools
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Bedrock
|
|
4
|
+
##
|
|
5
|
+
# Handles Bedrock API error responses.
|
|
6
|
+
#
|
|
7
|
+
# Bedrock errors come as JSON with:
|
|
8
|
+
# { "message" => "...", "__type" => "..." }
|
|
9
|
+
# or as standard HTTP status codes.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class ErrorHandler
|
|
13
|
+
##
|
|
14
|
+
# @return [Net::HTTPResponse]
|
|
15
|
+
attr_reader :res
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @return [Object, nil]
|
|
19
|
+
attr_reader :span
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @param [LLM::Tracer] tracer
|
|
23
|
+
# @param [Object, nil] span
|
|
24
|
+
# @param [Net::HTTPResponse] res
|
|
25
|
+
# @return [LLM::Bedrock::ErrorHandler]
|
|
26
|
+
def initialize(tracer, span, res)
|
|
27
|
+
@tracer = tracer
|
|
28
|
+
@span = span
|
|
29
|
+
@res = res
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @raise [LLM::Error]
|
|
34
|
+
def raise_error!
|
|
35
|
+
ex = error
|
|
36
|
+
@tracer.on_request_error(ex:, span:)
|
|
37
|
+
ensure
|
|
38
|
+
raise(ex) if ex
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [LLM::Error]
|
|
45
|
+
def error
|
|
46
|
+
message = extract_message
|
|
47
|
+
case res
|
|
48
|
+
when Net::HTTPServerError
|
|
49
|
+
LLM::ServerError.new(message).tap { _1.response = res }
|
|
50
|
+
when Net::HTTPUnauthorized
|
|
51
|
+
LLM::UnauthorizedError.new(message).tap { _1.response = res }
|
|
52
|
+
when Net::HTTPForbidden
|
|
53
|
+
LLM::UnauthorizedError.new(message).tap { _1.response = res }
|
|
54
|
+
when Net::HTTPTooManyRequests
|
|
55
|
+
LLM::RateLimitError.new(message).tap { _1.response = res }
|
|
56
|
+
when Net::HTTPNotFound
|
|
57
|
+
LLM::Error.new("Bedrock model not found: #{message}").tap { _1.response = res }
|
|
58
|
+
else
|
|
59
|
+
LLM::Error.new(message).tap { _1.response = res }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @return [String]
|
|
65
|
+
def extract_message
|
|
66
|
+
body = parse_body
|
|
67
|
+
body["message"] || body["Message"] || body["__type"] || "Unexpected error"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# @return [Hash]
|
|
72
|
+
def parse_body
|
|
73
|
+
return {} if res.body.nil? || res.body.empty?
|
|
74
|
+
parsed = LLM.json.load(res.body.dup.force_encoding(Encoding::UTF_8).scrub)
|
|
75
|
+
Hash === parsed ? parsed : {}
|
|
76
|
+
rescue *LLM.json.parser_error
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Bedrock
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Bedrock::Models} class provides a model object for
|
|
6
|
+
# interacting with [AWS Bedrock's ListFoundationModels API](
|
|
7
|
+
# https://docs.aws.amazon.com/bedrock/latest/APIReference/API_ListFoundationModels.html).
|
|
8
|
+
#
|
|
9
|
+
# Unlike the Converse API (which lives on `bedrock-runtime.<region>.amazonaws.com`),
|
|
10
|
+
# the models endpoint lives on the control plane at
|
|
11
|
+
# `bedrock.<region>.amazonaws.com`. This class manages its own HTTP
|
|
12
|
+
# connection since the provider's transport is pinned to the runtime host.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# llm = LLM.bedrock(
|
|
16
|
+
# access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
|
17
|
+
# secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
|
18
|
+
# region: "us-east-1"
|
|
19
|
+
# )
|
|
20
|
+
# llm.models.all.each { |m| puts m.id }
|
|
21
|
+
class Models
|
|
22
|
+
##
|
|
23
|
+
# @param [LLM::Bedrock] provider
|
|
24
|
+
# @return [LLM::Bedrock::Models]
|
|
25
|
+
def initialize(provider)
|
|
26
|
+
@provider = provider
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# List all foundation models available in the configured region.
|
|
31
|
+
#
|
|
32
|
+
# @note
|
|
33
|
+
# This calls AWS Bedrock's ListFoundationModels API which returns
|
|
34
|
+
# all models available in the region, not just the ones the
|
|
35
|
+
# current account is subscribed to.
|
|
36
|
+
#
|
|
37
|
+
# @param [Hash] params Optional query parameters
|
|
38
|
+
# (e.g. `byProvider: "Anthropic"`, `byInferenceType: "ON_DEMAND"`)
|
|
39
|
+
# @return [LLM::Response]
|
|
40
|
+
def all(**params)
|
|
41
|
+
host = credentials.host
|
|
42
|
+
handle_response http(host).request(build_request(host, params))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# @param [String] host
|
|
49
|
+
# @return [Net::HTTP]
|
|
50
|
+
def http(host)
|
|
51
|
+
http = Net::HTTP.new(host, 443)
|
|
52
|
+
http.use_ssl = true
|
|
53
|
+
http.read_timeout = timeout
|
|
54
|
+
http
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# @param [String] host
|
|
59
|
+
# @param [Hash] params
|
|
60
|
+
# @return [Net::HTTP::Get]
|
|
61
|
+
def build_request(host, params)
|
|
62
|
+
path = "/foundation-models"
|
|
63
|
+
query = URI.encode_www_form(params) unless params.empty?
|
|
64
|
+
path = "#{path}?#{query}" if query && !query.empty?
|
|
65
|
+
body = ""
|
|
66
|
+
req = Net::HTTP::Get.new(path, {"Content-Type" => "application/json", "Accept" => "application/json"})
|
|
67
|
+
req.tap { sign!(req, body, host, query) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# @param [Net::HTTPResponse] res
|
|
72
|
+
# @return [LLM::Response]
|
|
73
|
+
# @raise [LLM::Error]
|
|
74
|
+
def handle_response(res)
|
|
75
|
+
case res
|
|
76
|
+
when Net::HTTPSuccess
|
|
77
|
+
res.body = LLM::Object.from(LLM.json.load(res.body || "{}"))
|
|
78
|
+
LLM::Bedrock::ResponseAdapter.adapt(res, type: :models)
|
|
79
|
+
else
|
|
80
|
+
body = +""
|
|
81
|
+
res.read_body { body << _1 } if res.body.nil?
|
|
82
|
+
LLM::Bedrock::ErrorHandler.new(tracer, nil, res).raise_error!
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# @param [Net::HTTPRequest] req
|
|
88
|
+
# @param [String] body
|
|
89
|
+
# @param [String] host
|
|
90
|
+
# @param [String, nil] query
|
|
91
|
+
# @return [Net::HTTPRequest]
|
|
92
|
+
def sign!(req, body, host = credentials.host, query = nil)
|
|
93
|
+
creds = credentials.tap { _1.host = host }
|
|
94
|
+
Signature.new(credentials: creds, method: "GET", path: "/foundation-models", query:, body:).sign!(req)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# @return [LLM::Object]
|
|
99
|
+
def credentials
|
|
100
|
+
LLM::Object.from(@provider.send(:credentials).to_h).tap do
|
|
101
|
+
_1.host = "bedrock.#{_1.aws_region}.amazonaws.com"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[:timeout, :tracer].each do |m|
|
|
106
|
+
define_method(m) { @provider.send(m) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|