dify_llm 1.9.1 → 1.14.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -8
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
  51. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
  52. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
  53. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
  54. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
  55. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  56. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  57. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  59. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  60. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  61. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  64. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  65. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  66. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +1 -1
  67. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  68. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  69. data/lib/ruby_llm/active_record/acts_as.rb +10 -4
  70. data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
  71. data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
  72. data/lib/ruby_llm/active_record/message_methods.rb +58 -8
  73. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  74. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  75. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  76. data/lib/ruby_llm/agent.rb +365 -0
  77. data/lib/ruby_llm/aliases.json +199 -62
  78. data/lib/ruby_llm/attachment.rb +15 -4
  79. data/lib/ruby_llm/chat.rb +150 -22
  80. data/lib/ruby_llm/configuration.rb +65 -65
  81. data/lib/ruby_llm/connection.rb +11 -7
  82. data/lib/ruby_llm/content.rb +6 -2
  83. data/lib/ruby_llm/error.rb +37 -1
  84. data/lib/ruby_llm/message.rb +43 -15
  85. data/lib/ruby_llm/model/info.rb +15 -13
  86. data/lib/ruby_llm/models.json +37560 -14094
  87. data/lib/ruby_llm/models.rb +321 -38
  88. data/lib/ruby_llm/models_schema.json +2 -2
  89. data/lib/ruby_llm/provider.rb +26 -4
  90. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
  91. data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
  92. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  93. data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
  94. data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
  95. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
  96. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  97. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  98. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  99. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  100. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  101. data/lib/ruby_llm/providers/azure.rb +148 -0
  102. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  103. data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
  104. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  105. data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
  106. data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
  107. data/lib/ruby_llm/providers/bedrock.rb +69 -52
  108. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
  109. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  110. data/lib/ruby_llm/providers/dify/chat.rb +82 -7
  111. data/lib/ruby_llm/providers/dify/media.rb +2 -2
  112. data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
  113. data/lib/ruby_llm/providers/dify.rb +4 -0
  114. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
  115. data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
  116. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  117. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  118. data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
  119. data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
  120. data/lib/ruby_llm/providers/gemini.rb +4 -0
  121. data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
  122. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  123. data/lib/ruby_llm/providers/gpustack.rb +8 -0
  124. data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
  125. data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
  126. data/lib/ruby_llm/providers/mistral.rb +4 -0
  127. data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
  128. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  129. data/lib/ruby_llm/providers/ollama.rb +11 -1
  130. data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
  131. data/lib/ruby_llm/providers/openai/chat.rb +101 -7
  132. data/lib/ruby_llm/providers/openai/media.rb +5 -2
  133. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  134. data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
  135. data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
  136. data/lib/ruby_llm/providers/openai/tools.rb +27 -2
  137. data/lib/ruby_llm/providers/openai.rb +11 -1
  138. data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
  139. data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
  140. data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
  141. data/lib/ruby_llm/providers/openrouter.rb +37 -1
  142. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  143. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  144. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  145. data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
  146. data/lib/ruby_llm/providers/vertexai.rb +23 -7
  147. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  148. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  149. data/lib/ruby_llm/providers/xai.rb +32 -0
  150. data/lib/ruby_llm/stream_accumulator.rb +120 -18
  151. data/lib/ruby_llm/streaming.rb +82 -60
  152. data/lib/ruby_llm/thinking.rb +49 -0
  153. data/lib/ruby_llm/tokens.rb +47 -0
  154. data/lib/ruby_llm/tool.rb +49 -4
  155. data/lib/ruby_llm/tool_call.rb +6 -3
  156. data/lib/ruby_llm/version.rb +1 -1
  157. data/lib/ruby_llm.rb +14 -8
  158. data/lib/tasks/models.rake +62 -23
  159. data/lib/tasks/release.rake +1 -1
  160. data/lib/tasks/ruby_llm.rake +9 -1
  161. data/lib/tasks/vcr.rake +33 -1
  162. metadata +67 -16
  163. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
  164. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  165. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  166. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  167. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
  168. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  169. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
  170. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
