dify_llm 1.8.2 → 1.9.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/lib/generators/ruby_llm/generator_helpers.rb +31 -10
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +5 -0
  6. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +7 -1
  7. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +1 -1
  8. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  9. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  10. data/lib/ruby_llm/active_record/acts_as.rb +22 -24
  11. data/lib/ruby_llm/active_record/chat_methods.rb +41 -13
  12. data/lib/ruby_llm/active_record/message_methods.rb +11 -2
  13. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  14. data/lib/ruby_llm/aliases.json +61 -32
  15. data/lib/ruby_llm/attachment.rb +44 -13
  16. data/lib/ruby_llm/chat.rb +13 -2
  17. data/lib/ruby_llm/configuration.rb +6 -1
  18. data/lib/ruby_llm/connection.rb +3 -3
  19. data/lib/ruby_llm/content.rb +23 -0
  20. data/lib/ruby_llm/message.rb +11 -6
  21. data/lib/ruby_llm/model/info.rb +4 -0
  22. data/lib/ruby_llm/models.json +9649 -8211
  23. data/lib/ruby_llm/models.rb +14 -22
  24. data/lib/ruby_llm/provider.rb +23 -1
  25. data/lib/ruby_llm/providers/anthropic/chat.rb +22 -3
  26. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  27. data/lib/ruby_llm/providers/anthropic/media.rb +3 -2
  28. data/lib/ruby_llm/providers/anthropic/models.rb +15 -0
  29. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  30. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -18
  31. data/lib/ruby_llm/providers/bedrock/media.rb +2 -1
  32. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +15 -0
  33. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +2 -0
  34. data/lib/ruby_llm/providers/dify/chat.rb +16 -5
  35. data/lib/ruby_llm/providers/gemini/chat.rb +352 -69
  36. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  37. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  38. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  39. data/lib/ruby_llm/providers/gemini.rb +2 -1
  40. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  41. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  42. data/lib/ruby_llm/providers/openai/chat.rb +7 -2
  43. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  44. data/lib/ruby_llm/providers/openai/streaming.rb +7 -2
  45. data/lib/ruby_llm/providers/openai/tools.rb +26 -6
  46. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  47. data/lib/ruby_llm/providers/openai.rb +1 -0
  48. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  49. data/lib/ruby_llm/providers/vertexai.rb +11 -11
  50. data/lib/ruby_llm/railtie.rb +24 -22
  51. data/lib/ruby_llm/stream_accumulator.rb +10 -4
  52. data/lib/ruby_llm/tool.rb +126 -0
  53. data/lib/ruby_llm/transcription.rb +35 -0
  54. data/lib/ruby_llm/utils.rb +46 -0
  55. data/lib/ruby_llm/version.rb +1 -1
  56. data/lib/ruby_llm.rb +7 -0
  57. metadata +24 -3
@@ -10,16 +10,19 @@ module RubyLLM
10
10
  @instance ||= new
11
11
  end
12
12
 
13
- def provider_for(model)
14
- Provider.for(model)
13
+ def schema_file
14
+ File.expand_path('models_schema.json', __dir__)
15
15
  end
16
16
 
17
- def models_file
18
- File.expand_path('models.json', __dir__)
17
+ def load_models(file = RubyLLM.config.model_registry_file)
18
+ read_from_json(file)
19
19
  end
20
20
 
21
- def schema_file
22
- File.expand_path('models_schema.json', __dir__)
21
+ def read_from_json(file = RubyLLM.config.model_registry_file)
22
+ data = File.exist?(file) ? File.read(file) : '[]'
23
+ JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
24
+ rescue JSON::ParserError
25
+ []
23
26
  end
24
27
 
25
28
  def refresh!(remote_only: false)
@@ -151,26 +154,15 @@ module RubyLLM
151
154
  end
152
155
 
153
156
  def initialize(models = nil)
154
- @models = models || load_models
155
- end
156
-
157
- def load_models
158
- read_from_json
159
- end
160
-
161
- def load_from_json!
162
- @models = read_from_json
157
+ @models = models || self.class.load_models
163
158
  end
164
159
 
165
- def read_from_json
166
- data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
167
- JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
168
- rescue JSON::ParserError
169
- []
160
+ def load_from_json!(file = RubyLLM.config.model_registry_file)
161
+ @models = self.class.read_from_json(file)
170
162
  end
