lex-llm 0.4.9 → 0.4.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2b7839d1fcb47e176aa62970f3ec40d04b9e25095418285d3cf6a5d492ccb8d
4
- data.tar.gz: f444cd5054325a007f05749d6d4a2ca0933bf1f5b7004b2585daaa97745eb337
3
+ metadata.gz: a203372d751b290a71cc289e382d80a49fafcc0687925c02594b8c5cfe6ef7aa
4
+ data.tar.gz: 95cfd5a03c002a16da80bac58914f1fb808a940db378d99035f97b6256240863
5
5
  SHA512:
6
- metadata.gz: 7412bc0234b379941ae045fa826f267d97b1577dba15ee7268957d62c999808e27392d9acf65d0bf464c31aa7365e3912642a4b0041eb92294f108c4203d8f38
7
- data.tar.gz: a90e99a7c61f6fda2ffc4d2e4fd6b2fcba6f69bf75d5da526cacef550c345aadd157ea1ee6efeae59e3f69160a8e6ad92237a31997e9ef259c69dc29a6db01e6
6
+ metadata.gz: 645bde1f8e4b6701fa5092f2b92e2867f63d243c239341e691921efa9cc74a861b3382f00665efd8f4d1420976c462f9c47eaa89bdae93ae60655983681bcddc
7
+ data.tar.gz: f180a90275c427970e6129ae3f0ef285fabb68fa92a97059687fb37fcf9282f5e083b159f3757293a79ae4bf71f263a54fb9387469f55da25a23959e295d2371
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.10 - 2026-05-13
4
+
5
+ - Add cache-backed `model_detail` lookup with 24-hour TTL; nil results are not cached; `fetch_model_detail` hook for subclasses to override with live API calls.
6
+ - Build `model_detail_cache_key` from tier, slug, instance, and credential fingerprint so remote providers never share model detail entries across credentials.
7
+ - Add `credential_cache_fragment` — includes an 8-char SHA-256 credential fingerprint in cache keys for non-local providers.
8
+ - Add `source_tag`, `credential_fingerprint`, and `config_fingerprint` to `CredentialSources` for provenance tracking across discovered instances.
9
+ - Suppress Faraday raw stacktrace dumps on connection failures by setting `errors: false` on the response logger middleware.
10
+ - Rescue `Faraday::ConnectionFailed` in `discover_offerings` and return an empty list with a concise warning instead of propagating the exception.
11
+ - Wire `model_allowed?` filtering into `discover_offerings` so whitelist/blacklist settings are enforced during live discovery (was dead code before).
12
+ - Check instance config first for `model_whitelist`/`model_blacklist` before falling back to provider settings, enabling per-instance override.
13
+ - Add `legion-cache >= 1.3.0` as a runtime dependency and include `Legion::Cache::Helper` in the base `Provider` class.
14
+
3
15
  ## 0.4.9 - 2026-05-13
4
16
 
5
17
  - Route provider, tool, streaming, model, attachment, connection, credential, and fleet diagnostics through `Legion::Logging::Helper`.
data/lex-llm.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency 'faraday-multipart', '>= 1'
36
36
  spec.add_dependency 'faraday-net_http', '>= 1'
37
37
  spec.add_dependency 'faraday-retry', '>= 1'
38
+ spec.add_dependency 'legion-cache', '>= 1.3.0'
38
39
  spec.add_dependency 'legion-crypt', '>= 1.5.1'
39
40
  spec.add_dependency 'legion-json', '>= 1.2.1'
40
41
  spec.add_dependency 'legion-logging', '>= 1.3.2'
@@ -79,7 +79,7 @@ module Legion
79
79
  faraday.response :logger,
80
80
  logger,
81
81
  bodies: debug_logger?(logger),
82
- errors: true,
82
+ errors: false,
83
83
  headers: false,
84
84
  log_level: :debug do |logger|
85
85
  logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
@@ -167,6 +167,30 @@ module Legion
167
167
  Digest::SHA256.hexdigest(val.to_s)
168
168
  end
169
169
 
170
+ # Build a human-readable source tag describing where a credential was found.
171
+ # Format: "type:location:key" e.g. "env:ANTHROPIC_API_KEY", "file:~/.claude/settings.json:anthropicApiKey"
172
+ def source_tag(type, location, key = nil)
173
+ parts = [type.to_s, location.to_s]
174
+ parts << key.to_s if key && !key.to_s.empty?
175
+ parts.join(':')
176
+ end
177
+
178
+ # Generate a short fingerprint (first 8 chars of SHA-256) for a credential value.
179
+ # Stable for the lifetime of the credential; safe to log and include in audit events.
180
+ def credential_fingerprint(value)
181
+ return nil if value.nil? || value.to_s.strip.empty?
182
+
183
+ Digest::SHA256.hexdigest(value.to_s)[0, 8]
184
+ end
185
+
186
+ # Extract fingerprint from a config hash by finding the first credential field.
187
+ def config_fingerprint(config)
188
+ val = config[:api_key] || config['api_key'] ||
189
+ config[:bearer_token] || config['bearer_token'] ||
190
+ config[:access_token] || config['access_token']
191
+ credential_fingerprint(val)
192
+ end
193
+
170
194
  # Returns true when the URL points to localhost / 127.0.0.1 / ::1.
171
195
  def localhost?(url)
172
196
  return false if url.nil?
@@ -185,7 +209,9 @@ module Legion
185
209
  module_function :env, :claude_config, :claude_config_value,
186
210
  :claude_env_value, :codex_token, :codex_openai_key,
187
211
  :setting, :socket_open?, :http_ok?,
188
- :dedup_credentials, :credential_hash, :localhost?
212
+ :dedup_credentials, :credential_hash,
213
+ :source_tag, :credential_fingerprint, :config_fingerprint,
214
+ :localhost?
189
215
 
