ruby_llm 1.14.1 → 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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -7
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  5. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  6. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  7. data/lib/ruby_llm/active_record/acts_as.rb +4 -26
  8. data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
  9. data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
  10. data/lib/ruby_llm/active_record/message_methods.rb +87 -4
  11. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  12. data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
  13. data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
  14. data/lib/ruby_llm/agent.rb +4 -2
  15. data/lib/ruby_llm/aliases.json +108 -75
  16. data/lib/ruby_llm/aliases.rb +3 -0
  17. data/lib/ruby_llm/attachment.rb +41 -40
  18. data/lib/ruby_llm/chat.rb +229 -59
  19. data/lib/ruby_llm/configuration.rb +14 -1
  20. data/lib/ruby_llm/connection.rb +36 -7
  21. data/lib/ruby_llm/content.rb +15 -1
  22. data/lib/ruby_llm/cost.rb +224 -0
  23. data/lib/ruby_llm/deprecator.rb +24 -0
  24. data/lib/ruby_llm/embedding.rb +31 -1
  25. data/lib/ruby_llm/error.rb +11 -75
  26. data/lib/ruby_llm/error_middleware.rb +81 -0
  27. data/lib/ruby_llm/image.rb +39 -4
  28. data/lib/ruby_llm/instrumentation.rb +36 -0
  29. data/lib/ruby_llm/message.rb +20 -0
  30. data/lib/ruby_llm/mime_type.rb +25 -0
  31. data/lib/ruby_llm/model/info.rb +53 -2
  32. data/lib/ruby_llm/model/pricing.rb +19 -9
  33. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  34. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  35. data/lib/ruby_llm/model_registry.rb +39 -0
  36. data/lib/ruby_llm/models.json +17817 -13942
  37. data/lib/ruby_llm/models.rb +97 -31
  38. data/lib/ruby_llm/models_schema.json +3 -0
  39. data/lib/ruby_llm/provider.rb +20 -4
  40. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  41. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  42. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  43. data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
  44. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  45. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  46. data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
  47. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  48. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  49. data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
  50. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  51. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  52. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  53. data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
  54. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  55. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  56. data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
  57. data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
  58. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  59. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  60. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
  61. data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
  62. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  63. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  64. data/lib/ruby_llm/providers/mistral.rb +2 -2
  65. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  66. data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
  67. data/lib/ruby_llm/providers/openai/chat.rb +61 -7
  68. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  69. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  70. data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
  71. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  72. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  73. data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
  74. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  76. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  78. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  79. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  80. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  81. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  82. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  83. data/lib/ruby_llm/providers/xai.rb +2 -2
  84. data/lib/ruby_llm/railtie.rb +11 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  86. data/lib/ruby_llm/streaming.rb +4 -0
  87. data/lib/ruby_llm/tokens.rb +8 -0
  88. data/lib/ruby_llm/tool.rb +24 -7
  89. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  90. data/lib/ruby_llm/transcription.rb +2 -1
  91. data/lib/ruby_llm/utils.rb +39 -0
  92. data/lib/ruby_llm/version.rb +1 -1
  93. data/lib/ruby_llm.rb +11 -6
  94. data/lib/tasks/models.rake +45 -16
  95. data/lib/tasks/release.rake +50 -23
  96. metadata +35 -13
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'ruby_llm/active_record/payload_helpers'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Methods mixed into tool call models.
@@ -125,6 +125,7 @@ module RubyLLM
125
125
  record
126
126
  end
127
127
 
128
+ # Mutates persisted instructions on the configured chat record.
128
129
  def sync_instructions!(chat_or_id, **kwargs)
129
130
  raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
130
131
 
@@ -359,7 +360,8 @@ module RubyLLM
359
360
 
360
361
  def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
361
362
  :with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
362
- :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
363
- :add_message, :reset_messages!
363
+ :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :before_message,
364
+ :after_message, :before_tool_call, :after_tool_result, :each, :complete, :add_message,
365
+ :reset_messages!, :cost
364
366
  end