@@ -3,22 +3,26 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Anthropic
6
- # Chat methods of the OpenAI API integration
6
+ # Chat methods for the Anthropic API implementation
7
7
  module Chat
8
8
  module_function
9
9
 
10
10
  def completion_url
11
- '/v1/messages'
11
+ 'v1/messages'
12
12
  end
13
13
 
14
- def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
14
+ # rubocop:disable Metrics/ParameterLists
15
+ def render_payload(messages, tools:, temperature:, model:, stream: false,
16
+ schema: nil, thinking: nil, tool_prefs: nil)
17
+ tool_prefs ||= {}
15
18
  system_messages, chat_messages = separate_messages(messages)
16
19
  system_content = build_system_content(system_messages)
17
20
 
18
- build_base_payload(chat_messages, model, stream).tap do |payload|
19
- add_optional_fields(payload, system_content:, tools:, temperature:)
21
+ build_base_payload(chat_messages, model, stream, thinking).tap do |payload|
22
+ add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
20
23
  end
21
24
  end
25
+ # rubocop:enable Metrics/ParameterLists
22
26
 
23
27
  def separate_messages(messages)
24
28
  messages.partition { |msg| msg.role == :system }
@@ -45,19 +49,37 @@ module RubyLLM
45
49
  end
46
50
  end
47
51
 
48
- def build_base_payload(chat_messages, model, stream)
49
- {
52
+ def build_base_payload(chat_messages, model, stream, thinking)
53
+ payload = {
50
54
  model: model.id,
51
- messages: chat_messages.map { |msg| format_message(msg) },
55
+ messages: chat_messages.map { |msg| format_message(msg, thinking: thinking) },
52
56
  stream: stream,
53
57
  max_tokens: model.max_tokens || 4096
54
58
  }
59
+
60
+ thinking_payload = build_thinking_payload(thinking)
61
+ payload[:thinking] = thinking_payload if thinking_payload
62
+
63
+ payload
55
64
  end
56
65
 
57
- def add_optional_fields(payload, system_content:, tools:, temperature:)
58
- payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any?
66
+ def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil) # rubocop:disable Metrics/ParameterLists
67
+ if tools.any?
68
+ payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
69
+ unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
70
+ payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
71
+ end
72
+ end
59
73
  payload[:system] = system_content unless system_content.empty?
60
74
  payload[:temperature] = temperature unless temperature.nil?
75
+ payload[:output_config] = build_output_config(schema) if schema
76
+ end
77
+
78
+ def build_output_config(schema)
79
+ normalized = RubyLLM::Utils.deep_dup(schema[:schema])
80
+ normalized.delete(:strict)
81
+ normalized.delete('strict')
82
+ { format: { type: 'json_schema', schema: normalized } }
61
83
  end
62
84
 
63
85
  def parse_completion_response(response)
@@ -65,9 +87,11 @@ module RubyLLM
65
87
  content_blocks = data['content'] || []
66
88
 
67
89
  text_content = extract_text_content(content_blocks)
90
+ thinking_content = extract_thinking_content(content_blocks)
91
+ thinking_signature = extract_thinking_signature(content_blocks)
68
92
  tool_use_blocks = Tools.find_tool_uses(content_blocks)
69
93
 
70
- build_message(data, text_content, tool_use_blocks, response)
94
+ build_message(data, text_content, thinking_content, thinking_signature, tool_use_blocks, response)
71
95
  end
72
96
 
73
97
  def extract_text_content(blocks)
@@ -75,50 +99,158 @@ module RubyLLM
75
99
  text_blocks.map { |c| c['text'] }.join
76
100
  end
77
101
 
78
- def build_message(data, content, tool_use_blocks, response)
102
+ def extract_thinking_content(blocks)
103
+ thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
104
+ thoughts = thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
105
+ thoughts.empty? ? nil : thoughts
106
+ end
107
+
108
+ def extract_thinking_signature(blocks)
109
+ thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
110
+ blocks.find { |c| c['type'] == 'redacted_thinking' }
111
+ thinking_block&.dig('signature') || thinking_block&.dig('data')
112
+ end
113
+
114
+ def build_message(data, content, thinking, thinking_signature, tool_use_blocks, response) # rubocop:disable Metrics/ParameterLists
79
115
  usage = data['usage'] || {}
