ruby_llm_community 1.2.0 → 1.3.1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -9
  3. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -67
  4. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +12 -12
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +7 -7
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  7. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +6 -6
  8. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +4 -4
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -5
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +5 -5
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +4 -4
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +8 -8
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +5 -5
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +9 -6
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -5
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +9 -9
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +4 -6
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +11 -11
  21. data/lib/generators/ruby_llm/generator_helpers.rb +152 -87
  22. data/lib/generators/ruby_llm/install/install_generator.rb +75 -79
  23. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  24. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +5 -0
  25. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +7 -1
  26. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  27. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +88 -85
  28. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  29. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  30. data/lib/ruby_llm/active_record/acts_as.rb +23 -16
  31. data/lib/ruby_llm/active_record/chat_methods.rb +41 -13
  32. data/lib/ruby_llm/active_record/message_methods.rb +11 -2
  33. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  34. data/lib/ruby_llm/aliases.json +61 -32
  35. data/lib/ruby_llm/attachment.rb +42 -11
  36. data/lib/ruby_llm/chat.rb +13 -2
  37. data/lib/ruby_llm/configuration.rb +6 -1
  38. data/lib/ruby_llm/connection.rb +4 -4
  39. data/lib/ruby_llm/content.rb +23 -0
  40. data/lib/ruby_llm/message.rb +17 -9
  41. data/lib/ruby_llm/model/info.rb +4 -0
  42. data/lib/ruby_llm/models.json +7157 -6089
  43. data/lib/ruby_llm/models.rb +14 -22
  44. data/lib/ruby_llm/provider.rb +27 -5
  45. data/lib/ruby_llm/providers/anthropic/chat.rb +18 -5
  46. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  47. data/lib/ruby_llm/providers/anthropic/media.rb +6 -5
  48. data/lib/ruby_llm/providers/anthropic/models.rb +9 -2
  49. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -18
  50. data/lib/ruby_llm/providers/bedrock/media.rb +2 -1
  51. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +9 -2
  52. data/lib/ruby_llm/providers/gemini/chat.rb +353 -72
  53. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  54. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  55. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  56. data/lib/ruby_llm/providers/gemini.rb +2 -1
  57. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  58. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  59. data/lib/ruby_llm/providers/openai/capabilities.rb +15 -7
  60. data/lib/ruby_llm/providers/openai/chat.rb +7 -3
  61. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  62. data/lib/ruby_llm/providers/openai/streaming.rb +7 -3
  63. data/lib/ruby_llm/providers/openai/tools.rb +34 -12
  64. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  65. data/lib/ruby_llm/providers/openai_base.rb +1 -0
  66. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  67. data/lib/ruby_llm/providers/vertexai.rb +11 -11
  68. data/lib/ruby_llm/railtie.rb +24 -22
  69. data/lib/ruby_llm/stream_accumulator.rb +8 -12
  70. data/lib/ruby_llm/tool.rb +126 -0
  71. data/lib/ruby_llm/transcription.rb +35 -0
  72. data/lib/ruby_llm/utils.rb +46 -0
  73. data/lib/ruby_llm/version.rb +1 -1
  74. data/lib/ruby_llm_community.rb +7 -1
  75. metadata +27 -3
@@ -10,16 +10,19 @@ module RubyLLM
10
10
  @instance ||= new
11
11
  end
12
12
 
13
- def provider_for(model)
14
- Provider.for(model)
13
+ def schema_file
14
+ File.expand_path('models_schema.json', __dir__)
15
15
  end
16
16
 
17
- def models_file
18
- File.expand_path('models.json', __dir__)
17
+ def load_models(file = RubyLLM.config.model_registry_file)
18
+ read_from_json(file)
19
19
  end
20
20
 
21
- def schema_file
22
- File.expand_path('models_schema.json', __dir__)
21
+ def read_from_json(file = RubyLLM.config.model_registry_file)
22
+ data = File.exist?(file) ? File.read(file) : '[]'
23
+ JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
24
+ rescue JSON::ParserError
25
+ []
23
26
  end
24
27
 
