ruby_llm_community 1.1.0 → 1.2.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -5
  3. data/lib/generators/ruby_llm/generator_helpers.rb +129 -0
  4. data/lib/generators/ruby_llm/install/install_generator.rb +12 -129
  5. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +0 -1
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +0 -3
  8. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +1 -4
  9. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -1
  10. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +8 -0
  11. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +47 -96
  12. data/lib/ruby_llm/attachment.rb +5 -0
  13. data/lib/ruby_llm/configuration.rb +4 -0
  14. data/lib/ruby_llm/mime_type.rb +4 -0
  15. data/lib/ruby_llm/model/info.rb +4 -0
  16. data/lib/ruby_llm/models.json +780 -511
  17. data/lib/ruby_llm/models.rb +7 -3
  18. data/lib/ruby_llm/moderation.rb +56 -0
  19. data/lib/ruby_llm/provider.rb +6 -0
  20. data/lib/ruby_llm/providers/gemini/capabilities.rb +5 -0
  21. data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
  22. data/lib/ruby_llm/providers/openai_base.rb +1 -0
  23. data/lib/ruby_llm/providers/red_candle/capabilities.rb +124 -0
  24. data/lib/ruby_llm/providers/red_candle/chat.rb +317 -0
  25. data/lib/ruby_llm/providers/red_candle/models.rb +121 -0
  26. data/lib/ruby_llm/providers/red_candle/streaming.rb +40 -0
  27. data/lib/ruby_llm/providers/red_candle.rb +90 -0
  28. data/lib/ruby_llm/railtie.rb +1 -1
  29. data/lib/ruby_llm/version.rb +1 -1
  30. data/lib/ruby_llm_community.rb +32 -0
  31. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82545649470ef24d251c3cf4a5f7eceff777ea2d3939e48eb76fc5109e8a1b5a
4
- data.tar.gz: 378d2d26c2cc08362b5b552ca42ee397c9ab1e96cb394223cf8df2dae2548aa6
3
+ metadata.gz: 867ea1c17955648ccfc017acdcce2ba4710d452cca065657a8a527cec7f36cb7
4
+ data.tar.gz: 57f6401b2a0892f36885d5c4806587f6ed6f17f5cc7572b6a484f63074167b4e
5
5
  SHA512:
6
- metadata.gz: 6340051367db4bd6401e2d8c5468eefded5bbf5a72daa94b3bf930ab8b3601b7d3c3cc26e6e878a45e11d3fce15f99747e09eee7393d966f59ce8f968b7d3640
7
- data.tar.gz: 1c7de7980a77d5340325ef1db12c4e6b8e6347639f956a925b1ec8dffeb3ae819086159e35aeb3d7964cc4c60133f09657326dc0eb894c28a2c81efda1a4ddb4
6
+ metadata.gz: e542c506cb9591aa561d062197a2cc3d013be6ec30e35c0b2279084b9f04fbd6451de9db33a58826a5440fd953d069b636a1d3ff7c5605961561af72de376dbe
7
+ data.tar.gz: b7155e89145f0be7f6305baefd4286dd7ee7b948bd66ca685d8787309a755fa93852fb32db2639d9af5200b86a33010ab717c1705faa7932a5a17b178d39597c
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
11
11
 
