ruby_llm 1.14.0 → 1.15.0

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  5. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +52 -25
  7. data/lib/ruby_llm/active_record/chat_methods.rb +47 -23
  8. data/lib/ruby_llm/active_record/message_methods.rb +19 -14
  9. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  10. data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
  11. data/lib/ruby_llm/active_record/tool_call_methods.rb +5 -15
  12. data/lib/ruby_llm/agent.rb +3 -2
  13. data/lib/ruby_llm/aliases.json +53 -14
  14. data/lib/ruby_llm/attachment.rb +11 -27
  15. data/lib/ruby_llm/chat.rb +62 -21
  16. data/lib/ruby_llm/cost.rb +224 -0
  17. data/lib/ruby_llm/image.rb +37 -4
  18. data/lib/ruby_llm/message.rb +20 -0
  19. data/lib/ruby_llm/model/info.rb +17 -0
  20. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  21. data/lib/ruby_llm/models.json +26511 -24930
  22. data/lib/ruby_llm/models.rb +2 -1
  23. data/lib/ruby_llm/models_schema.json +3 -0
  24. data/lib/ruby_llm/provider.rb +10 -3
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
  26. data/lib/ruby_llm/providers/anthropic/models.rb +2 -8
  27. data/lib/ruby_llm/providers/anthropic/tools.rb +4 -1
  28. data/lib/ruby_llm/providers/bedrock/chat.rb +24 -13
  29. data/lib/ruby_llm/providers/bedrock/streaming.rb +4 -1
  30. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -119
  31. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
  32. data/lib/ruby_llm/providers/gemini/chat.rb +8 -1
  33. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  34. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  35. data/lib/ruby_llm/providers/gemini/streaming.rb +4 -1
  36. data/lib/ruby_llm/providers/gemini/tools.rb +3 -1
  37. data/lib/ruby_llm/providers/mistral/capabilities.rb +6 -1
  38. data/lib/ruby_llm/providers/mistral/chat.rb +55 -4
  39. data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
  40. data/lib/ruby_llm/providers/openai/chat.rb +45 -6
  41. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  42. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  43. data/lib/ruby_llm/providers/openai/streaming.rb +5 -6
  44. data/lib/ruby_llm/providers/openrouter/chat.rb +30 -6
  45. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  46. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  47. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  48. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  49. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  50. data/lib/ruby_llm/railtie.rb +6 -0
  51. data/lib/ruby_llm/tokens.rb +8 -0
  52. data/lib/ruby_llm/tool.rb +24 -7
  53. data/lib/ruby_llm/version.rb +1 -1
  54. data/lib/ruby_llm.rb +2 -4
  55. data/lib/tasks/models.rake +13 -12
  56. metadata +21 -5
@@ -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,25 +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_tool_choice?(_model_id)
66
- true
67
- end
68
-
69
- def supports_tool_parallel_control?(_model_id)
70
- false
71
- end
72
-
73
- def supports_json_mode?(model_id)
90
+ def supports_structured_output?(model_id)
74
91
  if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
75
92
  return false
76
93
  end
@@ -78,59 +95,6 @@ module RubyLLM
78
95
  model_id.match?(/gemini|pro|flash/)
79
96
  end
80
97
 
81
- def format_display_name(model_id)
82
- model_id
83
- .delete_prefix('models/')
84
- .split('-')
85
- .map(&:capitalize)
86
- .join(' ')
87
- .gsub(/(\d+\.\d+)/, ' \1')
88
- .gsub(/\s+/, ' ')
89
- .gsub('Aqa', 'AQA')
90
- .strip
91
- end
92
-
93
- def supports_caching?(model_id)
94
- if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
95
- return false
96
- end
97
-
98
- model_id.match?(/gemini|pro|flash/)
99
- end
100
-
101
- def supports_tuning?(model_id)
102
- model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
103
- end
104
-
105
- def supports_audio?(model_id)
106
- model_id.match?(/gemini|pro|flash/)
107
- end
108
-
109
- def model_type(model_id)
110
- case model_id
111
- when /text-embedding|embedding|gemini-embedding/ then 'embedding'
112
- when /imagen/ then 'image'
113
- else 'chat'
114
- end
115
- end
116
-
117
- def model_family(model_id)
118
- case model_id
119
- when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
120
- when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
121
- when /gemini-2\.0-flash/ then 'gemini20_flash'
122
- when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
123
- when /gemini-1\.5-flash/ then 'gemini15_flash'
124
- when /gemini-1\.5-pro/ then 'gemini15_pro'
125
- when /gemini-embedding-exp/ then 'gemini_embedding_exp'
126
- when /text-embedding-004/ then 'embedding4'
127
- when /embedding-001/ then 'embedding1'
128
- when /aqa/ then 'aqa'
129
- when /imagen-3/ then 'imagen3'
130
- else 'other'
131
- end
132
- end
133
-
134
98
  def pricing_family(model_id)