365
367
  end
@@ -8,17 +8,10 @@
8
8
  "anthropic": "claude-3-5-haiku-latest"
9
9
  },
10
10
  "claude-3-5-sonnet": {
11
- "anthropic": "claude-3-5-sonnet-20241022",
12
- "openrouter": "anthropic/claude-3.5-sonnet",
13
- "bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
11
+ "anthropic": "claude-3-5-sonnet-20241022"
14
12
  },
15
13
  "claude-3-7-sonnet": {
16
- "anthropic": "claude-3-7-sonnet-20250219",
17
- "openrouter": "anthropic/claude-3.7-sonnet",
18
- "bedrock": "anthropic.claude-3-7-sonnet-20250219-v1:0"
19
- },
20
- "claude-3-7-sonnet-latest": {
21
- "anthropic": "claude-3-7-sonnet-latest"
14
+ "anthropic": "claude-3-7-sonnet-20250219"
22
15
  },
23
16
  "claude-3-haiku": {
24
17
  "anthropic": "claude-3-haiku-20240307",
@@ -40,8 +33,7 @@
40
33
  },
41
34
  "claude-opus-4": {
42
35
  "anthropic": "claude-opus-4-20250514",
43
- "openrouter": "anthropic/claude-opus-4",
44
- "bedrock": "anthropic.claude-opus-4-1-20250805-v1:0"
36
+ "openrouter": "anthropic/claude-opus-4"
45
37
  },
46
38
  "claude-opus-4-0": {
47
39
  "anthropic": "claude-opus-4-0"
@@ -64,10 +56,21 @@
64
56
  "bedrock": "anthropic.claude-opus-4-6-v1",
65
57
  "azure": "claude-opus-4-6"
66
58
  },
59
+ "claude-opus-4-7": {
60
+ "anthropic": "claude-opus-4-7",
61
+ "openrouter": "anthropic/claude-opus-4.7",
62
+ "bedrock": "anthropic.claude-opus-4-7",
63
+ "azure": "claude-opus-4-7"
64
+ },
65
+ "claude-opus-4-8": {
66
+ "anthropic": "claude-opus-4-8",
67
+ "openrouter": "anthropic/claude-opus-4.8",
68
+ "bedrock": "anthropic.claude-opus-4-8"
69
+ },
67
70
  "claude-sonnet-4": {
68
71
  "anthropic": "claude-sonnet-4-20250514",
69
72
  "openrouter": "anthropic/claude-sonnet-4",
70
- "bedrock": "anthropic.claude-sonnet-4-20250514-v1:0"
73
+ "bedrock": "us.anthropic.claude-sonnet-4-20250514-v1:0"
71
74
  },
72
75
  "claude-sonnet-4-0": {
73
76
  "anthropic": "claude-sonnet-4-0"
@@ -88,17 +91,13 @@
88
91
  "deepseek": "deepseek-chat",
89
92
  "openrouter": "deepseek/deepseek-chat"
90
93
  },
