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
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/llm/utils'
4
+ require_relative 'contract_error'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Llm
9
+ module Fleet
10
+ # The ONE fleet wire-normalization entry (10 U5): typed readers over
11
+ # the raw wire Hash. Every fleet boundary (responder, worker, token
12
+ # validator) reads envelopes through these readers. Data is kept RAW
13
+ # (no eager symbolization) so the worker can still detect duplicate
14
+ # String/Symbol param spellings (W5).
15
+ FleetEnvelope = Struct.new(:data, keyword_init: true) do
16
+ # Wrap a wire payload (Hash) or pass an existing envelope through.
17
+ # L12: a non-Hash payload is a typed ContractError at the wrap
18
+ # boundary — the module is total instead of relying on call-site
19
+ # discipline (an untyped NoMethodError at the first reader).
20
+ def self.wrap(payload)
21
+ return payload if payload.is_a?(FleetEnvelope)
22
+
23
+ raise ContractError, "fleet envelope payload expected Hash, got #{payload.class}" unless payload.is_a?(::Hash)
24
+
25
+ new(data: payload)
26
+ end
27
+
28
+ def [](key)
29
+ symbol_key = key.to_sym
30
+ return data[symbol_key] if data.key?(symbol_key)
31
+
32
+ data[key.to_s]
33
+ end
34
+
35
+ def key?(key)
36
+ data.key?(key.to_sym) || data.key?(key.to_s)
37
+ end
38
+
39
+ def fetch(key, default = nil)
40
+ key?(key) ? self[key] : default
41
+ end
42
+
43
+ def to_h
44
+ Utils.deep_symbolize_keys(data)
45
+ end
46
+
47
+ def protocol_version = self[:protocol_version]
48
+ def request_id = self[:request_id]
49
+ def correlation_id = self[:correlation_id]
50
+ def idempotency_key = self[:idempotency_key]
51
+ def operation = self[:operation]
52
+ def provider = self[:provider]
53
+ def provider_instance = self[:provider_instance]
54
+ def model = self[:model]
55
+ def params = self[:params] || {}
56
+ def reply_to = self[:reply_to]
57
+ def message_context = self[:message_context] || {}
58
+ def trace_context = self[:trace_context] || {}
59
+ def execution_contract = self[:execution_contract]
60
+ def offering_id = self[:offering_id]
61
+ def signed_token = self[:signed_token]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -5,15 +5,19 @@ module Legion
5
5
  module Llm
6
6
  module Fleet
7
7
  module Protocol
8
- VERSION = 2
8
+ # Fleet protocol v3 — the contract cut (06 P1). v2 envelopes are
9
+ # rejected by design; there is no dual-protocol coexistence.
10
+ VERSION = 3
9
11
  REQUEST_TYPE = 'llm.fleet.request'
10
12
  RESPONSE_TYPE = 'llm.fleet.response'
11
13
  ERROR_TYPE = 'llm.fleet.error'
12
14
 
13
- # Additive exact-execution marker (phase-1-lex-llm-additive.md section 16.1).
14
- # An exact request is protocol v2 plus execution_contract == this marker
15
- # and a signed offering_id. Marker absence means legacy v2; an unknown
16
- # nonempty marker is rejected.
15
+ # P4: v1 legacy options rejected at both edges (one list).
16
+ LEGACY_FIELDS = %i[schema_version request_type fleet_correlation_id].freeze
17
+
18
+ # Every request is exact-execution (06 P2): the marker is REQUIRED and
19
+ # must equal this value (it names the contract semantics, not the
20
+ # protocol version). Marker absence is rejected.
17
21
  EXACT_EXECUTION_CONTRACT = 'exact_offering_v1'