135
99
  case model_id
136
100
  when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
@@ -147,142 +111,8 @@ module RubyLLM
147
111
  end
148
112
  end
149
113
 
150
- def long_context_model?(model_id)
151
- model_id.match?(/gemini-1\.5-(?:pro|flash)|gemini-1\.5-flash-8b/)
152
- end
153
-
154
- def context_length(model_id)
155
- context_window_for(model_id)
156
- end
157
-
158
- PRICES = {
159
- flash_2: { # rubocop:disable Naming/VariableNumber
160
- input: 0.10,
161
- output: 0.40,
162
- audio_input: 0.70,
163
- cache: 0.025,
164
- cache_storage: 1.00,
165
- grounding_search: 35.00
166
- },
167
- flash_lite_2: { # rubocop:disable Naming/VariableNumber
168
- input: 0.075,
169
- output: 0.30
170
- },
171
- flash: {
172
- input: 0.075,
173
- output: 0.30,
174
- cache: 0.01875,
175
- cache_storage: 1.00,
176
- grounding_search: 35.00
177
- },
178
- flash_8b: {
179
- input: 0.0375,
180
- output: 0.15,
181
- cache: 0.01,
182
- cache_storage: 0.25,
183
- grounding_search: 35.00
184
- },
185
- pro: {
186
- input: 1.25,
187
- output: 5.0,
188
- cache: 0.3125,
189
- cache_storage: 4.50,
190
- grounding_search: 35.00
191
- },
192
- pro_2_5: { # rubocop:disable Naming/VariableNumber
193
- input: 0.12,
194
- output: 0.50
195
- },
196
- gemini_embedding: {
197
- input: 0.002,
198
- output: 0.004
199
- },
200
- embedding: {
201
- input: 0.00,
202
- output: 0.00
203
- },
204
- imagen: {
205
- price: 0.03
206
- },
207
- aqa: {
208
- input: 0.00,
209
- output: 0.00
210
- }
211
- }.freeze
212
-
213
- def default_input_price
214
- 0.075
215
- end
216
-
217
- def default_output_price
218
- 0.30
219
- end
220
-
221
- def modalities_for(model_id)
222
- modalities = {
223
- input: ['text'],
224
- output: ['text']
225
- }
226
-
227
- if supports_vision?(model_id)
228
- modalities[:input] << 'image'
229
- modalities[:input] << 'pdf'
230
- end
231
-
232
- modalities[:input] << 'video' if supports_video?(model_id)
233
- modalities[:input] << 'audio' if model_id.match?(/audio/)
234
- modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
235
- modalities[:output] = ['image'] if model_id.match?(/imagen/)
236
-
237
- modalities
238
- end
239
-
240
- def capabilities_for(model_id)
241
- capabilities = ['streaming']
242
-
243
- capabilities << 'function_calling' if supports_functions?(model_id)
244
- capabilities << 'structured_output' if supports_json_mode?(model_id)
245
- capabilities << 'batch' if model_id.match?(/embedding|flash/)
246
- capabilities << 'caching' if supports_caching?(model_id)
247
- capabilities << 'fine_tuning' if supports_tuning?(model_id)
248
- capabilities
249
- end
250
-
251
- def pricing_for(model_id)
252
- family = pricing_family(model_id)
253
- prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
254
-
255
- standard_pricing = {
256
- input_per_million: prices[:input],
257
- output_per_million: prices[:output]
258
- }
259
-
260
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
261
-
262
- batch_pricing = {
263
- input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
264
- output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
265
- }
266
-
267
- if standard_pricing[:cached_input_per_million]
268
- batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
269
- end
270
-
271
- pricing = {
272
- text_tokens: {
273
- standard: standard_pricing,
274
- batch: batch_pricing
275
- }
276
- }
277
-
278
- if model_id.match?(/embedding|gemini-embedding/)
279
- pricing[:embeddings] = {
280
- standard: { input_per_million: prices[:price] || 0.002 }
281
- }
282
- end
283
-
284
- pricing
285
- 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
286
116
  end
