llm.rb 8.0.0 → 9.0.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +165 -2
  3. data/README.md +161 -509
  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_llm.rb +7 -8
  10. data/lib/llm/agent.rb +36 -16
  11. data/lib/llm/context.rb +30 -26
  12. data/lib/llm/contract/completion.rb +45 -0
  13. data/lib/llm/cost.rb +81 -4
  14. data/lib/llm/error.rb +1 -1
  15. data/lib/llm/function/array.rb +8 -5
  16. data/lib/llm/function/call_group.rb +39 -0
  17. data/lib/llm/function/fork/task.rb +6 -0
  18. data/lib/llm/function/ractor/task.rb +6 -0
  19. data/lib/llm/function/task.rb +10 -0
  20. data/lib/llm/function.rb +1 -0
  21. data/lib/llm/mcp/transport/http.rb +26 -46
  22. data/lib/llm/mcp/transport/stdio.rb +0 -8
  23. data/lib/llm/mcp.rb +6 -23
  24. data/lib/llm/object.rb +8 -0
  25. data/lib/llm/provider.rb +29 -19
  26. data/lib/llm/providers/anthropic/error_handler.rb +6 -7
  27. data/lib/llm/providers/anthropic/files.rb +2 -2
  28. data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
  29. data/lib/llm/providers/anthropic.rb +1 -1
  30. data/lib/llm/providers/bedrock/error_handler.rb +79 -0
  31. data/lib/llm/providers/bedrock/models.rb +109 -0
  32. data/lib/llm/providers/bedrock/request_adapter/completion.rb +153 -0
  33. data/lib/llm/providers/bedrock/request_adapter.rb +95 -0
  34. data/lib/llm/providers/bedrock/response_adapter/completion.rb +173 -0
  35. data/lib/llm/providers/bedrock/response_adapter/models.rb +34 -0
  36. data/lib/llm/providers/bedrock/response_adapter.rb +40 -0
  37. data/lib/llm/providers/bedrock/signature.rb +166 -0
  38. data/lib/llm/providers/bedrock/stream_decoder.rb +140 -0
  39. data/lib/llm/providers/bedrock/stream_parser.rb +201 -0
  40. data/lib/llm/providers/bedrock.rb +272 -0
  41. data/lib/llm/providers/google/error_handler.rb +6 -7
  42. data/lib/llm/providers/google/files.rb +2 -4
  43. data/lib/llm/providers/google/images.rb +1 -1
  44. data/lib/llm/providers/google/models.rb +0 -2
  45. data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
  46. data/lib/llm/providers/google.rb +1 -1
  47. data/lib/llm/providers/ollama/error_handler.rb +6 -7
  48. data/lib/llm/providers/ollama/models.rb +0 -2
  49. data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
  50. data/lib/llm/providers/ollama.rb +1 -1
  51. data/lib/llm/providers/openai/audio.rb +3 -3
  52. data/lib/llm/providers/openai/error_handler.rb +6 -7
  53. data/lib/llm/providers/openai/files.rb +2 -2
  54. data/lib/llm/providers/openai/images.rb +3 -3
  55. data/lib/llm/providers/openai/models.rb +1 -1
  56. data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
  57. data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
  58. data/lib/llm/providers/openai/responses.rb +2 -2
  59. data/lib/llm/providers/openai/vector_stores.rb +1 -1
  60. data/lib/llm/providers/openai.rb +1 -1
  61. data/lib/llm/response.rb +10 -8
  62. data/lib/llm/sequel/plugin.rb +7 -8
  63. data/lib/llm/stream/queue.rb +15 -42
  64. data/lib/llm/stream.rb +4 -4
  65. data/lib/llm/transport/execution.rb +67 -0
  66. data/lib/llm/transport/http.rb +134 -0
  67. data/lib/llm/transport/persistent_http.rb +152 -0
  68. data/lib/llm/transport/response/http.rb +113 -0
  69. data/lib/llm/transport/response.rb +112 -0
  70. data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
  71. data/lib/llm/transport.rb +139 -0
  72. data/lib/llm/usage.rb +14 -5
  73. data/lib/llm/version.rb +1 -1
  74. data/lib/llm.rb +10 -12
  75. data/llm.gemspec +2 -16
  76. metadata +23 -19
  77. data/lib/llm/provider/transport/http/execution.rb +0 -115
  78. data/lib/llm/provider/transport/http/interruptible.rb +0 -114
  79. data/lib/llm/provider/transport/http.rb +0 -145
  80. data/lib/llm/utils.rb +0 -19
