dify_llm 1.9.2 → 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 (168) 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/upgrade_to_v1_7_generator.rb +2 -4
  67. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  68. data/lib/ruby_llm/active_record/acts_as.rb +10 -4
  69. data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
  70. data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
  71. data/lib/ruby_llm/active_record/message_methods.rb +58 -8
  72. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  73. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  74. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  75. data/lib/ruby_llm/agent.rb +365 -0
  76. data/lib/ruby_llm/aliases.json +106 -61
  77. data/lib/ruby_llm/attachment.rb +8 -3
  78. data/lib/ruby_llm/chat.rb +150 -22
  79. data/lib/ruby_llm/configuration.rb +65 -65
  80. data/lib/ruby_llm/connection.rb +11 -7
  81. data/lib/ruby_llm/content.rb +6 -2
  82. data/lib/ruby_llm/error.rb +37 -1
  83. data/lib/ruby_llm/message.rb +43 -15
  84. data/lib/ruby_llm/model/info.rb +15 -13
  85. data/lib/ruby_llm/models.json +25039 -12260
  86. data/lib/ruby_llm/models.rb +185 -24
  87. data/lib/ruby_llm/provider.rb +26 -4
  88. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
  89. data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
  90. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  91. data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
  92. data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
  93. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
  94. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  95. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  96. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  97. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  98. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  99. data/lib/ruby_llm/providers/azure.rb +148 -0
  100. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  101. data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
  102. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  103. data/lib/ruby_llm/providers/bedrock/models.rb +104 -65
  104. data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
  105. data/lib/ruby_llm/providers/bedrock.rb +69 -52
  106. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
  107. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  108. data/lib/ruby_llm/providers/dify/chat.rb +82 -7
  109. data/lib/ruby_llm/providers/dify/media.rb +2 -2
  110. data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
  111. data/lib/ruby_llm/providers/dify.rb +4 -0
  112. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
  113. data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
  114. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  115. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  116. data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
  117. data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
  118. data/lib/ruby_llm/providers/gemini.rb +4 -0
  119. data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
  120. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  121. data/lib/ruby_llm/providers/gpustack.rb +8 -0
  122. data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
  123. data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
  124. data/lib/ruby_llm/providers/mistral.rb +4 -0
  125. data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
  126. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  127. data/lib/ruby_llm/providers/ollama.rb +11 -1
  128. data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
  129. data/lib/ruby_llm/providers/openai/chat.rb +101 -7
  130. data/lib/ruby_llm/providers/openai/media.rb +5 -2
  131. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  132. data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
  133. data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
  134. data/lib/ruby_llm/providers/openai/tools.rb +27 -2
  135. data/lib/ruby_llm/providers/openai.rb +11 -1
  136. data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
  137. data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
  138. data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
  139. data/lib/ruby_llm/providers/openrouter.rb +37 -1
  140. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  141. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  142. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  143. data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
  144. data/lib/ruby_llm/providers/vertexai.rb +23 -7
  145. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  146. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  147. data/lib/ruby_llm/providers/xai.rb +32 -0
  148. data/lib/ruby_llm/stream_accumulator.rb +120 -18
  149. data/lib/ruby_llm/streaming.rb +60 -57
  150. data/lib/ruby_llm/thinking.rb +49 -0
  151. data/lib/ruby_llm/tokens.rb +47 -0
  152. data/lib/ruby_llm/tool.rb +48 -3
  153. data/lib/ruby_llm/tool_call.rb +6 -3
  154. data/lib/ruby_llm/version.rb +1 -1
  155. data/lib/ruby_llm.rb +14 -8
  156. data/lib/tasks/models.rake +61 -22
  157. data/lib/tasks/release.rake +1 -1
  158. data/lib/tasks/ruby_llm.rake +9 -1
  159. data/lib/tasks/vcr.rake +33 -1
  160. metadata +67 -16
  161. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
  162. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  163. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  164. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  165. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
  166. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  167. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
  168. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
