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,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
|