171
163
 
172
- def save_to_json
173
- File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
164
+ def save_to_json(file = RubyLLM.config.model_registry_file)
165
+ File.write(file, JSON.pretty_generate(all.map(&:to_h)))
174
166
  end
175
167
 
176
168
  def all
@@ -82,6 +82,13 @@ module RubyLLM
82
82
  parse_image_response(response, model:)
83
83
  end
84
84
 
85
+ def transcribe(audio_file, model:, language:, **options)
86
+ file_part = build_audio_file_part(audio_file)
87
+ payload = render_transcription_payload(file_part, model:, language:, **options)
88
+ response = @connection.post transcription_url, payload
89
+ parse_transcription_response(response, model:)
90
+ end
91
+
85
92
  def configured?
86
93
  configuration_requirements.all? { |req| @config.send(req) }
87
94
  end
@@ -160,9 +167,13 @@ module RubyLLM
160
167
  providers[name.to_sym] = provider_class
161
168
  end
162
169
 
170
+ def resolve(name)
171
+ providers[name.to_sym]
172
+ end
173
+
163
174
  def for(model)
164
175
  model_info = Models.find(model)
165
- providers[model_info.provider.to_sym]
176
+ resolve model_info.provider
166
177
  end
167
178
 
168
179
  def providers
@@ -192,6 +203,17 @@ module RubyLLM
192
203
 
193
204
  private
194
205
 
206
+ def build_audio_file_part(file_path)
207
+ expanded_path = File.expand_path(file_path)
208
+ mime_type = Marcel::MimeType.for(Pathname.new(expanded_path))
209
+
210
+ Faraday::Multipart::FilePart.new(
211
+ expanded_path,
212
+ mime_type,
213
+ File.basename(expanded_path)
214
+ )
215
+ end
216
+
195
217
  def try_parse_json(maybe_json)
196
218
  return maybe_json unless maybe_json.is_a?(String)
197
219
 
@@ -25,6 +25,8 @@ module RubyLLM
25
25
  end
26
26
 
27
27
  def build_system_content(system_messages)
28
+ return [] if system_messages.empty?
29
+
28
30
  if system_messages.length > 1
29
31
  RubyLLM.logger.warn(
30
32
  "Anthropic's Claude implementation only supports a single system message. " \
@@ -32,7 +34,15 @@ module RubyLLM
32
34
  )
33
35
  end
34
36
 
35
- system_messages.map(&:content).join("\n\n")
37
+ system_messages.flat_map do |msg|
38
+ content = msg.content
39
+
40
+ if content.is_a?(RubyLLM::Content::Raw)
41
+ content.value
42
+ else
43
+ Media.format_content(content)
44
+ end
45
+ end
36
46
  end
37
47
 
38
48
  def build_base_payload(chat_messages, model, stream)
@@ -66,12 +76,21 @@ module RubyLLM
66
76
  end
67
77
 
68
78
  def build_message(data, content, tool_use_blocks, response)
