ruby_llm 1.13.2 → 1.16.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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -13
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +41 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
  51. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
  52. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
  53. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +3 -3
  54. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  55. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  56. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  57. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  59. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  60. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
  61. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +5 -7
  64. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  65. data/lib/ruby_llm/active_record/acts_as.rb +6 -26
  66. data/lib/ruby_llm/active_record/acts_as_legacy.rb +125 -29
  67. data/lib/ruby_llm/active_record/chat_methods.rb +50 -26
  68. data/lib/ruby_llm/active_record/message_methods.rb +104 -4
  69. data/lib/ruby_llm/active_record/model_methods.rb +8 -10
  70. data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
  71. data/lib/ruby_llm/active_record/tool_call_methods.rb +18 -0
  72. data/lib/ruby_llm/agent.rb +15 -2
  73. data/lib/ruby_llm/aliases.json +141 -89
  74. data/lib/ruby_llm/aliases.rb +3 -0
  75. data/lib/ruby_llm/attachment.rb +44 -40
  76. data/lib/ruby_llm/chat.rb +229 -59
  77. data/lib/ruby_llm/configuration.rb +69 -75
  78. data/lib/ruby_llm/connection.rb +37 -10
  79. data/lib/ruby_llm/content.rb +15 -1
  80. data/lib/ruby_llm/cost.rb +224 -0
  81. data/lib/ruby_llm/deprecator.rb +24 -0
  82. data/lib/ruby_llm/embedding.rb +31 -1
  83. data/lib/ruby_llm/error.rb +16 -75
  84. data/lib/ruby_llm/error_middleware.rb +81 -0
  85. data/lib/ruby_llm/image.rb +39 -4
  86. data/lib/ruby_llm/instrumentation.rb +36 -0
  87. data/lib/ruby_llm/message.rb +20 -0
  88. data/lib/ruby_llm/mime_type.rb +25 -0
  89. data/lib/ruby_llm/model/info.rb +67 -14
  90. data/lib/ruby_llm/model/pricing.rb +19 -9
  91. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  92. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  93. data/lib/ruby_llm/model_registry.rb +39 -0
  94. data/lib/ruby_llm/models.json +21796 -20601
  95. data/lib/ruby_llm/models.rb +107 -34
  96. data/lib/ruby_llm/models_schema.json +3 -0
  97. data/lib/ruby_llm/provider.rb +25 -4
  98. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
  99. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  100. data/lib/ruby_llm/providers/anthropic/models.rb +4 -8
  101. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  102. data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
  103. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  104. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  105. data/lib/ruby_llm/providers/azure.rb +4 -0
  106. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  107. data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
  108. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  109. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  110. data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
  111. data/lib/ruby_llm/providers/bedrock.rb +5 -1
  112. data/lib/ruby_llm/providers/deepseek/capabilities.rb +28 -103
  113. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  114. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  115. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
  116. data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
  117. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  118. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  119. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  120. data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
  121. data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
  122. data/lib/ruby_llm/providers/gemini.rb +4 -0
  123. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  124. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  125. data/lib/ruby_llm/providers/gpustack.rb +4 -0
  126. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
  127. data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
  128. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  129. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  130. data/lib/ruby_llm/providers/mistral.rb +5 -1
  131. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  132. data/lib/ruby_llm/providers/ollama.rb +4 -0
  133. data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
  134. data/lib/ruby_llm/providers/openai/chat.rb +61 -7
  135. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  136. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  137. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  138. data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
  139. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  140. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  141. data/lib/ruby_llm/providers/openai.rb +10 -0
  142. data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
  143. data/lib/ruby_llm/providers/openrouter/images.rb +3 -3
  144. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  145. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  146. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  147. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  148. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  149. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  150. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  151. data/lib/ruby_llm/providers/perplexity.rb +5 -1
  152. data/lib/ruby_llm/providers/vertexai.rb +8 -0
  153. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  154. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  155. data/lib/ruby_llm/providers/xai.rb +5 -1
  156. data/lib/ruby_llm/railtie.rb +11 -1
  157. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  158. data/lib/ruby_llm/streaming.rb +4 -0
  159. data/lib/ruby_llm/tokens.rb +8 -0
  160. data/lib/ruby_llm/tool.rb +24 -7
  161. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  162. data/lib/ruby_llm/transcription.rb +2 -1
  163. data/lib/ruby_llm/utils.rb +39 -0
  164. data/lib/ruby_llm/version.rb +1 -1
  165. data/lib/ruby_llm.rb +11 -6
  166. data/lib/tasks/models.rake +45 -16
  167. data/lib/tasks/release.rake +51 -24
  168. data/lib/tasks/ruby_llm.rake +6 -5
  169. data/lib/tasks/vcr.rake +1 -1
  170. metadata +81 -21
  171. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
