lex-llm 0.7.3 → 0.8.0
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 +106 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
- data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
- data/lib/legion/extensions/llm/canonical/message.rb +52 -67
- data/lib/legion/extensions/llm/canonical/params.rb +59 -33
- data/lib/legion/extensions/llm/canonical/request.rb +67 -54
- data/lib/legion/extensions/llm/canonical/response.rb +61 -76
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
- data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
- 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 +6 -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 -153
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
- 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 +82 -99
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -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 +34 -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 +29 -17
- 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 +10 -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 +57 -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_spec.rb +68 -148
- 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/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -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 +178 -67
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
- 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 +129 -41
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
- 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 +312 -0
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -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 +98 -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 -16
- 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
- data/spec/support/ssot_registry_helpers.rb +3 -2
- metadata +15 -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 -311
- 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 -337
- 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
|
@@ -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,16 +50,57 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
48
50
|
)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
def
|
|
52
|
-
Legion::Extensions::Llm::Inventory::
|
|
53
|
-
|
|
54
|
-
tier: :local,
|
|
55
|
-
|
|
53
|
+
def build_lane(**overrides)
|
|
54
|
+
Legion::Extensions::Llm::Inventory::LaneRecord.new(
|
|
55
|
+
lane_id: lane_id, instance_key: instance_key, provider_family: :vllm,
|
|
56
|
+
instance_id: 'h200', model: 'gemma4', tier: :local, operation: :chat, capability_evidence: {},
|
|
57
|
+
quota_domain: nil, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
|
|
56
58
|
**scalar_evidence_kwargs, **overrides
|
|
57
59
|
)
|
|
58
60
|
end
|
|
59
61
|
|
|
62
|
+
shared_examples 'an atomic immutable weight pair' do
|
|
63
|
+
it 'defaults both omitted fields to the frozen identity pair' do
|
|
64
|
+
record = build_record
|
|
65
|
+
expect(record.weight_inputs).to eq(tier: 100, provider: 100, instance: 100, model_or_offering: 100)
|
|
66
|
+
expect(record.weight_inputs).to be_frozen
|
|
67
|
+
expect(record.base_weight).to eq(100_000_000)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'requires the pair atomically and rejects every invalid shape' do
|
|
71
|
+
valid = { tier: 150, provider: 100, instance: 115, model_or_offering: 100 }
|
|
72
|
+
expect { build_record(weight_inputs: valid) }.to raise_error(errors::ValidationError)
|
|
73
|
+
expect { build_record(base_weight: 172_500_000) }.to raise_error(errors::ValidationError)
|
|
74
|
+
expect { build_record(weight_inputs: [], base_weight: 1) }.to raise_error(errors::ValidationError)
|
|
75
|
+
expect { build_record(weight_inputs: valid.merge('tier' => 150).except(:tier), base_weight: 172_500_000) }
|
|
76
|
+
.to raise_error(errors::ValidationError)
|
|
77
|
+
expect { build_record(weight_inputs: valid.merge(provider: -1), base_weight: -172_500) }
|
|
78
|
+
.to raise_error(errors::ValidationError)
|
|
79
|
+
expect { build_record(weight_inputs: valid.merge(provider: 100.0), base_weight: 172_500_000) }
|
|
80
|
+
.to raise_error(errors::ValidationError)
|
|
81
|
+
expect { build_record(weight_inputs: valid, base_weight: 172_500_000.0) }
|
|
82
|
+
.to raise_error(errors::ValidationError)
|
|
83
|
+
expect { build_record(weight_inputs: valid, base_weight: 1) }.to raise_error(errors::ValidationError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'defensively freezes a supplied valid pair' do
|
|
87
|
+
source = { tier: 150, provider: 100, instance: 115, model_or_offering: 100 }
|
|
88
|
+
record = build_record(weight_inputs: source, base_weight: 172_500_000)
|
|
89
|
+
source[:tier] = 1
|
|
90
|
+
|
|
91
|
+
expect(record.weight_inputs).to eq(tier: 150, provider: 100, instance: 115, model_or_offering: 100)
|
|
92
|
+
expect(record.weight_inputs).to be_frozen
|
|
93
|
+
expect(record.base_weight).to eq(172_500_000)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
60
97
|
describe inventory::OfferingDraft do
|
|
98
|
+
def build_record(**overrides)
|
|
99
|
+
build_draft(**overrides)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it_behaves_like 'an atomic immutable weight pair'
|
|
103
|
+
|
|
61
104
|
it 'builds a valid frozen draft' do
|
|
62
105
|
draft = build_draft
|
|
63
106
|
expect(draft).to be_frozen
|
|
@@ -111,52 +154,77 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
111
154
|
end
|
|
112
155
|
end
|
|
113
156
|
|
|
114
|
-
describe
|
|
115
|
-
it '
|
|
116
|
-
|
|
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)
|
|
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)
|
|
122
160
|
end
|
|
161
|
+
end
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
describe inventory::LaneRecord do
|
|
164
|
+
def build_record(**overrides)
|
|
165
|
+
build_lane(**overrides)
|
|
126
166
|
end
|
|
127
167
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
168
|
+
it_behaves_like 'an atomic immutable weight pair'
|
|
169
|
+
|
|
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
|
|
131
173
|
end
|
|
132
174
|
|
|
133
|
-
it '
|
|
134
|
-
|
|
135
|
-
expect(
|
|
136
|
-
|
|
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
|
+
)
|
|
137
183
|
end
|
|
138
|
-
end
|
|
139
184
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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'
|
|
147
189
|
)
|
|
190
|
+
lane = build_lane(lane_id: embed_id, operation: :embed)
|
|
191
|
+
expect(lane.lane_id).to eq('local:vllm:h200:embedding:gemma4')
|
|
148
192
|
end
|
|
149
193
|
|
|
150
|
-
it '
|
|
151
|
-
|
|
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)
|
|
152
203
|
end
|
|
153
204
|
|
|
154
205
|
it 'rejects provider_family/instance_id not equal to instance_key' do
|
|
155
206
|
expect { build_lane(instance_id: 'helios1') }.to raise_error(errors::ValidationError)
|
|
156
207
|
end
|
|
157
208
|
|
|
158
|
-
it 'rejects a lane_id that does not reproduce' do
|
|
159
|
-
|
|
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/)
|
|
215
|
+
end
|
|
216
|
+
|
|
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/)
|
|
223
|
+
end
|
|
224
|
+
|
|
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)
|
|
160
228
|
end
|
|
161
229
|
end
|
|
162
230
|
|
|
@@ -216,19 +284,39 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
216
284
|
|
|
217
285
|
describe inventory::InstanceRecord do
|
|
218
286
|
def build_instance(**overrides)
|
|
287
|
+
lane = build_lane
|
|
219
288
|
Legion::Extensions::Llm::Inventory::InstanceRecord.new(
|
|
220
289
|
instance_key: instance_key, callable_handle: callable_handle,
|
|
221
290
|
availability: Legion::Extensions::Llm::Inventory::AvailabilityFact.new(
|
|
222
291
|
state: :available, availability_revision: 1, source: :startup_readiness, reason: 'activated', observed_at: nil
|
|
223
292
|
),
|
|
224
|
-
|
|
293
|
+
lanes_by_id: { lane.lane_id => lane },
|
|
225
294
|
publisher_id: 'pub:v1:x', publisher_token_id: 'ptok:v1:y', published_sequence: 1, published_at: Time.now,
|
|
226
295
|
**overrides
|
|
227
296
|
)
|
|
228
297
|
end
|
|
229
298
|
|
|
230
|
-
it 'builds an available instance with a complete
|
|
231
|
-
|
|
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})
|
|
232
320
|
end
|
|
233
321
|
|
|
234
322
|
it 'rejects an initializing availability' do
|
|
@@ -238,8 +326,8 @@ RSpec.describe Legion::Extensions::Llm::Inventory::Records do
|
|
|
238
326
|
expect { build_instance(availability: initializing) }.to raise_error(errors::ValidationError)
|
|
239
327
|
end
|
|
240
328
|
|
|
241
|
-
it 'accepts an explicit empty
|
|
242
|
-
expect(build_instance(
|
|
329
|
+
it 'accepts an explicit empty lanes map' do
|
|
330
|
+
expect(build_instance(lanes_by_id: {}).lanes_by_id).to eq({})
|
|
243
331
|
end
|
|
244
332
|
end
|
|
245
333
|
|