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,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../version"
6
+
7
+ # Reuse existing provider command classes
8
+ require_relative "commands/providers/list"
9
+ require_relative "commands/providers/show"
10
+ require_relative "commands/providers/sync"
11
+
12
+ module Ace
13
+ module Support
14
+ module Models
15
+ # Flat CLI registry for ace-llm-providers (LLM provider management).
16
+ #
17
+ # Replaces the nested `ace-models providers <subcommand>` pattern with
18
+ # flat `ace-llm-providers <command>` invocations.
19
+ module ProvidersCLI
20
+ extend Ace::Support::Cli::RegistryDsl
21
+
22
+ PROGRAM_NAME = "ace-llm-providers"
23
+
24
+ # Application commands with descriptions (for help output)
25
+ REGISTERED_COMMANDS = [
26
+ ["list", "List all available LLM providers"],
27
+ ["show", "Show detailed information for a provider"],
28
+ ["sync", "Synchronize provider configurations"]
29
+ ].freeze
30
+
31
+ HELP_EXAMPLES = [
32
+ "ace-llm-providers",
33
+ "ace-llm-providers show openai",
34
+ "ace-llm-providers sync --apply",
35
+ "ace-llm-providers sync -p anthropic"
36
+ ].freeze
37
+
38
+ # Register flat commands (reusing existing command classes)
39
+ register "list", CLI::Commands::Providers::List
40
+ register "show", CLI::Commands::Providers::Show
41
+ register "sync", CLI::Commands::Providers::Sync
42
+
43
+ # Register version command
44
+ version_cmd = Ace::Support::Cli::VersionCommand.build(
45
+ gem_name: PROGRAM_NAME,
46
+ version: VERSION
47
+ )
48
+ register "version", version_cmd
49
+ register "--version", version_cmd
50
+
51
+ # Register help command
52
+ help_cmd = Ace::Support::Cli::HelpCommand.build(
53
+ program_name: PROGRAM_NAME,
54
+ version: VERSION,
55
+ commands: REGISTERED_COMMANDS,
56
+ examples: HELP_EXAMPLES
57
+ )
58
+ register "help", help_cmd
59
+ register "--help", help_cmd
60
+ register "-h", help_cmd
61
+
62
+ # Entry point for CLI invocation (used by tests and exe/)
63
+ #
64
+ # @param args [Array<String>] Command-line arguments
65
+ # @return [Integer] Exit code (0 for success, non-zero for errors)
66
+ def self.start(args)
67
+ Ace::Support::Cli::Runner.new(self).call(args: args)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "json"
5
+ require "ace/core"
6
+
7
+ # Load CLI command classes (Hanami pattern: CLI::Commands::)
8
+ require_relative "cli/commands/cache/sync"
9
+ require_relative "cli/commands/cache/status"
10
+ require_relative "cli/commands/cache/diff"
11
+ require_relative "cli/commands/cache/clear"
12
+ require_relative "cli/commands/models/search"
13
+ require_relative "cli/commands/models/info"
14
+ require_relative "cli/commands/models/cost"
15
+
16
+ module Ace
17
+ module Support
18
+ module Models
19
+ # CLI for ace-models using ace-support-cli.
20
+ #
21
+ # After the split, ace-models handles cache and model operations.
22
+ # Provider management moved to ace-llm-providers.
23
+ module CLI
24
+ extend Ace::Support::Cli::RegistryDsl
25
+
26
+ PROGRAM_NAME = "ace-models"
27
+
28
+ # Application commands with descriptions (for help output)
29
+ REGISTERED_COMMANDS = [
30
+ ["search", "Search for models by name or pattern"],
31
+ ["info", "Show detailed model information"],
32
+ ["cost", "Calculate token cost for a model"],
33
+ ["sync", "Sync model cache from models.dev"],
34
+ ["status", "Show cache sync status"],
35
+ ["diff", "Show changes since last sync"],
36
+ ["clear", "Clear the model cache"]
37
+ ].freeze
38
+
39
+ HELP_EXAMPLES = [
40
+ "ace-models search claude # Find models by name",
41
+ "ace-models info claude-sonnet-4-6 # Pricing and context window",
42
+ "ace-models cost claude-opus-4-6 # Token cost calculator",
43
+ "ace-models sync # Update from models.dev"
44
+ ].freeze
45
+
46
+ # Register flat commands (previously nested under cache/models namespaces)
47
+ register "search", CLI::Commands::ModelsSubcommands::Search
48
+ register "info", CLI::Commands::ModelsSubcommands::Info
49
+ register "cost", CLI::Commands::ModelsSubcommands::Cost
50
+ register "sync", CLI::Commands::Cache::Sync
51
+ register "status", CLI::Commands::Cache::Status
52
+ register "diff", CLI::Commands::Cache::Diff
53
+ register "clear", CLI::Commands::Cache::Clear
54
+
55
+ # Version command
56
+ version_cmd = Ace::Support::Cli::VersionCommand.build(
57
+ gem_name: PROGRAM_NAME,
58
+ version: VERSION
59
+ )
60
+ register "version", version_cmd
61
+ register "--version", version_cmd
62
+
63
+ # Help command
64
+ help_cmd = Ace::Support::Cli::HelpCommand.build(
65
+ program_name: PROGRAM_NAME,
66
+ version: VERSION,
67
+ commands: REGISTERED_COMMANDS,
68
+ examples: HELP_EXAMPLES
69
+ )
70
+ register "help", help_cmd
71
+ register "--help", help_cmd
72
+ register "-h", help_cmd
73
+
74
+ # Entry point for CLI invocation (used by tests and exe/)
75
+ #
76
+ # @param args [Array<String>] Command-line arguments
77
+ # @return [Integer] Exit code (0 for success, non-zero for errors)
78
+ def self.start(args)
79
+ Ace::Support::Cli::Runner.new(self).call(args: args)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ # Base error class for ace-support-models gem
7
+ class Error < StandardError; end
8
+
9
+ # Network-related errors
10
+ class NetworkError < Error; end
11
+
12
+ # API response errors
13
+ class ApiError < Error
14
+ attr_reader :status_code
15
+
16
+ def initialize(message, status_code: nil)
17
+ @status_code = status_code
18
+ super(message)
19
+ end
20
+ end
21
+
22
+ # Cache-related errors
23
+ class CacheError < Error; end
24
+
25
+ # Validation errors
26
+ class ValidationError < Error; end
27
+
28
+ # Model not found error
29
+ class ModelNotFoundError < ValidationError
30
+ attr_reader :model_id, :suggestions
31
+
32
+ def initialize(model_id, suggestions: [])
33
+ @model_id = model_id
34
+ @suggestions = suggestions
35
+ message = "Model '#{model_id}' not found"
36
+ message += ". Did you mean: #{suggestions.join(", ")}?" if suggestions.any?
37
+ super(message)
38
+ end
39
+ end
40
+
41
+ # Configuration errors
42
+ class ConfigError < Error; end
43
+
44
+ # Provider not found error
45
+ class ProviderNotFoundError < ValidationError
46
+ attr_reader :provider_id
47
+
48
+ def initialize(provider_id)
49
+ @provider_id = provider_id
50
+ super("Provider '#{provider_id}' not found")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Models
7
+ # Represents the diff between two API versions
8
+ class DiffResult
9
+ attr_reader :added_models, :removed_models, :updated_models,
10
+ :added_providers, :removed_providers,
11
+ :previous_sync_at, :current_sync_at
12
+
13
+ # Initialize diff result
14
+ def initialize(attrs = {})
15
+ @added_models = attrs[:added_models] || []
16
+ @removed_models = attrs[:removed_models] || []
17
+ @updated_models = attrs[:updated_models] || []
18
+ @added_providers = attrs[:added_providers] || []
19
+ @removed_providers = attrs[:removed_providers] || []
20
+ @previous_sync_at = attrs[:previous_sync_at]
21
+ @current_sync_at = attrs[:current_sync_at]
22
+ end
23
+
24
+ # Check if there are any changes
25
+ # @return [Boolean]
26
+ def any_changes?
27
+ added_models.any? || removed_models.any? || updated_models.any? ||
28
+ added_providers.any? || removed_providers.any?
29
+ end
30
+
31
+ # Total number of changes
32
+ # @return [Integer]
33
+ def total_changes
34
+ added_models.size + removed_models.size + updated_models.size +
35
+ added_providers.size + removed_providers.size
36
+ end
37
+
38
+ # Summary string
39
+ # @return [String]
40
+ def summary
41
+ parts = []
42
+ parts << "#{added_models.size} new model(s)" if added_models.any?
43
+ parts << "#{removed_models.size} removed model(s)" if removed_models.any?
44
+ parts << "#{updated_models.size} updated model(s)" if updated_models.any?
45
+ parts << "#{added_providers.size} new provider(s)" if added_providers.any?
46
+ parts << "#{removed_providers.size} removed provider(s)" if removed_providers.any?
47
+
48
+ return "No changes" if parts.empty?
49
+
50
+ parts.join(", ")
51
+ end
52
+
53
+ # Convert to hash
54
+ # @return [Hash]
55
+ def to_h
56
+ {
57
+ added_models: added_models,
58
+ removed_models: removed_models,
59
+ updated_models: updated_models,
60
+ added_providers: added_providers,
61
+ removed_providers: removed_providers,
62
+ previous_sync_at: previous_sync_at&.iso8601,
63
+ current_sync_at: current_sync_at&.iso8601,
64
+ summary: summary
65
+ }
66
+ end
67
+ end
68
+
69
+ # Represents an update to a model
70
+ class ModelUpdate
71
+ attr_reader :model_id, :changes
72
+
73
+ def initialize(model_id:, changes:)
74
+ @model_id = model_id
75
+ @changes = changes
76
+ end
77
+
78
+ # Convert to hash
79
+ def to_h
80
+ {
81
+ model_id: model_id,
82
+ changes: changes
83
+ }
84
+ end
85
+
86
+ # Summary of changes
87
+ def summary
88
+ changes.map { |field, (old_val, new_val)| "#{field}: #{old_val} → #{new_val}" }.join(", ")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Models
7
+ # Represents information about a single model
8
+ class ModelInfo
9
+ attr_reader :id, :name, :provider_id, :pricing, :context_limit, :output_limit,
10
+ :modalities, :capabilities, :status, :knowledge_date, :release_date,
11
+ :last_updated, :open_weights
12
+
13
+ # Initialize model info
14
+ def initialize(attrs = {})
15
+ @id = attrs[:id]
16
+ @name = attrs[:name]
17
+ @provider_id = attrs[:provider_id]
18
+ @pricing = attrs[:pricing] || PricingInfo.new
19
+ @context_limit = attrs[:context_limit]
20
+ @output_limit = attrs[:output_limit]
21
+ @modalities = attrs[:modalities] || {input: [], output: []}
22
+ @capabilities = attrs[:capabilities] || {}
23
+ @status = attrs[:status]
24
+ @knowledge_date = attrs[:knowledge_date]
25
+ @release_date = attrs[:release_date]
26
+ @last_updated = attrs[:last_updated]
27
+ @open_weights = attrs[:open_weights]
28
+ end
29
+
30
+ # Create from API hash
31
+ # @param hash [Hash] Model hash from API
32
+ # @param provider_id [String] Provider ID
33
+ # @return [ModelInfo] Parsed model info
34
+ def self.from_hash(hash, provider_id:)
35
+ new(
36
+ id: hash["id"],
37
+ name: hash["name"],
38
+ provider_id: provider_id,
39
+ pricing: PricingInfo.from_hash(hash["cost"]),
40
+ context_limit: hash.dig("limit", "context"),
41
+ output_limit: hash.dig("limit", "output"),
42
+ modalities: parse_modalities(hash["modalities"]),
43
+ capabilities: parse_capabilities(hash),
44
+ status: hash["status"],
45
+ knowledge_date: hash["knowledge"],
46
+ release_date: hash["release_date"],
47
+ last_updated: hash["last_updated"],
48
+ open_weights: hash["open_weights"]
49
+ )
50
+ end
51
+
52
+ # Full model identifier
53
+ # @return [String] provider:model format
54
+ def full_id
55
+ "#{provider_id}:#{id}"
56
+ end
57
+
58
+ # Check if model is deprecated
59
+ # @return [Boolean]
60
+ def deprecated?
61
+ status == "deprecated"
62
+ end
63
+
64
+ # Check if model is in preview/alpha/beta
65
+ # @return [Boolean]
66
+ def preview?
67
+ %w[alpha beta preview].include?(status)
68
+ end
69
+
70
+ # Check if model supports a capability
71
+ # @param capability [Symbol, String] Capability name
72
+ # @return [Boolean]
73
+ def supports?(capability)
74
+ capabilities[capability.to_sym] == true
75
+ end
76
+
77
+ # Convert to hash
78
+ # @return [Hash]
79
+ def to_h
80
+ {
81
+ id: id,
82
+ name: name,
83
+ provider_id: provider_id,
84
+ full_id: full_id,
85
+ pricing: pricing.to_h,
86
+ context_limit: context_limit,
87
+ output_limit: output_limit,
88
+ modalities: modalities,
89
+ capabilities: capabilities,
90
+ status: status,
91
+ knowledge_date: knowledge_date,
92
+ release_date: release_date,
93
+ last_updated: last_updated,
94
+ open_weights: open_weights
95
+ }
96
+ end
97
+
98
+ private
99
+
100
+ def self.parse_modalities(hash)
101
+ return {input: [], output: []} if hash.nil?
102
+
103
+ unless hash.is_a?(Hash)
104
+ warn "[ModelInfo] Unexpected modalities type: #{hash.class}, expected Hash"
105
+ return {input: [], output: []}
106
+ end
107
+
108
+ {
109
+ input: Array(hash["input"]),
110
+ output: Array(hash["output"])
111
+ }
112
+ end
113
+ private_class_method :parse_modalities
114
+
115
+ def self.parse_capabilities(hash)
116
+ {
117
+ attachment: hash["attachment"] == true,
118
+ reasoning: hash["reasoning"] == true,
119
+ tool_call: hash["tool_call"] == true,
120
+ structured_output: hash["structured_output"] == true,
121
+ temperature: hash["temperature"] == true
122
+ }
123
+ end
124
+ private_class_method :parse_capabilities
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Models
7
+ # Represents pricing information for a model (per million tokens)
8
+ class PricingInfo
9
+ attr_reader :input, :output, :reasoning, :cache_read, :cache_write
10
+
11
+ # Initialize pricing info
12
+ # @param input [Float, nil] Input cost per million tokens
13
+ # @param output [Float, nil] Output cost per million tokens
14
+ # @param reasoning [Float, nil] Reasoning cost per million tokens
15
+ # @param cache_read [Float, nil] Cache read cost per million tokens
16
+ # @param cache_write [Float, nil] Cache write cost per million tokens
17
+ def initialize(input: nil, output: nil, reasoning: nil, cache_read: nil, cache_write: nil)
18
+ @input = input
19
+ @output = output
20
+ @reasoning = reasoning
21
+ @cache_read = cache_read
22
+ @cache_write = cache_write
23
+ end
24
+
25
+ # Create from API hash
26
+ # @param hash [Hash] Cost hash from API
27
+ # @return [PricingInfo] Parsed pricing info
28
+ def self.from_hash(hash)
29
+ return new if hash.nil?
30
+
31
+ new(
32
+ input: hash["input"],
33
+ output: hash["output"],
34
+ reasoning: hash["reasoning"],
35
+ cache_read: hash["cache_read"],
36
+ cache_write: hash["cache_write"]
37
+ )
38
+ end
39
+
40
+ # Convert to hash
41
+ # @return [Hash] Pricing as hash
42
+ def to_h
43
+ {
44
+ input: input,
45
+ output: output,
46
+ reasoning: reasoning,
47
+ cache_read: cache_read,
48
+ cache_write: cache_write
49
+ }.compact
50
+ end
51
+
52
+ # Check if pricing data is available
53
+ # @return [Boolean] true if any pricing data exists
54
+ def available?
55
+ input || output
56
+ end
57
+
58
+ # Calculate total cost for token counts
59
+ # @param input_tokens [Integer] Input token count
60
+ # @param output_tokens [Integer] Output token count
61
+ # @param reasoning_tokens [Integer] Reasoning token count
62
+ # @return [Float] Total cost in dollars
63
+ def calculate(input_tokens:, output_tokens:, reasoning_tokens: 0)
64
+ total = 0.0
65
+ total += (input_tokens / 1_000_000.0) * input if input
66
+ total += (output_tokens / 1_000_000.0) * output if output
67
+ total += (reasoning_tokens / 1_000_000.0) * reasoning if reasoning && reasoning_tokens > 0
68
+ total
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Models
7
+ # Represents information about a provider
8
+ class ProviderInfo
9
+ attr_reader :id, :name, :env_keys, :npm_package, :api_url, :doc_url, :models
10
+
11
+ # Initialize provider info
12
+ def initialize(attrs = {})
13
+ @id = attrs[:id]
14
+ @name = attrs[:name]
15
+ @env_keys = attrs[:env_keys] || []
16
+ @npm_package = attrs[:npm_package]
17
+ @api_url = attrs[:api_url]
18
+ @doc_url = attrs[:doc_url]
19
+ @models = attrs[:models] || {}
20
+ end
21
+
22
+ # Create from API hash
23
+ # @param hash [Hash] Provider hash from API
24
+ # @return [ProviderInfo] Parsed provider info
25
+ def self.from_hash(hash)
26
+ provider_id = hash["id"]
27
+ models_hash = hash["models"] || {}
28
+
29
+ models = models_hash.transform_values do |model_hash|
30
+ ModelInfo.from_hash(model_hash, provider_id: provider_id)
31
+ end
32
+
33
+ new(
34
+ id: provider_id,
35
+ name: hash["name"],
36
+ env_keys: Array(hash["env"]),
37
+ npm_package: hash["npm"],
38
+ api_url: hash["api"],
39
+ doc_url: hash["doc"],
40
+ models: models
41
+ )
42
+ end
43
+
44
+ # Get model by ID
45
+ # @param model_id [String] Model ID
46
+ # @return [ModelInfo, nil] Model info or nil
47
+ def model(model_id)
48
+ models[model_id]
49
+ end
50
+
51
+ # List all model IDs
52
+ # @return [Array<String>] Model IDs
53
+ def model_ids
54
+ models.keys
55
+ end
56
+
57
+ # Count models
58
+ # @return [Integer] Number of models
59
+ def model_count
60
+ models.size
61
+ end
62
+
63
+ # Convert to hash
64
+ # @return [Hash]
65
+ def to_h
66
+ {
67
+ id: id,
68
+ name: name,
69
+ env_keys: env_keys,
70
+ npm_package: npm_package,
71
+ api_url: api_url,
72
+ doc_url: doc_url,
73
+ model_count: model_count,
74
+ models: models.transform_values(&:to_h)
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/core"
4
+
5
+ require_relative "version"
6
+ require_relative "errors"
7
+
8
+ # Atoms - Pure functions
9
+ require_relative "atoms/cache_path_resolver"
10
+ require_relative "atoms/api_fetcher"
11
+ require_relative "atoms/json_parser"
12
+ require_relative "atoms/file_reader"
13
+ require_relative "atoms/file_writer"
14
+ require_relative "atoms/model_filter"
15
+ require_relative "atoms/provider_config_reader"
16
+ require_relative "atoms/provider_config_writer"
17
+ require_relative "atoms/model_name_canonicalizer"
18
+
19
+ # Models - Data structures
20
+ require_relative "models/provider_info"
21
+ require_relative "models/model_info"
22
+ require_relative "models/pricing_info"
23
+ require_relative "models/diff_result"
24
+
25
+ # Molecules - Composed operations
26
+ require_relative "molecules/cache_manager"
27
+ require_relative "molecules/model_validator"
28
+ require_relative "molecules/cost_calculator"
29
+ require_relative "molecules/diff_generator"
30
+ require_relative "molecules/model_searcher"
31
+ require_relative "molecules/provider_sync_diff"
32
+
33
+ # Organisms - Business logic
34
+ require_relative "organisms/sync_orchestrator"
35
+ require_relative "organisms/provider_sync_orchestrator"
36
+
37
+ module Ace
38
+ module Support
39
+ module Models
40
+ API_URL = "https://models.dev/api.json"
41
+
42
+ class << self
43
+ # Get the default cache directory
44
+ # @return [String] Path to cache directory
45
+ def cache_dir
46
+ Atoms::CachePathResolver.resolve
47
+ end
48
+
49
+ # Sync models from API
50
+ # @param force [Boolean] Force sync even if cache is fresh
51
+ # @return [Hash] Sync result with stats
52
+ def sync(force: false)
53
+ Organisms::SyncOrchestrator.new.sync(force: force)
54
+ end
55
+
56
+ # Validate a model exists
57
+ # @param model_id [String] Model ID in format provider:model
58
+ # @return [Boolean] true if valid
59
+ # @raise [ModelNotFoundError] if model doesn't exist
60
+ def validate(model_id)
61
+ Molecules::ModelValidator.new.validate(model_id)
62
+ end
63
+
64
+ # Validate a model exists (returns boolean, no exception)
65
+ # @param model_id [String] Model ID in format provider:model
66
+ # @return [Boolean] true if valid, false otherwise
67
+ def valid?(model_id)
68
+ validate(model_id)
69
+ true
70
+ rescue ModelNotFoundError, ProviderNotFoundError
71
+ false
72
+ end
73
+
74
+ # Calculate cost for a query
75
+ # @param model_id [String] Model ID
76
+ # @param input_tokens [Integer] Input token count
77
+ # @param output_tokens [Integer] Output token count
78
+ # @param reasoning_tokens [Integer] Reasoning token count (optional)
79
+ # @return [Hash] Cost breakdown
80
+ def cost(model_id, input_tokens:, output_tokens:, reasoning_tokens: 0)
81
+ Molecules::CostCalculator.new.calculate(
82
+ model_id,
83
+ input_tokens: input_tokens,
84
+ output_tokens: output_tokens,
85
+ reasoning_tokens: reasoning_tokens
86
+ )
87
+ end
88
+
89
+ # Get diff between cached versions
90
+ # @return [Models::DiffResult] Changes since last sync
91
+ def diff
92
+ Molecules::DiffGenerator.new.generate
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end