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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -0
  3. data/RULES.md +97 -0
  4. data/lib/legion/extensions/llm/auto_registration.rb +4 -13
  5. data/lib/legion/extensions/llm/canonical/chunk.rb +107 -94
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +53 -69
  8. data/lib/legion/extensions/llm/canonical/params.rb +58 -34
  9. data/lib/legion/extensions/llm/canonical/request.rb +68 -56
  10. data/lib/legion/extensions/llm/canonical/response.rb +62 -78
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
  13. data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
  14. data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
  15. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  16. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  17. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  18. data/lib/legion/extensions/llm/canonical.rb +7 -5
  19. data/lib/legion/extensions/llm/configuration.rb +40 -10
  20. data/lib/legion/extensions/llm/connection.rb +8 -30
  21. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  22. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  23. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  24. data/lib/legion/extensions/llm/error.rb +0 -14
  25. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  26. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  27. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  28. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  29. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
  30. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  31. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
  32. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  33. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  34. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  35. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  36. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  37. data/lib/legion/extensions/llm/inventory/records.rb +36 -100
  38. data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
  39. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  40. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
  41. data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
  42. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -182
  43. data/lib/legion/extensions/llm/provider.rb +192 -315
  44. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  45. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  46. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  47. data/lib/legion/extensions/llm/responses/tool_arguments.rb +48 -0
  48. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  49. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  50. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  51. data/lib/legion/extensions/llm/streaming.rb +50 -35
  52. data/lib/legion/extensions/llm/taxonomies.rb +14 -26
  53. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  54. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  55. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  56. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  57. data/lib/legion/extensions/llm/utils.rb +23 -5
  58. data/lib/legion/extensions/llm/version.rb +1 -1
  59. data/lib/legion/extensions/llm.rb +8 -98
  60. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  61. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  62. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  63. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  64. data/spec/legion/extensions/llm/canonical/params_spec.rb +55 -136
  65. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  66. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  67. data/spec/legion/extensions/llm/canonical/thinking/config_spec.rb +222 -0
  68. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
  69. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  70. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  71. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  72. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  73. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  74. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  75. data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +1 -2
  76. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  77. data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
  78. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
  79. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  80. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
  81. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -0
  82. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  83. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  84. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  85. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  86. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
  87. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  88. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
  89. data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
  90. data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
  91. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  92. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  93. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  94. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  95. data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
  96. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
  97. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  98. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  99. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
  100. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
  101. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -68
  102. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  103. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  104. data/spec/legion/extensions/llm/provider_spec.rb +97 -78
  105. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  106. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  107. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  108. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
  109. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  110. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  111. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  112. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  113. data/spec/support/fake_llm_provider.rb +45 -39
  114. data/spec/support/fake_ssot_harness.rb +7 -2
  115. metadata +13 -54
  116. data/lib/legion/extensions/llm/agent.rb +0 -366
  117. data/lib/legion/extensions/llm/aliases.json +0 -436
  118. data/lib/legion/extensions/llm/aliases.rb +0 -67
  119. data/lib/legion/extensions/llm/attachment.rb +0 -229
  120. data/lib/legion/extensions/llm/chat.rb +0 -354
  121. data/lib/legion/extensions/llm/chunk.rb +0 -10
  122. data/lib/legion/extensions/llm/content.rb +0 -81
  123. data/lib/legion/extensions/llm/context.rb +0 -33
  124. data/lib/legion/extensions/llm/embedding.rb +0 -33
  125. data/lib/legion/extensions/llm/image.rb +0 -109
  126. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  127. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
  128. data/lib/legion/extensions/llm/message.rb +0 -118
  129. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  130. data/lib/legion/extensions/llm/model/info.rb +0 -286
  131. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  132. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  133. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  134. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  135. data/lib/legion/extensions/llm/model.rb +0 -11
  136. data/lib/legion/extensions/llm/models.json +0 -57313
  137. data/lib/legion/extensions/llm/models.rb +0 -530
  138. data/lib/legion/extensions/llm/models_schema.json +0 -168
  139. data/lib/legion/extensions/llm/moderation.rb +0 -60
  140. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  141. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  142. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  143. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  144. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  145. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  146. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  147. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  148. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  149. data/lib/legion/extensions/llm/thinking.rb +0 -53
  150. data/lib/legion/extensions/llm/tokens.rb +0 -51
  151. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  152. data/lib/legion/extensions/llm/transcription.rb +0 -39
  153. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  154. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  155. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  156. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  157. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  158. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  159. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
  160. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  161. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  162. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  163. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  164. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  165. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  166. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  167. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  168. 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 OfferingRecord LaneRecord AvailabilityFact
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 'retains legacy Types aliases and adds no new SSOT alias' do
73
- expect(Legion::Extensions::Llm::Types.const_defined?(:ModelOffering, false)).to be(true)
74
- expect(Legion::Extensions::Llm::Types.const_defined?(:InstanceKey, false)).to be(false)
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
- fixture_path = File.expand_path('../conformance/fixtures/ssot_identity_vectors.json', __dir__)
11
- fixture = Legion::JSON.load(File.read(fixture_path))
12
-
13
- def hex(binary)
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 'binding identity vectors (recomputed field by field)' do
18
- fixture[:vectors].each_with_index do |vector, index|
19
- context "with vector #{index}: #{vector[:provider_family]}+#{vector[:instance_id]}" do
20
- let(:instance_key) do
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
- it 'reproduces every framed field as lowercase hex bytes' do
28
- expect(hex(described_class.length_frame(value: instance_key.provider_family)))
29
- .to eq(vector[:framed_hex][:provider_family])
30
- expect(hex(described_class.length_frame(value: instance_key.instance_id)))
31
- .to eq(vector[:framed_hex][:instance_id])
32
- expect(hex(described_class.length_frame(value: vector[:provider_native_key])))
33
- .to eq(vector[:framed_hex][:provider_native_key])
34
- expect(hex(described_class.length_frame(value: vector[:operation])))
35
- .to eq(vector[:framed_hex][:operation])
36
- expect(hex(described_class.length_frame(value: vector[:model])))
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
- it 'reproduces the offering hash input and offering_id' do
41
- input = described_class.length_frame(value: instance_key.provider_family) +
42
- described_class.length_frame(value: instance_key.instance_id) +
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
- offering_id = described_class.offering_id(
47
- instance_key: instance_key, provider_native_key: vector[:provider_native_key]
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
- it 'reproduces the lane hash input and lane_id' do
53
- offering_id = described_class.offering_id(
54
- instance_key: instance_key, provider_native_key: vector[:provider_native_key]
55
- )
56
- input = "lane-v1\x00".b +
57
- described_class.length_frame(value: instance_key.provider_family) +
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(lane_id).to eq(vector[:expected_lane_id])
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 'independence and delimiter safety' do
92
- it 'produces distinct offering/lane IDs for the same model on two instance IDs' do
93
- expect(fixture[:vectors][0][:expected_offering_id]).not_to eq(fixture[:vectors][1][:expected_offering_id])
94
- expect(fixture[:vectors][0][:expected_lane_id]).not_to eq(fixture[:vectors][1][:expected_lane_id])
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 'is tier-independent: tier is never a framing input' do
98
- vector = fixture[:vectors][0]
99
- instance_key = instance_key_class.new(provider_family: vector[:provider_family], instance_id: vector[:instance_id])
100
- %i[local frontier].each do |_tier|
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 'frames punctuation inside a component without delimiter ambiguity' do
115
- vector = fixture[:vectors][2]
116
- instance_key = instance_key_class.new(provider_family: vector[:provider_family], instance_id: vector[:instance_id])
117
- expect(instance_key.instance_id).to eq('prod:eastus')
118
- expect(
119
- described_class.offering_id(instance_key: instance_key, provider_native_key: vector[:provider_native_key])
120
- ).to eq(vector[:expected_offering_id])
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 '.length_frame' do
125
- it 'is decimal byte length + ASCII colon + exact UTF-8 bytes, in binary' do
126
- frame = described_class.length_frame(value: 'gemma4')
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 'counts UTF-8 bytes, not characters' do
132
- composed = [0x00e9].pack('U*') # single codepoint => two UTF-8 bytes
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(with_physical.hash)
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: :ollama, instance_id: 'apollo', physical_id: 'localhost:11434')
272
- apollo_embed = instance_key_class.new(provider_family: :ollama, instance_id: 'apollo-embed', physical_id: 'localhost:11434')
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 seven documented module methods' do
290
+ it 'exposes exactly the documented module methods — no digest machinery' do
280
291
  expected = %i[
281
- normalize_text normalize_enum length_frame offering_id lane_id
282
- validate_offering_id! validate_lane_id!
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 'derives pub:v1: and ptok:v1: correlation IDs and is frozen' do
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 'derives publisher_token_id from the secret domain-separated SHA-256' do
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 = described_class.derive_token_id(secret)
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
- 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)
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 '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)
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.lane_id(instance_key: instance_key, operation: :chat, model: 'gemma4', offering_id: offering_id)
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, offering_id: offering_id, instance_key: instance_key, provider_family: :vllm,
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 inventory::OfferingRecord do
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
- build_offering_record(**overrides)
165
+ build_lane(**overrides)
167
166
  end
