llm.rb 8.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.
data/data/deepseek.json CHANGED
@@ -29,13 +29,13 @@
29
29
  },
30
30
  "open_weights": true,
31
31
  "cost": {
32
- "input": 0.28,
33
- "output": 0.42,
32
+ "input": 0.14,
33
+ "output": 0.28,
34
34
  "cache_read": 0.028
35
35
  },
36
36
  "limit": {
37
- "context": 131072,
38
- "output": 8192
37
+ "context": 1000000,
38
+ "output": 384000
39
39
  }
40
40
  },
41
41
  "deepseek-reasoner": {
@@ -62,13 +62,13 @@
62
62
  },
63
63
  "open_weights": true,
64
64
  "cost": {
65
- "input": 0.28,
66
- "output": 0.42,
65
+ "input": 0.14,
66
+ "output": 0.28,
67
67
  "cache_read": 0.028
68
68
  },
69
69
  "limit": {
70
- "context": 128000,
71
- "output": 64000
70
+ "context": 1000000,
71
+ "output": 384000
72
72
  }
73
73
  },
74
74
  "deepseek-v4-flash": {
data/data/openai.json CHANGED
@@ -230,8 +230,8 @@
230
230
  },
231
231
  "limit": {
232
232
  "context": 1050000,
233
- "input": 920000,
234
- "output": 130000
233
+ "input": 922000,
234
+ "output": 128000
235
235
  },
236
236
  "experimental": {
237
237
  "modes": {
@@ -1554,6 +1554,43 @@
1554
1554
  "output": 0
1555
1555
  }
1556
1556
  },
1557
+ "gpt-5.5-pro": {
1558
+ "id": "gpt-5.5-pro",
1559
+ "name": "GPT-5.5 Pro",
1560
+ "family": "gpt-pro",
1561
+ "attachment": true,
1562
+ "reasoning": true,
1563
+ "tool_call": true,
1564
+ "structured_output": true,
1565
+ "temperature": false,
1566
+ "knowledge": "2025-12-01",
1567
+ "release_date": "2026-04-23",
1568
+ "last_updated": "2026-04-23",
1569
+ "modalities": {
1570
+ "input": [
1571
+ "text",
1572
+ "image",
1573
+ "pdf"
1574
+ ],
1575
+ "output": [
1576
+ "text"
1577
+ ]
1578
+ },
1579
+ "open_weights": false,
1580
+ "cost": {
1581
+ "input": 30,
1582
+ "output": 180,
1583
+ "context_over_200k": {
1584
+ "input": 60,
1585
+ "output": 270
1586
+ }
1587
+ },
1588
+ "limit": {
1589
+ "context": 1050000,
1590
+ "input": 922000,
1591
+ "output": 128000
1592
+ }
1593
+ },
1557
1594
  "gpt-4.1": {
1558
1595
  "id": "gpt-4.1",
1559
1596
  "name": "GPT-4.1",
data/data/xai.json CHANGED
@@ -68,6 +68,41 @@
68
68
  "output": 4096
69
69
  }
70
70
  },