79
+ usage = data['usage'] || {}
80
+ cached_tokens = usage['cache_read_input_tokens']
81
+ cache_creation_tokens = usage['cache_creation_input_tokens']
82
+ if cache_creation_tokens.nil? && usage['cache_creation'].is_a?(Hash)
83
+ cache_creation_tokens = usage['cache_creation'].values.compact.sum
84
+ end
85
+
69
86
  Message.new(
70
87
  role: :assistant,
71
88
  content: content,
72
89
  tool_calls: Tools.parse_tool_calls(tool_use_blocks),
73
- input_tokens: data.dig('usage', 'input_tokens'),
74
- output_tokens: data.dig('usage', 'output_tokens'),
90
+ input_tokens: usage['input_tokens'],
91
+ output_tokens: usage['output_tokens'],
92
+ cached_tokens: cached_tokens,
93
+ cache_creation_tokens: cache_creation_tokens,
75
94
  model_id: data['model'],
76
95
  raw: response
77
96
  )
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Anthropic
6
+ # Helper for constructing Anthropic native content blocks.
7
+ class Content
8
+ class << self
9
+ def new(text = nil, cache: false, cache_control: nil, parts: nil, **extras)
10
+ payload = resolve_payload(
11
+ text: text,
12
+ parts: parts,
13
+ cache: cache,
14
+ cache_control: cache_control,
15
+ extras: extras
16
+ )
17
+
18
+ RubyLLM::Content::Raw.new(payload)
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_payload(text:, parts:, cache:, cache_control:, extras:)
24
+ return Array(parts) if parts
25
+
26
+ raise ArgumentError, 'text or parts must be provided' if text.nil?
27
+
28
+ block = { type: 'text', text: text }.merge(extras)
29
+ control = determine_cache_control(cache_control, cache)
30
+ block[:cache_control] = control if control
31
+
32
+ [block]
33
+ end
34
+
35
+ def determine_cache_control(cache_control, cache_flag)
36
+ return cache_control if cache_control
37
+
38
+ { type: 'ephemeral' } if cache_flag
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -7,9 +7,10 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content)
10
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
+ return content.value if content.is_a?(RubyLLM::Content::Raw)
11
12
  return [format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
12
- return [format_text(content)] unless content.is_a?(Content)
13
+ return [format_text(content)] unless content.is_a?(RubyLLM::Content)
13
14
 
14
15
  parts = []
15
16
  parts << format_text(content.text) if content.text
@@ -42,6 +42,21 @@ module RubyLLM
42
42
  def extract_output_tokens(data)
43
43
  data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
44
44
  end
45
+
46
+ def extract_cached_tokens(data)
47
+ data.dig('message', 'usage', 'cache_read_input_tokens') || data.dig('usage', 'cache_read_input_tokens')
48
+ end
49
+
50
+ def extract_cache_creation_tokens(data)
51
+ direct = data.dig('message', 'usage',
52
+ 'cache_creation_input_tokens') || data.dig('usage', 'cache_creation_input_tokens')
53
+ return direct if direct
54
+
55
+ breakdown = data.dig('message', 'usage', 'cache_creation') || data.dig('usage', 'cache_creation')
56
+ return unless breakdown.is_a?(Hash)
57
+
58
+ breakdown.values.compact.sum
59
+ end
45
60
  end
46
61
  end
47
62
  end
@@ -18,6 +18,8 @@ module RubyLLM
18
18
  content: data.dig('delta', 'text'),
19
19
  input_tokens: extract_input_tokens(data),
20
20
  output_tokens: extract_output_tokens(data),
21
+ cached_tokens: extract_cached_tokens(data),
22
+ cache_creation_tokens: extract_cache_creation_tokens(data),
21
23
  tool_calls: extract_tool_calls(data)
22
24
  )
23
25
  end
@@ -12,6 +12,8 @@ module RubyLLM
12
12
  end
13
13
 
14
14
  def format_tool_call(msg)
15
+ return { role: 'assistant', content: msg.content.value } if msg.content.is_a?(RubyLLM::Content::Raw)
16
+
15
17
  content = []
16
18
 
17
19
  content << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
@@ -29,7 +31,7 @@ module RubyLLM
29
31
  def format_tool_result(msg)
30
32
  {
31
33
  role: 'user',
32
- content: [format_tool_result_block(msg)]
34
+ content: msg.content.is_a?(RubyLLM::Content::Raw) ? msg.content.value : [format_tool_result_block(msg)]
33
35
  }
34
36
  end
35
37
 
@@ -51,15 +53,18 @@ module RubyLLM
51
53
  end
52
54
 
53
55
  def function_for(tool)
