lex-llm-vertex 0.2.13 → 0.2.15

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: 31112ce3580ae2a99c27285cbc5385d742faf52f4aaa854212802254c423112a
4
- data.tar.gz: 64ba36b8b97732e2d2b63605e2f7edb33f82b42065f5f79d8c2328d0dd3e3b35
3
+ metadata.gz: b1b2e665277fea3299658b992eac2b441a363e3c81dbf87d69e86c7328c6c99c
4
+ data.tar.gz: d81b38ce8e5e797a074d30a670a5e6fa9bcb92762c5dcd066ed86ad8196d5340
5
5
  SHA512:
6
- metadata.gz: 924fe07b104555a416524b8d7e9994e0a09e6051e307034161d485d285cad2bd44c7cc26653420cfee4d629e757b6cdbd02088d2c375e032c437eb660fd7a28d
7
- data.tar.gz: a1467f3de89eba4f4cc7e852e49f33153fe8caa79878a2da1b57e147a95a50a8122a1b1c60ab018cbb90e12c6b2a7c0b774dc9962f0288815b7e871fae6fdcc6
6
+ metadata.gz: 97009f1dd75f9f053ec06dfba726627f7c6202f92bb56a0185e9250d7579ff73de1c7c410b3ae6a82b4ad308032384362a0dd5c12ed2eeddc30e3c690d381753
7
+ data.tar.gz: 2f36d16c24610e1355ee50cdeb26e99818e8a1b7cc6441b9ef88f7e0d17130bd8691bba88e8db3c71bc3becd33a1abe5962ee65802a957abdcc6dec7bc4f839d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.15] - 2026-06-20
4
+
5
+ ### Changed
6
+ - Align Vertex offerings to the current `lex-llm` contract: shared `discover_offerings` now rebuilds
7
+ resource-name offerings from discovered models, preserves provider health on offerings, and keeps the
8
+ shared capability-override path intact.
9
+ - Fix the provider tail introduced during the contract refactor so the provider file closes cleanly again.
10
+
11
+ ## [0.2.14] - 2026-06-19
12
+
13
+ ### Changed
14
+ - Adopt `Legion::Extensions::Llm::Inventory::ScopedRefresher` mixin (lex-llm 0.6.0). Discovery
15
+ refresh actors now write directly to the live `Inventory` catalog via `Inventory.write_lane`.
16
+ - Pin `lex-llm >= 0.6.0` and `legion-llm >= 0.14.0` in gemspec.
17
+ - Standard `weight: 100` default added to provider instance settings schema.
18
+
3
19
  ## 0.2.13 - 2026-06-16
4
20
 
5
21
  - Dependency updates and code quality improvements.
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency 'legion-logging', '>= 1.3.2'
28
28
  spec.add_dependency 'legion-settings', '>= 1.3.14'
29
29
  spec.add_dependency 'legion-transport', '>= 1.4.14'
30
- spec.add_dependency 'lex-llm', '>= 0.5.0'
30
+ spec.add_dependency 'lex-llm', '>= 0.6.0'
31
31
  end
@@ -1,11 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'digest'
4
+
3
5
  begin
4
6
  require 'legion/extensions/actors/every'
5
7
  rescue LoadError => e
6
8
  warn(e.message) if $VERBOSE
7
9
  end
8
10
 
11
+ begin
12
+ require 'legion/extensions/llm/inventory/scoped_refresher'
13
+ rescue LoadError => e
14
+ warn(e.message) if $VERBOSE
15
+ end
16
+
9
17
  return unless defined?(Legion::Extensions::Actors::Every)
10
18
 
11
19
  module Legion
@@ -16,7 +24,11 @@ module Legion
16
24
  class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation
17
25
  include Legion::Logging::Helper
18
26
 
