ace-support-models 0.11.2 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ebc7ef6c3d124c4b7fa54702c0e856793c9e36fa376557f2d4bf6b9811dcf03
4
- data.tar.gz: f6c20651c3eea1364732f66a4d7732f2a8e23b84631819d8aada3dec9b6c5cd1
3
+ metadata.gz: 90738d0888e9e24b0a22f0febc4cd012472e31598e63f2f6dedcb59111966571
4
+ data.tar.gz: 391d5d2e24473b8e9377603c9ab33f42500397de98616d02ba530117a991787e
5
5
  SHA512:
6
- metadata.gz: 92b7a7ba187eb9a70d728ea4ac0095bbde6ed573c25501b82e763bd71676cf1222f3a79b438a0e40e8e3bb0ea254a778f6c202c37d18cee6403677846f390e96
7
- data.tar.gz: f13a21c74ccda53367251d01c23988ba86dfecc919671f8fc0d0cfd451c6a14acc18acd45f3545a44929aefb22f1a541877ccacc9a54a2553aeec7e25cde8faf
6
+ metadata.gz: b0c49eaebca896bf7f5ec8a522b952e187a4c9031efeb839036d01fb3505857d8522aef74ebb9bd20596b526547c967750ccfeaa5bec01b39e598e570feb2a04
7
+ data.tar.gz: 80a8dc203bfa7f0159b9d3056a9c7d0d6ac412a3b57a85f9b361cd5bf4dee161f014f2ff5878d73485018ab235557d0d36d9d6b8e563eb03e933b66ce3f1ce63
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-04-24
11
+
12
+ ### Added
13
+ - Synced provider context and output token limits from the models catalog into managed provider YAML via `limits.default` plus per-model overrides.
14
+
15
+ ### Fixed
16
+ - Kept normal provider sync constrained to the tracked model set while still updating limit metadata, and fixed exact-suffix model matching so repeated sync runs stay idempotent.
17
+
18
+ ## [0.11.3] - 2026-04-23
19
+
20
+ ### Technical
21
+ - Kept retained provider smoke coverage on one explicit `XDG_CACHE_HOME` across `sync`, `list`, and `show` so the scenario reads a single deterministic cache root.
22
+
10
23
  ## [0.11.2] - 2026-04-16
11
24
 
12
25
  ### Fixed
@@ -111,6 +111,24 @@ module Ace
111
111
  end
112
112
  end
113
113
 
114
+ # Extract normalized limit configuration from a provider config.
115
+ # @param config [Hash]
116
+ # @return [Hash] normalized {"default"=>{"context"=>..., "output"=>...}, "models"=>{...}}
117
+ def extract_limits(config)
118
+ limits = config["limits"]
119
+ return {} unless limits.is_a?(Hash)
120
+
121
+ normalized = {}
122
+
123
+ default_limits = normalize_limit_entry(limits["default"])
124
+ normalized["default"] = default_limits if default_limits.any?
125
+
126
+ model_limits = normalize_model_limits(limits["models"])
127
+ normalized["models"] = model_limits if model_limits.any?
128
+
129
+ normalized
130
+ end
131
+
114
132
  # Extract models.dev provider ID from config
115
133
  # Falls back to provider name if not specified
116
134
  # @param config [Hash] Provider config
@@ -138,6 +156,38 @@ module Ace
138
156
 
139
157
  private
140
158
 
159
+ def normalize_model_limits(model_limits)
160
+ return {} unless model_limits.is_a?(Hash)
161
+
162
+ model_limits.each_with_object({}) do |(model, entry), normalized|
163
+ next if model.to_s.strip.empty?
164
+
165
+ normalized_entry = normalize_limit_entry(entry)
166
+ normalized[model.to_s] = normalized_entry if normalized_entry.any?
167
+ end
168
+ end
169
+
170
+ def normalize_limit_entry(entry)
171
+ return {} unless entry.is_a?(Hash)
172
+
173
+ normalized = {}
174
+
175
+ context = normalize_limit_value(entry["context"] || entry[:context])
176
+ output = normalize_limit_value(entry["output"] || entry[:output])
177
+
178
+ normalized["context"] = context unless context.nil?
179
+ normalized["output"] = output unless output.nil?
180
+ normalized
181
+ end
182
+
183
+ def normalize_limit_value(value)
184
+ return nil if value.nil?
185
+
186
+ Integer(value)
187
+ rescue ArgumentError, TypeError
188
+ nil
189
+ end
190
+
141
191
  def project_config_dir