54
- {
56
+ input_schema = tool.params_schema ||
57
+ RubyLLM::Tool::SchemaDefinition.from_parameters(tool.parameters)&.json_schema
58
+
59
+ declaration = {
55
60
  name: tool.name,
56
61
  description: tool.description,
57
- input_schema: {
58
- type: 'object',
59
- properties: clean_parameters(tool.parameters),
60
- required: required_parameters(tool.parameters)
61
- }
62
+ input_schema: input_schema || default_input_schema
62
63
  }
64
+
65
+ return declaration if tool.provider_params.empty?
66
+
67
+ RubyLLM::Utils.deep_merge(declaration, tool.provider_params)
63
68
  end
64
69
 
65
70
  def extract_tool_calls(data)
@@ -89,17 +94,14 @@ module RubyLLM
89
94
  tool_calls.empty? ? nil : tool_calls
90
95
  end
91
96
 
92
- def clean_parameters(parameters)
93
- parameters.transform_values do |param|
94
- {
95
- type: param.type,
96
- description: param.description
97
- }.compact
98
- end
99
- end
100
-
101
- def required_parameters(parameters)
102
- parameters.select { |_, param| param.required }.keys
97
+ def default_input_schema
98
+ {
99
+ 'type' => 'object',
100
+ 'properties' => {},
101
+ 'required' => [],
102
+ 'additionalProperties' => false,
103
+ 'strict' => true
104
+ }
103
105
  end
104
106
  end
105
107
  end
@@ -10,7 +10,8 @@ module RubyLLM
10
10
 
11
11
  module_function
12
12
 
13
- def format_content(content)
13
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
14
+ return content.value if content.is_a?(RubyLLM::Content::Raw)
14
15
  return [Anthropic::Media.format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
15
16
  return [Anthropic::Media.format_text(content)] unless content.is_a?(Content)
16
17
 
@@ -32,6 +32,21 @@ module RubyLLM
32
32
  data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
33
33
  end
34
34
 
35
+ def extract_cached_tokens(data)
36
+ data.dig('message', 'usage', 'cache_read_input_tokens') || data.dig('usage', 'cache_read_input_tokens')
37
+ end
38
+
39
+ def extract_cache_creation_tokens(data)
40
+ direct = data.dig('message', 'usage',
41
+ 'cache_creation_input_tokens') || data.dig('usage', 'cache_creation_input_tokens')
42
+ return direct if direct
43
+
44
+ breakdown = data.dig('message', 'usage', 'cache_creation') || data.dig('usage', 'cache_creation')
45
+ return unless breakdown.is_a?(Hash)
46
+
47
+ breakdown.values.compact.sum
48
+ end
49
+
35
50
  private
36
51
 
37
52
  def extract_content_by_type(data)
@@ -59,6 +59,8 @@ module RubyLLM
59
59
  content: extract_streaming_content(data),
60
60
  input_tokens: extract_input_tokens(data),
61
61
  output_tokens: extract_output_tokens(data),
62
+ cached_tokens: extract_cached_tokens(data),
63
+ cache_creation_tokens: extract_cache_creation_tokens(data),
62
64
  tool_calls: extract_tool_calls(data)
63
65
  }
64
66
  end
@@ -6,12 +6,23 @@ module RubyLLM
6
6
  # Chat methods of the Dify API integration
7
7
  module Chat
8
8
  def upload_document(document_path, original_filename = nil)
9
- pn = Pathname.new(document_path)
9
+ path_like = if document_path.respond_to?(:path)
10
+ document_path.path
11
+ elsif document_path.respond_to?(:to_path)
12
+ document_path.to_path
13
+ else
14
+ document_path
15
+ end
16
+ pn = Pathname.new(path_like)
10
17
  mime_type = RubyLLM::MimeType.for pn
11
- original_filename ||= document_path.is_a?(String) ? pn.basename : (document_path.is_a?(Tempfile) ? File.basename(document_path) : document_path.original_filename)
18
+ original_filename ||= if document_path.respond_to?(:original_filename)
19
+ document_path.original_filename
20
+ else
21
+ pn.basename.to_s
22
+ end
12
23
  payload = {
13
- file: Faraday::Multipart::FilePart.new(document_path, mime_type, original_filename),
14
- user: config.dify_user || 'dify-user'
24
+ file: Faraday::Multipart::FilePart.new(path_like, mime_type, original_filename),
25
+ user: (@config&.dify_user || 'dify-user')
15
26
  }
16
27
  @connection.upload('v1/files/upload', payload)
17
28
  end
@@ -34,7 +45,7 @@ module RubyLLM
34
45
  query: current_message_content.is_a?(Content) ? current_message_content.text : current_message_content,
35
46
  response_mode: (stream ? 'streaming' : 'blocking'),
36
47
  conversation_id: latest_conversation_id,
37
- user: config.dify_user || 'dify-user',
48
+ user: (@config&.dify_user || 'dify-user'),
38
49
  files: format_files(current_message_content)
39
50
  }
40
51
  end