25
28
  def refresh!(remote_only: false)
@@ -151,26 +154,15 @@ module RubyLLM
151
154
  end
152
155
 
153
156
  def initialize(models = nil)
154
- @models = models || load_models
155
- end
156
-
157
- def load_models
158
- read_from_json
159
- end
160
-
161
- def load_from_json!
162
- @models = read_from_json
157
+ @models = models || self.class.load_models
163
158
  end
164
159
 
165
- def read_from_json
166
- data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
167
- JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
168
- rescue JSON::ParserError
169
- []
160
+ def load_from_json!(file = RubyLLM.config.model_registry_file)
161
+ @models = self.class.read_from_json(file)
170
162
  end
171
163
 
172
- def save_to_json
173
- File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
164
+ def save_to_json(file = RubyLLM.config.model_registry_file)
165
+ File.write(file, JSON.pretty_generate(all.map(&:to_h)))
174
166
  end
175
167
 
176
168
  def all
@@ -74,16 +74,23 @@ module RubyLLM
74
74
  parse_embedding_response(response, model:, text:)
75
75
  end
76
76
 
77
+ def moderate(input, model:)
78
+ payload = render_moderation_payload(input, model:)
79
+ response = @connection.post moderation_url, payload
80
+ parse_moderation_response(response, model:)
81
+ end
82
+
77
83
  def paint(prompt, model:, size:, with:, params:)
78
84
  payload = render_image_payload(prompt, model:, size:, with:, params:)
79
85
  response = @connection.post images_url, payload
80
86
  parse_image_response(response, model:)
81
87
  end
82
88
 
83
- def moderate(input, model:)
84
- payload = render_moderation_payload(input, model:)
85
- response = @connection.post moderation_url, payload
86
- parse_moderation_response(response, model:)
89
+ def transcribe(audio_file, model:, language:, **options)
90
+ file_part = build_audio_file_part(audio_file)
91
+ payload = render_transcription_payload(file_part, model:, language:, **options)
92
+ response = @connection.post transcription_url, payload
93
+ parse_transcription_response(response, model:)
87
94
  end
88
95
 
89
96
  def configured?
@@ -168,9 +175,13 @@ module RubyLLM
168
175
  providers[name.to_sym] = provider_class
169
176
  end
170
177
 
178
+ def resolve(name)
179
+ providers[name.to_sym]
180
+ end
181
+
171
182
  def for(model)
172
183
  model_info = Models.find(model)
173
- providers[model_info.provider.to_sym]
184
+ resolve model_info.provider
174
185
  end
175
186
 
176
187
  def providers
@@ -200,6 +211,17 @@ module RubyLLM
200
211
 
201
212
  private
202
213
 
214
+ def build_audio_file_part(file_path)
215
+ expanded_path = File.expand_path(file_path)
216
+ mime_type = Marcel::MimeType.for(Pathname.new(expanded_path))
217
+
218
+ Faraday::Multipart::FilePart.new(
219
+ expanded_path,
220
+ mime_type,
221
+ File.basename(expanded_path)
222
+ )
223
+ end
224
+
203
225
  def try_parse_json(maybe_json)
204
226
  return maybe_json unless maybe_json.is_a?(String)
205
227
 
@@ -74,15 +74,22 @@ module RubyLLM
74
74
  end
75
75
 
76
76
  def build_message(data, content, tool_use_blocks, response)
77
+ usage = data['usage'] || {}
78
+ cached_tokens = usage['cache_read_input_tokens']
79
+ cache_creation_tokens = usage['cache_creation_input_tokens']
80
+ if cache_creation_tokens.nil? && usage['cache_creation'].is_a?(Hash)
81
+ cache_creation_tokens = usage['cache_creation'].values.compact.sum
82
+ end
83
+
77
84
  Message.new(
78
85
  role: :assistant,
79
86
  content: content,
80
87
  tool_calls: Tools.parse_tool_calls(tool_use_blocks),
81
- input_tokens: data.dig('usage', 'input_tokens'),
82
- output_tokens: data.dig('usage', 'output_tokens'),
88
+ input_tokens: usage['input_tokens'],
89
+ output_tokens: usage['output_tokens'],
90
+ cached_tokens: cached_tokens,
91
+ cache_creation_tokens: cache_creation_tokens,
83
92
  model_id: data['model'],
84
- cache_creation_tokens: data.dig('usage', 'cache_creation_input_tokens'),
85
- cached_tokens: data.dig('usage', 'cache_read_input_tokens'),
86
93
  raw: response
87
94
  )
