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,127 +3,17 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class DeepSeek
6
- # Determines capabilities and pricing for DeepSeek models
6
+ # Provider-level capability checks used outside the model registry.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def context_window_for(model_id)
11
- case model_id
12
- when /deepseek-(?:chat|reasoner)/ then 64_000
13
- else 32_768
14
- end
10
+ def supports_tool_choice?(_model_id)
11
+ true
15
12
  end
16
13
 
17
- def max_tokens_for(model_id)
18
- case model_id
19
- when /deepseek-(?:chat|reasoner)/ then 8_192
20
- else 4_096
21
- end
22
- end
23
-
24
- def input_price_for(model_id)
25
- PRICES.dig(model_family(model_id), :input_miss) || default_input_price
26
- end
27
-
28
- def output_price_for(model_id)
29
- PRICES.dig(model_family(model_id), :output) || default_output_price
30
- end
31
-
32
- def cache_hit_price_for(model_id)
33
- PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
34
- end
35
-
36
- def supports_vision?(_model_id)
14
+ def supports_tool_parallel_control?(_model_id)
37
15
  false
38
16
  end
39
-
40
- def supports_functions?(model_id)
41
- model_id.match?(/deepseek-chat/)
42
- end
43
-
44
- def supports_json_mode?(_model_id)
45
- false
46
- end
47
-
48
- def format_display_name(model_id)
49
- case model_id
50
- when 'deepseek-chat' then 'DeepSeek V3'
51
- when 'deepseek-reasoner' then 'DeepSeek R1'
52
- else
53
- model_id.split('-')
54
- .map(&:capitalize)
55
- .join(' ')
56
- end
57
- end
58
-
59
- def model_type(_model_id)
60
- 'chat'
61
- end
62
-
63
- def model_family(model_id)
64
- case model_id
65
- when /deepseek-reasoner/ then :reasoner
66
- else :chat
67
- end
68
- end
69
-
70
- PRICES = {
71
- chat: {
72
- input_hit: 0.07,
73
- input_miss: 0.27,
74
- output: 1.10
75
- },
76
- reasoner: {
77
- input_hit: 0.14,
78
- input_miss: 0.55,
79
- output: 2.19
80
- }
81
- }.freeze
82
-
83
- def default_input_price
84
- 0.27
85
- end
86
-
87
- def default_output_price
88
- 1.10
89
- end
90
-
91
- def default_cache_hit_price
92
- 0.07
93
- end
94
-
95
- def modalities_for(_model_id)
96
- {
97
- input: ['text'],
98
- output: ['text']
99
- }
100
- end
101
-
102
- def capabilities_for(model_id)
103
- capabilities = ['streaming']
104
-
105
- capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
106
-
107
- capabilities
108
- end
109
-
110
- def pricing_for(model_id)
111
- family = model_family(model_id)
112
- prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
113
-
114
- standard_pricing = {
115
- input_per_million: prices[:input_miss],
116
- output_per_million: prices[:output]
117
- }
118
-
119
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
120
-
121
- {
122
- text_tokens: {
123
- standard: standard_pricing
124
- }
125
- }
126
- end
127
17
  end
128
18
  end
129
19
  end
@@ -7,7 +7,7 @@ module RubyLLM
7
7
  include DeepSeek::Chat
8
8
 
9
9
  def api_base
10
- 'https://api.deepseek.com'
10
+ @config.deepseek_api_base || 'https://api.deepseek.com'
11
11
  end
12
12
 
13
13
  def headers
@@ -21,6 +21,10 @@ module RubyLLM
21
21
  DeepSeek::Capabilities
22
22
  end
23
23
 
24
+ def configuration_options
25
+ %i[deepseek_api_key deepseek_api_base]
26
+ end
27
+
24
28
  def configuration_requirements
25
29
  %i[deepseek_api_key]
26
30
  end
@@ -26,21 +26,23 @@ module RubyLLM
26
26
  }
27
27
  @connection.upload('v1/files/upload', payload)
28
28
  end
29
-
29
+
30
30
  module_function
31
31
 
32
32
  def completion_url
33
33
  'v1/chat-messages'
34
34
  end
35
35
 
36
- def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Lint/UnusedMethodArgument
36
+ # rubocop:disable Lint/UnusedMethodArgument
37
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil,
38
+ tool_prefs: nil)
37
39
  current_message = messages[-1]
38
40
  current_message_content = current_message.content # dify using conversation_id to trace message history
39
41
 
