ask-llm-providers 0.13.0 → 0.13.2
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/ask/llm/catalog.rb +167 -16
- data/lib/ask/llm/models/commandcode.json +1443 -46
- data/lib/ask/llm/models.json +14435 -0
- data/lib/ask/llm/version.rb +1 -1
- data/lib/ask-llm-providers.rb +55 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ce937df3ae9db671946c8d5c3d837498fd521066bb45996e595dac977ff9f24
|
|
4
|
+
data.tar.gz: 9702d0c8aea1b61aa24df7e8bb76c5cc19e576ce926076fb274bdb095691b666
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ea28d9f251de830592fb92f7d78c71d9b7983dbfc69aa29617ef06df5d598edfa5d9e5a9c8f386a2f9b2d9be22cb76a9c54594c7710acfea17779aa7de9c16d
|
|
7
|
+
data.tar.gz: d67134217af9ddb29c91b8dd4c64cb1bbe68f2fd79da31bf0bac7a7b64abb681ca0f34e11e7327d587de13c531b722466075cb5e10f35c302acf174af4d3e17b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.13.1] — 2026-09-05
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Command Code catalog now matches the live API.** The `/models` listing serves namespaced IDs (`xiaomi/mimo-v2.5`, `Qwen/Qwen3.8-27B`, `deepseek/deepseek-v4-flash`, …) rather than the short names from the plan's config snapshot; the earlier entry's IDs were rejected with "Model not supported on this endpoint". Catalog regenerated from the provider's `/models` response (67 models, capabilities kept conservative).
|
|
6
|
+
|
|
1
7
|
## [0.13.0] — 2026-09-05
|
|
2
8
|
|
|
3
9
|
### Added
|
data/lib/ask/llm/catalog.rb
CHANGED
|
@@ -3,54 +3,169 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "date"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "set"
|
|
6
8
|
|
|
7
9
|
module Ask
|
|
8
10
|
module LLM
|
|
9
11
|
# Orchestrates model catalog loading from multiple sources:
|
|
10
12
|
#
|
|
11
|
-
# 1.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# 1. Single bundled JSON file lib/ask/llm/models.json (shipped with the gem)
|
|
14
|
+
# — legacy shard dir lib/ask/llm/models/*.json is still supported
|
|
15
|
+
# 2. OS-specific user cache (~/Library/Caches/ask-llm-providers/models.json
|
|
16
|
+
# on macOS, $XDG_CACHE_HOME/ask-llm-providers/models.json on Linux)
|
|
17
|
+
# populated by Catalog.refresh!
|
|
18
|
+
# 3. Explicit user overrides in ~/.ask-llm-providers/models.json
|
|
19
|
+
# 4. Provider API list_models() calls (on explicit refresh!)
|
|
14
20
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
21
|
+
# Mirrors ruby_llm's store → file → bundle fallback and pi's
|
|
22
|
+
# manifest-validated data dir.
|
|
17
23
|
#
|
|
18
|
-
# Ask::LLM::Catalog.load!
|
|
19
|
-
# Ask::LLM::Catalog.
|
|
24
|
+
# Ask::LLM::Catalog.load! # bundled + cache + user config
|
|
25
|
+
# Ask::LLM::Catalog.ensure_loaded! # no-op if already loaded (lazy)
|
|
26
|
+
# Ask::LLM::Catalog.refresh! # also fetch from provider APIs
|
|
20
27
|
#
|
|
21
28
|
class Catalog
|
|
22
29
|
class Error < StandardError; end
|
|
23
30
|
class LoadError < Error; end
|
|
24
31
|
|
|
25
32
|
USER_CONFIG_PATH = File.expand_path("~/.ask-llm-providers/models.json").freeze
|
|
33
|
+
BUNDLED_FILE = File.expand_path("models.json", __dir__).freeze
|
|
34
|
+
LEGACY_SHARD_PATTERN = File.expand_path("models/*.json", __dir__).freeze
|
|
35
|
+
SCHEMA_VERSION = 1
|
|
26
36
|
|
|
27
37
|
class << self
|
|
28
|
-
# Load bundled
|
|
38
|
+
# Load bundled + cached + user overrides into Ask::ModelCatalog.
|
|
29
39
|
# Idempotent — subsequent calls clear and reload.
|
|
30
40
|
def load!
|
|
31
41
|
instance.clear
|
|
32
42
|
instance.load_bundled
|
|
43
|
+
instance.load_cached
|
|
33
44
|
instance.load_user_config
|
|
34
45
|
instance.register_all
|
|
46
|
+
@loaded = true
|
|
35
47
|
true
|
|
36
48
|
end
|
|
37
49
|
|
|
50
|
+
# Ensure the catalog has been loaded at least once. Cheap no-op
|
|
51
|
+
# after the first load; lets callers be lazy like ruby_llm.
|
|
52
|
+
def ensure_loaded!
|
|
53
|
+
return true if @loaded
|
|
54
|
+
load!
|
|
55
|
+
end
|
|
38
56
|
|
|
57
|
+
def loaded?
|
|
58
|
+
!!@loaded
|
|
59
|
+
end
|
|
39
60
|
|
|
40
61
|
# Like load! but also fetches model lists from configured providers'
|
|
41
62
|
# list_models() APIs. Unknown models are added with minimal metadata.
|
|
63
|
+
# Persists the merged result to the OS cache.
|
|
42
64
|
def refresh!
|
|
43
65
|
load!
|
|
44
66
|
instance.fetch_from_providers
|
|
45
67
|
instance.register_all
|
|
68
|
+
instance.persist_cached
|
|
46
69
|
true
|
|
47
70
|
end
|
|
48
71
|
|
|
49
|
-
|
|
72
|
+
# OS-aware cache path, like ruby_llm's ModelRegistry.cache_path.
|
|
73
|
+
def cache_path
|
|
74
|
+
return @cache_path if defined?(@cache_path) && @cache_path
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
env_path = ENV["ASK_LLM_PROVIDERS_CACHE"]
|
|
77
|
+
if env_path && !env_path.strip.empty?
|
|
78
|
+
return @cache_path = env_path
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
home = Dir.home
|
|
82
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
83
|
+
|
|
84
|
+
dir = if host_os.match?(/darwin/i)
|
|
85
|
+
File.join(home, "Library", "Caches", "ask-llm-providers")
|
|
86
|
+
elsif host_os.match?(/mswin|mingw|cygwin/i)
|
|
87
|
+
local = ENV.fetch("LOCALAPPDATA", nil)
|
|
88
|
+
local = File.join(home, "AppData", "Local") if local.to_s.empty?
|
|
89
|
+
File.join(local, "ask-llm-providers", "Cache")
|
|
90
|
+
else
|
|
91
|
+
xdg = ENV.fetch("XDG_CACHE_HOME", nil)
|
|
92
|
+
xdg = File.join(home, ".cache") if xdg.to_s.empty?
|
|
93
|
+
File.join(xdg, "ask-llm-providers")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@cache_path = File.join(dir, "models.json")
|
|
97
|
+
rescue ArgumentError
|
|
98
|
+
@cache_path = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def cache_etag_path
|
|
102
|
+
cp = cache_path
|
|
103
|
+
cp ? "#{cp}.etag" : nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def manifest_path
|
|
107
|
+
cp = cache_path
|
|
108
|
+
cp ? File.join(File.dirname(cp), ".manifest.json") : nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def read_cached_etag
|
|
112
|
+
path = cache_etag_path
|
|
113
|
+
return nil unless path && File.file?(path)
|
|
114
|
+
v = File.read(path).strip
|
|
115
|
+
v.empty? ? nil : v
|
|
116
|
+
rescue SystemCallError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def write_cached_etag(etag)
|
|
123
|
+
path = cache_etag_path
|
|
124
|
+
return unless path
|
|
125
|
+
if etag && !etag.to_s.strip.empty?
|
|
126
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
127
|
+
atomic_write(path, "#{etag.strip}\n")
|
|
128
|
+
else
|
|
129
|
+
FileUtils.rm_f(path)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def write_manifest(data)
|
|
134
|
+
path = manifest_path
|
|
135
|
+
return unless path
|
|
136
|
+
require "digest"
|
|
137
|
+
sorted = data.sort_by { |e| [e["provider"].to_s, e["id"].to_s] }
|
|
138
|
+
hash = Digest::SHA256.hexdigest(JSON.generate(sorted))
|
|
139
|
+
manifest = {
|
|
140
|
+
"schemaVersion" => SCHEMA_VERSION,
|
|
141
|
+
"generatedAt" => Time.now.utc.iso8601,
|
|
142
|
+
"count" => data.size,
|
|
143
|
+
"sha256" => hash
|
|
144
|
+
}
|
|
145
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
146
|
+
atomic_write(path, JSON.pretty_generate(manifest) + "\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def atomic_write(destination, contents)
|
|
150
|
+
dir = File.dirname(destination)
|
|
151
|
+
FileUtils.mkdir_p(dir)
|
|
152
|
+
basename = File.basename(destination)
|
|
153
|
+
require "tempfile"
|
|
154
|
+
Tempfile.create([basename, ".tmp"], dir) do |tmp|
|
|
155
|
+
tmp.binmode
|
|
156
|
+
tmp.write(contents)
|
|
157
|
+
tmp.flush
|
|
158
|
+
tmp.fsync
|
|
159
|
+
mode = File.exist?(destination) ? (File.stat(destination).mode & 0o7777) : (0o666 & ~File.umask)
|
|
160
|
+
tmp.chmod(mode)
|
|
161
|
+
begin
|
|
162
|
+
File.rename(tmp.path, destination)
|
|
163
|
+
rescue Errno::EACCES, Errno::EEXIST
|
|
164
|
+
FileUtils.rm_f(destination)
|
|
165
|
+
File.rename(tmp.path, destination)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
54
169
|
|
|
55
170
|
def instance
|
|
56
171
|
@instance ||= new
|
|
@@ -67,15 +182,51 @@ module Ask
|
|
|
67
182
|
@model_keys.clear
|
|
68
183
|
end
|
|
69
184
|
|
|
70
|
-
# Load bundled model
|
|
185
|
+
# Load bundled model data. Prefers the single file; falls back to
|
|
186
|
+
# the legacy per-provider shard dir for local dev and for
|
|
187
|
+
# backwards-compatibility if the bundle is absent.
|
|
71
188
|
def load_bundled
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
raw = JSON.parse(File.read(path))
|
|
189
|
+
if File.exist?(self.class::BUNDLED_FILE)
|
|
190
|
+
raw = JSON.parse(File.read(self.class::BUNDLED_FILE))
|
|
75
191
|
raw.each { |entry| add_entry(entry) }
|
|
192
|
+
else
|
|
193
|
+
Dir[LEGACY_SHARD_PATTERN].sort.each do |path|
|
|
194
|
+
raw = JSON.parse(File.read(path))
|
|
195
|
+
raw.each { |entry| add_entry(entry) }
|
|
196
|
+
end
|
|
76
197
|
end
|
|
77
198
|
end
|
|
78
199
|
|
|
200
|
+
# Load the writable OS cache (populated by refresh!). Valid array
|
|
201
|
+
# entries are merged over the bundle; invalid cache is ignored like
|
|
202
|
+
# ruby_llm's models_from_file.
|
|
203
|
+
def load_cached
|
|
204
|
+
path = self.class.cache_path
|
|
205
|
+
return unless path && File.file?(path)
|
|
206
|
+
raw = JSON.parse(File.read(path))
|
|
207
|
+
return unless raw.is_a?(Array)
|
|
208
|
+
raw.each { |entry| merge_or_add(entry) }
|
|
209
|
+
rescue JSON::ParserError => e
|
|
210
|
+
warn "Warning: Failed to parse cache #{path}: #{e.message}"
|
|
211
|
+
rescue SystemCallError => e
|
|
212
|
+
warn "Warning: Failed to read cache #{path}: #{e.message}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Persist the current entries to the OS cache (used after refresh!).
|
|
216
|
+
def persist_cached
|
|
217
|
+
path = self.class.cache_path
|
|
218
|
+
return unless path
|
|
219
|
+
return if @entries.empty?
|
|
220
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
221
|
+
data = @entries.sort_by { |e| [(e["provider"] || e[:provider]).to_s, (e["id"] || e[:id]).to_s] }
|
|
222
|
+
# Normalize to string-keyed hashes for stable JSON
|
|
223
|
+
normalized = data.map { |e| e.transform_keys(&:to_s) }
|
|
224
|
+
self.class.send(:atomic_write, path, JSON.pretty_generate(normalized) + "\n")
|
|
225
|
+
self.class.send(:write_manifest, normalized)
|
|
226
|
+
rescue SystemCallError => e
|
|
227
|
+
warn "Warning: Failed to persist cache #{path}: #{e.message}"
|
|
228
|
+
end
|
|
229
|
+
|
|
79
230
|
# Load user-defined model overrides from ~/.ask-llm-providers/models.json.
|
|
80
231
|
# Silently skipped if the file doesn't exist.
|
|
81
232
|
def load_user_config
|