71
+ "grok-4.3": {
72
+ "id": "grok-4.3",
73
+ "name": "Grok 4.3",
74
+ "family": "grok",
75
+ "attachment": true,
76
+ "reasoning": true,
77
+ "tool_call": true,
78
+ "temperature": true,
79
+ "release_date": "2026-05-01",
80
+ "last_updated": "2026-05-01",
81
+ "modalities": {
82
+ "input": [
83
+ "text",
84
+ "image"
85
+ ],
86
+ "output": [
87
+ "text"
88
+ ]
89
+ },
90
+ "open_weights": false,
91
+ "cost": {
92
+ "input": 1.25,
93
+ "output": 2.5,
94
+ "cache_read": 0.2,
95
+ "context_over_200k": {
96
+ "input": 2.5,
97
+ "output": 5,
98
+ "cache_read": 0.4
99
+ }
100
+ },
101
+ "limit": {
102
+ "context": 1000000,
103
+ "output": 30000
104
+ }
105
+ },
71
106
  "grok-3-mini-fast": {
72
107
  "id": "grok-3-mini-fast",
73
108
  "name": "Grok 3 Mini Fast",
data/data/zai.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "models": {
11
11
  "glm-5v-turbo": {
12
12
  "id": "glm-5v-turbo",
13
- "name": "glm-5v-turbo",
13
+ "name": "GLM-5V-Turbo",
14
14
  "family": "glm",
15
15
  "attachment": true,
16
16
  "reasoning": true,
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]
@@ -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
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
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Bedrock::RequestAdapter
4
+ ##
5
+ # Adapts a single message to Bedrock Converse content blocks.
6
+ #
7
+ # Bedrock Converse content blocks include:
8
+ # - {text: "..."}
9
+ # - {image: {format: "png", source: {bytes: "..."}}}
10
+ # - {document: {format: "pdf", name: "...", source: {bytes: "..."}}}
11
+ # - {toolUse: {toolUseId: "...", name: "...", input: {...}}}
12
+ # - {toolResult: {toolUseId: "...", content: [{text: "..."}]}}
13
+ #
14
+ # @api private
15
+ class Completion
16
+ ##
17
+ # @param [LLM::Message, Hash] message
18
+ def initialize(message)
19
+ @message = message
20
+ end
21
+
22
+ ##
23
+ # Adapts the message for the Bedrock Converse API
24
+ # @return [Hash, nil]
25
+ def adapt
26
+ catch(:abort) do
27
+ if Hash === message
28
+ {role: message[:role], content: adapt_content(message[:content])}
29
+ else
30
+ adapt_message
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def adapt_message
38
+ if message.tool_call?
39
+ blocks = [*adapt_tool_calls]
40
+ blocks.unshift(*adapt_content(content)) unless String === content && content.empty?
41
+ {role: "assistant", content: blocks}
42
+ else
43
+ {role: message.role, content: adapt_content(content)}
44
+ end
45
+ end
46
+
47
+ def adapt_tool_calls
48
+ message.extra[:tool_calls].filter_map do |tool|
49
+ next unless tool[:id] && tool[:name]
50
+ {
51
+ toolUse: {
52
+ toolUseId: tool[:id],
53
+ name: tool[:name],
54
+ input: parse_tool_input(tool[:arguments])
55
+ }
56
+ }
57
+ end
58
+ end
59
+
60
+ ##
61
+ # @param [String, Array, LLM::Object, LLM::Function::Return] content
62
+ # @return [Array<Hash>, nil]
63
+ def adapt_content(content)
64
+ case content
65
+ when Hash
66
+ content.empty? ? throw(:abort, nil) : [content]
67
+ when Array
68
+ content.empty? ? throw(:abort, nil) : content.flat_map { adapt_content(_1) }
69
+ when LLM::Object
70
+ adapt_object(content)
71
+ when String
72
+ [{text: content}]
73
+ when LLM::Response
74
+ adapt_remote_file(content)
75
+ when LLM::Message
76
+ adapt_content(content.content)
77
+ when LLM::Function::Return
78
+ [{toolResult: {toolUseId: content.id, content: [{text: LLM.json.dump(content.value)}]}}]
79
+ else
80
+ prompt_error!(content)
81
+ end
82
+ end
83
+
84
+ def adapt_object(object)
85
+ case object.kind
86
+ when :image_url
87
+ [{image: {format: detect_format(object.value.to_s),
88
+ source: {url: object.value.to_s}}}]
89
+ when :local_file
90
+ adapt_local_file(object.value)
91
+ when :remote_file
92
+ adapt_remote_file(object.value)
93
+ else
94
+ prompt_error!(object)
95
+ end
96
+ end
97
+
98
+ def adapt_local_file(file)
99
+ if file.image?
100
+ [{image: {format: file.format,
101
+ source: {bytes: file.to_b64}}}]
102
+ elsif file.pdf?
103
+ name = sanitize_name(file.basename)
104
+ [{document: {format: "pdf", name:,
105
+ source: {bytes: file.to_b64}}}]
106
+ else
107
+ raise LLM::PromptError,
108
+ "The #{file.class} is not an image or PDF, " \
109
+ "and not supported by the Bedrock API"
110
+ end
111
+ end
112
+
113
+ def adapt_remote_file(file)
114
+ prompt_error!(file) unless file.file?
115
+ [{file.file_type => {source: {file_id: file.id}}}]
116
+ end
117
+
118
+ def detect_format(url)
119
+ case url
120
+ when /\.png/i then "png"
121
+ when /\.jpe?g/i then "jpeg"
122
+ when /\.gif/i then "gif"
123
+ when /\.webp/i then "webp"
124
+ else "png"
125
+ end
126
+ end
127
+
128
+ def sanitize_name(name)
129
+ name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
130
+ end
131
+
132
+ def parse_tool_input(input)
133
+ case input
134
+ when Hash then input
135
+ when String
136
+ parsed = LLM.json.load(input)
137
+ Hash === parsed ? parsed : {}
138
+ when nil then {}
139
+ else input.respond_to?(:to_h) ? input.to_h : {}
140
+ end
141
+ rescue *LLM.json.parser_error
142
+ {}
143
+ end
144
+
145
+ def prompt_error!(content)
146
+ raise LLM::PromptError,
147
+ "#{content.class} is not supported by the Bedrock API"
148
+ end
149
+
150
+ def message = @message
151
+ def content = message.content
152
+ end
153
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Bedrock
4
+ ##
5
+ # Adapts llm.rb internal message format to Bedrock Converse API format.
6
+ #
7
+ # Bedrock Converse uses:
8
+ # - system: [{text: "..."}] (top-level, separate from messages)
9
+ # - messages: [{role: "user"|"assistant", content: [{...}, ...]}]
10
+ # - Content blocks: text, image, document, toolUse, toolResult
11
+ # - toolConfig: {tools: [{toolSpec: {name:, description:, inputSchema: {json: ...}}}]}
12
+ #
13
+ # @api private
14
+ module RequestAdapter
15
+ ##
16
+ # @param [Array<LLM::Message>] messages
17
+ # @return [Hash]
18
+ def adapt(messages, mode: nil)
19
+ payload = {messages: [], system: []}
20
+ messages.each do |message|
21
+ adapted = Completion.new(message).adapt
22
+ next if adapted.nil?
23
+ if system?(message)
24
+ payload[:system].concat Array(adapted[:content])
25
+ else
26
+ payload[:messages] << adapted
27
+ end
28
+ end
29
+ payload.delete(:system) if payload[:system].empty?
30
+ payload
31
+ end
32
+
33
+ private
34
+
35
+ ##
36
+ # @param [Hash] params
37
+ # @return [Hash]
38
+ def adapt_schema(params)
39
+ return {} unless params&.key?(:schema)
40
+ schema = params.delete(:schema)
41
+ schema = schema.respond_to?(:object) ? schema.object : schema
42
+ cleaned = schema.respond_to?(:to_h) ? schema.to_h : schema
43
+ [:strict, "strict", :$schema, "$schema"].each { cleaned.delete(_1) }
44
+ {
45
+ outputConfig: {
46
+ textFormat: {
47
+ type: "json_schema",
48
+ structure: {
49
+ jsonSchema: {
50
+ name: "response",
51
+ schema: LLM.json.dump(cleaned)
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ ##
60
+ # @param [Array<LLM::Function>] tools
61
+ # @return [Hash]
62
+ def adapt_tools(tools)
63
+ return {} unless tools&.any?
64
+ {toolConfig: {tools: tools.map { |t| adapt_tool(t) }}}
65
+ end
66
+
67
+ ##
68
+ # @param [LLM::Function] tool
69
+ # @return [Hash]
70
+ def adapt_tool(tool)
71
+ function = tool.respond_to?(:function) ? tool.function : tool
72
+ {
73
+ toolSpec: {
74
+ name: function.name,
75
+ description: function.description,
76
+ inputSchema: {
77
+ json: function.params || default_input_schema
78
+ }
79
+ }
80
+ }
81
+ end
82
+
83
+ def default_input_schema
84
+ {"type" => "object", "properties" => {}, "required" => []}
85
+ end
86
+
87
+ def system?(message)
88
+ if message.respond_to?(:system?)
89
+ message.system?
90
+ else
91
+ Hash === message && message[:role].to_s == "system"
92
+ end
93
+ end
94
+ end
95
+ end