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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module Cache
11
+ # Show cache info (freshness, age, counts)
12
+ class Status < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Show cache info (freshness, age, counts)"
16
+
17
+ option :json, type: :boolean, desc: "Output as JSON"
18
+
19
+ def call(**options)
20
+ status_data = Organisms::SyncOrchestrator.new.status
21
+
22
+ if options[:json]
23
+ puts JSON.pretty_generate(status_data)
24
+ return
25
+ end
26
+
27
+ unless status_data[:cached]
28
+ raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models cache sync' first.")
29
+ end
30
+
31
+ puts "Cache Status:"
32
+ puts " Cached: Yes"
33
+ puts " Fresh: #{status_data[:fresh] ? "Yes" : "No (stale)"}"
34
+ puts " Last sync: #{status_data[:last_sync_at]}"
35
+ puts
36
+
37
+ if status_data[:stats]
38
+ puts "Statistics:"
39
+ puts " Providers: #{status_data[:stats][:provider_count]}"
40
+ puts " Models: #{status_data[:stats][:model_count]}"
41
+ puts
42
+ puts "Top providers by model count:"
43
+ status_data[:stats][:top_providers].each do |provider, count|
44
+ puts " #{provider}: #{count}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module Cache
11
+ # Fetch models from models.dev API
12
+ class Sync < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Fetch models from models.dev API"
16
+
17
+ option :force, type: :boolean, aliases: ["-f"], desc: "Force sync even if cache is fresh"
18
+ option :json, type: :boolean, desc: "Output as JSON"
19
+
20
+ example [
21
+ " # Sync cache",
22
+ "--force # Force sync even if cache is fresh",
23
+ "--json # Output as JSON"
24
+ ]
25
+
26
+ def call(**options)
27
+ result = Organisms::SyncOrchestrator.new.sync(force: options[:force])
28
+
29
+ if options[:json]
30
+ puts JSON.pretty_generate(result)
31
+ return
32
+ end
33
+
34
+ case result[:status]
35
+ when :success
36
+ puts result[:message]
37
+ puts "Duration: #{result[:duration]}s"
38
+ when :skipped
39
+ puts result[:message]
40
+ puts "Last synced: #{result[:last_sync_at]}"
41
+ when :error
42
+ raise Ace::Support::Cli::Error.new(result[:message])
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ # Top-level shortcut for models info
11
+ class InfoShortcut < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+
14
+ desc "Show model info (shortcut for: models info)"
15
+
16
+ argument :model_id, required: true, desc: "Model ID (provider:model)"
17
+ option :full, type: :boolean, desc: "Show complete details"
18
+ option :json, type: :boolean, desc: "Output as JSON"
19
+
20
+ example [
21
+ "openai:gpt-4o # Brief info",
22
+ "openai:gpt-4o --full # Full details"
23
+ ]
24
+
25
+ def call(model_id:, **options)
26
+ ModelsSubcommands::Info.new.call(model_id: model_id, **options)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module ModelsSubcommands
11
+ # Show pricing for a model
12
+ class Cost < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Show pricing for a model"
16
+
17
+ argument :model_id, required: true, desc: "Model ID (provider:model)"
18
+ option :input, type: :integer, aliases: ["-i"], default: 1000, desc: "Input tokens"
19
+ option :output, type: :integer, aliases: ["-o"], default: 500, desc: "Output tokens"
20
+ option :reasoning, type: :integer, aliases: ["-r"], default: 0, desc: "Reasoning tokens"
21
+ option :json, type: :boolean, desc: "Output as JSON"
22
+
23
+ example [
24
+ "openai:gpt-4o # Default token counts",
25
+ "openai:gpt-4o -i 5000 -o 2000 # Custom token counts",
26
+ "anthropic:claude-3-opus --json # JSON output"
27
+ ]
28
+
29
+ def call(model_id:, **options)
30
+ calculator = Molecules::CostCalculator.new
31
+ result = calculator.calculate(
32
+ model_id,
33
+ input_tokens: options[:input],
34
+ output_tokens: options[:output],
35
+ reasoning_tokens: options[:reasoning]
36
+ )
37
+
38
+ if options[:json]
39
+ puts JSON.pretty_generate(result)
40
+ else
41
+ puts calculator.format(result)
42
+ end
43
+ rescue ProviderNotFoundError, ModelNotFoundError => e
44
+ raise Ace::Support::Cli::Error.new(e.message)
45
+ rescue CacheError => e
46
+ raise Ace::Support::Cli::Error.new(e.message)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module ModelsSubcommands
11
+ # Show model information
12
+ class Info < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Show model information (brief by default)"
16
+
17
+ argument :model_id, required: true, desc: "Model ID (provider:model)"
18
+ option :full, type: :boolean, desc: "Show complete details"
19
+ option :json, type: :boolean, desc: "Output as JSON"
20
+
21
+ example [
22
+ "openai:gpt-4o # Brief info (default)",
23
+ "openai:gpt-4o --full # Full details",
24
+ "anthropic:claude-3-opus --json # JSON output"
25
+ ]
26
+
27
+ def call(model_id:, **options)
28
+ model = Molecules::ModelValidator.new.validate(model_id)
29
+
30
+ if options[:json]
31
+ puts JSON.pretty_generate(model.to_h)
32
+ elsif options[:full]
33
+ puts format_model_info_full(model)
34
+ else
35
+ puts format_model_info_brief(model)
36
+ end
37
+ rescue ProviderNotFoundError => e
38
+ raise Ace::Support::Cli::Error.new("Model '#{e.model_id || model_id}' not found in provider '#{e.provider_id}'")
39
+ rescue ModelNotFoundError => e
40
+ raise Ace::Support::Cli::Error.new(e.message)
41
+ rescue CacheError => e
42
+ raise Ace::Support::Cli::Error.new(e.message)
43
+ end
44
+
45
+ private
46
+
47
+ def format_model_info_brief(model)
48
+ lines = []
49
+ lines << "#{model.name} (#{model.full_id})"
50
+ lines << " Provider: #{model.provider_id}"
51
+ lines << " Status: #{model.status || "active"}"
52
+ lines << " Context: #{format_number(model.context_limit)} tokens"
53
+ lines << " Output: #{format_number(model.output_limit)} tokens"
54
+
55
+ pricing = model.pricing
56
+ if pricing&.available?
57
+ lines << " Pricing: $#{sprintf("%.2f", pricing.input)}/M input, $#{sprintf("%.2f", pricing.output)}/M output"
58
+ end
59
+
60
+ caps = model.capabilities
61
+ enabled = []
62
+ enabled << "reasoning" if caps[:reasoning]
63
+ enabled << "tools" if caps[:tool_call]
64
+ enabled << "structured" if caps[:structured_output]
65
+ lines << " Capabilities: #{enabled.any? ? enabled.join(", ") : "none"}"
66
+
67
+ lines << ""
68
+ lines << "Use --full for complete details"
69
+
70
+ lines.join("\n")
71
+ end
72
+
73
+ def format_model_info_full(model)
74
+ lines = []
75
+ lines << "Model: #{model.name} (#{model.full_id})"
76
+ lines << "Provider: #{model.provider_id}"
77
+ lines << "Status: #{model.status || "active"}"
78
+ lines << ""
79
+
80
+ lines << "Capabilities:"
81
+ caps = model.capabilities
82
+ lines << " Reasoning: #{caps[:reasoning] ? "Yes" : "No"}"
83
+ lines << " Tool Call: #{caps[:tool_call] ? "Yes" : "No"}"
84
+ lines << " Structured Output: #{caps[:structured_output] ? "Yes" : "No"}"
85
+ lines << " Attachment: #{caps[:attachment] ? "Yes" : "No"}"
86
+ lines << " Temperature: #{caps[:temperature] ? "Yes" : "No"}"
87
+ lines << ""
88
+
89
+ lines << "Modalities:"
90
+ lines << " Input: #{model.modalities[:input]&.join(", ") || "none"}"
91
+ lines << " Output: #{model.modalities[:output]&.join(", ") || "none"}"
92
+ lines << ""
93
+
94
+ lines << "Limits:"
95
+ lines << " Context: #{format_number(model.context_limit)} tokens"
96
+ lines << " Output: #{format_number(model.output_limit)} tokens"
97
+ lines << ""
98
+
99
+ pricing = model.pricing
100
+ if pricing&.available?
101
+ lines << "Pricing (per million tokens):"
102
+ lines << " Input: #{format_price(pricing.input)}"
103
+ lines << " Output: #{format_price(pricing.output)}"
104
+ lines << " Cache Read: #{format_price(pricing.cache_read)}" if pricing.cache_read
105
+ lines << " Cache Write: #{format_price(pricing.cache_write)}" if pricing.cache_write
106
+ lines << " Reasoning: #{format_price(pricing.reasoning)}" if pricing.reasoning
107
+ lines << ""
108
+ end
109
+
110
+ lines << "Metadata:"
111
+ lines << " Knowledge: #{model.knowledge_date || "unknown"}"
112
+ lines << " Released: #{model.release_date || "unknown"}"
113
+ lines << " Updated: #{model.last_updated || "unknown"}"
114
+ lines << " Open Weights: #{model.open_weights ? "Yes" : "No"}"
115
+
116
+ lines.join("\n")
117
+ end
118
+
119
+ def format_number(num)
120
+ return "unknown" unless num
121
+
122
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
123
+ end
124
+
125
+ def format_price(price)
126
+ return "N/A" unless price
127
+
128
+ "$#{sprintf("%.2f", price)}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ # Note: Using Models_ prefix to avoid conflict with outer Models module
11
+ module ModelsSubcommands
12
+ # Search for models
13
+ class Search < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+
16
+ desc "Search for models (query optional with filters)"
17
+
18
+ argument :query, required: false, desc: "Search query"
19
+ option :provider, type: :string, aliases: ["-p"], desc: "Limit to provider"
20
+ option :limit, type: :integer, aliases: ["-l"], default: 20, desc: "Max results"
21
+ option :filter, type: :array, aliases: ["-f"], desc: "Filter by key:value (repeatable)"
22
+ option :json, type: :boolean, desc: "Output as JSON"
23
+
24
+ example [
25
+ "gpt-4 # Search by name",
26
+ "opus -p anthropic # Search within a provider",
27
+ "-f reasoning:true # Filter by capability",
28
+ "-f tool_call:true -f min_context:100000 # Multiple filters",
29
+ "gpt -l 5 # Limit results"
30
+ ]
31
+
32
+ def call(query: nil, **options)
33
+ # Validate filters before searching
34
+ filter_errors = Atoms::ModelFilter.validate(options[:filter])
35
+ unless filter_errors.empty?
36
+ filter_errors.each { |e| warn "Error: #{e}" }
37
+ raise Ace::Support::Cli::Error.new("Invalid model search filters")
38
+ end
39
+
40
+ searcher = Molecules::ModelSearcher.new
41
+ filters = parse_filters(options[:filter])
42
+ limit = options[:limit] || 20
43
+
44
+ # Single search with total count for efficient pagination
45
+ result = searcher.search(
46
+ query,
47
+ provider: options[:provider],
48
+ limit: limit,
49
+ filters: filters.empty? ? nil : filters,
50
+ with_total: true
51
+ )
52
+
53
+ models = result[:models]
54
+ total_models_count = result[:total]
55
+
56
+ if models.empty?
57
+ if options[:json]
58
+ puts "[]"
59
+ else
60
+ message = query ? "No models found matching '#{query}'" : "No models found"
61
+ message += " with filters: #{options[:filter].join(", ")}" if options[:filter]&.any?
62
+ puts message
63
+ end
64
+ return
65
+ end
66
+
67
+ if options[:json]
68
+ json_result = {
69
+ models: models.map(&:to_h),
70
+ showing: models.size,
71
+ total: total_models_count
72
+ }
73
+ puts JSON.pretty_generate(json_result)
74
+ else
75
+ if models.size < total_models_count
76
+ puts "Showing #{models.size} of #{total_models_count} results:"
77
+ else
78
+ puts "Found #{models.size} model(s):"
79
+ end
80
+ models.each do |model|
81
+ status = model.deprecated? ? " (deprecated)" : ""
82
+ puts " #{model.full_id}#{status}"
83
+ puts " #{model.name}"
84
+ end
85
+ end
86
+ rescue CacheError => e
87
+ raise Ace::Support::Cli::Error.new(e.message)
88
+ end
89
+
90
+ private
91
+
92
+ def parse_filters(filter_array)
93
+ Atoms::ModelFilter.parse_all(filter_array)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module Providers
11
+ # List all providers with model counts
12
+ class List < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "List all providers with model counts"
16
+
17
+ option :json, type: :boolean, desc: "Output as JSON"
18
+
19
+ def call(**options)
20
+ cache_manager = Molecules::CacheManager.new
21
+
22
+ unless cache_manager.cached?
23
+ raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models cache sync' first.")
24
+ end
25
+
26
+ providers = cache_manager.list_providers
27
+
28
+ if options[:json]
29
+ puts JSON.pretty_generate(providers)
30
+ return
31
+ end
32
+
33
+ puts "Providers (#{providers.size}):"
34
+ providers.sort_by { |p| -p[:model_count] }.each do |provider|
35
+ puts " #{provider[:id]}: #{provider[:model_count]} models"
36
+ end
37
+ rescue CacheError => e
38
+ raise Ace::Support::Cli::Error.new(e.message)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module Providers
11
+ # Show provider details and models
12
+ class Show < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Show provider details and models"
16
+
17
+ argument :provider_id, required: true, desc: "Provider ID"
18
+ option :json, type: :boolean, desc: "Output as JSON"
19
+
20
+ def call(provider_id:, **options)
21
+ cache_manager = Molecules::CacheManager.new
22
+
23
+ unless cache_manager.cached?
24
+ raise Ace::Support::Cli::Error.new("No cache data. Run 'ace-models cache sync' first.")
25
+ end
26
+
27
+ provider_data = cache_manager.get_provider(provider_id)
28
+
29
+ unless provider_data
30
+ raise Ace::Support::Cli::Error.new("Provider '#{provider_id}' not found")
31
+ end
32
+
33
+ if options[:json]
34
+ puts JSON.pretty_generate(provider_data)
35
+ return
36
+ end
37
+
38
+ puts "Provider: #{provider_id}"
39
+ puts "Models (#{provider_data[:models].size}):"
40
+ provider_data[:models].each do |model|
41
+ status = model[:deprecated] ? " (deprecated)" : ""
42
+ puts " #{model[:id]}#{status}"
43
+ puts " #{model[:name]}"
44
+ end
45
+ rescue CacheError => e
46
+ raise Ace::Support::Cli::Error.new(e.message)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ module Providers
11
+ # Sync provider YAML configs with models.dev
12
+ class Sync < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc "Sync provider YAML configs with models.dev"
16
+
17
+ option :apply, type: :boolean, desc: "Apply changes to config files (default: dry-run)"
18
+ option :commit, type: :boolean, desc: "Commit changes via ace-git-commit"
19
+ option :provider, type: :string, aliases: ["-p"], desc: "Sync specific provider only"
20
+ option :config_dir, type: :string, desc: "Target config directory"
21
+ option :all, type: :boolean, desc: "Show all models regardless of release date"
22
+ option :since, type: :string, desc: "Show models released after DATE (YYYY-MM-DD)"
23
+ option :json, type: :boolean, desc: "Output as JSON"
24
+
25
+ example [
26
+ " # Dry-run: see what would change",
27
+ "-p openai # Sync specific provider only",
28
+ "--since 2024-01-01 # Show models released after a date",
29
+ "--all # Show all models (ignore release date filter)",
30
+ "--apply # Apply changes to config files",
31
+ "--apply --commit # Apply and commit changes"
32
+ ]
33
+
34
+ def call(**options)
35
+ orchestrator = Organisms::ProviderSyncOrchestrator.new
36
+
37
+ result = orchestrator.sync(
38
+ config_dir: options[:config_dir],
39
+ provider: options[:provider],
40
+ apply: options[:apply],
41
+ commit: options[:commit],
42
+ show_all: options[:all],
43
+ since: options[:since]
44
+ )
45
+
46
+ if result[:status] == :error
47
+ raise Ace::Support::Cli::Error.new(result[:message])
48
+ end
49
+
50
+ if options[:json]
51
+ puts JSON.pretty_generate(result)
52
+ else
53
+ puts orchestrator.format_result(result)
54
+ end
55
+ rescue CacheError => e
56
+ raise Ace::Support::Cli::Error.new("#{e.message}. Run 'ace-models cache sync' first to download model data.")
57
+ rescue ConfigError => e
58
+ raise Ace::Support::Cli::Error.new("Config error: #{e.message}")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ # Top-level shortcut for models search
11
+ class SearchShortcut < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+
14
+ desc "Search models (shortcut for: models search)"
15
+
16
+ argument :query, required: false, desc: "Search query"
17
+ option :provider, type: :string, aliases: ["-p"], desc: "Limit to provider"
18
+ option :limit, type: :integer, aliases: ["-l"], default: 20, desc: "Max results"
19
+ option :filter, type: :array, aliases: ["-f"], desc: "Filter by key:value (repeatable)"
20
+ option :json, type: :boolean, desc: "Output as JSON"
21
+
22
+ example [
23
+ "gpt-4 # Search by name",
24
+ "opus -p anthropic # Search within a provider"
25
+ ]
26
+
27
+ def call(query: nil, **options)
28
+ ModelsSubcommands::Search.new.call(query: query, **options)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module CLI
9
+ module Commands
10
+ # Top-level shortcut for cache sync
11
+ class SyncShortcut < Ace::Support::Cli::Command
12
+ include Ace::Support::Cli::Base
13
+
14
+ desc "Sync from models.dev (shortcut for: cache sync)"
15
+
16
+ option :force, type: :boolean, aliases: ["-f"], desc: "Force sync even if cache is fresh"
17
+ option :json, type: :boolean, desc: "Output as JSON"
18
+
19
+ example [
20
+ " # Sync cache",
21
+ "--force # Force sync even if cache is fresh"
22
+ ]
23
+
24
+ def call(**options)
25
+ Cache::Sync.new.call(**options)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end