data/lib/llm/function.rb CHANGED
@@ -32,6 +32,7 @@ class LLM::Function
32
32
  require_relative "function/registry"
33
33
  require_relative "function/tracing"
34
34
  require_relative "function/array"
35
+ require_relative "function/call_group"
35
36
  require_relative "function/task"
36
37
  require_relative "function/thread_group"
37
38
  require_relative "function/fiber_group"
@@ -16,12 +16,13 @@ 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 [LLM::Transport, Class, nil] transport
20
+ # Optional override with any {LLM::Transport} instance or subclass
19
21
  # @return [LLM::MCP::Transport::HTTP]
20
- def initialize(url:, headers: {}, timeout: nil)
22
+ def initialize(url:, headers: {}, timeout: nil, transport: nil)
21
23
  @uri = URI.parse(url)
22
- @use_ssl = @uri.scheme == "https"
23
24
  @headers = headers
24
- @timeout = timeout
25
+ @transport = resolve_transport(transport, timeout:)
25
26
  @queue = []
26
27
  @monitor = Monitor.new
27
28
  @running = false
@@ -61,21 +62,11 @@ module LLM::MCP::Transport
61
62
  # @return [void]
62
63
  def write(message)
63
64
  raise LLM::MCP::Error, "MCP transport is not running" unless running?
64
- req = Net::HTTP::Post.new(uri.path, headers.merge("content-type" => "application/json"))
65
+ req = Net::HTTP::Post.new(uri.request_uri, headers.merge("content-type" => "application/json"))
65
66
  req.body = LLM.json.dump(message)
66
- if persistent_client.nil?
67
- http = Net::HTTP.start(uri.host, uri.port, use_ssl:, open_timeout: timeout, read_timeout: timeout)
68
- args = [req]
69
- else
70
- http = persistent_client
71
- args = [uri, req]
72
- end
73
- http.request(*args) do |res|
74
- unless Net::HTTPSuccess === res
75
- raise LLM::MCP::Error, "MCP transport write failed with HTTP #{res.code}"
76
- end
77
- read(res)
78
- end
67
+ res = transport.request(req, owner: self) { consume(_1) }
68
+ res = LLM::Transport::Response.from(res)
69
+ raise LLM::MCP::Error, "MCP transport write failed with HTTP #{res.code}" unless res.success?
79
70
  end
80
71
 
81
72
  ##
@@ -100,30 +91,27 @@ module LLM::MCP::Transport
100
91
  @running
101
92
  end
102
93
 
103
- ##
104
- # Configures the transport to use a persistent HTTP connection pool
105
- # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
106
- # @example
107
- # mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
108
- # # do something with 'mcp'
109
- # @return [LLM::MCP::Transport::HTTP]
110
- def persist!
111
- LLM.lock(:mcp) do
112
- LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
113
- unless LLM::MCP.clients.key?(key)
114
- http = Net::HTTP::Persistent.new(name: self.class.name)
115
- http.read_timeout = timeout
116
- http.open_timeout = timeout
117
- LLM::MCP.clients[key] ||= http
118
- end
119
- end
120
- self
94
+ private
95
+
96
+ attr_reader :uri, :headers, :transport
97
+
98
+ def consume(res)
99
+ res = LLM::Transport::Response.from(res)
100
+ read(res)
101
+ res
121
102
  end
122
- alias_method :persistent, :persist!
123
103
 
124
- private
104
+ def resolve_transport(transport, timeout:)
105
+ return default_transport(timeout:) if transport.nil?
106
+ if Class === transport && transport <= LLM::Transport
107
+ return transport.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
108
+ end
109
+ transport
110
+ end
125
111
 
126
- attr_reader :uri, :use_ssl, :headers, :timeout
112
+ def default_transport(timeout:)
113
+ LLM::Transport::HTTP.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
114
+ end
127
115
 
128
116
  def read(res)
129
117
  if res["content-type"].to_s.include?("text/event-stream")
@@ -142,14 +130,6 @@ module LLM::MCP::Transport
142
130
  lock { @queue << message }
143
131
  end
144
132
 
145
- def persistent_client
146
- LLM::MCP.clients[key]
147
- end
148
-
149
- def key
150
- "#{uri.scheme}:#{uri.host}:#{uri.port}:#{timeout}"
151
- end
152
-
153
133
  def lock(&)
154
134
  @monitor.synchronize(&)
155
135
  end