91
- "gemini-1.5-flash": {
92
- "gemini": "gemini-1.5-flash",
93
- "vertexai": "gemini-1.5-flash"
94
+ "deepseek-v4-flash": {
95
+ "deepseek": "deepseek-v4-flash",
96
+ "openrouter": "deepseek/deepseek-v4-flash"
94
97
  },
95
- "gemini-1.5-flash-8b": {
96
- "gemini": "gemini-1.5-flash-8b",
97
- "vertexai": "gemini-1.5-flash-8b"
98
- },
99
- "gemini-1.5-pro": {
100
- "gemini": "gemini-1.5-pro",
101
- "vertexai": "gemini-1.5-pro"
98
+ "deepseek-v4-pro": {
99
+ "deepseek": "deepseek-v4-pro",
100
+ "openrouter": "deepseek/deepseek-v4-pro"
102
101
  },
103
102
  "gemini-2.0-flash": {
104
103
  "gemini": "gemini-2.0-flash",
@@ -106,7 +105,6 @@
106
105
  },
107
106
  "gemini-2.0-flash-001": {
108
107
  "gemini": "gemini-2.0-flash-001",
109
- "openrouter": "google/gemini-2.0-flash-001",
110
108
  "vertexai": "gemini-2.0-flash-001"
111
109
  },
112
110
  "gemini-2.0-flash-lite": {
@@ -115,7 +113,6 @@
115
113
  },
116
114
  "gemini-2.0-flash-lite-001": {
117
115
  "gemini": "gemini-2.0-flash-lite-001",
118
- "openrouter": "google/gemini-2.0-flash-lite-001",
119
116
  "vertexai": "gemini-2.0-flash-lite-001"
120
117
  },
121
118
  "gemini-2.5-flash": {
@@ -132,62 +129,42 @@
132
129
  "openrouter": "google/gemini-2.5-flash-lite",
133
130
  "vertexai": "gemini-2.5-flash-lite"
134
131
  },
135
- "gemini-2.5-flash-lite-preview-06-17": {
136
- "gemini": "gemini-2.5-flash-lite-preview-06-17",
137
- "vertexai": "gemini-2.5-flash-lite-preview-06-17"
138
- },
139
- "gemini-2.5-flash-lite-preview-09-2025": {
140
- "gemini": "gemini-2.5-flash-lite-preview-09-2025",
141
- "openrouter": "google/gemini-2.5-flash-lite-preview-09-2025",
142
- "vertexai": "gemini-2.5-flash-lite-preview-09-2025"
143
- },
144
- "gemini-2.5-flash-preview-04-17": {
145
- "gemini": "gemini-2.5-flash-preview-04-17",
146
- "vertexai": "gemini-2.5-flash-preview-04-17"
147
- },
148
- "gemini-2.5-flash-preview-05-20": {
149
- "gemini": "gemini-2.5-flash-preview-05-20",
150
- "vertexai": "gemini-2.5-flash-preview-05-20"
151
- },
152
- "gemini-2.5-flash-preview-09-2025": {
153
- "gemini": "gemini-2.5-flash-preview-09-2025",
154
- "openrouter": "google/gemini-2.5-flash-preview-09-2025",
155
- "vertexai": "gemini-2.5-flash-preview-09-2025"
156
- },
157
132
  "gemini-2.5-pro": {
158
133
  "gemini": "gemini-2.5-pro",
159
134
  "openrouter": "google/gemini-2.5-pro",
160
135
  "vertexai": "gemini-2.5-pro"
161
136
  },
162
- "gemini-2.5-pro-preview-05-06": {
163
- "gemini": "gemini-2.5-pro-preview-05-06",
164
- "openrouter": "google/gemini-2.5-pro-preview-05-06",
165
- "vertexai": "gemini-2.5-pro-preview-05-06"
166
- },
167
- "gemini-2.5-pro-preview-06-05": {
168
- "gemini": "gemini-2.5-pro-preview-06-05",
169
- "openrouter": "google/gemini-2.5-pro-preview-06-05",
170
- "vertexai": "gemini-2.5-pro-preview-06-05"
171
- },
172
137
  "gemini-3-flash-preview": {
173
138
  "gemini": "gemini-3-flash-preview",
174
139
  "openrouter": "google/gemini-3-flash-preview",
175
140
  "vertexai": "gemini-3-flash-preview"
176
141
  },
142
+ "gemini-3-pro-image": {
143
+ "gemini": "gemini-3-pro-image",
144
+ "vertexai": "gemini-3-pro-image"
145
+ },
177
146
  "gemini-3-pro-image-preview": {
178
147
  "gemini": "gemini-3-pro-image-preview",
179
148
  "openrouter": "google/gemini-3-pro-image-preview"
180
149
  },
181
150
  "gemini-3-pro-preview": {
182
151
  "gemini": "gemini-3-pro-preview",
183
- "openrouter": "google/gemini-3-pro-preview",
184
152
  "vertexai": "gemini-3-pro-preview"
185
153
  },
154
+ "gemini-3.1-flash-image": {
155
+ "gemini": "gemini-3.1-flash-image",
156
+ "vertexai": "gemini-3.1-flash-image"
157
+ },
186
158
  "gemini-3.1-flash-image-preview": {
187
159
  "gemini": "gemini-3.1-flash-image-preview",
188
160
  "openrouter": "google/gemini-3.1-flash-image-preview",
189
161
  "vertexai": "gemini-3.1-flash-image-preview"
190
162
  },
163
+ "gemini-3.1-flash-lite": {
164
+ "gemini": "gemini-3.1-flash-lite",
165
+ "openrouter": "google/gemini-3.1-flash-lite",
166
+ "vertexai": "gemini-3.1-flash-lite"
167
+ },
191
168
  "gemini-3.1-flash-lite-preview": {
192
169
  "gemini": "gemini-3.1-flash-lite-preview",
193
170
  "openrouter": "google/gemini-3.1-flash-lite-preview",
@@ -203,10 +180,19 @@
203
180
  "openrouter": "google/gemini-3.1-pro-preview-customtools",
204
181
  "vertexai": "gemini-3.1-pro-preview-customtools"
205
182
  },
183
+ "gemini-3.5-flash": {
184
+ "gemini": "gemini-3.5-flash",
185
+ "openrouter": "google/gemini-3.5-flash",
186
+ "vertexai": "gemini-3.5-flash"
187
+ },
206
188
  "gemini-embedding-001": {
207
189
  "gemini": "gemini-embedding-001",
208
190
  "vertexai": "gemini-embedding-001"
209
191
  },
192
+ "gemini-embedding-2": {
193
+ "gemini": "gemini-embedding-2",
194
+ "vertexai": "gemini-embedding-2"
195
+ },
210
196
  "gemini-flash": {
211
197
  "gemini": "gemini-flash-latest",
212
198
  "vertexai": "gemini-flash-latest"
@@ -215,21 +201,13 @@
215
201
  "gemini": "gemini-flash-lite-latest",
216
202
  "vertexai": "gemini-flash-lite-latest"
217
203
  },
218
- "gemma-3-12b-it": {
219
- "gemini": "gemma-3-12b-it",
220
- "openrouter": "google/gemma-3-12b-it"
221
- },
222
- "gemma-3-27b-it": {
223
- "gemini": "gemma-3-27b-it",
224
- "openrouter": "google/gemma-3-27b-it"
204
+ "gemma-4-26b-a4b-it": {
205
+ "gemini": "gemma-4-26b-a4b-it",
206
+ "openrouter": "google/gemma-4-26b-a4b-it"
225
207
  },
226
- "gemma-3-4b-it": {
227
- "gemini": "gemma-3-4b-it",
228
- "openrouter": "google/gemma-3-4b-it"
229
- },
230
- "gemma-3n-e4b-it": {
231
- "gemini": "gemma-3n-e4b-it",
232
- "openrouter": "google/gemma-3n-e4b-it"
208
+ "gemma-4-31b-it": {
209
+ "gemini": "gemma-4-31b-it",
210
+ "openrouter": "google/gemma-4-31b-it"
233
211
  },
234
212
  "gpt-3.5-turbo": {
235
213
  "openai": "gpt-3.5-turbo",
@@ -287,10 +265,6 @@
287
265
  "openrouter": "openai/gpt-4o-2024-11-20",
288
266
  "azure": "gpt-4o-2024-11-20"
289
267
  },
290
- "gpt-4o-audio-preview": {
291
- "openai": "gpt-4o-audio-preview",
292
- "openrouter": "openai/gpt-4o-audio-preview"
293
- },
294
268
  "gpt-4o-mini": {
295
269
  "openai": "gpt-4o-mini",
296
270
  "openrouter": "openai/gpt-4o-mini",
@@ -378,6 +352,14 @@
378
352
  "openai": "gpt-5.4-pro",
379
353
  "openrouter": "openai/gpt-5.4-pro"
380
354
  },
355
+ "gpt-5.5": {
356
+ "openai": "gpt-5.5",
357
+ "openrouter": "openai/gpt-5.5"
358
+ },
359
+ "gpt-5.5-pro": {
360
+ "openai": "gpt-5.5-pro",
361
+ "openrouter": "openai/gpt-5.5-pro"
362
+ },
381
363
  "gpt-audio": {
382
364
  "openai": "gpt-audio",
383
365
  "openrouter": "openai/gpt-audio"
@@ -386,6 +368,57 @@
386
368
  "openai": "gpt-audio-mini",
387
369
  "openrouter": "openai/gpt-audio-mini"
388
370
  },
371
+ "grok-3": {
372
+ "xai": "grok-4.3"
373
+ },
374
+ "grok-3-latest": {
375
+ "xai": "grok-4.3"
376
+ },
377
+ "grok-3-mini": {
378
+ "xai": "grok-4.3"
379
+ },
380
+ "grok-3-mini-latest": {
381
+ "xai": "grok-4.3"
382
+ },
383
+ "grok-4": {
384
+ "xai": "grok-4.3"
385
+ },
386
+ "grok-4-1-fast": {
387
+ "xai": "grok-4.3"
388
+ },
389
+ "grok-4-1-fast-non-reasoning": {
390
+ "xai": "grok-4.3"
391
+ },
392
+ "grok-4-1-fast-non-reasoning-latest": {
393
+ "xai": "grok-4.3"
394
+ },
395
+ "grok-4-1-fast-reasoning": {
396
+ "xai": "grok-4.3"
397
+ },
398
+ "grok-4-1-fast-reasoning-latest": {
399
+ "xai": "grok-4.3"
400
+ },
401
+ "grok-4-fast": {
402
+ "xai": "grok-4.3"
403
+ },
404
+ "grok-4-fast-non-reasoning": {
405
+ "xai": "grok-4.3"
406
+ },
407
+ "grok-4-fast-non-reasoning-latest": {
408
+ "xai": "grok-4.3"
409
+ },
410
+ "grok-4-fast-reasoning": {
411
+ "xai": "grok-4.3"
412
+ },
413
+ "grok-4-fast-reasoning-latest": {
414
+ "xai": "grok-4.3"
415
+ },
416
+ "grok-4-latest": {
417
+ "xai": "grok-4.3"
418
+ },
419
+ "grok-latest": {
420
+ "xai": "grok-4.3"
421
+ },
389
422
  "lyria-3-clip-preview": {
390
423
  "gemini": "lyria-3-clip-preview",
391
424
  "openrouter": "google/lyria-3-clip-preview"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  # Manages model aliases for provider-specific versions
5
7
  class Aliases
@@ -30,6 +32,7 @@ module RubyLLM
30
32
  end
31
33
  end
32
34
 
35
+ # Replaces the cached alias map from aliases.json.
33
36
  def reload!
34
37
  @aliases = load_aliases
35
38
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
3
4
  require 'pathname'
4
5
  require 'uri'
5
6
 
@@ -8,6 +9,16 @@ module RubyLLM
8
9
  class Attachment
9
10
  attr_reader :source, :filename, :mime_type
10
11
 
12
+ DOCUMENT_EXTENSIONS = %w[
13
+ doc docx dot key numbers odp ods odt pages pot pps ppt pptx rtf xls xlsx
14
+ ].freeze
15
+ ACTIVE_STORAGE_CLASS_NAMES = %w[
16
+ ActiveStorage::Blob
17
+ ActiveStorage::Attachment
18
+ ActiveStorage::Attached::One
19
+ ActiveStorage::Attached::Many
20
+ ].freeze
21
+
11
22
  def initialize(source, filename: nil)
12
23
  @source = source
13
24
  @source = source_type_cast
@@ -29,11 +40,7 @@ module RubyLLM
29
40
  end
30
41
 
31
42
  def active_storage?
32
- return false unless defined?(ActiveStorage)
33
-
34
- @source.is_a?(ActiveStorage::Blob) ||
35
- @source.is_a?(ActiveStorage::Attached::One) ||
36
- @source.is_a?(ActiveStorage::Attached::Many)
43
+ ACTIVE_STORAGE_CLASS_NAMES.any? { |class_name| source_is_a?(class_name) }
37
44
  end
38
45
 
39
46
  def content
@@ -82,6 +89,7 @@ module RubyLLM
82
89
  return :audio if audio?
83
90
  return :pdf if pdf?
84
91
  return :text if text?
92
+ return :document if document?
85
93
 
86
94
  :unknown
87
95
  end
@@ -113,6 +121,17 @@ module RubyLLM
113
121
  RubyLLM::MimeType.pdf? mime_type
114
122
  end
115
123
 
124
+ def document?
125
+ return false if pdf? || text?
126
+
127
+ RubyLLM::MimeType.document?(mime_type) || DOCUMENT_EXTENSIONS.include?(extension)
128
+ end
129
+
130
+ def extension
131
+ extension = File.extname(filename.to_s).delete_prefix('.').downcase
132
+ extension.empty? ? nil : extension
133
+ end
134
+
116
135
  def text?
117
136
  RubyLLM::MimeType.text? mime_type
118
137
  end
@@ -146,18 +165,7 @@ module RubyLLM
146
165
  end
147
166
 
148
167
  def load_content_from_active_storage
149
- return unless defined?(ActiveStorage)
150
-
151
- @content = case @source
152
- when ActiveStorage::Blob
153
- @source.download
154
- when ActiveStorage::Attached::One
155
- @source.blob&.download
156
- when ActiveStorage::Attached::Many
157
- # For multiple attachments, just take the first one
158
- # This maintains the single-attachment interface
159
- @source.blobs.first&.download
160
- end
168
+ @content = active_storage_blob&.download
161
169
  end
162
170
 
163
171
  def source_type_cast
@@ -192,32 +200,25 @@ module RubyLLM
192
200
  end
193
201
  end
194
202
 
195
- def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
196
- return 'attachment' unless defined?(ActiveStorage)
197
-
198
- case @source
199
- when ActiveStorage::Blob
200
- @source.filename.to_s
201
- when ActiveStorage::Attached::One
202
- @source.blob&.filename&.to_s || 'attachment'
203
- when ActiveStorage::Attached::Many
204
- @source.blobs.first&.filename&.to_s || 'attachment'
205
- else
206
- 'attachment'
207
- end
203
+ def extract_filename_from_active_storage
204
+ active_storage_blob&.filename&.to_s || 'attachment'
208
205
  end
209
206
 
210
207
  def active_storage_content_type
211
- return unless defined?(ActiveStorage)
212
-
213
- case @source
214
- when ActiveStorage::Blob
215
- @source.content_type
216
- when ActiveStorage::Attached::One
217
- @source.blob&.content_type
218
- when ActiveStorage::Attached::Many
219
- @source.blobs.first&.content_type
220
- end
208
+ active_storage_blob&.content_type
209
+ end
210
+
211
+ def active_storage_blob
212
+ return @source if source_is_a?('ActiveStorage::Blob')
213
+ return @source.blob if source_is_a?('ActiveStorage::Attachment')
214
+ return @source.blob if source_is_a?('ActiveStorage::Attached::One')
215
+
216
+ @source.blobs.first if source_is_a?('ActiveStorage::Attached::Many')
217
+ end
218
+
219
+ def source_is_a?(class_name)
220
+ klass = RubyLLM::Utils.safe_constantize(class_name)
221
+ klass ? @source.is_a?(klass) : false
221
222
  end
222
223
  end
223
224
  end