ace-support-models 0.9.3 → 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 +50 -0
- data/README.md +21 -0
- data/exe/ace-models +1 -1
- data/lib/ace/support/models/atoms/api_fetcher.rb +8 -2
- 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/cli/commands/cache/status.rb +1 -1
- data/lib/ace/support/models/cli/commands/providers/list.rb +1 -1
- data/lib/ace/support/models/cli/commands/providers/show.rb +1 -1
- data/lib/ace/support/models/cli/commands/providers/sync.rb +1 -1
- data/lib/ace/support/models/molecules/cache_manager.rb +28 -7
- 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/organisms/sync_orchestrator.rb +9 -1
- 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,56 @@ 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
|
+
|
|
23
|
+
## [0.11.2] - 2026-04-16
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Normalized sync stats calculation for partial and string-keyed provider payloads so `ace-models sync` and `ace-models status` no longer raise when cache/provider entries omit structured model hashes.
|
|
27
|
+
|
|
28
|
+
## [0.11.1] - 2026-04-16
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Updated smoke scenario setup to source `mise.toml` from `ACE_E2E_SOURCE_ROOT` inside sandboxed runs and relaxed diff verification to accept the current public diff summary vocabulary.
|
|
32
|
+
|
|
33
|
+
## [0.11.0] - 2026-04-15
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Reworked `TS-MODELS-001` E2E to public-surface goal contracts by replacing cache hand-seeding with public `ace-models sync` setup, simplifying invalid-filter coverage, and adding cache status/diff goal cases.
|
|
37
|
+
- Added deterministic fixture-driven sync support via `ACE_MODELS_FIXTURE_JSON` / `ACE_MODELS_API_URL` to keep cache-dependent provider E2E flows reproducible without hidden cache-shape recipes.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Aligned cache-missing guidance to the flat command surface (`ace-models sync`) across provider/cache CLI errors and executable migration hints.
|
|
41
|
+
|
|
42
|
+
## [0.10.2] - 2026-04-13
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- **ace-support-models v0.10.2**: Normalized API cache provider and model payloads at read time (including string-array model lists), fixing `ace-llm-providers list`/`show` output from cached responses.
|
|
46
|
+
|
|
47
|
+
## [0.10.1] - 2026-04-13
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
|
|
51
|
+
|
|
52
|
+
### Technical
|
|
53
|
+
- Included in the coordinated assignment-driven patch release for batch i05 package updates.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
- Migrated package tests to the restarted `fast` / `feat` / `e2e` model by moving deterministic coverage to `test/fast`, relocating legacy integration coverage to `test/feat`, and retaining workflow-value E2E scenario coverage.
|
|
58
|
+
- Updated package README testing guidance and E2E scenario decision metadata to reflect the `fast` / `feat` / `e2e` contract.
|
|
59
|
+
|
|
10
60
|
## [0.9.3] - 2026-03-29
|
|
11
61
|
|
|
12
62
|
### Technical
|
data/README.md
CHANGED
|
@@ -34,6 +34,27 @@ ace-models sync # Update cache from models.dev
|
|
|
34
34
|
ace-llm-providers list # List known providers
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Testing
|
|
38
|
+
|
|
39
|
+
Use the package test lanes directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ace-test ace-support-models # fast lane default
|
|
43
|
+
ace-test ace-support-models feat # feature-contract lane
|
|
44
|
+
ace-test ace-support-models all # all deterministic lanes
|
|
45
|
+
ace-test-e2e ace-support-models # retained workflow scenarios
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For deterministic E2E setup of cache-dependent provider commands, use the public
|
|
49
|
+
`ace-models sync` path with a fixture payload:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export ACE_MODELS_FIXTURE_JSON='{"anthropic":{"models":{"claude-sonnet-4":{"id":"claude-sonnet-4"}}}}'
|
|
53
|
+
ace-models sync
|
|
54
|
+
ace-llm-providers list
|
|
55
|
+
unset ACE_MODELS_FIXTURE_JSON
|
|
56
|
+
```
|
|
57
|
+
|
|
37
58
|
---
|
|
38
59
|
|
|
39
60
|
Part of [ACE](https://github.com/cs3b/ace)
|
data/exe/ace-models
CHANGED
|
@@ -8,7 +8,7 @@ require_relative "../lib/ace/support/models/cli"
|
|
|
8
8
|
old_cache_path = File.join(Dir.home, ".cache", "ace-llm-models-dev")
|
|
9
9
|
if Dir.exist?(old_cache_path)
|
|
10
10
|
warn "Note: Old cache detected at ~/.cache/ace-llm-models-dev"
|
|
11
|
-
warn "Run: ace-models sync
|
|
11
|
+
warn "Run: ace-models sync then rm -rf ~/.cache/ace-llm-models-dev"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# No args → show help
|
|
@@ -10,6 +10,8 @@ module Ace
|
|
|
10
10
|
# Fetches data from the models.dev API using Faraday (ADR-010 compliant)
|
|
11
11
|
class ApiFetcher
|
|
12
12
|
API_URL = "https://models.dev/api.json"
|
|
13
|
+
FIXTURE_JSON_ENV = "ACE_MODELS_FIXTURE_JSON"
|
|
14
|
+
API_URL_ENV = "ACE_MODELS_API_URL"
|
|
13
15
|
TIMEOUT = 30
|
|
14
16
|
OPEN_TIMEOUT = 10
|
|
15
17
|
MAX_RETRIES = 2
|
|
@@ -21,8 +23,12 @@ module Ace
|
|
|
21
23
|
# @return [String] Raw JSON response
|
|
22
24
|
# @raise [NetworkError] on network failures
|
|
23
25
|
# @raise [ApiError] on non-200 responses
|
|
24
|
-
def fetch(url =
|
|
25
|
-
|
|
26
|
+
def fetch(url = nil)
|
|
27
|
+
fixture_json = ENV[FIXTURE_JSON_ENV]
|
|
28
|
+
return fixture_json unless fixture_json.to_s.strip.empty?
|
|
29
|
+
|
|
30
|
+
resolved_url = url || ENV[API_URL_ENV] || API_URL
|
|
31
|
+
response = connection.get(resolved_url)
|
|
26
32
|
|
|
27
33
|
unless response.success?
|
|
28
34
|
raise ApiError.new(
|
|
@@ -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
|
|
@@ -25,7 +25,7 @@ module Ace
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
unless status_data[:cached]
|
|
28
|
-
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models
|
|
28
|
+
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models sync' first.")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
puts "Cache Status:"
|
|
@@ -20,7 +20,7 @@ module Ace
|
|
|
20
20
|
cache_manager = Molecules::CacheManager.new
|
|
21
21
|
|
|
22
22
|
unless cache_manager.cached?
|
|
23
|
-
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models
|
|
23
|
+
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models sync' first.")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
providers = cache_manager.list_providers
|
|
@@ -21,7 +21,7 @@ module Ace
|
|
|
21
21
|
cache_manager = Molecules::CacheManager.new
|
|
22
22
|
|
|
23
23
|
unless cache_manager.cached?
|
|
24
|
-
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models
|
|
24
|
+
raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models sync' first.")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
provider_data = cache_manager.get_provider(provider_id)
|
|
@@ -53,7 +53,7 @@ module Ace
|
|
|
53
53
|
puts orchestrator.format_result(result)
|
|
54
54
|
end
|
|
55
55
|
rescue CacheError => e
|
|
56
|
-
raise Ace::Support::Cli::Error.new("#{e.message}. Run 'ace-models
|
|
56
|
+
raise Ace::Support::Cli::Error.new("#{e.message}. Run 'ace-models sync' first to download model data.")
|
|
57
57
|
rescue ConfigError => e
|
|
58
58
|
raise Ace::Support::Cli::Error.new("Config error: #{e.message}")
|
|
59
59
|
end
|
|
@@ -26,7 +26,8 @@ module Ace
|
|
|
26
26
|
content = Atoms::FileReader.read(api_cache_path)
|
|
27
27
|
return nil unless content
|
|
28
28
|
|
|
29
|
-
Atoms::JsonParser.parse(content)
|
|
29
|
+
data = Atoms::JsonParser.parse(content)
|
|
30
|
+
normalize_cache_data(data)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Read previous API cache (for diff)
|
|
@@ -35,7 +36,8 @@ module Ace
|
|
|
35
36
|
content = Atoms::FileReader.read(previous_cache_path)
|
|
36
37
|
return nil unless content
|
|
37
38
|
|
|
38
|
-
Atoms::JsonParser.parse(content)
|
|
39
|
+
data = Atoms::JsonParser.parse(content)
|
|
40
|
+
normalize_cache_data(data)
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Write API data to cache
|
|
@@ -197,6 +199,17 @@ module Ace
|
|
|
197
199
|
data
|
|
198
200
|
end
|
|
199
201
|
|
|
202
|
+
def normalize_cache_data(data)
|
|
203
|
+
normalized = normalize_providers(data)
|
|
204
|
+
normalized.each_value do |provider_data|
|
|
205
|
+
next unless provider_data.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
provider_data["models"] = normalize_models(provider_data)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
normalized
|
|
211
|
+
end
|
|
212
|
+
|
|
200
213
|
def normalize_provider_collection(providers)
|
|
201
214
|
case providers
|
|
202
215
|
when Hash
|
|
@@ -205,7 +218,7 @@ module Ace
|
|
|
205
218
|
providers.each_with_object({}) do |provider, acc|
|
|
206
219
|
next unless provider.is_a?(Hash)
|
|
207
220
|
|
|
208
|
-
provider_id = provider["id"]
|
|
221
|
+
provider_id = provider["id"] || provider[:id] || provider["provider_id"]
|
|
209
222
|
acc[provider_id] = provider if provider_id
|
|
210
223
|
end
|
|
211
224
|
else
|
|
@@ -221,10 +234,18 @@ module Ace
|
|
|
221
234
|
models
|
|
222
235
|
when Array
|
|
223
236
|
models.each_with_object({}) do |model, acc|
|
|
224
|
-
next unless model
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
next unless model
|
|
238
|
+
|
|
239
|
+
case model
|
|
240
|
+
when Hash
|
|
241
|
+
model_id = model["id"] || model[:id]
|
|
242
|
+
acc[model_id] = model if model_id
|
|
243
|
+
when String
|
|
244
|
+
acc[model] = {"id" => model, "name" => model}
|
|
245
|
+
when Symbol
|
|
246
|
+
model_id = model.to_s
|
|
247
|
+
acc[model_id] = {"id" => model_id, "name" => model_id}
|
|
248
|
+
end
|
|
228
249
|
end
|
|
229
250
|
else
|
|
230
251
|
{}
|
|
@@ -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
|
|
@@ -90,7 +90,8 @@ module Ace
|
|
|
90
90
|
models_by_provider = {}
|
|
91
91
|
|
|
92
92
|
data.each do |provider_id, provider_data|
|
|
93
|
-
|
|
93
|
+
models = provider_models(provider_data)
|
|
94
|
+
count = models.size
|
|
94
95
|
models_by_provider[provider_id] = count
|
|
95
96
|
model_count += count
|
|
96
97
|
end
|
|
@@ -101,6 +102,13 @@ module Ace
|
|
|
101
102
|
top_providers: models_by_provider.sort_by { |_, v| -v }.first(10).to_h
|
|
102
103
|
}
|
|
103
104
|
end
|
|
105
|
+
|
|
106
|
+
def provider_models(provider_data)
|
|
107
|
+
return {} unless provider_data.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
models = provider_data["models"] || provider_data[:models]
|
|
110
|
+
models.is_a?(Hash) ? models : {}
|
|
111
|
+
end
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
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-
|
|
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
|