lex-llm-azure-foundry 0.3.4 → 0.3.5
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49e5d8dc9a03ec7e353324aaf22050bf23a28ff085da22a8f779c7b01b28ffe0
|
|
4
|
+
data.tar.gz: 3c8c342a9bc24b2133a2060f9cc9dc9a9743d4d69bda1205e26db02f6d5a4f59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba398b8cdb7dcd17fa6138c9edccc482b66abb201c8ce198d4373d83ced0de5f8c1c756be4b55ec3b3a64921278db4d3b6673d146a77ed2121d6e7317622be79
|
|
7
|
+
data.tar.gz: d1c2877ae02270b74dba8be1daa23c9c676070c4d705194fa35933d31206d3c9bd5eccb0d2837c80ffe2fe278153766f0865d1e53d077cd4b9e84ce67c5fdef0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.5] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Write-time SSOT lane weights** — Build Azure deployment drafts with the shared
|
|
7
|
+
four-component weight pair using the deployment name as the provider-native
|
|
8
|
+
offering identity, and reconcile weight-only changes atomically on the existing
|
|
9
|
+
discovery cadence. Initial/recovery activation now rebuilds from current settings,
|
|
10
|
+
removal wins readiness races, and dormant configured weights log once per absence
|
|
11
|
+
period without any Settings lifecycle callback.
|
|
12
|
+
- **Callable-path system conformance** — Capture the rendered Azure/OpenAI-compatible
|
|
13
|
+
HTTP payload and verify a folded leading system message remains in the dialect-native
|
|
14
|
+
`role: system` message position.
|
|
15
|
+
- **Dependency floor** — Raise `lex-llm` to `>= 0.7.6` for `WeightSchema`,
|
|
16
|
+
`WeightReconciler`, and the immutable record weight pair. The `legion-settings`
|
|
17
|
+
dependency and provider/operator workflow are unchanged.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Malformed-weight startup cleanup** — Validate and build weighted offering
|
|
21
|
+
drafts before constructing or claiming any callable scope, so invalid weight
|
|
22
|
+
values cannot leave an orphaned initializing Registry publication. A later
|
|
23
|
+
corrected discovery pass claims and activates the instance normally without
|
|
24
|
+
requiring a restart or operator cleanup.
|
|
25
|
+
- **Complete offering replacement comparison** — Ordinary discovery now compares
|
|
26
|
+
every authoritative `OfferingDraft` field while ignoring only evidence
|
|
27
|
+
observation timestamps. Deployment order does not churn, duplicate counts stay
|
|
28
|
+
significant, and native-key, evidence, quota, metadata, publication-source,
|
|
29
|
+
tier, or weight drift advances the Registry snapshot exactly once.
|
|
30
|
+
|
|
3
31
|
## [0.3.4] - 2026-08-18
|
|
4
32
|
|
|
5
33
|
### Fixed
|
|
@@ -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.
|
|
30
|
+
spec.add_dependency 'lex-llm', '>= 0.7.6'
|
|
31
31
|
end
|
|
@@ -11,6 +11,7 @@ require 'legion/extensions/llm/inventory/records'
|
|
|
11
11
|
require 'legion/extensions/llm/inventory/evidence'
|
|
12
12
|
require 'legion/extensions/llm/inventory/probe_coordinator'
|
|
13
13
|
require 'legion/extensions/llm/inventory/scoped_refresher'
|
|
14
|
+
require 'legion/extensions/llm/inventory/weight_reconciler'
|
|
14
15
|
require 'legion/extensions/llm/routing/provider_outcome'
|
|
15
16
|
require 'legion/extensions/llm/taxonomies'
|
|
16
17
|
require 'legion/extensions/llm/capabilities'
|
|
@@ -257,18 +258,34 @@ module Legion
|
|
|
257
258
|
|
|
258
259
|
Legion::Extensions::Llm::Inventory::OfferingDraft.new(
|
|
259
260
|
provider_native_key: deployment_name, model: model_id, tier: tier,
|
|
261
|
+
**offering_evidence_fields(deployment: deployment, instance_cfg: instance_cfg, embed: embed),
|
|
262
|
+
quota_domains: build_quota_domains(deployment_name: deployment_name),
|
|
263
|
+
metadata: build_offering_metadata(deployment: deployment, deployment_name: deployment_name,
|
|
264
|
+
instance_key: instance_key).freeze,
|
|
265
|
+
publication_source: :provider_static_catalog,
|
|
266
|
+
**weight_fields(instance_key: instance_key, deployment_name: deployment_name,
|
|
267
|
+
model_id: model_id, tier: tier)
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def weight_fields(instance_key:, deployment_name:, model_id:, tier:)
|
|
272
|
+
inputs = Legion::Extensions::Llm::Inventory::WeightSchema.weight_inputs(
|
|
273
|
+
settings: Legion::Settings, instance_key: instance_key,
|
|
274
|
+
provider_native_key: deployment_name, model: model_id, tier: tier
|
|
275
|
+
)
|
|
276
|
+
{ weight_inputs: inputs, base_weight: Legion::Extensions::Llm::Inventory::WeightSchema.base_weight(inputs) }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def offering_evidence_fields(deployment:, instance_cfg:, embed:)
|
|
280
|
+
{
|
|
260
281
|
operation_evidence: build_operation_evidence(embed_supported: embed),
|
|
261
282
|
capability_evidence: build_capability_evidence(deployment: deployment, embed_supported: embed),
|
|
262
283
|
context_evidence: build_context_evidence(deployment: deployment, instance_cfg: instance_cfg),
|
|
263
284
|
max_output_evidence: build_max_output_evidence(deployment: deployment, instance_cfg: instance_cfg),
|
|
264
285
|
embedding_dimensions_evidence: absent_value_evidence,
|
|
265
286
|
model_revision_evidence: absent_value_evidence,
|
|
266
|
-
tokenizer_evidence: absent_value_evidence
|
|
267
|
-
|
|
268
|
-
metadata: build_offering_metadata(deployment: deployment, deployment_name: deployment_name,
|
|
269
|
-
instance_key: instance_key).freeze,
|
|
270
|
-
publication_source: :provider_static_catalog
|
|
271
|
-
)
|
|
287
|
+
tokenizer_evidence: absent_value_evidence
|
|
288
|
+
}
|
|
272
289
|
end
|
|
273
290
|
|
|
274
291
|
def build_context_evidence(deployment:, instance_cfg:)
|
|
@@ -311,59 +328,47 @@ module Legion
|
|
|
311
328
|
end
|
|
312
329
|
end
|
|
313
330
|
|
|
314
|
-
#
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
#
|
|
331
|
+
# Complete OfferingDraft comparison. Evidence observation timestamps
|
|
332
|
+
# are telemetry-only; every other draft and evidence field remains
|
|
333
|
+
# authoritative. Catalog order is set-like and duplicate counts remain
|
|
334
|
+
# significant.
|
|
318
335
|
module OfferingComparisonHelpers
|
|
336
|
+
SCALAR_EVIDENCE_FIELDS = %i[
|
|
337
|
+
context_evidence max_output_evidence embedding_dimensions_evidence
|
|
338
|
+
model_revision_evidence tokenizer_evidence
|
|
339
|
+
].freeze
|
|
340
|
+
|
|
319
341
|
private
|
|
320
342
|
|
|
321
343
|
def offerings_changed?(previous:, current:)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
current.any? do |draft|
|
|
325
|
-
previous.none? { |candidate| drafts_stable?(candidate, draft) }
|
|
326
|
-
end
|
|
344
|
+
offerings_contract(previous) != offerings_contract(current)
|
|
327
345
|
end
|
|
328
346
|
|
|
329
|
-
def
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def basic_fields_stable?(candidate, draft)
|
|
339
|
-
candidate.model == draft.model &&
|
|
340
|
-
candidate.tier == draft.tier &&
|
|
341
|
-
evidence_key_set_stable?(candidate.operation_evidence, draft.operation_evidence) &&
|
|
342
|
-
evidence_key_set_stable?(candidate.capability_evidence, draft.capability_evidence)
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def evidence_key_set_stable?(previous_map, current_map)
|
|
346
|
-
previous_map.keys.sort == current_map.keys.sort
|
|
347
|
+
def offerings_contract(offerings)
|
|
348
|
+
contracts = offerings.map { |draft| stable_offering_contract(draft) }
|
|
349
|
+
grouped = contracts.group_by do |contract|
|
|
350
|
+
[contract.fetch(:provider_native_key), contract.fetch(:model)]
|
|
351
|
+
end
|
|
352
|
+
grouped.sort_by { |identity, _| identity.map { |value| value.to_s.b } }
|
|
353
|
+
.to_h.transform_values(&:tally)
|
|
347
354
|
end
|
|
348
355
|
|
|
349
|
-
def
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
356
|
+
def stable_offering_contract(draft)
|
|
357
|
+
contract = draft.to_h
|
|
358
|
+
contract[:operation_evidence] = stable_evidence_map(contract.fetch(:operation_evidence))
|
|
359
|
+
contract[:capability_evidence] = stable_evidence_map(contract.fetch(:capability_evidence))
|
|
360
|
+
SCALAR_EVIDENCE_FIELDS.each do |field|
|
|
361
|
+
contract[field] = stable_evidence(contract.fetch(field))
|
|
353
362
|
end
|
|
363
|
+
contract
|
|
354
364
|
end
|
|
355
365
|
|
|
356
|
-
def
|
|
357
|
-
|
|
358
|
-
values_stable?(candidate.max_output_evidence, draft.max_output_evidence) &&
|
|
359
|
-
values_stable?(candidate.embedding_dimensions_evidence, draft.embedding_dimensions_evidence) &&
|
|
360
|
-
values_stable?(candidate.model_revision_evidence, draft.model_revision_evidence)
|
|
366
|
+
def stable_evidence_map(evidence_by_key)
|
|
367
|
+
evidence_by_key.transform_values { |evidence| stable_evidence(evidence) }
|
|
361
368
|
end
|
|
362
369
|
|
|
363
|
-
def
|
|
364
|
-
|
|
365
|
-
previous_value.value == current_value.value &&
|
|
366
|
-
previous_value.source == current_value.source
|
|
370
|
+
def stable_evidence(evidence)
|
|
371
|
+
evidence.to_h.except(:observed_at)
|
|
367
372
|
end
|
|
368
373
|
end
|
|
369
374
|
|
|
@@ -460,7 +465,7 @@ module Legion
|
|
|
460
465
|
end
|
|
461
466
|
|
|
462
467
|
def handle_reactive_probe(instance_id:, request:)
|
|
463
|
-
state =
|
|
468
|
+
state = tracked_instance_state(instance_id)
|
|
464
469
|
return unless state
|
|
465
470
|
|
|
466
471
|
coordinator = state[:probe_coordinator]
|
|
@@ -481,6 +486,12 @@ module Legion
|
|
|
481
486
|
instance_id: instance_id)
|
|
482
487
|
end
|
|
483
488
|
|
|
489
|
+
def tracked_instance_state(instance_id)
|
|
490
|
+
return unless @instance_states
|
|
491
|
+
|
|
492
|
+
state_mutex.synchronize { @instance_states[instance_id] }
|
|
493
|
+
end
|
|
494
|
+
|
|
484
495
|
def finish_probe_safely(coordinator:, request: nil)
|
|
485
496
|
args = request ? { request: request } : {}
|
|
486
497
|
coordinator&.finish_probe(**args)
|
|
@@ -528,17 +539,29 @@ module Legion
|
|
|
528
539
|
def remove_all_instances
|
|
529
540
|
return unless @instance_states
|
|
530
541
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
instance_id: instance_id, publisher_token: state[:publisher_token],
|
|
534
|
-
physical_id: state[:instance_key].physical_id
|
|
535
|
-
)
|
|
536
|
-
clear_instance_health(config_name: state[:name])
|
|
542
|
+
drain_instance_states.each do |instance_id, state|
|
|
543
|
+
remove_published_instance(instance_id: instance_id, state: state)
|
|
537
544
|
rescue StandardError => e
|
|
538
545
|
handle_exception(e, level: :warn, operation: 'azure_foundry.actor.remove_instance',
|
|
539
546
|
instance_id: instance_id)
|
|
540
547
|
end
|
|
541
|
-
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def drain_instance_states
|
|
551
|
+
state_mutex.synchronize do
|
|
552
|
+
tracked = @instance_states.each_pair.map { |instance_id, state| [instance_id, state] }
|
|
553
|
+
@instance_states.clear
|
|
554
|
+
dormant_weight_tracker.clear!
|
|
555
|
+
tracked
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def remove_published_instance(instance_id:, state:)
|
|
560
|
+
publisher.remove_instance(
|
|
561
|
+
instance_id: instance_id, publisher_token: state[:publisher_token],
|
|
562
|
+
physical_id: state[:instance_key].physical_id
|
|
563
|
+
)
|
|
564
|
+
clear_instance_health(config_name: state[:name])
|
|
542
565
|
end
|
|
543
566
|
end
|
|
544
567
|
|
|
@@ -562,12 +585,15 @@ module Legion
|
|
|
562
585
|
# enable_*) by. The derived endpoint id is the secondary
|
|
563
586
|
# physical field only.
|
|
564
587
|
instance_id = name.to_s
|
|
565
|
-
next if @instance_states.key?(instance_id)
|
|
588
|
+
next if state_mutex.synchronize { @instance_states.key?(instance_id) }
|
|
566
589
|
|
|
567
590
|
physical_id = derive_physical_id(instance_cfg: instance_cfg)
|
|
568
|
-
|
|
591
|
+
state = build_instance_context(
|
|
569
592
|
name: name, instance_id: instance_id, physical_id: physical_id, instance_cfg: instance_cfg
|
|
570
593
|
)
|
|
594
|
+
Legion::Extensions::Llm::Inventory::WeightReconciler.track_initializing!(
|
|
595
|
+
states: @instance_states, state_key: instance_id, state: state, mutex: state_mutex
|
|
596
|
+
)
|
|
571
597
|
rescue StandardError => e
|
|
572
598
|
handle_exception(e, level: :warn, operation: 'azure_foundry.actor.claim_instance',
|
|
573
599
|
instance_name: name.to_s)
|
|
@@ -576,11 +602,13 @@ module Legion
|
|
|
576
602
|
|
|
577
603
|
def release_removed_instances(discovered)
|
|
578
604
|
discovered_names = discovered.keys.map(&:to_s)
|
|
579
|
-
|
|
605
|
+
existing_names = state_mutex.synchronize { @instance_states.keys }
|
|
606
|
+
(existing_names - discovered_names).each { |instance_id| remove_instance_state(instance_id) }
|
|
580
607
|
end
|
|
581
608
|
|
|
582
609
|
def build_instance_context(name:, instance_id:, physical_id:, instance_cfg:)
|
|
583
610
|
instance_key = build_instance_key(instance_id: instance_id, physical_id: physical_id)
|
|
611
|
+
offerings = discover_offerings_for_instance(instance_cfg: instance_cfg, instance_key: instance_key)
|
|
584
612
|
callable = Legion::Extensions::Llm::AzureFoundry::Actor::AzureFoundryCallable.new(
|
|
585
613
|
instance_cfg: instance_cfg, logger: log
|
|
586
614
|
)
|
|
@@ -594,7 +622,7 @@ module Legion
|
|
|
594
622
|
name: name, instance_key: instance_key, instance_cfg: instance_cfg,
|
|
595
623
|
callable: callable, probe_coordinator: probe_coordinator,
|
|
596
624
|
publisher_token: publisher_token, sequence: 0, last_probe_outcome: nil,
|
|
597
|
-
offerings:
|
|
625
|
+
offerings: offerings
|
|
598
626
|
}
|
|
599
627
|
end
|
|
600
628
|
|
|
@@ -605,7 +633,7 @@ module Legion
|
|
|
605
633
|
end
|
|
606
634
|
|
|
607
635
|
def remove_instance_state(instance_id)
|
|
608
|
-
state = @instance_states.delete(instance_id)
|
|
636
|
+
state = state_mutex.synchronize { @instance_states.delete(instance_id) }
|
|
609
637
|
return unless state
|
|
610
638
|
|
|
611
639
|
publisher.remove_instance(
|
|
@@ -619,48 +647,54 @@ module Legion
|
|
|
619
647
|
end
|
|
620
648
|
end
|
|
621
649
|
|
|
622
|
-
#
|
|
623
|
-
#
|
|
624
|
-
module
|
|
650
|
+
# Weight publication adapters bind Azure Foundry's existing publisher
|
|
651
|
+
# kwargs and stable comparison to the shared atomic reconciler.
|
|
652
|
+
module WeightPublicationHelpers
|
|
625
653
|
private
|
|
626
654
|
|
|
627
|
-
def
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
655
|
+
def refresh_cached_offerings(instance_id:, state:)
|
|
656
|
+
offerings = discover_offerings_for_instance(
|
|
657
|
+
instance_cfg: state[:instance_cfg], instance_key: state[:instance_key]
|
|
658
|
+
)
|
|
659
|
+
commit_weight_snapshot(instance_id: instance_id, state: state, offerings: offerings)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def commit_weight_snapshot(instance_id:, state:, offerings:)
|
|
663
|
+
Legion::Extensions::Llm::Inventory::WeightReconciler.commit_if_changed!(
|
|
664
|
+
settings: Legion::Settings,
|
|
665
|
+
instance_id: instance_id,
|
|
666
|
+
state: state,
|
|
667
|
+
discovered_offerings: offerings,
|
|
668
|
+
mutex: state_mutex,
|
|
669
|
+
equivalent: lambda do |previous, current|
|
|
670
|
+
!offerings_changed?(previous: previous, current: current)
|
|
671
|
+
end,
|
|
672
|
+
replace: method(:replace_weight_snapshot)
|
|
673
|
+
)
|
|
635
674
|
end
|
|
636
675
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
release_removed_instances(discovered)
|
|
676
|
+
def replace_weight_snapshot(instance_id:, state:, offerings:, sequence:)
|
|
677
|
+
publisher.replace_instance_snapshot(
|
|
678
|
+
instance_id: instance_id, publisher_token: state[:publisher_token],
|
|
679
|
+
offerings: offerings, sequence: sequence,
|
|
680
|
+
physical_id: state[:instance_key].physical_id
|
|
681
|
+
)
|
|
644
682
|
end
|
|
645
683
|
|
|
646
|
-
def
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
run_initialization_probe(instance_id: instance_id, state: state)
|
|
653
|
-
else
|
|
654
|
-
refresh_activated_instance(instance_id: instance_id, state: state)
|
|
655
|
-
end
|
|
684
|
+
def activate_weight_snapshot(instance_id:, state:, offerings:, sequence:, probe_token:)
|
|
685
|
+
publisher.activate_instance_snapshot(
|
|
686
|
+
instance_id: instance_id, publisher_token: state[:publisher_token],
|
|
687
|
+
offerings: offerings, sequence: sequence, probe_token: probe_token,
|
|
688
|
+
physical_id: state[:instance_key].physical_id
|
|
689
|
+
)
|
|
656
690
|
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Initial and recovery readiness share the tracked-state activation
|
|
694
|
+
# gate, so removal wins without resurrecting an instance.
|
|
695
|
+
module InitialReadinessHelpers
|
|
696
|
+
private
|
|
657
697
|
|
|
658
|
-
# D4: an instance whose initial readiness failed stays :initializing.
|
|
659
|
-
# activate_instance_snapshot is the only transition legal from
|
|
660
|
-
# :initializing, so a later passing probe re-activates the claim
|
|
661
|
-
# (fresh probe token, current offerings, next sequence) instead of
|
|
662
|
-
# calling replace/readiness_succeeded, which would raise
|
|
663
|
-
# InvalidTransitionError.
|
|
664
698
|
def run_initialization_probe(instance_id:, state:)
|
|
665
699
|
coordinator = state[:probe_coordinator]
|
|
666
700
|
return unless coordinator.begin_probe
|
|
@@ -683,7 +717,7 @@ module Legion
|
|
|
683
717
|
|
|
684
718
|
def apply_initial_readiness(instance_id:, state:, probe_token:, readiness:)
|
|
685
719
|
if readiness.ready?
|
|
686
|
-
activate_after_readiness(instance_id: instance_id, state: state, probe_token: probe_token)
|
|
720
|
+
activate_after_readiness?(instance_id: instance_id, state: state, probe_token: probe_token)
|
|
687
721
|
else
|
|
688
722
|
report_initial_failure(
|
|
689
723
|
instance_id: instance_id, state: state, probe_token: probe_token, reason: readiness.reason
|
|
@@ -691,19 +725,27 @@ module Legion
|
|
|
691
725
|
end
|
|
692
726
|
end
|
|
693
727
|
|
|
694
|
-
def activate_after_readiness(instance_id:, state:, probe_token:)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
728
|
+
def activate_after_readiness?(instance_id:, state:, probe_token:)
|
|
729
|
+
activated = Legion::Extensions::Llm::Inventory::WeightReconciler.activate_tracked!(
|
|
730
|
+
settings: Legion::Settings,
|
|
731
|
+
instance_id: instance_id,
|
|
732
|
+
state_key: instance_id,
|
|
733
|
+
state: state,
|
|
734
|
+
states: @instance_states,
|
|
735
|
+
mutex: state_mutex,
|
|
736
|
+
probe_token: probe_token,
|
|
737
|
+
activate: method(:activate_weight_snapshot),
|
|
738
|
+
activation_sequence: ->(tracked) { tracked.fetch(:sequence) + 1 }
|
|
701
739
|
)
|
|
740
|
+
return false unless activated
|
|
741
|
+
|
|
742
|
+
state[:last_probe_outcome] = :success
|
|
702
743
|
write_instance_health(
|
|
703
744
|
config_name: state[:name], available: true, reason: 'startup readiness succeeded',
|
|
704
745
|
probe_outcome: :success, source: :startup_readiness,
|
|
705
746
|
capabilities: instance_capabilities(state[:offerings])
|
|
706
747
|
)
|
|
748
|
+
true
|
|
707
749
|
end
|
|
708
750
|
|
|
709
751
|
def report_initial_failure(instance_id:, state:, probe_token:, reason:)
|
|
@@ -717,33 +759,77 @@ module Legion
|
|
|
717
759
|
probe_outcome: :failure, source: :startup_readiness
|
|
718
760
|
)
|
|
719
761
|
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Periodic refresh helpers — reconcile configured instances, recover
|
|
765
|
+
# instances still :initializing, diff offerings, drive cadence probes.
|
|
766
|
+
module RefreshHelpers
|
|
767
|
+
private
|
|
768
|
+
|
|
769
|
+
def tick_refresh
|
|
770
|
+
reconcile_configured_instances
|
|
771
|
+
states = state_mutex.synchronize do
|
|
772
|
+
@instance_states.each_pair.map { |instance_id, state| [instance_id, state] }
|
|
773
|
+
end
|
|
774
|
+
states.each do |instance_id, state|
|
|
775
|
+
refresh_instance(instance_id: instance_id, state: state)
|
|
776
|
+
rescue StandardError => e
|
|
777
|
+
handle_exception(e, level: :warn, operation: 'azure_foundry.actor.refresh_instance',
|
|
778
|
+
instance_id: instance_id)
|
|
779
|
+
end
|
|
780
|
+
Legion::Extensions::Llm::Inventory::WeightReconciler.observe_dormant!(
|
|
781
|
+
settings: Legion::Settings,
|
|
782
|
+
provider_family: :azure_foundry,
|
|
783
|
+
states: @instance_states,
|
|
784
|
+
mutex: state_mutex,
|
|
785
|
+
tracker: dormant_weight_tracker,
|
|
786
|
+
dormant_logger: lambda do |key|
|
|
787
|
+
log.info(
|
|
788
|
+
"[llm][azure_foundry] action=dormant_weight weight_key=#{key.inspect} no_lane_published=true"
|
|
789
|
+
)
|
|
790
|
+
end
|
|
791
|
+
)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Re-scans configured instances every tick so instances configured
|
|
795
|
+
# after boot appear without a restart, and removed instances are
|
|
796
|
+
# released from the registry.
|
|
797
|
+
def reconcile_configured_instances
|
|
798
|
+
discovered = configured_instances
|
|
799
|
+
claim_new_instances(discovered)
|
|
800
|
+
release_removed_instances(discovered)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def refresh_instance(instance_id:, state:)
|
|
804
|
+
status = publisher.snapshot.publication_status(instance_key: state[:instance_key])
|
|
805
|
+
return log.debug { "[azure_foundry] no publication status for #{instance_id}; skipping refresh" } if
|
|
806
|
+
status.nil?
|
|
807
|
+
|
|
808
|
+
if status.state == :initializing
|
|
809
|
+
refresh_cached_offerings(instance_id: instance_id, state: state) unless state[:last_probe_outcome].nil?
|
|
810
|
+
run_initialization_probe(instance_id: instance_id, state: state)
|
|
811
|
+
else
|
|
812
|
+
refresh_activated_instance(instance_id: instance_id, state: state)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
720
815
|
|
|
721
816
|
def refresh_activated_instance(instance_id:, state:)
|
|
722
817
|
new_offerings = discover_offerings_for_instance(
|
|
723
818
|
instance_cfg: state[:instance_cfg], instance_key: state[:instance_key]
|
|
724
819
|
)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
820
|
+
changed = commit_weight_snapshot(
|
|
821
|
+
instance_id: instance_id, state: state, offerings: new_offerings
|
|
822
|
+
)
|
|
823
|
+
if changed
|
|
729
824
|
write_instance_health(
|
|
730
825
|
config_name: state[:name], available: true, reason: 'offerings refreshed',
|
|
731
826
|
probe_outcome: state[:last_probe_outcome], source: :discovery,
|
|
732
|
-
capabilities: instance_capabilities(
|
|
827
|
+
capabilities: instance_capabilities(state[:offerings])
|
|
733
828
|
)
|
|
734
829
|
end
|
|
735
830
|
|
|
736
831
|
run_cadence_probe(instance_id: instance_id, state: state)
|
|
737
832
|
end
|
|
738
|
-
|
|
739
|
-
def replace_instance_offerings(instance_id:, state:, offerings:)
|
|
740
|
-
state[:sequence] += 1
|
|
741
|
-
publisher.replace_instance_snapshot(
|
|
742
|
-
instance_id: instance_id, publisher_token: state[:publisher_token],
|
|
743
|
-
offerings: offerings, sequence: state[:sequence],
|
|
744
|
-
physical_id: state[:instance_key].physical_id
|
|
745
|
-
)
|
|
746
|
-
end
|
|
747
833
|
end
|
|
748
834
|
|
|
749
835
|
# Display-only health + capabilities projection into
|
|
@@ -846,6 +932,8 @@ module Legion
|
|
|
846
932
|
include ProbeHelpers
|
|
847
933
|
include ShutdownHelpers
|
|
848
934
|
include ActivationHelpers
|
|
935
|
+
include WeightPublicationHelpers
|
|
936
|
+
include InitialReadinessHelpers
|
|
849
937
|
include RefreshHelpers
|
|
850
938
|
include DiscoveryHealthDisplay
|
|
851
939
|
|
|
@@ -862,6 +950,8 @@ module Legion
|
|
|
862
950
|
|
|
863
951
|
def manual
|
|
864
952
|
@instance_states ||= Concurrent::Map.new
|
|
953
|
+
state_mutex
|
|
954
|
+
dormant_weight_tracker
|
|
865
955
|
tick_refresh
|
|
866
956
|
rescue StandardError => e
|
|
867
957
|
handle_exception(e, level: :warn, operation: 'azure_foundry.actor.discovery_refresh')
|
|
@@ -875,6 +965,14 @@ module Legion
|
|
|
875
965
|
|
|
876
966
|
private
|
|
877
967
|
|
|
968
|
+
def state_mutex
|
|
969
|
+
@state_mutex ||= Mutex.new
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def dormant_weight_tracker
|
|
973
|
+
@dormant_weight_tracker ||= Legion::Extensions::Llm::Inventory::DormantWeightTracker.new
|
|
974
|
+
end
|
|
975
|
+
|
|
878
976
|
# D9: the tick interval is the registered discovery.interval_seconds.
|
|
879
977
|
# A missing or non-positive value falls back to the registered
|
|
880
978
|
# default — never nil (a nil interval stops the TimerTask after the
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-azure-foundry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
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.
|
|
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.
|
|
81
|
+
version: 0.7.6
|
|
82
82
|
description: Azure AI Foundry and Azure OpenAI hosted provider integration for LegionIO
|
|
83
83
|
LLM routing.
|
|
84
84
|
email:
|