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