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.
- checksums.yaml +4 -4
- data/README.md +27 -8
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +10 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
- data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
- data/lib/ruby_llm/active_record/message_methods.rb +58 -8
- data/lib/ruby_llm/active_record/model_methods.rb +1 -1
- data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/ruby_llm/agent.rb +365 -0
- data/lib/ruby_llm/aliases.json +199 -62
- data/lib/ruby_llm/attachment.rb +15 -4
- data/lib/ruby_llm/chat.rb +150 -22
- data/lib/ruby_llm/configuration.rb +65 -65
- data/lib/ruby_llm/connection.rb +11 -7
- data/lib/ruby_llm/content.rb +6 -2
- data/lib/ruby_llm/error.rb +37 -1
- data/lib/ruby_llm/message.rb +43 -15
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +37560 -14094
- data/lib/ruby_llm/models.rb +321 -38
- data/lib/ruby_llm/models_schema.json +2 -2
- data/lib/ruby_llm/provider.rb +26 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +148 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
- data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
- data/lib/ruby_llm/providers/bedrock.rb +69 -52
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/dify/chat.rb +82 -7
- data/lib/ruby_llm/providers/dify/media.rb +2 -2
- data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
- data/lib/ruby_llm/providers/dify.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
- data/lib/ruby_llm/providers/gpustack.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
- data/lib/ruby_llm/providers/openai/chat.rb +101 -7
- data/lib/ruby_llm/providers/openai/media.rb +5 -2
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
- data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +11 -1
- data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
- data/lib/ruby_llm/providers/openrouter.rb +37 -1
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +4 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +23 -7
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +32 -0
- data/lib/ruby_llm/stream_accumulator.rb +120 -18
- data/lib/ruby_llm/streaming.rb +82 -60
- data/lib/ruby_llm/thinking.rb +49 -0
- data/lib/ruby_llm/tokens.rb +47 -0
- data/lib/ruby_llm/tool.rb +49 -4
- data/lib/ruby_llm/tool_call.rb +6 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +62 -23
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +33 -1
- metadata +67 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
- 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
|
|
6
|
+
# Chat methods for the Anthropic API implementation
|
|
7
7
|
module Chat
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def completion_url
|
|
11
|
-
'
|
|
11
|
+
'v1/messages'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
format_basic_message_with_thinking(msg, thinking_enabled)
|
|
106
150
|
end
|
|
107
151
|
end
|
|
108
152
|
|
|
109
|
-
def
|
|
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:
|
|
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
|
-
'
|
|
11
|
+
'v1/models'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def parse_list_models_response(response, slug,
|
|
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
|
|
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,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
|