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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/ace/support/models/atoms/provider_config_reader.rb +50 -0
- data/lib/ace/support/models/atoms/provider_config_writer.rb +104 -128
- data/lib/ace/support/models/molecules/provider_sync_diff.rb +77 -7
- data/lib/ace/support/models/organisms/provider_sync_orchestrator.rb +32 -5
- data/lib/ace/support/models/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90738d0888e9e24b0a22f0febc4cd012472e31598e63f2f6dedcb59111966571
|
|
4
|
+
data.tar.gz: 391d5d2e24473b8e9377603c9ab33f42500397de98616d02ba530117a991787e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
123
|
+
default_limits = normalize_limit_entry(limits["default"] || limits[:default])
|
|
124
|
+
normalized["default"] = default_limits if default_limits.any?
|
|
179
125
|
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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[:
|
|
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(
|
|
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
|
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.
|
|
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-
|
|
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
|