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
@@ -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
@@ -31,6 +31,35 @@ module Legion
31
31
 
32
32
  PUBLICATION_SOURCES = %i[provider_catalog provider_static_catalog provider_control_plane].freeze
33
33
 
34
+ WEIGHT_INPUT_KEYS = %i[instance model_or_offering provider tier].freeze
35
+ IDENTITY_WEIGHT_INPUTS = {
36
+ tier: 100, provider: 100, instance: 100, model_or_offering: 100
37
+ }.freeze
38
+ IDENTITY_BASE_WEIGHT = 100_000_000
39
+
40
+ def validated_weight_pair(weight_inputs:, base_weight:)
41
+ raise Errors::ValidationError, 'weight_inputs and base_weight must be supplied together' \
42
+ if weight_inputs.nil? != base_weight.nil?
43
+
44
+ return [IDENTITY_WEIGHT_INPUTS, IDENTITY_BASE_WEIGHT] if weight_inputs.nil?
45
+
46
+ raise Errors::ValidationError, 'weight_inputs must be a Hash' \
47
+ unless weight_inputs.is_a?(::Hash)
48
+ unless weight_inputs.keys.length == WEIGHT_INPUT_KEYS.length &&
49
+ WEIGHT_INPUT_KEYS.all? { |key| weight_inputs.key?(key) }
50
+ raise Errors::ValidationError,
51
+ 'weight_inputs must have keys tier/provider/instance/model_or_offering'
52
+ end
53
+ raise Errors::ValidationError, 'weight_inputs values must be Integers >= 0' \
54
+ unless weight_inputs.values.all? { |value| value.is_a?(::Integer) && value >= 0 }
55
+ raise Errors::ValidationError, 'base_weight must be an Integer >= 0' \
56
+ unless base_weight.is_a?(::Integer) && base_weight >= 0
57
+ raise Errors::ValidationError, 'base_weight must equal the product of weight_inputs' \
58
+ unless base_weight == weight_inputs.values.reduce(1, :*)
59
+
60
+ [weight_inputs.dup.freeze, base_weight]
61
+ end
62
+
34
63
  def check_unknown_kwargs!(kwargs:, members:)
35
64
  unknown = kwargs.keys - members
36
65
  raise Errors::ValidationError, "unknown keyword(s): #{unknown.join(', ')}" unless unknown.empty?
@@ -122,7 +151,7 @@ module Legion
122
151
 
123
152
  normalized = {}
124
153
  evidence.each do |key, value|
125
- op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
154
+ op = Taxonomies.normalize_operation(value: key)
126
155
  raise Errors::ValidationError, "operation_evidence[#{op}] must be an OperationEvidence" unless value.is_a?(OperationEvidence)
127
156
  raise Errors::ValidationError, "operation_evidence[#{op}] operation must match key" unless value.operation == op
128
157
  raise Errors::ValidationError, "duplicate operation_evidence key #{op}" if normalized.key?(op)
@@ -155,7 +184,7 @@ module Legion
155
184
 
156
185
  normalized = {}
157
186
  quota_domains.each do |key, value|
158
- op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
187
+ op = Taxonomies.normalize_operation(value: key)
159
188
  normalized[op] = validate_quota_domain_value!(value: value, field: "quota_domains[#{op}]")
160
189
  end
161
190
  normalized.freeze
@@ -168,7 +197,7 @@ module Legion
168
197
  end
169
198
 
170
199
  # Validates and returns the five scalar ValueEvidence fields shared by
171
- # OfferingDraft, OfferingRecord, and LaneRecord as a member Hash.
200
+ # OfferingDraft and LaneRecord as a member Hash.
172
201
  def scalar_value_evidences(kwargs)
