lex-llm-openai 0.6.3 → 0.6.4

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: 84567e208562add7567e8de290c9d69ce3a7ae7e10cfab7e7e6ddf92173d2b7d
4
- data.tar.gz: 912a0379793e6e7dc10353aaf198ddc2c08b50310eb1642b8e2f7e5f1f244010
3
+ metadata.gz: f0ea7679c67d23d7a6b30bf8ec6057136f72c791c2846ffa805fa7fdb1b7b735
4
+ data.tar.gz: 3eb7f7fce099a71a77b6f5fa1acdc55610c870a0834aedcfa89c954bc7c8d687
5
5
  SHA512:
6
- metadata.gz: a92b8cc928c93cf271ab575f15a9c582ddb4a53ceab09f8d0817a9b4268abc4dddcb997a11e926490c739798a9f25b763ea7cdbe3e348b328570f6ee3714b1fb
7
- data.tar.gz: f8686fa71f7b9a23b7ee2f0a26bd74b8ea44afbefc33f6c47f0ed3836c34fe3e75acce1cd49bc98407a9ecf41e4937642f84b3fe0ce50f5d24c66b1f0da29884
6
+ metadata.gz: 7ff08b8ae9072732c1858bd7f88845496624c43a864d0d90d8727ca568d9edf8b59b538afdb87fc1081ddeb4354fd5c919db8bf89f6199d79f78b6c101f57792
7
+ data.tar.gz: c9162a28401cf7ac3544d06603f71bca9bf31a9e1136146146c23bdee261f1b90c698a2f17299f6a16e986ba0b7a0498c231f79a0bc4eb25aa456fdd4d2ac1f8
data/CHANGELOG.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.6.4] - 2026-08-19
4
4
 
5
5
  ### Fixed
6
+ - **Write-time lane weights** — every discovered offering now carries the validated four-axis `weight_inputs` and `base_weight` pair from live settings. The existing periodic actor cadence atomically compares, publishes, sequences, and caches catalog or weight-only changes; unchanged passes stay no-op, initializing instances cache without replacing, and removals cannot race into late activation.
7
+ - **Dormant weight visibility** — ordinary discovery passes emit one info log when a configured provider, instance, model, or offering weight has no published lane; the state clears when the lane appears and logs once again if it later disappears.
8
+ - **D14 callable wire conformance** — an actual `OpenaiCallable#chat` request with a folded leading system message is captured at the provider connection and verified as OpenAI's native leading `role: system` message.
9
+ - **Malformed discovery failures fail loud** — programming/configuration errors from draft construction are no longer swallowed as an empty catalog; only Faraday transport failures and JSON parse failures take the existing empty-discovery recovery path.
10
+ - **lex-llm floor bumped to >= 0.7.6** — requires the shared `WeightSchema`, atomic `WeightReconciler`, weighted offering records, and operation-to-lane taxonomy used by this writer. The `legion-settings >= 1.4.2` floor and lifecycle remain unchanged.
6
11
  - **Credential-less configured instances are discovered normally** — removed the synthetic-default suppression and its one-time warning cache. A configured instance, including one named `default`, now follows the ordinary discovery and readiness path without a skip warning.
12
+ - **Complete offering equivalence** — ordinary discovery now compares the complete `OfferingDraft` contract as an order-insensitive, duplicate-sensitive multiset, ignoring only evidence `observed_at` timestamps. Provider-native identity, publication source, tokenizer evidence, and all future draft fields trigger exactly one replacement when they change, while catalog reordering remains a no-op.
7
13
 
8
14
  ## [0.6.2] - 2026-08-17
9
15
 
@@ -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.4.2'
29
29
  spec.add_dependency 'legion-transport', '>= 1.4.14'
30
- spec.add_dependency 'lex-llm', '>= 0.7.1'
30
+ spec.add_dependency 'lex-llm', '>= 0.7.6'
31
31
  end
@@ -9,8 +9,10 @@ require 'legion/extensions/llm/openai/discovery_evidence_builders'
9
9
  require 'legion/extensions/llm/openai/discovery_drafts'