80
116
  cached_tokens = usage['cache_read_input_tokens']
81
117
  cache_creation_tokens = usage['cache_creation_input_tokens']
82
118
  if cache_creation_tokens.nil? && usage['cache_creation'].is_a?(Hash)
83
119
  cache_creation_tokens = usage['cache_creation'].values.compact.sum
84
120
  end
121
+ thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
122
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
123
+ usage['thinking_tokens'] ||
124
+ usage['reasoning_tokens']
85
125
 
86
126
  Message.new(
87
127
  role: :assistant,
88
128
  content: content,
129
+ thinking: Thinking.build(text: thinking, signature: thinking_signature),
89
130
  tool_calls: Tools.parse_tool_calls(tool_use_blocks),
90
131
  input_tokens: usage['input_tokens'],
91
132
  output_tokens: usage['output_tokens'],
92
133
  cached_tokens: cached_tokens,
93
134
  cache_creation_tokens: cache_creation_tokens,
135
+ thinking_tokens: thinking_tokens,
94
136
  model_id: data['model'],
95
137
  raw: response
96
138
  )
97
139
  end
98
140
 
99
- def format_message(msg)
141
+ def format_message(msg, thinking: nil)
142
+ thinking_enabled = thinking&.enabled?
143
+
100
144
  if msg.tool_call?
101
- Tools.format_tool_call(msg)
145
+ format_tool_call_with_thinking(msg, thinking_enabled)
102
146
  elsif msg.tool_result?
103
147
  Tools.format_tool_result(msg)
104
148
  else
105
- format_basic_message(msg)
149
+ format_basic_message_with_thinking(msg, thinking_enabled)
106
150
  end
107
151
  end
108
152
 
109
- def format_basic_message(msg)
153
+ def format_basic_message_with_thinking(msg, thinking_enabled)
154
+ content_blocks = []
155
+
156
+ if msg.role == :assistant && thinking_enabled
157
+ thinking_block = build_thinking_block(msg.thinking)
158
+ content_blocks << thinking_block if thinking_block
159
+ end
160
+
161
+ append_formatted_content(content_blocks, msg.content)
162
+
110
163
  {
111
164
  role: convert_role(msg.role),
112
- content: Media.format_content(msg.content)
165
+ content: content_blocks
166
+ }
167
+ end
168
+
169
+ def format_tool_call_with_thinking(msg, thinking_enabled)
170
+ if msg.content.is_a?(RubyLLM::Content::Raw)
171
+ content_blocks = msg.content.value
172
+ content_blocks = [content_blocks] unless content_blocks.is_a?(Array)
173
+ content_blocks = prepend_thinking_block(content_blocks, msg, thinking_enabled)
174
+
175
+ return { role: 'assistant', content: content_blocks }
176
+ end
177
+
178
+ content_blocks = prepend_thinking_block([], msg, thinking_enabled)
179
+ content_blocks << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
180
+
181
+ msg.tool_calls.each_value do |tool_call|
182
+ content_blocks << {
183
+ type: 'tool_use',
184
+ id: tool_call.id,
185
+ name: tool_call.name,
186
+ input: tool_call.arguments
187
+ }
188
+ end
189
+
190
+ {
191
+ role: 'assistant',
192
+ content: content_blocks
113
193
  }
114
194
  end
115
195
 
196
+ def prepend_thinking_block(content_blocks, msg, thinking_enabled)
197
+ return content_blocks unless thinking_enabled
198
+
199
+ thinking_block = build_thinking_block(msg.thinking)
200
+ content_blocks.unshift(thinking_block) if thinking_block
201
+
202
+ content_blocks
203
+ end
204
+
205
+ def build_thinking_block(thinking)
206
+ return nil unless thinking
207
+
208
+ if thinking.text
209
+ {
210
+ type: 'thinking',
211
+ thinking: thinking.text,
212
+ signature: thinking.signature
213
+ }.compact
214
+ elsif thinking.signature
215
+ {
216
+ type: 'redacted_thinking',
217
+ data: thinking.signature
218
+ }
219
+ end
220
+ end
221
+
222
+ def append_formatted_content(content_blocks, content)
223
+ formatted_content = Media.format_content(content)
224
+ if formatted_content.is_a?(Array)
225
+ content_blocks.concat(formatted_content)
226
+ else
227
+ content_blocks << formatted_content
228
+ end
229
+ end
230
+
116
231
  def convert_role(role)