287
117
  end
288
118
  end
@@ -118,7 +118,7 @@ module RubyLLM
118
118
  signature: extract_thought_signature(parts)
119
119
  ),
120
120
  tool_calls: tool_calls,
121
- input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
121
+ input_tokens: input_tokens(data),
122
122
  output_tokens: calculate_output_tokens(data),
123
123
  cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
124
124
  thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
@@ -127,6 +127,13 @@ module RubyLLM
127
127
  )
128
128
  end
129
129
 
130
+ def input_tokens(data)
131
+ prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
132
+ return unless prompt_tokens
133
+
134
+ [prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
135
+ end
136
+
130
137
  def convert_schema_to_gemini(schema)
131
138
  return nil unless schema
132
139
 
@@ -5,11 +5,11 @@ module RubyLLM
5
5
  class Gemini
6
6
  # Image generation methods for the Gemini API implementation
7
7
  module Images
8
- def images_url
8
+ def images_url(with: nil, mask: nil) # rubocop:disable Lint/UnusedMethodArgument
9
9
  "models/#{@model}:predict"
10
10
  end
11
11
 
12
- def render_image_payload(prompt, model:, size:)
12
+ def render_image_payload(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
13
13
  RubyLLM.logger.debug { "Ignoring size #{size}. Gemini does not support image size customization." }
14
14
  @model = model
15
15
  {
@@ -17,14 +17,12 @@ module RubyLLM
17
17
 
18
18
  Model::Info.new(
19
19
  id: model_id,
20
- name: model_data['displayName'],
20
+ name: model_data['displayName'] || model_id,
21
21
  provider: slug,
22
- family: capabilities.model_family(model_id),
23
22
  created_at: nil,
24
23
  context_window: model_data['inputTokenLimit'] || capabilities.context_window_for(model_id),
25
24
  max_output_tokens: model_data['outputTokenLimit'] || capabilities.max_tokens_for(model_id),
26
- modalities: capabilities.modalities_for(model_id),
27
- capabilities: capabilities.capabilities_for(model_id),
25
+ capabilities: capabilities.critical_capabilities_for(model_id),
28
26
  pricing: capabilities.pricing_for(model_id),
29
27
  metadata: {
30
28
  version: model_data['version'],
@@ -70,7 +70,10 @@ module RubyLLM
70
70
  end
71
71
 
72
72
  def extract_input_tokens(data)
73
- data.dig('usageMetadata', 'promptTokenCount')
73
+ prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
74
+ return unless prompt_tokens
75
+
76
+ [prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
74
77
  end
75
78
 
76
79
  def extract_output_tokens(data)
@@ -46,13 +46,15 @@ module RubyLLM
46
46
 
47
47
  def format_tool_result(msg, function_name = nil)
48
48
  function_name ||= msg.tool_call_id
49
+ content = msg.content
50
+ content = '(no output)' if content.nil? || (content.respond_to?(:empty?) && content.empty?)
49
51
 
50
52
  [{
51
53
  functionResponse: {
52
54
  name: function_name,
53
55
  response: {
54
56
  name: function_name,
55
- content: Media.format_content(msg.content)
57
+ content: Media.format_content(content)
56
58
  }
57
59
  }
58
60
  }]
@@ -31,6 +31,11 @@ module RubyLLM
31
31
  !model_id.match?(/embed|moderation|ocr|voxtral|transcriptions/) && supports_tools?(model_id)
32
32
  end
33
33
 
34
+ def supports_reasoning?(model_id)
35
+ model_id.match?(/magistral/) ||
36
+ model_id.match?(/\Amistral-(?:small-latest|medium-(?:3(?:[.-]5)?|latest))\z/)
37
+ end
38
+
34
39
  def format_display_name(model_id)
35
40
  case model_id
36
41
  when /mistral-large/ then 'Mistral Large'
@@ -101,7 +106,7 @@ module RubyLLM
101
106
  capabilities << 'structured_output' if supports_json_mode?(model_id)
102
107
  capabilities << 'vision' if supports_vision?(model_id)
103
108
 
104
- capabilities << 'reasoning' if model_id.match?(/magistral/)
109
+ capabilities << 'reasoning' if supports_reasoning?(model_id)
105
110
  capabilities << 'batch' unless model_id.match?(/voxtral|ocr|embed|moderation/)
106
111
  capabilities << 'fine_tuning' if model_id.match?(/mistral-(small|medium|large)|devstral/)
107
112
  capabilities << 'distillation' if model_id.match?(/ministral/)
@@ -27,12 +27,30 @@ module RubyLLM
27
27
  schema: nil, thinking: nil, tool_prefs: nil)
28
28
  payload = super
29
29
  payload.delete(:stream_options)
30
- payload.delete(:reasoning_effort)
31
- warn_on_unsupported_thinking(model, thinking)
30
+ configure_thinking_payload(payload, model, thinking)
31
+ normalize_required_tool_choice(payload)
32
32
  payload
33
33
  end
34
34
  # rubocop:enable Metrics/ParameterLists
35
35
 
36
+ def build_tool_choice(tool_choice)
37
+ return 'any' if tool_choice == :required
38
+
39
+ OpenAI::Tools.build_tool_choice(tool_choice)
40
+ end
41
+
42
+ def normalize_required_tool_choice(payload)
43
+ return unless payload[:tool_choice] == 'any' && Array(payload[:tools]).one?
44
+
45
+ function_name = payload.dig(:tools, 0, :function, :name)
46
+ return unless function_name
47
+
48
+ payload[:tool_choice] = {
49
+ type: 'function',
50
+ function: { name: function_name }
51
+ }
52
+ end
53
+
36
54
  def format_content_with_thinking(msg)
37
55
  formatted_content = OpenAI::Media.format_content(msg.content)
38
56
  return formatted_content unless msg.role == :assistant && msg.thinking
@@ -45,14 +63,47 @@ module RubyLLM
45
63
 
46
64
  def warn_on_unsupported_thinking(model, thinking)
47
65
  return unless thinking&.enabled?
48
- return if model.id.to_s.include?('magistral')
66
+ return if native_reasoning_model?(model.id) || adjustable_reasoning_model?(model.id)
49
67
 
50
68
  RubyLLM.logger.warn(
51
- 'Mistral thinking is only supported on Magistral models. ' \
69
+ 'Mistral thinking is only supported on Magistral and adjustable-reasoning models. ' \
52
70
  "Ignoring thinking settings for #{model.id}."
53
71
  )
54
72
  end
55
73
 
74
+ def configure_thinking_payload(payload, model, thinking)
75
+ return unless thinking&.enabled?
76
+
77
+ if native_reasoning_model?(model.id)
78
+ configure_native_reasoning_payload(payload, thinking)
79
+ elsif adjustable_reasoning_model?(model.id)
80
+ payload[:reasoning_effort] = reasoning_effort_for(thinking)
81
+ else
82
+ payload.delete(:reasoning_effort)
83
+ warn_on_unsupported_thinking(model, thinking)
84
+ end
85
+ end
86
+
87
+ def configure_native_reasoning_payload(payload, thinking)
88
+ payload.delete(:reasoning_effort)
89
+ payload[:prompt_mode] = thinking.effort == 'none' ? nil : 'reasoning'
90
+ end
91
+
92
+ def reasoning_effort_for(thinking)
93
+ effort = thinking.respond_to?(:effort) ? thinking.effort : nil
94
+ return effort if %w[high none].include?(effort)
95
+
96
+ 'high'
97
+ end
98
+
99
+ def native_reasoning_model?(model_id)
100
+ model_id.to_s.include?('magistral')
101
+ end
102
+
103
+ def adjustable_reasoning_model?(model_id)
104
+ model_id.to_s.match?(/\Amistral-(?:small-latest|medium-(?:3(?:[.-]5)?|latest))\z/)
105
+ end
106
+
56
107
  def build_thinking_blocks(thinking)
57
108
  return [] unless thinking
58
109