ruby_llm_community 0.0.1 → 0.0.3

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -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_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +226 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +73 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +81 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +214 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +221 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +120 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +116 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +56 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +76 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +73 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +92 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +146 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +66 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +87 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +116 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +191 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +103 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. metadata +128 -7
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Ollama API integration.
6
+ class Ollama < OpenAIBase
7
+ include Ollama::Chat
8
+ include Ollama::Media
9
+
10
+ def api_base
11
+ @config.ollama_api_base
12
+ end
13
+
14
+ def headers
15
+ {}
16
+ end
17
+
18
+ class << self
19
+ def configuration_requirements
20
+ %i[ollama_api_base]
21
+ end
22
+
23
+ def local?
24
+ true
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Determines capabilities and pricing for OpenAI models
7
+ module Capabilities
8
+ module_function
9
+
10
+ MODEL_PATTERNS = {
11
+ dall_e: /^dall-e/,
12
+ chatgpt4o: /^chatgpt-4o/,
13
+ gpt41: /^gpt-4\.1(?!-(?:mini|nano))/,
14
+ gpt41_mini: /^gpt-4\.1-mini/,
15
+ gpt41_nano: /^gpt-4\.1-nano/,
16
+ gpt4: /^gpt-4(?:-\d{6})?$/,
17
+ gpt4_turbo: /^gpt-4(?:\.5)?-(?:\d{6}-)?(preview|turbo)/,
18
+ gpt35_turbo: /^gpt-3\.5-turbo/,
19
+ gpt4o: /^gpt-4o(?!-(?:mini|audio|realtime|transcribe|tts|search))/,
20
+ gpt4o_audio: /^gpt-4o-(?:audio)/,
21
+ gpt4o_mini: /^gpt-4o-mini(?!-(?:audio|realtime|transcribe|tts|search))/,
22
+ gpt4o_mini_audio: /^gpt-4o-mini-audio/,
23
+ gpt4o_mini_realtime: /^gpt-4o-mini-realtime/,
24
+ gpt4o_mini_transcribe: /^gpt-4o-mini-transcribe/,
25
+ gpt4o_mini_tts: /^gpt-4o-mini-tts/,
26
+ gpt4o_realtime: /^gpt-4o-realtime/,
27
+ gpt4o_search: /^gpt-4o-search/,
28
+ gpt4o_transcribe: /^gpt-4o-transcribe/,
29
+ o1: /^o1(?!-(?:mini|pro))/,
30
+ o1_mini: /^o1-mini/,
31
+ o1_pro: /^o1-pro/,
32
+ o3_mini: /^o3-mini/,
33
+ babbage: /^babbage/,
34
+ davinci: /^davinci/,
35
+ embedding3_large: /^text-embedding-3-large/,
36
+ embedding3_small: /^text-embedding-3-small/,
37
+ embedding_ada: /^text-embedding-ada/,
38
+ tts1: /^tts-1(?!-hd)/,
39
+ tts1_hd: /^tts-1-hd/,
40
+ whisper: /^whisper/,
41
+ moderation: /^(?:omni|text)-moderation/
42
+ }.freeze
43
+
44
+ def context_window_for(model_id)
45
+ case model_family(model_id)
46
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
47
+ when 'chatgpt4o', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
48
+ 'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime',
49
+ 'gpt4o_search', 'gpt4o_transcribe', 'gpt4o_mini_search', 'o1_mini' then 128_000
50
+ when 'gpt4' then 8_192
51
+ when 'gpt4o_mini_transcribe' then 16_000
52
+ when 'o1', 'o1_pro', 'o3_mini' then 200_000
53
+ when 'gpt35_turbo' then 16_385
54
+ when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
55
+ 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
56
+ else 4_096
57
+ end
58
+ end
59
+
60
+ def max_tokens_for(model_id)
61
+ case model_family(model_id)
62
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
63
+ when 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'gpt4o_mini_search' then 16_384
64
+ when 'babbage', 'davinci' then 16_384 # rubocop:disable Lint/DuplicateBranch
65
+ when 'gpt4' then 8_192
66
+ when 'gpt35_turbo' then 4_096
67
+ when 'gpt4_turbo', 'gpt4o_realtime', 'gpt4o_mini_realtime' then 4_096 # rubocop:disable Lint/DuplicateBranch
68
+ when 'gpt4o_mini_transcribe' then 2_000
69
+ when 'o1', 'o1_pro', 'o3_mini' then 100_000
70
+ when 'o1_mini' then 65_536
71
+ when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
72
+ 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
73
+ else 16_384 # rubocop:disable Lint/DuplicateBranch
74
+ end
75
+ end
76
+
77
+ def supports_vision?(model_id)
78
+ case model_family(model_id)
79
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1',
80
+ 'o1_pro', 'moderation', 'gpt4o_search', 'gpt4o_mini_search' then true
81
+ else false
82
+ end
83
+ end
84
+
85
+ def supports_functions?(model_id)
86
+ case model_family(model_id)
87
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro',
88
+ 'o3_mini' then true
89
+ when 'chatgpt4o', 'gpt35_turbo', 'o1_mini', 'gpt4o_mini_tts',
90
+ 'gpt4o_transcribe', 'gpt4o_search', 'gpt4o_mini_search' then false
91
+ else false # rubocop:disable Lint/DuplicateBranch
92
+ end
93
+ end
94
+
95
+ def supports_structured_output?(model_id)
96
+ case model_family(model_id)
97
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro',
98
+ 'o3_mini' then true
99
+ else false
100
+ end
101
+ end
102
+
103
+ def supports_json_mode?(model_id)
104
+ supports_structured_output?(model_id)
105
+ end
106
+
107
+ PRICES = {
108
+ gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
109
+ gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
110
+ gpt41_nano: { input: 0.1, output: 0.4 },
111
+ chatgpt4o: { input: 5.0, output: 15.0 },
112
+ gpt4: { input: 10.0, output: 30.0 },
113
+ gpt4_turbo: { input: 10.0, output: 30.0 },
114
+ gpt45: { input: 75.0, output: 150.0 },
115
+ gpt35_turbo: { input: 0.5, output: 1.5 },
116
+ gpt4o: { input: 2.5, output: 10.0 },
117
+ gpt4o_audio: { input: 2.5, output: 10.0, audio_input: 40.0, audio_output: 80.0 },
118
+ gpt4o_mini: { input: 0.15, output: 0.6 },
119
+ gpt4o_mini_audio: { input: 0.15, output: 0.6, audio_input: 10.0, audio_output: 20.0 },
120
+ gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
121
+ gpt4o_mini_transcribe: { input: 1.25, output: 5.0, audio_input: 3.0 },
122
+ gpt4o_mini_tts: { input: 0.6, output: 12.0 },
123
+ gpt4o_realtime: { input: 5.0, output: 20.0 },
124
+ gpt4o_search: { input: 2.5, output: 10.0 },
125
+ gpt4o_transcribe: { input: 2.5, output: 10.0, audio_input: 6.0 },
126
+ o1: { input: 15.0, output: 60.0 },
127
+ o1_mini: { input: 1.1, output: 4.4 },
128
+ o1_pro: { input: 150.0, output: 600.0 },
129
+ o3_mini: { input: 1.1, output: 4.4 },
130
+ babbage: { input: 0.4, output: 0.4 },
131
+ davinci: { input: 2.0, output: 2.0 },
132
+ embedding3_large: { price: 0.13 },
133
+ embedding3_small: { price: 0.02 },
134
+ embedding_ada: { price: 0.10 },
135
+ tts1: { price: 15.0 },
136
+ tts1_hd: { price: 30.0 },
137
+ whisper: { price: 0.006 },
138
+ moderation: { price: 0.0 }
139
+ }.freeze
140
+
141
+ def model_family(model_id)
142
+ MODEL_PATTERNS.each do |family, pattern|
143
+ return family.to_s if model_id.match?(pattern)
144
+ end
145
+ 'other'
146
+ end
147
+
148
+ def input_price_for(model_id)
149
+ family = model_family(model_id).to_sym
150
+ prices = PRICES.fetch(family, { input: default_input_price })
151
+ prices[:input] || prices[:price] || default_input_price
152
+ end
153
+
154
+ def cached_input_price_for(model_id)
155
+ family = model_family(model_id).to_sym
156
+ prices = PRICES.fetch(family, {})
157
+ prices[:cached_input]
158
+ end
159
+
160
+ def output_price_for(model_id)
161
+ family = model_family(model_id).to_sym
162
+ prices = PRICES.fetch(family, { output: default_output_price })
163
+ prices[:output] || prices[:price] || default_output_price
164
+ end
165
+
166
+ def model_type(model_id)
167
+ case model_family(model_id)
168
+ when /embedding/ then 'embedding'
169
+ when /^tts|whisper|gpt4o_(?:mini_)?(?:transcribe|tts)$/ then 'audio'
170
+ when 'moderation' then 'moderation'
171
+ when /dall/ then 'image'
172
+ else 'chat'
173
+ end
174
+ end
175
+
176
+ def default_input_price
177
+ 0.50
178
+ end
179
+
180
+ def default_output_price
181
+ 1.50
182
+ end
183
+
184
+ def format_display_name(model_id)
185
+ model_id.then { |id| humanize(id) }
186
+ .then { |name| apply_special_formatting(name) }
187
+ end
188
+
189
+ def humanize(id)
190
+ id.tr('-', ' ')
191
+ .split
192
+ .map(&:capitalize)
193
+ .join(' ')
194
+ end
195
+
196
+ def apply_special_formatting(name)
197
+ name
198
+ .gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
199
+ .gsub(/^(?:Gpt|Chatgpt|Tts|Dall E) /) { |m| special_prefix_format(m.strip) }
200
+ .gsub(/^O([13]) /, 'O\1-')
201
+ .gsub(/^O[13] Mini/, '\0'.tr(' ', '-'))
202
+ .gsub(/\d\.\d /, '\0'.sub(' ', '-'))
203
+ .gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
204
+ .gsub(/\bHd\b/, 'HD')
205
+ .gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
206
+ .gsub('Text Embedding', 'text-embedding-')
207
+ end
208
+
209
+ def special_prefix_format(prefix)
210
+ case prefix # rubocop:disable Style/HashLikeCase
211
+ when 'Gpt' then 'GPT-'
212
+ when 'Chatgpt' then 'ChatGPT-'
213
+ when 'Tts' then 'TTS-'
214
+ when 'Dall E' then 'DALL-E-'
215
+ end
216
+ end
217
+
218
+ def self.normalize_temperature(temperature, model_id)
219
+ if model_id.match?(/^(o\d|gpt-5)/)
220
+ RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, ignoring provided value"
221
+ 1.0
222
+ elsif model_id.match?(/-search/)
223
+ RubyLLM.logger.debug "Model #{model_id} does not accept temperature parameter, removing"
224
+ nil
225
+ else
226
+ temperature
227
+ end
228
+ end
229
+
230
+ def modalities_for(model_id)
231
+ modalities = {
232
+ input: ['text'],
233
+ output: ['text']
234
+ }
235
+
236
+ # Vision support
237
+ modalities[:input] << 'image' if supports_vision?(model_id)
238
+
239
+ # Audio support
240
+ modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
241
+
242
+ # PDF support
243
+ modalities[:input] << 'pdf' if supports_vision?(model_id)
244
+
245
+ # Output modalities
246
+ modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
247
+
248
+ modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
249
+
250
+ modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
251
+
252
+ modalities[:output] << 'moderation' if model_id.match?(/moderation/)
253
+
254
+ modalities
255
+ end
256
+
257
+ def capabilities_for(model_id) # rubocop:disable Metrics/PerceivedComplexity
258
+ capabilities = []
259
+
260
+ # Common capabilities
261
+ capabilities << 'streaming' unless model_id.match?(/moderation|embedding/)
262
+ capabilities << 'function_calling' if supports_functions?(model_id)
263
+ capabilities << 'structured_output' if supports_json_mode?(model_id)
264
+ capabilities << 'batch' if model_id.match?(/embedding|batch/)
265
+
266
+ # Advanced capabilities
267
+ capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
268
+
269
+ if model_id.match?(/gpt-4-turbo|gpt-4o/)
270
+ capabilities << 'image_generation' if model_id.match?(/vision/)
271
+ capabilities << 'speech_generation' if model_id.match?(/audio/)
272
+ capabilities << 'transcription' if model_id.match?(/audio/)
273
+ end
274
+
275
+ capabilities
276
+ end
277
+
278
+ def pricing_for(model_id)
279
+ standard_pricing = {
280
+ input_per_million: input_price_for(model_id),
281
+ output_per_million: output_price_for(model_id)
282
+ }
283
+
284
+ # Add cached pricing if available
285
+ if respond_to?(:cached_input_price_for)
286
+ cached_price = cached_input_price_for(model_id)
287
+ standard_pricing[:cached_input_per_million] = cached_price if cached_price
288
+ end
289
+
290
+ # Pricing structure
291
+ pricing = { text_tokens: { standard: standard_pricing } }
292
+
293
+ # Add batch pricing if applicable
294
+ if model_id.match?(/embedding|batch/)
295
+ pricing[:text_tokens][:batch] = {
296
+ input_per_million: standard_pricing[:input_per_million] * 0.5,
297
+ output_per_million: standard_pricing[:output_per_million] * 0.5
298
+ }
299
+ end
300
+
301
+ pricing
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Chat methods of the OpenAI API integration
7
+ module Chat
8
+ def completion_url
9
+ 'chat/completions'
10
+ end
11
+
12
+ module_function
13
+
14
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, cache_prompts: {}) # rubocop:disable Lint/UnusedMethodArgument, Metrics/ParameterLists
15
+ payload = {
16
+ model: model,
17
+ messages: format_messages(messages),
18
+ stream: stream
19
+ }
20
+
21
+ # Only include temperature if it's not nil (some models don't accept it)
22
+ payload[:temperature] = temperature unless temperature.nil?
23
+
24
+ payload[:tools] = tools.map { |_, tool| chat_tool_for(tool) } if tools.any?
25
+
26
+ if schema
27
+ # Use strict mode from schema if specified, default to true
28
+ strict = schema[:strict] != false
29
+
30
+ payload[:response_format] = {
31
+ type: 'json_schema',
32
+ json_schema: {
33
+ name: 'response',
34
+ schema: schema,
35
+ strict: strict
36
+ }
37
+ }
38
+ end
39
+
40
+ payload[:stream_options] = { include_usage: true } if stream
41
+ payload
42
+ end
43
+
44
+ def parse_completion_response(response)
45
+ data = response.body
46
+ return if data.empty?
47
+
48
+ raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')
49
+
50
+ message_data = data.dig('choices', 0, 'message')
51
+ return unless message_data
52
+
53
+ Message.new(
54
+ role: :assistant,
55
+ content: message_data['content'],
56
+ tool_calls: parse_tool_calls(message_data['tool_calls']),
57
+ input_tokens: data['usage']['prompt_tokens'],
58
+ output_tokens: data['usage']['completion_tokens'],
59
+ cached_tokens: data.dig('usage', 'prompt_tokens_details', 'cached_tokens'),
60
+ model_id: data['model'],
61
+ raw: response
62
+ )
63
+ end
64
+
65
+ def format_messages(messages)
66
+ messages.map do |msg|
67
+ {
68
+ role: format_role(msg.role),
69
+ content: Media.format_content(msg.content),
70
+ tool_calls: format_tool_calls(msg.tool_calls),
71
+ tool_call_id: msg.tool_call_id
72
+ }.compact
73
+ end
74
+ end
75
+
76
+ def format_role(role)
77
+ case role
78
+ when :system
79
+ @config.openai_use_system_role ? 'system' : 'developer'
80
+ else
81
+ role.to_s
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Embeddings methods of the OpenAI API integration
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(...)
11
+ 'embeddings'
12
+ end
13
+
14
+ def render_embedding_payload(text, model:, dimensions:)
15
+ {
16
+ model: model,
17
+ input: text,
18
+ dimensions: dimensions
19
+ }.compact
20
+ end
21
+
22
+ def parse_embedding_response(response, model:, text:)
23
+ data = response.body
24
+ input_tokens = data.dig('usage', 'prompt_tokens') || 0
25
+ vectors = data['data'].map { |d| d['embedding'] }
26
+
27
+ # If we only got one embedding AND the input was a single string (not an array),
28
+ # return it as a single vector
29
+ vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
30
+
31
+ Embedding.new(vectors:, model:, input_tokens:)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Image generation methods for the OpenAI API integration
7
+ module Images
8
+ module_function
9
+
10
+ def images_url
11
+ 'images/generations'
12
+ end
13
+
14
+ def render_image_payload(prompt, model:, size:)
15
+ {
16
+ model: model,
17
+ prompt: prompt,
18
+ n: 1,
19
+ size: size
20
+ }
21
+ end
22
+
23
+ def parse_image_response(response, model:)
24
+ data = response.body
25
+ image_data = data['data'].first
26
+
27
+ Image.new(
28
+ url: image_data['url'],
29
+ mime_type: 'image/png', # DALL-E typically returns PNGs
30
+ revised_prompt: image_data['revised_prompt'],
31
+ model_id: model,
32
+ data: image_data['b64_json']
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Handles formatting of media content (images, audio) for OpenAI APIs
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content)
11
+ # Convert Hash/Array back to JSON string for API
12
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
13
+ return content unless content.is_a?(Content)
14
+
15
+ parts = []
16
+ parts << format_text(content.text) if content.text
17
+
18
+ content.attachments.each do |attachment|
19
+ case attachment.type
20
+ when :image
21
+ parts << format_image(attachment)
22
+ when :pdf
23
+ parts << format_pdf(attachment)
24
+ when :audio
25
+ parts << format_audio(attachment)
26
+ when :text
27
+ parts << format_text_file(attachment)
28
+ else
29
+ raise UnsupportedAttachmentError, attachment.type
30
+ end
31
+ end
32
+
33
+ parts
34
+ end
35
+
36
+ def format_image(image)
37
+ {
38
+ type: 'image_url',
39
+ image_url: {
40
+ url: image.url? ? image.source : "data:#{image.mime_type};base64,#{image.encoded}"
41
+ }
42
+ }
43
+ end
44
+
45
+ def format_pdf(pdf)
46
+ {
47
+ type: 'file',
48
+ file: {
49
+ filename: pdf.filename,
50
+ file_data: "data:#{pdf.mime_type};base64,#{pdf.encoded}"
51
+ }
52
+ }
53
+ end
54
+
55
+ def format_text_file(text_file)
56
+ {
57
+ type: 'text',
58
+ text: Utils.format_text_file_for_llm(text_file)
59
+ }
60
+ end
61
+
62
+ def format_audio(audio)
63
+ {
64
+ type: 'input_audio',
65
+ input_audio: {
66
+ data: audio.encoded,
67
+ format: audio.mime_type.split('/').last
68
+ }
69
+ }
70
+ end
71
+
72
+ def format_text(text)
73
+ {
74
+ type: 'text',
75
+ text: text
76
+ }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Models methods of the OpenAI API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug, capabilities)
15
+ Array(response.body['data']).map do |model_data|
16
+ model_id = model_data['id']
17
+
18
+ Model::Info.new(
19
+ id: model_id,
20
+ name: capabilities.format_display_name(model_id),
21
+ provider: slug,
22
+ family: capabilities.model_family(model_id),
23
+ created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
24
+ context_window: capabilities.context_window_for(model_id),
25
+ max_output_tokens: capabilities.max_tokens_for(model_id),
26
+ modalities: capabilities.modalities_for(model_id),
27
+ capabilities: capabilities.capabilities_for(model_id),
28
+ pricing: capabilities.pricing_for(model_id),
29
+ metadata: {
30
+ object: model_data['object'],
31
+ owned_by: model_data['owned_by']
32
+ }
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end