@@ -78,14 +78,6 @@ module LLM::MCP::Transport
78
78
  command.wait
79
79
  end
80
80
 
81
- ##
82
- # This method is a no-op for stdio transports
83
- # @return [LLM::MCP::Transport::Stdio]
84
- def persist!
85
- self
86
- end
87
- alias_method :persistent, :persist!
88
-
89
81
  private
90
82
 
91
83
  attr_reader :command, :stdin, :stdout, :stderr
data/lib/llm/mcp.rb CHANGED
@@ -24,14 +24,6 @@ class LLM::MCP
24
24
 
25
25
  include RPC
26
26
 
27
- @clients = {}
28
-
29
- ##
30
- # @api private
31
- def self.clients
32
- @clients
33
- end
34
-
35
27
  ##
36
28
  # Builds an MCP client that uses the stdio transport.
37
29
  # @param [LLM::Provider, nil] llm
@@ -69,6 +61,9 @@ class LLM::MCP
69
61
  # The URL for the MCP HTTP endpoint
70
62
  # @option http [Hash] :headers
71
63
  # Extra headers for requests
64
+ # @option http [LLM::Transport, Class] :transport
65
+ # Optional override with any {LLM::Transport} instance or subclass,
66
+ # similar to {LLM::Provider}
72
67
  # @param [Integer] timeout
73
68
  # The maximum amount of time to wait when reading from an MCP process
74
69
  # @return [LLM::MCP] A new MCP instance
@@ -82,8 +77,9 @@ class LLM::MCP
82
77
  @transport = Transport::Stdio.new(command:)
83
78
  elsif http
84
79
  persistent = http.delete(:persistent)
85
- @transport = Transport::HTTP.new(**http, timeout:)
86
- @transport.persistent if persistent
80
+ transport = http.delete(:transport)
81
+ transport ||= LLM::Transport::PersistentHTTP if persistent
82
+ @transport = Transport::HTTP.new(**http, timeout:, transport:)
87
83
  else
88
84
  raise ArgumentError, "stdio or http is required"
89
85
  end
@@ -121,19 +117,6 @@ class LLM::MCP
121
117
  stop
122
118
  end
123
119
 
124
- ##
125
- # Configures an HTTP MCP transport to use a persistent connection pool
126
- # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
127
- # @example
128
- # mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
129
- # # do something with 'mcp'
130
- # @return [LLM::MCP]
131
- def persist!
132
- transport.persist!
133
- self
134
- end
135
- alias_method :persistent, :persist!
136
-
137
120
  ##
138
121
  # Returns the tools provided by the MCP process.
139
122
  # @return [Array<Class<LLM::Tool>>]
data/lib/llm/object.rb CHANGED
@@ -60,6 +60,14 @@ class LLM::Object < BasicObject
60
60
  @h.each(&)
61
61
  end
62
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
+
63
71
  ##
64
72
  # @param [Symbol, #to_sym] k
65
73
  # @return [Object]
data/lib/llm/provider.rb CHANGED
@@ -6,10 +6,7 @@
6
6
  #
7
7
  # @abstract
8
8
  class LLM::Provider
9
- require "net/http"
10
- require_relative "provider/transport/http"
11
- require_relative "provider/transport/http/execution"
12
- include Transport::HTTP::Execution
9
+ include LLM::Transport::Execution
13
10
 
14
11
  ##
15
12
  # @param [String, nil] key
@@ -27,7 +24,9 @@ class LLM::Provider
27
24
  # @param [Boolean] persistent
28
25
  # Whether to use a persistent connection.
29
26
  # Requires the net-http-persistent gem.
30
- def initialize(key:, host:, port: 443, timeout: 60, ssl: true, base_path: "", persistent: false)
27
+ # @param [LLM::Transport, Class, nil] transport
28
+ # Optional override with any {LLM::Transport} instance or subclass.
29
+ def initialize(key:, host:, port: 443, timeout: 60, ssl: true, base_path: "", persistent: false, transport: nil)
31
30
  @key = key
32
31
  @host = host
33
32
  @port = port
@@ -36,7 +35,7 @@ class LLM::Provider
36
35
  @base_path = normalize_base_path(base_path)
37
36
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
38
37
  @headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
39
- @transport = Transport::HTTP.new(host:, port:, timeout:, ssl:, persistent:)
38
+ @transport = resolve_transport(transport, persistent:)
40
39
  @monitor = Monitor.new
41
40
  end
42
41
 
