dify_llm 1.6.4

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 (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. metadata +291 -0
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into chat models.
6
+ module ChatMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_save :resolve_model_from_strings
11
+ end
12
+
13
+ class_methods do
14
+ attr_reader :tool_call_class, :model_class
15
+ end
16
+
17
+ attr_accessor :assume_model_exists, :context
18
+
19
+ def model=(value)
20
+ @model_string = value if value.is_a?(String)
21
+ super unless value.is_a?(String)
22
+ end
23
+
24
+ def model_id=(value)
25
+ @model_string = value
26
+ end
27
+
28
+ def model_id
29
+ model&.model_id
30
+ end
31
+
32
+ def provider=(value)
33
+ @provider_string = value
34
+ end
35
+
36
+ def provider
37
+ model&.provider
38
+ end
39
+
40
+ private
41
+
42
+ def resolve_model_from_strings # rubocop:disable Metrics/PerceivedComplexity
43
+ return unless @model_string
44
+
45
+ model_info, _provider = Models.resolve(
46
+ @model_string,
47
+ provider: @provider_string,
48
+ assume_exists: assume_model_exists || false,
49
+ config: context&.config || RubyLLM.config
50
+ )
51
+
52
+ model_class = self.class.model_class.constantize
53
+ model_record = model_class.find_or_create_by!(
54
+ model_id: model_info.id,
55
+ provider: model_info.provider
56
+ ) do |m|
57
+ m.name = model_info.name || model_info.id
58
+ m.family = model_info.family
59
+ m.context_window = model_info.context_window
60
+ m.max_output_tokens = model_info.max_output_tokens
61
+ m.capabilities = model_info.capabilities || []
62
+ m.modalities = model_info.modalities || {}
63
+ m.pricing = model_info.pricing || {}
64
+ m.metadata = model_info.metadata || {}
65
+ end
66
+
67
+ self.model = model_record
68
+ @model_string = nil
69
+ @provider_string = nil
70
+ end
71
+
72
+ public
73
+
74
+ def to_llm
75
+ raise 'No model specified' unless model
76
+
77
+ @chat ||= (context || RubyLLM).chat(
78
+ model: model.model_id,
79
+ provider: model.provider.to_sym
80
+ )
81
+ @chat.reset_messages!
82
+
83
+ messages.each do |msg|
84
+ @chat.add_message(msg.to_llm)
85
+ end
86
+
87
+ setup_persistence_callbacks
88
+ end
89
+
90
+ def with_instructions(instructions, replace: false)
91
+ transaction do
92
+ messages.where(role: :system).destroy_all if replace
93
+ messages.create!(role: :system, content: instructions)
94
+ end
95
+ to_llm.with_instructions(instructions)
96
+ self
97
+ end
98
+
99
+ def with_tool(...)
100
+ to_llm.with_tool(...)
101
+ self
102
+ end
103
+
104
+ def with_tools(...)
105
+ to_llm.with_tools(...)
106
+ self
107
+ end
108
+
109
+ def with_model(model_name, provider: nil)
110
+ self.provider = provider if provider
111
+ self.model = model_name
112
+ resolve_model_from_strings
113
+ save!
114
+ to_llm.with_model(model.model_id, provider: model.provider.to_sym)
115
+ self
116
+ end
117
+
118
+ def with_temperature(...)
119
+ to_llm.with_temperature(...)
120
+ self
121
+ end
122
+
123
+ def with_params(...)
124
+ to_llm.with_params(...)
125
+ self
126
+ end
127
+
128
+ def with_headers(...)
129
+ to_llm.with_headers(...)
130
+ self
131
+ end
132
+
133
+ def with_schema(...)
134
+ to_llm.with_schema(...)
135
+ self
136
+ end
137
+
138
+ def on_new_message(&block)
139
+ to_llm
140
+
141
+ existing_callback = @chat.instance_variable_get(:@on)[:new_message]
142
+
143
+ @chat.on_new_message do
144
+ existing_callback&.call
145
+ block&.call
146
+ end
147
+ self
148
+ end
149
+
150
+ def on_end_message(&block)
151
+ to_llm
152
+
153
+ existing_callback = @chat.instance_variable_get(:@on)[:end_message]
154
+
155
+ @chat.on_end_message do |msg|
156
+ existing_callback&.call(msg)
157
+ block&.call(msg)
158
+ end
159
+ self
160
+ end
161
+
162
+ def on_tool_call(...)
163
+ to_llm.on_tool_call(...)
164
+ self
165
+ end
166
+
167
+ def on_tool_result(...)
168
+ to_llm.on_tool_result(...)
169
+ self
170
+ end
171
+
172
+ def create_user_message(content, with: nil)
173
+ message_record = messages.create!(role: :user, content: content)
174
+ persist_content(message_record, with) if with.present?
175
+ message_record
176
+ end
177
+
178
+ def ask(message, with: nil, &)
179
+ create_user_message(message, with:)
180
+ complete(&)
181
+ end
182
+
183
+ alias say ask
184
+
185
+ def complete(...)
186
+ to_llm.complete(...)
187
+ rescue RubyLLM::Error => e
188
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
189
+ cleanup_orphaned_tool_results
190
+ raise e
191
+ end
192
+
193
+ private
194
+
195
+ def cleanup_failed_messages
196
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
197
+ @message.destroy
198
+ end
199
+
200
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
201
+ messages.reload
202
+ last = messages.order(:id).last
203
+
204
+ return unless last&.tool_call? || last&.tool_result?
205
+
206
+ if last.tool_call?
207
+ last.destroy
208
+ elsif last.tool_result?
209
+ tool_call_message = last.parent_tool_call.message
210
+ expected_results = tool_call_message.tool_calls.pluck(:id)
211
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
212
+
213
+ if expected_results.sort != actual_results.sort
214
+ tool_call_message.tool_results.each(&:destroy)
215
+ tool_call_message.destroy
216
+ end
217
+ end
218
+ end
219
+
220
+ def setup_persistence_callbacks
221
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
222
+
223
+ @chat.on_new_message { persist_new_message }
224
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
225
+
226
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
227
+ @chat
228
+ end
229
+
230
+ def persist_new_message
231
+ @message = messages.create!(role: :assistant, content: '')
232
+ end
233
+
234
+ def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
235
+ return unless message
236
+
237
+ tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
238
+
239
+ transaction do
240
+ content = message.content
241
+ attachments_to_persist = nil
242
+
243
+ if content.is_a?(RubyLLM::Content)
244
+ attachments_to_persist = content.attachments if content.attachments.any?
245
+ content = content.text
246
+ elsif content.is_a?(Hash) || content.is_a?(Array)
247
+ content = content.to_json
248
+ end
249
+
250
+ @message.update!(
251
+ role: message.role,
252
+ content: content,
253
+ model: model,
254
+ input_tokens: message.input_tokens,
255
+ output_tokens: message.output_tokens
256
+ )
257
+ @message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
258
+ @message.save!
259
+
260
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
261
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
262
+ end
263
+ end
264
+
265
+ def persist_tool_calls(tool_calls)
266
+ tool_calls.each_value do |tool_call|
267
+ attributes = tool_call.to_h
268
+ attributes[:tool_call_id] = attributes.delete(:id)
269
+ @message.tool_calls.create!(**attributes)
270
+ end
271
+ end
272
+
273
+ def find_tool_call_id(tool_call_id)
274
+ self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
275
+ end
276
+
277
+ def persist_content(message_record, attachments)
278
+ return unless message_record.respond_to?(:attachments)
279
+
280
+ attachables = prepare_for_active_storage(attachments)
281
+ message_record.attachments.attach(attachables) if attachables.any?
282
+ end
283
+
284
+ def prepare_for_active_storage(attachments)
285
+ Utils.to_safe_array(attachments).filter_map do |attachment|
286
+ case attachment
287
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
288
+ attachment
289
+ when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
290
+ attachment.blobs
291
+ when Hash
292
+ attachment.values.map { |v| prepare_for_active_storage(v) }
293
+ else
294
+ convert_to_active_storage_format(attachment)
295
+ end
296
+ end.flatten.compact
297
+ end
298
+
299
+ def convert_to_active_storage_format(source)
300
+ return if source.blank?
301
+
302
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
303
+
304
+ {
305
+ io: StringIO.new(attachment.content),
306
+ filename: attachment.filename,
307
+ content_type: attachment.mime_type
308
+ }
309
+ rescue StandardError => e
310
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
311
+ nil
312
+ end
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into message models.
6
+ module MessageMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
11
+ end
12
+
13
+ def to_llm
14
+ RubyLLM::Message.new(
15
+ role: role.to_sym,
16
+ content: extract_content,
17
+ tool_calls: extract_tool_calls,
18
+ tool_call_id: extract_tool_call_id,
19
+ input_tokens: input_tokens,
20
+ output_tokens: output_tokens,
21
+ model_id: model_id
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def extract_tool_calls
28
+ tool_calls.to_h do |tool_call|
29
+ [
30
+ tool_call.tool_call_id,
31
+ RubyLLM::ToolCall.new(
32
+ id: tool_call.tool_call_id,
33
+ name: tool_call.name,
34
+ arguments: tool_call.arguments
35
+ )
36
+ ]
37
+ end
38
+ end
39
+
40
+ def extract_tool_call_id
41
+ parent_tool_call&.tool_call_id
42
+ end
43
+
44
+ def extract_content
45
+ return content unless respond_to?(:attachments) && attachments.attached?
46
+
47
+ RubyLLM::Content.new(content).tap do |content_obj|
48
+ @_tempfiles = []
49
+
50
+ attachments.each do |attachment|
51
+ tempfile = download_attachment(attachment)
52
+ content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
53
+ end
54
+ end
55
+ end
56
+
57
+ def download_attachment(attachment)
58
+ ext = File.extname(attachment.filename.to_s)
59
+ basename = File.basename(attachment.filename.to_s, ext)
60
+ tempfile = Tempfile.new([basename, ext])
61
+ tempfile.binmode
62
+
63
+ attachment.download { |chunk| tempfile.write(chunk) }
64
+
65
+ tempfile.flush
66
+ tempfile.rewind
67
+ @_tempfiles << tempfile
68
+ tempfile
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into model registry models.
6
+ module ModelMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:disable Metrics/BlockLength
10
+ def refresh!
11
+ RubyLLM.models.refresh!
12
+
13
+ transaction do
14
+ RubyLLM.models.all.each do |model_info|
15
+ model = find_or_initialize_by(
16
+ model_id: model_info.id,
17
+ provider: model_info.provider
18
+ )
19
+ model.update!(from_llm_attributes(model_info))
20
+ end
21
+ end
22
+ end
23
+
24
+ def save_to_database
25
+ transaction do
26
+ RubyLLM.models.all.each do |model_info|
27
+ model = find_or_initialize_by(
28
+ model_id: model_info.id,
29
+ provider: model_info.provider
30
+ )
31
+ model.update!(from_llm_attributes(model_info))
32
+ end
33
+ end
34
+ end
35
+
36
+ def from_llm(model_info)
37
+ new(from_llm_attributes(model_info))
38
+ end
39
+
40
+ private
41
+
42
+ def from_llm_attributes(model_info)
43
+ {
44
+ model_id: model_info.id,
45
+ name: model_info.name,
46
+ provider: model_info.provider,
47
+ family: model_info.family,
48
+ model_created_at: model_info.created_at,
49
+ context_window: model_info.context_window,
50
+ max_output_tokens: model_info.max_output_tokens,
51
+ knowledge_cutoff: model_info.knowledge_cutoff,
52
+ modalities: model_info.modalities.to_h,
53
+ capabilities: model_info.capabilities,
54
+ pricing: model_info.pricing.to_h,
55
+ metadata: model_info.metadata
56
+ }
57
+ end
58
+ end
59
+
60
+ def to_llm
61
+ RubyLLM::Model::Info.new(
62
+ id: model_id,
63
+ name: name,
64
+ provider: provider,
65
+ family: family,
66
+ created_at: model_created_at,
67
+ context_window: context_window,
68
+ max_output_tokens: max_output_tokens,
69
+ knowledge_cutoff: knowledge_cutoff,
70
+ modalities: modalities&.deep_symbolize_keys || {},
71
+ capabilities: capabilities,
72
+ pricing: pricing&.deep_symbolize_keys || {},
73
+ metadata: metadata&.deep_symbolize_keys || {}
74
+ )
75
+ end
76
+
77
+ delegate :supports?, :supports_vision?, :supports_functions?, :type,
78
+ :input_price_per_million, :output_price_per_million,
79
+ :function_calling?, :structured_output?, :batch?,
80
+ :reasoning?, :citations?, :streaming?,
81
+ to: :to_llm
82
+ end
83
+ end
84
+ end