@@ -16,7 +16,7 @@ module RubyLLM
16
16
 
17
17
  content = []
18
18
 
19
- content << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
19
+ append_formatted_content(content, msg.content) unless msg.content.nil? || msg.content.empty?
20
20
 
21
21
  msg.tool_calls.each_value do |tool_call|
22
22
  content << format_tool_use_block(tool_call)
@@ -44,11 +44,23 @@ module RubyLLM
44
44
  }
45
45
  end
46
46
 
47
+ def append_formatted_content(content_blocks, content)
48
+ formatted_content = Media.format_content(content)
49
+ if formatted_content.is_a?(Array)
50
+ content_blocks.concat(formatted_content)
51
+ else
52
+ content_blocks << formatted_content
53
+ end
54
+ end
55
+
47
56
  def format_tool_result_block(msg)
57
+ content = msg.content
58
+ content = '(no output)' if content.nil? || (content.respond_to?(:empty?) && content.empty?)
59
+
48
60
  {
49
61
  type: 'tool_result',
50
62
  tool_use_id: msg.tool_call_id,
51
- content: Media.format_content(msg.content)
63
+ content: Media.format_content(content)
52
64
  }
53
65
  end
54
66
 
@@ -69,12 +81,29 @@ module RubyLLM
69
81
 
70
82
  def extract_tool_calls(data)
71
83
  if json_delta?(data)