168
167
 
169
168
  it_behaves_like 'an atomic immutable weight pair'
170
169
 
171
- it 'validates offering_id reproduction and exposes operation views' do
172
- record = build_offering_record
173
- expect(record.supported_operations).to eq(%i[chat])
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 'rejects an offering_id that does not reproduce' do
181
- expect { build_offering_record(offering_id: "off:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
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 'rejects a non-InstanceKey and a non-CallableHandle' do
185
- expect { build_offering_record(instance_key: :vllm) }.to raise_error(errors::ValidationError)
186
- expect { build_offering_record(callable_handle: Object.new) }.to raise_error(errors::ValidationError)
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 'returns the quota domain for an operation' do
190
- record = build_offering_record(quota_domains: { chat: 'domain-a' })
191
- expect(record.quota_domain(operation: :chat)).to eq('domain-a')
192
- expect(record.quota_domain(operation: :embed)).to be_nil
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
- describe inventory::LaneRecord do
197
- def build_record(**overrides)
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
- it_behaves_like 'an atomic immutable weight pair'
202
-
203
- it 'builds a valid lane whose id reproduces' do
204
- expect(build_lane.lane_id).to eq(lane_id)
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 provider_family/instance_id not equal to instance_key' do
208
- expect { build_lane(instance_id: 'helios1') }.to raise_error(errors::ValidationError)
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 does not reproduce' do
212
- expect { build_lane(lane_id: "lane:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
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
- offerings_by_id: { offering_id => build_offering_record },
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 offerings map' do
284
- expect(build_instance.offerings_by_id).to be_frozen
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 offerings map' do
295
- expect(build_instance(offerings_by_id: {}).offerings_by_id).to eq({})
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