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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 451533dc289b010f698dcd3ce6ab3e0a459a769f8a54a30431623fb78cfa3550
4
+ data.tar.gz: 37e77876733d1a94e9dfbf6278e848d1408492a8c8e29f18b1cd8824c562da22
5
+ SHA512:
6
+ metadata.gz: 387e58aef48d5dc364aca965839d2cb883583ed9551648bc02514b565ac12a81779904634e52d9e17dc0c4b47d0a7d9b71a51548e21d6a13a47a86b189a89b9a
7
+ data.tar.gz: e06be949d2190b9b46a76b4e027c03808ece2356b9ac39cf534fbb0e8da1f46b4e9780f42920b34d38634a5c820e9b25ff297ed2777c8b050339ef26ce45d33a
data/CHANGELOG.md ADDED
@@ -0,0 +1,162 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.9.0] - 2026-03-23
11
+
12
+ ### Changed
13
+ - Added `ace-models` and `ace-llm-providers` CLI references to README intro and new Quick Start section with usage examples.
14
+
15
+ ### Technical
16
+ - Removed phantom `handbook/**/*` glob from gemspec (no handbook directory exists).
17
+
18
+ ## [0.8.2] - 2026-03-23
19
+
20
+ ### Fixed
21
+ - Normalize provider and model data shapes in cache manager — handles wrapped (`{"providers" => ...}`), array-of-hashes, and flat hash formats so cache reads don't break when the upstream response shape varies.
22
+
23
+ ## [0.8.1] - 2026-03-22
24
+
25
+ ### Changed
26
+ - Refreshed README structure with a dedicated purpose section, dual installation paths, a basic usage entry point, and the ACE project footer link.
27
+
28
+ ## [0.8.0] - 2026-03-21
29
+
30
+ ### Changed
31
+ - Added initial `TS-MODELS-001` value-gated smoke E2E coverage for `ace-models` and `ace-llm-providers`, including ADD/SKIP decision evidence.
32
+
33
+ ## [0.7.1] - 2026-03-18
34
+
35
+ ### Changed
36
+ - Migrated CLI namespace from `Ace::Core::CLI::*` to `Ace::Support::Cli::*` (ace-support-cli is now the canonical home for CLI infrastructure).
37
+
38
+
39
+ ## [0.7.0] - 2026-03-18
40
+
41
+ ### Changed
42
+ - Removed legacy backward-compatibility behavior as part of the 0.10 cleanup release.
43
+
44
+
45
+ ## [0.6.3] - 2026-03-15
46
+
47
+ ### Changed
48
+ - Migrated CLI framework from dry-cli to ace-support-cli
49
+
50
+ ## [0.6.2] - 2026-02-23
51
+
52
+ ### Technical
53
+ - Updated internal dependency version constraints to current releases
54
+
55
+ ## [0.6.1] - 2026-02-22
56
+
57
+ ### Changed
58
+ - Migrate ace-llm-providers CLI to standard help pattern
59
+ - Remove DefaultRouting extension and DWIM default behavior
60
+ - Add HelpCommand registration for `--help` and `-h`
61
+ - Update REGISTERED_COMMANDS to [name, description] format
62
+ - Add explicit start(args) method for CLI invocation
63
+
64
+ ## [0.6.0] - 2026-02-22
65
+
66
+ ### Added
67
+ - Migrate CLI to standard help pattern with HelpCommand
68
+ - Register `--help` and `-h` for formatted help output
69
+ - No args now shows help instead of command list
70
+
71
+ ### Changed
72
+ - Remove DefaultRouting extension and DWIM default behavior
73
+ - Remove KNOWN_COMMANDS, DEFAULT_COMMAND, BUILTIN_COMMANDS constants
74
+ - Update REGISTERED_COMMANDS to [name, description] format
75
+ - Convert HELP_EXAMPLES to simple string array
76
+ - Add explicit start(args) method for CLI invocation
77
+ - Centralize CLI help routing and formatting (shared with ace-support-core)
78
+ - Centralize CLI error handling and exit code management (shared)
79
+
80
+ ### Technical
81
+ - Update CLI command usage in README
82
+ - Lower Ruby version requirement to >= 3.2.0
83
+
84
+ ## [0.5.2] - 2026-02-02
85
+
86
+ ### Technical
87
+ - Update support-packages metadata
88
+
89
+ ## [0.5.1] - 2026-01-15
90
+
91
+ ### Changed
92
+ - Migrate CLI commands to Hanami pattern
93
+ - Move commands from `commands/` to `cli/commands/`
94
+ - Update namespace from `Commands::*` to `CLI::Commands::*`
95
+ - Models subcommands use `ModelsSubcommands::` to avoid namespace conflict
96
+ - Update test file references for new namespace
97
+
98
+ ## [0.5.0] - 2026-01-13
99
+
100
+ ### Changed
101
+ - **BREAKING: Package renamed** from `ace-llm-models-dev` to `ace-support-models`
102
+ - Follows `ace-support-*` naming pattern for infrastructure gems
103
+ - CLI executable renamed: `ace-llm-models` → `ace-models`
104
+ - Ruby module renamed: `Ace::LLM::ModelsDev` → `Ace::Support::Models`
105
+ - Require path changed: `require 'ace/llm/models/dev'` → `require 'ace/support/models'`
106
+ - Cache directory changed: `~/.cache/ace-llm-models-dev` → `~/.cache/ace-models`
107
+ - All functionality remains identical
108
+ - **CLI migrated to dry-cli** (task 179.16)
109
+ - Replaced Thor-based CLI with dry-cli registry pattern
110
+ - Thor dependency replaced with dry-cli ~> 1.1 in gemspec
111
+ - Removed old Thor subcommand files (cache_cli.rb, providers_cli.rb, models_cli.rb)
112
+ - New command classes in `cli/{cache,providers,models}/` directories
113
+ - Commands now use keyword arguments for options
114
+ - Subcommands registered hierarchically: `cache sync`, `providers list`, etc.
115
+ - **Internal refactor**: Renamed `Cli::` module to `Commands::` for consistency with other ACE gems
116
+ - Directory renamed: `lib/ace/support/models/cli/` → `lib/ace/support/models/commands/`
117
+ - All command classes now use `Commands::` namespace
118
+
119
+ ## [0.4.1] - 2026-01-05
120
+
121
+ ### Added
122
+ - Thor CLI migration with ConfigSummary display
123
+
124
+ ### Changed
125
+ - Adopted Ace::Core::CLI::Base for standardized options
126
+
127
+
128
+ ## [0.4.0] - 2026-01-03
129
+
130
+ ### Changed
131
+ - **BREAKING**: Minimum Ruby version raised to 3.3.0 (was 3.1.0)
132
+ - Standardized gemspec file patterns with deterministic Dir.glob
133
+ - Added MIT LICENSE file
134
+
135
+ ## [0.3.3] - 2025-12-30
136
+
137
+ ### Changed
138
+
139
+ - Update provider config path references from `.ace.example` to `.ace-defaults`
140
+
141
+ ## [0.3.2] - 2025-12-08
142
+
143
+ ### Fixed
144
+
145
+ - OpenRouter model sync false positives for suffixed models (`:nitro`, `:floor`, `:online`, etc.)
146
+ - ModelNameCanonicalizer now strips known routing suffixes before comparing against models.dev
147
+
148
+ ### Added
149
+
150
+ - ModelNameCanonicalizer atom for OpenRouter model name canonicalization
151
+
152
+ ---
153
+
154
+ *For history prior to 0.3.2 (versions 0.1.0-0.3.1), see git history under the original gem name `ace-llm-models-dev`.*
155
+
156
+
157
+ ## [0.5.2] - 2026-02-22
158
+
159
+ ### Fixed
160
+ - Added command grouping (Cache, Providers, Models, Shortcuts)
161
+ - Fixed namespace subcommand help (cache/providers/models --help) to exit 0 and print to stdout
162
+ - Standardized quiet, verbose, debug option descriptions to canonical strings
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michal Czyz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ <div align="center">
2
+ <h1> ACE - Support Models </h1>
3
+
4
+ Shared model metadata and pricing helpers for ACE provider tooling.
5
+
6
+ <img src="https://raw.githubusercontent.com/cs3b/ace/main/docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
7
+ <br><br>
8
+
9
+ <a href="https://rubygems.org/gems/ace-support-models"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-support-models.svg" /></a>
10
+ <a href="https://www.ruby-lang.org"><img alt="Ruby" src="https://img.shields.io/badge/Ruby-3.2+-CC342D?logo=ruby" /></a>
11
+ <a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg" /></a>
12
+
13
+ </div>
14
+
15
+ > Works with: Claude Code, Codex CLI, OpenCode, Gemini CLI, pi-agent, and more.
16
+
17
+ `ace-support-models` normalizes provider and model metadata into a single canonical source so ACE tools that reason about LLM capabilities, pricing, and compatibility do not duplicate catalogs or drift out of sync. Includes `ace-models` for search, pricing, and cache sync, and `ace-llm-providers` for provider listing.
18
+
19
+ ## Use Cases
20
+
21
+ **Resolve model metadata consistently** - avoid duplicated model catalogs across ACE features by querying one shared registry used by [ace-llm](../ace-llm) and [ace-review](../ace-review).
22
+
23
+ **Calculate usage expectations** - support stable cost and compatibility assumptions during tool workflows with shared pricing and capability primitives.
24
+
25
+ **Share validation rules** - apply one metadata model for provider and model checks so that [ace-llm-providers-cli](../ace-llm-providers-cli) and other provider-aware packages stay aligned.
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ ace-models search claude # Find models by name
31
+ ace-models info claude-sonnet-4-6 # Pricing and context window
32
+ ace-models cost claude-opus-4-6 # Token cost calculator
33
+ ace-models sync # Update cache from models.dev
34
+ ace-llm-providers list # List known providers
35
+ ```
36
+
37
+ ---
38
+
39
+ Part of [ACE](https://github.com/cs3b/ace)
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task spec: :test
13
+ task default: :test
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "ace/support/models"
8
+ require "ace/support/models/cli/providers_cli"
9
+
10
+ # No args → show help
11
+ args = ARGV.empty? ? ["--help"] : ARGV
12
+
13
+ # Start ace-support-cli with exception-based exit code handling (per ADR-023)
14
+ begin
15
+ Ace::Support::Models::ProvidersCLI.start(args)
16
+ rescue Ace::Support::Cli::Error => e
17
+ warn e.message
18
+ exit(e.exit_code)
19
+ end
data/exe/ace-models ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/ace/support/models"
5
+ require_relative "../lib/ace/support/models/cli"
6
+
7
+ # Migration warning: Help users clean up old cache directory
8
+ old_cache_path = File.join(Dir.home, ".cache", "ace-llm-models-dev")
9
+ if Dir.exist?(old_cache_path)
10
+ warn "Note: Old cache detected at ~/.cache/ace-llm-models-dev"
11
+ warn "Run: ace-models sync && rm -rf ~/.cache/ace-llm-models-dev"
12
+ end
13
+
14
+ # No args → show help
15
+ args = ARGV.empty? ? ["--help"] : ARGV
16
+
17
+ # Start ace-support-cli with exception-based exit code handling (per ADR-023)
18
+ begin
19
+ Ace::Support::Models::CLI.start(args)
20
+ rescue Ace::Support::Cli::Error => e
21
+ warn e.message
22
+ exit(e.exit_code)
23
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+
6
+ module Ace
7
+ module Support
8
+ module Models
9
+ module Atoms
10
+ # Fetches data from the models.dev API using Faraday (ADR-010 compliant)
11
+ class ApiFetcher
12
+ API_URL = "https://models.dev/api.json"
13
+ TIMEOUT = 30
14
+ OPEN_TIMEOUT = 10
15
+ MAX_RETRIES = 2
16
+ RETRY_INTERVAL = 0.5
17
+
18
+ class << self
19
+ # Fetch the API JSON
20
+ # @param url [String] API URL (default: models.dev)
21
+ # @return [String] Raw JSON response
22
+ # @raise [NetworkError] on network failures
23
+ # @raise [ApiError] on non-200 responses
24
+ def fetch(url = API_URL)
25
+ response = connection.get(url)
26
+
27
+ unless response.success?
28
+ raise ApiError.new(
29
+ "API request failed: #{response.status} #{response.reason_phrase}",
30
+ status_code: response.status
31
+ )
32
+ end
33
+
34
+ response.body
35
+ rescue Faraday::TimeoutError => e
36
+ raise NetworkError, "Request timed out: #{e.message}"
37
+ rescue Faraday::ConnectionFailed => e
38
+ raise NetworkError, "Connection failed: #{e.message}"
39
+ rescue Faraday::SSLError => e
40
+ raise NetworkError, "SSL error: #{e.message}"
41
+ rescue Faraday::Error => e
42
+ raise NetworkError, "Network error fetching API: #{e.message}"
43
+ end
44
+
45
+ private
46
+
47
+ # Build Faraday connection with retry middleware
48
+ # @return [Faraday::Connection]
49
+ def connection
50
+ @connection ||= Faraday.new do |faraday|
51
+ faraday.options.timeout = TIMEOUT
52
+ faraday.options.open_timeout = OPEN_TIMEOUT
53
+
54
+ # Retry middleware for transient failures (ADR-010)
55
+ faraday.request :retry, {
56
+ max: MAX_RETRIES,
57
+ interval: RETRY_INTERVAL,
58
+ interval_randomness: 0.5,
59
+ backoff_factor: 2,
60
+ retry_statuses: [429, 500, 502, 503, 504],
61
+ methods: [:get]
62
+ }
63
+
64
+ # Set headers
65
+ faraday.headers["User-Agent"] = "ace-models/#{VERSION}"
66
+ faraday.headers["Accept"] = "application/json"
67
+
68
+ faraday.adapter Faraday.default_adapter
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Atoms
7
+ # Resolves the cache directory path following XDG conventions
8
+ class CachePathResolver
9
+ DEFAULT_CACHE_DIR = "ace-models"
10
+
11
+ class << self
12
+ # Resolve the cache directory path
13
+ # @return [String] Full path to cache directory
14
+ def resolve
15
+ base_dir = xdg_cache_home || default_cache_home
16
+ File.join(base_dir, DEFAULT_CACHE_DIR)
17
+ end
18
+
19
+ private
20
+
21
+ # Get XDG_CACHE_HOME if set
22
+ # @return [String, nil] XDG cache home or nil
23
+ def xdg_cache_home
24
+ xdg = ENV["XDG_CACHE_HOME"]
25
+ xdg if xdg && !xdg.empty?
26
+ end
27
+
28
+ # Get default cache home (~/.cache)
29
+ # @return [String] Default cache directory
30
+ def default_cache_home
31
+ File.join(Dir.home, ".cache")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Atoms
7
+ # Reads files from the cache
8
+ class FileReader
9
+ class << self
10
+ # Read file contents
11
+ # @param path [String] File path
12
+ # @return [String, nil] File contents or nil if not found
13
+ def read(path)
14
+ return nil unless File.exist?(path)
15
+
16
+ File.read(path)
17
+ rescue Errno::EACCES => e
18
+ raise CacheError, "Permission denied reading #{path}: #{e.message}"
19
+ rescue Errno::ENOENT
20
+ nil
21
+ end
22
+
23
+ # Check if file exists
24
+ # @param path [String] File path
25
+ # @return [Boolean] true if file exists
26
+ def exist?(path)
27
+ File.exist?(path)
28
+ end
29
+
30
+ # Get file modification time
31
+ # @param path [String] File path
32
+ # @return [Time, nil] Modification time or nil
33
+ def mtime(path)
34
+ return nil unless File.exist?(path)
35
+
36
+ File.mtime(path)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module Atoms
9
+ # Writes files to the cache
10
+ class FileWriter
11
+ class << self
12
+ # Write content to file
13
+ # @param path [String] File path
14
+ # @param content [String] Content to write
15
+ # @return [Boolean] true on success
16
+ # @raise [CacheError] on write errors
17
+ def write(path, content)
18
+ ensure_directory(File.dirname(path))
19
+ File.write(path, content)
20
+ true
21
+ rescue Errno::EACCES => e
22
+ raise CacheError, "Permission denied writing #{path}: #{e.message}"
23
+ rescue Errno::ENOSPC => e
24
+ raise CacheError, "No space left writing #{path}: #{e.message}"
25
+ end
26
+
27
+ # Ensure directory exists
28
+ # @param dir [String] Directory path
29
+ # @return [Boolean] true if created or exists
30
+ def ensure_directory(dir)
31
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
32
+ true
33
+ rescue Errno::EACCES => e
34
+ raise CacheError, "Permission denied creating directory #{dir}: #{e.message}"
35
+ end
36
+
37
+ # Delete file
38
+ # @param path [String] File path
39
+ # @return [Boolean] true on success
40
+ def delete(path)
41
+ File.delete(path) if File.exist?(path)
42
+ true
43
+ rescue Errno::EACCES => e
44
+ raise CacheError, "Permission denied deleting #{path}: #{e.message}"
45
+ end
46
+
47
+ # Rename/move file
48
+ # @param from [String] Source path
49
+ # @param to [String] Destination path
50
+ # @return [Boolean] true on success
51
+ def rename(from, to)
52
+ ensure_directory(File.dirname(to))
53
+ File.rename(from, to)
54
+ true
55
+ rescue Errno::EACCES => e
56
+ raise CacheError, "Permission denied moving file: #{e.message}"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Models
8
+ module Atoms
9
+ # Parses JSON responses from the API
10
+ class JsonParser
11
+ class << self
12
+ # Parse JSON string
13
+ # @param json_string [String] JSON to parse
14
+ # @return [Hash] Parsed data
15
+ # @raise [ApiError] on parse errors
16
+ def parse(json_string)
17
+ JSON.parse(json_string)
18
+ rescue JSON::ParserError => e
19
+ raise ApiError, "Failed to parse JSON: #{e.message}"
20
+ end
21
+
22
+ # Convert hash to JSON string
23
+ # @param data [Hash] Data to convert
24
+ # @param pretty [Boolean] Pretty print output
25
+ # @return [String] JSON string
26
+ def to_json(data, pretty: false)
27
+ if pretty
28
+ JSON.pretty_generate(data)
29
+ else
30
+ JSON.generate(data)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Models
6
+ module Atoms
7
+ # Filters models based on various criteria
8
+ class ModelFilter
9
+ # Supported filter predicates
10
+ FILTERS = {
11
+ # Provider filter
12
+ provider: ->(model, value) { model.provider_id == value },
13
+
14
+ # Capability filters (boolean)
15
+ reasoning: ->(model, value) { model.capabilities[:reasoning] == parse_boolean(value) },
16
+ tool_call: ->(model, value) { model.capabilities[:tool_call] == parse_boolean(value) },
17
+ attachment: ->(model, value) { model.capabilities[:attachment] == parse_boolean(value) },
18
+ structured_output: ->(model, value) { model.capabilities[:structured_output] == parse_boolean(value) },
19
+ temperature: ->(model, value) { model.capabilities[:temperature] == parse_boolean(value) },
20
+
21
+ # Open weights filter
22
+ open_weights: ->(model, value) { model.open_weights == parse_boolean(value) },
23
+
24
+ # Modality filter (checks input modalities)
25
+ modality: ->(model, value) { model.modalities[:input]&.include?(value) },
26
+
27
+ # Numeric filters
28
+ min_context: ->(model, value) { (model.context_limit || 0) >= value.to_i },
29
+ max_input_cost: lambda { |model, value|
30
+ pricing_input = model.pricing&.input
31
+ return false unless pricing_input
32
+
33
+ pricing_input <= value.to_f
34
+ }
35
+ }.freeze
36
+
37
+ class << self
38
+ # Apply filters to a list of models
39
+ # @param models [Array<Models::ModelInfo>] Models to filter
40
+ # @param filters [Hash] Filter key-value pairs
41
+ # @return [Array<Models::ModelInfo>] Filtered models
42
+ def apply(models, filters)
43
+ return models if filters.nil? || filters.empty?
44
+
45
+ models.select do |model|
46
+ filters.all? do |key, value|
47
+ filter = FILTERS[key.to_sym]
48
+ # Unknown filters are ignored (forward compatibility)
49
+ filter.nil? || filter.call(model, value)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Parse a filter string into key-value pair
55
+ # @param filter_string [String] Filter in "key:value" format
56
+ # @return [Array<Symbol, String>, nil] [key, value] or nil if invalid
57
+ def parse(filter_string)
58
+ return nil unless filter_string.is_a?(String)
59
+
60
+ parts = filter_string.split(":", 2)
61
+ return nil if parts.size != 2 || parts.any?(&:empty?)
62
+
63
+ [parts[0].to_sym, parts[1]]
64
+ end
65
+
66
+ # Parse multiple filter strings into a hash
67
+ # @param filter_strings [Array<String>] Array of "key:value" strings
68
+ # @return [Hash] Parsed filters
69
+ def parse_all(filter_strings)
70
+ return {} if filter_strings.nil? || filter_strings.empty?
71
+
72
+ filter_strings.each_with_object({}) do |filter_string, hash|
73
+ parsed = parse(filter_string)
74
+ hash[parsed[0]] = parsed[1] if parsed
75
+ end
76
+ end
77
+
78
+ # Validate filter strings and return errors for invalid ones
79
+ # @param filter_strings [Array<String>] Array of "key:value" strings
80
+ # @return [Array<String>] Array of error messages (empty if all valid)
81
+ def validate(filter_strings)
82
+ return [] if filter_strings.nil? || filter_strings.empty?
83
+
84
+ errors = []
85
+ filter_strings.each do |filter_string|
86
+ parsed = parse(filter_string)
87
+ errors << "Invalid filter format '#{filter_string}'. Use key:value" if parsed.nil?
88
+ end
89
+ errors
90
+ end
91
+
92
+ private
93
+
94
+ # Parse boolean string value
95
+ # @param value [String, Boolean] Value to parse
96
+ # @return [Boolean]
97
+ def parse_boolean(value)
98
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
99
+
100
+ %w[true 1 yes].include?(value.to_s.downcase)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end