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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/lex-llm.gemspec +1 -1
- data/lib/legion/extensions/llm/auto_registration.rb +3 -0
- data/lib/legion/extensions/llm/canonical/message.rb +5 -1
- data/lib/legion/extensions/llm/fleet/protocol.rb +12 -0
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +40 -5
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -0
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +166 -2
- data/lib/legion/extensions/llm/inventory/callable_handle.rb +144 -0
- data/lib/legion/extensions/llm/inventory/errors.rb +34 -0
- data/lib/legion/extensions/llm/inventory/evidence.rb +134 -0
- data/lib/legion/extensions/llm/inventory/identity.rb +141 -0
- data/lib/legion/extensions/llm/inventory/immutable_value.rb +96 -0
- data/lib/legion/extensions/llm/inventory/probe_coordinator.rb +117 -0
- data/lib/legion/extensions/llm/inventory/probe_token.rb +146 -0
- data/lib/legion/extensions/llm/inventory/publisher.rb +116 -0
- data/lib/legion/extensions/llm/inventory/records.rb +599 -0
- data/lib/legion/extensions/llm/inventory/registry.rb +636 -0
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +206 -0
- data/lib/legion/extensions/llm/inventory/snapshot.rb +91 -0
- data/lib/legion/extensions/llm/provider.rb +55 -29
- data/lib/legion/extensions/llm/routing/lane_key.rb +4 -0
- data/lib/legion/extensions/llm/routing/model_offering.rb +3 -0
- data/lib/legion/extensions/llm/routing/offering_registry.rb +2 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +54 -0
- data/lib/legion/extensions/llm/routing/records.rb +288 -0
- data/lib/legion/extensions/llm/settings_cascade.rb +120 -0
- data/lib/legion/extensions/llm/taxonomies.rb +90 -0
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -1
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +3 -1
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +18 -0
- data/spec/legion/extensions/llm/canonical/message_spec.rb +24 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +84 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_conformance_spec.rb +13 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +196 -0
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +179 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +26 -0
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +14 -0
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +80 -0
- data/spec/legion/extensions/llm/inventory/callable_handle_spec.rb +127 -0
- data/spec/legion/extensions/llm/inventory/evidence_spec.rb +138 -0
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +280 -0
- data/spec/legion/extensions/llm/inventory/immutable_value_spec.rb +132 -0
- data/spec/legion/extensions/llm/inventory/probe_coordinator_spec.rb +107 -0
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +107 -0
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +144 -0
- data/spec/legion/extensions/llm/inventory/records_spec.rb +300 -0
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +140 -0
- data/spec/legion/extensions/llm/inventory/registry_availability_spec.rb +152 -0
- data/spec/legion/extensions/llm/inventory/registry_callable_spec.rb +63 -0
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +69 -0
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +128 -0
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +109 -0
- data/spec/legion/extensions/llm/provider_contract_spec.rb +20 -0
- data/spec/legion/extensions/llm/provider_spec.rb +83 -0
- data/spec/legion/extensions/llm/routing/provider_outcome_spec.rb +73 -0
- data/spec/legion/extensions/llm/routing/records_spec.rb +173 -0
- data/spec/legion/extensions/llm/settings_cascade_spec.rb +208 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +51 -0
- data/spec/support/fake_llm_provider.rb +11 -0
- data/spec/support/fake_ssot_harness.rb +137 -0
- data/spec/support/ssot_registry_helpers.rb +75 -0
- 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
|