88
95
  end
@@ -98,7 +105,13 @@ module RubyLLM
98
105
  end
99
106
 
100
107
  def format_system_message(msg, cache: false)
101
- Media.format_content(msg.content, cache:)
108
+ content = msg.content
109
+
110
+ if content.is_a?(RubyLLM::Content::Raw)
111
+ content.value
112
+ else
113
+ Media.format_content(content, cache:)
114
+ end
102
115
  end
103
116
 
104
117
  def format_basic_message(msg, cache: false)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Anthropic
6
+ # Helper for constructing Anthropic native content blocks.
7
+ class Content
8
+ class << self
9
+ def new(text = nil, cache: false, cache_control: nil, parts: nil, **extras)
10
+ payload = resolve_payload(
11
+ text: text,
12
+ parts: parts,
13
+ cache: cache,
14
+ cache_control: cache_control,
15
+ extras: extras
16
+ )
17
+
18
+ RubyLLM::Content::Raw.new(payload)
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_payload(text:, parts:, cache:, cache_control:, extras:)
24
+ return Array(parts) if parts
25
+
26
+ raise ArgumentError, 'text or parts must be provided' if text.nil?
27
+
28
+ block = { type: 'text', text: text }.merge(extras)
29
+ control = determine_cache_control(cache_control, cache)
30
+ block[:cache_control] = control if control
31
+
32
+ [block]
33
+ end
34
+
35
+ def determine_cache_control(cache_control, cache_flag)
36
+ return cache_control if cache_control
37
+
38
+ { type: 'ephemeral' } if cache_flag
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -7,9 +7,10 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content, cache: false)
10
+ def format_content(content, cache: false) # rubocop:disable Metrics/PerceivedComplexity
11
+ return content.value if content.is_a?(RubyLLM::Content::Raw)
11
12
  return [format_text(content.to_json, cache:)] if content.is_a?(Hash) || content.is_a?(Array)
12
- return [format_text(content, cache:)] unless content.is_a?(Content)
13
+ return [format_text(content, cache:)] unless content.is_a?(RubyLLM::Content)
13
14
 
14
15
  parts = []
15
16
  parts << format_text(content.text, cache:) if content.text
@@ -17,11 +18,11 @@ module RubyLLM
17
18
  content.attachments.each do |attachment|
18
19
  case attachment.type
19
20
  when :image
20
- parts << format_image(attachment)
21
+ parts << format_image(attachment, cache:)
21
22
  when :pdf
22
- parts << format_pdf(attachment)
23
+ parts << format_pdf(attachment, cache:)
23
24
  when :text
24
- parts << format_text_file(attachment)
25
+ parts << format_text_file(attachment, cache:)
25
26
  else
26
27
  raise UnsupportedAttachmentError, attachment.mime_type
27
28
  end
@@ -44,11 +44,18 @@ module RubyLLM
44
44
  end
45
45
 
46
46
  def extract_cached_tokens(data)
47
- data.dig('message', 'usage', 'cache_read_input_tokens')
47
+ data.dig('message', 'usage', 'cache_read_input_tokens') || data.dig('usage', 'cache_read_input_tokens')
48
48
  end
49
49
 
50
50
  def extract_cache_creation_tokens(data)
51
- data.dig('message', 'usage', 'cache_creation_input_tokens')
51
+ direct = data.dig('message', 'usage',
52
+ 'cache_creation_input_tokens') || data.dig('usage', 'cache_creation_input_tokens')
53
+ return direct if direct
54
+
55
+ breakdown = data.dig('message', 'usage', 'cache_creation') || data.dig('usage', 'cache_creation')
56
+ return unless breakdown.is_a?(Hash)
57
+
58
+ breakdown.values.compact.sum
52
59
  end