@@ -316,19 +315,6 @@ class LLM::Provider
316
315
  end
317
316
  end
318
317
 
319
- ##
320
- # This method configures a provider to use a persistent connection pool
321
- # via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
322
- # @example
323
- # llm = LLM.openai(key: ENV["KEY"]).persistent
324
- # # do something with 'llm'
325
- # @return [LLM::Provider]
326
- def persist!
327
- transport.persist!
328
- self
329
- end
330
- alias_method :persistent, :persist!
331
-
332
318
  ##
333
319
  # Interrupt the active request, if any.
334
320
  # @param [Fiber] owner
@@ -399,6 +385,13 @@ class LLM::Provider
399
385
  raise NotImplementedError
400
386
  end
401
387
 
388
+ ##
389
+ # @return [Class]
390
+ # Returns the class responsible for decoding streamed response bodies
391
+ def stream_decoder
392
+ LLM::Transport::StreamDecoder
393
+ end
394
+
402
395
  ##
403
396
  # Resolves tools to their function representations
404
397
  # @param [Array<LLM::Function, LLM::Tool>] tools
@@ -424,6 +417,23 @@ class LLM::Provider
424
417
  @monitor.synchronize(&)
425
418
  end
426
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
+
427
437
  ##
428
438
  # @api private
429
439
  def thread
@@ -5,7 +5,7 @@ class LLM::Anthropic
5
5
  # @private
6
6
  class ErrorHandler
7
7
  ##
8
- # @return [Net::HTTPResponse]
8
+ # @return [LLM::Transport::Response]
9
9
  # Non-2XX response from the server
10
10
  attr_reader :res
11
11
 
@@ -19,13 +19,13 @@ class LLM::Anthropic
19
19
  # The tracer
20
20
  # @param [Object, nil] span
21
21
  # The span
22
- # @param [Net::HTTPResponse] res
22
+ # @param [LLM::Transport::Response, Net::HTTPResponse] res
23
23
  # The response from the server
24
24
  # @return [LLM::Anthropic::ErrorHandler]
25
25
  def initialize(tracer, span, res)
26
26
  @tracer = tracer
27
27
  @span = span
28
- @res = res
28
+ @res = LLM::Transport::Response.from(res)
29
29
  end
30
30
 
31
31
  ##
@@ -43,12 +43,11 @@ class LLM::Anthropic
43
43
  ##
44
44
  # @return [LLM::Error]
45
45
  def error
46
- case res
47
- when Net::HTTPServerError
46
+ if res.server_error?
48
47
  LLM::ServerError.new("Server error").tap { _1.response = res }
49
- when Net::HTTPUnauthorized
48
+ elsif res.unauthorized?
50
49
  LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
51
- when Net::HTTPTooManyRequests
50
+ elsif res.rate_limited?
52
51
  LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
53
52
  else
54
53
  LLM::Error.new("Unexpected response").tap { _1.response = res }
@@ -58,7 +58,7 @@ class LLM::Anthropic
58
58
  multi = LLM::Multipart.new(params.merge!(file: LLM.File(file)))
59
59
  req = Net::HTTP::Post.new("/v1/files", headers)
60
60
  req["content-type"] = multi.content_type
61
- set_body_stream(req, multi.body)
61
+ transport.set_body_stream(req, multi.body)
62
62
  res, span, tracer = execute(request: req, operation: "request")
63
63
  res = ResponseAdapter.adapt(res, type: :file)
64
64
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -159,7 +159,7 @@ class LLM::Anthropic
159
159
  @provider.instance_variable_get(:@key)
160
160
  end
161
161
 
162
- [:headers, :execute, :set_body_stream].each do |m|
162
+ [:headers, :execute, :transport].each do |m|
163
163
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
164
164
  end
165
165
  end
@@ -27,6 +27,36 @@ module LLM::Anthropic::ResponseAdapter
27
27
  0
28
28
  end
29
29
 
30
+ ##
31
+ # (see LLM::Contract::Completion#input_audio_tokens)
32
+ def input_audio_tokens
33
+ super
34
+ end
35
+
36
+ ##
37
+ # (see LLM::Contract::Completion#output_audio_tokens)
38
+ def output_audio_tokens
39
+ super
40
+ end
41
+
42
+ ##
43
+ # (see LLM::Contract::Completion#input_image_tokens)
44
+ def input_image_tokens
45
+ super
46
+ end
47
+
48
+ ##
49
+ # (see LLM::Contract::Completion#cache_read_tokens)
50
+ def cache_read_tokens
51
+ body.usage&.cache_read_input_tokens || 0
52
+ end
53
+
54
+ ##
55
+ # (see LLM::Contract::Completion#cache_write_tokens)
56
+ def cache_write_tokens
57
+ body.usage&.cache_creation_input_tokens || 0
58
+ end
59
+
30
60
  ##