data/lib/ruby_llm/tool.rb CHANGED
@@ -99,9 +99,13 @@ module RubyLLM
99
99
  end
100
100
 
101
101
  def call(args)
102
- RubyLLM.logger.debug "Tool #{name} called with: #{args.inspect}"
103
- result = execute(**args.transform_keys(&:to_sym))
104
- RubyLLM.logger.debug "Tool #{name} returned: #{result.inspect}"
102
+ normalized_args = normalize_args(args)
103
+ validation_error = validate_keyword_arguments(normalized_args)
104
+ return { error: "Invalid tool arguments: #{validation_error}" } if validation_error
105
+
106
+ RubyLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
107
+ result = execute(**normalized_args)
108
+ RubyLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
105
109
  result
106
110
  end
107
111
 
@@ -115,6 +119,47 @@ module RubyLLM
115
119
  Halt.new(message)
116
120
  end
117
121
 
122
+ def normalize_args(args)
123
+ return {} if args.nil?
124
+ return args.transform_keys(&:to_sym) if args.respond_to?(:transform_keys)
125
+
126
+ {}
127
+ end
128
+
129
+ def validate_keyword_arguments(arguments)
130
+ required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
131
+
132
+ return nil if required_keywords.empty? && optional_keywords.empty?
133
+
134
+ argument_keys = arguments.keys
135
+ missing_keyword = first_missing_keyword(required_keywords, argument_keys)
136
+ return "missing keyword: #{missing_keyword}" if missing_keyword
137
+ return nil if accepts_extra_keywords
138
+
139
+ allowed_keywords = required_keywords + optional_keywords
140
+ unknown_keyword = first_unknown_keyword(argument_keys, allowed_keywords)
141
+ return "unknown keyword: #{unknown_keyword}" if unknown_keyword
142
+
143
+ nil
144
+ end
145
+
146
+ def execute_keyword_signature
147
+ keyword_signature = method(:execute).parameters
148
+ required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
149
+ optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
150
+ accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
151
+
152
+ [required_keywords, optional_keywords, accepts_extra_keywords]
153
+ end
154
+
155
+ def first_missing_keyword(required_keywords, argument_keys)
156
+ (required_keywords - argument_keys).first
157
+ end
158
+
159
+ def first_unknown_keyword(argument_keys, allowed_keywords)
160
+ (argument_keys - allowed_keywords).first
161
+ end
162
+
118
163
  # Wraps schema handling for tool parameters, supporting JSON Schema hashes,
119
164
  # RubyLLM::Schema instances/classes, and DSL blocks.
120
165
  class SchemaDefinition
@@ -4,19 +4,22 @@ module RubyLLM
4
4
  # Represents a function call from an AI model to a Tool.
5
5
  class ToolCall
6
6
  attr_reader :id, :name, :arguments
7
+ attr_accessor :thought_signature
7
8
 
8
- def initialize(id:, name:, arguments: {})
9
+ def initialize(id:, name:, arguments: {}, thought_signature: nil)
9
10
  @id = id
10
11
  @name = name
11
12
  @arguments = arguments
13
+ @thought_signature = thought_signature
12
14
  end
13
15
 
14
16
  def to_h
15
17
  {
16
18
  id: @id,
17
19
  name: @name,
18
- arguments: @arguments
19
- }
20
+ arguments: @arguments,
21
+ thought_signature: @thought_signature
22
+ }.compact
20
23
  end
21
24
  end
22
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.9.2'
4
+ VERSION = '1.14.1'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -10,23 +10,27 @@ require 'json'
10
10
  require 'logger'
11
11
  require 'marcel'
12
12
  require 'securerandom'
13
+ require 'date'
14
+ require 'time'
13
15
  require 'zeitwerk'
14
16
 
15
17
  loader = Zeitwerk::Loader.for_gem