19
- REFRESH_INTERVAL = 1800
27
+ if defined?(Legion::Extensions::Llm::Inventory::ScopedRefresher)
28
+ include Legion::Extensions::Llm::Inventory::ScopedRefresher
29
+ end
30
+
31
+ def self.every_seconds = 3600
20
32
 
21
33
  def runner_class = self.class
22
34
  def runner_function = 'manual'
@@ -26,23 +38,106 @@ module Legion
26
38
  def generate_task? = false
27
39
 
28
40
  def time
29
- return REFRESH_INTERVAL unless defined?(Legion::Settings)
41
+ return self.class.every_seconds unless defined?(Legion::Settings)
30
42
 
31
- Legion::Settings.dig(:extensions, :llm, :vertex, :discovery_interval) || REFRESH_INTERVAL
43
+ Legion::Settings.dig(:extensions, :llm, :vertex, :discovery_interval) || self.class.every_seconds
32
44
  end
33
45
 
34
- def manual
35
- log.debug('[vertex][discovery_refresh] refreshing model list')
36
- return unless defined?(Legion::LLM::Discovery)
46
+ def scope_key
47
+ { provider: :vertex }
48
+ end
37
49
 
38
- Legion::LLM::Discovery.refresh_discovered_models!(provider: :vertex)
50
+ def compute_lanes_for_scope(**)
51
+ return [] unless defined?(Legion::LLM::Call::Registry)
39
52
 
40
- if defined?(Legion::LLM::Router) && Legion::LLM::Router.respond_to?(:populate_auto_rules)
41
- Legion::LLM::Router.populate_auto_rules(Legion::LLM::Discovery.discovered_instances)
53
+ settings = Legion::Settings.dig(:extensions, :llm, :vertex) || {}
54
+ fleet_enabled = settings.dig(:fleet, :dispatch, :enabled)
55
+ instances = Legion::LLM::Call::Registry.all_instances.select do |e|
56
+ (e[:provider] || '').to_sym == :vertex
42
57
  end
43
- if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
44
- Legion::LLM::Inventory.invalidate_offerings_cache!
58
+
59
+ instances.flat_map do |entry|
60
+ lanes_for_instance(entry, fleet_enabled: fleet_enabled)
45
61
  end
62
+ rescue StandardError => e
63
+ handle_exception(e, level: :warn, handled: true, operation: 'vertex.actor.compute_lanes_for_scope')
64
+ []
65
+ end
66
+
67
+ private
68
+
69
+ def lanes_for_instance(entry, fleet_enabled: false)
70
+ adapter = entry[:adapter]
71
+ instance_id = entry[:instance] || entry[:instance_id] || entry[:id]
72
+ lanes = []
73
+ Array(adapter.discover_offerings(live: false)).each do |raw_offering|
74
+ offering = offering_to_hash(raw_offering)
75
+ next unless offering
76
+
77
+ lane = build_lane(offering, instance_id)
78
+ lanes << lane
79
+ lanes << fleet_lane(lane, instance_id, offering) if fleet_enabled && lane[:type] == :inference
80
+ end
81
+ lanes
82
+ end
83
+
84
+ def offering_to_hash(offering)
85
+ return nil if offering.nil?
86
+ return offering if offering.is_a?(Hash)
87
+
88
+ hash = offering.to_h
89
+ hash[:type] ||= hash[:usage_type]
90
+ hash[:enabled] = offering.respond_to?(:enabled?) ? offering.enabled? : true
91
+ hash
92
+ end
93
+
94
+ def build_lane(offering, instance_id)
95
+ type = offering_type(offering)
96
+ tier = offering[:tier]&.to_sym || :cloud
97
+ caps = normalize_capabilities(offering[:capabilities])
98
+ flds = { tier: tier, provider_family: :vertex, instance_id: instance_id,
99
+ type: type, model: offering[:model] }
100
+ {
101
+ id: Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(flds),
102
+ tier: tier,
103
+ provider_family: :vertex,
104
+ instance_id: instance_id,
105
+ model: offering[:model],
106
+ canonical_model_alias: offering[:canonical_model_alias],
107
+ type: type,
108
+ capabilities: caps,
109
+ limits: offering[:limits] || {},
110
+ enabled: offering.fetch(:enabled, true),
111
+ cost: offering[:cost] || {}
112
+ }
113
+ end
114
+
115
+ def fleet_lane(lane, instance_id, offering)
116
+ flds = { tier: :fleet, provider_family: :vertex, instance_id: instance_id,
117
+ type: lane[:type], model: offering[:model] }
118
+ lane.merge(id: Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(flds), tier: :fleet)
119
+ end
120
+
121
+ def offering_type(offering)
122
+ %i[embed embedding].include?(offering[:type]&.to_sym) ? :embedding : :inference
123
+ end
124
+
125
+ def normalize_capabilities(caps)
126
+ return [] unless defined?(Legion::Extensions::Llm::Inventory::Capabilities) &&
127
+ Legion::Extensions::Llm::Inventory::Capabilities.respond_to?(:normalize)
128
+
129
+ Legion::Extensions::Llm::Inventory::Capabilities.normalize(caps)
130
+ end
131
+
132
+ public
133
+
134
+ def credential_hash(**)
135
+ settings = Legion::Settings.dig(:extensions, :llm, :vertex) || {}
136
+ ::Digest::SHA256.hexdigest(settings[:api_key].to_s + settings[:instances].to_s)[0, 16]
137
+ end
138
+
139
+ def manual
140
+ tick if respond_to?(:tick)
46
141
  rescue StandardError => e