142
192
  # Use ace-core if available and has project_root method
143
193
  if defined?(Ace::Core) && Ace::Core.respond_to?(:project_root)
@@ -18,11 +18,9 @@ module Ace
18
18
  # @return [Boolean] true on success
19
19
  # @raise [ConfigError] on write errors
20
20
  def update_models(path, models)
21
- content = read_file_content(path)
22
- raise ConfigError, "Config file not found: #{path}" unless content
23
-
24
- updated_content = replace_models_section(content, models)
25
- write_file(path, updated_content)
21
+ config = read_config(path)
22
+ config["models"] = Array(models)
23
+ write(path, config)
26
24
  true
27
25
  end
28
26
 
@@ -32,7 +30,7 @@ module Ace
32
30
  # @return [Boolean] true on success
33
31
  def write(path, config)
34
32
  ensure_directory(File.dirname(path))
35
- content = YAML.dump(config)
33
+ content = format_config(config)
36
34
  write_file(path, content)
37
35
  true
38
36
  end
@@ -55,11 +53,9 @@ module Ace
55
53
  # @return [Boolean] true on success
56
54
  # @raise [ConfigError] on write errors
57
55
  def update_last_synced(path, date = Date.today)
58
- content = read_file_content(path)
59
- raise ConfigError, "Config file not found: #{path}" unless content
60
-
61
- updated_content = replace_or_add_field(content, "last_synced", date.to_s)
62
- write_file(path, updated_content)
56
+ config = read_config(path)
57
+ config["last_synced"] = date
58
+ write(path, config)
63
59
  true
64
60
  end
65
61
 
@@ -68,29 +64,30 @@ module Ace
68
64
  # @param models [Array<String>] New list of model IDs
69
65
  # @param date [Date] Date to set for last_synced
70
66
  # @return [Boolean] true on success
71
- def update_models_and_sync_date(path, models, date = Date.today)
72
- content = read_file_content(path)
73
- raise ConfigError, "Config file not found: #{path}" unless content
67
+ def update_models_and_sync_date(path, models, date = Date.today, limits: nil)
68
+ config = read_config(path)
69
+ config["models"] = Array(models)
70
+ config["last_synced"] = date
71
+
72
+ unless limits.nil?
73
+ normalized_limits = normalize_limits(limits)
74
+ if normalized_limits.empty?
75
+ config.delete("limits")
76
+ else
77
+ config["limits"] = normalized_limits
78
+ end
79
+ config.delete("context_limit")
80
+ end
74
81
 
75
- updated_content = replace_models_section(content, models)
76
- updated_content = replace_or_add_field(updated_content, "last_synced", date.to_s)
77
- write_file(path, updated_content)
82
+ write(path, config)
78
83
  true
79
84
  end
80
85
 
81
86
  private
82
87
 
83
- def read_file_content(path)
84
- return nil unless File.exist?(path)
85
-
86
- File.read(path)
87
- rescue Errno::EACCES => e
88
- raise ConfigError, "Permission denied reading #{path}: #{e.message}"
89
- end
90
-
91
88
  def write_file(path, content)
92
89
  # Validate YAML before writing to catch regex manipulation errors
93
- YAML.safe_load(content, permitted_classes: [Symbol, Date])
90
+ YAML.safe_load(content, permitted_classes: [Symbol, Date], aliases: true)
94
91
  File.write(path, content)
95
92
  rescue Psych::SyntaxError => e
96
93
  raise ConfigError, "Generated invalid YAML for #{path}: #{e.message}"
@@ -108,119 +105,98 @@ module Ace
108
105
  raise ConfigError, "Permission denied creating directory #{dir}: #{e.message}"
109
106
  end
110
107
 