16
18
  loader.inflector.inflect(
17
- 'ruby_llm' => 'RubyLLM',
18
- 'llm' => 'LLM',
19
- 'openai' => 'OpenAI',
19
+ 'azure' => 'Azure',
20
+ 'UI' => 'UI',
20
21
  'api' => 'API',
21
- 'deepseek' => 'DeepSeek',
22
- 'perplexity' => 'Perplexity',
23
22
  'bedrock' => 'Bedrock',
24
- 'openrouter' => 'OpenRouter',
23
+ 'deepseek' => 'DeepSeek',
25
24
  'gpustack' => 'GPUStack',
25
+ 'llm' => 'LLM',
26
26
  'mistral' => 'Mistral',
27
- 'vertexai' => 'VertexAI',
27
+ 'openai' => 'OpenAI',
28
+ 'openrouter' => 'OpenRouter',
28
29
  'pdf' => 'PDF',
29
- 'UI' => 'UI'
30
+ 'perplexity' => 'Perplexity',
31
+ 'ruby_llm' => 'RubyLLM',
32
+ 'vertexai' => 'VertexAI',
33
+ 'xai' => 'XAI'
30
34
  )
31
35
  loader.ignore("#{__dir__}/tasks")
32
36
  loader.ignore("#{__dir__}/generators")
@@ -92,6 +96,7 @@ module RubyLLM
92
96
  end
93
97
 
94
98
  RubyLLM::Provider.register :anthropic, RubyLLM::Providers::Anthropic
99
+ RubyLLM::Provider.register :azure, RubyLLM::Providers::Azure
95
100
  RubyLLM::Provider.register :bedrock, RubyLLM::Providers::Bedrock
96
101
  RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
97
102
  RubyLLM::Provider.register :dify, RubyLLM::Providers::Dify
@@ -103,6 +108,7 @@ RubyLLM::Provider.register :openai, RubyLLM::Providers::OpenAI
103
108
  RubyLLM::Provider.register :openrouter, RubyLLM::Providers::OpenRouter
104
109
  RubyLLM::Provider.register :perplexity, RubyLLM::Providers::Perplexity
105
110
  RubyLLM::Provider.register :vertexai, RubyLLM::Providers::VertexAI
111
+ RubyLLM::Provider.register :xai, RubyLLM::Providers::XAI
106
112
 
107
113
  if defined?(Rails::Railtie)
108
114
  require 'ruby_llm/railtie'
@@ -39,15 +39,18 @@ end
39
39
 
40
40
  def configure_from_env
41
41
  RubyLLM.configure do |config|
42
- config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
43
42
  config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
44
- config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
43
+ config.azure_api_base = ENV.fetch('AZURE_API_BASE', nil)
44
+ config.azure_api_key = ENV.fetch('AZURE_API_KEY', nil)
45
45
  config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
46
- config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
47
- config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
46
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
48
47
  config.mistral_api_key = ENV.fetch('MISTRAL_API_KEY', nil)
48
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
49
+ config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
50
+ config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
49
51
  config.vertexai_location = ENV.fetch('GOOGLE_CLOUD_LOCATION', nil)
50
52
  config.vertexai_project_id = ENV.fetch('GOOGLE_CLOUD_PROJECT', nil)
53
+ config.xai_api_key = ENV.fetch('XAI_API_KEY', nil)
51
54
  configure_bedrock(config)
52
55
  config.request_timeout = 30
53
56
  end
@@ -61,7 +64,8 @@ def configure_bedrock(config)
61
64
  end
62
65
 
63
66
  def refresh_models
64
- initial_count = RubyLLM.models.all.size
67
+ existing_models = RubyLLM::Models.read_from_json
68
+ initial_count = existing_models.size
65
69
  puts "Refreshing models (#{initial_count} cached)..."
66
70
 
67
71
  models = RubyLLM.models.refresh!
