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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +165 -2
- data/README.md +161 -509
- 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_llm.rb +7 -8
- data/lib/llm/agent.rb +36 -16
- data/lib/llm/context.rb +30 -26
- data/lib/llm/contract/completion.rb +45 -0
- data/lib/llm/cost.rb +81 -4
- data/lib/llm/error.rb +1 -1
- data/lib/llm/function/array.rb +8 -5
- data/lib/llm/function/call_group.rb +39 -0
- data/lib/llm/function/fork/task.rb +6 -0
- data/lib/llm/function/ractor/task.rb +6 -0
- data/lib/llm/function/task.rb +10 -0
- data/lib/llm/function.rb +1 -0
- data/lib/llm/mcp/transport/http.rb +26 -46
- data/lib/llm/mcp/transport/stdio.rb +0 -8
- data/lib/llm/mcp.rb +6 -23
- data/lib/llm/object.rb +8 -0
- data/lib/llm/provider.rb +29 -19
- data/lib/llm/providers/anthropic/error_handler.rb +6 -7
- data/lib/llm/providers/anthropic/files.rb +2 -2
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/error_handler.rb +79 -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 +173 -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/providers/google/error_handler.rb +6 -7
- data/lib/llm/providers/google/files.rb +2 -4
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +0 -2
- data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama/error_handler.rb +6 -7
- data/lib/llm/providers/ollama/models.rb +0 -2
- data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/error_handler.rb +6 -7
- data/lib/llm/providers/openai/files.rb +2 -2
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
- data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
- data/lib/llm/providers/openai/responses.rb +2 -2
- data/lib/llm/providers/openai/vector_stores.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/response.rb +10 -8
- data/lib/llm/sequel/plugin.rb +7 -8
- data/lib/llm/stream/queue.rb +15 -42
- data/lib/llm/stream.rb +4 -4
- data/lib/llm/transport/execution.rb +67 -0
- data/lib/llm/transport/http.rb +134 -0
- data/lib/llm/transport/persistent_http.rb +152 -0
- data/lib/llm/transport/response/http.rb +113 -0
- data/lib/llm/transport/response.rb +112 -0
- data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
- data/lib/llm/transport.rb +139 -0
- data/lib/llm/usage.rb +14 -5
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +10 -12
- data/llm.gemspec +2 -16
- metadata +23 -19
- data/lib/llm/provider/transport/http/execution.rb +0 -115
- data/lib/llm/provider/transport/http/interruptible.rb +0 -114
- data/lib/llm/provider/transport/http.rb +0 -145
- 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
|
-
@
|
|
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.
|
|
65
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers.merge("content-type" => "application/json"))
|
|
65
66
|
req.body = LLM.json.dump(message)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
47
|
-
when Net::HTTPServerError
|
|
46
|
+
if res.server_error?
|
|
48
47
|
LLM::ServerError.new("Server error").tap { _1.response = res }
|
|
49
|
-
|
|
48
|
+
elsif res.unauthorized?
|
|
50
49
|
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
51
|
-
|
|
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, :
|
|
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
|