lex-llm 0.7.2 → 0.7.6
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 +4 -4
- data/CHANGELOG.md +24 -0
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +4 -1
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +5 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +4 -2
- data/lib/legion/extensions/llm/inventory/records.rb +56 -9
- data/lib/legion/extensions/llm/inventory/registry.rb +4 -2
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +1 -2
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +242 -0
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +107 -0
- data/lib/legion/extensions/llm/taxonomies.rb +24 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +2 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +3 -3
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +33 -0
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +20 -0
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +11 -4
- data/spec/legion/extensions/llm/inventory/records_spec.rb +60 -7
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +18 -0
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +3 -0
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +308 -0
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +91 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +27 -0
- data/spec/support/ssot_registry_helpers.rb +3 -2
- metadata +5 -1
|
@@ -52,6 +52,8 @@ module Legion
|
|
|
52
52
|
require_relative 'llm/inventory/errors'
|
|
53
53
|
require_relative 'llm/inventory/immutable_value'
|
|
54
54
|
require_relative 'llm/inventory/identity'
|
|
55
|
+
require_relative 'llm/inventory/weight_schema'
|
|
56
|
+
require_relative 'llm/inventory/weight_reconciler'
|
|
55
57
|
require_relative 'llm/inventory/evidence'
|
|
56
58
|
require_relative 'llm/inventory/callable_handle'
|
|
57
59
|
require_relative 'llm/inventory/probe_token'
|
|
@@ -164,10 +164,10 @@ RSpec.shared_examples 'an SSOT v3 provider adapter' do
|
|
|
164
164
|
expect(ssot_harness.inference_call_count(callable: context[:callable])).to eq(1)
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
it '
|
|
167
|
+
it 'accepts "default" as a plain instance_id label and rejects a nil instance' do
|
|
168
168
|
identity = Legion::Extensions::Llm::Inventory::Identity
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
key = identity::InstanceKey.new(provider_family: ssot_harness.provider_family, instance_id: 'default')
|
|
170
|
+
expect(key.instance_id).to eq('default')
|
|
171
171
|
expect { identity::InstanceKey.new(provider_family: ssot_harness.provider_family, instance_id: nil) }
|
|
172
172
|
.to raise_error(Legion::Extensions::Llm::Inventory::Errors::ValidationError)
|
|
173
173
|
end
|
|
@@ -119,11 +119,44 @@ RSpec.describe Legion::Extensions::Llm::Fleet::ProviderResponder do
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
describe 'execution_contract marker validation' do
|
|
122
|
+
it 'preserves false values for both envelope key spellings' do
|
|
123
|
+
symbol_envelope = described_class::FleetEnvelope.new(data: { execution_contract: false })
|
|
124
|
+
string_envelope = described_class::FleetEnvelope.new(data: { 'execution_contract' => false })
|
|
125
|
+
|
|
126
|
+
expect(symbol_envelope[:execution_contract]).to be(false)
|
|
127
|
+
expect(string_envelope[:execution_contract]).to be(false)
|
|
128
|
+
end
|
|
129
|
+
|
|
122
130
|
it 'accepts a legacy v2 envelope with no marker' do
|
|
123
131
|
envelope = described_class.parse_payload(payload)
|
|
124
132
|
expect { described_class.check_envelope!(envelope, provider_family: :ollama) }.not_to raise_error
|
|
125
133
|
end
|
|
126
134
|
|
|
135
|
+
it 'accepts an explicitly nil marker as legacy v2' do
|
|
136
|
+
envelope = described_class.parse_payload(payload.merge(execution_contract: nil))
|
|
137
|
+
expect { described_class.check_envelope!(envelope, provider_family: :ollama) }.not_to raise_error
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
{ symbol: { execution_contract: false }, string: { 'execution_contract' => false } }.each do |spelling, marker|
|
|
141
|
+
it "rejects a false #{spelling}-key marker before provider or registry dispatch" do
|
|
142
|
+
registry = class_spy(Legion::Extensions::Llm::Inventory::Registry)
|
|
143
|
+
allow(provider_class).to receive(:new).and_call_original
|
|
144
|
+
|
|
145
|
+
expect do
|
|
146
|
+
described_class.call(
|
|
147
|
+
payload: payload.merge(marker),
|
|
148
|
+
provider_family: :ollama,
|
|
149
|
+
provider_class: provider_class,
|
|
150
|
+
provider_instances: provider_instances,
|
|
151
|
+
registry: registry
|
|
152
|
+
)
|
|
153
|
+
end.to raise_error(ArgumentError, /unknown execution_contract: false/)
|
|
154
|
+
|
|
155
|
+
expect(provider_class).not_to have_received(:new)
|
|
156
|
+
expect(registry).not_to have_received(:snapshot)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
127
160
|
it 'rejects an unknown nonempty marker' do
|
|
128
161
|
envelope = described_class.parse_payload(payload.merge(execution_contract: 'made_up'))
|
|
129
162
|
expect { described_class.check_envelope!(envelope, provider_family: :ollama) }
|
|
@@ -125,4 +125,24 @@ RSpec.describe Legion::Extensions::Llm::Fleet::WorkerExecution do
|
|
|
125
125
|
|
|
126
126
|
expect(Legion::Extensions::Llm::Fleet::TokenValidator).to have_received(:release_replay!).with('jti-2')
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
describe '.envelope_value' do
|
|
130
|
+
it 'preserves false values for both key spellings' do
|
|
131
|
+
expect(described_class.envelope_value({ execution_contract: false }, :execution_contract)).to be(false)
|
|
132
|
+
expect(described_class.envelope_value({ 'execution_contract' => false }, :execution_contract)).to be(false)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'prefers a present symbol key even when its value is false' do
|
|
136
|
+
mixed = { execution_contract: false, 'execution_contract' => 'exact_offering_v1' }
|
|
137
|
+
|
|
138
|
+
expect(described_class.envelope_value(mixed, :execution_contract)).to be(false)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'preserves legacy absence and nil while returning the exact marker unchanged' do
|
|
142
|
+
expect(described_class.envelope_value({}, :execution_contract)).to be_nil
|
|
143
|
+
expect(described_class.envelope_value({ execution_contract: nil }, :execution_contract)).to be_nil
|
|
144
|
+
expect(described_class.envelope_value({ execution_contract: 'exact_offering_v1' }, :execution_contract))
|
|
145
|
+
.to eq('exact_offering_v1')
|
|
146
|
+
end
|
|
147
|
+
end
|
|
128
148
|
end
|
|
@@ -202,9 +202,16 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Identity do
|
|
|
202
202
|
.to raise_error(errors::ValidationError)
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
it '
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
it 'accepts "default" as a plain instance_id label (no reserved values — v2 parity)' do
|
|
206
|
+
key = instance_key_class.new(provider_family: 'vllm', instance_id: 'default')
|
|
207
|
+
expect(key.instance_id).to eq('default')
|
|
208
|
+
|
|
209
|
+
other = instance_key_class.new(provider_family: 'vllm', instance_id: ' default ')
|
|
210
|
+
expect(key).to eq(other)
|
|
211
|
+
expect(key.hash).to eq(other.hash)
|
|
212
|
+
|
|
213
|
+
expect(key.to_h).to eq(provider_family: :vllm, instance_id: 'default', physical_id: nil)
|
|
214
|
+
expect(key.inspect).to include('instance_id="default"')
|
|
208
215
|
end
|
|
209
216
|
|
|
210
217
|
it 'rejects nil fields' do
|
|
@@ -248,7 +255,7 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Identity do
|
|
|
248
255
|
expect({ bare => :found }[with_physical]).to eq(:found)
|
|
249
256
|
end
|
|
250
257
|
|
|
251
|
-
it '
|
|
258
|
+
it 'accepts "default" for physical_id (no field is reserved)' do
|
|
252
259
|
key = instance_key_class.new(provider_family: :vllm, instance_id: 'apollo', physical_id: 'default')
|
|
253
260
|
expect(key.physical_id).to eq('default')
|
|
254
261
|
end
|
|
@@ -57,7 +57,57 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
57
57
|
)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
def build_lane(**overrides)
|
|
61
|
+
Legion::Extensions::Llm::Inventory::LaneRecord.new(
|
|
62
|
+
lane_id: lane_id, offering_id: offering_id, instance_key: instance_key, provider_family: :vllm,
|
|
63
|
+
instance_id: 'h200', model: 'gemma4', tier: :local, operation: :chat, capability_evidence: {},
|
|
64
|
+
quota_domain: nil, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
|
|
65
|
+
**scalar_evidence_kwargs, **overrides
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
shared_examples 'an atomic immutable weight pair' do
|
|
70
|
+
it 'defaults both omitted fields to the frozen identity pair' do
|
|
71
|
+
record = build_record
|
|
72
|
+
expect(record.weight_inputs).to eq(tier: 100, provider: 100, instance: 100, model_or_offering: 100)
|
|
73
|
+
expect(record.weight_inputs).to be_frozen
|
|
74
|
+
expect(record.base_weight).to eq(100_000_000)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'requires the pair atomically and rejects every invalid shape' do
|
|
78
|
+
valid = { tier: 150, provider: 100, instance: 115, model_or_offering: 100 }
|
|
79
|
+
expect { build_record(weight_inputs: valid) }.to raise_error(errors::ValidationError)
|
|
80
|
+
expect { build_record(base_weight: 172_500_000) }.to raise_error(errors::ValidationError)
|
|
81
|
+
expect { build_record(weight_inputs: [], base_weight: 1) }.to raise_error(errors::ValidationError)
|
|
82
|
+
expect { build_record(weight_inputs: valid.merge('tier' => 150).except(:tier), base_weight: 172_500_000) }
|
|
83
|
+
.to raise_error(errors::ValidationError)
|
|
84
|
+
expect { build_record(weight_inputs: valid.merge(provider: -1), base_weight: -172_500) }
|
|
85
|
+
.to raise_error(errors::ValidationError)
|
|
86
|
+
expect { build_record(weight_inputs: valid.merge(provider: 100.0), base_weight: 172_500_000) }
|
|
87
|
+
.to raise_error(errors::ValidationError)
|
|
88
|
+
expect { build_record(weight_inputs: valid, base_weight: 172_500_000.0) }
|
|
89
|
+
.to raise_error(errors::ValidationError)
|
|
90
|
+
expect { build_record(weight_inputs: valid, base_weight: 1) }.to raise_error(errors::ValidationError)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'defensively freezes a supplied valid pair' do
|
|
94
|
+
source = { tier: 150, provider: 100, instance: 115, model_or_offering: 100 }
|
|
95
|
+
record = build_record(weight_inputs: source, base_weight: 172_500_000)
|
|
96
|
+
source[:tier] = 1
|
|
97
|
+
|
|
98
|
+
expect(record.weight_inputs).to eq(tier: 150, provider: 100, instance: 115, model_or_offering: 100)
|
|
99
|
+
expect(record.weight_inputs).to be_frozen
|
|
100
|
+
expect(record.base_weight).to eq(172_500_000)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
60
104
|
describe inventory::OfferingDraft do
|
|
105
|
+
def build_record(**overrides)
|
|
106
|
+
build_draft(**overrides)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it_behaves_like 'an atomic immutable weight pair'
|
|
110
|
+
|
|
61
111
|
it 'builds a valid frozen draft' do
|
|
62
112
|
draft = build_draft
|
|
63
113
|
expect(draft).to be_frozen
|
|
@@ -112,6 +162,12 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
112
162
|
end
|
|
113
163
|
|
|
114
164
|
describe inventory::OfferingRecord do
|
|
165
|
+
def build_record(**overrides)
|
|
166
|
+
build_offering_record(**overrides)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it_behaves_like 'an atomic immutable weight pair'
|
|
170
|
+
|
|
115
171
|
it 'validates offering_id reproduction and exposes operation views' do
|
|
116
172
|
record = build_offering_record
|
|
117
173
|
expect(record.supported_operations).to eq(%i[chat])
|
|
@@ -138,15 +194,12 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
138
194
|
end
|
|
139
195
|
|
|
140
196
|
describe inventory::LaneRecord do
|
|
141
|
-
def
|
|
142
|
-
|
|
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
|
-
)
|
|
197
|
+
def build_record(**overrides)
|
|
198
|
+
build_lane(**overrides)
|
|
148
199
|
end
|
|
149
200
|
|
|
201
|
+
it_behaves_like 'an atomic immutable weight pair'
|
|
202
|
+
|
|
150
203
|
it 'builds a valid lane whose id reproduces' do
|
|
151
204
|
expect(build_lane.lane_id).to eq(lane_id)
|
|
152
205
|
end
|
|
@@ -74,6 +74,24 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Registry do
|
|
|
74
74
|
expect(snapshot.publication_status(instance_key: key).state).to eq(:complete)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
it 'copies the draft weight pair unchanged onto offering and lane records' do
|
|
78
|
+
weights = { tier: 150, provider: 100, instance: 115, model_or_offering: 100 }
|
|
79
|
+
claim_and_activate(
|
|
80
|
+
key: key, callable: callable, coordinator: coordinator,
|
|
81
|
+
weight_inputs: weights, base_weight: 172_500_000
|
|
82
|
+
)
|
|
83
|
+
snapshot = described_class.snapshot
|
|
84
|
+
offering = snapshot.offerings_for(instance_key: key).first
|
|
85
|
+
lane = snapshot.lanes_for(instance_key: key).first
|
|
86
|
+
|
|
87
|
+
expect(offering.weight_inputs).to eq(weights)
|
|
88
|
+
expect(offering.base_weight).to eq(172_500_000)
|
|
89
|
+
expect(lane.weight_inputs).to eq(weights)
|
|
90
|
+
expect(lane.base_weight).to eq(172_500_000)
|
|
91
|
+
expect(offering.weight_inputs).to be_frozen
|
|
92
|
+
expect(lane.weight_inputs).to be_frozen
|
|
93
|
+
end
|
|
94
|
+
|
|
77
95
|
it 'accepts an explicit complete empty activation as complete, not initializing' do
|
|
78
96
|
token = described_class.claim_instance(instance_key: key, callable: callable, probe_request_handle: coordinator)
|
|
79
97
|
probe = described_class.readiness_probe_started(instance_key: key, publisher_token: token)
|
|
@@ -253,8 +253,11 @@ RSpec.describe Legion::Extensions::Llm::Inventory::ScopedRefresher do
|
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
it 'projects an available instance into an exact five-field legacy lane and round-trips' do
|
|
256
|
+
allow(Legion::Extensions::Llm::Taxonomies).to receive(:lane_type_for).and_call_original
|
|
257
|
+
|
|
256
258
|
result = adapter.sync_snapshot(snapshot: activate_chat, instance_key: key, mutation_reason: :activated)
|
|
257
259
|
expect(result).to eq(:applied)
|
|
260
|
+
expect(Legion::Extensions::Llm::Taxonomies).to have_received(:lane_type_for).with(operation: :chat)
|
|
258
261
|
|
|
259
262
|
legacy_id = 'local:vllm:h200:inference:gemma4'
|
|
260
263
|
expect(fake_inventory.lanes).to have_key(legacy_id)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'legion/extensions/llm/inventory/records'
|
|
7
|
+
require_relative '../../../../support/ssot_registry_helpers'
|
|
8
|
+
|
|
9
|
+
RSpec.describe 'Inventory::WeightReconciler' do
|
|
10
|
+
include SsotRegistryHelpers
|
|
11
|
+
|
|
12
|
+
inventory = Legion::Extensions::Llm::Inventory
|
|
13
|
+
|
|
14
|
+
let(:reconciler) { inventory.const_get(:WeightReconciler) }
|
|
15
|
+
let(:tracker_class) { inventory.const_get(:DormantWeightTracker) }
|
|
16
|
+
let(:key) { instance_key(family: 'vllm', instance: 'helios') }
|
|
17
|
+
let(:mutex) { Mutex.new }
|
|
18
|
+
let(:identity_draft) { drafts.first }
|
|
19
|
+
|
|
20
|
+
it 'loads its Set dependency when directly required without the ambient constant' do
|
|
21
|
+
script = <<~RUBY
|
|
22
|
+
require 'tmpdir'
|
|
23
|
+
|
|
24
|
+
Object.send(:remove_const, :Set)
|
|
25
|
+
$LOADED_FEATURES.delete_if { |path| File.basename(path) == 'set.rb' }
|
|
26
|
+
Dir.mktmpdir do |directory|
|
|
27
|
+
File.write(File.join(directory, 'set.rb'), <<~'SET')
|
|
28
|
+
class Set
|
|
29
|
+
def initialize(values = []) = @values = values.to_a.uniq
|
|
30
|
+
def difference(other) = self.class.new(@values.reject { |value| other.include?(value) })
|
|
31
|
+
def sort_by(&block) = @values.sort_by(&block)
|
|
32
|
+
def replace(other) = (@values = other.to_a; self)
|
|
33
|
+
def include?(value) = @values.include?(value)
|
|
34
|
+
def to_a = @values.dup
|
|
35
|
+
end
|
|
36
|
+
SET
|
|
37
|
+
$LOAD_PATH.unshift(directory)
|
|
38
|
+
require 'legion/extensions/llm/inventory/weight_reconciler'
|
|
39
|
+
tracker = Legion::Extensions::Llm::Inventory::DormantWeightTracker.new
|
|
40
|
+
abort 'wrong result' unless tracker.observe(configured_keys: [[:vllm]], published_keys: []).size == 1
|
|
41
|
+
end
|
|
42
|
+
RUBY
|
|
43
|
+
|
|
44
|
+
_stdout, stderr, status = Open3.capture3(
|
|
45
|
+
RbConfig.ruby, '-Ilib', '-e', script,
|
|
46
|
+
chdir: File.expand_path('../../../../..', __dir__)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(status.success?).to be(true), stderr
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def settings_for(provider: {}, tier: 100)
|
|
53
|
+
{
|
|
54
|
+
extensions: { llm: { vllm: provider } },
|
|
55
|
+
llm: { routing: { tier_weights: { local: tier } } }
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def state_for(draft: identity_draft, published: true, sequence: 0)
|
|
60
|
+
{
|
|
61
|
+
instance_key: key, offerings: [draft].freeze, published: published,
|
|
62
|
+
sequence: sequence, publisher_token: 'ptok:v1:test'
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'rebuilds every offering with the current weight pair in a frozen array' do
|
|
67
|
+
rebuilt = reconciler.rebuild_offerings(
|
|
68
|
+
settings: settings_for(provider: { weight: 110 }), instance_key: key,
|
|
69
|
+
offerings: [identity_draft]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(rebuilt).to be_frozen
|
|
73
|
+
expect(rebuilt.first.weight_inputs)
|
|
74
|
+
.to eq(tier: 100, provider: 110, instance: 100, model_or_offering: 100)
|
|
75
|
+
expect(rebuilt.first.base_weight).to eq(110_000_000)
|
|
76
|
+
expect(rebuilt.first).to eq(
|
|
77
|
+
identity_draft.with(
|
|
78
|
+
weight_inputs: { tier: 100, provider: 110, instance: 100, model_or_offering: 100 },
|
|
79
|
+
base_weight: 110_000_000
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns false and publishes nothing when the rebuilt offerings are equivalent' do
|
|
85
|
+
state = state_for
|
|
86
|
+
replace = instance_spy(Proc)
|
|
87
|
+
|
|
88
|
+
result = reconciler.commit_if_changed!(
|
|
89
|
+
settings: settings_for, instance_id: 'helios', state: state,
|
|
90
|
+
discovered_offerings: [identity_draft], mutex: mutex,
|
|
91
|
+
equivalent: ->(previous, current) { previous == current }, replace: replace
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
expect(result).to be(false)
|
|
95
|
+
expect(replace).not_to have_received(:call)
|
|
96
|
+
expect(state).to include(sequence: 0, offerings: [identity_draft])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'publishes a changed frozen array then atomically updates sequence, cache, and signature' do
|
|
100
|
+
state = state_for
|
|
101
|
+
published = []
|
|
102
|
+
signature = ->(offerings) { offerings.map { |draft| [draft.base_weight, draft.model] } }
|
|
103
|
+
replace = lambda do |instance_id:, state:, offerings:, sequence:|
|
|
104
|
+
published << { instance_id: instance_id, state: state, offerings: offerings, sequence: sequence }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
result = reconciler.commit_if_changed!(
|
|
108
|
+
settings: settings_for(provider: { weight: 110 }), instance_id: 'helios', state: state,
|
|
109
|
+
discovered_offerings: [identity_draft], mutex: mutex,
|
|
110
|
+
equivalent: ->(previous, current) { previous == current }, replace: replace,
|
|
111
|
+
stable_signature: signature
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(result).to be(true)
|
|
115
|
+
expect(published.one?).to be(true)
|
|
116
|
+
expect(published.first).to include(instance_id: 'helios', state: state, sequence: 1)
|
|
117
|
+
expect(published.first[:offerings]).to be_frozen
|
|
118
|
+
expect(published.first[:offerings]).to be_a(Array)
|
|
119
|
+
expect(state[:sequence]).to eq(1)
|
|
120
|
+
expect(state[:offerings]).to equal(published.first[:offerings])
|
|
121
|
+
expect(state[:signature]).to eq([[110_000_000, 'gemma4']])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'updates an unpublished cache without replacing or advancing its sequence' do
|
|
125
|
+
state = state_for(published: false)
|
|
126
|
+
replace = instance_spy(Proc)
|
|
127
|
+
|
|
128
|
+
result = reconciler.commit_if_changed!(
|
|
129
|
+
settings: settings_for(provider: { weight: 110 }), instance_id: 'helios', state: state,
|
|
130
|
+
discovered_offerings: [identity_draft], mutex: mutex,
|
|
131
|
+
equivalent: ->(previous, current) { previous == current }, replace: replace
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
expect(result).to be(true)
|
|
135
|
+
expect(replace).not_to have_received(:call)
|
|
136
|
+
expect(state[:sequence]).to eq(0)
|
|
137
|
+
expect(state[:offerings].first.base_weight).to eq(110_000_000)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'leaves published state unchanged when replacement raises so the next pass can retry' do
|
|
141
|
+
state = state_for
|
|
142
|
+
before = state.dup
|
|
143
|
+
replace = ->(**) { raise 'publisher down' }
|
|
144
|
+
|
|
145
|
+
expect do
|
|
146
|
+
reconciler.commit_if_changed!(
|
|
147
|
+
settings: settings_for(provider: { weight: 110 }), instance_id: 'helios', state: state,
|
|
148
|
+
discovered_offerings: [identity_draft], mutex: mutex,
|
|
149
|
+
equivalent: ->(previous, current) { previous == current }, replace: replace
|
|
150
|
+
)
|
|
151
|
+
end.to raise_error(RuntimeError, 'publisher down')
|
|
152
|
+
expect(state).to eq(before)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'tracks initializing state and activates it only while the same object remains tracked' do
|
|
156
|
+
states = {}
|
|
157
|
+
state = state_for(published: true)
|
|
158
|
+
reconciler.track_initializing!(states: states, state_key: 'helios', state: state, mutex: mutex)
|
|
159
|
+
expect(state[:published]).to be(false)
|
|
160
|
+
expect(states['helios']).to equal(state)
|
|
161
|
+
|
|
162
|
+
activations = []
|
|
163
|
+
activated = reconciler.activate_tracked!(
|
|
164
|
+
settings: settings_for(provider: { weight: 120 }), instance_id: 'helios',
|
|
165
|
+
state_key: 'helios', state: state, states: states, mutex: mutex,
|
|
166
|
+
probe_token: :probe, activation_sequence: ->(tracked) { tracked.fetch(:sequence) },
|
|
167
|
+
activate: ->(**kwargs) { activations << kwargs }
|
|
168
|
+
)
|
|
169
|
+
expect(activated).to be(true)
|
|
170
|
+
expect(activations.first[:offerings].first.base_weight).to eq(120_000_000)
|
|
171
|
+
expect(state[:offerings]).to equal(activations.first[:offerings])
|
|
172
|
+
expect(state[:published]).to be(true)
|
|
173
|
+
|
|
174
|
+
states.delete('helios')
|
|
175
|
+
expect(
|
|
176
|
+
reconciler.activate_tracked!(
|
|
177
|
+
settings: settings_for, instance_id: 'helios', state_key: 'helios', state: state,
|
|
178
|
+
states: states, mutex: mutex, probe_token: :probe,
|
|
179
|
+
activation_sequence: ->(tracked) { tracked.fetch(:sequence) }, activate: ->(**) { raise 'not called' }
|
|
180
|
+
)
|
|
181
|
+
).to be(false)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'leaves initializing state unchanged when activation publication raises' do
|
|
185
|
+
state = state_for(published: false)
|
|
186
|
+
states = { 'helios' => state }
|
|
187
|
+
before = state.dup
|
|
188
|
+
|
|
189
|
+
expect do
|
|
190
|
+
reconciler.activate_tracked!(
|
|
191
|
+
settings: settings_for(provider: { weight: 120 }), instance_id: 'helios',
|
|
192
|
+
state_key: 'helios', state: state, states: states, mutex: mutex,
|
|
193
|
+
probe_token: :probe, activation_sequence: ->(tracked) { tracked.fetch(:sequence) + 1 },
|
|
194
|
+
activate: ->(**) { raise 'activation down' }
|
|
195
|
+
)
|
|
196
|
+
end.to raise_error(RuntimeError, 'activation down')
|
|
197
|
+
expect(state).to eq(before)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'logs each dormant key once, clears it on appearance, and logs re-disappearance' do
|
|
201
|
+
tracker = tracker_class.new
|
|
202
|
+
model_settings = settings_for(provider: { models: { ghost: { weight: 125 } } })
|
|
203
|
+
states = { 'helios' => state_for }
|
|
204
|
+
logged = []
|
|
205
|
+
observe = lambda do
|
|
206
|
+
reconciler.observe_dormant!(
|
|
207
|
+
settings: model_settings, provider_family: :vllm, states: states,
|
|
208
|
+
mutex: mutex, tracker: tracker, dormant_logger: ->(key_value) { logged << key_value }
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
observe.call
|
|
213
|
+
observe.call
|
|
214
|
+
expect(logged).to eq([[:vllm, :model, 'ghost']])
|
|
215
|
+
|
|
216
|
+
states['helios'] = state_for(draft: drafts(model: 'ghost', native: 'ghost').first)
|
|
217
|
+
observe.call
|
|
218
|
+
states['helios'] = state_for
|
|
219
|
+
observe.call
|
|
220
|
+
expect(logged).to eq([[:vllm, :model, 'ghost'], [:vllm, :model, 'ghost']])
|
|
221
|
+
|
|
222
|
+
tracker.clear!
|
|
223
|
+
observe.call
|
|
224
|
+
expect(logged.size).to eq(3)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'does not let an unpublished state satisfy a configured key and rejects malformed scopes' do
|
|
228
|
+
tracker = tracker_class.new
|
|
229
|
+
logged = []
|
|
230
|
+
states = { 'helios' => state_for(published: false) }
|
|
231
|
+
settings = settings_for(provider: { models: { gemma4: { weight: 125 } } })
|
|
232
|
+
reconciler.observe_dormant!(
|
|
233
|
+
settings: settings, provider_family: :vllm, states: states, mutex: mutex,
|
|
234
|
+
tracker: tracker, dormant_logger: ->(value) { logged << value }
|
|
235
|
+
)
|
|
236
|
+
expect(logged).to include([:vllm, :model, 'gemma4'])
|
|
237
|
+
|
|
238
|
+
expect do
|
|
239
|
+
reconciler.observe_dormant!(
|
|
240
|
+
settings: settings_for(provider: { models: false }), provider_family: :vllm,
|
|
241
|
+
states: {}, mutex: mutex, tracker: tracker, dormant_logger: ->(_value) {}
|
|
242
|
+
)
|
|
243
|
+
end.to raise_error(ArgumentError, /weight configuration scope must be a Hash/)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'uses the exact provider, instance, model, instance-model, and offering dormant keys' do
|
|
247
|
+
offering_id = inventory::Identity.offering_id(instance_key: key, provider_native_key: 'gemma4')
|
|
248
|
+
settings = settings_for(
|
|
249
|
+
provider: {
|
|
250
|
+
weight: false,
|
|
251
|
+
models: { gemma4: { weight: 101 } },
|
|
252
|
+
instances: { helios: { weight: 0, models: { gemma4: { weight: 102 } } } },
|
|
253
|
+
offerings: { offering_id => { weight: 103 } }
|
|
254
|
+
},
|
|
255
|
+
tier: 777
|
|
256
|
+
)
|
|
257
|
+
tracker = tracker_class.new
|
|
258
|
+
logged = []
|
|
259
|
+
|
|
260
|
+
reconciler.observe_dormant!(
|
|
261
|
+
settings: settings, provider_family: :vllm, states: {}, mutex: mutex,
|
|
262
|
+
tracker: tracker, dormant_logger: ->(value) { logged << value }
|
|
263
|
+
)
|
|
264
|
+
expect(logged).to contain_exactly(
|
|
265
|
+
%i[vllm provider],
|
|
266
|
+
[:vllm, :instance, 'helios'],
|
|
267
|
+
[:vllm, :model, 'gemma4'],
|
|
268
|
+
[:vllm, :instance, 'helios', :model, 'gemma4'],
|
|
269
|
+
[:vllm, :offering, offering_id]
|
|
270
|
+
)
|
|
271
|
+
expect(logged.flatten).not_to include(:tier)
|
|
272
|
+
|
|
273
|
+
reconciler.observe_dormant!(
|
|
274
|
+
settings: settings, provider_family: :vllm,
|
|
275
|
+
states: { 'helios' => state_for }, mutex: mutex,
|
|
276
|
+
tracker: tracker, dormant_logger: ->(value) { logged << value }
|
|
277
|
+
)
|
|
278
|
+
expect(logged.size).to eq(5)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it 'serializes concurrent ordinary commits and leaves cache equal to the final publication' do
|
|
282
|
+
state = state_for
|
|
283
|
+
publications = []
|
|
284
|
+
ready = Queue.new
|
|
285
|
+
start = Queue.new
|
|
286
|
+
replace = lambda do |offerings:, sequence:, **|
|
|
287
|
+
publications << [sequence, offerings]
|
|
288
|
+
end
|
|
289
|
+
run = lambda do |weight|
|
|
290
|
+
ready << true
|
|
291
|
+
start.pop
|
|
292
|
+
reconciler.commit_if_changed!(
|
|
293
|
+
settings: settings_for(provider: { weight: weight }), instance_id: 'helios', state: state,
|
|
294
|
+
discovered_offerings: [identity_draft], mutex: mutex,
|
|
295
|
+
equivalent: ->(previous, current) { previous == current }, replace: replace
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
threads = [Thread.new { run.call(110) }, Thread.new { run.call(120) }]
|
|
299
|
+
2.times { ready.pop }
|
|
300
|
+
2.times { start << true }
|
|
301
|
+
threads.each(&:join)
|
|
302
|
+
|
|
303
|
+
expect(publications.map(&:first)).to eq([1, 2])
|
|
304
|
+
expect(state[:sequence]).to eq(2)
|
|
305
|
+
expect(state[:offerings]).to equal(publications.last.last)
|
|
306
|
+
expect(state[:offerings].first.weight_inputs).to eq(publications.last.last.first.weight_inputs)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
5
|
+
require 'legion/extensions/llm/inventory/weight_schema'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'Inventory::WeightSchema' do
|
|
8
|
+
inventory = Legion::Extensions::Llm::Inventory
|
|
9
|
+
|
|
10
|
+
let(:schema) { inventory.const_get(:WeightSchema) }
|
|
11
|
+
let(:instance_key) do
|
|
12
|
+
inventory::Identity::InstanceKey.new(provider_family: :vllm, instance_id: 'helios')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def settings_for(provider: {})
|
|
16
|
+
{ extensions: { llm: { vllm: provider } }, llm: { routing: { tier_weights: { direct: 100 } } } }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inputs(settings, native: 'deployment-x', model: 'model-y')
|
|
20
|
+
schema.weight_inputs(
|
|
21
|
+
settings: settings, instance_key: instance_key,
|
|
22
|
+
provider_native_key: native, model: model, tier: :direct
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'multiplies independent scopes without double-counting and lets offering override model' do
|
|
27
|
+
offering_id = inventory::Identity.offering_id(instance_key: instance_key, provider_native_key: 'deployment-x')
|
|
28
|
+
settings = settings_for(
|
|
29
|
+
provider: {
|
|
30
|
+
weight: 100,
|
|
31
|
+
models: { 'model-y' => { weight: 200 } },
|
|
32
|
+
instances: { helios: { weight: 115 } },
|
|
33
|
+
offerings: { offering_id => { weight: 300 } }
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
result = inputs(settings)
|
|
38
|
+
expect(result).to eq(tier: 100, provider: 100, instance: 115, model_or_offering: 300)
|
|
39
|
+
expect(schema.base_weight(result)).to eq(345_000_000)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'uses provider model weight and lets instance model weight override it' do
|
|
43
|
+
provider = { models: { 'model-y' => { weight: 200 } }, instances: { helios: {} } }
|
|
44
|
+
expect(inputs(settings_for(provider: provider))[:model_or_offering]).to eq(200)
|
|
45
|
+
|
|
46
|
+
provider[:instances][:helios][:models] = { 'model-y' => { weight: 250 } }
|
|
47
|
+
expect(inputs(settings_for(provider: provider))[:model_or_offering]).to eq(250)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'preserves zero, rejects false, and defaults all missing axes to identity' do
|
|
51
|
+
zero = inputs(settings_for(provider: { weight: 0, instances: { helios: {} } }))
|
|
52
|
+
expect(zero[:provider]).to eq(0)
|
|
53
|
+
expect(schema.base_weight(zero)).to eq(0)
|
|
54
|
+
|
|
55
|
+
expect { inputs(settings_for(provider: { weight: false })) }
|
|
56
|
+
.to raise_error(ArgumentError, /weight component/)
|
|
57
|
+
|
|
58
|
+
identity = inputs(settings_for)
|
|
59
|
+
expect(identity).to eq(tier: 100, provider: 100, instance: 100, model_or_offering: 100)
|
|
60
|
+
expect(schema.base_weight(identity)).to eq(100_000_000)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'derives offering identity from the provider-native key rather than the model' do
|
|
64
|
+
deployment_id = inventory::Identity.offering_id(
|
|
65
|
+
instance_key: instance_key, provider_native_key: 'deployment-x'
|
|
66
|
+
)
|
|
67
|
+
model_id = inventory::Identity.offering_id(instance_key: instance_key, provider_native_key: 'model-y')
|
|
68
|
+
settings = settings_for(provider: { offerings: { deployment_id => { weight: 321 }, model_id => { weight: 999 } } })
|
|
69
|
+
|
|
70
|
+
expect(inputs(settings)[:model_or_offering]).to eq(321)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'rejects every present malformed configuration scope instead of defaulting it' do
|
|
74
|
+
cases = {
|
|
75
|
+
'extensions.llm' => { extensions: { llm: false } },
|
|
76
|
+
'extensions.llm.vllm' => { extensions: { llm: { vllm: false } } },
|
|
77
|
+
'provider.instances' => settings_for(provider: { instances: false }),
|
|
78
|
+
'provider.instances.helios' => settings_for(provider: { instances: { helios: false } }),
|
|
79
|
+
'llm.routing.tier_weights' => { extensions: { llm: {} }, llm: { routing: { tier_weights: false } } },
|
|
80
|
+
'provider.offerings' => settings_for(provider: { offerings: false }),
|
|
81
|
+
'provider.models.model-y' => settings_for(provider: { models: { 'model-y' => false } }),
|
|
82
|
+
'instance.models.model-y' => settings_for(
|
|
83
|
+
provider: { instances: { helios: { models: { 'model-y' => false } } } }
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cases.each do |path, settings|
|
|
88
|
+
expect { inputs(settings) }.to raise_error(ArgumentError, /#{Regexp.escape(path)}/)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|