@@ -69,19 +73,29 @@ def refresh_models
69
73
  if models.all.empty? && initial_count.zero?
70
74
  puts 'Error: Failed to fetch models.'
71
75
  exit(1)
72
- elsif models.all.size == initial_count && initial_count.positive?
73
- puts 'Warning: Model list unchanged.'
74
76
  else
75
- puts 'Validating models...'
76
- validate_models!(models)
77
+ existing_data = sorted_models_data(existing_models)
78
+ new_data = sorted_models_data(models.all)
79
+
80
+ if new_data == existing_data && initial_count.positive?
81
+ puts 'Warning: Model list unchanged.'
82
+ else
83
+ puts 'Validating models...'
84
+ validate_models!(models)
77
85
 
78
- puts "Saving models.json (#{models.all.size} models)"
79
- models.save_to_json
86
+ puts "Saving models.json (#{models.all.size} models)"
87
+ models.save_to_json
88
+ end
80
89
  end
81
90
 
82
91
  @models = models
83
92
  end
84
93
 
94
+ def sorted_models_data(models)
95
+ models.map(&:to_h)
96
+ .sort_by { |model| [model[:provider].to_s, model[:id].to_s] }
97
+ end
98
+
85
99
  def validate_models!(models)
86
100
  schema_path = RubyLLM::Models.schema_file
87
101
  models_data = models.all.map(&:to_h)
@@ -130,12 +144,17 @@ def status(provider_sym)
130
144
  end
131
145
 
132
146
  def generate_models_markdown
147
+ models = RubyLLM.models.all
148
+ total_models = models.count
149
+ provider_count = models.map(&:provider).uniq.count
150
+ generated_on = Time.now.utc.strftime('%Y-%m-%d')
151
+
133
152
  <<~MARKDOWN
134
153
  ---
135
154
  layout: default
136
155
  title: Available Models
137
156
  nav_order: 1
138
- description: Browse hundreds of AI models from every major provider. Always up-to-date, automatically generated.
157
+ description: Browse #{total_models} AI models across #{provider_count} providers (not including local providers). Updated #{generated_on}.
139
158
  redirect_from:
140
159
  - /guides/available-models
141
160
  ---
@@ -154,17 +173,19 @@ def generate_models_markdown
154
173
 
155
174
  ---
156
175
 
