ace-support-models 0.9.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/LICENSE +21 -0
  4. data/README.md +39 -0
  5. data/Rakefile +13 -0
  6. data/exe/ace-llm-providers +19 -0
  7. data/exe/ace-models +23 -0
  8. data/lib/ace/support/models/atoms/api_fetcher.rb +76 -0
  9. data/lib/ace/support/models/atoms/cache_path_resolver.rb +38 -0
  10. data/lib/ace/support/models/atoms/file_reader.rb +43 -0
  11. data/lib/ace/support/models/atoms/file_writer.rb +63 -0
  12. data/lib/ace/support/models/atoms/json_parser.rb +38 -0
  13. data/lib/ace/support/models/atoms/model_filter.rb +107 -0
  14. data/lib/ace/support/models/atoms/model_name_canonicalizer.rb +119 -0
  15. data/lib/ace/support/models/atoms/provider_config_reader.rb +218 -0
  16. data/lib/ace/support/models/atoms/provider_config_writer.rb +230 -0
  17. data/lib/ace/support/models/cli/commands/cache/clear.rb +43 -0
  18. data/lib/ace/support/models/cli/commands/cache/diff.rb +74 -0
  19. data/lib/ace/support/models/cli/commands/cache/status.rb +54 -0
  20. data/lib/ace/support/models/cli/commands/cache/sync.rb +51 -0
  21. data/lib/ace/support/models/cli/commands/info.rb +33 -0
  22. data/lib/ace/support/models/cli/commands/models/cost.rb +54 -0
  23. data/lib/ace/support/models/cli/commands/models/info.rb +136 -0
  24. data/lib/ace/support/models/cli/commands/models/search.rb +101 -0
  25. data/lib/ace/support/models/cli/commands/providers/list.rb +46 -0
  26. data/lib/ace/support/models/cli/commands/providers/show.rb +54 -0
  27. data/lib/ace/support/models/cli/commands/providers/sync.rb +66 -0
  28. data/lib/ace/support/models/cli/commands/search.rb +35 -0
  29. data/lib/ace/support/models/cli/commands/sync_shortcut.rb +32 -0
  30. data/lib/ace/support/models/cli/providers_cli.rb +72 -0
  31. data/lib/ace/support/models/cli.rb +84 -0
  32. data/lib/ace/support/models/errors.rb +55 -0
  33. data/lib/ace/support/models/models/diff_result.rb +94 -0
  34. data/lib/ace/support/models/models/model_info.rb +129 -0
  35. data/lib/ace/support/models/models/pricing_info.rb +74 -0
  36. data/lib/ace/support/models/models/provider_info.rb +81 -0
  37. data/lib/ace/support/models/models.rb +97 -0
  38. data/lib/ace/support/models/molecules/cache_manager.rb +237 -0
  39. data/lib/ace/support/models/molecules/cost_calculator.rb +135 -0
  40. data/lib/ace/support/models/molecules/diff_generator.rb +171 -0
  41. data/lib/ace/support/models/molecules/model_searcher.rb +176 -0
  42. data/lib/ace/support/models/molecules/model_validator.rb +177 -0
  43. data/lib/ace/support/models/molecules/provider_sync_diff.rb +291 -0
  44. data/lib/ace/support/models/organisms/provider_sync_orchestrator.rb +278 -0
  45. data/lib/ace/support/models/organisms/sync_orchestrator.rb +108 -0
  46. data/lib/ace/support/models/version.rb +9 -0
  47. data/lib/ace/support/models.rb +3 -0
  48. metadata +149 -0
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module Organisms
9
+ # Orchestrates the provider config synchronization workflow
10
+ # Coordinates reading configs, generating diffs, applying changes, and committing
11
+ class ProviderSyncOrchestrator
12
+ PROVIDER_SYNC_CACHE_MAX_AGE = 86_400 * 7 # 7 days
13
+
14
+ attr_reader :diff_generator, :output
15
+
16
+ # Initialize orchestrator
17
+ # @param cache_manager [Molecules::CacheManager, nil] Cache manager
18
+ # @param output [IO] Output stream for messages
19
+ def initialize(cache_manager: nil, output: $stdout)
20
+ @cache_manager = cache_manager || Molecules::CacheManager.new
21
+ @diff_generator = Molecules::ProviderSyncDiff.new(cache_manager: @cache_manager)
22
+ @output = output
23
+ end
24
+
25
+ # Run the sync-providers workflow
26
+ # @param config_dir [String, nil] Override config directory
27
+ # @param provider [String, nil] Limit to specific provider
28
+ # @param apply [Boolean] Apply changes to files
29
+ # @param commit [Boolean] Commit changes via ace-git-commit
30
+ # @param show_all [Boolean] Show all models regardless of release date
31
+ # @param since [String, Date, nil] Only show models released after this date
32
+ # @return [Hash] Result with status and details
33
+ def sync(config_dir: nil, provider: nil, apply: false, commit: false, show_all: false, since: nil)
34
+ # Ensure cache is fresh
35
+ ensure_cache_fresh
36
+
37
+ # Read current provider configs
38
+ current_configs = Atoms::ProviderConfigReader.read_all(config_dir: config_dir)
39
+
40
+ if current_configs.empty?
41
+ return {
42
+ status: :error,
43
+ message: "No provider configs found. Check config directory."
44
+ }
45
+ end
46
+
47
+ # Parse since date if provided
48
+ since_date = parse_since_date(since)
49
+
50
+ # Generate diff
51
+ diff_results = @diff_generator.generate(
52
+ current_configs,
53
+ provider_filter: provider,
54
+ since_date: since_date,
55
+ show_all: show_all
56
+ )
57
+
58
+ # Build result
59
+ result = {
60
+ status: :ok,
61
+ diff: diff_results,
62
+ summary: @diff_generator.summary(diff_results),
63
+ changes_detected: @diff_generator.any_changes?(diff_results),
64
+ applied: false,
65
+ committed: false,
66
+ show_all: show_all,
67
+ since_date: since_date
68
+ }
69
+
70
+ # Apply changes if requested
71
+ if apply && result[:changes_detected]
72
+ apply_result = apply_changes(diff_results, current_configs)
73
+ result[:applied] = apply_result[:success]
74
+ result[:apply_errors] = apply_result[:errors] if apply_result[:errors].any?
75
+
76
+ # Commit if requested and apply succeeded
77
+ if commit && result[:applied]
78
+ result[:committed] = commit_changes(result[:summary])
79
+ end
80
+ end
81
+
82
+ result
83
+ end
84
+
85
+ # Format diff results for display
86
+ # @param result [Hash] Sync result
87
+ # @return [String] Formatted output
88
+ def format_result(result)
89
+ lines = []
90
+ lines << "Syncing provider configs with models.dev..."
91
+
92
+ # Show date filter info
93
+ if result[:show_all]
94
+ lines << "(Showing all models)"
95
+ elsif result[:since_date]
96
+ lines << "(Showing models released after #{result[:since_date]})"
97
+ end
98
+
99
+ lines << ""
100
+
101
+ result[:diff].each do |provider_name, diff|
102
+ lines << format_provider_diff(provider_name, diff)
103
+ end
104
+
105
+ # Add summary
106
+ summary = result[:summary]
107
+ lines << ""
108
+ lines << "Summary: #{summary[:added]} added, #{summary[:removed]} removed, " \
109
+ "#{summary[:unchanged]} unchanged across #{summary[:providers_synced]} providers"
110
+
111
+ if summary[:deprecated] > 0
112
+ lines << " (#{summary[:deprecated]} deprecated models flagged)"
113
+ end
114
+
115
+ if summary[:providers_skipped] > 0
116
+ lines << " (#{summary[:providers_skipped]} providers not found in models.dev)"
117
+ end
118
+
119
+ # Add action hints
120
+ lines << ""
121
+ if result[:changes_detected]
122
+ if result[:applied]
123
+ lines << "Changes applied to config files."
124
+ lines << if result[:committed]
125
+ "Changes committed."
126
+ else
127
+ "Run with --commit to commit changes."
128
+ end
129
+ else
130
+ lines << "Run with --apply to update config files."
131
+ end
132
+ else
133
+ lines << "All providers are up to date."
134
+ end
135
+
136
+ unless result[:show_all]
137
+ lines << "Run with --all to see all models (not just new releases)."
138
+ end
139
+
140
+ lines.join("\n")
141
+ end
142
+
143
+ private
144
+
145
+ def parse_since_date(value)
146
+ return nil unless value
147
+
148
+ case value
149
+ when Date
150
+ value
151
+ when String
152
+ Date.parse(value)
153
+ end
154
+ rescue ArgumentError
155
+ nil
156
+ end
157
+
158
+ def ensure_cache_fresh
159
+ unless @cache_manager.exists?
160
+ raise CacheError, "No models.dev cache found. Run 'ace-models sync' first."
161
+ end
162
+
163
+ unless @cache_manager.fresh?(max_age: PROVIDER_SYNC_CACHE_MAX_AGE)
164
+ output.puts "Warning: models.dev cache is more than 7 days old. Consider running 'ace-models sync'."
165
+ end
166
+ end
167
+
168
+ def apply_changes(diff_results, current_configs)
169
+ errors = []
170
+ success = true
171
+
172
+ diff_results.each do |provider_name, diff|
173
+ next unless diff[:status] == :ok
174
+ next if diff[:added].empty? && diff[:removed].empty?
175
+
176
+ begin
177
+ config = current_configs[provider_name]
178
+ source_file = config["_source_file"]
179
+
180
+ unless source_file
181
+ errors << "#{provider_name}: No source file found"
182
+ success = false
183
+ next
184
+ end
185
+
186
+ # Calculate new model list
187
+ current_models = Atoms::ProviderConfigReader.extract_models(config)
188
+ new_models = (current_models + diff[:added] - diff[:removed]).uniq.sort
189
+
190
+ # Create backup
191
+ Atoms::ProviderConfigWriter.backup(source_file)
192
+
193
+ # Update file with models and last_synced date
194
+ Atoms::ProviderConfigWriter.update_models_and_sync_date(source_file, new_models)
195
+ output.puts "Updated: #{source_file}"
196
+ rescue ConfigError => e
197
+ errors << "#{provider_name}: #{e.message}"
198
+ success = false
199
+ rescue => e
200
+ errors << "#{provider_name}: Unexpected error: #{e.message}"
201
+ success = false
202
+ end
203
+ end
204
+
205
+ {success: success, errors: errors}
206
+ end
207
+
208
+ def commit_changes(summary)
209
+ message = "chore(providers): Sync model lists with models.dev\n\n" \
210
+ "Added: #{summary[:added]} models\n" \
211
+ "Removed: #{summary[:removed]} models"
212
+
213
+ # Try to use ace-git-commit if available
214
+ begin
215
+ system("ace-git-commit", "-m", message)
216
+ $?.success?
217
+ rescue Errno::ENOENT
218
+ output.puts "Warning: ace-git-commit not found. Please commit changes manually."
219
+ false
220
+ end
221
+ end
222
+
223
+ def format_provider_diff(provider_name, diff)
224
+ lines = []
225
+
226
+ # Show models_dev_id mapping if different from provider name
227
+ lines << if diff[:models_dev_id]
228
+ "#{provider_name}: (mapped to #{diff[:models_dev_id]})"
229
+ else
230
+ "#{provider_name}:"
231
+ end
232
+
233
+ if diff[:status] == :not_found
234
+ lines << " ⚠ Provider not found in models.dev"
235
+ if diff[:hint]
236
+ lines << " Hint: #{diff[:hint]}"
237
+ end
238
+ return lines.join("\n")
239
+ end
240
+
241
+ if diff[:added].empty? && diff[:removed].empty? && diff[:deprecated].empty?
242
+ lines << " = (no changes)"
243
+ if diff[:last_synced]
244
+ lines << " Last synced: #{diff[:last_synced]}"
245
+ end
246
+ return lines.join("\n")
247
+ end
248
+
249
+ diff[:added].each do |model|
250
+ release_date = diff[:added_with_dates]&.[](model)
251
+ lines << if release_date
252
+ " + #{model.ljust(35)} (new, released: #{release_date})"
253
+ else
254
+ " + #{model.ljust(35)} (new)"
255
+ end
256
+ end
257
+
258
+ diff[:removed].each do |model|
259
+ lines << " - #{model.ljust(35)} (removed)"
260
+ end
261
+
262
+ diff[:deprecated].each do |model|
263
+ lines << " ! #{model.ljust(35)} (deprecated)"
264
+ end
265
+
266
+ if diff[:last_synced]
267
+ lines << " Last synced: #{diff[:last_synced]}"
268
+ end
269
+
270
+ lines << "" if diff[:added].any? || diff[:removed].any?
271
+
272
+ lines.join("\n")
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Organisms
7
+ # Orchestrates the sync workflow
8
+ class SyncOrchestrator
9
+ DEFAULT_CACHE_MAX_AGE = 86_400 # 24 hours
10
+
11
+ # Initialize orchestrator
12
+ # @param cache_manager [Molecules::CacheManager, nil] Cache manager instance
13
+ def initialize(cache_manager: nil)
14
+ @cache_manager = cache_manager || Molecules::CacheManager.new
15
+ end
16
+
17
+ # Sync models from API
18
+ # @param force [Boolean] Force sync even if cache is fresh
19
+ # @param max_age [Integer] Max cache age in seconds
20
+ # @return [Hash] Sync result with stats
21
+ def sync(force: false, max_age: DEFAULT_CACHE_MAX_AGE)
22
+ # Check if we need to sync
23
+ unless force
24
+ if @cache_manager.fresh?(max_age: max_age)
25
+ return {
26
+ status: :skipped,
27
+ message: "Cache is fresh (less than #{max_age / 3600}h old)",
28
+ last_sync_at: @cache_manager.last_sync_at
29
+ }
30
+ end
31
+ end
32
+
33
+ # Fetch from API
34
+ start_time = Time.now
35
+ raw_json = Atoms::ApiFetcher.fetch
36
+
37
+ # Parse JSON
38
+ data = Atoms::JsonParser.parse(raw_json)
39
+
40
+ # Write to cache
41
+ @cache_manager.write(data)
42
+
43
+ # Calculate stats
44
+ stats = calculate_stats(data)
45
+ duration = Time.now - start_time
46
+
47
+ {
48
+ status: :success,
49
+ message: "Synced #{stats[:model_count]} models from #{stats[:provider_count]} providers",
50
+ duration: duration.round(2),
51
+ stats: stats,
52
+ sync_at: Time.now
53
+ }
54
+ rescue NetworkError, ApiError => e
55
+ {
56
+ status: :error,
57
+ message: e.message,
58
+ error_class: e.class.name
59
+ }
60
+ end
61
+
62
+ # Check sync status
63
+ # @return [Hash] Status info
64
+ def status
65
+ if @cache_manager.exists?
66
+ data = @cache_manager.read
67
+ stats = calculate_stats(data) if data
68
+
69
+ {
70
+ cached: true,
71
+ fresh: @cache_manager.fresh?,
72
+ last_sync_at: @cache_manager.last_sync_at,
73
+ stats: stats
74
+ }
75
+ else
76
+ {
77
+ cached: false,
78
+ fresh: false,
79
+ last_sync_at: nil,
80
+ stats: nil
81
+ }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def calculate_stats(data)
88
+ provider_count = data.size
89
+ model_count = 0
90
+ models_by_provider = {}
91
+
92
+ data.each do |provider_id, provider_data|
93
+ count = (provider_data["models"] || {}).size
94
+ models_by_provider[provider_id] = count
95
+ model_count += count
96
+ end
97
+
98
+ {
99
+ provider_count: provider_count,
100
+ model_count: model_count,
101
+ top_providers: models_by_provider.sort_by { |_, v| -v }.first(10).to_h
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ VERSION = "0.9.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "models/models"
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ace-support-models
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Czyz
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ace-support-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.25'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ace-support-cli
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday-retry
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ description: Integrates with models.dev API to provide model validation, cost tracking,
69
+ and change monitoring for 40+ LLM providers. Validate model names, calculate query
70
+ costs, and track pricing changes.
71
+ email:
72
+ - mc@cs3b.com
73
+ executables:
74
+ - ace-models
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - exe/ace-llm-providers
83
+ - exe/ace-models
84
+ - lib/ace/support/models.rb
85
+ - lib/ace/support/models/atoms/api_fetcher.rb
86
+ - lib/ace/support/models/atoms/cache_path_resolver.rb
87
+ - lib/ace/support/models/atoms/file_reader.rb
88
+ - lib/ace/support/models/atoms/file_writer.rb
89
+ - lib/ace/support/models/atoms/json_parser.rb
90
+ - lib/ace/support/models/atoms/model_filter.rb
91
+ - lib/ace/support/models/atoms/model_name_canonicalizer.rb
92
+ - lib/ace/support/models/atoms/provider_config_reader.rb
93
+ - lib/ace/support/models/atoms/provider_config_writer.rb
94
+ - lib/ace/support/models/cli.rb
95
+ - lib/ace/support/models/cli/commands/cache/clear.rb
96
+ - lib/ace/support/models/cli/commands/cache/diff.rb
97
+ - lib/ace/support/models/cli/commands/cache/status.rb
98
+ - lib/ace/support/models/cli/commands/cache/sync.rb
99
+ - lib/ace/support/models/cli/commands/info.rb
100
+ - lib/ace/support/models/cli/commands/models/cost.rb
101
+ - lib/ace/support/models/cli/commands/models/info.rb
102
+ - lib/ace/support/models/cli/commands/models/search.rb
103
+ - lib/ace/support/models/cli/commands/providers/list.rb
104
+ - lib/ace/support/models/cli/commands/providers/show.rb
105
+ - lib/ace/support/models/cli/commands/providers/sync.rb
106
+ - lib/ace/support/models/cli/commands/search.rb
107
+ - lib/ace/support/models/cli/commands/sync_shortcut.rb
108
+ - lib/ace/support/models/cli/providers_cli.rb
109
+ - lib/ace/support/models/errors.rb
110
+ - lib/ace/support/models/models.rb
111
+ - lib/ace/support/models/models/diff_result.rb
112
+ - lib/ace/support/models/models/model_info.rb
113
+ - lib/ace/support/models/models/pricing_info.rb
114
+ - lib/ace/support/models/models/provider_info.rb
115
+ - lib/ace/support/models/molecules/cache_manager.rb
116
+ - lib/ace/support/models/molecules/cost_calculator.rb
117
+ - lib/ace/support/models/molecules/diff_generator.rb
118
+ - lib/ace/support/models/molecules/model_searcher.rb
119
+ - lib/ace/support/models/molecules/model_validator.rb
120
+ - lib/ace/support/models/molecules/provider_sync_diff.rb
121
+ - lib/ace/support/models/organisms/provider_sync_orchestrator.rb
122
+ - lib/ace/support/models/organisms/sync_orchestrator.rb
123
+ - lib/ace/support/models/version.rb
124
+ homepage: https://github.com/cs3b/ace
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ allowed_push_host: https://rubygems.org
129
+ homepage_uri: https://github.com/cs3b/ace
130
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-support-models/
131
+ changelog_uri: https://github.com/cs3b/ace/blob/main/ace-support-models/CHANGELOG.md
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 3.2.0
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.6.9
147
+ specification_version: 4
148
+ summary: Model metadata, validation, and cost tracking for ACE
149
+ test_files: []