53
60
  end
54
61
  end
@@ -12,6 +12,8 @@ module RubyLLM
12
12
  end
13
13
 
14
14
  def format_tool_call(msg)
15
+ return { role: 'assistant', content: msg.content.value } if msg.content.is_a?(RubyLLM::Content::Raw)
16
+
15
17
  content = []
16
18
 
17
19
  content << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
@@ -29,7 +31,7 @@ module RubyLLM
29
31
  def format_tool_result(msg)
30
32
  {
31
33
  role: 'user',
32
- content: [format_tool_result_block(msg)]
34
+ content: msg.content.is_a?(RubyLLM::Content::Raw) ? msg.content.value : [format_tool_result_block(msg)]
33
35
  }
34
36
  end
35
37
 
@@ -51,15 +53,18 @@ module RubyLLM
51
53
  end
52
54
 
53
55
  def function_for(tool)
54
- {
56
+ input_schema = tool.params_schema ||
57
+ RubyLLM::Tool::SchemaDefinition.from_parameters(tool.parameters)&.json_schema
58
+
59
+ declaration = {
55
60
  name: tool.name,
56
61
  description: tool.description,
57
- input_schema: {
58
- type: 'object',
59
- properties: clean_parameters(tool.parameters),
60
- required: required_parameters(tool.parameters)
61
- }
62
+ input_schema: input_schema || default_input_schema
62
63
  }
64
+
65
+ return declaration if tool.provider_params.empty?
66
+
67
+ RubyLLM::Utils.deep_merge(declaration, tool.provider_params)
63
68
  end
64
69
 
65
70
  def extract_tool_calls(data)
@@ -89,17 +94,14 @@ module RubyLLM
89
94
  tool_calls.empty? ? nil : tool_calls
90
95
  end
91
96
 
92
- def clean_parameters(parameters)
93
- parameters.transform_values do |param|
94
- {
95
- type: param.type,
96
- description: param.description
97
- }.compact
98
- end
99
- end
100
-
101
- def required_parameters(parameters)
102
- parameters.select { |_, param| param.required }.keys
97
+ def default_input_schema
98
+ {
99
+ 'type' => 'object',
100
+ 'properties' => {},
101
+ 'required' => [],
102
+ 'additionalProperties' => false,
103
+ 'strict' => true
104
+ }
103
105
  end
104
106
  end
105
107
  end
@@ -10,7 +10,8 @@ module RubyLLM
10
10
 
11
11
  module_function
12
12
 
13
- def format_content(content, cache: false)
13
+ def format_content(content, cache: false) # rubocop:disable Metrics/PerceivedComplexity
14
+ return content.value if content.is_a?(RubyLLM::Content::Raw)
14
15
  return [Anthropic::Media.format_text(content.to_json, cache:)] if content.is_a?(Hash) || content.is_a?(Array)
15
16
  return [Anthropic::Media.format_text(content, cache:)] unless content.is_a?(Content)
16
17
 
@@ -33,11 +33,18 @@ module RubyLLM
33
33
  end
34
34
 
35
35
  def extract_cached_tokens(data)
36
- data.dig('message', 'usage', 'cache_read_input_tokens')
36
+ data.dig('message', 'usage', 'cache_read_input_tokens') || data.dig('usage', 'cache_read_input_tokens')
37
37
  end
38
38
 
39
39
  def extract_cache_creation_tokens(data)
40
- data.dig('message', 'usage', 'cache_creation_input_tokens')
40
+ direct = data.dig('message', 'usage',
41
+ 'cache_creation_input_tokens') || data.dig('usage', 'cache_creation_input_tokens')
42
+ return direct if direct
43
+
44
+ breakdown = data.dig('message', 'usage', 'cache_creation') || data.dig('usage', 'cache_creation')
45
+ return unless breakdown.is_a?(Hash)
46
+
47
+ breakdown.values.compact.sum
41
48
  end
42
49
 
43
50
  private