157
- ## Model Data Sources
176
+ _Model information enriched by [models.dev](https://models.dev) and our custom code._
177
+
178
+ Can't find a newly released model? Refresh your registry:
158
179
 
159
- - **OpenAI, Anthropic, DeepSeek, Gemini, VertexAI**: Enriched by [models.dev](https://models.dev/) *([LLM metadata API](https://models.dev/api.json))*
160
- - **OpenRouter**: Direct API
161
- - **Others**: Local capabilities files
180
+ ```ruby
181
+ # Plain Ruby
182
+ RubyLLM.models.refresh!
162
183
 
163
- ## Last Updated
164
- {: .d-inline-block }
184
+ # Rails
185
+ Model.refresh!
186
+ ```
165
187
 
166
- #{Time.now.utc.strftime('%Y-%m-%d')}
167
- {: .label .label-green }
188
+ See [Model Registry: Refreshing the Registry]({% link _advanced/models.md %}#refreshing-the-registry).
168
189
 
169
190
  ## Models by Provider
170
191
 
@@ -265,8 +286,8 @@ end
265
286
  def models_table(models)
266
287
  return '*No models found*' if models.none?
267
288
 
268
- headers = ['Model', 'Provider', 'Context', 'Max Output', 'Standard Pricing (per 1M tokens)']
269
- alignment = [':--', ':--', '--:', '--:', ':--']
289
+ headers = ['Model', 'Provider', 'I/O', 'Capabilities', 'Context', 'Max Output', 'Standard Pricing (per 1M tokens)']
290
+ alignment = [':--', ':--', ':--', ':--', '--:', '--:', ':--']
270
291
 
271
292
  rows = models.sort_by { |m| [m.provider, m.name] }.map do |model|
272
293
  pricing = standard_pricing_display(model)
@@ -274,6 +295,8 @@ def models_table(models)
274
295
  [
275
296
  model.id,
276
297
  model.provider,
298
+ modalities_display(model),
299
+ list_display(model.capabilities),
277
300
  model.context_window || '-',
278
301
  model.max_output_tokens || '-',
279
302
  pricing
@@ -291,6 +314,19 @@ def models_table(models)
291
314
  table.join("\n")
292
315
  end
293
316
 
317
+ def modalities_display(model)
318
+ input_modalities = list_display(model.modalities.input)
319
+ output_modalities = list_display(model.modalities.output)
320
+ "In: #{input_modalities}; Out: #{output_modalities}"
321
+ end
322
+
323
+ def list_display(values)
324
+ items = Array(values).compact.map(&:to_s).reject(&:empty?)
325
+ return '-' if items.empty?
326
+
327
+ items.join(', ')
328
+ end
329
+
294
330
  def standard_pricing_display(model)
295
331
  pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
296
332
 
@@ -323,6 +359,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
323
359
  # OpenAI models
324
360
  models['openai'].each do |model|
325
361
  openrouter_model = "openai/#{model}"
362
+ azure_model = models['azure'].include?(model) ? model : nil
326
363
  next unless models['openrouter'].include?(openrouter_model)
327
364
 
328
365
  alias_key = model.gsub('-latest', '')
@@ -330,6 +367,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
330
367
  'openai' => model,
331
368
  'openrouter' => openrouter_model
332
369
  }
370
+ aliases[alias_key]['azure'] = azure_model if azure_model
333
371
  end
334
372
 
335
373
  anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
@@ -350,6 +388,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
350
388
  aliases[base_name] = { 'anthropic' => latest_model }
351
389
  aliases[base_name]['openrouter'] = openrouter_model if openrouter_model
352
390
  aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
391
+ aliases[base_name]['azure'] = latest_model if models['azure'].include?(latest_model)
353
392
  end
354
393
 
355
394
  models['bedrock'].each do |bedrock_model|
@@ -26,7 +26,7 @@ namespace :release do # rubocop:disable Metrics/BlockLength
26
26
  if stale_count.positive?
27
27
  puts "\n🗑️ Removed #{stale_count} stale cassettes"
28
28
  puts '🔄 Re-recording cassettes...'
29
- system('bundle exec rspec') || exit(1)
29
+ run_test_queue_rspec || exit(1)
30
30
  puts '✅ Cassettes refreshed!'
31
31
  else
32
32
  puts '✅ No stale cassettes found'
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ def run_test_queue_rspec
4
+ workers = ENV.fetch('RSPEC_WORKERS', nil)
5
+ env = {}
6
+ env['TEST_QUEUE_WORKERS'] = workers if workers && !workers.empty? && ENV.fetch('TEST_QUEUE_WORKERS', '').empty?
7
+
8
+ system(env, 'bundle', 'exec', 'bin/rspec-queue')
9
+ end
10
+
3
11
  namespace :ruby_llm do
4
12
  desc 'Load models from models.json into the database'
5
13
  task load_models: :environment do
@@ -9,7 +17,7 @@ namespace :ruby_llm do
9
17
  model_class.save_to_database
10
18
  puts "✅ Loaded #{model_class.count} models into database"
11
19
  else
12
- puts 'Model registry not configured. Run rails generate ruby_llm:install'
20
+ puts 'Model registry not configured. Run bin/rails generate ruby_llm:install'
13
21
  end
14
22
  end
15
23
  end
data/lib/tasks/vcr.rake CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dotenv/load'
4
+ require 'time'
4
5
 
5
6
  def record_all_cassettes(cassette_dir)
6
7
  FileUtils.rm_rf(cassette_dir)
@@ -70,7 +71,32 @@ def delete_cassettes(cassettes)
70
71
  end
71
72
 
72
73
  def run_tests
73
- system('bundle exec rspec') || abort('Tests failed')
74
+ run_test_queue_rspec || abort('Tests failed')
75
+ end
76
+
77
+ def retimestamp_cassettes(cassette_dir)
78
+ timestamp = Time.now.utc.httpdate
79
+ updated_files = 0
80
+ updated_entries = 0
81
+
82
+ Dir.glob("#{cassette_dir}/**/*.yml").each do |file|
83
+ content = File.read(file)
84
+ replacements = 0
85
+
86
+ updated = content.gsub(/^(\s*recorded_at:\s*).+$/) do
87
+ replacements += 1
88
+ "#{Regexp.last_match(1)}#{timestamp}"
89
+ end
90
+
91
+ next if replacements.zero?
92
+
93
+ File.write(file, updated)
94
+ updated_files += 1
95
+ updated_entries += replacements
96
+ end
97
+
98
+ puts "Updated #{updated_entries} recorded_at entries in #{updated_files} cassette files."
99
+ puts "New recorded_at value: #{timestamp}"
74
100
  end
75
101
 
76
102
  namespace :vcr do
@@ -89,4 +115,10 @@ namespace :vcr do
89
115
  record_for_providers(providers, cassette_dir)
90
116
  end
91
117
  end
118
+
119
+ desc 'Update recorded_at timestamps for all cassette entries'
120
+ task :retimestamp do
121
+ cassette_dir = 'spec/fixtures/vcr_cassettes'
122
+ retimestamp_cassettes(cassette_dir)
123
+ end
92
124
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dify_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -99,28 +99,28 @@ dependencies:
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '1.0'
102
+ version: '1'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '1.0'
109
+ version: '1'
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: ruby_llm-schema
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 0.2.1
116
+ version: '0'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 0.2.1
123
+ version: '0'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: zeitwerk
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -151,21 +151,49 @@ files:
151
151
  - LICENSE
152
152
  - README.md
153
153
  - lib/dify_llm.rb
154
+ - lib/generators/ruby_llm/agent/agent_generator.rb
155
+ - lib/generators/ruby_llm/agent/templates/agent.rb.tt
156
+ - lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt
154
157
  - lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb
155
158
  - lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt
156
159
  - lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt
157
160
  - lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt
161
+ - lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt
158
162
  - lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt
163
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt
164
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt
165
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt
166
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt
167
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt
168
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt
169
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt
170
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt
171
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt
172
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt
173
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt
174
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt
175
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt
176
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt
177
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt
178
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt
179
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt
180
+ - lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt
159
181
  - lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt
160
182
  - lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt
161
183
  - lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt
162
184
  - lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt
163
185
  - lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt
186
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt
164
187
  - lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt
188
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt
165
189
  - lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt
166
- - lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt
190
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt
191
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt
167
192
  - lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt
193
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt
168
194
  - lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt
195
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt
196
+ - lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt
169
197
  - lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt
170
198
  - lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt
171
199
  - lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt
@@ -181,6 +209,16 @@ files:
181
209
  - lib/generators/ruby_llm/install/templates/message_model.rb.tt
182
210
  - lib/generators/ruby_llm/install/templates/model_model.rb.tt
183
211
  - lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt
212
+ - lib/generators/ruby_llm/schema/schema_generator.rb
213
+ - lib/generators/ruby_llm/schema/templates/schema.rb.tt
214
+ - lib/generators/ruby_llm/tool/templates/tool.rb.tt
215
+ - lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt
216
+ - lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt
217
+ - lib/generators/ruby_llm/tool/tool_generator.rb
218
+ - lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt
219
+ - lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb
220
+ - lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt
221
+ - lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb
184
222
  - lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt
185
223
  - lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb
186
224
  - lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt
@@ -191,6 +229,9 @@ files:
191
229
  - lib/ruby_llm/active_record/chat_methods.rb
192
230
  - lib/ruby_llm/active_record/message_methods.rb
193
231
  - lib/ruby_llm/active_record/model_methods.rb
232
+ - lib/ruby_llm/active_record/payload_helpers.rb
233
+ - lib/ruby_llm/active_record/tool_call_methods.rb
234
+ - lib/ruby_llm/agent.rb
194
235
  - lib/ruby_llm/aliases.json
195
236
  - lib/ruby_llm/aliases.rb
196
237
  - lib/ruby_llm/attachment.rb
@@ -225,18 +266,17 @@ files:
225
266
  - lib/ruby_llm/providers/anthropic/models.rb
226
267
  - lib/ruby_llm/providers/anthropic/streaming.rb
227
268
  - lib/ruby_llm/providers/anthropic/tools.rb
269
+ - lib/ruby_llm/providers/azure.rb
270
+ - lib/ruby_llm/providers/azure/chat.rb
271
+ - lib/ruby_llm/providers/azure/embeddings.rb
272
+ - lib/ruby_llm/providers/azure/media.rb
273
+ - lib/ruby_llm/providers/azure/models.rb
228
274
  - lib/ruby_llm/providers/bedrock.rb
229
- - lib/ruby_llm/providers/bedrock/capabilities.rb
275
+ - lib/ruby_llm/providers/bedrock/auth.rb
230
276
  - lib/ruby_llm/providers/bedrock/chat.rb
231
277
  - lib/ruby_llm/providers/bedrock/media.rb
232
278
  - lib/ruby_llm/providers/bedrock/models.rb
233
- - lib/ruby_llm/providers/bedrock/signing.rb
234
279
  - lib/ruby_llm/providers/bedrock/streaming.rb
235
- - lib/ruby_llm/providers/bedrock/streaming/base.rb
236
- - lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb
237
- - lib/ruby_llm/providers/bedrock/streaming/message_processing.rb
238
- - lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb
239
- - lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb
240
280
  - lib/ruby_llm/providers/deepseek.rb
241
281
  - lib/ruby_llm/providers/deepseek/capabilities.rb
242
282
  - lib/ruby_llm/providers/deepseek/chat.rb
@@ -256,6 +296,7 @@ files:
256
296
  - lib/ruby_llm/providers/gemini/tools.rb
257
297
  - lib/ruby_llm/providers/gemini/transcription.rb
258
298
  - lib/ruby_llm/providers/gpustack.rb
299
+ - lib/ruby_llm/providers/gpustack/capabilities.rb
259
300
  - lib/ruby_llm/providers/gpustack/chat.rb
260
301
  - lib/ruby_llm/providers/gpustack/media.rb
261
302
  - lib/ruby_llm/providers/gpustack/models.rb
@@ -265,6 +306,7 @@ files:
265
306
  - lib/ruby_llm/providers/mistral/embeddings.rb
266
307
  - lib/ruby_llm/providers/mistral/models.rb
267
308
  - lib/ruby_llm/providers/ollama.rb
309
+ - lib/ruby_llm/providers/ollama/capabilities.rb
268
310
  - lib/ruby_llm/providers/ollama/chat.rb
269
311
  - lib/ruby_llm/providers/ollama/media.rb
270
312
  - lib/ruby_llm/providers/ollama/models.rb
@@ -277,10 +319,14 @@ files:
277
319
  - lib/ruby_llm/providers/openai/models.rb
278
320
  - lib/ruby_llm/providers/openai/moderation.rb
279
321
  - lib/ruby_llm/providers/openai/streaming.rb
322
+ - lib/ruby_llm/providers/openai/temperature.rb
280
323
  - lib/ruby_llm/providers/openai/tools.rb
281
324
  - lib/ruby_llm/providers/openai/transcription.rb
282
325
  - lib/ruby_llm/providers/openrouter.rb
326
+ - lib/ruby_llm/providers/openrouter/chat.rb
327
+ - lib/ruby_llm/providers/openrouter/images.rb
283
328
  - lib/ruby_llm/providers/openrouter/models.rb
329
+ - lib/ruby_llm/providers/openrouter/streaming.rb
284
330
  - lib/ruby_llm/providers/perplexity.rb
285
331
  - lib/ruby_llm/providers/perplexity/capabilities.rb
286
332
  - lib/ruby_llm/providers/perplexity/chat.rb
@@ -291,9 +337,14 @@ files:
291
337
  - lib/ruby_llm/providers/vertexai/models.rb
292
338
  - lib/ruby_llm/providers/vertexai/streaming.rb
293
339
  - lib/ruby_llm/providers/vertexai/transcription.rb
340
+ - lib/ruby_llm/providers/xai.rb
341
+ - lib/ruby_llm/providers/xai/chat.rb
342
+ - lib/ruby_llm/providers/xai/models.rb
294
343
  - lib/ruby_llm/railtie.rb
295
344
  - lib/ruby_llm/stream_accumulator.rb
296
345
  - lib/ruby_llm/streaming.rb
346
+ - lib/ruby_llm/thinking.rb
347
+ - lib/ruby_llm/tokens.rb
297
348
  - lib/ruby_llm/tool.rb
298
349
  - lib/ruby_llm/tool_call.rb
299
350
  - lib/ruby_llm/transcription.rb
@@ -315,7 +366,7 @@ metadata:
315
366
  funding_uri: https://github.com/sponsors/crmne
316
367
  rubygems_mfa_required: 'true'
317
368
  post_install_message: |
318
- Upgrading from RubyLLM <= 1.8.x? Check the upgrade guide for new features and migration instructions
369
+ Upgrading from RubyLLM < 1.14.x? Check the upgrade guide for new features and migration instructions
319
370
  --> https://rubyllm.com/upgrading/
320
371
  rdoc_options: []
321
372
  require_paths:
@@ -324,14 +375,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
324
375
  requirements:
325
376
  - - ">="
326
377
  - !ruby/object:Gem::Version
327
- version: 3.1.0
378
+ version: 3.1.3
328
379
  required_rubygems_version: !ruby/object:Gem::Requirement
329
380
  requirements:
330
381
  - - ">="
331
382
  - !ruby/object:Gem::Version
332
383
  version: '0'
333
384
  requirements: []
334
- rubygems_version: 4.0.3
385
+ rubygems_version: 4.0.9
335
386
  specification_version: 4
336
387
  summary: One beautiful Ruby API for GPT, Claude, Gemini, and more.
337
388
  test_files: []
@@ -1,13 +0,0 @@
1
- <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>" class="<%= message_variable_name %>"
2
- style="margin-bottom: 20px; padding: 10px; border-left: 3px solid <%%= <%= message_model_name.demodulize.underscore %>.role == 'user' ? '#007bff' : '#28a745' %>">
3
- <div style="font-weight: bold; margin-bottom: 5px;">
4
- <%%= <%= message_model_name.demodulize.underscore %>.role&.capitalize %>
5
- </div>
6
- <div id="<%= message_variable_name %>_<%%= <%= message_model_name.demodulize.underscore %>.id %>_content" style="white-space: pre-wrap;"><%%= <%= message_model_name.demodulize.underscore %>.content %></div>
7
- <%% if <%= message_model_name.demodulize.underscore %>.tool_call? %>
8
- <%%= render "<%= message_model_name.underscore.pluralize %>/tool_calls", <%= message_model_name.demodulize.underscore %>: <%= message_model_name.demodulize.underscore %> %>
9
- <%% end %>
10
- <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
11
- <%%= <%= message_model_name.demodulize.underscore %>.created_at&.strftime("%I:%M %p") %>
12
- </div>
13
- </div>