lex-llm-bedrock 0.5.4 → 0.5.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: 0457af083b20a03a506e162791bb8e460483dd05da16c6f208e4411af673315d
4
- data.tar.gz: 13c7ac1b8ecc5737f141dc5c3327baf9068eb955a6d5d57fefc8688939322521
3
+ metadata.gz: 5dff754b7817098396287ff77ec64205f5dea845b43eddd9c73ddb742b8b638c
4
+ data.tar.gz: 754c16f98e51acfcfaee31bf936741616adb92bacb7042956f278d7ff65f9c66
5
5
  SHA512:
6
- metadata.gz: 2334dbb6abcd57924ba64329fc1691fada8190d84acc3e7cb98d6fdc60b4b202e53a57258c356e2385e32da3379f81083dfd1f3ffb307291a2cd208b31e0da11
7
- data.tar.gz: d3d290e0b54de08ae5d2fbf517fe0b5a448faa8d568f849c3dbde381f21b2461fc9aa9c688482181bf1be4ff1e105579392b75eebd43b02ffb86258fa840bbf9
6
+ metadata.gz: 3a674c63e6fd3f99855f89dcbd63319148751ff4dea85596a1cf6024a90d5de015ba35eb8c7bff563143f40608ced83dbda1196794569560c7df79337c545e17
7
+ data.tar.gz: 645ed19662a6b3097202b494a4ef941f08b7fdc39d8af8feed201fa13b341697736f0b517294b0980d0bad7009dc81f60e6f3ad95abf2987cc4ed36971c1c064
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.5] - 2026-08-19
4
+
5
+ ### Added
6
+ - **Callable-path system conformance** — The production `BedrockCallable#chat` path now has a rendered-wire fixture proving a folded leading system message reaches the AWS Converse payload's native `system` field.
7
+
8
+ ### Changed
9
+ - **Write-time SSOT lane weights** — Bedrock discovery computes the shared four-component weight pair for every offering, reconciles weight-only changes on the existing `Actors::Every` cadence, and publishes immutable weighted snapshots without adding a Settings callback, reload path, restart, migration, or operator step.
10
+ - **Atomic writer lifecycle** — Initial and recovery activation, ordinary replacement, state removal, sequence allocation, cached offerings, and dormant-weight tracking now share one actor mutex. Initializing states never replace, failed publisher calls leave the cache retryable, and late readiness cannot resurrect a removed state.
11
+ - **Dependency floor** — Requires `lex-llm >= 0.7.6` for `WeightSchema`, weighted Inventory records, `WeightReconciler`, and `DormantWeightTracker`; the existing `legion-settings >= 1.4.2` floor is unchanged.
12
+
13
+ ### Fixed
14
+ - **Startup validation cannot orphan an initializing claim** — Initial offering construction and write-time weight validation now finish before Bedrock creates a callable or probe coordinator and before Inventory issues a publisher token. A malformed weight leaves no Registry status, callable, token, or actor state; the next corrected cadence claims and activates normally.
15
+ - **Ordinary discovery compares the complete offering contract** — Replacement detection now retains every authoritative `OfferingDraft` field and ignores only evidence observation timestamps. Catalog order is set-like while duplicate multiplicity remains visible, so metadata, quota, evidence-source/value, publication-source, native-key, capability, operation, tier, or weight drift publishes exactly once without churn from reorder-only discovery.
16
+
3
17
  ## [0.5.4] - 2026-08-18
4
18
 
5
19
  ### Fixed
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency 'legion-logging', '>= 1.3.2'
30
30
  spec.add_dependency 'legion-settings', '>= 1.4.2'
31
31
  spec.add_dependency 'legion-transport', '>= 1.4.14'
32
- spec.add_dependency 'lex-llm', '>= 0.7.1'
32
+ spec.add_dependency 'lex-llm', '>= 0.7.6'
33
33
  end
@@ -18,6 +18,7 @@ require 'legion/extensions/llm/inventory/records'
18
18
  require 'legion/extensions/llm/inventory/evidence'
19
19
  require 'legion/extensions/llm/inventory/probe_coordinator'
20
20
  require 'legion/extensions/llm/inventory/scoped_refresher'
21
+ require 'legion/extensions/llm/inventory/weight_reconciler'
21
22
  require 'legion/extensions/llm/taxonomies'
22
23
  require 'legion/extensions/llm/capabilities'
23
24
 
@@ -225,6 +226,13 @@ module Legion
225
226
  input_mods: input_mods, output_mods: output_mods,
226
227
  streaming_supported: streaming, model_id: model_id