10
10
  require 'legion/extensions/llm/openai/discovery_identity'
11
11
  require 'legion/extensions/llm/openai/discovery_probing'
12
+ require 'legion/extensions/llm/openai/discovery_probe_reporting'
12
13
  require 'legion/extensions/llm/openai/discovery_transport'
13
14
  require 'legion/extensions/llm/openai/discovery_health_display'
15
+ require 'legion/extensions/llm/openai/discovery_weight_publication'
14
16
  require 'legion/extensions/llm/inventory/publisher'
15
17
  require 'legion/extensions/llm/inventory/identity'
16
18
  require 'legion/extensions/llm/inventory/records'
@@ -54,8 +56,10 @@ module Legion
54
56
  include Legion::Extensions::Llm::Openai::DiscoveryDrafts
55
57
  include Legion::Extensions::Llm::Openai::DiscoveryIdentity
56
58
  include Legion::Extensions::Llm::Openai::DiscoveryProbing
59
+ include Legion::Extensions::Llm::Openai::DiscoveryProbeReporting
57
60
  include Legion::Extensions::Llm::Openai::DiscoveryTransport
58
61
  include Legion::Extensions::Llm::Openai::DiscoveryHealthDisplay
62
+ include Legion::Extensions::Llm::Openai::DiscoveryWeightPublication
59
63
 
60
64
  def runner_class = self.class
61
65
  def runner_function = 'manual'
@@ -69,7 +73,7 @@ module Legion
69
73
  end
70
74
 
71
75
  def manual
72
- @instance_states ||= {}
76
+ initialize_weight_publication
73
77
  tick_refresh
74
78
  rescue StandardError => e
75
79
  handle_exception(e, level: :warn, operation: 'openai.actor.discovery_refresh')
@@ -78,7 +82,11 @@ module Legion
78
82
  def shutdown
79
83
  return unless @instance_states
80
84
 
81
- @instance_states.each_key { |instance_id| remove_instance_state(instance_id) }
85
+ instance_states_snapshot.each_key { |instance_id| remove_instance_state(instance_id) }
86
+ @instance_state_mutex.synchronize do
87
+ @instance_states.clear
88
+ @dormant_weight_tracker.clear!
89
+ end
82
90
  end
83
91
 
84
92
  private
@@ -109,12 +117,13 @@ module Legion
109
117
 
110
118
  def tick_refresh
111
119
  reconcile_configured_instances
112
- @instance_states.each do |instance_id, state|
120
+ instance_states_snapshot.each do |instance_id, state|
113
121
  refresh_instance(instance_id: instance_id, state: state)
114
122
  rescue StandardError => e
115
123
  handle_exception(e, level: :warn, operation: 'openai.actor.refresh_instance',
116
124
  instance_id: instance_id)
117
125
  end
126
+ observe_dormant_weights
118
127
  end
119
128
 
120
129
  # Re-scans configured instances every tick so instances configured
@@ -128,10 +137,15 @@ module Legion
128
137
 
129
138
  def claim_new_instances(discovered)
130
139
  discovered.each do |name, instance_cfg|
