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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'legion/logging'
4
3
  require 'legion/extensions/llm/inventory/errors'
5
4
  require 'legion/extensions/llm/inventory/identity'
6
5
  require 'legion/extensions/llm/inventory/registry'
@@ -12,16 +11,14 @@ module Legion
12
11
  # Provider-facing direct wrapper over the local Registry. It constructs
13
12
  # and validates the exact InstanceKey from its fixed provider family and
14
13
  # delegates to the registry with the exact kwargs. It does no retries,
15
- # selection, scheduling, defaulting, provider discovery, or exception
16
- # swallowing beyond the explicitly injected, best-effort post-commit
17
- # old-coordinator projection. See phase-1-lex-llm-additive.md section 13.1.
14
+ # selection, scheduling, defaulting, or provider discovery. M7: the
15
+ # legacy dual-sink seam (compatibility_adapter / old-coordinator
16
+ # projection) is deleted — one representation, one routing authority;
17
+ # the SSOT registry is the only inventory sink.
18
18
  class Publisher
19
- include Legion::Logging::Helper
20
-
21
- def initialize(provider_family:, registry: Registry, compatibility_adapter: nil)
19
+ def initialize(provider_family:, registry: Registry)
22
20
  @provider_family = provider_family
23
21
  @registry = registry
24
- @compatibility_adapter = compatibility_adapter
25
22
  end
26
23
 
27
24
  # Every instance operation takes `instance_id:` — the operator's
@@ -30,9 +27,7 @@ module Legion
30
27
  # dedup and diagnostics only. See Identity::InstanceKey.
31
28
  def claim_instance(instance_id:, callable:, probe_request_handle:, physical_id: nil)
32
29
  key = instance_key(instance_id: instance_id, physical_id: physical_id)
33
- token = @registry.claim_instance(instance_key: key, callable: callable, probe_request_handle: probe_request_handle)
34
- sync_compatibility(key, :claimed)
35
- token
30
+ @registry.claim_instance(instance_key: key, callable: callable, probe_request_handle: probe_request_handle)
36
31
  end
37
32
 
38
33
  def readiness_probe_started(instance_id:, publisher_token:, physical_id: nil)
@@ -42,35 +37,29 @@ module Legion
42
37
  end
43
38
 
44
39
  def activate_instance_snapshot(instance_id:, publisher_token:, offerings:, sequence:, probe_token:, physical_id: nil) # rubocop:disable Metrics/ParameterLists