227
228
  )
229
+ weight_inputs = Legion::Extensions::Llm::Inventory::WeightSchema.weight_inputs(
230
+ settings: ::Legion::Settings,
231
+ instance_key: instance_key,
232
+ provider_native_key: model_id,
233
+ model: model_id,
234
+ tier: tier
235
+ )
228
236
  Legion::Extensions::Llm::Inventory::OfferingDraft.new(
229
237
  provider_native_key: model_id, model: model_id, tier: tier,
230
238
  operation_evidence: op_ev,
@@ -236,7 +244,9 @@ module Legion
236
244
  tokenizer_evidence: build_tokenizer_evidence,
237
245
  quota_domains: build_quota_domains(instance_cfg: instance_cfg, model_id: model_id),
238
246
  metadata: build_offering_metadata(model_id: model_id, instance_key: instance_key),
239
- publication_source: :provider_catalog
247
+ publication_source: :provider_catalog,
248
+ weight_inputs: weight_inputs,
249
+ base_weight: Legion::Extensions::Llm::Inventory::WeightSchema.base_weight(weight_inputs)
240
250
  )
241
251
  end
242
252
 
@@ -380,7 +390,7 @@ module Legion
380
390
  end
381
391
 
382
392
  def handle_reactive_probe(instance_id:, request:)
383
- state = @instance_states[instance_id]
393
+ state = instance_state_mutex.synchronize { @instance_states[instance_id] }
384
394
  return unless state
385
395
 
386
396
  coordinator = state[:probe_coordinator]
@@ -435,6 +445,11 @@ module Legion
435
445
  # tick reconciliation, shutdown, and the post-commit settings health
436
446
  # + capabilities display writes (D14).
437
447
  module DiscoveryOrchestrationHelpers
448
+ SCALAR_EVIDENCE_FIELDS = %i[
449
+ context_evidence max_output_evidence embedding_dimensions_evidence
450
+ model_revision_evidence tokenizer_evidence
451
+ ].freeze
452
+
438
453
  private
439
454
 
440
455
  # SSOT instance source, shared with the fleet worker/runner: every
@@ -507,6 +522,7 @@ module Legion
507
522
  instance_key = Legion::Extensions::Llm::Inventory::Identity::InstanceKey.new(
508
523
  provider_family: :bedrock, instance_id: instance_id, physical_id: physical_id
509
524
  )
525
+ offerings = discover_offerings_for_instance(instance_cfg: instance_cfg, instance_key: instance_key)
510
526
  callable = BedrockCallable.new(instance_cfg: instance_cfg, logger: log)
511
527
  probe_coordinator = Legion::Extensions::Llm::Inventory::ProbeCoordinator.new(
512
528
  instance_key: instance_key,
@@ -516,31 +532,33 @@ module Legion
516
532
  instance_id: instance_id, callable: callable, probe_request_handle: probe_coordinator,
517
533
  physical_id: physical_id
518
534
  )
519
- offerings = discover_offerings_for_instance(instance_cfg: instance_cfg, instance_key: instance_key)
520
- probe_token = publisher.readiness_probe_started(instance_id: instance_id, physical_id: physical_id,
521
- publisher_token: publisher_token)
522
- readiness = check_health(instance_cfg: instance_cfg)
523
-
524
535
  state = {
525
536
  name: name, instance_key: instance_key, instance_cfg: instance_cfg,
526
537
  callable: callable, probe_coordinator: probe_coordinator,
527
538
  publisher_token: publisher_token, sequence: 0, offerings: offerings,
528
- last_probe_outcome: nil
539
+ published: false, last_probe_outcome: nil
529
540
  }
530
- @instance_states[instance_id] = state
541
+ Legion::Extensions::Llm::Inventory::WeightReconciler.track_initializing!(
542
+ states: @instance_states, state_key: instance_id, state: state,
543
+ mutex: instance_state_mutex
544
+ )
545
+
546
+ probe_token = publisher.readiness_probe_started(instance_id: instance_id, physical_id: physical_id,
547
+ publisher_token: publisher_token)
548
+ readiness = check_health(instance_cfg: instance_cfg)
531
549
 
532
550
  if readiness.ready?