47
142
  handle_exception(e, level: :warn, handled: true, operation: 'vertex.actor.discovery_refresh')
48
143
  end
@@ -116,7 +116,7 @@ module Legion
116
116
  "#{publisher_model_path(model)}:#{suffix}"
117
117
  end
118
118
 
119
- def list_models(**)
119
+ def list_models(**_filters)
120
120
  log.info { 'listing available Vertex models from static catalog' }
121
121
  STATIC_MODELS.map { |entry| model_info_from_static(entry) }.tap do |models|
122
122
  log.info { "discovered #{models.size} Vertex model(s); publishing to registry" }
@@ -125,22 +125,13 @@ module Legion
125
125
  end
126
126
 
127
127
  def discover_offerings(live: false, **filters)
128
- log.info { "discovering offerings live=#{live} project=#{project} location=#{location}" }
129
- return static_offerings(**filters) unless live
130
-
131
- response = connection.get(models_url)
132
- models = response.body['publisherModels'] || response.body['models'] || []
133
- offerings = models.filter_map do |model|
134
- offering = offering_from_live_model(model)
135
- model_id = offering.respond_to?(:model) ? offering.model : (offering[:model] || offering[:id])
136
- next unless model_allowed?(model_id.to_s)
137
-
138
- offering
128
+ unless live
129
+ return static_offerings(**filters).select do |offering|
130
+ model_allowed?(short_model_id(offering.model))
131
+ end
139
132
  end
140
- log.info { "discovered #{offerings.size} live offering(s) from Vertex" }
141
- model_infos = offerings.map { |o| model_info_from_offering(o) }
142
- self.class.registry_publisher.publish_models_async(model_infos, readiness: readiness(live: false))
143
- offerings
133
+
134
+ super
144
135
  end
145
136
 
146
137
  def offering_for(model:, model_family: nil, instance_id: :default, **metadata)
@@ -325,8 +316,48 @@ module Legion
325
316
  offering_for(model: id, publisher:, metadata: model)
326
317
  end
327
318
 