111
- # Replace the models section in YAML content while preserving structure
112
- # @param content [String] Original YAML content
113
- # @param models [Array<String>] New models list
114
- # @return [String] Updated content
115
- # @raise [ConfigError] if unsupported YAML styles are detected
116
- def replace_models_section(content, models)
117
- # Check for flow-style arrays which are not supported
118
- if /^\s*models:\s*\[/m.match?(content)
119
- raise ConfigError, "Flow-style arrays (models: [...]) are not supported for auto-update. " \
120
- "Please convert to block style (models: followed by list items)."
121
- end
108
+ def read_config(path)
109
+ raise ConfigError, "Config file not found: #{path}" unless File.exist?(path)
122
110
 
123
- # Check for inline comments on models: line which are not preserved
124
- if /^\s*models:\s*#/m.match?(content)
125
- raise ConfigError, "Inline comments on 'models:' line (e.g., 'models: # comment') are not supported. " \
126
- "Please move the comment to a separate line above 'models:'."
127
- end
111
+ YAML.safe_load(File.read(path), permitted_classes: [Symbol, Date], aliases: true) || {}
112
+ rescue Errno::EACCES => e
113
+ raise ConfigError, "Permission denied reading #{path}: #{e.message}"
114
+ rescue Psych::SyntaxError => e
115
+ raise ConfigError, "Invalid YAML in #{path}: #{e.message}"
116
+ end
128
117
 
129
- lines = content.lines
130
- result = []
131
- in_models_section = false
132
- models_base_indent = 0
133
-
134
- lines.each do |line|
135
- # Detect start of models section (with or without items on same line)
136
- if line =~ /^(\s*)models:\s*$/
137
- in_models_section = true
138
- models_base_indent = $1.length
139
- result << line
140
-
141
- # Add new models with standard YAML indent
142
- models.each do |model|
143
- result << "#{" " * (models_base_indent + 2)}- #{model}\n"
144
- end
145
- next
146
- end
118
+ def normalize_limits(limits)
119
+ return {} unless limits.is_a?(Hash)
147
120
 
148
- # If in models section, skip old model items
149
- if in_models_section
150
- # Check if this line is a list item (model entry)
151
- if line =~ /^(\s*)-\s+/
152
- item_indent = $1.length
153
- # Skip if it's at the expected indent for models (base + 0 or base + 2)
154
- if item_indent == models_base_indent || item_indent == models_base_indent + 2
155
- next
156
- end
157
- end
158
-
159
- # Empty line - keep but stay in models section
160
- if line.strip.empty?
161
- result << line
162
- next
163
- end
164
-
165
- # A new key at base level (not indented more) ends models section
166
- if line =~ /^(\s*)\S/
167
- current_indent = $1.length
168
- if current_indent <= models_base_indent
169
- in_models_section = false
170
- result << line
171
- end
172
- # Otherwise skip (shouldn't happen for well-formed YAML)
173
- end
174
- next
175
- end
121
+ normalized = {}
176
122
 
177
- result << line
178
- end
123
+ default_limits = normalize_limit_entry(limits["default"] || limits[:default])
124
+ normalized["default"] = default_limits if default_limits.any?
179
125
 
180
- result.join
126
+ models = limits["models"] || limits[:models]
127
+ normalized_models = normalize_model_limits(models)
128
+ normalized["models"] = normalized_models if normalized_models.any?
129
+
130
+ normalized
181
131
  end
182
132
 
183
- # Replace or add a field in YAML content
184
- # @param content [String] Original YAML content
185
- # @param field_name [String] Field name to replace or add
186
- # @param value [String] New value
187
- # @return [String] Updated content
188
- def replace_or_add_field(content, field_name, value)
189
- lines = content.lines
190
-
191
- # Try to find and replace existing field
192
- field_found = false
193
- result = lines.map do |line|
194
- if line =~ /^(\s*)#{Regexp.escape(field_name)}:\s*(.*)$/
195
- field_found = true
196
- "#{$1}#{field_name}: #{value}\n"
197
- else
198
- line
199
- end
133
+ def normalize_model_limits(models)
134
+ return {} unless models.is_a?(Hash)
135
+
136
+ models.each_with_object({}) do |(model, entry), normalized|
137
+ next if model.to_s.strip.empty?
138
+
139
+ normalized_entry = normalize_limit_entry(entry)
140
+ normalized[model.to_s] = normalized_entry if normalized_entry.any?
141
+ end.sort.to_h
142
+ end
143
+
144
+ def normalize_limit_entry(entry)
145
+ return {} unless entry.is_a?(Hash)
146
+
147
+ normalized = {}
148
+
149
+ context = normalize_limit_value(entry["context"] || entry[:context])
150
+ output = normalize_limit_value(entry["output"] || entry[:output])
151
+
152
+ normalized["context"] = context unless context.nil?
153
+ normalized["output"] = output unless output.nil?
154
+ normalized
155
+ end
156
+
157
+ def normalize_limit_value(value)
158
+ return nil if value.nil?
159
+
160
+ Integer(value)
161
+ rescue ArgumentError, TypeError
162
+ nil
163
+ end
164
+
165
+ def format_config(config)
166
+ YAML.dump(canonicalize_config(config)).sub(/\A---\n/, "")
167
+ end
168
+
169
+ def canonicalize_config(config)
170
+ config = config.dup
171
+ config.delete("_source_file")
172
+
173
+ ordered = {}
174
+ preferred_order = %w[
175
+ name
176
+ last_synced
177
+ class
178
+ gem
179
+ models_dev_id
180
+ models
181
+ limits
182
+ aliases
183
+ api_key
184
+ capabilities
185
+ default_options
186
+ backends
187
+ endpoint
188
+ version
189
+ ]
190
+
191
+ preferred_order.each do |key|
192
+ ordered[key] = config.delete(key) if config.key?(key)
200
193
  end
201
194
 
202
- # If field not found, add it after the name or first line
203
- unless field_found
204
- insert_index = 0
205
- result.each_with_index do |line, idx|
206
- if /^name:/.match?(line)
207
- insert_index = idx + 1
208
- break
209
- end
210
- end
211
- # If no name field, add after first non-comment, non-blank line
212
- if insert_index == 0
213
- result.each_with_index do |line, idx|
214
- next if line.strip.empty? || line.strip.start_with?("#") || line.strip.start_with?("---")
215
-
216
- insert_index = idx + 1
217
- break
218
- end
219
- end
220
- result.insert(insert_index, "#{field_name}: #{value}\n")
195
+ config.each do |key, value|
196
+ ordered[key] = value
221
197
  end
222
198
 
223
- result.join
199
+ ordered
224
200
  end
225
201
  end
226
202
  end
@@ -81,11 +81,11 @@ module Ace
81
81
 
82
82
  # Build a mapping of canonical names to original names for current models
83
83
  # This handles cases like "model:nitro" -> "model"
84
- current_canonical_to_original = {}
84
+ current_lookup = Set.new
85
85
  current_models.each do |model_id|
86
+ current_lookup << model_id
86
87
  canonical = Atoms::ModelNameCanonicalizer.canonicalize(model_id, provider: provider_name)
87
- current_canonical_to_original[canonical] ||= []
88
- current_canonical_to_original[canonical] << model_id
88
+ current_lookup << canonical
89
89
  end
90
90
 
91
91
  added = []
@@ -98,7 +98,7 @@ module Ace
98
98
  # Use canonical names for matching
99
99
  models_dev_models.each do |model_id, model_data|
100
100
  # Check if any current model (or its canonical form) matches this models.dev model
101
- has_match = current_canonical_to_original.key?(model_id)
101
+ has_match = current_lookup.include?(model_id)
102
102
 
103
103
  if has_match
104
104
  if model_data[:status] == "deprecated"
@@ -126,11 +126,16 @@ module Ace
126
126
  # Use canonical names to avoid false positives for suffixed models
127
127
  current_models.each do |model_id|
128
128
  canonical = Atoms::ModelNameCanonicalizer.canonicalize(model_id, provider: provider_name)
129
- unless models_dev_canonical.include?(canonical)
129
+ unless models_dev_canonical.include?(model_id) || models_dev_canonical.include?(canonical)
130
130
  removed << model_id
131
131
  end
132
132
  end
133
133
 
134
+ desired_models = (current_models.to_a + added - removed).uniq.sort
135
+ desired_limits = build_desired_limits(desired_models, models_dev_models, provider_name: provider_name)
136
+ current_limits = Atoms::ProviderConfigReader.extract_limits(config)
137
+ limits_changed = current_limits != desired_limits
138
+
134
139
  {
135
140
  status: :ok,
136
141
  added: added.sort,
@@ -138,6 +143,8 @@ module Ace
138
143
  removed: removed.sort,
139
144
  unchanged: unchanged.sort,
140
145
  deprecated: deprecated.sort,
146
+ desired_limits: desired_limits,
147
+ limits_changed: limits_changed,
141
148
  models_dev_count: models_dev_models.size,
142
149
  current_count: current_models.size,
143
150
  filtered_by_date: !since_date.nil?
@@ -152,6 +159,7 @@ module Ace
152
159
  total_removed = 0
153
160
  total_unchanged = 0
154
161
  total_deprecated = 0
162
+ total_limit_updates = 0
155
163
  providers_synced = 0
156
164
  providers_skipped = 0
157
165
 
@@ -162,6 +170,7 @@ module Ace
162
170
  total_removed += result[:removed].size
163
171
  total_unchanged += result[:unchanged].size
164
172
  total_deprecated += result[:deprecated].size
173
+ total_limit_updates += 1 if result[:limits_changed]
165
174
  else
166
175
  providers_skipped += 1
167
176
  end
@@ -172,6 +181,7 @@ module Ace
172
181
  removed: total_removed,
173
182
  unchanged: total_unchanged,
174
183
  deprecated: total_deprecated,
184
+ limit_updates: total_limit_updates,
175
185
  providers_synced: providers_synced,
176
186
  providers_skipped: providers_skipped
177
187
  }
@@ -184,7 +194,7 @@ module Ace
184
194
  results.any? do |_provider, result|
185
195
  next false unless result[:status] == :ok
186
196
 
187
- result[:added].any? || result[:removed].any?
197
+ result[:added].any? || result[:removed].any? || result[:limits_changed]
188
198
  end
189
199
  end
190
200
 
@@ -223,10 +233,13 @@ module Ace
223
233
 
224
234
  models.each do |model_id, model_data|
225
235
  release_date = parse_date(model_data["release_date"])
236
+ limit = model_data["limit"] || {}
226
237
  result[model_id] = {
227
238
  status: model_data["status"],
228
239
  name: model_data["name"] || model_id,
229
- release_date: release_date
240
+ release_date: release_date,
241
+ context_limit: limit["context"],
242
+ output_limit: limit["output"]
230
243
  }
231
244
  end
232
245
 
@@ -256,6 +269,63 @@ module Ace
256
269
  Atoms::ProviderConfigReader.extract_last_synced(config)
257
270
  end
258
271
 
272
+ def build_desired_limits(models, models_dev_models, provider_name:)
273
+ limit_entries = models.each_with_object({}) do |model_id, acc|
274
+ entry = extract_limit_entry(model_id, models_dev_models, provider_name: provider_name)
275
+ acc[model_id] = entry if entry.any?
276
+ end
277
+
278
+ return {} if limit_entries.empty?
279
+
280
+ default_pair = select_default_limit_pair(limit_entries.values)
281
+ default_limits = {}
282
+ unless default_pair.nil?
283
+ default_limits["context"] = default_pair[0]
284
+ default_limits["output"] = default_pair[1]
285
+ end
286
+
287
+ overrides = limit_entries.each_with_object({}) do |(model_id, entry), acc|
288
+ override = entry.dup
289
+ override.delete("context") if default_limits["context"] && override["context"] == default_limits["context"]
290
+ override.delete("output") if default_limits["output"] && override["output"] == default_limits["output"]
291
+ acc[model_id] = override if override.any?
292
+ end
293
+
294
+ desired = {}
295
+ desired["default"] = default_limits if default_limits.any?
296
+ desired["models"] = overrides.sort.to_h if overrides.any?
297
+ desired
298
+ end
299
+
300
+ def select_default_limit_pair(entries)
301
+ counts = Hash.new(0)
302
+
303
+ entries.each do |entry|
304
+ context = entry["context"]
305
+ output = entry["output"]
306
+ next if context.nil? || output.nil?
307
+
308
+ counts[[context, output]] += 1
309
+ end
310
+
311
+ counts.max_by { |(context, output), count| [count, context, output] }&.first
312
+ end
313
+
314
+ def extract_limit_entry(model_id, models_dev_models, provider_name:)
315
+ model_data = lookup_model_data(model_id, models_dev_models, provider_name: provider_name)
316
+ return {} unless model_data
317
+
318
+ entry = {}
319
+ entry["context"] = model_data[:context_limit] unless model_data[:context_limit].nil?
320
+ entry["output"] = model_data[:output_limit] unless model_data[:output_limit].nil?
321
+ entry
322
+ end
323
+
324
+ def lookup_model_data(model_id, models_dev_models, provider_name:)
325
+ models_dev_models[model_id] ||
326
+ models_dev_models[Atoms::ModelNameCanonicalizer.canonicalize(model_id, provider: provider_name)]
327
+ end
328
+
259
329
  def suggest_models_dev_id(models_dev_data, provider_name)
260
330
  # Try to find a provider that might match
261
331
  provider_lower = provider_name.downcase
@@ -108,11 +108,15 @@ module Ace
108
108
  lines << "Summary: #{summary[:added]} added, #{summary[:removed]} removed, " \
109
109
  "#{summary[:unchanged]} unchanged across #{summary[:providers_synced]} providers"
110
110
 
111
- if summary[:deprecated] > 0
111
+ if summary[:deprecated].to_i > 0
112
112
  lines << " (#{summary[:deprecated]} deprecated models flagged)"
113
113
  end
114
114
 
115
- if summary[:providers_skipped] > 0
115
+ if summary[:limit_updates].to_i > 0
116
+ lines << " (#{summary[:limit_updates]} provider limit configs need updates)"
117
+ end
118
+
119
+ if summary[:providers_skipped].to_i > 0
116
120
  lines << " (#{summary[:providers_skipped]} providers not found in models.dev)"
117
121
  end
118
122
 
@@ -171,7 +175,7 @@ module Ace
171
175
 
172
176
  diff_results.each do |provider_name, diff|
173
177
  next unless diff[:status] == :ok
174
- next if diff[:added].empty? && diff[:removed].empty?
178
+ next if diff[:added].empty? && diff[:removed].empty? && !diff[:limits_changed]
175
179
 
176
180
  begin
177
181
  config = current_configs[provider_name]
@@ -191,7 +195,11 @@ module Ace
191
195
  Atoms::ProviderConfigWriter.backup(source_file)
192
196
 
193
197
  # Update file with models and last_synced date
194
- Atoms::ProviderConfigWriter.update_models_and_sync_date(source_file, new_models)
198
+ Atoms::ProviderConfigWriter.update_models_and_sync_date(
199
+ source_file,
200
+ new_models,
201
+ limits: diff[:desired_limits]
202
+ )
195
203
  output.puts "Updated: #{source_file}"
196
204
  rescue ConfigError => e
197
205
  errors << "#{provider_name}: #{e.message}"
@@ -238,7 +246,7 @@ module Ace
238
246
  return lines.join("\n")
239
247
  end
240
248
 
241
- if diff[:added].empty? && diff[:removed].empty? && diff[:deprecated].empty?
249
+ if diff[:added].empty? && diff[:removed].empty? && diff[:deprecated].empty? && !diff[:limits_changed]
242
250
  lines << " = (no changes)"
243
251
  if diff[:last_synced]
244
252
  lines << " Last synced: #{diff[:last_synced]}"
@@ -259,6 +267,21 @@ module Ace
259
267
  lines << " - #{model.ljust(35)} (removed)"
260
268
  end
261
269
 
270
+ if diff[:limits_changed]
271
+ default_limits = diff.dig(:desired_limits, "default") || {}
272
+ default_parts = []
273
+ default_parts << "context=#{format_number(default_limits["context"])}" if default_limits["context"]
274
+ default_parts << "output=#{format_number(default_limits["output"])}" if default_limits["output"]
275
+ unless default_parts.empty?
276
+ lines << " ~ default limits -> #{default_parts.join(', ')}"
277
+ end
278
+
279
+ override_models = diff.dig(:desired_limits, "models")&.keys || []
280
+ unless override_models.empty?
281
+ lines << " ~ per-model limit overrides: #{override_models.join(', ')}"
282
+ end
283
+ end
284
+
262
285
  diff[:deprecated].each do |model|
263
286
  lines << " ! #{model.ljust(35)} (deprecated)"
264
287
  end
@@ -271,6 +294,10 @@ module Ace
271
294
 
272
295
  lines.join("\n")
273
296
  end
297
+
298
+ def format_number(value)
299
+ value.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')
300
+ end
274
301
  end
275
302
  end
276
303
  end
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Support
5
5
  module Models
6
- VERSION = "0.11.2"
6
+ VERSION = "0.12.0"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-support-models
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-20 00:00:00.000000000 Z
10
+ date: 2026-04-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-core