533
- publisher.activate_instance_snapshot(
534
- instance_id: instance_id, physical_id: physical_id, publisher_token: publisher_token,
535
- offerings: offerings, sequence: state[:sequence], probe_token: probe_token
551
+ activated = activate_tracked_snapshot(
552
+ instance_id: instance_id, state: state, probe_token: probe_token
536
553
  )
537
- state[:last_probe_outcome] = :success
554
+ state[:last_probe_outcome] = :success if activated
538
555
  else
539
556
  publisher.readiness_failed(instance_id: instance_id, physical_id: physical_id,
540
557
  probe_token: probe_token, reason: readiness.reason)
541
558
  state[:last_probe_outcome] = :failure
542
559
  end
543
- write_instance_health(name: name, state: state, reason: readiness.reason)
560
+ write_instance_health(name: name, state: state, reason: readiness.reason) \
561
+ if tracked_instance_state?(instance_id: instance_id, state: state)
544
562
  end
545
563
 
546
564
  def tick_refresh
@@ -549,25 +567,50 @@ module Legion
549
567
 
550
568
  claimable.each do |name, instance_cfg|
551
569
  instance_id = name.to_s
552
- state = @instance_states[instance_id]
570
+ state = instance_state_mutex.synchronize { @instance_states[instance_id] }
553
571
  if state.nil?
554
572
  claim_and_activate_instance(name: name, instance_cfg: instance_cfg)
555
- elsif state[:instance_cfg] != instance_cfg
573
+ elsif runtime_instance_config(state[:instance_cfg]) != runtime_instance_config(instance_cfg)
556
574
  # Config changed under a live instance: re-claim with the new
557
575
  # identity/config rather than mutating a live publication.
558
576
  remove_instance_state(instance_id: instance_id)
559
577
  claim_and_activate_instance(name: name, instance_cfg: instance_cfg)
560
578
  else
579
+ instance_state_mutex.synchronize do
580
+ state[:instance_cfg] = instance_cfg if @instance_states[instance_id].equal?(state)
581
+ end
561
582
  refresh_instance(instance_id: instance_id, state: state)
562
583
  end
563
584
  rescue StandardError => e
564
585
  handle_exception(e, level: :warn, operation: 'bedrock.actor.tick_refresh', instance_name: name.to_s)
565
586
  end
587
+ observe_dormant_weights
588
+ end
589
+
590
+ # Weight is publication data, not callable/credential identity. Ignore only
591
+ # the instance and instance-model weight fields when deciding whether an
592
+ # existing callable must be torn down and re-claimed; every other config
593
+ # change preserves the existing re-claim behavior.
594
+ def runtime_instance_config(instance_cfg)
595
+ runtime = instance_cfg.dup
596
+ runtime.delete(:weight)
597
+ runtime.delete('weight')
598
+ models_key = runtime.key?(:models) ? :models : 'models'
599
+ models = runtime[models_key]
600
+ return runtime unless models.is_a?(::Hash)
601
+
602
+ runtime[models_key] = models.to_h do |model, model_config|
603
+ next [model, model_config] unless model_config.is_a?(::Hash)
604
+
605
+ [model, model_config.except(:weight, 'weight')]
606
+ end
607
+ runtime
566
608
  end
567
609
 
568
610
  def reconcile_removed_instances(claimable)
569
611
  claimed_ids = claimable.keys.map(&:to_s)
570
- @instance_states.each_key do |instance_id|
612
+ tracked_ids = instance_state_mutex.synchronize { @instance_states.keys }
613
+ tracked_ids.each do |instance_id|
571
614
  next if claimed_ids.include?(instance_id)
572
615
 
573
616
  remove_instance_state(instance_id: instance_id)
@@ -587,7 +630,7 @@ module Legion
587
630
  end
588
631
 
589
632
  def publication_initializing?(state)
590
- publisher.snapshot.publication_status(instance_key: state[:instance_key]).state == :initializing
633
+ instance_state_mutex.synchronize { !state.fetch(:published) }
591
634
  end
592
635
 
593
636
  def reactivate_if_ready(instance_id:, state:)
@@ -600,13 +643,11 @@ module Legion
600
643
  offerings = discover_offerings_for_instance(
601
644
  instance_cfg: state[:instance_cfg], instance_key: state[:instance_key]
602
645
  )
603
- state[:offerings] = offerings
604
- publisher.activate_instance_snapshot(
605
- instance_id: instance_id, physical_id: state[:instance_key].physical_id,
606
- publisher_token: state[:publisher_token],
607
- offerings: offerings, sequence: state[:sequence], probe_token: probe_token
646
+ reconcile_offerings(instance_id: instance_id, state: state, offerings: offerings)
647
+ activated = activate_tracked_snapshot(
648
+ instance_id: instance_id, state: state, probe_token: probe_token
608
649
  )
609
- state[:last_probe_outcome] = :success
650
+ state[:last_probe_outcome] = :success if activated
610
651
  else
611
652
  publisher.readiness_failed(
612
653
  instance_id: instance_id, physical_id: state[:instance_key].physical_id,
@@ -614,60 +655,150 @@ module Legion
614
655
  )
615
656
  state[:last_probe_outcome] = :failure
616
657
  end
617
- write_instance_health(name: state[:name], state: state, reason: readiness.reason)
658
+ write_instance_health(name: state[:name], state: state, reason: readiness.reason) \
659
+ if tracked_instance_state?(instance_id: instance_id, state: state)
618
660
  end
619
661
 
620
662
  def replace_offerings_if_changed(instance_id:, state:)
621
663
  new_offerings = discover_offerings_for_instance(
622
664
  instance_cfg: state[:instance_cfg], instance_key: state[:instance_key]
623
665
  )
624
- # Compare on identity/status, not Data#==: evidence carries a
625
- # fresh observed_at per discovery and would force a replace
626
- # (generation bump) every tick on an unchanged catalog.
627
- return if offerings_signature(new_offerings) == offerings_signature(state[:offerings])
666
+ changed = reconcile_offerings(instance_id: instance_id, state: state, offerings: new_offerings)
667
+ write_instance_health(name: state[:name], state: state, reason: 'offerings refreshed') \
668
+ if changed && state[:published]
669
+ end
670
+
671
+ def reconcile_offerings(instance_id:, state:, offerings:)
672
+ Legion::Extensions::Llm::Inventory::WeightReconciler.commit_if_changed!(
673
+ settings: ::Legion::Settings,
674
+ instance_id: instance_id,
675
+ state: state,
676
+ discovered_offerings: offerings,
677
+ mutex: instance_state_mutex,
678
+ equivalent: lambda do |previous, current|
679
+ offerings_signature(previous) == offerings_signature(current)
680
+ end,
681
+ replace: method(:replace_weight_snapshot)
682
+ )
683
+ end
628
684
 
629
- state[:sequence] += 1
685
+ def replace_weight_snapshot(instance_id:, state:, offerings:, sequence:)
630
686
  publisher.replace_instance_snapshot(
631
- instance_id: instance_id, physical_id: state[:instance_key].physical_id,
632
- publisher_token: state[:publisher_token],
633
- offerings: new_offerings, sequence: state[:sequence]
687
+ instance_id: instance_id,
688
+ publisher_token: state.fetch(:publisher_token),
689
+ offerings: offerings,
690
+ sequence: sequence,
691
+ physical_id: state.fetch(:instance_key).physical_id
692
+ )
693
+ end
694
+
695
+ def activate_tracked_snapshot(instance_id:, state:, probe_token:)
696
+ Legion::Extensions::Llm::Inventory::WeightReconciler.activate_tracked!(
697
+ settings: ::Legion::Settings,
698
+ instance_id: instance_id,
699
+ state_key: instance_id,
700
+ state: state,
701
+ states: @instance_states,
702
+ mutex: instance_state_mutex,
703
+ probe_token: probe_token,
704
+ activate: method(:activate_weight_snapshot),
705
+ activation_sequence: ->(tracked) { tracked.fetch(:sequence) }
706
+ )
707
+ end
708
+
709
+ def activate_weight_snapshot(instance_id:, state:, offerings:, sequence:, probe_token:)
710
+ publisher.activate_instance_snapshot(
711
+ instance_id: instance_id,
712
+ publisher_token: state.fetch(:publisher_token),
713
+ offerings: offerings,
714
+ sequence: sequence,
715
+ probe_token: probe_token,
716
+ physical_id: state.fetch(:instance_key).physical_id
634
717
  )
635
- state[:offerings] = new_offerings
636
- write_instance_health(name: state[:name], state: state, reason: 'offerings refreshed')
637
718
  end
638
719
 
639
720
  def offerings_signature(offerings)
640
- signatures = offerings.map do |draft|
641
- {
642
- model: draft.model,
643
- tier: draft.tier,
644
- operations: draft.operation_evidence.transform_values(&:status).sort.to_h,
645
- capabilities: draft.capability_evidence.transform_values(&:status).sort.to_h
646
- }
721
+ contracts = offerings.map { |draft| stable_offering_contract(draft) }
722
+ grouped = contracts.group_by do |contract|
723
+ [contract.fetch(:provider_native_key), contract.fetch(:model)]
647
724
  end
648
- signatures.sort_by { |entry| entry[:model].to_s }
725
+ grouped.sort_by { |identity, _| identity.map { |value| value.to_s.b } }
726
+ .to_h.transform_values(&:tally)
727
+ end
728
+
729
+ def stable_offering_contract(draft)
730
+ contract = draft.to_h
731
+ contract[:operation_evidence] = stable_evidence_map(contract.fetch(:operation_evidence))
732
+ contract[:capability_evidence] = stable_evidence_map(contract.fetch(:capability_evidence))
733
+ SCALAR_EVIDENCE_FIELDS.each do |field|
734
+ contract[field] = stable_evidence(contract.fetch(field))
735
+ end
736
+ contract
737
+ end
738
+
739
+ def stable_evidence_map(evidence_by_key)
740
+ evidence_by_key.transform_values { |evidence| stable_evidence(evidence) }
741
+ end
742
+
743
+ def stable_evidence(evidence)
744
+ evidence.to_h.except(:observed_at)
649
745
  end
650
746
 
651
747
  def remove_instance_state(instance_id:)
652
- state = @instance_states.delete(instance_id)
748
+ state = instance_state_mutex.synchronize { @instance_states.delete(instance_id) }
653
749
  return unless state
654
750
 
751
+ remove_published_state(state)
752
+ rescue StandardError => e
753
+ handle_exception(e, level: :warn, operation: 'bedrock.actor.remove_instance', instance_id: instance_id)
754
+ end
755
+
756
+ def remove_all_instances
757
+ return unless @instance_states
758
+
759
+ states = instance_state_mutex.synchronize do
760
+ removed = @instance_states.values
761
+ @instance_states.clear
762
+ dormant_weight_tracker.clear!
763
+ removed
764
+ end
765
+ states.each { |state| remove_published_state(state) }
766
+ end
767
+
768
+ def remove_published_state(state)
655
769
  publisher.remove_instance(
656
770
  instance_id: state[:instance_key].instance_id,
657
771
  physical_id: state[:instance_key].physical_id,
658
772
  publisher_token: state[:publisher_token]
659
773
  )
660
774
  clear_instance_health(name: state[:name])
661
- rescue StandardError => e
662
- handle_exception(e, level: :warn, operation: 'bedrock.actor.remove_instance', instance_id: instance_id)
663
775
  end
664
776
 
665
- def remove_all_instances
666
- return unless @instance_states
777
+ def tracked_instance_state?(instance_id:, state:)
778
+ instance_state_mutex.synchronize { @instance_states[instance_id].equal?(state) }
779
+ end
667
780
 
668
- @instance_states.each_key do |instance_id|
669
- remove_instance_state(instance_id: instance_id)
670
- end
781
+ def observe_dormant_weights
782
+ Legion::Extensions::Llm::Inventory::WeightReconciler.observe_dormant!(
783
+ settings: ::Legion::Settings,
784
+ provider_family: :bedrock,
785
+ states: @instance_states,
786
+ mutex: instance_state_mutex,
787
+ tracker: dormant_weight_tracker,
788
+ dormant_logger: lambda do |key|
789
+ log.info(
790
+ "[llm][bedrock] action=dormant_weight weight_key=#{key.inspect} no_lane_published=true"
791
+ )
792
+ end
793
+ )
794
+ end
795
+
796
+ def instance_state_mutex
797
+ @instance_state_mutex ||= Mutex.new
798
+ end
799
+
800
+ def dormant_weight_tracker
801
+ @dormant_weight_tracker ||= Legion::Extensions::Llm::Inventory::DormantWeightTracker.new
671
802
  end
672
803
 
673
804
  # ── D14: settings health + capabilities display writes ────────────
@@ -807,12 +938,14 @@ module Legion
807
938
  end
808
939
 
809
940
  def initial_discovery
810
- @instance_states = {}
941
+ instance_state_mutex.synchronize { @instance_states = {} }
942
+ dormant_weight_tracker
811
943
  claimable_instances.each do |name, instance_cfg|
812
944
  claim_and_activate_instance(name: name, instance_cfg: instance_cfg)
813
945
  rescue StandardError => e
814
946
  handle_exception(e, level: :warn, operation: 'bedrock.actor.claim_instance', instance_name: name.to_s)
815
947
  end
948
+ observe_dormant_weights
816
949
  end
817
950
  end
818
951
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Bedrock
7
- VERSION = '0.5.4'
7
+ VERSION = '0.5.5'
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-bedrock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -99,14 +99,14 @@ dependencies:
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: 0.7.1
102
+ version: 0.7.6
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
- version: 0.7.1
109
+ version: 0.7.6
110
110
  description: Amazon Bedrock provider integration for the LegionIO LLM routing framework.
111
111
  email:
112
112
  - matthewdiverson@gmail.com