117
232
  case role
118
233
  when :tool, :user then 'user'
119
234
  else 'assistant'
120
235
  end
121
236
  end
237
+
238
+ def build_thinking_payload(thinking)
239
+ return nil unless thinking&.enabled?
240
+
241
+ budget = resolve_budget(thinking)
242
+ raise ArgumentError, 'Anthropic thinking requires a budget' if budget.nil?
243
+
244
+ {
245
+ type: 'enabled',
246
+ budget_tokens: budget
247
+ }
248
+ end
249
+
250
+ def resolve_budget(thinking)
251
+ budget = thinking.respond_to?(:budget) ? thinking.budget : thinking
252
+ budget.is_a?(Integer) ? budget : nil
253
+ end
122
254
  end
123
255
  end
124
256
  end
@@ -44,7 +44,7 @@ module RubyLLM
44
44
  type: 'image',
45
45
  source: {
46
46
  type: 'url',
47
- url: image.source
47
+ url: image.source.to_s
48
48
  }
49
49
  }
50
50
  else
@@ -65,7 +65,7 @@ module RubyLLM
65
65
  type: 'document',
66
66
  source: {
67
67
  type: 'url',
68
- url: pdf.source
68
+ url: pdf.source.to_s
69
69
  }
70
70
  }
71
71
  else
@@ -8,24 +8,18 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def models_url
11
- '/v1/models'
11
+ 'v1/models'
12
12
  end
13
13
 
14
- def parse_list_models_response(response, slug, capabilities)
14
+ def parse_list_models_response(response, slug, _capabilities)
15
15
  Array(response.body['data']).map do |model_data|
16
16
  model_id = model_data['id']
17
17
 
18
18
  Model::Info.new(
19
19
  id: model_id,
20
- name: model_data['display_name'],
20
+ name: model_data['display_name'] || model_id,
21
21
  provider: slug,
22
- family: capabilities.model_family(model_id),
23
22
  created_at: Time.parse(model_data['created_at']),
24
- context_window: capabilities.determine_context_window(model_id),
25
- max_output_tokens: capabilities.determine_max_tokens(model_id),
26
- modalities: capabilities.modalities_for(model_id),
27
- capabilities: capabilities.capabilities_for(model_id),
28
- pricing: capabilities.pricing_for(model_id),
29
23
  metadata: {}
30
24
  )
31
25
  end
@@ -12,10 +12,16 @@ module RubyLLM
12
12
  end
13
13
 
14
14
  def build_chunk(data)
15
+ delta_type = data.dig('delta', 'type')
16
+
15
17
  Chunk.new(
16
18
  role: :assistant,
17
19
  model_id: extract_model_id(data),
18
- content: data.dig('delta', 'text'),
20
+ content: extract_content_delta(data, delta_type),
21
+ thinking: Thinking.build(
22
+ text: extract_thinking_delta(data, delta_type),
23
+ signature: extract_signature_delta(data, delta_type)
24
+ ),
19
25
  input_tokens: extract_input_tokens(data),
20
26
  output_tokens: extract_output_tokens(data),
21
27
  cached_tokens: extract_cached_tokens(data),
@@ -24,6 +30,24 @@ module RubyLLM
24
30
  )
25
31
  end
26
32
 
33
+ def extract_content_delta(data, delta_type)
34
+ return data.dig('delta', 'text') if delta_type == 'text_delta'
35
+
36
+ nil
37
+ end
38
+
39
+ def extract_thinking_delta(data, delta_type)
40
+ return data.dig('delta', 'thinking') if delta_type == 'thinking_delta'
41
+
42
+ nil
43
+ end
44
+
45
+ def extract_signature_delta(data, delta_type)
46
+ return data.dig('delta', 'signature') if delta_type == 'signature_delta'
47
+
48
+ nil
49
+ end
50
+
27
51
  def json_delta?(data)