40
42
  # Find the latest non-nil conversation_id from all messages
41
43
  latest_conversation_id = messages.reverse.find { |msg| msg.conversation_id }&.conversation_id
42
44
 
43
- {
45
+ payload = {
44
46
  inputs: {},
45
47
  query: current_message_content.is_a?(Content) ? current_message_content.text : current_message_content,
46
48
  response_mode: (stream ? 'streaming' : 'blocking'),
@@ -48,22 +50,95 @@ module RubyLLM
48
50
  user: (@config&.dify_user || 'dify-user'),
49
51
  files: format_files(current_message_content)
50
52
  }
53
+
54
+ payload[:thinking] = { type: 'enabled' } if thinking&.enabled?
55
+ payload
51
56
  end
57
+ # rubocop:enable Lint/UnusedMethodArgument
52
58
 
53
59
  def parse_completion_response(response)
54
60
  data = response.body
61
+ message_data = data.dig('choices', 0, 'message')
62
+ usage = data['usage'] || {}
63
+
64
+ if message_data
65
+ content, thinking_from_tags = extract_content_and_thinking(message_data['content'])
66
+ thinking_text = thinking_from_tags || extract_thinking_text(message_data)
67
+ thinking_signature = extract_thinking_signature(message_data)
68
+ thinking_tokens = extract_thinking_tokens(data)
69
+ else
70
+ answer = data['answer']
71
+ content, thinking_from_tags = extract_content_and_thinking(answer)
72
+ thinking_text = thinking_from_tags || extract_thinking_text(data)
73
+ thinking_signature = extract_thinking_signature(data)
74
+ thinking_tokens = extract_thinking_tokens(data)
75
+ end
55
76
 
56
77
  Message.new(
57
78
  role: :assistant,
58
- content: data['answer'],
79
+ content: content,
80
+ thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
59
81
  tool_calls: nil,
60
- input_tokens: data.dig('metadata', 'usage', 'prompt_tokens'),
61
- output_tokens: data.dig('metadata', 'usage', 'completion_tokens'),
82
+ input_tokens: usage['prompt_tokens'] || data.dig('metadata', 'usage', 'prompt_tokens'),
83
+ output_tokens: usage['completion_tokens'] || data.dig('metadata', 'usage', 'completion_tokens'),
84
+ thinking_tokens: thinking_tokens,
62
85
  conversation_id: data['conversation_id'],
63
- model_id: 'dify-model',
86
+ model_id: data['model'] || 'dify-model',
64
87
  raw: response
65
88
  )
66
89
  end
90
+
91
+ def extract_content_and_thinking(answer)
92
+ return [answer, nil] unless answer.is_a?(String)
93
+ return [answer, nil] unless answer.include?('<think>')
94
+
95
+ thinking = answer.scan(%r{<think>(.*?)</think>}m).join
96
+ content = answer.gsub(%r{<think>.*?</think>}m, '').strip
97
+
98
+ [content.empty? ? nil : content, thinking.empty? ? nil : thinking]
99
+ end
100
+
101
+ def extract_thinking_text(data)
102
+ candidate = data['reasoning_content'] || data['reasoning'] || data['thinking'] || data['thought']
103
+ return candidate if candidate.is_a?(String)
104
+
105
+ metadata = data['metadata']
106
+ candidate = metadata&.dig('reasoning_content') ||
107
+ metadata&.dig('reasoning') ||
108
+ metadata&.dig('thinking') ||
109
+ metadata&.dig('thought')
110
+ return candidate if candidate.is_a?(String)
111
+
112
+ thoughts = data['thoughts'] || metadata&.dig('thoughts')
113
+ return nil unless thoughts.is_a?(Array)
114
+
115
+ text = thoughts.filter_map do |thought|
116
+ next thought if thought.is_a?(String)
117
+
118
+ thought['thought'] || thought['thinking'] || thought['content'] || thought['text']
119
+ end.join
120
+
121
+ text.empty? ? nil : text
122
+ end
123
+
124
+ def extract_thinking_signature(data)
125
+ candidate = data['thinking_signature'] || data['reasoning_signature'] || data['signature']
126
+ return candidate if candidate.is_a?(String)
127
+
128
+ metadata = data['metadata']
129
+ candidate = metadata&.dig('thinking_signature') ||
130
+ metadata&.dig('reasoning_signature') ||
131
+ metadata&.dig('signature')
132
+ candidate if candidate.is_a?(String)
133
+ end
134
+
135
+ def extract_thinking_tokens(data)
136
+ usage = data['usage'] || data.dig('metadata', 'usage') || {}
137
+ usage['thinking_tokens'] ||
138
+ usage['reasoning_tokens'] ||
139
+ usage.dig('completion_tokens_details', 'reasoning_tokens') ||
140
+ usage.dig('output_tokens_details', 'thinking_tokens')
141
+ end
67
142
  end
68
143
  end
69
144
  end
@@ -9,13 +9,13 @@ module RubyLLM
9
9
 
10
10
  def format_files(content)
11
11
  return nil unless content.is_a?(Content)
12
-
12
+
13
13
  parts = []
14
14
 
15
15
  content.attachments.each do |attachment|
16
16
  case attachment.type
17
17
  when :file_id
18
- parts << format_document_type(attachment)
18
+ parts << format_document_type(attachment)
19
19
  else
20
20
  raise UnsupportedAttachmentError, attachment.class
21
21
  end
@@ -12,14 +12,36 @@ module RubyLLM
12
12
  end
13
13
 
14
14
  def build_chunk(data)
15
+ if data['choices'].is_a?(Array)
16
+ delta = data.dig('choices', 0, 'delta') || {}
17
+ usage = data['usage'] || {}
18
+ content = delta['content']
19
+ thinking_text = delta['reasoning_content'] || delta['reasoning'] || delta['thinking']
20
+ thinking_signature = delta['reasoning_signature'] || delta['signature']
21
+ thinking_tokens = Chat.extract_thinking_tokens(data)
22
+ input_tokens = usage['prompt_tokens'] || data.dig('metadata', 'usage', 'prompt_tokens')
23
+ output_tokens = usage['completion_tokens'] || data.dig('metadata', 'usage', 'completion_tokens')
24
+ model_id = data['model']
25
+ else
26
+ content = data['answer']
27
+ thinking_text = Chat.extract_thinking_text(data)
28
+ thinking_signature = Chat.extract_thinking_signature(data)
29
+ thinking_tokens = Chat.extract_thinking_tokens(data)
30
+ input_tokens = data.dig('metadata', 'usage', 'prompt_tokens') || data.dig('usage', 'prompt_tokens')
31
+ output_tokens = data.dig('metadata', 'usage', 'completion_tokens') || data.dig('usage', 'completion_tokens')
32
+ model_id = nil
33
+ end
34
+
15
35
  Chunk.new(
16
36
  role: :assistant,
17
37
  conversation_id: data['conversation_id'],
18
- model_id: nil,
19
- content: data['answer'],
38
+ model_id: model_id,
39
+ content: content,
40
+ thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
20
41
  tool_calls: nil,
21
- input_tokens: data.dig('metadata', 'usage', 'prompt_tokens'),
22
- output_tokens: data.dig('metadata', 'usage', 'completion_tokens')
42
+ input_tokens: input_tokens,
43
+ output_tokens: output_tokens,
44
+ thinking_tokens: thinking_tokens
23
45
  )
24
46
  end
25
47
  end
@@ -35,6 +35,10 @@ module RubyLLM
35
35
  Dify::Capabilities
36
36
  end
37
37
 
38
+ def configuration_options
39
+ %i[dify_api_base dify_api_key dify_user]
40
+ end
41
+
38
42
  def local?
39
43
  true
40
44
  end
@@ -3,13 +3,35 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Gemini
6
- # Determines capabilities and pricing for Google Gemini models
6
+ # Provider-level capability checks and narrow registry fallbacks.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
+ PRICES = {
11
+ flash_2: { input: 0.10, output: 0.40 }, # rubocop:disable Naming/VariableNumber
12
+ flash_lite_2: { input: 0.075, output: 0.30 }, # rubocop:disable Naming/VariableNumber
13
+ flash: { input: 0.075, output: 0.30 },
14
+ flash_8b: { input: 0.0375, output: 0.15 },
15
+ pro: { input: 1.25, output: 5.0 },
16
+ pro_2_5: { input: 0.12, output: 0.50 }, # rubocop:disable Naming/VariableNumber
17
+ gemini_embedding: { input: 0.002, output: 0.004 },
18
+ embedding: { input: 0.00, output: 0.00 },
19
+ imagen: { price: 0.03 },
20
+ aqa: { input: 0.00, output: 0.00 }
21
+ }.freeze
22
+
23
+ def supports_tool_choice?(_model_id)
24
+ true
25
+ end
26
+
27
+ def supports_tool_parallel_control?(_model_id)
28
+ false
29
+ end
30
+
10
31
  def context_window_for(model_id)
11
32
  case model_id
12
- when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/ # rubocop:disable Layout/LineLength
33
+ when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
34
+ /gemini-1\.5-flash-8b/
13
35
  1_048_576
14
36
  when /gemini-1\.5-pro/ then 2_097_152
15
37
  when /gemini-embedding-exp/ then 8_192
@@ -23,7 +45,8 @@ module RubyLLM
23
45
  def max_tokens_for(model_id)
24
46
  case model_id
25
47
  when /gemini-2\.5-pro-exp-03-25/ then 64_000
26
- when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/, /gemini-1\.5-pro/ # rubocop:disable Layout/LineLength
48
+ when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
49
+ /gemini-1\.5-pro/
27
50
  8_192
28
51
  when /gemini-embedding-exp/ then nil
29
52
  when /text-embedding-004/, /embedding-001/ then 768
@@ -32,18 +55,24 @@ module RubyLLM
32
55
  end
33
56
  end
34
57
 
35
- def input_price_for(model_id)
36
- base_price = PRICES.dig(pricing_family(model_id), :input) || default_input_price
37
- return base_price unless long_context_model?(model_id)
38
-
39
- context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
58
+ def critical_capabilities_for(model_id)
59
+ capabilities = []
60
+ capabilities << 'function_calling' if supports_functions?(model_id)
61
+ capabilities << 'structured_output' if supports_structured_output?(model_id)
62
+ capabilities << 'vision' if supports_vision?(model_id)
63
+ capabilities
40
64
  end
41
65
 
42
- def output_price_for(model_id)
43
- base_price = PRICES.dig(pricing_family(model_id), :output) || default_output_price
44
- return base_price unless long_context_model?(model_id)
45
-
46
- context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
66
+ def pricing_for(model_id)
67
+ prices = PRICES.fetch(pricing_family(model_id), { input: 0.075, output: 0.30 })
68
+ {
69
+ text_tokens: {
70
+ standard: {
71
+ input_per_million: prices[:input] || prices[:price] || 0.075,
72
+ output_per_million: prices[:output] || prices[:price] || 0.30
73
+ }
74
+ }
75
+ }
47
76
  end
48
77
 
49
78
  def supports_vision?(model_id)
@@ -52,17 +81,13 @@ module RubyLLM
52
81
  model_id.match?(/gemini|flash|pro|imagen/)
53
82
  end
54
83
 
55
- def supports_video?(model_id)
56
- model_id.match?(/gemini/)
57
- end
58
-
59
84
  def supports_functions?(model_id)
60
85
  return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
61
86
 
62
87
  model_id.match?(/gemini|pro|flash/)
63
88
  end
64
89
 
65
- def supports_json_mode?(model_id)
90
+ def supports_structured_output?(model_id)
66
91
  if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
67
92
  return false
68
93
  end
@@ -70,59 +95,6 @@ module RubyLLM
70
95
  model_id.match?(/gemini|pro|flash/)
71
96
  end
72
97
 
73
- def format_display_name(model_id)
74
- model_id
75
- .delete_prefix('models/')
76
- .split('-')
77
- .map(&:capitalize)
78
- .join(' ')
79
- .gsub(/(\d+\.\d+)/, ' \1')
80
- .gsub(/\s+/, ' ')
81
- .gsub('Aqa', 'AQA')
82
- .strip
83
- end
84
-
85
- def supports_caching?(model_id)
86
- if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
87
- return false
88
- end
89
-
90
- model_id.match?(/gemini|pro|flash/)
91
- end
92
-
93
- def supports_tuning?(model_id)
94
- model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
95
- end
96
-
97
- def supports_audio?(model_id)
98
- model_id.match?(/gemini|pro|flash/)
99
- end
100
-
101
- def model_type(model_id)
102
- case model_id
103
- when /text-embedding|embedding|gemini-embedding/ then 'embedding'
104
- when /imagen/ then 'image'
105
- else 'chat'
106
- end
107
- end
108
-
109
- def model_family(model_id)
110
- case model_id
111
- when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
112
- when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
113
- when /gemini-2\.0-flash/ then 'gemini20_flash'
114
- when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
115
- when /gemini-1\.5-flash/ then 'gemini15_flash'
116
- when /gemini-1\.5-pro/ then 'gemini15_pro'
117
- when /gemini-embedding-exp/ then 'gemini_embedding_exp'
118
- when /text-embedding-004/ then 'embedding4'
119
- when /embedding-001/ then 'embedding1'
120
- when /aqa/ then 'aqa'
121
- when /imagen-3/ then 'imagen3'
122
- else 'other'
123
- end
124
- end
125
-
126
98
  def pricing_family(model_id)
127
99
  case model_id
128
100
  when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
@@ -139,142 +111,8 @@ module RubyLLM
139
111
  end
140
112
  end
141
113
 
142
- def long_context_model?(model_id)
143
- model_id.match?(/gemini-1\.5-(?:pro|flash)|gemini-1\.5-flash-8b/)
144
- end
145
-
146
- def context_length(model_id)
147
- context_window_for(model_id)
148
- end
149
-
150
- PRICES = {
151
- flash_2: { # rubocop:disable Naming/VariableNumber
152
- input: 0.10,
153
- output: 0.40,
154
- audio_input: 0.70,
155
- cache: 0.025,
156
- cache_storage: 1.00,
157
- grounding_search: 35.00
158
- },
159
- flash_lite_2: { # rubocop:disable Naming/VariableNumber
160
- input: 0.075,
161
- output: 0.30
162
- },
163
- flash: {
164
- input: 0.075,
165
- output: 0.30,
166
- cache: 0.01875,
167
- cache_storage: 1.00,
168
- grounding_search: 35.00
169
- },
170
- flash_8b: {
171
- input: 0.0375,
172
- output: 0.15,
173
- cache: 0.01,
174
- cache_storage: 0.25,
175
- grounding_search: 35.00
176
- },
177
- pro: {
178
- input: 1.25,
179
- output: 5.0,
180
- cache: 0.3125,
181
- cache_storage: 4.50,
182
- grounding_search: 35.00
183
- },
184
- pro_2_5: { # rubocop:disable Naming/VariableNumber
185
- input: 0.12,
186
- output: 0.50
187
- },
188
- gemini_embedding: {
189
- input: 0.002,
190
- output: 0.004
191
- },
192
- embedding: {
193
- input: 0.00,
194
- output: 0.00
195
- },
196
- imagen: {
197
- price: 0.03
198
- },
199
- aqa: {
200
- input: 0.00,
201
- output: 0.00
202
- }
203
- }.freeze
204
-
205
- def default_input_price
206
- 0.075
207
- end
208
-
209
- def default_output_price
210
- 0.30
211
- end
212
-
213
- def modalities_for(model_id)
214
- modalities = {
215
- input: ['text'],
216
- output: ['text']
217
- }
218
-
219
- if supports_vision?(model_id)
220
- modalities[:input] << 'image'
221
- modalities[:input] << 'pdf'
222
- end
223
-
224
- modalities[:input] << 'video' if supports_video?(model_id)
225
- modalities[:input] << 'audio' if model_id.match?(/audio/)
226
- modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
227
- modalities[:output] = ['image'] if model_id.match?(/imagen/)
228
-
229
- modalities
230
- end
231
-
232
- def capabilities_for(model_id)
233
- capabilities = ['streaming']
234
-
235
- capabilities << 'function_calling' if supports_functions?(model_id)
236
- capabilities << 'structured_output' if supports_json_mode?(model_id)
237
- capabilities << 'batch' if model_id.match?(/embedding|flash/)
238
- capabilities << 'caching' if supports_caching?(model_id)
239
- capabilities << 'fine_tuning' if supports_tuning?(model_id)
240
- capabilities
241
- end
242
-
243
- def pricing_for(model_id)
244
- family = pricing_family(model_id)
245
- prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
246
-
247
- standard_pricing = {
248
- input_per_million: prices[:input],
249
- output_per_million: prices[:output]
250
- }
251
-
252
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
253
-
254
- batch_pricing = {
255
- input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
256
- output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
257
- }
258
-
259
- if standard_pricing[:cached_input_per_million]
260
- batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
261
- end
262
-
263
- pricing = {
264
- text_tokens: {
265
- standard: standard_pricing,
266
- batch: batch_pricing
267
- }
268
- }
269
-
270
- if model_id.match?(/embedding|gemini-embedding/)
271
- pricing[:embeddings] = {
272
- standard: { input_per_million: prices[:price] || 0.002 }
273
- }
274
- end
275
-
276
- pricing
277
- end
114
+ module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
115
+ :supports_vision?, :supports_functions?, :supports_structured_output?, :pricing_family
278
116
  end
279
117
  end
280
118
  end