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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -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 +106 -92
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +52 -67
  8. data/lib/legion/extensions/llm/canonical/params.rb +59 -33
  9. data/lib/legion/extensions/llm/canonical/request.rb +67 -54
  10. data/lib/legion/extensions/llm/canonical/response.rb +61 -76
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
  13. data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
  14. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  15. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  16. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  17. data/lib/legion/extensions/llm/canonical.rb +6 -5
  18. data/lib/legion/extensions/llm/configuration.rb +40 -10
  19. data/lib/legion/extensions/llm/connection.rb +8 -30
  20. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  21. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  22. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  23. data/lib/legion/extensions/llm/error.rb +0 -14
  24. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  25. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  26. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  27. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  28. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -153
  29. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  30. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
  31. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  32. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  33. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  34. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  35. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  36. data/lib/legion/extensions/llm/inventory/records.rb +82 -99
  37. data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
  38. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  39. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
  40. data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
  41. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
  42. data/lib/legion/extensions/llm/provider.rb +192 -315
  43. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  44. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  45. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  46. data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
  47. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  48. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  49. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  50. data/lib/legion/extensions/llm/streaming.rb +50 -35
  51. data/lib/legion/extensions/llm/taxonomies.rb +29 -17
  52. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  53. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  54. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  55. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  56. data/lib/legion/extensions/llm/utils.rb +23 -5
  57. data/lib/legion/extensions/llm/version.rb +1 -1
  58. data/lib/legion/extensions/llm.rb +10 -98
  59. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  60. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  61. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  62. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  63. data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
  64. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  65. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  66. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
  67. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  68. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  69. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  70. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  71. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  72. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  73. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  74. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  75. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
  76. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
  77. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  78. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  79. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  80. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  81. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +178 -67
  82. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  83. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
  84. data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
  85. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  86. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  87. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  88. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  89. data/spec/legion/extensions/llm/inventory/records_spec.rb +129 -41
  90. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
  91. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  92. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  93. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +312 -0
  94. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
  95. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
  96. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  97. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  98. data/spec/legion/extensions/llm/provider_spec.rb +98 -78
  99. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  100. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  101. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  102. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -16
  103. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  104. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  105. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  106. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  107. data/spec/support/fake_llm_provider.rb +45 -39
  108. data/spec/support/fake_ssot_harness.rb +7 -2
  109. data/spec/support/ssot_registry_helpers.rb +3 -2
  110. metadata +15 -54
  111. data/lib/legion/extensions/llm/agent.rb +0 -366
  112. data/lib/legion/extensions/llm/aliases.json +0 -436
  113. data/lib/legion/extensions/llm/aliases.rb +0 -67
  114. data/lib/legion/extensions/llm/attachment.rb +0 -229
  115. data/lib/legion/extensions/llm/chat.rb +0 -354
  116. data/lib/legion/extensions/llm/chunk.rb +0 -10
  117. data/lib/legion/extensions/llm/content.rb +0 -81
  118. data/lib/legion/extensions/llm/context.rb +0 -33
  119. data/lib/legion/extensions/llm/embedding.rb +0 -33
  120. data/lib/legion/extensions/llm/image.rb +0 -109
  121. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  122. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -311
  123. data/lib/legion/extensions/llm/message.rb +0 -118
  124. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  125. data/lib/legion/extensions/llm/model/info.rb +0 -286
  126. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  127. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  128. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  129. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  130. data/lib/legion/extensions/llm/model.rb +0 -11
  131. data/lib/legion/extensions/llm/models.json +0 -57313
  132. data/lib/legion/extensions/llm/models.rb +0 -530
  133. data/lib/legion/extensions/llm/models_schema.json +0 -168
  134. data/lib/legion/extensions/llm/moderation.rb +0 -60
  135. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  136. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  137. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  138. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  139. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  140. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  141. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  142. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  143. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  144. data/lib/legion/extensions/llm/thinking.rb +0 -53
  145. data/lib/legion/extensions/llm/tokens.rb +0 -51
  146. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  147. data/lib/legion/extensions/llm/transcription.rb +0 -39
  148. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  149. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  150. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  151. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  152. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  153. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  154. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -337
  155. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  156. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  157. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  158. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  159. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  160. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  161. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  162. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  163. 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
- 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,16 +50,57 @@ 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,
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 inventory::OfferingRecord do
115
- it 'validates offering_id reproduction and exposes operation views' do
116
- record = build_offering_record
117
- expect(record.supported_operations).to eq(%i[chat])
118
- expect(record.operation_status(operation: :chat)).to eq(:supported)
119
- expect(record.operation_status(operation: :embed)).to eq(:unknown)
120
- expect(record.capability_status(capability: :vision)).to eq(:unknown)
121
- expect(record.unknown_operations).to include(:embed)
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
- it 'rejects an offering_id that does not reproduce' do
125
- expect { build_offering_record(offering_id: "off:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
163
+ describe inventory::LaneRecord do
164
+ def build_record(**overrides)
165
+ build_lane(**overrides)
126
166
  end
127
167
 
128
- it 'rejects a non-InstanceKey and a non-CallableHandle' do
129
- expect { build_offering_record(instance_key: :vllm) }.to raise_error(errors::ValidationError)
130
- expect { build_offering_record(callable_handle: Object.new) }.to raise_error(errors::ValidationError)
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 'returns the quota domain for an operation' do
134
- record = build_offering_record(quota_domains: { chat: 'domain-a' })
135
- expect(record.quota_domain(operation: :chat)).to eq('domain-a')
136
- expect(record.quota_domain(operation: :embed)).to be_nil
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
- describe inventory::LaneRecord do
141
- def build_lane(**overrides)
142
- Legion::Extensions::Llm::Inventory::LaneRecord.new(
143
- lane_id: lane_id, offering_id: offering_id, instance_key: instance_key, provider_family: :vllm,
144
- instance_id: 'h200', model: 'gemma4', tier: :local, operation: :chat, capability_evidence: {},
145
- quota_domain: nil, metadata: {}, callable_handle: callable_handle, publication_source: :provider_catalog,
146
- **scalar_evidence_kwargs, **overrides
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 'builds a valid lane whose id reproduces' do
151
- expect(build_lane.lane_id).to eq(lane_id)
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
- expect { build_lane(lane_id: "lane:v1:#{'0' * 64}") }.to raise_error(errors::ValidationError)
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
- offerings_by_id: { offering_id => build_offering_record },
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 offerings map' do
231
- 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})
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 offerings map' do
242
- 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({})
243
331
  end
244
332
  end
245
333