173
202
  {
174
203
  context_evidence: positive_int_value_evidence!(kwargs[:context_evidence], :context_evidence),
@@ -251,106 +280,56 @@ module Legion
251
280
  end
252
281
  end
253
282
 
254
- # An off-registry provider draft of one offering. Carries no provider
255
- # family, instance ID, offering ID, lane ID, callable, health, weight, or
256
- # default model. See section 10.1.
283
+ # An off-registry provider draft of one offering. Carries the validated
284
+ # write-time weight pair, but no provider family, instance ID, offering ID,
285
+ # lane ID, callable, health, or default model. See section 10.1.
257
286
  OfferingDraft = ::Data.define(
258
287
  :provider_native_key, :model, :tier, :operation_evidence, :capability_evidence,
259
288
  :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
260
- :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source
289
+ :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source,
290
+ :weight_inputs, :base_weight
261
291
  ) do
262
292
  def initialize(**kwargs)
263
293
  kwargs = { capability_evidence: {}, quota_domains: {}, metadata: {} }.merge(kwargs)
264
294
  RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
265
-
266
- super(
267
- provider_native_key: Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key),
268
- model: Identity.normalize_text(value: kwargs[:model], field: :model),
269
- tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
270
- operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
271
- capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
272
- quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
273
- metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
274
- publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
275
- **RecordSupport.scalar_value_evidences(kwargs)
295
+ weight_inputs, base_weight = RecordSupport.validated_weight_pair(
296
+ weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
276
297
  )
277
- end
278
- end
279
-
280
- # A registry-owned offering with canonical identity and captured callable.
281
- # Only Inventory::Publisher/Registry constructs it. See section 10.2.
282
- OfferingRecord = ::Data.define(
283
- :offering_id, :provider_native_key, :instance_key, :model, :tier, :operation_evidence,
284
- :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
285
- :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :callable_handle,
286
- :publication_source
287
- ) do
288
- def initialize(**kwargs)
289
- RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
290
- instance_key = kwargs[:instance_key]
291
- callable_handle = kwargs[:callable_handle]
292
- raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
293
- raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
294
-
295
- native_key = Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key)
296
- Identity.validate_offering_id!(value: kwargs[:offering_id], instance_key: instance_key, provider_native_key: native_key)
297
298
 
298
299
  super(
299
- offering_id: kwargs[:offering_id].dup.freeze,
300
- provider_native_key: native_key,
301
- instance_key: instance_key,
300
+ provider_native_key: Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key),
302
301
  model: Identity.normalize_text(value: kwargs[:model], field: :model),
303
302
  tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
304
303
  operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
305
304
  capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
306
305
  quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
307
306
  metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
308
- callable_handle: callable_handle,
309
307
  publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
308
+ weight_inputs: weight_inputs,
309
+ base_weight: base_weight,
310
310
  **RecordSupport.scalar_value_evidences(kwargs)
311
311
  )
312
312
  end
313
-
314
- def supported_operations
315
- operation_evidence.select { |_op, evidence| evidence.supported? }.keys.sort
316
- end
317
-
318
- def unsupported_operations
319
- operation_evidence.select { |_op, evidence| evidence.unsupported? }.keys.sort
320
- end
321
-
322
- def unknown_operations
323
- operation_evidence.select { |_op, evidence| evidence.unknown? }.keys.sort
324
- end
325
-
326
- def operation_status(operation:)
327
- operation_evidence.fetch(Taxonomies.normalize_operation(value: operation, allow_aliases: false)).status
328
- end
329
-
330
- def capability_evidence_for(capability:)
331
- capability_evidence[Legion::Extensions::Llm::Capabilities.canonical(capability)]
332
- end
333
-
334
- def capability_status(capability:)
335
- evidence = capability_evidence_for(capability: capability)
336
- evidence.nil? ? :unknown : evidence.status
337
- end
338
-
339
- def quota_domain(operation:)
340
- quota_domains[Taxonomies.normalize_operation(value: operation, allow_aliases: false)]
341
- end
342
313
  end
343
314
 
344
- # An executable lane derived by the registry from one supported operation
345
- # of an OfferingRecord. See section 10.3.
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).
321
+ # See section 10.3.
346
322
  LaneRecord = ::Data.define(
347
- :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,
348
324
  :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
349
325
  :model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
350
- :publication_source
326
+ :publication_source, :weight_inputs, :base_weight
351
327
  ) do
352
328
  def initialize(**kwargs)
353
329
  RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
330
+ weight_inputs, base_weight = RecordSupport.validated_weight_pair(
331
+ weight_inputs: kwargs[:weight_inputs], base_weight: kwargs[:base_weight]
332
+ )
354
333
  instance_key = kwargs[:instance_key]
355
334
  callable_handle = kwargs[:callable_handle]
356
335
  raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
@@ -362,27 +341,32 @@ module Legion
362
341
  raise Errors::ValidationError, 'provider_family and instance_id must equal instance_key'
363
342
  end
364
343
 
344
+ canonical_tier = Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS)
365
345
  canonical_model = Identity.normalize_text(value: kwargs[:model], field: :model)
366
- canonical_operation = Taxonomies.normalize_operation(value: kwargs[:operation], allow_aliases: false)
367
- Identity.validate_lane_id!(
368
- value: kwargs[:lane_id], instance_key: instance_key, operation: canonical_operation,
369
- 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
370
351
  )
