lex-llm 0.7.6 → 0.8.4
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 +139 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +107 -94
- data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
- data/lib/legion/extensions/llm/canonical/message.rb +53 -69
- data/lib/legion/extensions/llm/canonical/params.rb +58 -34
- data/lib/legion/extensions/llm/canonical/request.rb +68 -56
- data/lib/legion/extensions/llm/canonical/response.rb +62 -78
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
- data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
- data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
- data/lib/legion/extensions/llm/canonical.rb +7 -5
- data/lib/legion/extensions/llm/configuration.rb +40 -10
- data/lib/legion/extensions/llm/connection.rb +8 -30
- data/lib/legion/extensions/llm/credential_sources.rb +32 -49
- data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
- data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
- data/lib/legion/extensions/llm/error.rb +0 -14
- data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
- data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
- data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
- data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
- data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
- data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
- data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
- data/lib/legion/extensions/llm/inventory/records.rb +36 -100
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -182
- data/lib/legion/extensions/llm/provider.rb +192 -315
- data/lib/legion/extensions/llm/provider_contract.rb +25 -8
- data/lib/legion/extensions/llm/provider_settings.rb +5 -26
- data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
- data/lib/legion/extensions/llm/responses/tool_arguments.rb +48 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
- data/lib/legion/extensions/llm/routing/records.rb +34 -17
- data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
- data/lib/legion/extensions/llm/streaming.rb +50 -35
- data/lib/legion/extensions/llm/taxonomies.rb +14 -26
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
- data/lib/legion/extensions/llm/utils.rb +23 -5
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +8 -98
- data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
- data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
- data/spec/legion/extensions/llm/canonical/params_spec.rb +55 -136
- data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
- data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
- data/spec/legion/extensions/llm/canonical/thinking/config_spec.rb +222 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
- data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
- data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
- data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +1 -2
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
- data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
- data/spec/legion/extensions/llm/error_spec.rb +2 -12
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
- data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
- data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -68
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
- data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
- data/spec/legion/extensions/llm/provider_spec.rb +97 -78
- data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
- data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
- data/spec/legion/extensions/llm/utils_spec.rb +26 -7
- data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
- data/spec/legion/extensions/llm_extension_spec.rb +5 -5
- data/spec/support/fake_llm_provider.rb +45 -39
- data/spec/support/fake_ssot_harness.rb +7 -2
- metadata +13 -54
- data/lib/legion/extensions/llm/agent.rb +0 -366
- data/lib/legion/extensions/llm/aliases.json +0 -436
- data/lib/legion/extensions/llm/aliases.rb +0 -67
- data/lib/legion/extensions/llm/attachment.rb +0 -229
- data/lib/legion/extensions/llm/chat.rb +0 -354
- data/lib/legion/extensions/llm/chunk.rb +0 -10
- data/lib/legion/extensions/llm/content.rb +0 -81
- data/lib/legion/extensions/llm/context.rb +0 -33
- data/lib/legion/extensions/llm/embedding.rb +0 -33
- data/lib/legion/extensions/llm/image.rb +0 -109
- data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
- data/lib/legion/extensions/llm/message.rb +0 -118
- data/lib/legion/extensions/llm/mime_type.rb +0 -75
- data/lib/legion/extensions/llm/model/info.rb +0 -286
- data/lib/legion/extensions/llm/model/modalities.rb +0 -26
- data/lib/legion/extensions/llm/model/pricing.rb +0 -52
- data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
- data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
- data/lib/legion/extensions/llm/model.rb +0 -11
- data/lib/legion/extensions/llm/models.json +0 -57313
- data/lib/legion/extensions/llm/models.rb +0 -530
- data/lib/legion/extensions/llm/models_schema.json +0 -168
- data/lib/legion/extensions/llm/moderation.rb +0 -60
- data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
- data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
- data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
- data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
- data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
- data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
- data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
- data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
- data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
- data/lib/legion/extensions/llm/thinking.rb +0 -53
- data/lib/legion/extensions/llm/tokens.rb +0 -51
- data/lib/legion/extensions/llm/tool_call.rb +0 -34
- data/lib/legion/extensions/llm/transcription.rb +0 -39
- data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
- data/spec/legion/extensions/llm/agent_spec.rb +0 -179
- data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
- data/spec/legion/extensions/llm/context_spec.rb +0 -127
- data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
- data/spec/legion/extensions/llm/message_spec.rb +0 -64
- data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
- data/spec/legion/extensions/llm/models_spec.rb +0 -104
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
|
@@ -47,7 +47,7 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Boot do
|
|
|
47
47
|
it 'exposes every require-order constant' do
|
|
48
48
|
%i[
|
|
49
49
|
Errors ImmutableValue Identity Evidence CallableHandle DispatchLease ProbeToken ProbeRequest
|
|
50
|
-
ProbeCoordinator PublisherToken OfferingDraft
|
|
50
|
+
ProbeCoordinator PublisherToken OfferingDraft LaneRecord AvailabilityFact
|
|
51
51
|
ReadinessResult InstanceRecord PublicationStatus MutationResult Snapshot Registry Publisher
|
|
52
52
|
].each { |const| expect(inventory.const_defined?(const)).to be(true), "missing Inventory::#{const}" }
|
|
53
53
|
%i[AttemptTargetKey QuotaDomainKey Exclusion Selection Rejection BodyModelHintDecision ProviderOutcome].each do |const|
|
|
@@ -69,9 +69,9 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Boot do
|
|
|
69
69
|
expect(Thread.list.size).to eq(baseline)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
it '
|
|
73
|
-
expect(Legion::Extensions::Llm
|
|
74
|
-
expect(Legion::Extensions::Llm::
|
|
72
|
+
it 'removes the legacy Types aliases (0.8.0 rip)' do
|
|
73
|
+
expect(Legion::Extensions::Llm.const_defined?(:Types, false)).to be(false)
|
|
74
|
+
expect(Legion::Extensions::Llm::Inventory.const_defined?(:InstanceKey, false)).to be(false)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
it 'preserves the optional transport message registration (autoload or loaded)' do
|
|
@@ -6,131 +6,130 @@ require 'legion/extensions/llm/inventory/identity'
|
|
|
6
6
|
RSpec.describe Legion::Extensions::Llm::Inventory::Identity do
|
|
7
7
|
let(:errors) { Legion::Extensions::Llm::Inventory::Errors }
|
|
8
8
|
let(:instance_key_class) { Legion::Extensions::Llm::Inventory::Identity::InstanceKey }
|
|
9
|
+
let(:taxonomies) { Legion::Extensions::Llm::Taxonomies }
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
binary.b.unpack1('H*')
|
|
11
|
+
def compose(tier: :local, family: 'vllm', instance: 'h200', type: :inference, model: 'gemma4')
|
|
12
|
+
described_class.compose_lane_id(
|
|
13
|
+
tier: tier, provider_family: family, instance_id: instance, type: type, model: model
|
|
14
|
+
)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
describe '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
instance_key_class.new(
|
|
22
|
-
provider_family: vector[:provider_family],
|
|
23
|
-
instance_id: vector[:instance_id]
|
|
24
|
-
)
|
|
25
|
-
end
|
|
17
|
+
describe 'compose_lane_id (the ONE 5-tuple composer, G22)' do
|
|
18
|
+
it 'joins tier:provider_family:instance_id:type:model exactly' do
|
|
19
|
+
expect(compose).to eq('local:vllm:h200:inference:gemma4')
|
|
20
|
+
end
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.to eq(vector[:framed_hex][:model])
|
|
38
|
-
end
|
|
22
|
+
it 'is byte-identical to the v0.6.16 ScopedRefresher.compose_id shape' do
|
|
23
|
+
lane_fields = { tier: 'frontier', provider_family: 'openai', instance_id: 'prod', type: 'embedding', model: 'text-embed-3' }
|
|
24
|
+
expected = "#{lane_fields[:tier]}:#{lane_fields[:provider_family]}:#{lane_fields[:instance_id]}:#{lane_fields[:type]}:#{lane_fields[:model]}"
|
|
25
|
+
expect(
|
|
26
|
+
described_class.compose_lane_id(
|
|
27
|
+
tier: lane_fields[:tier], provider_family: lane_fields[:provider_family],
|
|
28
|
+
instance_id: lane_fields[:instance_id], type: lane_fields[:type], model: lane_fields[:model]
|
|
29
|
+
)
|
|
30
|
+
).to eq(expected)
|
|
31
|
+
end
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
described_class.length_frame(value: vector[:provider_native_key])
|
|
44
|
-
expect(hex(input)).to eq(vector[:offering_hash_input_hex])
|
|
33
|
+
it 'preserves colons inside the model part (Ollama model:tag, Bedrock ids)' do
|
|
34
|
+
id = compose(family: 'ollama', instance: 'apollo', model: 'llama3.2:8b-instruct')
|
|
35
|
+
expect(id).to eq('local:ollama:apollo:inference:llama3.2:8b-instruct')
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
expect(offering_id).to eq(vector[:expected_offering_id])
|
|
50
|
-
end
|
|
37
|
+
bedrock = compose(family: 'bedrock', instance: 'us-east-1', type: :inference, model: 'anthropic.claude-opus-4-7:0')
|
|
38
|
+
expect(bedrock).to eq('local:bedrock:us-east-1:inference:anthropic.claude-opus-4-7:0')
|
|
39
|
+
end
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
described_class.length_frame(value: instance_key.instance_id) +
|
|
59
|
-
described_class.length_frame(value: vector[:operation]) +
|
|
60
|
-
described_class.length_frame(value: vector[:model]) +
|
|
61
|
-
described_class.length_frame(value: offering_id)
|
|
62
|
-
expect(hex(input)).to eq(vector[:lane_hash_input_hex])
|
|
63
|
-
|
|
64
|
-
lane_id = described_class.lane_id(
|
|
65
|
-
instance_key: instance_key,
|
|
66
|
-
operation: vector[:operation].to_sym,
|
|
67
|
-
model: vector[:model],
|
|
68
|
-
offering_id: offering_id
|
|
41
|
+
it 'round-trips through parse_lane_id for every type and tier' do
|
|
42
|
+
taxonomies::TYPES.each do |type|
|
|
43
|
+
taxonomies::TIERS.each do |tier|
|
|
44
|
+
id = compose(tier: tier, type: type, model: 'model:tag:v1')
|
|
45
|
+
expect(described_class.parse_lane_id(id)).to eq(
|
|
46
|
+
[tier.to_s, 'vllm', 'h200', type.to_s, 'model:tag:v1']
|
|
69
47
|
)
|
|
70
|
-
expect(
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it 'validates the reproduced offering_id and lane_id' do
|
|
74
|
-
offering_id = vector[:expected_offering_id]
|
|
75
|
-
expect(
|
|
76
|
-
described_class.validate_offering_id!(
|
|
77
|
-
value: offering_id, instance_key: instance_key, provider_native_key: vector[:provider_native_key]
|
|
78
|
-
)
|
|
79
|
-
).to eq(offering_id)
|
|
80
|
-
expect(
|
|
81
|
-
described_class.validate_lane_id!(
|
|
82
|
-
value: vector[:expected_lane_id], instance_key: instance_key,
|
|
83
|
-
operation: vector[:operation].to_sym, model: vector[:model], offering_id: offering_id
|
|
84
|
-
)
|
|
85
|
-
).to eq(vector[:expected_lane_id])
|
|
48
|
+
expect(described_class.validate_lane_id!(value: id)).to eq(id)
|
|
86
49
|
end
|
|
87
50
|
end
|
|
88
51
|
end
|
|
89
52
|
end
|
|
90
53
|
|
|
91
|
-
describe '
|
|
92
|
-
it '
|
|
93
|
-
expect(
|
|
94
|
-
|
|
54
|
+
describe 'parse_lane_id (bounded split — the 5th part keeps its colons)' do
|
|
55
|
+
it 'returns exactly five parts with the model keeping its colons' do
|
|
56
|
+
expect(described_class.parse_lane_id('local:vllm:h200:inference:a:b:c')).to eq(
|
|
57
|
+
%w[local vllm h200 inference a:b:c]
|
|
58
|
+
)
|
|
95
59
|
end
|
|
96
60
|
|
|
97
|
-
it '
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
offering_id = described_class.offering_id(
|
|
102
|
-
instance_key: instance_key, provider_native_key: vector[:provider_native_key]
|
|
103
|
-
)
|
|
104
|
-
expect(offering_id).to eq(vector[:expected_offering_id])
|
|
105
|
-
expect(
|
|
106
|
-
described_class.lane_id(
|
|
107
|
-
instance_key: instance_key, operation: vector[:operation].to_sym,
|
|
108
|
-
model: vector[:model], offering_id: offering_id
|
|
109
|
-
)
|
|
110
|
-
).to eq(vector[:expected_lane_id])
|
|
111
|
-
end
|
|
61
|
+
it 'accepts a Symbol input' do
|
|
62
|
+
expect(described_class.parse_lane_id(:'local:vllm:h200:inference:gemma4')).to eq(
|
|
63
|
+
%w[local vllm h200 inference gemma4]
|
|
64
|
+
)
|
|
112
65
|
end
|
|
113
66
|
|
|
114
|
-
it '
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
).to eq(
|
|
67
|
+
it 'rejects fewer than five parts' do
|
|
68
|
+
expect { described_class.parse_lane_id('local:vllm:h200:inference') }
|
|
69
|
+
.to raise_error(errors::ValidationError, /exactly 5 parts, got 4/)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'never returns more than five parts (colons do not split further)' do
|
|
73
|
+
expect(described_class.parse_lane_id('local:vllm:h200:inference:x:y').length).to eq(5)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'rejects non-text input' do
|
|
77
|
+
expect { described_class.parse_lane_id(123) }.to raise_error(errors::ValidationError)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe 'validate_lane_id!' do
|
|
82
|
+
it 'accepts a well-formed 5 tuple and returns it unchanged' do
|
|
83
|
+
expect(described_class.validate_lane_id!(value: 'cloud:gemini:default:audio:speech-2')).to eq('cloud:gemini:default:audio:speech-2')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'rejects any non-5-tuple value on the plain shape check alone' do
|
|
87
|
+
expect { described_class.validate_lane_id!(value: 'weird:identity') }
|
|
88
|
+
.to raise_error(errors::ValidationError, /exactly 5 parts, got 2/)
|
|
89
|
+
expect { described_class.validate_lane_id!(value: 'a:b:c:d') }
|
|
90
|
+
.to raise_error(errors::ValidationError, /exactly 5 parts, got 4/)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'rejects a tier outside Taxonomies::TIERS' do
|
|
94
|
+
expect { described_class.validate_lane_id!(value: 'bogus:vllm:h200:inference:gemma4') }
|
|
95
|
+
.to raise_error(errors::ValidationError, /tier/)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'rejects a type outside Taxonomies::TYPES' do
|
|
99
|
+
expect { described_class.validate_lane_id!(value: 'local:vllm:h200:moderation:gemma4') }
|
|
100
|
+
.to raise_error(errors::ValidationError, /type/)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'rejects an empty provider_family or instance_id' do
|
|
104
|
+
expect { described_class.validate_lane_id!(value: 'local::h200:inference:gemma4') }
|
|
105
|
+
.to raise_error(errors::ValidationError, /provider_family/)
|
|
106
|
+
expect { described_class.validate_lane_id!(value: 'local:vllm::inference:gemma4') }
|
|
107
|
+
.to raise_error(errors::ValidationError, /instance_id/)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'rejects an empty model' do
|
|
111
|
+
expect { described_class.validate_lane_id!(value: 'local:vllm:h200:inference:') }
|
|
112
|
+
.to raise_error(errors::ValidationError, /model/)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'rejects a non-NFC provider_family, instance_id, or model' do
|
|
116
|
+
decomposed = [0x0065, 0x0301].pack('U*') # e + combining acute; not NFC
|
|
117
|
+
expect { described_class.validate_lane_id!(value: "local:#{decomposed}vllm:h200:inference:gemma4") }
|
|
118
|
+
.to raise_error(errors::ValidationError, /provider_family/)
|
|
119
|
+
expect { described_class.validate_lane_id!(value: "local:vllm:#{decomposed}h200:inference:gemma4") }
|
|
120
|
+
.to raise_error(errors::ValidationError, /instance_id/)
|
|
121
|
+
expect { described_class.validate_lane_id!(value: "local:vllm:h200:inference:#{decomposed}emma4") }
|
|
122
|
+
.to raise_error(errors::ValidationError, /model/)
|
|
121
123
|
end
|
|
122
124
|
end
|
|
123
125
|
|
|
124
|
-
describe '
|
|
125
|
-
it '
|
|
126
|
-
|
|
127
|
-
expect(frame.encoding).to eq(Encoding::BINARY)
|
|
128
|
-
expect(frame).to eq('6:gemma4'.b)
|
|
126
|
+
describe 'independence' do
|
|
127
|
+
it 'produces distinct ids for the same model on two instance ids' do
|
|
128
|
+
expect(compose(instance: 'h200')).not_to eq(compose(instance: 'helios1'))
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
it '
|
|
132
|
-
|
|
133
|
-
expect(described_class.length_frame(value: composed)).to eq('2:'.b + composed.b)
|
|
131
|
+
it 'is tier-SENSITIVE: the tier is the first id part (unlike the deleted digest)' do
|
|
132
|
+
expect(compose(tier: :local)).not_to eq(compose(tier: :frontier))
|
|
134
133
|
end
|
|
135
134
|
end
|
|
136
135
|
|
|
@@ -250,8 +249,7 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Identity do
|
|
|
250
249
|
provider_family: :vllm, instance_id: 'apollo', physical_id: '10.0.0.9:9000'
|
|
251
250
|
)
|
|
252
251
|
expect(bare).to eq(with_physical)
|
|
253
|
-
expect(bare.hash).to eq(
|
|
254
|
-
expect(with_physical).to eq(other_physical)
|
|
252
|
+
expect(bare.hash).to eq(other_physical.hash)
|
|
255
253
|
expect({ bare => :found }[with_physical]).to eq(:found)
|
|
256
254
|
end
|
|
257
255
|
|
|
@@ -268,20 +266,39 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Identity do
|
|
|
268
266
|
end
|
|
269
267
|
|
|
270
268
|
it 'still distinguishes distinct config names on the same physical endpoint' do
|
|
271
|
-
apollo = instance_key_class.new(provider_family:
|
|
272
|
-
apollo_embed = instance_key_class.new(provider_family:
|
|
269
|
+
apollo = instance_key_class.new(provider_family: 'ollama', instance_id: 'apollo', physical_id: 'localhost:11434')
|
|
270
|
+
apollo_embed = instance_key_class.new(provider_family: 'ollama', instance_id: 'apollo-embed', physical_id: 'localhost:11434')
|
|
273
271
|
expect(apollo).not_to eq(apollo_embed)
|
|
274
272
|
end
|
|
275
273
|
end
|
|
276
274
|
end
|
|
277
275
|
|
|
276
|
+
describe 'M6 — the single config→instance-id derivation (owner law)' do
|
|
277
|
+
it 'derives the operator config name from a provider config' do
|
|
278
|
+
config = Struct.new(:instance_id).new('h200')
|
|
279
|
+
expect(described_class.instance_id(config)).to eq('h200')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'uses the ordinary "default" label (not a reserved value) when no instance name is configured' do
|
|
283
|
+
expect(described_class.instance_id(Struct.new(:instance_id).new(nil))).to eq('default')
|
|
284
|
+
expect(described_class.instance_id({})).to eq('default')
|
|
285
|
+
expect(described_class.instance_id(nil)).to eq('default')
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
278
289
|
describe 'public surface' do
|
|
279
|
-
it 'exposes exactly the
|
|
290
|
+
it 'exposes exactly the documented module methods — no digest machinery' do
|
|
280
291
|
expected = %i[
|
|
281
|
-
normalize_text normalize_enum
|
|
282
|
-
|
|
292
|
+
normalize_text normalize_enum compose_lane_id parse_lane_id
|
|
293
|
+
validate_lane_id! instance_id
|
|
283
294
|
].sort
|
|
284
295
|
expect(described_class.singleton_methods(false).sort).to eq(expected)
|
|
285
296
|
end
|
|
297
|
+
|
|
298
|
+
it 'defines no digest encoder or legacy validator' do
|
|
299
|
+
%i[length_frame offering_id lane_id validate_offering_id!].each do |gone|
|
|
300
|
+
expect(described_class.singleton_methods(false)).not_to include(gone)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
286
303
|
end
|
|
287
304
|
end
|
|
@@ -30,13 +30,13 @@ RSpec.describe Legion::Extensions::Llm::Inventory::ProbeToken do
|
|
|
30
30
|
let(:token) { described_class.issue(instance_key: instance_key) }
|
|
31
31
|
|
|
32
32
|
describe '.issue' do
|
|
33
|
-
it '
|
|
33
|
+
it 'mints pub:v1: and ptok:v1: correlation IDs and is frozen' do
|
|
34
34
|
expect(token.publisher_id).to start_with('pub:v1:')
|
|
35
35
|
expect(token.publisher_token_id).to start_with('ptok:v1:')
|
|
36
36
|
expect(token).to be_frozen
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
it '
|
|
39
|
+
it 'mints a distinct token id per issue' do
|
|
40
40
|
other = described_class.issue(instance_key: instance_key)
|
|
41
41
|
expect(token.publisher_token_id).not_to eq(other.publisher_token_id)
|
|
42
42
|
end
|
|
@@ -70,9 +70,9 @@ RSpec.describe Legion::Extensions::Llm::Inventory::ProbeToken do
|
|
|
70
70
|
expect(token.authenticates?(Object.new)).to be(false)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
it 'authenticates two tokens built from the same secret' do
|
|
73
|
+
it 'authenticates two tokens built from the same secret (fencing is the secret comparison)' do
|
|
74
74
|
secret = 'deadbeef' * 8
|
|
75
|
-
token_id =
|
|
75
|
+
token_id = "ptok:v1:#{SecureRandom.uuid}"
|
|
76
76
|
a = described_class.new(instance_key: instance_key, secret: secret, publisher_id: 'pub:v1:x', publisher_token_id: token_id)
|
|
77
77
|
b = described_class.new(instance_key: instance_key, secret: secret, publisher_id: 'pub:v1:y', publisher_token_id: token_id)
|
|
78
78
|
expect(a.authenticates?(b)).to be(true)
|
|
@@ -11,21 +11,6 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Publisher do
|
|
|
11
11
|
|
|
12
12
|
before { inventory::Registry.reset! }
|
|
13
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
14
|
let(:plain_publisher) { described_class.new(provider_family: :vllm) }
|
|
30
15
|
|
|
31
16
|
def full_activate(pub)
|
|
@@ -49,56 +34,18 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Publisher do
|
|
|
49
34
|
expect(result.applied).to be(true)
|
|
50
35
|
expect(plain_publisher.snapshot.instance(instance_key: instance_key)).to be_nil
|
|
51
36
|
end
|
|
52
|
-
end
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(
|
|
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)
|
|
38
|
+
it 'M7: exposes no legacy dual-sink seam (the compatibility adapter is deleted)' do
|
|
39
|
+
expect { described_class.new(provider_family: :vllm, compatibility_adapter: Object.new) }
|
|
40
|
+
.to raise_error(ArgumentError, /compatibility_adapter/)
|
|
41
|
+
expect(described_class.instance_method(:initialize).parameters).not_to include(%i[key compatibility_adapter])
|
|
62
42
|
end
|
|
63
43
|
|
|
64
|
-
it '
|
|
65
|
-
token = full_activate(
|
|
66
|
-
|
|
67
|
-
|
|
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)
|
|
44
|
+
it 'replaces the same scope when a new claim supersedes the publisher' do
|
|
45
|
+
token = full_activate(plain_publisher)
|
|
46
|
+
plain_publisher.claim_instance(instance_id: 'h200', callable: fake_callable, probe_request_handle: probe_coordinator(instance_key))
|
|
47
|
+
result = plain_publisher.replace_instance_snapshot(instance_id: 'h200', publisher_token: token, offerings: drafts, sequence: 9)
|
|
71
48
|
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
49
|
end
|
|
103
50
|
end
|
|
104
51
|
|
|
@@ -16,9 +16,11 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
16
16
|
end
|
|
17
17
|
let(:callable_handle) { inventory::CallableHandle.new(handle_id: 'call:v1:test', callable: callable_class.new) }
|
|
18
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
19
|
let(:lane_id) do
|
|
21
|
-
identity.
|
|
20
|
+
identity.compose_lane_id(
|
|
21
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200',
|
|
22
|
+
type: taxonomies.lane_type_for(operation: :chat), model: 'gemma4'
|
|
23
|
+
)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def value_unknown
|
|
@@ -48,18 +50,9 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
48
50
|
)
|
|
49
51
|
end
|
|
50
52
|
|
|
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
53
|
def build_lane(**overrides)
|
|
61
54
|
Legion::Extensions::Llm::Inventory::LaneRecord.new(
|
|
62
|
-
lane_id: lane_id,
|
|
55
|
+
lane_id: lane_id, instance_key: instance_key, provider_family: :vllm,
|
|
63
56
|
instance_id: 'h200', model: 'gemma4', tier: :local, operation: :chat, capability_evidence: {},
|
|
64
57
|
quota_domain: nil, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
|
|
65
58
|
**scalar_evidence_kwargs, **overrides
|
|
@@ -161,55 +154,77 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
161
154
|
end
|
|
162
155
|
end
|
|
163
156
|
|
|
164
|
-
describe
|
|
157
|
+
describe 'OfferingRecord (deleted — D2: in the one bucket an offering IS a lane)' do
|
|
158
|
+
it 'is not defined' do
|
|
159
|
+
expect(inventory.const_defined?(:OfferingRecord, false)).to be(false)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe inventory::LaneRecord do
|
|
165
164
|
def build_record(**overrides)
|
|
166
|
-
|
|
165
|
+
build_lane(**overrides)
|
|
167
166
|
end
|
|
168
167
|
|
|
169
168
|
it_behaves_like 'an atomic immutable weight pair'
|
|
170
169
|
|
|
171
|
-
it '
|
|
172
|
-
|
|
173
|
-
expect(
|
|
174
|
-
expect(record.operation_status(operation: :chat)).to eq(:supported)
|
|
175
|
-
expect(record.operation_status(operation: :embed)).to eq(:unknown)
|
|
176
|
-
expect(record.capability_status(capability: :vision)).to eq(:unknown)
|
|
177
|
-
expect(record.unknown_operations).to include(:embed)
|
|
170
|
+
it 'builds a valid lane whose id reproduces from its fields' do
|
|
171
|
+
expect(build_lane.lane_id).to eq('local:vllm:h200:inference:gemma4')
|
|
172
|
+
expect(build_lane.lane_id).to be_frozen
|
|
178
173
|
end
|
|
179
174
|
|
|
180
|
-
it '
|
|
181
|
-
expect
|
|
175
|
+
it 'has no offering_id member — the 5 tuple is the single id' do
|
|
176
|
+
expect(build_lane.members).not_to include(:offering_id)
|
|
177
|
+
expect(build_lane.to_h.keys).to contain_exactly(
|
|
178
|
+
:lane_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
|
|
179
|
+
:capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
180
|
+
:model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
|
|
181
|
+
:publication_source, :weight_inputs, :base_weight
|
|
182
|
+
)
|
|
182
183
|
end
|
|
183
184
|
|
|
184
|
-
it '
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
it 'derives the id type part from the operation (embed → embedding)' do
|
|
186
|
+
embed_id = identity.compose_lane_id(
|
|
187
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200',
|
|
188
|
+
type: taxonomies.lane_type_for(operation: :embed), model: 'gemma4'
|
|
189
|
+
)
|
|
190
|
+
lane = build_lane(lane_id: embed_id, operation: :embed)
|
|
191
|
+
expect(lane.lane_id).to eq('local:vllm:h200:embedding:gemma4')
|
|
187
192
|
end
|
|
188
193
|
|
|
189
|
-
it '
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
it 'keeps colons inside the model part of the id' do
|
|
195
|
+
colon_model = 'llama3.2:8b-instruct'
|
|
196
|
+
id = identity.compose_lane_id(
|
|
197
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200',
|
|
198
|
+
type: taxonomies.lane_type_for(operation: :chat), model: colon_model
|
|
199
|
+
)
|
|
200
|
+
lane = build_lane(lane_id: id, model: colon_model)
|
|
201
|
+
expect(lane.lane_id).to eq("local:vllm:h200:inference:#{colon_model}")
|
|
202
|
+
expect(identity.parse_lane_id(lane.lane_id).last).to eq(colon_model)
|
|
193
203
|
end
|
|
194
|
-
end
|
|
195
204
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
build_lane(**overrides)
|
|
205
|
+
it 'rejects provider_family/instance_id not equal to instance_key' do
|
|
206
|
+
expect { build_lane(instance_id: 'helios1') }.to raise_error(errors::ValidationError)
|
|
199
207
|
end
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
it 'rejects a lane_id that does not reproduce from the record fields' do
|
|
210
|
+
other = identity.compose_lane_id(
|
|
211
|
+
tier: :local, provider_family: 'vllm', instance_id: 'helios1',
|
|
212
|
+
type: :inference, model: 'gemma4'
|
|
213
|
+
)
|
|
214
|
+
expect { build_lane(lane_id: other) }.to raise_error(errors::ValidationError, /does not reproduce/)
|
|
205
215
|
end
|
|
206
216
|
|
|
207
|
-
it 'rejects
|
|
208
|
-
|
|
217
|
+
it 'rejects a lane_id whose type part disagrees with the operation' do
|
|
218
|
+
wrong_type = identity.compose_lane_id(
|
|
219
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200',
|
|
220
|
+
type: :embedding, model: 'gemma4'
|
|
221
|
+
)
|
|
222
|
+
expect { build_lane(lane_id: wrong_type) }.to raise_error(errors::ValidationError, /does not reproduce/)
|
|
209
223
|
end
|
|
210
224
|
|
|
211
|
-
it 'rejects a lane_id that
|
|
212
|
-
expect { build_lane(lane_id:
|
|
225
|
+
it 'rejects a lane_id that is not a 5-tuple' do
|
|
226
|
+
expect { build_lane(lane_id: 'not-a-five-tuple') }
|
|
227
|
+
.to raise_error(errors::ValidationError)
|
|
213
228
|
end
|
|
214
229
|
end
|
|
215
230
|
|
|
@@ -269,19 +284,39 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
269
284
|
|
|
270
285
|
describe inventory::InstanceRecord do
|
|
271
286
|
def build_instance(**overrides)
|
|
287
|
+
lane = build_lane
|
|
272
288
|
Legion::Extensions::Llm::Inventory::InstanceRecord.new(
|
|
273
289
|
instance_key: instance_key, callable_handle: callable_handle,
|
|
274
290
|
availability: Legion::Extensions::Llm::Inventory::AvailabilityFact.new(
|
|
275
291
|
state: :available, availability_revision: 1, source: :startup_readiness, reason: 'activated', observed_at: nil
|
|
276
292
|
),
|
|
277
|
-
|
|
293
|
+
lanes_by_id: { lane.lane_id => lane },
|
|
278
294
|
publisher_id: 'pub:v1:x', publisher_token_id: 'ptok:v1:y', published_sequence: 1, published_at: Time.now,
|
|
279
295
|
**overrides
|
|
280
296
|
)
|
|
281
297
|
end
|
|
282
298
|
|
|
283
|
-
it 'builds an available instance with a complete
|
|
284
|
-
|
|
299
|
+
it 'builds an available instance with a complete 5-tuple-keyed lanes map' do
|
|
300
|
+
record = build_instance
|
|
301
|
+
expect(record.lanes_by_id).to be_frozen
|
|
302
|
+
expect(record.lanes_by_id.keys).to eq(%w[local:vllm:h200:inference:gemma4])
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'rejects a lane whose key does not equal its own id' do
|
|
306
|
+
lane = build_lane
|
|
307
|
+
expect do
|
|
308
|
+
build_instance(lanes_by_id: { 'local:vllm:h200:inference:other' => lane })
|
|
309
|
+
end.to raise_error(errors::ValidationError, /key mismatch/)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'rejects a lane that does not share the instance callable/key' do
|
|
313
|
+
other_key = identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'helios1')
|
|
314
|
+
other_id = identity.compose_lane_id(
|
|
315
|
+
tier: :local, provider_family: 'vllm', instance_id: 'helios1', type: :inference, model: 'gemma4'
|
|
316
|
+
)
|
|
317
|
+
other_lane = build_lane(lane_id: other_id, instance_key: other_key, instance_id: 'helios1')
|
|
318
|
+
expect { build_instance(lanes_by_id: { other_id => other_lane }) }
|
|
319
|
+
.to raise_error(errors::ValidationError, %r{callable/key})
|
|
285
320
|
end
|
|
286
321
|
|
|
287
322
|
it 'rejects an initializing availability' do
|
|
@@ -291,8 +326,8 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
291
326
|
expect { build_instance(availability: initializing) }.to raise_error(errors::ValidationError)
|
|
292
327
|
end
|
|
293
328
|
|
|
294
|
-
it 'accepts an explicit empty
|
|
295
|
-
expect(build_instance(
|
|
329
|
+
it 'accepts an explicit empty lanes map' do
|
|
330
|
+
expect(build_instance(lanes_by_id: {}).lanes_by_id).to eq({})
|
|
296
331
|
end
|
|
297
332
|
end
|
|
298
333
|
|