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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Mistral API integration.
6
+ class Mistral < OpenAI
7
+ include Mistral::Chat
8
+ include Mistral::Models
9
+ include Mistral::Embeddings
10
+
11
+ def api_base
12
+ 'https://api.mistral.ai/v1'
13
+ end
14
+
15
+ def headers
16
+ {
17
+ 'Authorization' => "Bearer #{@config.mistral_api_key}"
18
+ }
19
+ end
20
+
21
+ class << self
22
+ def capabilities
23
+ Mistral::Capabilities
24
+ end
25
+
26
+ def configuration_requirements
27
+ %i[mistral_api_key]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Chat methods of the Ollama API integration
7
+ module Chat
8
+ module_function
9
+
10
+ def format_messages(messages)
11
+ messages.map do |msg|
12
+ {
13
+ role: format_role(msg.role),
14
+ content: Ollama::Media.format_content(msg.content),
15
+ tool_calls: format_tool_calls(msg.tool_calls),
16
+ tool_call_id: msg.tool_call_id
17
+ }.compact
18
+ end
19
+ end
20
+
21
+ def format_role(role)
22
+ role.to_s
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Handles formatting of media content (images, audio) for Ollama APIs
7
+ module Media
8
+ extend OpenAI::Media
9
+
10
+ module_function
11
+
12
+ def format_content(content)
13
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
14
+ return content unless content.is_a?(Content)
15
+
16
+ parts = []
17
+ parts << format_text(content.text) if content.text
18
+
19
+ content.attachments.each do |attachment|
20
+ case attachment.type
21
+ when :image
22
+ parts << Ollama::Media.format_image(attachment)
23
+ when :text
24
+ parts << format_text_file(attachment)
25
+ else
26
+ raise UnsupportedAttachmentError, attachment.mime_type
27
+ end
28
+ end
29
+
30
+ parts
31
+ end
32
+
33
+ def format_image(image)
34
+ {
35
+ type: 'image_url',
36
+ image_url: {
37
+ url: image.for_llm,
38
+ detail: 'auto'
39
+ }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Models methods for the Ollama API integration
7
+ module Models
8
+ def models_url
9
+ 'models'
10
+ end
11
+
12
+ def parse_list_models_response(response, slug, _capabilities)
13
+ data = response.body['data'] || []
14
+ data.map do |model|
15
+ Model::Info.new(
16
+ id: model['id'],
17
+ name: model['id'],
18
+ provider: slug,
19
+ family: 'ollama',
20
+ created_at: model['created'] ? Time.at(model['created']) : nil,
21
+ modalities: {
22
+ input: %w[text image],
23
+ output: %w[text]
24
+ },
25
+ capabilities: %w[streaming function_calling structured_output vision],
26
+ pricing: {},
27
+ metadata: {
28
+ owned_by: model['owned_by']
29
+ }
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Ollama API integration.
6
+ class Ollama < OpenAI
7
+ include Ollama::Chat
8
+ include Ollama::Media
9
+ include Ollama::Models
10
+
11
+ def api_base
12
+ @config.ollama_api_base
13
+ end
14
+
15
+ def headers
16
+ {}
17
+ end
18
+
19
+ class << self
20
+ def configuration_requirements
21
+ %i[ollama_api_base]
22
+ end
23
+
24
+ def local?
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,291 @@
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
+ modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
239
+ modalities[:input] << 'pdf' if supports_vision?(model_id)
240
+ modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
241
+ modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
242
+ modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
243
+ modalities[:output] << 'moderation' if model_id.match?(/moderation/)
244
+
245
+ modalities
246
+ end
247
+
248
+ def capabilities_for(model_id) # rubocop:disable Metrics/PerceivedComplexity
249
+ capabilities = []
250
+
251
+ capabilities << 'streaming' unless model_id.match?(/moderation|embedding/)
252
+ capabilities << 'function_calling' if supports_functions?(model_id)
253
+ capabilities << 'structured_output' if supports_json_mode?(model_id)
254
+ capabilities << 'batch' if model_id.match?(/embedding|batch/)
255
+ capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
256
+
257
+ if model_id.match?(/gpt-4-turbo|gpt-4o/)
258
+ capabilities << 'image_generation' if model_id.match?(/vision/)
259
+ capabilities << 'speech_generation' if model_id.match?(/audio/)
260
+ capabilities << 'transcription' if model_id.match?(/audio/)
261
+ end
262
+
263
+ capabilities
264
+ end
265
+
266
+ def pricing_for(model_id)
267
+ standard_pricing = {
268
+ input_per_million: input_price_for(model_id),
269
+ output_per_million: output_price_for(model_id)
270
+ }
271
+
272
+ if respond_to?(:cached_input_price_for)
273
+ cached_price = cached_input_price_for(model_id)
274
+ standard_pricing[:cached_input_per_million] = cached_price if cached_price
275
+ end
276
+
277
+ pricing = { text_tokens: { standard: standard_pricing } }
278
+
279
+ if model_id.match?(/embedding|batch/)
280
+ pricing[:text_tokens][:batch] = {
281
+ input_per_million: standard_pricing[:input_per_million] * 0.5,
282
+ output_per_million: standard_pricing[:output_per_million] * 0.5
283
+ }
284
+ end
285
+
286
+ pricing
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,83 @@
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) # rubocop:disable Metrics/ParameterLists
15
+ payload = {
16
+ model: model.id,
17
+ messages: format_messages(messages),
18
+ stream: stream
19
+ }
20
+
21
+ payload[:temperature] = temperature unless temperature.nil?
22
+ payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any?
23
+
24
+ if schema
25
+ strict = schema[:strict] != false
26
+
27
+ payload[:response_format] = {
28
+ type: 'json_schema',
29
+ json_schema: {
30
+ name: 'response',
31
+ schema: schema,
32
+ strict: strict
33
+ }
34
+ }
35
+ end
36
+
37
+ payload[:stream_options] = { include_usage: true } if stream
38
+ payload
39
+ end
40
+
41
+ def parse_completion_response(response)
42
+ data = response.body
43
+ return if data.empty?
44
+
45
+ raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')
46
+
47
+ message_data = data.dig('choices', 0, 'message')
48
+ return unless message_data
49
+
50
+ Message.new(
51
+ role: :assistant,
52
+ content: message_data['content'],
53
+ tool_calls: parse_tool_calls(message_data['tool_calls']),
54
+ input_tokens: data['usage']['prompt_tokens'],
55
+ output_tokens: data['usage']['completion_tokens'],
56
+ model_id: data['model'],
57
+ raw: response
58
+ )
59
+ end
60
+
61
+ def format_messages(messages)
62
+ messages.map do |msg|
63
+ {
64
+ role: format_role(msg.role),
65
+ content: Media.format_content(msg.content),
66
+ tool_calls: format_tool_calls(msg.tool_calls),
67
+ tool_call_id: msg.tool_call_id
68
+ }.compact
69
+ end
70
+ end
71
+
72
+ def format_role(role)
73
+ case role
74
+ when :system
75
+ @config.openai_use_system_role ? 'system' : 'developer'
76
+ else
77
+ role.to_s
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,33 @@
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
+ vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
27
+
28
+ Embedding.new(vectors:, model:, input_tokens:)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ 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