28
52
  data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
29
53
  end
@@ -103,6 +103,26 @@ module RubyLLM
103
103
  'strict' => true
104
104
  }
105
105
  end
106
+
107
+ def build_tool_choice(tool_prefs)
108
+ tool_choice = tool_prefs[:choice]
109
+ calls_in_response = tool_prefs[:calls]
110
+ tool_choice = :auto if tool_choice.nil?
111
+
112
+ {
113
+ type: case tool_choice
114
+ when :auto, :none
115
+ tool_choice
116
+ when :required
117
+ :any
118
+ else
119
+ :tool
120
+ end
121
+ }.tap do |tc|
122
+ tc[:name] = tool_choice if tc[:type] == :tool
123
+ tc[:disable_parallel_tool_use] = calls_in_response == :one if tc[:type] != :none && !calls_in_response.nil?
124
+ end
125
+ end
106
126
  end
107
127
  end
108
128
  end
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  include Anthropic::Tools
13
13
 
14
14
  def api_base
15
- 'https://api.anthropic.com'
15
+ @config.anthropic_api_base || 'https://api.anthropic.com'
16
16
  end
17
17
 
18
18
  def headers
@@ -27,6 +27,10 @@ module RubyLLM
27
27
  Anthropic::Capabilities
28
28
  end
29
29
 
30
+ def configuration_options
31
+ %i[anthropic_api_key anthropic_api_base]
32
+ end
33
+
30
34
  def configuration_requirements
31
35
  %i[anthropic_api_key]