18
22
  EXACT_REQUIRED_FIELDS = %i[
19
23
  execution_contract offering_id provider provider_instance model operation
@@ -21,6 +25,15 @@ module Legion
21
25
  EXACT_SIGNED_SCALAR_CLAIMS = %i[
22
26
  execution_contract offering_id
23
27
  ].freeze
28
+
29
+ # The complete required envelope field set (06 E2) — the 16 protocol
30
+ # fields plus the two exact fields. Both edges (FleetRequest and
31
+ # ProviderResponder) consume this one list.
32
+ REQUIRED_FIELDS = %i[
33
+ request_id correlation_id idempotency_key operation provider provider_instance model params reply_to
34
+ message_context caller trace_context signed_token timeout_seconds expires_at protocol_version
35
+ execution_contract offering_id
36
+ ].freeze
24
37
  end
25
38
  end
26
39
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
3
+ require 'legion/extensions/llm/utils'
4
+ require 'legion/extensions/llm/inventory/errors'
5
+ require 'legion/extensions/llm/routing/provider_outcome'
5
6
  require_relative 'protocol'
6
7
  require_relative 'settings'
8
+ require_relative 'contract_error'
9
+ require_relative 'fleet_envelope'
7
10
  require_relative 'worker_execution'
8
11
  require 'legion/extensions/llm/inventory/registry'
9
12
 
@@ -22,57 +25,28 @@ module Legion
22
25
 
23
26
  module Fleet
24
27
  # Shared implementation for provider-owned fleet responder runners.
28
+ # Protocol v3 (06 W9): parse → legacy-field rejection → required fields
29
+ # (one Protocol::REQUIRED_FIELDS list) → explicit version →
30
+ # provider-family match → exact execution contract (required, P2) →
31
+ # WorkerExecution.call → publish FleetResponse (E3/E4) → ack. On error:
32
+ # publish FleetError (E6), reject per F6, re-raise.
25
33
  module ProviderResponder
26
34
  include Legion::Logging::Helper
27
35
  extend Legion::Logging::Helper
28
36
 
29
37
  class ConfigurationError < StandardError; end
30
38
 
31
- REQUIRED_FIELDS = %i[
32
- request_id correlation_id idempotency_key operation provider provider_instance model params reply_to
33
- message_context caller trace_context signed_token timeout_seconds expires_at protocol_version
34
- ].freeze
35
- LEGACY_FIELDS = %i[schema_version request_type fleet_correlation_id].freeze
36
-
37
- FleetEnvelope = Struct.new(:data, keyword_init: true) do
38
- def [](key)
39
- data[key.to_sym] || data[key.to_s]
40
- end
41
-
42
- def key?(key)
43
- data.key?(key.to_sym) || data.key?(key.to_s)
44
- end
45
-
46
- def fetch(key, default = nil)
47
- key?(key) ? self[key] : default
48
- end
49
-
50
- def to_h = data
51
- def protocol_version = self[:protocol_version]
52
- def request_id = self[:request_id]
53
- def correlation_id = self[:correlation_id]
54
- def idempotency_key = self[:idempotency_key]
55
- def operation = self[:operation]
56
- def provider = self[:provider]
57
- def provider_instance = self[:provider_instance]
58
- def model = self[:model]
59
- def params = self[:params] || {}
60
- def reply_to = self[:reply_to]
61
- def message_context = self[:message_context] || {}
62
- def trace_context = self[:trace_context] || {}
63
- def execution_contract = self[:execution_contract]
64
- def offering_id = self[:offering_id]
65
- end
66
-
67
39
  module_function
68
40
 
69
41
  # Public runner entry point mirrors AMQP delivery callbacks, which carry both delivery and property metadata.
70
- # rubocop:disable Metrics/ParameterLists
71
- def call(payload:, provider_family:, provider_class:, provider_instances:,
42
+ # L6: the dead provider_class/provider_instances params are deleted —
43
+ # v3 dispatch is exact-only and never constructs a provider; passing
44
+ # provider objects here was a latent second execution truth.
45
+ def call(payload:, provider_family:,
72
46
  registry: ::Legion::Extensions::Llm::Inventory::Registry, delivery: nil, properties: nil)
73
47
  envelope = parse_payload(payload)
74
48
  check_envelope!(envelope, provider_family:)
75
- response = dispatch_request(envelope, provider_class, provider_instances, registry)
49
+ response = WorkerExecution.call(envelope: envelope, registry:)
76
50
  publish_response(envelope, response)
77
51
  ack(delivery || properties)
78
52
  response
@@ -83,30 +57,30 @@ module Legion
83
57
  reject(delivery || properties, requeue: requeue_error?(e))
84
58
  raise
85
59
  end
86
- # rubocop:enable Metrics/ParameterLists
87
60
 
88
61
  def enabled_for?(provider_instances)
89
62
  instances = resolve_provider_instances(provider_instances)
90
63
  instances.any? do |_instance_id, settings|
91
- truthy?(dig(settings, :fleet, :respond_to_requests))
64
+ truthy?(Utils.deep_symbolize_keys(settings).dig(:fleet, :respond_to_requests))
92
65
  end
93
66
  end
94
67
 
68
+ # E1: the single wire-normalization entry. Wrong-shape payloads
69
+ # (neither Hash, String, nor envelope) raise — the silent {} fallback
70
+ # is deleted.
95
71
  def parse_payload(payload)
96
- hash = case payload
97
- when FleetEnvelope
98
- payload.to_h
99
- when String
100
- parse_json(payload)
101
- else
102
- payload.respond_to?(:to_h) ? payload.to_h : {}
103
- end
104
- FleetEnvelope.new(data: deep_symbolize(hash))
72
+ case payload
73
+ when FleetEnvelope then payload
74
+ when String then FleetEnvelope.new(data: parse_json(payload))
75
+ when Hash then FleetEnvelope.new(data: payload)
76
+ else
77
+ raise ContractError, "fleet payload expected Hash or String, got #{payload.class}"
78
+ end
105
79
  end
106
80
 
107
81
  def check_envelope!(envelope, provider_family:)
108
82
  reject_legacy_fields!(envelope)
109
- REQUIRED_FIELDS.each do |field|
83
+ Protocol::REQUIRED_FIELDS.each do |field|
110
84
  raise ArgumentError, "#{field} is required" unless envelope.key?(field) && !envelope[field].nil?
111
85
  end
112
86
 
@@ -115,46 +89,21 @@ module Legion
115
89
  validate_execution_contract!(envelope)
116
90
  end
117
91
 
118
- # Marker absence means legacy v2; an unknown nonempty marker is rejected;
119
- # the exact marker additionally requires every EXACT_REQUIRED_FIELDS value.
92
+ # P2: exact execution only — the marker is required and must equal
93
+ # the exact marker; absence is rejected. The exact fields are
94
+ # additionally required by the marker.
120
95
  def validate_execution_contract!(envelope)
121
96
  marker = envelope.execution_contract
122
- return if marker.nil?
123
- raise ArgumentError, "unknown execution_contract: #{marker}" unless marker == Protocol::EXACT_EXECUTION_CONTRACT
97
+ raise ContractError, "execution_contract must be #{Protocol::EXACT_EXECUTION_CONTRACT}" unless
98
+ marker == Protocol::EXACT_EXECUTION_CONTRACT
124
99
 
125
100
  Protocol::EXACT_REQUIRED_FIELDS.each do |field|
126
- raise ArgumentError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
127
- end
128
- end
129
-
130
- def exact?(envelope)
131
- envelope.execution_contract == Protocol::EXACT_EXECUTION_CONTRACT
132
- end
133
-
134
- # Exact requests dispatch through the registry and never call
135
- # build_provider; legacy v2 keeps the provider-object path.
136
- def dispatch_request(envelope, provider_class, provider_instances, registry)
137
- if exact?(envelope)
138
- WorkerExecution.call(envelope: envelope, registry: registry)
139
- else
140
- provider = build_provider(envelope:, provider_class:, provider_instances:)
141
- WorkerExecution.call(envelope: envelope, provider: provider)
142
- end
143
- end
144
-
145
- def build_provider(envelope:, provider_class:, provider_instances:)
146
- instances = resolve_provider_instances(provider_instances)
147
- instance_id = envelope.provider_instance.to_s
148
- instance_settings = instances[instance_id.to_sym] || instances[instance_id]
149
- unless instance_settings
150
- raise ConfigurationError,
151
- "fleet provider instance is not configured: #{instance_id}"
101
+ raise ContractError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
152
102
  end
153
- raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}" unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
154
-
155
- provider_class.new(deep_symbolize(instance_settings))
156
103
  end
157
104
 
105
+ # E3: the response envelope carries the serialized Canonical::Response.
106
+ # E4/G5: thinking never crosses the fleet — excluded exactly once, here.
158
107
  def publish_response(envelope, response)
159
108
  transport_message_class(:FleetResponse).new(
160
109
  protocol_version: envelope.protocol_version,
@@ -168,16 +117,13 @@ module Legion
168
117
  reply_to: envelope.reply_to,
169
118
  message_context: envelope.message_context,
170
119
  trace_context: envelope.trace_context,
171
- content: response_content(response),
172
- tool_calls: response_field(response, :tool_calls) || [],
173
- usage: response_usage(response),
174
- finish_reason: response_field(response, :finish_reason),
175
- metadata: response_metadata(response),
176
- execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
177
- offering_id: exact?(envelope) ? envelope.offering_id : nil
120
+ response: response.to_h.except(:thinking),
121
+ execution_contract: envelope.execution_contract,
122
+ offering_id: envelope.offering_id
178
123
  ).publish
179
124
  end
180
125
 
126
+ # E6: the error envelope. retryable is derived per F6.
181
127
  def publish_error(envelope, error)
182
128
  transport_message_class(:FleetError).new(
183
129
  protocol_version: envelope.protocol_version,
@@ -196,8 +142,8 @@ module Legion
196
142
  error_class: error.class.name,
197
143
  retryable: retryable_error?(error),
198
144
  metadata: {},
199
- execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
200
- offering_id: exact?(envelope) ? envelope.offering_id : nil
145
+ execution_contract: envelope.execution_contract,
146
+ offering_id: envelope.offering_id
201
147
  ).publish
202
148
  end
203
149
 
@@ -236,20 +182,19 @@ module Legion
236
182
  end
237
183
  end
238
184
 
185
+ # L1: Legion::JSON only (house rule) — the bare ::JSON fallback is
186
+ # deleted; Legion::JSON is a hard dependency of this gem.
239
187
  def parse_json(payload)
240
- if defined?(::Legion::JSON)
241
- ::Legion::JSON.parse(payload)
242
- else
243
- ::JSON.parse(payload)
244
- end
188
+ ::Legion::JSON.parse(payload)
245
189
  end
246
190
 
247
191
  def reject_legacy_fields!(envelope)
248
- LEGACY_FIELDS.each do |field|
249
- raise ArgumentError, "#{field} is not supported by fleet protocol v2" if envelope.key?(field)
192
+ Protocol::LEGACY_FIELDS.each do |field|
193
+ raise ArgumentError, "#{field} is not supported by fleet protocol v3" if envelope.key?(field)
250
194
  end
251
195
  end
252
196
 
197
+ # P3: explicit version — no default fill.
253
198
  def validate_protocol_version!(envelope)
254
199
  return if envelope.protocol_version == Protocol::VERSION
255
200
 
@@ -264,7 +209,7 @@ module Legion
264
209
 
265
210
  def resolve_provider_instances(provider_instances)
266
211
  instances = provider_instances.respond_to?(:call) ? provider_instances.call : provider_instances
267
- deep_symbolize(instances || {})
212
+ Utils.deep_symbolize_keys(instances || {})
268
213
  end
269
214
 
270
215
  def requeue_error?(error)
@@ -272,72 +217,32 @@ module Legion
272
217
  Settings.value(:fleet, :consumer, :requeue_transient, default: true) != false
273
218
  end
274
219
 
220
+ # F6: retryability is derived from the one ProviderOutcome kind table
221
+ # (05 O6) — transient kinds retry; contract/policy/auth/classification
222
+ # kinds never do. The default-true catch-all is deleted.
275
223
  def retryable_error?(error)
276
224
  return false if error.is_a?(ConfigurationError)
277
225
  return false if error.is_a?(WorkerExecution::PolicyError)
226
+ return false if error.is_a?(ContractError)
227
+ return false if error.is_a?(TokenError)
228
+ return false if error.is_a?(Inventory::Errors::ExactOfferingMismatchError)
278
229
 
279
- true
230
+ ::Legion::Extensions::Llm::Routing::ProviderOutcome::RETRYABLE_KINDS.include?(
231
+ ::Legion::Extensions::Llm::Routing::ProviderOutcome.kind_for(error)
232
+ )
280
233
  end
281
234
 
282
235
  def error_code(error)
283
236
  return 'configuration_error' if error.is_a?(ConfigurationError)
284
237
  return 'policy_error' if error.is_a?(WorkerExecution::PolicyError)
238
+ return 'contract_error' if error.is_a?(ContractError)
285
239
 
286
240
  'provider_error'
287
241
  end
288
242
 
289
- def response_content(response)
290
- response_field(response, :content) || response_field(response, :result) || response.to_s
291
- end
292
-
293
- def response_usage(response)
294
- usage = response_field(response, :usage) || response_field(response, :tokens)
295
- return deep_symbolize(usage) if usage.respond_to?(:to_h)
296
-
297
- {
298
- input_tokens: response_field(response, :input_tokens),
299
- output_tokens: response_field(response, :output_tokens),
300
- thinking_tokens: response_field(response, :thinking_tokens)
301
- }.compact
302
- end
303
-
304
- def response_metadata(response)
305
- metadata = response_field(response, :metadata)
306
- metadata.respond_to?(:to_h) ? deep_symbolize(metadata) : {}
307
- end
308
-
309
- def response_field(response, field)
310
- return response[field] if response.respond_to?(:key?) && response.key?(field)
311
- return response[field.to_s] if response.respond_to?(:key?) && response.key?(field.to_s)
312
- return response.public_send(field) if response.respond_to?(field)
313
-
314
- nil
315
- end
316
-
317
- def dig(hash, *keys)
318
- keys.reduce(hash) do |current, key|
319
- break nil unless current.respond_to?(:key?)
320
-
321
- current[key.to_sym] || current[key.to_s]
322
- end
323
- end
324
-
325
243
  def truthy?(value)
326
244
  value == true || value.to_s == 'true'
327
245
  end
328
-
329
- def deep_symbolize(value)
330
- case value
331
- when Hash
332
- value.each_with_object({}) do |(key, child), result|
333
- result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize(child)
334
- end
335
- when Array
336
- value.map { |child| deep_symbolize(child) }
337
- else
338
- value
339
- end
340
- end
341
246
  end
342
247
  end
343
248
  end
@@ -3,6 +3,7 @@
3
3
  require 'concurrent'
4
4
  require 'time'
5
5
 
6
+ require 'legion/extensions/llm/utils'
6
7
  require_relative 'protocol'
7
8
  require_relative 'settings'
8
9
  require_relative 'token_error'
@@ -21,19 +22,22 @@ module Legion
21
22
 
22
23
  module_function
23
24
 
25
+ # The envelope is a plain (already symbolized) Hash — the single
26
+ # wire-normalization entry (FleetEnvelope) happens before this
27
+ # boundary.
24
28
  def validate!(token:, envelope:, record_replay: true)
25
29
  raise TokenError, 'fleet token is required' if token.to_s.empty?
26
30
 
27
- claims = symbolize_keys(jwt_module.verify(
28
- token,
29
- verification_key: signing_key,
30
- issuer: issuer,
31
- algorithm: algorithm,
32
- verify_issuer: true
33
- ))
31
+ claims = Utils.deep_symbolize_keys(jwt_module.verify(
32
+ token,
33
+ verification_key: signing_key,
34
+ issuer: issuer,
35
+ algorithm: algorithm,
36
+ verify_issuer: true
37
+ ))
34
38
  validate_registered_claims!(claims)
35
39
  validate_request_expiry!(claims)
36
- validate_envelope_claims!(claims, symbolize_keys(envelope || {}))
40
+ validate_envelope_claims!(claims, Utils.deep_symbolize_keys(envelope || {}))
37
41
  record_replay ? reserve_replay!(claims[:jti]) : ensure_not_replayed!(claims[:jti])
38
42
  claims
39
43
  rescue TokenError
@@ -96,12 +100,10 @@ module Legion
96
100
  validate_exact_execution_claims!(claims, envelope)
97
101
  end
98
102
 
99
- # When the exact-offering marker is present on the envelope, both exact
100
- # scalar claims must be signed into the verified JWT and match the
101
- # envelope. An unsigned exact marker is never authoritative.
103
+ # S3: every request is exact (06 P2), so both exact scalar claims are
104
+ # verified UNCONDITIONALLY — the marker-absence branch is deleted. An
105
+ # unsigned exact marker is never authoritative.
102
106
  def validate_exact_execution_claims!(claims, envelope)
103
- return unless envelope[:execution_contract] == Protocol::EXACT_EXECUTION_CONTRACT
104
-
105
107
  Protocol::EXACT_SIGNED_SCALAR_CLAIMS.each do |key|
106
108
  raise TokenError, "fleet token missing signed #{key}" if claims[key].to_s.empty?
107
109
  raise TokenError, "fleet token #{key} claim mismatch" unless canonical_value(claims[key]) == canonical_value(envelope[key])
@@ -217,14 +219,6 @@ module Legion
217
219
  raise TokenError, 'Legion::Crypt::JWT.verify unavailable'
218
220
  end
219
221
 
220
- def symbolize_keys(hash)
221
- return {} unless hash.respond_to?(:each)
222
-
223
- hash.each_with_object({}) do |(key, value), result|
224
- result[key.respond_to?(:to_sym) ? key.to_sym : key] = value
225
- end
226
- end
227
-
228
222
  def canonical_value(value)
229
223
  case value
230
224
  when Hash