72
- { nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
84
+ extract_tool_call_delta(data)
85
+ elsif content_block_start?(data)
86
+ extract_tool_call_start(data)
73
87
  else
74
88
  parse_tool_calls(data['content_block'])
75
89
  end
76
90
  end
77
91
 
92
+ def extract_tool_call_delta(data)
93
+ { data['index'] => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
94
+ end
95
+
96
+ def extract_tool_call_start(data)
97
+ tool_calls = parse_tool_calls(data['content_block'])
98
+ return tool_calls if tool_calls.nil? || data['index'].nil?
99
+
100
+ { data['index'] => tool_calls.values.first }
101
+ end
102
+
103
+ def content_block_start?(data)
104
+ data['type'] == 'content_block_start'
105
+ end
106
+
78
107
  def parse_tool_calls(content_blocks)
79
108
  return nil if content_blocks.nil?
80
109
 
@@ -27,6 +27,10 @@ module RubyLLM
27
27
  Anthropic::Capabilities
28
28
  end
29
29
 
30
+ def configuration_options
31
+ %i[anthropic_api_key anthropic_api_base]
32
+ end
33
+
30
34
  def configuration_requirements
31
35
  %i[anthropic_api_key]
32
36
  end
@@ -24,7 +24,7 @@ module RubyLLM
24
24
  when :text
25
25
  parts << OpenAI::Media.format_text_file(attachment)
26
26
  else
27
- raise UnsupportedAttachmentError, attachment.type
27
+ raise UnsupportedAttachmentError, attachment.mime_type
28
28
  end
29
29
  end
30
30
 
@@ -44,6 +44,10 @@ module RubyLLM
44
44
  end
45
45
 
46
46
  class << self
47
+ def configuration_options
48
+ %i[azure_api_base azure_api_key azure_ai_auth_token]
49
+ end
50
+
47
51
  def configuration_requirements
48
52
  %i[azure_api_base]
49
53
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'digest'
4
+ require 'json'
4
5
  require 'openssl'
5
6
 
6
7
  module RubyLLM
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class Bedrock
@@ -56,7 +58,7 @@ module RubyLLM
56
58
  content: parse_text_content(content_blocks),
57
59
  thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
58
60
  tool_calls: parse_tool_calls(content_blocks),
59
- input_tokens: usage['inputTokens'],
61
+ input_tokens: input_tokens(usage),
60
62
  output_tokens: usage['outputTokens'],
61
63
  cached_tokens: usage['cacheReadInputTokens'],
62
64
  cache_creation_tokens: usage['cacheWriteInputTokens'],
@@ -66,6 +68,13 @@ module RubyLLM
66
68
  )
67
69
  end
68
70
 
71
+ def input_tokens(usage)
72
+ input_tokens = usage['inputTokens']
73
+ return unless input_tokens
74
+
75
+ [input_tokens.to_i - usage['cacheReadInputTokens'].to_i - usage['cacheWriteInputTokens'].to_i, 0].max
76
+ end
77
+
69
78
  def render_messages(messages)
70
79
  rendered = []
71
80
  tool_result_blocks = []
@@ -154,19 +163,23 @@ module RubyLLM
154
163
 
155
164
  def render_tool_result_content(content)
156
165
  return render_raw_tool_result_content(content.value) if content.is_a?(RubyLLM::Content::Raw)
166
+ return [{ json: content }] if content.is_a?(Hash) || content.is_a?(Array)
167
+ return render_content_tool_result_content(content) if content.is_a?(RubyLLM::Content)
157
168
 
158
- if content.is_a?(Hash) || content.is_a?(Array)
159
- [{ json: content }]
160
- elsif content.is_a?(RubyLLM::Content)
161
- blocks = []
162
- blocks << { text: content.text } if content.text
163
- content.attachments.each do |attachment|
164
- blocks << { text: attachment.for_llm }
165
- end
166
- blocks
167
- else
168
- [{ text: content.to_s }]
169
- end
169
+ [text_tool_result_block(content)]
170
+ end
171
+
172
+ def render_content_tool_result_content(content)
173
+ blocks = []
174
+ blocks << text_tool_result_block(content.text) unless content.text.to_s.empty?
175
+ content.attachments.each { |attachment| blocks << text_tool_result_block(attachment.for_llm) }
176
+ blocks.empty? ? [text_tool_result_block(nil)] : blocks
177
+ end
178
+
179
+ def text_tool_result_block(text)
180
+ text = text.to_s
181
+ text = '(no output)' if text.empty?
182
+ { text: text }
170
183
  end
171
184
 
172
185
  def render_raw_tool_result_content(raw_value)
@@ -38,15 +38,17 @@ module RubyLLM
38
38
  case attachment.type
39
39
  when :image
40
40
  render_image_attachment(attachment)
41
- when :pdf
41
+ when :pdf, :document
42
42
  render_document_attachment(attachment, used_document_names:)
43
43
  when :text
44
- { text: attachment.for_llm }
44
+ render_text_attachment(attachment)
45
45
  else
46
46
  raise UnsupportedAttachmentError, attachment.mime_type
47
47
  end
48
48
  end
49
49
 
50
+ SUPPORTED_DOCUMENT_FORMATS = %w[pdf csv doc docx xls xlsx html txt md].freeze
51
+
50
52
  def render_image_attachment(attachment)
51
53
  {
52
54
  image: {
@@ -58,11 +60,19 @@ module RubyLLM
58
60
  }
59
61
  end
60
62
 
63
+ def render_text_attachment(attachment)
64
+ { text: attachment.for_llm }
65
+ end
66
+
61
67
  def render_document_attachment(attachment, used_document_names:)
68
+ format = document_format(attachment)
69
+
70
+ raise UnsupportedAttachmentError, attachment.mime_type unless supported_document_format?(attachment)
71
+
62
72
  document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
63
73
  {
64
74
  document: {
65
- format: attachment.format,
75
+ format: format,
66
76
  name: document_name,
67
77
  source: {
68
78
  bytes: attachment.encoded
@@ -71,6 +81,14 @@ module RubyLLM
71
81
  }
72
82
  end
73
83
 
84
+ def supported_document_format?(attachment)
85
+ SUPPORTED_DOCUMENT_FORMATS.include?(document_format(attachment))
86
+ end
87
+
88
+ def document_format(attachment)
89
+ attachment.extension || attachment.format
90
+ end
91
+
74
92
  def sanitize_document_name(filename)
75
93
  base = File.basename(filename.to_s, '.*')
76
94
  safe = base.gsub(/[^a-zA-Z0-9_-]/, '_')
@@ -10,7 +10,7 @@ module RubyLLM
10
10
  REGION_PREFIXES = %w[global us eu ap sa ca me af il].freeze
11
11
 
12
12
  def models_api_base
13
- "https://bedrock.#{bedrock_region}.amazonaws.com"
13
+ @config.bedrock_api_base || "https://bedrock.#{bedrock_region}.amazonaws.com"
14
14
  end
15
15
 
16
16
  def models_url
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
4
+ require 'faraday'
5
+ require 'json'
6
+
3
7
  module RubyLLM
4
8
  module Providers
5
9
  class Bedrock
@@ -158,7 +162,10 @@ module RubyLLM
158
162
  end
159
163
 
160
164
  def extract_input_tokens(metadata_usage, usage, message_usage)
161
- metadata_usage['inputTokens'] || usage['inputTokens'] || message_usage['input_tokens']
165
+ bedrock_usage = metadata_usage['inputTokens'] ? metadata_usage : usage
166
+ return Bedrock::Chat.input_tokens(bedrock_usage) if bedrock_usage['inputTokens']
167
+
168
+ message_usage['input_tokens']
162
169
  end
163
170
 
164
171
  def extract_output_tokens(metadata_usage, usage)
@@ -221,6 +228,7 @@ module RubyLLM
221
228
 
222
229
  reasoning_text = reasoning_content['reasoningText'] || {}
223
230
  return reasoning_text['text'] if reasoning_text['text']
231
+ return reasoning_content['text'] if reasoning_content['text']
224
232
  return event.dig('delta', 'thinking') if event.dig('delta', 'type') == 'thinking_delta'
225
233
 
226
234
  nil
@@ -241,6 +249,7 @@ module RubyLLM
241
249
  reasoning_content = delta['reasoningContent'] || {}
242
250
  reasoning_text = reasoning_content['reasoningText'] || {}
243
251
  return reasoning_text['signature'] if reasoning_text['signature']
252
+ return reasoning_content['signature'] if reasoning_content['signature']
244
253
  return event.dig('delta', 'signature') if event.dig('delta', 'type') == 'signature_delta'
245
254
 
246
255
  nil
@@ -11,7 +11,7 @@ module RubyLLM
11
11
  include Bedrock::Streaming
12
12
 
13
13
  def api_base
14
- "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
14
+ @config.bedrock_api_base || "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
15
15
  end
16
16
 
17
17
  def headers
@@ -53,6 +53,10 @@ module RubyLLM
53
53
  end
54
54
 
55
55
  class << self
56
+ def configuration_options
57
+ %i[bedrock_api_key bedrock_secret_key bedrock_region bedrock_session_token bedrock_api_base]
58
+ end
59
+
56
60
  def configuration_requirements
57
61
  %i[bedrock_api_key bedrock_secret_key bedrock_region]
58
62
  end
@@ -3,43 +3,22 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class DeepSeek
6
- # Determines capabilities and pricing for DeepSeek models
6
+ # Provider-level capability checks used outside the model registry.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def context_window_for(model_id)
11
- case model_id
12
- when /deepseek-(?:chat|reasoner)/ then 64_000
13
- else 32_768
14
- end
15
- end
16
-
17
- def max_tokens_for(model_id)
18
- case model_id
19
- when /deepseek-(?:chat|reasoner)/ then 8_192
20
- else 4_096
21
- end
22
- end
23
-
24
- def input_price_for(model_id)
25
- PRICES.dig(model_family(model_id), :input_miss) || default_input_price
26
- end
27
-
28
- def output_price_for(model_id)
29
- PRICES.dig(model_family(model_id), :output) || default_output_price
30
- end
31
-
32
- def cache_hit_price_for(model_id)
33
- PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
34
- end
35
-
36
- def supports_vision?(_model_id)
37
- false
38
- end
39
-
40
- def supports_functions?(model_id)
41
- model_id.match?(/deepseek-chat/)
42
- end
10
+ DEFAULT_CONTEXT_WINDOW = 1_000_000
11
+ DEFAULT_MAX_OUTPUT_TOKENS = 384_000
12
+ DEFAULT_PRICES = {
13
+ input: 0.14,
14
+ output: 0.28,
15
+ cache_read: 0.0028
16
+ }.freeze
17
+ PRO_PRICES = {
18
+ input: 0.435,
19
+ output: 0.87,
20
+ cache_read: 0.003625
21
+ }.freeze
43
22
 
44
23
  def supports_tool_choice?(_model_id)
45
24
  true
@@ -49,86 +28,32 @@ module RubyLLM
49
28
  false
50
29
  end
51
30
 
52
- def supports_json_mode?(_model_id)
53
- false
31
+ def context_window_for(_model_id)
32
+ DEFAULT_CONTEXT_WINDOW
54
33
  end
55
34
 
56
- def format_display_name(model_id)
57
- case model_id
58
- when 'deepseek-chat' then 'DeepSeek V3'
59
- when 'deepseek-reasoner' then 'DeepSeek R1'
60
- else
61
- model_id.split('-')
62
- .map(&:capitalize)
63
- .join(' ')
64
- end
35
+ def max_tokens_for(_model_id)
36
+ DEFAULT_MAX_OUTPUT_TOKENS
65
37
  end
66
38
 
67
- def model_type(_model_id)
68
- 'chat'
69
- end
70
-
71
- def model_family(model_id)
72
- case model_id
73
- when /deepseek-reasoner/ then :reasoner
74
- else :chat
75
- end
76
- end
77
-
78
- PRICES = {
79
- chat: {
80
- input_hit: 0.07,
81
- input_miss: 0.27,
82
- output: 1.10
83
- },
84
- reasoner: {
85
- input_hit: 0.14,
86
- input_miss: 0.55,
87
- output: 2.19
88
- }
89
- }.freeze
90
-
91
- def default_input_price
92
- 0.27
93
- end
94
-
95
- def default_output_price
96
- 1.10
97
- end
98
-
99
- def default_cache_hit_price
100
- 0.07
101
- end
102
-
103
- def modalities_for(_model_id)
104
- {
105
- input: ['text'],
106
- output: ['text']
107
- }
108
- end
109
-
110
- def capabilities_for(model_id)
111
- capabilities = ['streaming']
112
-
113
- capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
114
-
39
+ def critical_capabilities_for(model_id)
40
+ v4_model = model_id.start_with?('deepseek-v4-')
41
+ capabilities = ['function_calling']
42
+ capabilities << 'structured_output' if v4_model
43
+ capabilities << 'reasoning' if model_id == 'deepseek-reasoner' || v4_model
115
44
  capabilities
116
45
  end
117
46
 
118
47
  def pricing_for(model_id)
119
- family = model_family(model_id)
120
- prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
121
-
122
- standard_pricing = {
123
- input_per_million: prices[:input_miss],
124
- output_per_million: prices[:output]
125
- }
126
-
127
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
48
+ prices = model_id == 'deepseek-v4-pro' ? PRO_PRICES : DEFAULT_PRICES
128
49
 
129
50
  {
130
51
  text_tokens: {
131
- standard: standard_pricing
52
+ standard: {
53
+ input_per_million: prices[:input],
54
+ output_per_million: prices[:output],
55
+ cache_read_input_per_million: prices[:cache_read]
56
+ }
132
57
  }
133
58
  }
134
59
  end
@@ -10,6 +10,15 @@ module RubyLLM
10
10
  def format_role(role)
11
11
  role.to_s
12
12
  end
13
+
14
+ def format_content(content)
15
+ OpenAI::Media.format_content(
16
+ content,
17
+ document_attachments: :none,
18
+ image_attachments: false,
19
+ audio_attachments: false
20
+ )
21
+ end
13
22
  end
14
23
  end
15
24
  end
@@ -21,6 +21,10 @@ module RubyLLM
21
21
  DeepSeek::Capabilities
22
22
  end
23
23
 
24
+ def configuration_options
25
+ %i[deepseek_api_key deepseek_api_base]
26
+ end
27
+
24
28
  def configuration_requirements
25
29
  %i[deepseek_api_key]
26
30
  end