lex-llm 0.6.16 → 0.7.2

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/lex-llm.gemspec +1 -1
  4. data/lib/legion/extensions/llm/auto_registration.rb +3 -0
  5. data/lib/legion/extensions/llm/canonical/message.rb +5 -1
  6. data/lib/legion/extensions/llm/fleet/protocol.rb +12 -0
  7. data/lib/legion/extensions/llm/fleet/provider_responder.rb +40 -5
  8. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -0
  9. data/lib/legion/extensions/llm/fleet/worker_execution.rb +166 -2
  10. data/lib/legion/extensions/llm/inventory/callable_handle.rb +144 -0
  11. data/lib/legion/extensions/llm/inventory/errors.rb +34 -0
  12. data/lib/legion/extensions/llm/inventory/evidence.rb +134 -0
  13. data/lib/legion/extensions/llm/inventory/identity.rb +141 -0
  14. data/lib/legion/extensions/llm/inventory/immutable_value.rb +96 -0
  15. data/lib/legion/extensions/llm/inventory/probe_coordinator.rb +117 -0
  16. data/lib/legion/extensions/llm/inventory/probe_token.rb +146 -0
  17. data/lib/legion/extensions/llm/inventory/publisher.rb +116 -0
  18. data/lib/legion/extensions/llm/inventory/records.rb +599 -0
  19. data/lib/legion/extensions/llm/inventory/registry.rb +636 -0
  20. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +206 -0
  21. data/lib/legion/extensions/llm/inventory/snapshot.rb +91 -0
  22. data/lib/legion/extensions/llm/provider.rb +55 -29
  23. data/lib/legion/extensions/llm/routing/lane_key.rb +4 -0
  24. data/lib/legion/extensions/llm/routing/model_offering.rb +3 -0
  25. data/lib/legion/extensions/llm/routing/offering_registry.rb +2 -0
  26. data/lib/legion/extensions/llm/routing/provider_outcome.rb +54 -0
  27. data/lib/legion/extensions/llm/routing/records.rb +288 -0
  28. data/lib/legion/extensions/llm/settings_cascade.rb +120 -0
  29. data/lib/legion/extensions/llm/taxonomies.rb +90 -0
  30. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -1
  31. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +3 -1
  32. data/lib/legion/extensions/llm/version.rb +1 -1
  33. data/lib/legion/extensions/llm.rb +18 -0
  34. data/spec/legion/extensions/llm/canonical/message_spec.rb +24 -0
  35. data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +1 -0
  36. data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +1 -0
  37. data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +1 -0
  38. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +1 -0
  39. data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +1 -0
  40. data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +1 -0
  41. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +84 -0
  42. data/spec/legion/extensions/llm/conformance/ssot_provider_conformance_spec.rb +13 -0
  43. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +196 -0
  44. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +179 -0
  45. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +26 -0
  46. data/spec/legion/extensions/llm/fleet_messages_spec.rb +14 -0
  47. data/spec/legion/extensions/llm/inventory/boot_spec.rb +80 -0
  48. data/spec/legion/extensions/llm/inventory/callable_handle_spec.rb +127 -0
  49. data/spec/legion/extensions/llm/inventory/evidence_spec.rb +138 -0
  50. data/spec/legion/extensions/llm/inventory/identity_spec.rb +280 -0
  51. data/spec/legion/extensions/llm/inventory/immutable_value_spec.rb +132 -0
  52. data/spec/legion/extensions/llm/inventory/probe_coordinator_spec.rb +107 -0
  53. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +107 -0
  54. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +144 -0
  55. data/spec/legion/extensions/llm/inventory/records_spec.rb +300 -0
  56. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +140 -0
  57. data/spec/legion/extensions/llm/inventory/registry_availability_spec.rb +152 -0
  58. data/spec/legion/extensions/llm/inventory/registry_callable_spec.rb +63 -0
  59. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +69 -0
  60. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +128 -0
  61. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +109 -0
  62. data/spec/legion/extensions/llm/provider_contract_spec.rb +20 -0
  63. data/spec/legion/extensions/llm/provider_spec.rb +83 -0
  64. data/spec/legion/extensions/llm/routing/provider_outcome_spec.rb +73 -0
  65. data/spec/legion/extensions/llm/routing/records_spec.rb +173 -0
  66. data/spec/legion/extensions/llm/settings_cascade_spec.rb +208 -0
  67. data/spec/legion/extensions/llm/taxonomies_spec.rb +51 -0
  68. data/spec/support/fake_llm_provider.rb +11 -0
  69. data/spec/support/fake_ssot_harness.rb +137 -0
  70. data/spec/support/ssot_registry_helpers.rb +75 -0
  71. metadata +40 -3
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/inventory/probe_coordinator'
5
+
6
+ RSpec.describe Legion::Extensions::Llm::Inventory::ProbeCoordinator do
7
+ inventory = Legion::Extensions::Llm::Inventory
8
+ errors = inventory::Errors
9
+
10
+ let(:instance_key) { inventory::Identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'h200') }
11
+ let(:enqueued) { [] }
12
+ let(:enqueue_result) { [true] }
13
+ let(:enqueue) do
14
+ lambda { |request:|
15
+ enqueued << request
16
+ enqueue_result.first
17
+ }
18
+ end
19
+ let(:coordinator) { described_class.new(instance_key: instance_key, enqueue: enqueue) }
20
+
21
+ def enqueue_request(revision)
22
+ coordinator.enqueue_probe_request(
23
+ instance_key: instance_key, publisher_token_id: 'ptok:v1:x', unavailable_revision: revision, reason: 'dispatch unavailable'
24
+ )
25
+ end
26
+
27
+ it 'does not invoke enqueue at construction (enqueue-only)' do
28
+ coordinator
29
+ expect(enqueued).to be_empty
30
+ end
31
+
32
+ it 'enqueues a ProbeRequest built from the reported parameters' do
33
+ enqueue_request(1)
34
+ expect(enqueued.size).to eq(1)
35
+ expect(enqueued.first).to be_a(inventory::ProbeRequest)
36
+ expect(enqueued.first.unavailable_revision).to eq(1)
37
+ end
38
+
39
+ it 'rejects a request for a different instance' do
40
+ other = inventory::Identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'helios1')
41
+ expect do
42
+ coordinator.enqueue_probe_request(instance_key: other, publisher_token_id: 'ptok:v1:x', unavailable_revision: 1, reason: 'x')
43
+ end.to raise_error(errors::ValidationError)
44
+ end
45
+
46
+ it 'coalesces duplicate-revision requests to a single enqueue' do
47
+ enqueue_request(5)
48
+ enqueue_request(5)
49
+ expect(enqueued.size).to eq(1)
50
+ expect(coordinator.pending?(unavailable_revision: 5)).to be(true)
51
+ end
52
+
53
+ it 'retains only the greatest pending revision without re-enqueuing' do
54
+ enqueue_request(5)
55
+ enqueue_request(7)
56
+ expect(enqueued.size).to eq(1)
57
+ expect(coordinator.pending?(unavailable_revision: 7)).to be(true)
58
+ expect(coordinator.pending?(unavailable_revision: 5)).to be(false)
59
+ end
60
+
61
+ it 'retains pending and returns false when enqueue fails, without inline readiness' do
62
+ enqueue_result[0] = false
63
+ expect(enqueue_request(3)).to be(false)
64
+ expect(coordinator.pending?(unavailable_revision: 3)).to be(true)
65
+ end
66
+
67
+ describe 'single flight and post-finish re-enqueue' do
68
+ it 'admits one flight, holds newer work, and re-enqueues the greatest pending on finish' do
69
+ enqueue_request(5)
70
+ first = enqueued.last
71
+ expect(coordinator.begin_probe(request: first)).to be(true)
72
+ expect(coordinator.in_flight?).to be(true)
73
+
74
+ # A newer revision arrives during the flight: retained, not enqueued.
75
+ enqueue_request(7)
76
+ expect(enqueued.size).to eq(1)
77
+
78
+ coordinator.finish_probe(request: first)
79
+ expect(coordinator.in_flight?).to be(false)
80
+ expect(enqueued.size).to eq(2)
81
+ expect(enqueued.last.unavailable_revision).to eq(7)
82
+ end
83
+
84
+ it 'rejects a second begin while a flight is active' do
85
+ enqueue_request(5)
86
+ expect(coordinator.begin_probe(request: enqueued.last)).to be(true)
87
+ expect(coordinator.begin_probe(request: nil)).to be(false)
88
+ end
89
+
90
+ it 'supports the cadence (nil) begin/finish form and rejects a mismatched finish form' do
91
+ mismatched = inventory::ProbeRequest.new(
92
+ instance_key: instance_key, publisher_token_id: 'ptok:v1:x', unavailable_revision: 1, reason: 'x'
93
+ )
94
+ expect(coordinator.begin_probe(request: nil)).to be(true)
95
+ expect { coordinator.finish_probe(request: mismatched) }.to raise_error(errors::InvalidTransitionError)
96
+ expect { coordinator.finish_probe(request: nil) }.not_to raise_error
97
+ expect(coordinator.in_flight?).to be(false)
98
+ end
99
+
100
+ it 'only admits a begin for the exact greatest pending request' do
101
+ enqueue_request(5)
102
+ stale = enqueued.last
103
+ enqueue_request(9) # replaces pending; stale is no longer greatest
104
+ expect(coordinator.begin_probe(request: stale)).to be(false)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/inventory/probe_token'
5
+
6
+ RSpec.describe Legion::Extensions::Llm::Inventory::ProbeToken do
7
+ inventory = Legion::Extensions::Llm::Inventory
8
+ errors = inventory::Errors
9
+
10
+ let(:instance_key) { inventory::Identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'h200') }
11
+
12
+ it 'issues a frozen, immutable probe token' do
13
+ token = described_class.issue(
14
+ instance_key: instance_key, publisher_token_id: 'ptok:v1:abc', started_availability_revision: 0, started_at: Time.now
15
+ )
16
+ expect(token.token_id).to start_with('probe:v1:')
17
+ expect(token).to be_frozen
18
+ end
19
+
20
+ it 'validates its fields' do
21
+ expect do
22
+ described_class.issue(instance_key: instance_key, publisher_token_id: 'nope', started_availability_revision: 0, started_at: Time.now)
23
+ end.to raise_error(errors::ValidationError)
24
+ expect do
25
+ described_class.issue(instance_key: instance_key, publisher_token_id: 'ptok:v1:x', started_availability_revision: -1, started_at: Time.now)
26
+ end.to raise_error(errors::ValidationError)
27
+ end
28
+
29
+ describe Legion::Extensions::Llm::Inventory::PublisherToken do
30
+ let(:token) { described_class.issue(instance_key: instance_key) }
31
+
32
+ describe '.issue' do
33
+ it 'derives pub:v1: and ptok:v1: correlation IDs and is frozen' do
34
+ expect(token.publisher_id).to start_with('pub:v1:')
35
+ expect(token.publisher_token_id).to start_with('ptok:v1:')
36
+ expect(token).to be_frozen
37
+ end
38
+
39
+ it 'derives publisher_token_id from the secret domain-separated SHA-256' do
40
+ other = described_class.issue(instance_key: instance_key)
41
+ expect(token.publisher_token_id).not_to eq(other.publisher_token_id)
42
+ end
43
+ end
44
+
45
+ describe 'secret redaction' do
46
+ it 'never exposes the secret through a public reader' do
47
+ expect(token).not_to respond_to(:secret)
48
+ end
49
+
50
+ it 'redacts the secret in inspect/to_s' do
51
+ expect(token.inspect).to include('[REDACTED]')
52
+ expect(token.inspect).not_to match(/secret=[0-9a-f]{8}/)
53
+ expect(token.to_s).to include('[REDACTED]')
54
+ end
55
+ end
56
+
57
+ describe '#authenticates?' do
58
+ it 'rejects a token with a different secret' do
59
+ other = described_class.issue(instance_key: instance_key)
60
+ expect(token.authenticates?(other)).to be(false)
61
+ end
62
+
63
+ it 'rejects a token for a different instance' do
64
+ other_key = inventory::Identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'helios1')
65
+ other = described_class.issue(instance_key: other_key)
66
+ expect(token.authenticates?(other)).to be(false)
67
+ end
68
+
69
+ it 'rejects a non-PublisherToken' do
70
+ expect(token.authenticates?(Object.new)).to be(false)
71
+ end
72
+
73
+ it 'authenticates two tokens built from the same secret' do
74
+ secret = 'deadbeef' * 8
75
+ token_id = described_class.derive_token_id(secret)
76
+ a = described_class.new(instance_key: instance_key, secret: secret, publisher_id: 'pub:v1:x', publisher_token_id: token_id)
77
+ b = described_class.new(instance_key: instance_key, secret: secret, publisher_id: 'pub:v1:y', publisher_token_id: token_id)
78
+ expect(a.authenticates?(b)).to be(true)
79
+ end
80
+ end
81
+
82
+ describe 'validation' do
83
+ it 'rejects a non-InstanceKey and malformed ids' do
84
+ expect { described_class.new(instance_key: :vllm, secret: 'x', publisher_id: 'pub:v1:a', publisher_token_id: 'ptok:v1:a') }
85
+ .to raise_error(errors::ValidationError)
86
+ expect { described_class.new(instance_key: instance_key, secret: 'x', publisher_id: 'bad', publisher_token_id: 'ptok:v1:a') }
87
+ .to raise_error(errors::ValidationError)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe Legion::Extensions::Llm::Inventory::ProbeRequest do
93
+ it 'requires a positive unavailable_revision and a ptok:v1: publisher token id' do
94
+ request = described_class.new(instance_key: instance_key, publisher_token_id: 'ptok:v1:x', unavailable_revision: 3, reason: 'dispatch reported unavailable')
95
+ expect(request.unavailable_revision).to eq(3)
96
+ expect { described_class.new(instance_key: instance_key, publisher_token_id: 'ptok:v1:x', unavailable_revision: 0, reason: 'x') }
97
+ .to raise_error(errors::ValidationError)
98
+ expect { described_class.new(instance_key: instance_key, publisher_token_id: 'nope', unavailable_revision: 1, reason: 'x') }
99
+ .to raise_error(errors::ValidationError)
100
+ end
101
+
102
+ it 'sanitizes and bounds the reason' do
103
+ expect { described_class.new(instance_key: instance_key, publisher_token_id: 'ptok:v1:x', unavailable_revision: 1, reason: ' ') }
104
+ .to raise_error(errors::ValidationError)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/inventory/publisher'
5
+ require_relative '../../../../support/ssot_registry_helpers'
6
+
7
+ RSpec.describe Legion::Extensions::Llm::Inventory::Publisher do
8
+ include SsotRegistryHelpers
9
+
10
+ inventory = Legion::Extensions::Llm::Inventory
11
+
12
+ before { inventory::Registry.reset! }
13
+
14
+ # Records adapter invocations; returns a configurable outcome.
15
+ let(:adapter_calls) { [] }
16
+ let(:adapter_outcome) { [:applied] }
17
+ let(:adapter) do
18
+ calls = adapter_calls
19
+ outcome = adapter_outcome
20
+ Object.new.tap do |obj|
21
+ obj.define_singleton_method(:sync_snapshot) do |snapshot:, instance_key:, mutation_reason:|
22
+ calls << { snapshot: snapshot, instance_key: instance_key, mutation_reason: mutation_reason }
23
+ outcome.first
24
+ end
25
+ end
26
+ end
27
+
28
+ let(:publisher) { described_class.new(provider_family: :vllm, compatibility_adapter: adapter) }
29
+ let(:plain_publisher) { described_class.new(provider_family: :vllm) }
30
+
31
+ def full_activate(pub)
32
+ token = pub.claim_instance(instance_id: 'h200', callable: fake_callable, probe_request_handle: probe_coordinator(instance_key))
33
+ probe = pub.readiness_probe_started(instance_id: 'h200', publisher_token: token)
34
+ pub.activate_instance_snapshot(instance_id: 'h200', publisher_token: token, offerings: drafts, sequence: 0, probe_token: probe)
35
+ token
36
+ end
37
+
38
+ describe 'delegation' do
39
+ it 'claims, activates, and exposes the snapshot through the fixed provider family' do
40
+ full_activate(plain_publisher)
41
+ record = plain_publisher.snapshot.instance(instance_key: instance_key)
42
+ expect(record.availability.state).to eq(:available)
43
+ expect(record.instance_key.provider_family).to eq(:vllm)
44
+ end
45
+
46
+ it 'delegates removal with the publisher token' do
47
+ token = full_activate(plain_publisher)
48
+ result = plain_publisher.remove_instance(instance_id: 'h200', publisher_token: token)
49
+ expect(result.applied).to be(true)
50
+ expect(plain_publisher.snapshot.instance(instance_key: instance_key)).to be_nil
51
+ end
52
+ end
53
+
54
+ describe 'post-commit compatibility projection' do
55
+ it 'invokes the adapter after a successful claim and applied mutation with the committed snapshot' do
56
+ full_activate(publisher)
57
+ expect(adapter_calls.map { |c| c[:mutation_reason] }).to eq(%i[claimed activated])
58
+ # The adapter sees the already-committed snapshot on activation.
59
+ activation_call = adapter_calls.last
60
+ expect(activation_call[:snapshot].instance(instance_key: instance_key).availability.state).to eq(:available)
61
+ expect(activation_call[:instance_key]).to eq(instance_key)
62
+ end
63
+
64
+ it 'does not invoke the adapter for a non-applied (stale) mutation' do
65
+ token = full_activate(publisher)
66
+ adapter_calls.clear
67
+ # Supersede the publisher, then a stale replace returns applied: false.
68
+ publisher.claim_instance(instance_id: 'h200', callable: fake_callable, probe_request_handle: probe_coordinator(instance_key))
69
+ adapter_calls.clear
70
+ result = publisher.replace_instance_snapshot(instance_id: 'h200', publisher_token: token, offerings: drafts, sequence: 9)
71
+ expect(result.applied).to be(false)
72
+ expect(adapter_calls).to be_empty
73
+ end
74
+
75
+ it 'never changes the local result when the adapter returns :failed' do
76
+ adapter_outcome[0] = :failed
77
+ token = publisher.claim_instance(instance_id: 'h200', callable: fake_callable, probe_request_handle: probe_coordinator(instance_key))
78
+ expect(token).to be_a(inventory::PublisherToken)
79
+ end
80
+
81
+ it 'never changes the local result when the adapter raises' do
82
+ failing = Object.new.tap do |obj|
83
+ obj.define_singleton_method(:sync_snapshot) { |**| raise 'boom' }
84
+ end
85
+ pub = described_class.new(provider_family: :vllm, compatibility_adapter: failing)
86
+ expect { pub.claim_instance(instance_id: 'h200', callable: fake_callable, probe_request_handle: probe_coordinator(instance_key)) }
87
+ .not_to raise_error
88
+ end
89
+
90
+ it 'invokes the adapter again on the next applied cadence mutation' do
91
+ token = full_activate(publisher)
92
+ adapter_calls.clear
93
+ publisher.replace_instance_snapshot(instance_id: 'h200', publisher_token: token, offerings: drafts(native: 'gemma5', model: 'gemma5'), sequence: 1)
94
+ expect(adapter_calls.map { |c| c[:mutation_reason] }).to eq(%i[snapshot_replaced])
95
+ end
96
+ end
97
+
98
+ describe 'without a compatibility adapter' do
99
+ it 'performs no projection and returns the plain registry results' do
100
+ result = full_activate(plain_publisher)
101
+ expect(result).to be_a(inventory::PublisherToken)
102
+ end
103
+ end
104
+
105
+ describe 'secondary physical_id' do
106
+ it 'carries the physical id on the committed key while identity stays the config name' do
107
+ token = plain_publisher.claim_instance(
108
+ instance_id: 'h200', callable: fake_callable,
109
+ probe_request_handle: probe_coordinator(instance_key), physical_id: '10.0.0.1:8000/ak:abc123'
110
+ )
111
+ probe = plain_publisher.readiness_probe_started(instance_id: 'h200', publisher_token: token)
112
+ plain_publisher.activate_instance_snapshot(
113
+ instance_id: 'h200', publisher_token: token, offerings: drafts, sequence: 0, probe_token: probe
114
+ )
115
+
116
+ keyed_with_physical = inventory::Identity::InstanceKey.new(
117
+ provider_family: 'vllm', instance_id: 'h200', physical_id: '10.0.0.1:8000/ak:abc123'
118
+ )
119
+ record = plain_publisher.snapshot.instance(instance_key: keyed_with_physical)
120
+ expect(record).not_to be_nil
121
+ expect(record.instance_key.physical_id).to eq('10.0.0.1:8000/ak:abc123')
122
+
123
+ # Identity lookup without the physical id finds the same committed instance.
124
+ expect(plain_publisher.snapshot.instance(instance_key: instance_key)).to eq(record)
125
+ end
126
+
127
+ it 'replaces the same scope when the physical id changes for the same config name' do
128
+ old_token = plain_publisher.claim_instance(
129
+ instance_id: 'h200', callable: fake_callable,
130
+ probe_request_handle: probe_coordinator(instance_key), physical_id: '10.0.0.1:8000/ak:abc123'
131
+ )
132
+ new_token = plain_publisher.claim_instance(
133
+ instance_id: 'h200', callable: fake_callable,
134
+ probe_request_handle: probe_coordinator(instance_key), physical_id: '10.0.0.9:9000'
135
+ )
136
+
137
+ # The old publisher is superseded on the SAME scope (no parallel scope).
138
+ expect { plain_publisher.readiness_probe_started(instance_id: 'h200', publisher_token: old_token) }
139
+ .to raise_error(inventory::Errors::FencedPublisherError)
140
+ expect(plain_publisher.readiness_probe_started(instance_id: 'h200', publisher_token: new_token))
141
+ .to be_a(inventory::ProbeToken)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/inventory/records'
5
+
6
+ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
7
+ inventory = Legion::Extensions::Llm::Inventory
8
+ errors = inventory::Errors
9
+ identity = inventory::Identity
10
+ taxonomies = Legion::Extensions::Llm::Taxonomies
11
+
12
+ let(:callable_class) do
13
+ Class.new do
14
+ def disconnect; end
15
+ end
16
+ end
17
+ let(:callable_handle) { inventory::CallableHandle.new(handle_id: 'call:v1:test', callable: callable_class.new) }
18
+ let(:instance_key) { identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'h200') }
19
+ let(:offering_id) { identity.offering_id(instance_key: instance_key, provider_native_key: 'gemma4') }
20
+ let(:lane_id) do
21
+ identity.lane_id(instance_key: instance_key, operation: :chat, model: 'gemma4', offering_id: offering_id)
22
+ end
23
+
24
+ def value_unknown
25
+ Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :unknown, source: :absent)
26
+ end
27
+
28
+ def full_operation_evidence(supported: [])
29
+ Legion::Extensions::Llm::Taxonomies::OPERATIONS.to_h do |op|
30
+ status, source = supported.include?(op) ? %i[supported provider_implementation] : %i[unknown absent]
31
+ [op, Legion::Extensions::Llm::Inventory::OperationEvidence.new(operation: op, status: status, source: source)]
32
+ end
33
+ end
34
+
35
+ def scalar_evidence_kwargs
36
+ {
37
+ context_evidence: value_unknown, max_output_evidence: value_unknown,
38
+ embedding_dimensions_evidence: value_unknown, model_revision_evidence: value_unknown,
39
+ tokenizer_evidence: value_unknown
40
+ }
41
+ end
42
+
43
+ def build_draft(**overrides)
44
+ Legion::Extensions::Llm::Inventory::OfferingDraft.new(
45
+ provider_native_key: 'gemma4', model: 'gemma4', tier: :local,
46
+ operation_evidence: full_operation_evidence(supported: %i[chat]),
47
+ publication_source: :provider_catalog, **scalar_evidence_kwargs, **overrides
48
+ )
49
+ end
50
+
51
+ def build_offering_record(**overrides)
52
+ Legion::Extensions::Llm::Inventory::OfferingRecord.new(
53
+ offering_id: offering_id, provider_native_key: 'gemma4', instance_key: instance_key, model: 'gemma4',
54
+ tier: :local, operation_evidence: full_operation_evidence(supported: %i[chat]), capability_evidence: {},
55
+ quota_domains: {}, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
56
+ **scalar_evidence_kwargs, **overrides
57
+ )
58
+ end
59
+
60
+ describe inventory::OfferingDraft do
61
+ it 'builds a valid frozen draft' do
62
+ draft = build_draft
63
+ expect(draft).to be_frozen
64
+ expect(draft.model).to eq('gemma4')
65
+ expect(draft.operation_evidence).to be_frozen
66
+ end
67
+
68
+ it 'rejects an unknown kwarg' do
69
+ expect { build_draft(bogus: 1) }.to raise_error(ArgumentError)
70
+ end
71
+
72
+ it 'requires operation_evidence to contain every canonical operation exactly' do
73
+ incomplete = full_operation_evidence.except(:moderate)
74
+ expect { build_draft(operation_evidence: incomplete) }.to raise_error(errors::ValidationError)
75
+ end
76
+
77
+ it 'rejects an operation_evidence value whose operation mismatches its key' do
78
+ mismatched = full_operation_evidence
79
+ mismatched[:chat] = Legion::Extensions::Llm::Inventory::OperationEvidence.new(
80
+ operation: :embed, status: :supported, source: :provider_implementation
81
+ )
82
+ expect { build_draft(operation_evidence: mismatched) }.to raise_error(errors::ValidationError)
83
+ end
84
+
85
+ it 'rejects secret-like metadata keys at any nesting level' do
86
+ expect { build_draft(metadata: { api_key: 'x' }) }.to raise_error(errors::ValidationError)
87
+ expect { build_draft(metadata: { nested: [{ authorization_header: 'x' }] }) }
88
+ .to raise_error(errors::ValidationError)
89
+ end
90
+
91
+ it 'rejects a known context value that is not a positive Integer' do
92
+ bad = Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :known, value: 0, source: :model_metadata)
93
+ expect { build_draft(context_evidence: bad) }.to raise_error(errors::ValidationError)
94
+ end
95
+
96
+ it 'rejects unsorted embedding dimensions' do
97
+ bad = Legion::Extensions::Llm::Inventory::ValueEvidence.new(
98
+ status: :known, value: [1024, 512], source: :provider_catalog
99
+ )
100
+ expect { build_draft(embedding_dimensions_evidence: bad) }.to raise_error(errors::ValidationError)
101
+ end
102
+
103
+ it 'freezes copied metadata deeply' do
104
+ draft = build_draft(metadata: { a: { b: [1] } })
105
+ expect(draft.metadata[:a][:b]).to be_frozen
106
+ end
107
+
108
+ it 'normalizes quota_domain keys to canonical operations' do
109
+ draft = build_draft(quota_domains: { 'chat' => 'domain-a' })
110
+ expect(draft.quota_domains).to eq(chat: 'domain-a')
111
+ end
112
+ end
113
+
114
+ describe inventory::OfferingRecord do
115
+ it 'validates offering_id reproduction and exposes operation views' do
116
+ record = build_offering_record
117
+ expect(record.supported_operations).to eq(%i[chat])
118
+ expect(record.operation_status(operation: :chat)).to eq(:supported)
119
+ expect(record.operation_status(operation: :embed)).to eq(:unknown)
120
+ expect(record.capability_status(capability: :vision)).to eq(:unknown)
121
+ expect(record.unknown_operations).to include(:embed)
122
+ end
123
+
124
+ it 'rejects an offering_id that does not reproduce' do
125
+ expect { build_offering_record(offering_id: "off:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
126
+ end
127
+
128
+ it 'rejects a non-InstanceKey and a non-CallableHandle' do
129
+ expect { build_offering_record(instance_key: :vllm) }.to raise_error(errors::ValidationError)
130
+ expect { build_offering_record(callable_handle: Object.new) }.to raise_error(errors::ValidationError)
131
+ end
132
+
133
+ it 'returns the quota domain for an operation' do
134
+ record = build_offering_record(quota_domains: { chat: 'domain-a' })
135
+ expect(record.quota_domain(operation: :chat)).to eq('domain-a')
136
+ expect(record.quota_domain(operation: :embed)).to be_nil
137
+ end
138
+ end
139
+
140
+ describe inventory::LaneRecord do
141
+ def build_lane(**overrides)
142
+ Legion::Extensions::Llm::Inventory::LaneRecord.new(
143
+ lane_id: lane_id, offering_id: offering_id, instance_key: instance_key, provider_family: :vllm,
144
+ instance_id: 'h200', model: 'gemma4', tier: :local, operation: :chat, capability_evidence: {},
145
+ quota_domain: nil, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
146
+ **scalar_evidence_kwargs, **overrides
147
+ )
148
+ end
149
+
150
+ it 'builds a valid lane whose id reproduces' do
151
+ expect(build_lane.lane_id).to eq(lane_id)
152
+ end
153
+
154
+ it 'rejects provider_family/instance_id not equal to instance_key' do
155
+ expect { build_lane(instance_id: 'helios1') }.to raise_error(errors::ValidationError)
156
+ end
157
+
158
+ it 'rejects a lane_id that does not reproduce' do
159
+ expect { build_lane(lane_id: "lane:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
160
+ end
161
+ end
162
+
163
+ describe inventory::AvailabilityFact do
164
+ def build_fact(**overrides)
165
+ Legion::Extensions::Llm::Inventory::AvailabilityFact.new(
166
+ state: :available, availability_revision: 1, source: :startup_readiness, reason: 'activated', observed_at: nil,
167
+ **overrides
168
+ )
169
+ end
170
+
171
+ it 'accepts an available fact with nil unavailable_revision' do
172
+ expect(build_fact.state).to eq(:available)
173
+ end
174
+
175
+ it 'requires unavailable_revision to equal availability_revision when unavailable' do
176
+ expect { build_fact(state: :unavailable, availability_revision: 3, source: :dispatch, unavailable_revision: 2) }
177
+ .to raise_error(errors::ValidationError)
178
+ expect do
179
+ build_fact(state: :unavailable, availability_revision: 3, source: :dispatch, unavailable_revision: 3)
180
+ end.not_to raise_error
181
+ end
182
+
183
+ it 'rejects a non-unavailable state that carries an unavailable_revision' do
184
+ expect { build_fact(unavailable_revision: 1) }.to raise_error(errors::ValidationError)
185
+ end
186
+
187
+ it 'requires both probe timestamps for a non-nil outcome and forbids completion before start' do
188
+ now = Time.now
189
+ expect { build_fact(last_probe_outcome: :success) }.to raise_error(errors::ValidationError)
190
+ expect do
191
+ build_fact(last_probe_outcome: :success, last_probe_started_at: now + 5, last_probe_completed_at: now)
192
+ end.to raise_error(errors::ValidationError)
193
+ end
194
+
195
+ it 'has no cooldown/half-open/latency fields' do
196
+ expect(build_fact.to_h.keys).to contain_exactly(
197
+ :state, :availability_revision, :source, :reason, :observed_at,
198
+ :last_probe_started_at, :last_probe_completed_at, :last_probe_outcome, :unavailable_revision
199
+ )
200
+ end
201
+ end
202
+
203
+ describe inventory::ReadinessResult do
204
+ it 'exposes ready? and rejects a non-boolean ready' do
205
+ result = Legion::Extensions::Llm::Inventory::ReadinessResult.new(ready: true, reason: 'ok')
206
+ expect(result).to be_ready
207
+ expect { Legion::Extensions::Llm::Inventory::ReadinessResult.new(ready: 'yes', reason: 'ok') }
208
+ .to raise_error(errors::ValidationError)
209
+ end
210
+
211
+ it 'rejects secret metadata' do
212
+ expect { Legion::Extensions::Llm::Inventory::ReadinessResult.new(ready: true, reason: 'ok', metadata: { secret: 'x' }) }
213
+ .to raise_error(errors::ValidationError)
214
+ end
215
+ end
216
+
217
+ describe inventory::InstanceRecord do
218
+ def build_instance(**overrides)
219
+ Legion::Extensions::Llm::Inventory::InstanceRecord.new(
220
+ instance_key: instance_key, callable_handle: callable_handle,
221
+ availability: Legion::Extensions::Llm::Inventory::AvailabilityFact.new(
222
+ state: :available, availability_revision: 1, source: :startup_readiness, reason: 'activated', observed_at: nil
223
+ ),
224
+ offerings_by_id: { offering_id => build_offering_record },
225
+ publisher_id: 'pub:v1:x', publisher_token_id: 'ptok:v1:y', published_sequence: 1, published_at: Time.now,
226
+ **overrides
227
+ )
228
+ end
229
+
230
+ it 'builds an available instance with a complete offerings map' do
231
+ expect(build_instance.offerings_by_id).to be_frozen
232
+ end
233
+
234
+ it 'rejects an initializing availability' do
235
+ initializing = Legion::Extensions::Llm::Inventory::AvailabilityFact.new(
236
+ state: :initializing, availability_revision: 0, source: :startup_readiness, reason: 'claim', observed_at: nil
237
+ )
238
+ expect { build_instance(availability: initializing) }.to raise_error(errors::ValidationError)
239
+ end
240
+
241
+ it 'accepts an explicit empty offerings map' do
242
+ expect(build_instance(offerings_by_id: {}).offerings_by_id).to eq({})
243
+ end
244
+ end
245
+
246
+ describe inventory::PublicationStatus do
247
+ it 'accepts initializing with a nil sequence and complete with a sequence' do
248
+ initializing = Legion::Extensions::Llm::Inventory::PublicationStatus.new(
249
+ instance_key: instance_key, state: :initializing, publisher_token_id: 'ptok:v1:x', published_sequence: nil
250
+ )
251
+ expect(initializing.state).to eq(:initializing)
252
+ complete = Legion::Extensions::Llm::Inventory::PublicationStatus.new(
253
+ instance_key: instance_key, state: :complete, publisher_token_id: 'ptok:v1:x', published_sequence: 2
254
+ )
255
+ expect(complete.published_sequence).to eq(2)
256
+ end
257
+
258
+ it 'rejects an invalid state' do
259
+ expect do
260
+ Legion::Extensions::Llm::Inventory::PublicationStatus.new(
261
+ instance_key: instance_key, state: :available, publisher_token_id: nil, published_sequence: nil
262
+ )
263
+ end.to raise_error(errors::ValidationError)
264
+ end
265
+ end
266
+
267
+ describe inventory::MutationResult do
268
+ it 'accepts applied: true with a normal reason' do
269
+ result = Legion::Extensions::Llm::Inventory::MutationResult.new(
270
+ applied: true, reason: :activated, generation: 3, instance_key: instance_key
271
+ )
272
+ expect(result).to be_applied
273
+ end
274
+
275
+ it 'requires a stale/already-removed reason when applied is false' do
276
+ expect do
277
+ Legion::Extensions::Llm::Inventory::MutationResult.new(
278
+ applied: false, reason: :stale_publisher, generation: 3, instance_key: instance_key
279
+ )
280
+ end.not_to raise_error
281
+ expect do
282
+ Legion::Extensions::Llm::Inventory::MutationResult.new(
283
+ applied: false, reason: :activated, generation: 3, instance_key: instance_key
284
+ )
285
+ end.to raise_error(errors::ValidationError)
286
+ end
287
+
288
+ it 'rejects a stale reason when applied is true' do
289
+ expect do
290
+ Legion::Extensions::Llm::Inventory::MutationResult.new(
291
+ applied: true, reason: :stale_publisher, generation: 3, instance_key: instance_key
292
+ )
293
+ end.to raise_error(errors::ValidationError)
294
+ end
295
+ end
296
+
297
+ it 'exposes taxonomies operations used by the records' do
298
+ expect(taxonomies::OPERATIONS).to include(:chat, :embed)
299
+ end
300
+ end