45
- key = instance_key(instance_id: instance_id, physical_id: physical_id)
46
- result = @registry.activate_instance_snapshot(
47
- publisher_token: publisher_token, instance_key: key, offerings: offerings,
48
- sequence: sequence, probe_token: probe_token
40
+ @registry.activate_instance_snapshot(
41
+ publisher_token: publisher_token, instance_key: instance_key(instance_id: instance_id, physical_id: physical_id),
42
+ offerings: offerings, sequence: sequence, probe_token: probe_token
49
43
  )
50
- sync_applied(key, result)
51
44
  end
52
45
 
53
46
  def replace_instance_snapshot(instance_id:, publisher_token:, offerings:, sequence:, physical_id: nil)
54
- key = instance_key(instance_id: instance_id, physical_id: physical_id)
55
- result = @registry.replace_instance_snapshot(
56
- publisher_token: publisher_token, instance_key: key, offerings: offerings, sequence: sequence
47
+ @registry.replace_instance_snapshot(
48
+ publisher_token: publisher_token, instance_key: instance_key(instance_id: instance_id, physical_id: physical_id),
49
+ offerings: offerings, sequence: sequence
57
50
  )
58
- sync_applied(key, result)
59
51
  end
60
52
 
61
53
  def readiness_succeeded(instance_id:, probe_token:, physical_id: nil)
62
- key = instance_key(instance_id: instance_id, physical_id: physical_id)
63
- sync_applied(key, @registry.readiness_succeeded(instance_key: key, probe_token: probe_token))
54
+ @registry.readiness_succeeded(instance_key: instance_key(instance_id: instance_id, physical_id: physical_id), probe_token: probe_token)
64
55
  end
65
56
 
66
57
  def readiness_failed(instance_id:, probe_token:, reason:, physical_id: nil)
67
- key = instance_key(instance_id: instance_id, physical_id: physical_id)
68
- sync_applied(key, @registry.readiness_failed(instance_key: key, probe_token: probe_token, reason: reason))
58
+ @registry.readiness_failed(instance_key: instance_key(instance_id: instance_id, physical_id: physical_id), probe_token: probe_token, reason: reason)
69
59
  end
70
60
 
71
61
  def remove_instance(instance_id:, publisher_token:, physical_id: nil)
72
- key = instance_key(instance_id: instance_id, physical_id: physical_id)
73
- sync_applied(key, @registry.remove_instance(instance_key: key, publisher_token: publisher_token))
62
+ @registry.remove_instance(instance_key: instance_key(instance_id: instance_id, physical_id: physical_id), publisher_token: publisher_token)
74
63
  end
75
64
 
76
65
  def snapshot
@@ -82,33 +71,6 @@ module Legion
82
71
  def instance_key(instance_id:, physical_id: nil)
83
72
  Identity::InstanceKey.new(provider_family: @provider_family, instance_id: instance_id, physical_id: physical_id)
84
73
  end
85
-
86
- def sync_applied(instance_key, result)
87
- sync_compatibility(instance_key, result.reason) if result.applied
88
- result
89
- end
90
-
91
- # Best-effort, post-commit projection into the old coordinator. It never
92
- # changes the local return and can never roll back or authorize the SSOT.
93
- def sync_compatibility(instance_key, mutation_reason)
94
- return if @compatibility_adapter.nil?
95
-
96
- outcome = @compatibility_adapter.sync_snapshot(
97
- snapshot: @registry.snapshot, instance_key: instance_key, mutation_reason: mutation_reason
98
- )
99
- return unless outcome == :failed
100
-
101
- log.warn(
102
- '[llm][inventory][publisher] action=compatibility_sync result=failed ' \
103
- "provider_family=#{instance_key.provider_family} instance=#{instance_key.instance_id} reason=#{mutation_reason}"
104
- )
105
- rescue StandardError => e
106
- handle_exception(
107
- e, handled: true, level: :warn, operation: 'llm.inventory.publisher.compatibility_sync',
108
- provider_family: instance_key.provider_family, instance_id: instance_key.instance_id,
109
- mutation_reason: mutation_reason
110
- )
111
- end
112
74
  end
113
75
  end
114
76
  end
@@ -13,10 +13,10 @@ module Legion
13
13
  module Llm
14
14
  module Inventory
15
15
  # Documentation namespace anchor for the immutable inventory records
16
- # defined in this file: OfferingDraft, OfferingRecord, LaneRecord,
17
- # AvailabilityFact, ReadinessResult, InstanceRecord, PublicationStatus,
18
- # and MutationResult. The records themselves live directly under
19
- # Inventory so their canonical constant paths stay flat.
16
+ # defined in this file: OfferingDraft, LaneRecord, AvailabilityFact,
17
+ # ReadinessResult, InstanceRecord, PublicationStatus, and
18
+ # MutationResult. The records themselves live directly under Inventory
19
+ # so their canonical constant paths stay flat.
20
20
  module Records; end
21
21
 
22
22
  # Shared normalization/validation used by the immutable inventory and
@@ -151,7 +151,7 @@ module Legion
151
151
 
152
152
  normalized = {}
153
153
  evidence.each do |key, value|
154
- op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
154
+ op = Taxonomies.normalize_operation(value: key)
155
155
  raise Errors::ValidationError, "operation_evidence[#{op}] must be an OperationEvidence" unless value.is_a?(OperationEvidence)
156
156
  raise Errors::ValidationError, "operation_evidence[#{op}] operation must match key" unless value.operation == op
157
157
  raise Errors::ValidationError, "duplicate operation_evidence key #{op}" if normalized.key?(op)
@@ -184,7 +184,7 @@ module Legion
184
184
 
185
185
  normalized = {}
186
186
  quota_domains.each do |key, value|
187
- op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
187
+ op = Taxonomies.normalize_operation(value: key)
188
188
  normalized[op] = validate_quota_domain_value!(value: value, field: "quota_domains[#{op}]")
189
189
  end
190
190
  normalized.freeze
@@ -197,7 +197,7 @@ module Legion
197
197
  end
198
198
 
199
199
  # Validates and returns the five scalar ValueEvidence fields shared by
200
- # OfferingDraft, OfferingRecord, and LaneRecord as a member Hash.
200
+ # OfferingDraft and LaneRecord as a member Hash.
201
201
  def scalar_value_evidences(kwargs)
202
202
  {
203
203
  context_evidence: positive_int_value_evidence!(kwargs[:context_evidence], :context_evidence),
@@ -312,81 +312,15 @@ module Legion
312
312
  end
313
313
  end
314
314
 
315
- # A registry-owned offering with canonical identity, captured callable, and
316
- # the write-time weight pair copied unchanged from its draft. Only
317
- # Inventory::Publisher/Registry constructs it. See section 10.2.
318
- OfferingRecord = ::Data.define(
319
- :offering_id, :provider_native_key, :instance_key, :model, :tier, :operation_evidence,
320
- :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
321
- :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :callable_handle,
322
- :publication_source, :weight_inputs, :base_weight
323
- ) do
324
- def initialize(**kwargs)
325
- RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
326
- weight_inputs, base_weight = RecordSupport.validated_weight_pair(
327
- weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
328
- )
329
- instance_key = kwargs[:instance_key]
330
- callable_handle = kwargs[:callable_handle]
331
- raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
332
- raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
333
-
334
- native_key = Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key)
335
- Identity.validate_offering_id!(value: kwargs[:offering_id], instance_key: instance_key, provider_native_key: native_key)
336
-
337
- super(
338
- offering_id: kwargs[:offering_id].dup.freeze,
339
- provider_native_key: native_key,
340
- instance_key: instance_key,
341
- model: Identity.normalize_text(value: kwargs[:model], field: :model),
342
- tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
343
- operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
344
- capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
345
- quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
346
- metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
347
- callable_handle: callable_handle,
348
- publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
349
- weight_inputs: weight_inputs,
350
- base_weight: base_weight,
351
- **RecordSupport.scalar_value_evidences(kwargs)
352
- )
353
- end
354
-
355
- def supported_operations
356
- operation_evidence.select { |_op, evidence| evidence.supported? }.keys.sort
357
- end
358
-
359
- def unsupported_operations
360
- operation_evidence.select { |_op, evidence| evidence.unsupported? }.keys.sort
361
- end
362
-
363
- def unknown_operations
364
- operation_evidence.select { |_op, evidence| evidence.unknown? }.keys.sort
365
- end
366
-
367
- def operation_status(operation:)
368
- operation_evidence.fetch(Taxonomies.normalize_operation(value: operation, allow_aliases: false)).status
369
- end
370
-
371
- def capability_evidence_for(capability:)
372
- capability_evidence[Legion::Extensions::Llm::Capabilities.canonical(capability)]
373
- end
374
-
375
- def capability_status(capability:)
376
- evidence = capability_evidence_for(capability: capability)
377
- evidence.nil? ? :unknown : evidence.status
378
- end
379
-
380
- def quota_domain(operation:)
381
- quota_domains[Taxonomies.normalize_operation(value: operation, allow_aliases: false)]
382
- end
383
- end
384
-
385
- # An executable lane derived by the registry from one supported operation
386
- # of an OfferingRecord, with its write-time weight pair copied unchanged.
315
+ # An executable lane: the ONE stored inventory unit, keyed by the
316
+ # 5-tuple id `tier:provider_family:instance_id:type:model`. Derived by
317
+ # the registry from one supported operation of an OfferingDraft, with
318
+ # its write-time weight pair copied unchanged. The 5 tuple is composed
319
+ # at validation from the record's own fields; there is no separate
320
+ # offering id (D2 — in the one bucket an offering IS a lane).
387
321
  # See section 10.3.
388
322
  LaneRecord = ::Data.define(
389
- :lane_id, :offering_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
323
+ :lane_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
390
324
  :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
391
325
  :model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
392
326
  :publication_source, :weight_inputs, :base_weight
@@ -407,21 +341,24 @@ module Legion
407
341
  raise Errors::ValidationError, 'provider_family and instance_id must equal instance_key'
408
342
  end
409
343
 
344
+ canonical_tier = Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS)
410
345
  canonical_model = Identity.normalize_text(value: kwargs[:model], field: :model)
411
- canonical_operation = Taxonomies.normalize_operation(value: kwargs[:operation], allow_aliases: false)
412
- Identity.validate_lane_id!(
413
- value: kwargs[:lane_id], instance_key: instance_key, operation: canonical_operation,
414
- model: canonical_model, offering_id: kwargs[:offering_id]
346
+ canonical_operation = Taxonomies.normalize_operation(value: kwargs[:operation])
347
+ Identity.validate_lane_id!(value: kwargs[:lane_id])
348
+ expected_lane_id = Identity.compose_lane_id(
349
+ tier: canonical_tier, provider_family: family, instance_id: resolved_instance,
350
+ type: Taxonomies.lane_type_for(operation: canonical_operation), model: canonical_model
415
351
  )
352
+ raise Errors::ValidationError, 'lane_id does not reproduce from its identity fields' \
353
+ unless kwargs[:lane_id].to_s == expected_lane_id
416
354
 
417
355
  super(
418
- lane_id: kwargs[:lane_id].dup.freeze,
419
- offering_id: kwargs[:offering_id].dup.freeze,
356
+ lane_id: kwargs[:lane_id].to_s.dup.freeze,
420
357
  instance_key: instance_key,
421
358
  provider_family: family,
422
359
  instance_id: resolved_instance,
423
360
  model: canonical_model,
424
- tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
361
+ tier: canonical_tier,
425
362
  operation: canonical_operation,
426
363
  capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
427
364
  quota_domain: kwargs[:quota_domain].nil? ? nil : RecordSupport.validate_quota_domain_value!(value: kwargs[:quota_domain], field: :quota_domain),
@@ -507,10 +444,11 @@ module Legion
507
444
  end
508
445
  end
509
446
 
510
- # An activated exact instance's complete published state. Its availability
511
- # is available or unavailable, never initializing. See section 10.6.
447
+ # An activated exact instance's complete published state. Its lanes are
448
+ # keyed by the 5-tuple lane id; its availability is available or
449
+ # unavailable, never initializing. See section 10.6.
512
450
  InstanceRecord = ::Data.define(
513
- :instance_key, :callable_handle, :availability, :offerings_by_id,
451
+ :instance_key, :callable_handle, :availability, :lanes_by_id,
514
452
  :publisher_id, :publisher_token_id, :published_sequence, :published_at
515
453
  ) do
516
454
  def initialize(**kwargs)
@@ -528,7 +466,7 @@ module Legion
528
466
  instance_key: instance_key,
529
467
  callable_handle: callable_handle,
530
468
  availability: availability,
531
- offerings_by_id: validate_offerings!(kwargs[:offerings_by_id], instance_key, callable_handle),
469
+ lanes_by_id: validate_lanes!(kwargs[:lanes_by_id], instance_key, callable_handle),
532
470
  publisher_id: nonempty_id!(kwargs[:publisher_id], :publisher_id),
533
471
  publisher_token_id: nonempty_id!(kwargs[:publisher_token_id], :publisher_token_id),
534
472
  published_sequence: RecordSupport.nonnegative_integer(value: kwargs[:published_sequence], field: :published_sequence),
@@ -536,17 +474,15 @@ module Legion
536
474
  )
537
475
  end
538
476
 
539
- def validate_offerings!(offerings_by_id, instance_key, callable_handle)
540
- raise Errors::ValidationError, 'offerings_by_id must be a Hash' unless offerings_by_id.is_a?(::Hash)
477
+ def validate_lanes!(lanes_by_id, instance_key, callable_handle)
478
+ raise Errors::ValidationError, 'lanes_by_id must be a Hash' unless lanes_by_id.is_a?(::Hash)
541
479
 
542
- offerings_by_id.each do |offering_id, offering|
543
- raise Errors::ValidationError, "offering #{offering_id} must be an OfferingRecord" unless offering.is_a?(OfferingRecord)
544
- raise Errors::ValidationError, "offering #{offering_id} key mismatch" unless offering.offering_id == offering_id
545
- unless offering.instance_key == instance_key && offering.callable_handle.equal?(callable_handle)
546
- raise Errors::ValidationError, "offering #{offering_id} must share the instance callable/key"
547
- end
480
+ lanes_by_id.each do |lane_id, lane|
481
+ raise Errors::ValidationError, "lane #{lane_id} must be a LaneRecord" unless lane.is_a?(LaneRecord)
482
+ raise Errors::ValidationError, "lane #{lane_id} key mismatch" unless lane.lane_id == lane_id
483
+ raise Errors::ValidationError, "lane #{lane_id} must share the instance callable/key" unless lane.instance_key == instance_key && lane.callable_handle.equal?(callable_handle)
548
484
  end
549
- offerings_by_id.dup.freeze
485
+ lanes_by_id.dup.freeze
550
486
  end
551
487
 
552
488
  def nonempty_id!(value, field)
@@ -9,6 +9,7 @@ require 'legion/extensions/llm/inventory/callable_handle'
9
9
  require 'legion/extensions/llm/inventory/probe_token'
10
10
  require 'legion/extensions/llm/inventory/records'
11
11
  require 'legion/extensions/llm/inventory/snapshot'
12
+ require 'legion/extensions/llm/taxonomies'
12
13
 
13
14
  module Legion
14
15
  module Extensions
@@ -21,7 +22,7 @@ module Legion
21
22
  # Private per-instance state. Never leaves registry.rb.
22
23
  ScopeState = ::Data.define(
23
24
  :instance_key, :publisher_token, :callable, :callable_handle, :probe_request_handle,
24
- :publication_status, :last_sequence, :availability, :offerings, :lanes,
25
+ :publication_status, :last_sequence, :availability, :lanes,
25
26
  :availability_revision, :unavailable_revision, :published_at
26
27
  )
27
28
 
@@ -196,7 +197,7 @@ module Legion
196
197
  @scopes[instance_key] = ScopeState.new(
197
198
  instance_key: instance_key, publisher_token: token, callable: callable, callable_handle: handle,
198
199
  probe_request_handle: probe_request_handle, publication_status: status,
199
- last_sequence: -1, availability: nil, offerings: {}, lanes: {},
200
+ last_sequence: -1, availability: nil, lanes: {},
200
201
  availability_revision: prior&.availability_revision || 0,
201
202
  unavailable_revision: prior&.unavailable_revision, published_at: nil
202
203
  )
@@ -218,7 +219,7 @@ module Legion
218
219
  status = complete_status(scope, sequence, probe_token, now, :success)
219
220
  new_scope = scope.with(
220
221
  publication_status: status, last_sequence: sequence, availability: availability,
221
- offerings: prepared[1], lanes: prepared[2], availability_revision: new_rev,
222
+ lanes: prepared[1], availability_revision: new_rev,
222
223
  unavailable_revision: nil, published_at: now
223
224
  )
224
225
  @scopes[instance_key] = new_scope
@@ -237,7 +238,7 @@ module Legion
237
238
 
238
239
  def apply_replacement(scope, instance_key, prepared, sequence)
239
240
  status = scope.publication_status.with(published_sequence: sequence)
240
- new_scope = scope.with(offerings: prepared[1], lanes: prepared[2], last_sequence: sequence, publication_status: status)
241
+ new_scope = scope.with(lanes: prepared[1], last_sequence: sequence, publication_status: status)
241
242
  @scopes[instance_key] = new_scope
242
243
  bump_and_snapshot!
243
244
  applied(:snapshot_replaced, new_scope)
@@ -428,7 +429,6 @@ module Legion
428
429
 
429
430
  def build_snapshot_locked
430
431
  instances = {}
431
- offerings = {}
432
432
  lanes = {}
433
433
  statuses = {}
434
434
  @scopes.each_pair do |key, scope|
@@ -436,11 +436,10 @@ module Legion
436
436
  next if scope.availability.nil?
437
437
 
438
438
  instances[key] = instance_record_for(scope)
439
- offerings.merge!(scope.offerings)
440
439
  lanes.merge!(scope.lanes)
441
440
  end
442
441
  Snapshot.new(
443
- generation: @generation, instances_by_key: instances, offerings_by_id: offerings,
442
+ generation: @generation, instances_by_key: instances,
444
443
  lanes_by_id: lanes, publication_status_by_key: statuses
445
444
  )
446
445
  end
@@ -448,7 +447,7 @@ module Legion
448
447
  def instance_record_for(scope)
449
448
  InstanceRecord.new(
450
449
  instance_key: scope.instance_key, callable_handle: scope.callable_handle, availability: scope.availability,
451
- offerings_by_id: scope.offerings, publisher_id: scope.publisher_token.publisher_id,
450
+ lanes_by_id: scope.lanes, publisher_id: scope.publisher_token.publisher_id,
452
451
  publisher_token_id: scope.publisher_token.publisher_token_id, published_sequence: scope.last_sequence,
453
452
  published_at: scope.published_at
454
453
  )
@@ -460,69 +459,76 @@ module Legion
460
459
  scope = @scopes[instance_key]
461
460
  return nil if scope.nil?
462
461
 
463
- offerings_by_id, lanes_by_id = build_records(instance_key, scope.callable_handle, drafts)
464
- [scope.callable_handle, offerings_by_id, lanes_by_id]
462
+ [scope.callable_handle, build_records(instance_key, scope.callable_handle, drafts)]
465
463
  end
466
464
 
467
465
  def prepared_matches?(prepared, scope)
468
466
  !prepared.nil? && prepared[0].equal?(scope.callable_handle)
469
467
  end
470
468
 
469
+ # The stored inventory is lanes ONLY, keyed by the 5 tuple
470
+ # tier:provider_family:instance_id:type:model. The 4th part is the
471
+ # COARSE type, not the fine operation: the operations a draft
472
+ # supports collapse to one lane per distinct type (chat +
473
+ # stream_chat + count_tokens are ONE inference lane — the v0.15.2
474
+ # model, where the operation is a request property matched against
475
+ # the lane type, not a lane identity part). The LaneRecord's
476
+ # operation member is the first supported operation of the lane's
477
+ # type in canonical order — the representative, not the identity.
478
+ # A duplicate (operation, model) published under a conflicting
479
+ # tier, or a second draft claiming an already-published 5 tuple,
480
+ # raises — never a silent merge (D1).
471
481
  def build_records(instance_key, handle, drafts)
472
- offerings_by_id = {}
482
+ lanes = {}
473
483
  native_keys = {}
484
+ op_model_tiers = {}
474
485
  drafts.each do |draft|
475
- offering_id = Identity.offering_id(instance_key: instance_key, provider_native_key: draft.provider_native_key)
476
486
  raise Errors::ValidationError, 'duplicate provider_native_key' if native_keys.key?(draft.provider_native_key)
477
- raise Errors::ValidationError, 'duplicate offering_id' if offerings_by_id.key?(offering_id)
478
487
 
479
488
  native_keys[draft.provider_native_key] = true
480
- offerings_by_id[offering_id] = build_offering_record(instance_key, handle, offering_id, draft)
481
- end
482
- [offerings_by_id.freeze, build_lanes(instance_key, handle, offerings_by_id).freeze]
483
- end
489
+ draft.operation_evidence.each_value do |evidence|
490
+ next unless evidence.supported?
484
491
 
485
- def build_offering_record(instance_key, handle, offering_id, draft)
486
- OfferingRecord.new(
487
- offering_id: offering_id, provider_native_key: draft.provider_native_key, instance_key: instance_key,
488
- model: draft.model, tier: draft.tier, operation_evidence: draft.operation_evidence,
489
- capability_evidence: draft.capability_evidence, context_evidence: draft.context_evidence,
490
- max_output_evidence: draft.max_output_evidence,
491
- embedding_dimensions_evidence: draft.embedding_dimensions_evidence,
492
- model_revision_evidence: draft.model_revision_evidence, tokenizer_evidence: draft.tokenizer_evidence,
493
- quota_domains: draft.quota_domains, metadata: draft.metadata, callable_handle: handle,
494
- publication_source: draft.publication_source,
495
- weight_inputs: draft.weight_inputs, base_weight: draft.base_weight
496
- )
497
- end
492
+ op_model = [evidence.operation, draft.model]
493
+ raise Errors::ValidationError, 'duplicate (operation, model) published under a conflicting tier' if op_model_tiers.key?(op_model) && op_model_tiers[op_model] != draft.tier
498
494
 
499
- def build_lanes(instance_key, handle, offerings_by_id)
500
- lanes = {}
501
- offerings_by_id.each_value do |offering|
502
- offering.supported_operations.each do |operation|
503
- lane_id = Identity.lane_id(
504
- instance_key: instance_key, operation: operation, model: offering.model, offering_id: offering.offering_id
495
+ op_model_tiers[op_model] = draft.tier
496
+ end
497
+
498
+ # One lane per distinct type the draft supports.
499
+ draft.operation_evidence.each_value
500
+ .select(&:supported?)
501
+ .group_by { |evidence| Taxonomies.lane_type_for(operation: evidence.operation) }
502
+ .each do |type, evidences|
503
+ representative = evidences.min_by { |evidence| Taxonomies::OPERATIONS.index(evidence.operation) }
504
+ lane_id = Identity.compose_lane_id(
505
+ tier: draft.tier, provider_family: instance_key.provider_family,
506
+ instance_id: instance_key.instance_id, type: type, model: draft.model
505
507
  )
506
508
  raise Errors::ValidationError, 'duplicate lane_id' if lanes.key?(lane_id)
507
509
 
508
- lanes[lane_id] = build_lane_record(instance_key, handle, offering, operation, lane_id)
510
+ lanes[lane_id] = build_lane_record(instance_key, handle, draft, type, representative.operation)
509
511
  end
510
512
  end
511
- lanes
513
+ lanes.freeze
512
514
  end
513
515
 
514
- def build_lane_record(instance_key, handle, offering, operation, lane_id)
516
+ def build_lane_record(instance_key, handle, draft, type, operation)
515
517
  LaneRecord.new(
516
- lane_id: lane_id, offering_id: offering.offering_id, instance_key: instance_key,
518
+ lane_id: Identity.compose_lane_id(
519
+ tier: draft.tier, provider_family: instance_key.provider_family,
520
+ instance_id: instance_key.instance_id, type: type, model: draft.model
521
+ ),
522
+ instance_key: instance_key,
517
523
  provider_family: instance_key.provider_family, instance_id: instance_key.instance_id,
518
- model: offering.model, tier: offering.tier, operation: operation,
519
- capability_evidence: offering.capability_evidence, context_evidence: offering.context_evidence,
520
- max_output_evidence: offering.max_output_evidence,
521
- embedding_dimensions_evidence: offering.embedding_dimensions_evidence,
522
- model_revision_evidence: offering.model_revision_evidence, tokenizer_evidence: offering.tokenizer_evidence,
523
- quota_domain: offering.quota_domain(operation: operation), metadata: offering.metadata,
524
- callable_handle: handle, publication_source: offering.publication_source,
525
- weight_inputs: offering.weight_inputs, base_weight: offering.base_weight
524
+ model: draft.model, tier: draft.tier, operation: operation,
525
+ capability_evidence: draft.capability_evidence, context_evidence: draft.context_evidence,
526
+ max_output_evidence: draft.max_output_evidence,
527
+ embedding_dimensions_evidence: draft.embedding_dimensions_evidence,
528
+ model_revision_evidence: draft.model_revision_evidence, tokenizer_evidence: draft.tokenizer_evidence,
529
+ quota_domain: draft.quota_domains[operation], metadata: draft.metadata,
530
+ callable_handle: handle, publication_source: draft.publication_source,
531
+ weight_inputs: draft.weight_inputs, base_weight: draft.base_weight
526
532
  )
527
533
  end
528
534
 
@@ -11,20 +11,20 @@ module Legion
11
11
  # A structurally immutable, generation-tagged read model of the registry.
12
12
  # Lookups return the frozen record or nil and never synthesize a default.
13
13
  # `instances_by_key` contains only activated instances;
14
- # `publication_status_by_key` also contains initializing claims. A caller
15
- # cannot observe a later inventory-record or generation mutation through
16
- # an older snapshot; the captured CallableHandle lifecycle is the single
17
- # intentional exception. See phase-1-lex-llm-additive.md section 11.3.
14
+ # `publication_status_by_key` also contains initializing claims. Lanes
15
+ # are keyed by the 5-tuple lane id and iterate in lexicographic id
16
+ # order (readable iteration). A caller cannot observe a later
17
+ # inventory-record or generation mutation through an older snapshot;
18
+ # the captured CallableHandle lifecycle is the single intentional
19
+ # exception. See phase-1-lex-llm-additive.md section 11.3.
18
20
  class Snapshot
19
21
  attr_reader :generation
20
22
 
21
- def initialize(generation:, instances_by_key:, offerings_by_id:, lanes_by_id:, publication_status_by_key:)
23
+ def initialize(generation:, instances_by_key:, lanes_by_id:, publication_status_by_key:)
22
24
  @generation = generation
23
25
  @instances_by_key = order_instances(instances_by_key).freeze
24
- @offerings_by_id = offerings_by_id.dup.freeze
25
26
  @lanes_by_id = lanes_by_id.dup.freeze
26
27
  @publication_status_by_key = order_instances(publication_status_by_key).freeze
27
- @ordered_offerings = @offerings_by_id.keys.sort.map { |id| @offerings_by_id[id] }.freeze
28
28
  @ordered_lanes = @lanes_by_id.keys.sort.map { |id| @lanes_by_id[id] }.freeze
29
29
  freeze
30
30
  end
@@ -33,10 +33,6 @@ module Legion
33
33
  @instances_by_key[instance_key]
34
34
  end
35
35
 
36
- def offering(offering_id:)
37
- @offerings_by_id[offering_id]
38
- end
39
-
40
36
  def lane(lane_id:)
41
37
  @lanes_by_id[lane_id]
42
38
  end
@@ -45,10 +41,6 @@ module Legion
45
41
  @ordered_lanes.select { |lane| lane.instance_key == instance_key }.freeze
46
42
  end
47
43
 
48
- def offerings_for(instance_key:)
49
- @ordered_offerings.select { |offering| offering.instance_key == instance_key }.freeze
50
- end
51
-
52
44
  def publication_status(instance_key:)
53
45
  @publication_status_by_key[instance_key]
54
46
  end
@@ -59,12 +51,6 @@ module Legion
59
51
  @ordered_lanes.each(&block)
60
52
  end
61
53
 
62
- def each_offering(&block)
63
- return to_enum(:each_offering) unless block
64
-
65
- @ordered_offerings.each(&block)
66
- end
67
-
68
54
  def each_instance(&block)
69
55
  return to_enum(:each_instance) unless block
70
56
 
@@ -5,6 +5,7 @@ Kernel.require 'set'
5
5
  require 'legion/extensions/llm/inventory/weight_schema'
6
6
  require 'legion/extensions/llm/settings_cascade'
7
7
  require 'legion/extensions/llm/inventory/identity'
8
+ require 'legion/extensions/llm/taxonomies'
8
9
 
9
10
  module Legion
10
11
  module Extensions
@@ -37,9 +38,9 @@ module Legion
37
38
  inputs = WeightSchema.weight_inputs(
38
39
  settings: settings,
39
40
  instance_key: instance_key,
40
- provider_native_key: draft.provider_native_key,
41
41
  model: draft.model,
42
- tier: draft.tier
42
+ tier: draft.tier,
43
+ operation_evidence: draft.operation_evidence
43
44
  )
44
45
  draft.with(weight_inputs: inputs, base_weight: WeightSchema.base_weight(inputs))
45
46
  end.freeze
@@ -209,10 +210,16 @@ module Legion
209
210
  provider_family, :instance, instance_key.instance_id.to_s,
210
211
  :model, draft.model.to_s
211
212
  )
212
- offering_id = Identity.offering_id(
213
- instance_key: instance_key, provider_native_key: draft.provider_native_key
214
- )
215
- keys << canonical_key(provider_family, :offering, offering_id.to_s)
213
+ draft.operation_evidence.each_value do |evidence|
214
+ next unless evidence.supported?
215
+
216
+ lane_id = Identity.compose_lane_id(
217
+ tier: draft.tier, provider_family: instance_key.provider_family,
218
+ instance_id: instance_key.instance_id,
219
+ type: Taxonomies.lane_type_for(operation: evidence.operation), model: draft.model
220
+ )
221
+ keys << canonical_key(provider_family, :offering, lane_id)
222
+ end
216
223
  end
217
224
  end
218
225
  keys