190
216
  # --- private helpers -----------------------------------------------
191
217
 
@@ -28,6 +28,7 @@ module Legion
28
28
  class Provider
29
29
  include Streaming
30
30
  include Legion::Logging::Helper
31
+ include Legion::Cache::Helper
31
32
 
32
33
  attr_reader :config, :connection
33
34
 
@@ -123,10 +124,14 @@ module Legion
123
124
  provider_health = health(live:)
124
125
  @cached_offerings = Array(list_models(live:, **filters)).filter_map do |model|
125
126
  next unless model_matches_filters?(model, filters)
127
+ next unless model_allowed?(model.id)
126
128
 
127
129
  offering_from_model(model, health: provider_health)
128
130
  end
129
131
  @cached_offerings
132
+ rescue Faraday::ConnectionFailed => e
133
+ log.warn("[#{slug}] instance=#{provider_instance_id} unreachable: #{e.message}")
134
+ []
130
135
  end
131
136
 
132
137
  def health(live: false)
@@ -284,12 +289,14 @@ module Legion
284
289
  # ── Model allow-list / deny-list filtering ────────────────────────
285
290
 
286
291
  def model_whitelist
287
- wl = settings[:model_whitelist] if respond_to?(:settings)
292
+ wl = config.model_whitelist if config.respond_to?(:model_whitelist)
293
+ wl ||= settings[:model_whitelist] if respond_to?(:settings)
288
294
  Array(wl).map { |p| p.to_s.downcase }
289
295
  end
290
296
 
291
297
  def model_blacklist
292
- bl = settings[:model_blacklist] if respond_to?(:settings)
298
+ bl = config.model_blacklist if config.respond_to?(:model_blacklist)
299
+ bl ||= settings[:model_blacklist] if respond_to?(:settings)
293
300
  Array(bl).map { |p| p.to_s.downcase }
294
301
  end
295
302
 
@@ -371,21 +378,24 @@ module Legion
371
378
  nil
372
379
  end
373
380
 
374
- def model_cache_set(key, value, ttl:)
375
- return unless defined?(Legion::Cache)
381
+ def model_detail(model_name)
382
+ key = model_detail_cache_key(model_name)
383
+ cached = cache_get(key)
384
+ return cached if cached
376
385
 
377
- cache_local_instance? ? local_cache_set(key, value, ttl: ttl) : cache_set(key, value, ttl: ttl)
386
+ result = fetch_model_detail(model_name)
387
+ cache_set(key, result, ttl: 86_400) if result
388
+ result
378
389
  rescue StandardError => e
379
- handle_exception(e, level: :debug, handled: true, operation: 'lex.provider.model_cache_set')
390
+ handle_exception(e, level: :warn, handled: true, operation: 'llm.provider.model_detail',
391
+ model: model_name)
392
+ nil
380
393
  end
381
394
 
382
- def model_cache_fetch(key, ttl:, &)
383
- return yield unless defined?(Legion::Cache)
384
-
385
- cache_local_instance? ? local_cache_fetch(key, ttl: ttl, &) : cache_fetch(key, ttl: ttl, &)
386
- rescue StandardError => e
387
- handle_exception(e, level: :debug, handled: true, operation: 'llm.provider.model_cache_fetch', key:)
388
- yield
395
+ # Override in subclasses to make a live API call for model detail.
396
+ # Must return a Hash with symbol keys (e.g. { context_window: 128000 }).
397
+ def fetch_model_detail(_model_name)
398
+ nil
389
399
  end
390
400
 
391
401
  def cache_instance_key
@@ -448,6 +458,26 @@ module Legion
448
458
 
449
459
  private
450
460
 
461
+ def model_detail_cache_key(model_name)
462
+ tier = offering_tier
463
+ instance_key = cache_instance_key
464
+ cred_fp = credential_cache_fragment
465
+ key_parts = ['model_info', tier, slug, instance_key, cred_fp, model_name].compact
466
+ key_parts.join('.')
467
+ end
468
+
469
+ def credential_cache_fragment
470
+ return nil if cache_local_instance?
471
+
472
+ cred = config.respond_to?(:bearer_token) && config.bearer_token
473
+ cred ||= config.respond_to?(:api_key) && config.api_key
474
+ cred ||= config.respond_to?(:bedrock_access_key_id) && config.bedrock_access_key_id
475
+ return nil unless cred
476
+
477
+ require 'digest'
478
+ Digest::SHA256.hexdigest(cred.to_s)[0, 8]
479
+ end
480
+
451
481
  def validate_paint_inputs!(with:, mask:)
452
482
  return if with.nil? && mask.nil?
453
483
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Llm
6
- VERSION = '0.4.9'
6
+ VERSION = '0.4.10'
7
7
  end
8
8
  end
9
9
  end
@@ -9,6 +9,15 @@ require 'faraday/multipart'
9
9
  require 'faraday/retry'
10
10
  require 'legion/json'
11
11
  require 'legion/logging'
12
+ # legion/cache writes DEBUG lines to $stdout on first load; suppress them here
13
+ # so callers that capture our stdout (e.g. Open3-based integration tests) are unaffected.
14
+ begin
15
+ old_stdout = $stdout
16
+ $stdout = File.open(File::NULL, 'w')
17
+ require 'legion/cache'
18
+ ensure
19
+ $stdout = old_stdout
20
+ end
12
21
  require 'logger'
13
22
  require 'marcel'
14
23
  require 'ruby_llm/schema'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.9
4
+ version: 0.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: legion-cache
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 1.3.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 1.3.0
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: legion-crypt
113
127
  requirement: !ruby/object:Gem::Requirement