131
- next if @instance_states.key?(name.to_s)
132
-
133
- @instance_states[name.to_s] = build_instance_context(
134
- name: name, instance_cfg: instance_cfg
140
+ instance_id = name.to_s
141
+ next if tracked_instance_state(instance_id)
142
+
143
+ state = build_instance_context(name: name, instance_cfg: instance_cfg)
144
+ Legion::Extensions::Llm::Inventory::WeightReconciler.track_initializing!(
145
+ states: @instance_states,
146
+ state_key: instance_id,
147
+ state: state,
148
+ mutex: @instance_state_mutex
135
149
  )
136
150
  rescue StandardError => e
137
151
  handle_exception(e, level: :warn, operation: 'openai.actor.claim_instance',
@@ -141,7 +155,9 @@ module Legion
141
155
 
142
156
  def release_removed_instances(discovered)
143
157
  discovered_names = discovered.keys.map(&:to_s)
144
- (@instance_states.keys - discovered_names).each { |instance_id| remove_instance_state(instance_id) }
158
+ (instance_states_snapshot.keys - discovered_names).each do |instance_id|
159
+ remove_instance_state(instance_id)
160
+ end
145
161
  end
146
162
 
147
163
  def build_instance_context(name:, instance_cfg:)
@@ -149,6 +165,9 @@ module Legion
149
165
  instance_key = Legion::Extensions::Llm::Inventory::Identity::InstanceKey.new(
150
166
  provider_family: :openai, instance_id: name.to_s, physical_id: physical_id
151
167
  )
168
+ offerings = discover_offerings_for_instance(
169
+ instance_cfg: instance_cfg, instance_key: instance_key
170
+ )
152
171
  callable = Legion::Extensions::Llm::Openai::OpenaiCallable.new(instance_cfg: instance_cfg, logger: log)
153
172
  probe_coordinator = Legion::Extensions::Llm::Inventory::ProbeCoordinator.new(
154
173
  instance_key: instance_key,
@@ -165,7 +184,8 @@ module Legion
165
184
  instance_cfg: instance_cfg,
166
185
  callable: callable, probe_coordinator: probe_coordinator,
167
186
  publisher_token: publisher_token, sequence: 0, last_probe_outcome: nil,
168
- offerings: discover_offerings_for_instance(instance_cfg: instance_cfg, instance_key: instance_key)
187
+ offerings: offerings,
188
+ published: false
169
189
  }
170
190
  end
171
191
 
@@ -187,6 +207,8 @@ module Legion
187
207
  # calling replace/readiness_succeeded, which would raise
188
208
  # InvalidTransitionError.
189
209
  def run_initialization_probe(instance_id:, state:)
210
+ refresh_unpublished_offerings(instance_id: instance_id, state: state) \
211
+ if state[:last_probe_outcome] == :failure
190
212
  coordinator = state[:probe_coordinator]
191
213
  return unless coordinator.begin_probe
192
214
 
@@ -224,16 +246,24 @@ module Legion
224
246
  end
225
247
 
226
248
  def activate_after_readiness(instance_id:, state:, probe_token:)
227
- state[:sequence] += 1
228
- state[:last_probe_outcome] = :success
229
- publisher.activate_instance_snapshot(
249
+ activated = Legion::Extensions::Llm::Inventory::WeightReconciler.activate_tracked!(
250
+ settings: Legion::Settings,
230
251
  instance_id: instance_id,
231
- physical_id: state[:physical_id],
232
- publisher_token: state[:publisher_token],
233
- offerings: state[:offerings],
234
- sequence: state[:sequence],
235
- probe_token: probe_token
252
+ state_key: instance_id,
253
+ state: state,
254
+ states: @instance_states,
255
+ mutex: @instance_state_mutex,
256
+ probe_token: probe_token,
257
+ activate: method(:activate_weight_snapshot),
258
+ activation_sequence: ->(tracked) { tracked.fetch(:sequence) + 1 }
236
259
  )
260
+ return unless activated
261
+
262
+ updated = update_tracked_instance(instance_id, state) do
263
+ state[:last_probe_outcome] = :success
264
+ end
265
+ return unless updated
266
+
237
267
  write_instance_health(
238
268
  config_name: state[:name], available: true, reason: 'startup readiness succeeded',
239
269
  probe_outcome: :success, source: :startup_readiness,
@@ -242,11 +272,15 @@ module Legion
242
272
  end
243
273
 
244
274
  def report_initial_failure(instance_id:, state:, probe_token:, reason:)
245
- state[:last_probe_outcome] = :failure
246
275
  publisher.readiness_failed(
247
276
  instance_id: instance_id, physical_id: state[:physical_id],
248
277
  probe_token: probe_token, reason: reason
249
278
  )
279
+ updated = update_tracked_instance(instance_id, state) do
280
+ state[:last_probe_outcome] = :failure
281
+ end
282
+ return unless updated
283
+
250
284
  write_instance_health(
251
285
  config_name: state[:name], available: false, reason: reason,
252
286
  probe_outcome: :failure, source: :startup_readiness
@@ -259,16 +293,10 @@ module Legion
259
293
  instance_key: state[:instance_key]
260
294
  )
261
295
 
262
- if offerings_changed?(previous: state[:offerings], current: new_offerings)
263
- state[:sequence] += 1
264
- publisher.replace_instance_snapshot(
265
- instance_id: instance_id,
266
- physical_id: state[:physical_id],
267
- publisher_token: state[:publisher_token],
268
- offerings: new_offerings,
269
- sequence: state[:sequence]
270
- )
271
- state[:offerings] = new_offerings
296
+ changed = commit_discovered_offerings(
297
+ instance_id: instance_id, state: state, offerings: new_offerings
298
+ )
299
+ if changed && tracked_instance?(instance_id, state)
272
300
  write_instance_health(
273
301
  config_name: state[:name], available: true, reason: 'offerings refreshed',
274
302
  probe_outcome: state[:last_probe_outcome], source: :discovery
@@ -278,17 +306,29 @@ module Legion
278
306
  run_cadence_probe(instance_id: instance_id, state: state)
279
307
  end
280
308
 
309
+ def refresh_unpublished_offerings(instance_id:, state:)
310
+ offerings = discover_offerings_for_instance(
311
+ instance_cfg: state[:instance_cfg], instance_key: state[:instance_key]
312
+ )
313
+ commit_discovered_offerings(instance_id: instance_id, state: state, offerings: offerings)
314
+ end
315
+
281
316
  # -- Removal -----------------------------------------------------------
282
317
 
283
318
  def remove_instance_state(instance_id)
284
- state = @instance_states.delete(instance_id)
319
+ state = @instance_state_mutex.synchronize do
320
+ tracked = @instance_states[instance_id]
321
+ next unless tracked
322
+
323
+ publisher.remove_instance(
324
+ instance_id: instance_id,
325
+ physical_id: tracked[:physical_id],
326
+ publisher_token: tracked[:publisher_token]
327
+ )
328
+ @instance_states.delete(instance_id)
329
+ end
285
330
  return unless state
286
331
 
287
- publisher.remove_instance(
288
- instance_id: instance_id,
289
- physical_id: state[:physical_id],
290
- publisher_token: state[:publisher_token]
291
- )
292
332
  clear_instance_health(config_name: state[:name])
293
333
  rescue StandardError => e
294
334
  handle_exception(e, level: :warn, operation: 'openai.actor.remove_instance',
@@ -311,7 +351,7 @@ module Legion
311
351
  instance_key: instance_key
312
352
  )
313
353
  end
314
- rescue StandardError => e
354
+ rescue Faraday::Error, Legion::JSON::ParseError => e
315
355
  handle_exception(e, level: :warn, operation: 'openai.actor.discover_offerings')
316
356
  []
317
357
  end
@@ -325,11 +365,20 @@ module Legion
325
365
  def build_offering_draft(model_id:, model_data:, instance_cfg:, instance_key:)
326
366
  tier = instance_cfg[:tier] || :frontier
327
367
  operations = infer_operations(model_id: model_id)
368
+ weight_inputs = Legion::Extensions::Llm::Inventory::WeightSchema.weight_inputs(
369
+ settings: Legion::Settings,
370
+ instance_key: instance_key,
371
+ provider_native_key: model_id,
372
+ model: model_id,
373
+ tier: tier
374
+ )
328
375
 
329
376
  Legion::Extensions::Llm::Inventory::OfferingDraft.new(
330
377
  provider_native_key: model_id,
331
378
  model: model_id,
332
379
  tier: tier,
380
+ weight_inputs: weight_inputs,
381
+ base_weight: Legion::Extensions::Llm::Inventory::WeightSchema.base_weight(weight_inputs),
333
382
  operation_evidence: build_operation_evidence(operations: operations),
334
383
  capability_evidence: build_capability_evidence(model_id: model_id),
335
384
  context_evidence: build_context_evidence(model_id: model_id, model_data: model_data),
@@ -8,6 +8,12 @@ module Legion
8
8
  # DiscoveryRefresh. Extracted to keep the main class and the evidence
9
9
  # module within Metrics limits.
10
10
  module DiscoveryDrafts
11
+ EVIDENCE_MAP_FIELDS = %i[operation_evidence capability_evidence].freeze
12
+ VALUE_EVIDENCE_FIELDS = %i[
13
+ context_evidence max_output_evidence embedding_dimensions_evidence
14
+ model_revision_evidence tokenizer_evidence
15
+ ].freeze
16
+
11
17
  private
12
18
 
13
19
  # -- Operation inference -----------------------------------------------
@@ -55,56 +61,31 @@ module Legion
55
61
  # -- Offerings change detection (D3) -----------------------------------
56
62
  # Time.now observed_at stamps in the evidence poison Data#==, so a
57
63
  # rebuilt-but-unchanged catalog would otherwise replace the snapshot
58
- # every tick. Compare identity/status fields only.
64
+ # every tick. Every other OfferingDraft field is authoritative.
59
65
 
60
66
  def offerings_changed?(previous:, current:)
61
- return true unless previous.size == current.size
62
-
63
- # Changed when any draft has no stable counterpart in the
64
- # previous catalog (size mismatch or an identity/status diff).
65
- current.any? do |draft|
66
- previous.none? { |candidate| drafts_stable?(candidate, draft) }
67
- end
68
- end
69
-
70
- def drafts_stable?(candidate, draft)
71
- basic_fields_stable?(candidate, draft) &&
72
- evidence_maps_stable?(candidate.operation_evidence, draft.operation_evidence) &&
73
- evidence_maps_stable?(candidate.capability_evidence, draft.capability_evidence) &&
74
- value_fields_stable?(candidate, draft) &&
75
- candidate.quota_domains == draft.quota_domains &&
76
- candidate.metadata == draft.metadata
67
+ offering_multiset(previous) != offering_multiset(current)
77
68
  end
78
69
 
79
- def basic_fields_stable?(candidate, draft)
80
- candidate.model == draft.model &&
81
- candidate.tier == draft.tier &&
82
- evidence_keys_stable?(candidate.operation_evidence, draft.operation_evidence) &&
83
- evidence_keys_stable?(candidate.capability_evidence, draft.capability_evidence)
70
+ def offering_multiset(offerings)
71
+ offerings.map { |draft| stable_draft_state(draft) }.tally
84
72
  end
85
73
 
86
- def evidence_keys_stable?(previous_map, current_map)
87
- previous_map.keys.sort == current_map.keys.sort
88
- end
89
-
90
- def evidence_maps_stable?(previous_map, current_map)
91
- previous_map.all? do |key, evidence|
92
- other = current_map[key]
93
- other&.status == evidence.status && other&.source == evidence.source
74
+ def stable_draft_state(draft)
75
+ state = draft.to_h
76
+ EVIDENCE_MAP_FIELDS.each do |field|
77
+ state[field] = state.fetch(field).transform_values do |evidence|
78
+ stable_evidence_state(evidence)
79
+ end
94
80
  end
81
+ VALUE_EVIDENCE_FIELDS.each do |field|
82
+ state[field] = stable_evidence_state(state.fetch(field))
83
+ end
84
+ state
95
85
  end
96
86
 
97
- def value_fields_stable?(candidate, draft)
98
- values_stable?(candidate.context_evidence, draft.context_evidence) &&
99
- values_stable?(candidate.max_output_evidence, draft.max_output_evidence) &&
100
- values_stable?(candidate.embedding_dimensions_evidence, draft.embedding_dimensions_evidence) &&
101
- values_stable?(candidate.model_revision_evidence, draft.model_revision_evidence)
102
- end
103
-
104
- def values_stable?(previous_value, current_value)
105
- previous_value.status == current_value.status &&
106
- previous_value.value == current_value.value &&
107
- previous_value.source == current_value.source
87
+ def stable_evidence_state(evidence)
88
+ evidence.to_h.except(:observed_at)
108
89
  end
109
90
  end
110
91
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Llm
6
+ module Openai
7
+ # Applies cadence/reactive readiness results after provider I/O.
8
+ module DiscoveryProbeReporting
9
+ private
10
+
11
+ def report_probe_result(instance_id:, probe_token:, readiness:, state:)
12
+ if readiness.ready?
13
+ report_probe_success(instance_id: instance_id, probe_token: probe_token, state: state)
14
+ else
15
+ report_probe_failure(
16
+ instance_id: instance_id, probe_token: probe_token, readiness: readiness, state: state
17
+ )
18
+ end
19
+ end
20
+
21
+ def report_probe_success(instance_id:, probe_token:, state:)
22
+ publisher.readiness_succeeded(
23
+ instance_id: instance_id, physical_id: state[:physical_id], probe_token: probe_token
24
+ )
25
+ updated = update_tracked_instance(instance_id, state) do
26
+ state[:last_probe_outcome] = :success
27
+ end
28
+ return unless updated
29
+
30
+ write_instance_health(
31
+ config_name: state[:name], available: true, reason: 'readiness probe succeeded',
32
+ probe_outcome: :success, source: :readiness_probe
33
+ )
34
+ end
35
+
36
+ def report_probe_failure(instance_id:, probe_token:, readiness:, state:)
37
+ publisher.readiness_failed(
38
+ instance_id: instance_id, physical_id: state[:physical_id],
39
+ probe_token: probe_token, reason: readiness.reason
40
+ )
41
+ updated = update_tracked_instance(instance_id, state) do
42
+ state[:last_probe_outcome] = :failure
43
+ end
44
+ return unless updated
45
+
46
+ write_instance_health(
47
+ config_name: state[:name], available: false, reason: readiness.reason,
48
+ probe_outcome: :failure, source: :readiness_probe
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -68,7 +68,7 @@ module Legion
68
68
  end
69
69
 
70
70
  def handle_reactive_probe(instance_id:, request:)
71
- state = @instance_states[instance_id]
71
+ state = tracked_instance_state(instance_id)
72
72
  return unless state
73
73
 
74
74
  coordinator = state[:probe_coordinator]
@@ -95,29 +95,6 @@ module Legion
95
95
  instance_id: instance_id)
96
96
  end
97
97
 
98
- def report_probe_result(instance_id:, probe_token:, readiness:, state:)
99
- if readiness.ready?
100
- publisher.readiness_succeeded(
101
- instance_id: instance_id, physical_id: state[:physical_id], probe_token: probe_token
102
- )
103
- state[:last_probe_outcome] = :success
104
- write_instance_health(
105
- config_name: state[:name], available: true, reason: 'readiness probe succeeded',
106
- probe_outcome: :success, source: :readiness_probe
107
- )
108
- else
109
- publisher.readiness_failed(
110
- instance_id: instance_id, physical_id: state[:physical_id],
111
- probe_token: probe_token, reason: readiness.reason
112
- )
113
- state[:last_probe_outcome] = :failure
114
- write_instance_health(
115
- config_name: state[:name], available: false, reason: readiness.reason,
116
- probe_outcome: :failure, source: :readiness_probe
117
- )
118
- end
119
- end
120
-
121
98
  def build_probe_enqueue(instance_id:)
122
99
  proc do |request:|
123
100
  handle_reactive_probe(instance_id: instance_id, request: request)
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/llm/inventory/weight_reconciler'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Llm
8
+ module Openai
9
+ # Binds the shared write-time weight reconciler to OpenAI's existing
10
+ # periodic discovery actor. Settings are read only during ordinary
11
+ # actor passes; this module never registers reload/reset callbacks.
12
+ module DiscoveryWeightPublication
13
+ private
14
+
15
+ def initialize_weight_publication
16
+ @instance_states ||= {}
17
+ @instance_state_mutex ||= Mutex.new
18
+ dormant_weight_tracker
19
+ end
20
+
21
+ def dormant_weight_tracker
22
+ @dormant_weight_tracker ||= Legion::Extensions::Llm::Inventory::DormantWeightTracker.new
23
+ end
24
+
25
+ def commit_discovered_offerings(instance_id:, state:, offerings:)
26
+ Legion::Extensions::Llm::Inventory::WeightReconciler.commit_if_changed!(
27
+ settings: Legion::Settings,
28
+ instance_id: instance_id,
29
+ state: state,
30
+ discovered_offerings: offerings,
31
+ mutex: @instance_state_mutex,
32
+ equivalent: lambda do |previous, current|
33
+ !offerings_changed?(previous: previous, current: current)
34
+ end,
35
+ replace: method(:replace_weight_snapshot)
36
+ )
37
+ end
38
+
39
+ def replace_weight_snapshot(instance_id:, state:, offerings:, sequence:)
40
+ publisher.replace_instance_snapshot(
41
+ instance_id: instance_id,
42
+ physical_id: state.fetch(:physical_id),
43
+ publisher_token: state.fetch(:publisher_token),
44
+ offerings: offerings,
45
+ sequence: sequence
46
+ )
47
+ end
48
+
49
+ def activate_weight_snapshot(instance_id:, state:, offerings:, sequence:, probe_token:)
50
+ publisher.activate_instance_snapshot(
51
+ instance_id: instance_id,
52
+ physical_id: state.fetch(:physical_id),
53
+ publisher_token: state.fetch(:publisher_token),
54
+ offerings: offerings,
55
+ sequence: sequence,
56
+ probe_token: probe_token
57
+ )
58
+ end
59
+
60
+ def tracked_instance_state(instance_id)
61
+ return unless @instance_states && @instance_state_mutex
62
+
63
+ @instance_state_mutex.synchronize { @instance_states[instance_id] }
64
+ end
65
+
66
+ def instance_states_snapshot
67
+ @instance_state_mutex.synchronize { @instance_states.each_pair.to_h }
68
+ end
69
+
70
+ def update_tracked_instance(instance_id, state)
71
+ @instance_state_mutex.synchronize do
72
+ return false unless @instance_states[instance_id].equal?(state)
73
+
74
+ yield
75
+ true
76
+ end
77
+ end
78
+
79
+ def tracked_instance?(instance_id, state)
80
+ tracked_instance_state(instance_id).equal?(state)
81
+ end
82
+
83
+ def observe_dormant_weights
84
+ Legion::Extensions::Llm::Inventory::WeightReconciler.observe_dormant!(
85
+ settings: Legion::Settings,
86
+ provider_family: :openai,
87
+ states: @instance_states,
88
+ mutex: @instance_state_mutex,
89
+ tracker: dormant_weight_tracker,
90
+ dormant_logger: lambda do |key|
91
+ log.info do
92
+ "[llm][openai] action=dormant_weight weight_key=#{key.inspect} no_lane_published=true"
93
+ end
94
+ end
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Openai
7
- VERSION = '0.6.3'
7
+ VERSION = '0.6.4'
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-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.4
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.7.1
74
+ version: 0.7.6
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.7.1
81
+ version: 0.7.6
82
82
  description: OpenAI provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com
@@ -103,8 +103,10 @@ files:
103
103
  - lib/legion/extensions/llm/openai/discovery_evidence_builders.rb
104
104
  - lib/legion/extensions/llm/openai/discovery_health_display.rb
105
105
  - lib/legion/extensions/llm/openai/discovery_identity.rb
106
+ - lib/legion/extensions/llm/openai/discovery_probe_reporting.rb
106
107
  - lib/legion/extensions/llm/openai/discovery_probing.rb
107
108
  - lib/legion/extensions/llm/openai/discovery_transport.rb
109
+ - lib/legion/extensions/llm/openai/discovery_weight_publication.rb
108
110
  - lib/legion/extensions/llm/openai/instance_discovery.rb
109
111
  - lib/legion/extensions/llm/openai/openai_callable.rb
110
112
  - lib/legion/extensions/llm/openai/provider.rb