352
+ raise Errors::ValidationError, 'lane_id does not reproduce from its identity fields' \
353
+ unless kwargs[:lane_id].to_s == expected_lane_id
371
354
 
372
355
  super(
373
- lane_id: kwargs[:lane_id].dup.freeze,
374
- offering_id: kwargs[:offering_id].dup.freeze,
356
+ lane_id: kwargs[:lane_id].to_s.dup.freeze,
375
357
  instance_key: instance_key,
376
358
  provider_family: family,
377
359
  instance_id: resolved_instance,
378
360
  model: canonical_model,
379
- tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
361
+ tier: canonical_tier,
380
362
  operation: canonical_operation,
381
363
  capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
382
364
  quota_domain: kwargs[:quota_domain].nil? ? nil : RecordSupport.validate_quota_domain_value!(value: kwargs[:quota_domain], field: :quota_domain),
383
365
  metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
384
366
  callable_handle: callable_handle,
385
367
  publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
368
+ weight_inputs: weight_inputs,
369
+ base_weight: base_weight,
386
370
  **RecordSupport.scalar_value_evidences(kwargs)
387
371
  )
388
372
  end
@@ -460,10 +444,11 @@ module Legion
460
444
  end
461
445
  end
462
446
 
463
- # An activated exact instance's complete published state. Its availability
464
- # 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.
465
450
  InstanceRecord = ::Data.define(
466
- :instance_key, :callable_handle, :availability, :offerings_by_id,
451
+ :instance_key, :callable_handle, :availability, :lanes_by_id,
467
452
  :publisher_id, :publisher_token_id, :published_sequence, :published_at
468
453
  ) do
469
454
  def initialize(**kwargs)
@@ -481,7 +466,7 @@ module Legion
481
466
  instance_key: instance_key,
482
467
  callable_handle: callable_handle,
483
468
  availability: availability,
484
- 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),
485
470
  publisher_id: nonempty_id!(kwargs[:publisher_id], :publisher_id),
486
471
  publisher_token_id: nonempty_id!(kwargs[:publisher_token_id], :publisher_token_id),
487
472
  published_sequence: RecordSupport.nonnegative_integer(value: kwargs[:published_sequence], field: :published_sequence),
@@ -489,17 +474,15 @@ module Legion
489
474
  )
490
475
  end
491
476
 
492
- def validate_offerings!(offerings_by_id, instance_key, callable_handle)
493
- 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)
494
479
 
495
- offerings_by_id.each do |offering_id, offering|
496
- raise Errors::ValidationError, "offering #{offering_id} must be an OfferingRecord" unless offering.is_a?(OfferingRecord)
497
- raise Errors::ValidationError, "offering #{offering_id} key mismatch" unless offering.offering_id == offering_id
498
- unless offering.instance_key == instance_key && offering.callable_handle.equal?(callable_handle)
499
- raise Errors::ValidationError, "offering #{offering_id} must share the instance callable/key"
500
- 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)
501
484
  end
502
- offerings_by_id.dup.freeze
485
+ lanes_by_id.dup.freeze
503
486
  end
504
487
 
505
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,67 +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
- )
496
- 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
497
494
 
498
- def build_lanes(instance_key, handle, offerings_by_id)
499
- lanes = {}
500
- offerings_by_id.each_value do |offering|
501
- offering.supported_operations.each do |operation|
502
- lane_id = Identity.lane_id(
503
- 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
504
507
  )
505
508
  raise Errors::ValidationError, 'duplicate lane_id' if lanes.key?(lane_id)
506
509
 
507
- 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)
508
511
  end
509
512
  end
510
- lanes
513
+ lanes.freeze
511
514
  end
512
515
 
513
- def build_lane_record(instance_key, handle, offering, operation, lane_id)
516
+ def build_lane_record(instance_key, handle, draft, type, operation)
514
517
  LaneRecord.new(
515
- 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,
516
523
  provider_family: instance_key.provider_family, instance_id: instance_key.instance_id,
517
- model: offering.model, tier: offering.tier, operation: operation,
518
- capability_evidence: offering.capability_evidence, context_evidence: offering.context_evidence,
519
- max_output_evidence: offering.max_output_evidence,
520
- embedding_dimensions_evidence: offering.embedding_dimensions_evidence,
521
- model_revision_evidence: offering.model_revision_evidence, tokenizer_evidence: offering.tokenizer_evidence,
522
- quota_domain: offering.quota_domain(operation: operation), metadata: offering.metadata,
523
- callable_handle: handle, publication_source: offering.publication_source
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
524
532
  )
525
533
  end
526
534