12
- [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=8)](https://badge.fury.io/rb/ruby_llm)
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=10)](https://badge.fury.io/rb/ruby_llm)
13
13
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
14
14
  [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm)](https://rubygems.org/gems/ruby_llm)
15
15
  [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
@@ -34,11 +34,13 @@ RubyLLM gives you one beautiful API for all of them. Same interface whether you'
34
34
 
35
35
  Use this gem to get early access to features currently in PR at the main gem.
36
36
 
37
- Initial examples --
37
+ Some examples --
38
38
 
39
39
  - Prompt caching for Anthropic
40
40
  - Image editing for Gemini and OpenAI
41
41
  - Responses API for OpenAI
42
+ - xAI provider that supports Grok 2, Grok 3, Grok 4, and Grok Code
43
+ - Red Candle provider
42
44
 
43
45
  This project is intended to be compatible with RubyLLM. I will attempt to keep it up to date as Carmine pushes commits often.
44
46
 
@@ -55,6 +57,7 @@ chat.ask "What's the best way to learn Ruby?"
55
57
  ```ruby
56
58
  # Analyze any file type
57
59
  chat.ask "What's in this image?", with: "ruby_conf.jpg"
60
+ chat.ask "What's happening in this video?", with: "video.mp4"
58
61
  chat.ask "Describe this meeting", with: "meeting.wav"
59
62
  chat.ask "Summarize this document", with: "contract.pdf"
60
63
  chat.ask "Explain this code", with: "app.rb"
@@ -82,6 +85,11 @@ RubyLLM.paint "a sunset over mountains in watercolor style"
82
85
  RubyLLM.embed "Ruby is elegant and expressive"
83
86
  ```
84
87
 
88
+ ```ruby
89
+ # Moderate content for safety
90
+ RubyLLM.moderate("Check if this text is safe").flagged? # => false
91
+ ```
92
+
85
93
  ```ruby
86
94
  # Let AI use your code
87
95
  class Weather < RubyLLM::Tool
@@ -114,18 +122,19 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
114
122
  ## Features
115
123
 
116
124
  * **Chat:** Conversational AI with `RubyLLM.chat`
117
- * **Vision:** Analyze images and screenshots
125
+ * **Vision:** Analyze images and videos
118
126
  * **Audio:** Transcribe and understand speech
119
127
  * **Documents:** Extract from PDFs, CSVs, JSON, any file type
120
128
  * **Image generation:** Create images with `RubyLLM.paint`
121
129
  * **Embeddings:** Vector search with `RubyLLM.embed`
130
+ * **Moderation:** Content safety with `RubyLLM.moderate`
122
131
  * **Tools:** Let AI call your Ruby methods
123
132
  * **Structured output:** JSON schemas that just work
124
133
  * **Streaming:** Real-time responses with blocks
125
134
  * **Rails:** ActiveRecord integration with `acts_as_chat`
126
135
  * **Async:** Fiber-based concurrency
127
136
  * **Model registry:** 500+ models with capability detection and pricing
128
- * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
137
+ * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, [RedCandle](https://github.com/scientist-labs/red-candle), and any OpenAI-compatible API
129
138
 
130
139
  ## Installation
131
140
 
@@ -146,7 +155,11 @@ end
146
155
  ## Rails
147
156
 
148
157
  ```bash
158
+ # Install database models
149
159
  rails generate ruby_llm:install
160
+
161
+ # Add chat UI (optional)
162
+ rails generate ruby_llm:chat_ui
150
163
  ```
151
164
 
152
165
  ```ruby
@@ -154,10 +167,12 @@ class Chat < ApplicationRecord
154
167
  acts_as_chat
155
168
  end
156
169
 
157
- chat = Chat.create! model_id: "claude-sonnet-4"
170
+ chat = Chat.create! model: "claude-sonnet-4"
158
171
  chat.ask "What's in this file?", with: "report.pdf"
159
172
  ```
160
173
 
174
+ Visit `http://localhost:3000/chats` for a ready-to-use chat interface!
175
+
161
176
  ## Documentation
162
177
 
163
178
  [rubyllm.com](https://rubyllm.com)
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Shared helpers for RubyLLM generators
5
+ module GeneratorHelpers
6
+ def parse_model_mappings
7
+ @model_names = {
8
+ chat: 'Chat',
9
+ message: 'Message',
10
+ tool_call: 'ToolCall',
11
+ model: 'Model'
12
+ }
13
+
14
+ model_mappings.each do |mapping|
15
+ if mapping.include?(':')
16
+ key, value = mapping.split(':', 2)
17
+ @model_names[key.to_sym] = value.classify
18
+ end
19
+ end
20
+
21
+ @model_names
22
+ end
23
+
24
+ %i[chat message tool_call model].each do |type|
25
+ define_method("#{type}_model_name") do
26
+ @model_names ||= parse_model_mappings
27
+ @model_names[type]
28
+ end
29
+
30
+ define_method("#{type}_table_name") do
31
+ table_name_for(send("#{type}_model_name"))
32
+ end
33
+ end
34
+
35
+ def acts_as_chat_declaration
36
+ params = []
37
+
38
+ add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
39
+ add_association_params(params, :model, model_table_name, model_model_name)
40
+
41
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
42
+ end
43
+
44
+ def acts_as_message_declaration
45
+ params = []
46
+
47
+ add_association_params(params, :chat, chat_table_name, chat_model_name)
48
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
49
+ add_association_params(params, :model, model_table_name, model_model_name)
50
+
51
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
52
+ end
53
+
54
+ def acts_as_model_declaration
55
+ params = []
56
+
57
+ add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
58
+
59
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
60
+ end
61
+
62
+ def acts_as_tool_call_declaration
63
+ params = []
64
+
65
+ add_association_params(params, :message, message_table_name, message_model_name)
66
+
67
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
68
+ end
69
+
70
+ def create_namespace_modules
71
+ namespaces = []
72
+
73
+ [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
74
+ if model_name.include?('::')
75
+ namespace = model_name.split('::').first
76
+ namespaces << namespace unless namespaces.include?(namespace)
77
+ end
78
+ end
79
+
80
+ namespaces.each do |namespace|
81
+ module_path = "app/models/#{namespace.underscore}.rb"
82
+ next if File.exist?(Rails.root.join(module_path))
83
+
84
+ create_file module_path do
85
+ <<~RUBY
86
+ module #{namespace}
87
+ def self.table_name_prefix
88
+ "#{namespace.underscore}_"
89
+ end
90
+ end
91
+ RUBY
92
+ end
93
+ end
94
+ end
95
+
96
+ def migration_version
97
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
98
+ end
99
+
100
+ def postgresql?
101
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
102
+ rescue StandardError
103
+ false
104
+ end
105
+
106
+ def table_exists?(table_name)
107
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
108
+ rescue StandardError
109
+ false
110
+ end
111
+
112
+ private
113
+
114
+ def add_association_params(params, default_assoc, table_name, model_name, plural: false)
115
+ assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
116
+
117
+ return if assoc == default_assoc
118
+
119
+ params << "#{default_assoc}: :#{assoc}"
120
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
121
+ end
122
+
123
+ def table_name_for(model_name)
124
+ # Convert namespaced model names to proper table names
125
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
126
+ model_name.underscore.pluralize.tr('/', '_')
127
+ end
128
+ end
129
+ end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require 'rails/generators'
4
4
  require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
5
6
 
6
7
  module RubyLLM
7
8
  # Generator for RubyLLM Rails models and migrations
8
9
  class InstallGenerator < Rails::Generators::Base
9
10
  include Rails::Generators::Migration
11
+ include RubyLLM::GeneratorHelpers
10
12
 
11
13
  namespace 'ruby_llm:install'
12
14
 
@@ -24,139 +26,18 @@ module RubyLLM
24
26
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
25
27
  end
26
28
 
27
- def migration_version
28
- "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
29
- end
30
-
31
- def postgresql?
32
- ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
33
- rescue StandardError
34
- false
35
- end
36
-
37
- def parse_model_mappings
38
- @model_names = {
39
- chat: 'Chat',
40
- message: 'Message',
41
- tool_call: 'ToolCall',
42
- model: 'Model'
43
- }
44
-
45
- model_mappings.each do |mapping|
46
- if mapping.include?(':')
47
- key, value = mapping.split(':', 2)
48
- @model_names[key.to_sym] = value.classify
49
- end
50
- end
51
-
52
- @model_names
53
- end
54
-
55
- %i[chat message tool_call model].each do |type|
56
- define_method("#{type}_model_name") do
57
- @model_names ||= parse_model_mappings
58
- @model_names[type]
59
- end
60
-
61
- define_method("#{type}_table_name") do
62
- table_name_for(send("#{type}_model_name"))
63
- end
64
- end
65
-
66
- def acts_as_chat_declaration
67
- acts_as_chat_params = []
68
- messages_assoc = message_model_name.tableize.to_sym
69
- model_assoc = model_model_name.underscore.to_sym
70
-
71
- if messages_assoc != :messages
72
- acts_as_chat_params << "messages: :#{messages_assoc}"
73
- if message_model_name != messages_assoc.to_s.classify
74
- acts_as_chat_params << "message_class: '#{message_model_name}'"
75
- end
76
- end
77
-
78
- if model_assoc != :model
79
- acts_as_chat_params << "model: :#{model_assoc}"
80
- acts_as_chat_params << "model_class: '#{model_model_name}'" if model_model_name != model_assoc.to_s.classify
81
- end
82
-
83
- if acts_as_chat_params.any?
84
- "acts_as_chat #{acts_as_chat_params.join(', ')}"
85
- else
86
- 'acts_as_chat'
87
- end
88
- end
89
-
90
- def acts_as_message_declaration
91
- params = []
92
-
93
- add_message_association_params(params, :chat, chat_model_name)
94
- add_message_association_params(params, :tool_calls, tool_call_model_name, tableize: true)
95
- add_message_association_params(params, :model, model_model_name)
96
-
97
- params.any? ? "acts_as_message #{params.join(', ')}" : 'acts_as_message'
98
- end
99
-
100
- private
101
-
102
- def add_message_association_params(params, default_assoc, model_name, tableize: false)
103
- assoc = tableize ? model_name.tableize.to_sym : model_name.underscore.to_sym
104
-
105
- return if assoc == default_assoc
106
-
107
- params << "#{default_assoc}: :#{assoc}"
108
- expected_class = assoc.to_s.classify
109
- params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != expected_class
110
- end
111
-
112
- public
113
-
114
- def acts_as_tool_call_declaration
115
- acts_as_tool_call_params = []
116
- message_assoc = message_model_name.underscore.to_sym
117
-
118
- if message_assoc != :message
119
- acts_as_tool_call_params << "message: :#{message_assoc}"
120
- if message_model_name != message_assoc.to_s.classify
121
- acts_as_tool_call_params << "message_class: '#{message_model_name}'"
122
- end
123
- end
124
-
125
- if acts_as_tool_call_params.any?
126
- "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
127
- else
128
- 'acts_as_tool_call'
129
- end
130
- end
131
-
132
- def acts_as_model_declaration
133
- acts_as_model_params = []
134
- chats_assoc = chat_model_name.tableize.to_sym
135
-
136
- if chats_assoc != :chats
137
- acts_as_model_params << "chats: :#{chats_assoc}"
138
- acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
139
- end
140
-
141
- if acts_as_model_params.any?
142
- "acts_as_model #{acts_as_model_params.join(', ')}"
143
- else
144
- 'acts_as_model'
145
- end
146
- end
147
-
148
29
  def create_migration_files
149
30
  # Create migrations with timestamps to ensure proper order
150
31
  # First create chats table
151
32
  migration_template 'create_chats_migration.rb.tt',
152
33
  "db/migrate/create_#{chat_table_name}.rb"
153
34
 
154
- # Then create messages table (must come before tool_calls due to foreign key)
35
+ # Then create messages table
155
36
  sleep 1 # Ensure different timestamp
156
37
  migration_template 'create_messages_migration.rb.tt',
157
38
  "db/migrate/create_#{message_table_name}.rb"
158
39
 
159
- # Then create tool_calls table (references messages)
40
+ # Then create tool_calls table
160
41
  sleep 1 # Ensure different timestamp
161
42
  migration_template 'create_tool_calls_migration.rb.tt',
162
43
  "db/migrate/create_#{tool_call_table_name}.rb"
@@ -165,9 +46,17 @@ module RubyLLM
165
46
  sleep 1 # Ensure different timestamp
166
47
  migration_template 'create_models_migration.rb.tt',
167
48
  "db/migrate/create_#{model_table_name}.rb"
49
+
50
+ # Add references to chats, tool_calls and messages.
51
+ sleep 1 # Ensure different timestamp
52
+ migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
53
+ 'db/migrate/add_references_to_' \
54
+ "#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
168
55
  end
169
56
 
170
57
  def create_model_files
58
+ create_namespace_modules
59
+
171
60
  template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
172
61
  template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
173
62
  template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
@@ -186,12 +75,6 @@ module RubyLLM
186
75
  rails_command 'active_storage:install'
187
76
  end
188
77
 
189
- def table_name_for(model_name)
190
- # Convert namespaced model names to proper table names
191
- # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
192
- model_name.underscore.pluralize.tr('/', '_')
193
- end
194
-
195
78
  def show_install_info
196
79
  say "\n ✅ RubyLLM installed!", :green
197
80
 
@@ -0,0 +1,9 @@
1
+ class AddReferencesTo<%= "#{chat_model_name.gsub('::', '').pluralize}#{tool_call_model_name.gsub('::', '').pluralize}And#{message_model_name.gsub('::', '').pluralize}" %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
4
+ add_reference :<%= tool_call_table_name %>, :<%= message_table_name.singularize %>, null: false, foreign_key: true
5
+ add_reference :<%= message_table_name %>, :<%= chat_table_name.singularize %>, null: false, foreign_key: true
6
+ add_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
7
+ add_reference :<%= message_table_name %>, :<%= tool_call_table_name.singularize %>, foreign_key: true
8
+ end
9
+ end
@@ -1,7 +1,6 @@
1
1
  class Create<%= chat_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :<%= chat_table_name %> do |t|
4
- t.references :<%= model_table_name.singularize %>, foreign_key: true
5
4
  t.timestamps
6
5
  end
7
6
  end
@@ -1,13 +1,10 @@
1
1
  class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :<%= message_table_name %> do |t|
4
- t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: true
5
4
  t.string :role, null: false
6
5
  t.text :content
7
- t.references :<%= model_table_name.singularize %>, foreign_key: true
8
6
  t.integer :input_tokens
9
7
  t.integer :output_tokens
10
- t.references :<%= tool_call_table_name.singularize %>, foreign_key: true
11
8
  t.timestamps
12
9
  end
13
10
 
@@ -34,10 +34,7 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
34
34
  # Load models from JSON
35
35
  say_with_time "Loading models from models.json" do
36
36
  RubyLLM.models.load_from_json!
37
- model_class = '<%= model_model_name %>'.constantize
38
- model_class.save_to_database
39
-
40
- "Loaded #{model_class.count} models"
37
+ <%= model_model_name %>.save_to_database
41
38
  end
42
39
  end
43
40
  end
@@ -2,7 +2,6 @@
2
2
  class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  create_table :<%= tool_call_table_name %> do |t|
5
- t.references :<%= message_table_name.singularize %>, null: false, foreign_key: true
6
5
  t.string :tool_call_id, null: false
7
6
  t.string :name, null: false
8
7
  t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
@@ -3,6 +3,14 @@ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_ver
3
3
  model_class = <%= model_model_name %>
4
4
  chat_class = <%= chat_model_name %>
5
5
  message_class = <%= message_model_name %>
6
+ <% if @model_table_already_existed %>
7
+ # Load models from models.json if Model table already existed
8
+ say_with_time "Loading models from models.json" do
9
+ RubyLLM.models.load_from_json!
10
+ model_class.save_to_database
11
+ "Loaded #{model_class.count} models"
12
+ end
13
+ <% end %>
6
14
 
7
15
  # Then check for any models in existing data that aren't in models.json
8
16
  say_with_time "Checking for additional models in existing data" do