32
36
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Chat methods of the Azure AI Foundry API integration
7
+ module Chat
8
+ def completion_url
9
+ azure_endpoint(:chat)
10
+ end
11
+
12
+ def format_messages(messages)
13
+ messages.map do |msg|
14
+ {
15
+ role: format_role(msg.role),
16
+ content: Media.format_content(msg.content),
17
+ tool_calls: format_tool_calls(msg.tool_calls),
18
+ tool_call_id: msg.tool_call_id
19
+ }.compact.merge(format_thinking(msg))
20
+ end
21
+ end
22
+
23
+ def format_role(role)
24
+ role.to_s
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Embeddings methods of the Azure AI Foundry API integration
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(...)
11
+ azure_endpoint(:embeddings)
12
+ end
13
+
14
+ def render_embedding_payload(text, model:, dimensions:)
15
+ {
16
+ model: model,
17
+ input: [text].flatten,
18
+ dimensions: dimensions
19
+ }.compact
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Handles formatting of media content (images, audio) for Azure OpenAI-compatible APIs.
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
+ return content.value if content.is_a?(RubyLLM::Content::Raw)
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 << OpenAI::Media.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 :audio
23
+ parts << OpenAI::Media.format_audio(attachment)
24
+ when :text
25
+ parts << OpenAI::Media.format_text_file(attachment)
26
+ else
27
+ raise UnsupportedAttachmentError, attachment.type
28
+ end
29
+ end
30
+
31
+ parts
32
+ end
33
+
34
+ def format_image(image)
35
+ {
36
+ type: 'image_url',
37
+ image_url: {
38
+ url: image.for_llm
39
+ }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Azure
6
+ # Models methods of the Azure AI Foundry API integration
7
+ module Models
8
+ def models_url
9
+ azure_endpoint(:models)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Azure AI Foundry / OpenAI-compatible API integration.
6
+ class Azure < OpenAI
7
+ AZURE_DEFAULT_CHAT_API_VERSION = '2024-05-01-preview'
8
+ AZURE_DEFAULT_MODELS_API_VERSION = 'preview'
9
+
10
+ include Azure::Chat
11
+ include Azure::Embeddings
12
+ include Azure::Media
13
+ include Azure::Models
14
+
15
+ def api_base
16
+ @config.azure_api_base
17
+ end
18
+
19
+ def headers
20
+ if @config.azure_api_key
21
+ { 'api-key' => @config.azure_api_key }
22
+ else
23
+ { 'Authorization' => "Bearer #{@config.azure_ai_auth_token}" }
24
+ end
25
+ end
26
+
27
+ def configured?
28
+ self.class.configured?(@config)
29
+ end
30
+
31
+ def azure_endpoint(kind)
32
+ parts = azure_base_parts
33
+
34
+ case kind
35
+ when :chat
36
+ chat_endpoint(parts)
37
+ when :embeddings
38
+ embeddings_endpoint(parts)
39
+ when :models
40
+ models_endpoint(parts)
41
+ else
42
+ raise ArgumentError, "Unknown Azure endpoint kind: #{kind.inspect}"
43
+ end
44
+ end
45
+
46
+ class << self
47
+ def configuration_options
48
+ %i[azure_api_base azure_api_key azure_ai_auth_token]
49
+ end
50
+
51
+ def configuration_requirements
52
+ %i[azure_api_base]
53
+ end
54
+
55
+ def configured?(config)
56
+ config.azure_api_base && (config.azure_api_key || config.azure_ai_auth_token)
57
+ end
58
+
59
+ # Azure works with deployment names, instead of model names
60
+ def assume_models_exist?
61
+ true
62
+ end
63
+ end
64
+
65
+ def ensure_configured!
66
+ missing = []
67
+ missing << :azure_api_base unless @config.azure_api_base
68
+ if @config.azure_api_key.nil? && @config.azure_ai_auth_token.nil?
69
+ missing << 'azure_api_key or azure_ai_auth_token'
70
+ end
71
+ return if missing.empty?
72
+
73
+ raise ConfigurationError,
74
+ "Missing configuration for Azure: #{missing.join(', ')}"
75
+ end
76
+
77
+ private
78
+
79
+ def azure_base_parts
80
+ @azure_base_parts ||= begin
81
+ raw_base = api_base.to_s.sub(%r{/+\z}, '')
82
+ version = raw_base[/[?&]api-version=([^&]+)/i, 1]
83
+ path_base = raw_base.sub(/\?.*\z/, '')
84
+
85
+ mode = if path_base.include?('/chat/completions')
86
+ :chat_endpoint
87
+ elsif path_base.include?('/openai/deployments/')
88
+ :deployment_base
89
+ elsif path_base.include?('/openai/v1')
90
+ :openai_v1_base
91
+ else
92
+ :resource_base
93
+ end
94
+
95
+ {
96
+ raw_base: raw_base,
97
+ path_base: path_base,
98
+ root: azure_host_root(path_base),
99
+ mode: mode,
100
+ version: version
101
+ }
102
+ end
103
+ end
104
+
105
+ def chat_endpoint(parts)
106
+ case parts[:mode]
107
+ when :chat_endpoint
108
+ ''
109
+ when :deployment_base
110
+ with_api_version('chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
111
+ when :openai_v1_base
112
+ with_api_version('chat/completions', parts[:version])
113
+ else
114
+ with_api_version('models/chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
115
+ end
116
+ end
117
+
118
+ def embeddings_endpoint(parts)
119
+ case parts[:mode]
120
+ when :deployment_base, :openai_v1_base
121
+ with_api_version('embeddings', parts[:version])
122
+ else
123
+ "#{parts[:root]}/openai/v1/embeddings"
124
+ end
125
+ end
126
+
127
+ def models_endpoint(parts)
128
+ case parts[:mode]
129
+ when :openai_v1_base
130
+ with_api_version('models', parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION)
131
+ else
132
+ "#{parts[:root]}/openai/v1/models?api-version=#{parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION}"
133
+ end
134
+ end
135
+
136
+ def with_api_version(path, version)
137
+ return path unless version
138
+
139
+ separator = path.include?('?') ? '&' : '?'
140
+ "#{path}#{separator}api-version=#{version}"
141
+ end
142
+
143
+ def azure_host_root(base_without_query)
144
+ base_without_query.sub(%r{/(models|openai)/.*\z}, '').sub(%r{/+\z}, '')
145
+ end
146
+ end
147
+ end
148
+ end