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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +162 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/Rakefile +13 -0
- data/exe/ace-llm-providers +19 -0
- data/exe/ace-models +23 -0
- data/lib/ace/support/models/atoms/api_fetcher.rb +76 -0
- data/lib/ace/support/models/atoms/cache_path_resolver.rb +38 -0
- data/lib/ace/support/models/atoms/file_reader.rb +43 -0
- data/lib/ace/support/models/atoms/file_writer.rb +63 -0
- data/lib/ace/support/models/atoms/json_parser.rb +38 -0
- data/lib/ace/support/models/atoms/model_filter.rb +107 -0
- data/lib/ace/support/models/atoms/model_name_canonicalizer.rb +119 -0
- data/lib/ace/support/models/atoms/provider_config_reader.rb +218 -0
- data/lib/ace/support/models/atoms/provider_config_writer.rb +230 -0
- data/lib/ace/support/models/cli/commands/cache/clear.rb +43 -0
- data/lib/ace/support/models/cli/commands/cache/diff.rb +74 -0
- data/lib/ace/support/models/cli/commands/cache/status.rb +54 -0
- data/lib/ace/support/models/cli/commands/cache/sync.rb +51 -0
- data/lib/ace/support/models/cli/commands/info.rb +33 -0
- data/lib/ace/support/models/cli/commands/models/cost.rb +54 -0
- data/lib/ace/support/models/cli/commands/models/info.rb +136 -0
- data/lib/ace/support/models/cli/commands/models/search.rb +101 -0
- data/lib/ace/support/models/cli/commands/providers/list.rb +46 -0
- data/lib/ace/support/models/cli/commands/providers/show.rb +54 -0
- data/lib/ace/support/models/cli/commands/providers/sync.rb +66 -0
- data/lib/ace/support/models/cli/commands/search.rb +35 -0
- data/lib/ace/support/models/cli/commands/sync_shortcut.rb +32 -0
- data/lib/ace/support/models/cli/providers_cli.rb +72 -0
- data/lib/ace/support/models/cli.rb +84 -0
- data/lib/ace/support/models/errors.rb +55 -0
- data/lib/ace/support/models/models/diff_result.rb +94 -0
- data/lib/ace/support/models/models/model_info.rb +129 -0
- data/lib/ace/support/models/models/pricing_info.rb +74 -0
- data/lib/ace/support/models/models/provider_info.rb +81 -0
- data/lib/ace/support/models/models.rb +97 -0
- data/lib/ace/support/models/molecules/cache_manager.rb +237 -0
- data/lib/ace/support/models/molecules/cost_calculator.rb +135 -0
- data/lib/ace/support/models/molecules/diff_generator.rb +171 -0
- data/lib/ace/support/models/molecules/model_searcher.rb +176 -0
- data/lib/ace/support/models/molecules/model_validator.rb +177 -0
- data/lib/ace/support/models/molecules/provider_sync_diff.rb +291 -0
- data/lib/ace/support/models/organisms/provider_sync_orchestrator.rb +278 -0
- data/lib/ace/support/models/organisms/sync_orchestrator.rb +108 -0
- data/lib/ace/support/models/version.rb +9 -0
- data/lib/ace/support/models.rb +3 -0
- 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
|