319
+ def offering_from_model(model_info, health: {})
320
+ metadata = model_info.respond_to?(:metadata) ? model_info.metadata.to_h : {}
321
+ raw_model = model_info.respond_to?(:id) ? model_info.id : model_info
322
+ publisher = metadata[:publisher] || metadata['publisher'] || publisher_for(raw_model)
323
+ api = metadata[:api] || metadata['api'] || api_for(raw_model)
324
+ alias_name = model_info.respond_to?(:name) ? model_info.name : nil
325
+ alias_name = nil if alias_name.to_s.empty? || alias_name.to_s == raw_model.to_s
326
+
327
+ build_offering(
328
+ model: resource_name(raw_model, publisher: publisher),
329
+ alias_name: alias_name,
330
+ model_family: if model_info.respond_to?(:family) && model_info.family
331
+ model_info.family.to_sym
332
+ else
333
+ model_family_for(
334
+ raw_model, publisher
335
+ )
336
+ end,
337
+ instance_id: if model_info.respond_to?(:instance)
338
+ model_info.instance || provider_instance_id
339
+ else
340
+ provider_instance_id
341
+ end,
342
+ publisher: publisher,
343
+ usage_type: if model_info.respond_to?(:embedding?) && model_info.embedding?
344
+ :embedding
345
+ else
346
+ usage_type_for(raw_model)
347
+ end,
348
+ api: api,
349
+ health: health,
350
+ metadata: metadata.merge(
351
+ limits: {
352
+ context_window: model_info.respond_to?(:context_length) ? model_info.context_length : nil,
353
+ max_output_tokens: model_info.respond_to?(:max_output_tokens) ? model_info.max_output_tokens : nil
354
+ }.compact
355
+ )
356
+ )
357
+ end
358
+
328
359
  def build_offering(model:, model_family:, usage_type:, publisher:, api:, instance_id: :default,
329
- alias_name: nil, metadata: {})
360
+ alias_name: nil, health: {}, metadata: {})
330
361
  policy = resolve_capability_policy(model, api:, metadata:, instance_id:)
331
362
 
332
363
  Legion::Extensions::Llm::Routing::ModelOffering.new(
@@ -339,6 +370,7 @@ module Legion
339
370
  capabilities: base_capabilities(model, api:) + policy[:capabilities],
340
371
  capability_sources: policy[:sources],
341
372
  limits: metadata.delete(:limits) || {},
373
+ health: health,
342
374
  metadata: metadata.merge(
343
375
  model_family: model_family,
344
376
  alias: alias_name,
@@ -697,7 +729,7 @@ module Legion
697
729
 
698
730
  cfg.except(:instances, 'instances')
699
731
  rescue StandardError => e
700
- handle_exception(e, level: :debug, handled: true, operation: 'vertex.provider.capability_policy_config')
732
+ handle_exception(e, level: :warn, handled: true, operation: 'vertex.provider.capability_policy_config')
701
733
  {}
702
734
  end
703
735
 
@@ -710,7 +742,7 @@ module Legion
710
742
 
711
743
  (instances[instance_id] || instances[instance_id.to_s] || {}).to_h
712
744
  rescue StandardError => e
713
- handle_exception(e, level: :debug, handled: true, operation: 'vertex.provider.instance_config')
745
+ handle_exception(e, level: :warn, handled: true, operation: 'vertex.provider.instance_config')
714
746
  {}
715
747
  end
716
748
 
@@ -724,7 +756,7 @@ module Legion
724
756
  id = short_model_id(model)
725
757
  (models[id.to_sym] || models[id.to_s] || {}).to_h
726
758
  rescue StandardError => e
727
- handle_exception(e, level: :debug, handled: true, operation: 'vertex.provider.model_config')
759
+ handle_exception(e, level: :warn, handled: true, operation: 'vertex.provider.model_config')
728
760
  {}
729
761
  end
730
762
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Vertex
7
- VERSION = '0.2.13'
7
+ VERSION = '0.2.15'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-vertex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.13
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 0.5.0
74
+ version: 0.6.0
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 0.5.0
81
+ version: 0.6.0
82
82
  description: Google Cloud Vertex AI provider integration for the LegionIO LLM routing
83
83
  framework.
84
84
  email: