lex-llm-gemini 0.3.13 → 0.3.18

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: d81052fa4455e1c73b7567864422ae494442f0fce0d9206fb0634bc238c939eb
4
- data.tar.gz: 9f56e59676cc93aa0b55e4d8958975ce3e0a2c02308daaaeecb40c46c8535564
3
+ metadata.gz: 564af323ba9c521e6404595eb47f3c1470da8459aec2790c42f51d004e3c2394
4
+ data.tar.gz: 4a7bce02b3c4399425ed0749ca5fb580e67d445af04c85480c7b1c0d33a01782
5
5
  SHA512:
6
- metadata.gz: 0fe88e33de1fa4575867f35ffe717528a3759aa1b0e3c15fe42a9cfe99ff8e00826b0a8069f1d4788091b17448c307cf8e4ad694684c166249b9a57360c8dccc
7
- data.tar.gz: 999f873f14e211211232b72ba3f5d5dd6dd31560de9e44081a1e2355c6574baa7a4a169701492a00803fcb9fb1bc6b9d73de37f399cb2439505a7c3c970717a7
6
+ metadata.gz: 62157a365a6a265791ae7ec076332976241b3137076e5b8fa5f389625b035679ac38afa9ff16324b578f1ab2d9cd35e33b30564eb37a7870eda05dee66ae40cf
7
+ data.tar.gz: 98c631924f43613cf28f034a972ef8b2326d88286364768094640080a68e5daf73bdd945ce60867cce6f58f75d6640fc65c0235738c3e7a3d7dd374602f0069f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.18] - 2026-08-04
4
+
5
+ ### Changed
6
+ - Prepare the Gemini provider standardization baseline for a patch release.
7
+
8
+ ## [0.3.17] - 2026-07-02
9
+
10
+ ### Fixed
11
+ - Emit canonical capability vocabulary (`:embedding`, `:tools`) from `Capabilities.critical_capabilities_for`. `lex-llm` 0.6.1+ retains both the raw and canonical forms on `Model::Info`, so the pre-canonical aliases (`embeddings`, `function_calling`) leaked duplicate tokens (e.g. `[:embeddings, :embedding]`) into discovered model capabilities.
12
+
13
+ ## [0.3.16] - 2026-06-20
14
+
15
+ ### Fixed
16
+ - Stub shared registry publishing through `RegistryPublisher#schedule` in specs so async availability-event coverage stays stable after the shared publisher moved off raw `Thread.new`.
17
+
18
+ ## [0.3.15] - 2026-06-20
19
+
20
+ ### Changed
21
+ - Align Gemini instance discovery with the shared `lex-llm` contract by preserving explicit tier overrides while defaulting unconfigured instances to `:cloud`.
22
+ - Expand instance-level capability override passthrough to the shared `enable_*` vocabulary, including `structured_output`.
23
+ - Normalize Gemini contract coverage around canonical `:tools` and `:embedding` capabilities.
24
+
25
+ ## [0.3.14] - 2026-06-19
26
+
27
+ ### Changed
28
+ - Adopt `Legion::Extensions::Llm::Inventory::ScopedRefresher` mixin (lex-llm 0.6.0). Discovery
29
+ refresh actors now write directly to the live `Inventory` catalog via `Inventory.write_lane`.
30
+ - Pin `lex-llm >= 0.6.0` and `legion-llm >= 0.14.0` in gemspec.
31
+ - Standard `weight: 100` default added to provider instance settings schema.
32
+
3
33
  ## 0.3.13 - 2026-06-16
4
34
 
5
35
  - Dependency updates, 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
@@ -13,10 +21,14 @@ module Legion
13
21
  module Llm
14
22
  module Gemini
15
23
  module Actor
16
- class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation
24
+ class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation,Metrics/ClassLength
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,26 +38,137 @@ 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, :gemini, :discovery_interval) || REFRESH_INTERVAL
43
+ Legion::Settings.dig(:extensions, :llm, :gemini, :discovery_interval) || self.class.every_seconds
32
44
  end
33
45
 
34
- def manual
35
- log.debug('[gemini][discovery_refresh] refreshing model list')
36
- return unless defined?(Legion::LLM::Discovery)
46
+ def scope_key(**)
47
+ { provider: :gemini }
48
+ end
37
49
 
38
- Legion::LLM::Discovery.refresh_discovered_models!(provider: :gemini)
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)
42
- end
43
- if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
44
- Legion::LLM::Inventory.invalidate_offerings_cache!
53
+ lanes = []
54
+ gemini_instances.each do |instance|
55
+ collect_lanes_for_instance(instance, lanes)
56
+ rescue StandardError => e
57
+ handle_exception(e, level: :warn, handled: true,
58
+ operation: 'gemini.discovery_refresh.compute_lanes',
59
+ instance: instance[:id])
45
60
  end
61
+ lanes
62
+ rescue StandardError => e
63
+ handle_exception(e, level: :warn, handled: true,
64
+ operation: 'gemini.discovery_refresh.compute_lanes_for_scope')
65
+ []
66
+ end
67
+
68
+ def credential_hash(**)
69
+ settings = gemini_settings
70
+ Digest::SHA256.hexdigest(settings[:api_key].to_s + settings[:instances].to_s)[0, 16]
71
+ rescue StandardError => e
72
+ handle_exception(e, level: :warn, handled: true, operation: 'gemini.discovery_refresh.credential_hash')
73
+ 'unknown'
74
+ end
75
+
76
+ def manual(**)
77
+ tick if defined?(Legion::Extensions::Llm::Inventory::ScopedRefresher) &&
78
+ respond_to?(:tick, true)
46
79
  rescue StandardError => e
47
80
  handle_exception(e, level: :warn, handled: true, operation: 'gemini.actor.discovery_refresh')
48
81
  end
82
+
83
+ private
84
+
85
+ def gemini_instances
86
+ Legion::LLM::Call::Registry.all_instances.select do |e|
87
+ (e[:provider] || '').to_sym == :gemini
88
+ end
89
+ end
90
+
91
+ def collect_lanes_for_instance(instance, lanes)
92
+ adapter = instance[:adapter]
93
+ return unless adapter.respond_to?(:discover_offerings)
94
+
95
+ Array(adapter.discover_offerings(live: false)).each do |raw_offering|
96
+ offering = offering_to_hash(raw_offering)
97
+ next unless offering
98
+
99
+ model = offering[:model] || offering['model']
100
+ next unless model
101
+
102
+ lane = build_lane(offering, instance)
103
+ lanes << lane
104
+ lanes << fleet_lane(lane, instance) if emit_fleet_lane?(lane)
105
+ end
106
+ end
107
+
108
+ def offering_to_hash(offering)
109
+ return nil if offering.nil?
110
+ return offering if offering.is_a?(Hash)
111
+
112
+ hash = offering.to_h
113
+ hash[:type] ||= hash[:usage_type]
114
+ hash[:enabled] = offering.respond_to?(:enabled?) ? offering.enabled? : true
115
+ hash
116
+ end
117
+
118
+ def build_lane(offering, instance) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
119
+ instance_id = instance[:instance] || instance[:instance_id] || instance[:id]
120
+ raw_tier = offering[:tier] || :frontier
121
+ offer_type = offering[:type]
122
+ type = %i[embed embedding].include?(offer_type) ? :embedding : :inference
123
+ capabilities = normalize_capabilities(offering[:capabilities] || [])
124
+ model = offering[:model] || offering['model']
125
+
126
+ lane_id = Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(
127
+ tier: raw_tier, provider_family: :gemini,
128
+ instance_id: instance_id, type: type, model: model
129
+ )
130
+
131
+ {
132
+ id: lane_id,
133
+ tier: raw_tier,
134
+ provider_family: :gemini,
135
+ instance_id: instance_id,
136
+ model: model,
137
+ canonical_model_alias: offering[:canonical_model_alias] || offering[:name],
138
+ type: type,
139
+ capabilities: capabilities,
140
+ limits: offering[:limits] || {},
141
+ enabled: offering.fetch(:enabled, true),
142
+ cost: offering[:cost] || {}
143
+ }
144
+ end
145
+
146
+ def emit_fleet_lane?(lane)
147
+ return false unless lane[:type] == :inference
148
+
149
+ gemini_settings&.dig(:fleet, :dispatch, :enabled)
150
+ end
151
+
152
+ def fleet_lane(lane, instance)
153
+ fleet_id = Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(
154
+ tier: :fleet, provider_family: :gemini,
155
+ instance_id: instance[:instance] || instance[:instance_id] || instance[:id],
156
+ type: lane[:type], model: lane[:model]
157
+ )
158
+ lane.merge(id: fleet_id, tier: :fleet)
159
+ end
160
+
161
+ def normalize_capabilities(caps)
162
+ return [] unless defined?(Legion::Extensions::Llm::Inventory::Capabilities)
163
+
164
+ Legion::Extensions::Llm::Inventory::Capabilities.normalize(caps)
165
+ end
166
+
167
+ def gemini_settings
168
+ Legion::Settings.dig(:extensions, :llm, :gemini)
169
+ rescue StandardError
170
+ {}
171
+ end
49
172
  end
50
173
  end
51
174
  end
@@ -33,11 +33,14 @@ module Legion
33
33
  def vision?(model) = chat?(model) && model_id(model).match?(/gemini|flash|pro/)
34
34
  def functions?(model) = chat?(model)
35
35
 
36
+ # Emit canonical capability vocabulary (see Legion::Extensions::Llm::Capabilities::CANONICAL).
37
+ # Model::Info retains both raw and canonical forms, so aliases like :embeddings/:function_calling
38
+ # would otherwise leak duplicate tokens into the capability list.
36
39
  def critical_capabilities_for(model)
37
40
  [
38
41
  ('streaming' if streaming?(model)),
39
- ('embeddings' if embeddings?(model)),
40
- ('function_calling' if functions?(model)),
42
+ ('embedding' if embeddings?(model)),
43
+ ('tools' if functions?(model)),
41
44
  ('vision' if vision?(model))
42
45
  ].compact
43
46
  end
@@ -350,7 +353,7 @@ module Legion
350
353
  methods = Array(meta[:supported_generation_methods] || meta['supported_generation_methods'])
351
354
  {
352
355
  streaming: methods.include?('streamGenerateContent'),
353
- embeddings: methods.include?('embedContent'),
356
+ embedding: methods.include?('embedContent'),
354
357
  vision: Capabilities.vision?(meta.merge('name' => "models/#{model.id}"))
355
358
  }.compact
356
359
  end
@@ -361,24 +364,14 @@ module Legion
361
364
  conf = Legion::Extensions::Llm::CredentialSources.setting(:extensions, :llm, :gemini)
362
365
  conf.is_a?(Hash) ? conf.to_h.except(:instances, 'instances') : {}
363
366
  rescue StandardError => e
364
- handle_exception(e, level: :debug, handled: true, operation: 'gemini.provider_capability_config')
367
+ handle_exception(e, level: :warn, handled: true, operation: 'gemini.provider_capability_config')
365
368
  {}
366
369
  end
367
370
 
368
371
  def instance_capability_config
369
- cfg = config
370
- result = {}
371
- %i[capabilities enable_streaming enable_tools enable_thinking enable_vision enable_embeddings
372
- thinking_flag tools_flag streaming_flag vision_flag embedding_flag embeddings_flag
373
- tool_flag images_flag image_flag].each do |key|
374
- next unless cfg.respond_to?(key)
375
-
376
- val = cfg.send(key)
377
- result[key] = val unless val.nil?
378
- rescue StandardError
379
- next
380
- end
381
- result
372
+ return {} unless config.respond_to?(:to_h)
373
+
374
+ config.to_h.slice(*Legion::Extensions::Llm::Provider::CAPABILITY_CONFIG_KEYS)
382
375
  end
383
376
 
384
377
  def model_capability_config(model_id)
@@ -388,7 +381,7 @@ module Legion
388
381
  hash = models_conf.to_h
389
382
  hash[model_id.to_s] || hash[model_id.to_sym] || {}
390
383
  rescue StandardError => e
391
- handle_exception(e, level: :debug, handled: true, operation: 'gemini.model_capability_config')
384
+ handle_exception(e, level: :warn, handled: true, operation: 'gemini.model_capability_config')
392
385
  {}
393
386
  end
394
387
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Gemini
7
- VERSION = '0.3.13'
7
+ VERSION = '0.3.18'
8
8
  end
9
9
  end
10
10
  end
@@ -69,7 +69,7 @@ module Legion
69
69
 
70
70
  candidates[:settings] = normalize_instance_config(cfg).merge(api_key: api_key,
71
71
  gemini_api_key: api_key,
72
- tier: :cloud)
72
+ tier: cfg[:tier] || cfg['tier'] || :cloud)
73
73
  end
74
74
 
75
75
  def self.add_settings_instances(candidates, cfg)
@@ -83,7 +83,8 @@ module Legion
83
83
  next unless normalized[:gemini_api_key]
84
84
 
85
85
  normalized[:api_key] = normalized[:gemini_api_key]
86
- candidates[name.to_sym] = normalized.merge(tier: :cloud)
86
+ normalized[:tier] ||= :cloud
87
+ candidates[name.to_sym] = normalized
87
88
  end
88
89
  end
89
90
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-gemini
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.13
4
+ version: 0.3.18
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: Gemini provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com