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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +151 -1
  3. data/README.md +45 -25
  4. data/data/bedrock.json +2948 -0
  5. data/data/deepseek.json +8 -8
  6. data/data/openai.json +39 -2
  7. data/data/xai.json +35 -0
  8. data/data/zai.json +1 -1
  9. data/lib/llm/active_record/acts_as_agent.rb +2 -6
  10. data/lib/llm/active_record/acts_as_llm.rb +4 -82
  11. data/lib/llm/active_record.rb +80 -2
  12. data/lib/llm/agent.rb +9 -4
  13. data/lib/llm/error.rb +4 -0
  14. data/lib/llm/function/array.rb +7 -3
  15. data/lib/llm/function/fiber_group.rb +9 -3
  16. data/lib/llm/function/fork/job.rb +67 -0
  17. data/lib/llm/function/fork/task.rb +76 -0
  18. data/lib/llm/function/fork.rb +8 -0
  19. data/lib/llm/function/fork_group.rb +36 -0
  20. data/lib/llm/function/ractor/task.rb +13 -3
  21. data/lib/llm/function/task.rb +10 -2
  22. data/lib/llm/function.rb +24 -11
  23. data/lib/llm/mcp/command.rb +1 -1
  24. data/lib/llm/mcp/transport/http.rb +2 -2
  25. data/lib/llm/mcp.rb +7 -4
  26. data/lib/llm/object/kernel.rb +8 -2
  27. data/lib/llm/object.rb +75 -21
  28. data/lib/llm/{mcp/pipe.rb → pipe.rb} +9 -8
  29. data/lib/llm/provider/transport/http/execution.rb +1 -1
  30. data/lib/llm/provider/transport/http.rb +1 -1
  31. data/lib/llm/provider.rb +7 -0
  32. data/lib/llm/providers/bedrock/error_handler.rb +80 -0
  33. data/lib/llm/providers/bedrock/models.rb +109 -0
  34. data/lib/llm/providers/bedrock/request_adapter/completion.rb +153 -0
  35. data/lib/llm/providers/bedrock/request_adapter.rb +95 -0
  36. data/lib/llm/providers/bedrock/response_adapter/completion.rb +143 -0
  37. data/lib/llm/providers/bedrock/response_adapter/models.rb +34 -0
  38. data/lib/llm/providers/bedrock/response_adapter.rb +40 -0
  39. data/lib/llm/providers/bedrock/signature.rb +166 -0
  40. data/lib/llm/providers/bedrock/stream_decoder.rb +140 -0
  41. data/lib/llm/providers/bedrock/stream_parser.rb +201 -0
  42. data/lib/llm/providers/bedrock.rb +272 -0
  43. data/lib/llm/stream/queue.rb +1 -1
  44. data/lib/llm/version.rb +1 -1
  45. data/lib/llm.rb +27 -1
  46. data/llm.gemspec +2 -1
  47. 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
- @mailbox = Ractor::Mailbox.new(build_task(runner_class, id, name, arguments))
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(runner_class, id, name, arguments)
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
@@ -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, and async tasks.
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
- task.resume if task.alive?
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
- # - `:fiber`: Use raw fibers
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.new do
226
- call!
227
- ensure
228
- Fiber.yield
229
- end.tap(&:resume)
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 = ((Class === @runner) ? @runner.new : @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
@@ -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.mcp(http: {url: "https://example.com/mcp"}).persistent
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
- @@clients = {}
27
+ @clients = {}
29
28
 
30
29
  ##
31
30
  # @api private
32
- def self.clients = @@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.mcp(http: {url: "https://example.com/mcp"}).persistent
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!
@@ -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
- to_h.to_json(...)
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.key?(key(k))
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.fetch(key(k), *args, &b)
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.dig(...)
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.slice(...)
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
- class LLM::MCP
3
+ module LLM
4
4
  ##
5
- # The {LLM::MCP::Pipe LLM::MCP::Pipe} class wraps a pair of IO
6
- # objects created by {IO.pipe}. It is used by
7
- # {LLM::MCP::Transport::Stdio LLM::MCP::Transport::Stdio} to manage
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
- # @return [LLM::MCP::Pipe]
24
- def initialize
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 = StreamDecoder.new(stream_parser.new(stream))
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
@@ -53,7 +53,7 @@ class LLM::Provider
53
53
  # @return [Object]
54
54
  def request_owner
55
55
  return Fiber.current unless defined?(::Async)
56
- Async::Task.current || Fiber.current
56
+ Async::Task.current? ? Async::Task.current : Fiber.current
57
57
  end
58
58
 
59
59
  ##
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