31
61
  # (see LLM::Contract::Completion#total_tokens)
32
62
  def total_tokens
@@ -161,7 +161,7 @@ module LLM
161
161
  payload = adapt(messages)
162
162
  body = LLM.json.dump(payload.merge!(params))
163
163
  req = Net::HTTP::Post.new("/v1/messages", headers)
164
- set_body_stream(req, StringIO.new(body))
164
+ transport.set_body_stream(req, StringIO.new(body))
165
165
  req
166
166
  end
167
167
 
@@ -0,0 +1,79 @@
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 [LLM::Transport::Response]
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 [LLM::Transport::Response, Net::HTTPResponse] res
25
+ # @return [LLM::Bedrock::ErrorHandler]
26
+ def initialize(tracer, span, res)
27
+ @tracer = tracer
28
+ @span = span
29
+ @res = LLM::Transport::Response.from(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
+ if res.server_error?
48
+ LLM::ServerError.new(message).tap { _1.response = res }
49
+ elsif res.unauthorized?
50
+ LLM::UnauthorizedError.new(message).tap { _1.response = res }
51
+ elsif res.forbidden?
52
+ LLM::UnauthorizedError.new(message).tap { _1.response = res }
53
+ elsif res.rate_limited?
54
+ LLM::RateLimitError.new(message).tap { _1.response = res }
55
+ elsif res.not_found?
56
+ LLM::Error.new("Bedrock model not found: #{message}").tap { _1.response = res }
57
+ else
58
+ LLM::Error.new(message).tap { _1.response = res }
59
+ end
60
+ end
61
+
62
+ ##
63
+ # @return [String]
64
+ def extract_message
65
+ body = parse_body
66
+ body["message"] || body["Message"] || body["__type"] || "Unexpected error"
67
+ end
68
+
69
+ ##
70
+ # @return [Hash]
71
+ def parse_body
72
+ return {} if res.body.nil? || res.body.empty?
73
+ parsed = LLM.json.load(res.body.dup.force_encoding(Encoding::UTF_8).scrub)
74
+ Hash === parsed ? parsed : {}
75
+ rescue *LLM.json.parser_error
76
+ {}
77
+ end
78
+ end
79
+ 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 builds a matching
12
+ # transport for the control-plane host from the provider's current
13
+ # transport class.
14
+ #
15
+ # @example
16
+ # llm = LLM.bedrock(
17
+ # access_key_id: ENV["AWS_ACCESS_KEY_ID"],
18
+ # secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
19
+ # region: "us-east-1"
20
+ # )
21
+ # llm.models.all.each { |m| puts m.id }
22
+ class Models
23
+ ##
24
+ # @param [LLM::Bedrock] provider
25
+ # @return [LLM::Bedrock::Models]
26
+ def initialize(provider)
27
+ @provider = provider
28
+ end
29
+
30
+ ##
31
+ # List all foundation models available in the configured region.
32
+ #
33
+ # @note
34
+ # This calls AWS Bedrock's ListFoundationModels API which returns
35
+ # all models available in the region, not just the ones the
36
+ # current account is subscribed to.
37
+ #
38
+ # @param [Hash] params Optional query parameters
39
+ # (e.g. `byProvider: "Anthropic"`, `byInferenceType: "ON_DEMAND"`)
40
+ # @return [LLM::Response]
41
+ def all(**params)
42
+ host = credentials.host
43
+ req = build_request(host, params)
44
+ res = build_transport(host).request(req, owner: self)
45
+ handle_response(res)
46
+ end
47
+
48
+ private
49
+
50
+ ##
51
+ # @param [String] host
52
+ # @return [LLM::Transport]
53
+ def build_transport(host)
54
+ transport.class.new(host:, port: 443, timeout:, ssl: true)
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 [LLM::Transport::Response, Net::HTTPResponse] res
72
+ # @return [LLM::Response]
73
+ # @raise [LLM::Error]
74
+ def handle_response(res)
75
+ res = LLM::Transport::Response.from(res)
76
+ if res.success?
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, :transport].each do |m|
106
+ define_method(m) { @provider.send(m) }
107
+ end
108
+ end
109
+ end