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
@@ -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,60 +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
- symbol_key = key.to_sym
40
- return data[symbol_key] if data.key?(symbol_key)
41
-
42
- data[key.to_s]
43
- end
44
-
45
- def key?(key)
46
- data.key?(key.to_sym) || data.key?(key.to_s)
47
- end
48
-
49
- def fetch(key, default = nil)
50
- key?(key) ? self[key] : default
51
- end
52
-
53
- def to_h = data
54
- def protocol_version = self[:protocol_version]
55
- def request_id = self[:request_id]
56
- def correlation_id = self[:correlation_id]
57
- def idempotency_key = self[:idempotency_key]
58
- def operation = self[:operation]
59
- def provider = self[:provider]
60
- def provider_instance = self[:provider_instance]
61
- def model = self[:model]
62
- def params = self[:params] || {}
63
- def reply_to = self[:reply_to]
64
- def message_context = self[:message_context] || {}
65
- def trace_context = self[:trace_context] || {}
66
- def execution_contract = self[:execution_contract]
67
- def offering_id = self[:offering_id]
68
- end
69
-
70
39
  module_function
71
40
 
72
41
  # Public runner entry point mirrors AMQP delivery callbacks, which carry both delivery and property metadata.
73
- # rubocop:disable Metrics/ParameterLists
74
- 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:,
75
46
  registry: ::Legion::Extensions::Llm::Inventory::Registry, delivery: nil, properties: nil)
76
47
  envelope = parse_payload(payload)
77
48
  check_envelope!(envelope, provider_family:)
78
- response = dispatch_request(envelope, provider_class, provider_instances, registry)
49
+ response = WorkerExecution.call(envelope: envelope, registry:)
79
50
  publish_response(envelope, response)
80
51
  ack(delivery || properties)
81
52
  response
@@ -86,30 +57,30 @@ module Legion
86
57
  reject(delivery || properties, requeue: requeue_error?(e))
87
58
  raise
88
59
  end
89
- # rubocop:enable Metrics/ParameterLists
90
60
 
91
61
  def enabled_for?(provider_instances)
92
62
  instances = resolve_provider_instances(provider_instances)
93
63
  instances.any? do |_instance_id, settings|
94
- truthy?(dig(settings, :fleet, :respond_to_requests))
64
+ truthy?(Utils.deep_symbolize_keys(settings).dig(:fleet, :respond_to_requests))
95
65
  end
96
66
  end
97
67
 
68
+ # E1: the single wire-normalization entry. Wrong-shape payloads
69
+ # (neither Hash, String, nor envelope) raise — the silent {} fallback
70
+ # is deleted.
98
71
  def parse_payload(payload)
99
- hash = case payload
100
- when FleetEnvelope
101
- payload.to_h
102
- when String
103
- parse_json(payload)
104
- else
105
- payload.respond_to?(:to_h) ? payload.to_h : {}
106
- end
107
- 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
108
79
  end
109
80
 
110
81
  def check_envelope!(envelope, provider_family:)
111
82
  reject_legacy_fields!(envelope)
112
- REQUIRED_FIELDS.each do |field|
83
+ Protocol::REQUIRED_FIELDS.each do |field|
113
84
  raise ArgumentError, "#{field} is required" unless envelope.key?(field) && !envelope[field].nil?
114
85
  end
115
86
 
@@ -118,46 +89,21 @@ module Legion
118
89
  validate_execution_contract!(envelope)
119
90
  end
120
91
 
121
- # Marker absence means legacy v2; an unknown nonempty marker is rejected;
122
- # 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.
123
95
  def validate_execution_contract!(envelope)
124
96
  marker = envelope.execution_contract
125
- return if marker.nil?
126
- 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
127
99
 
128
100
  Protocol::EXACT_REQUIRED_FIELDS.each do |field|
129
- raise ArgumentError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
101
+ raise ContractError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
130
102
  end
131
103
  end
132
104
 
133
- def exact?(envelope)
134
- envelope.execution_contract == Protocol::EXACT_EXECUTION_CONTRACT
135
- end
136
-
137
- # Exact requests dispatch through the registry and never call
138
- # build_provider; legacy v2 keeps the provider-object path.
139
- def dispatch_request(envelope, provider_class, provider_instances, registry)
140
- if exact?(envelope)
141
- WorkerExecution.call(envelope: envelope, registry: registry)
142
- else
143
- provider = build_provider(envelope:, provider_class:, provider_instances:)
144
- WorkerExecution.call(envelope: envelope, provider: provider)
145
- end
146
- end
147
-
148
- def build_provider(envelope:, provider_class:, provider_instances:)
149
- instances = resolve_provider_instances(provider_instances)
150
- instance_id = envelope.provider_instance.to_s
151
- instance_settings = instances[instance_id.to_sym] || instances[instance_id]
152
- unless instance_settings
153
- raise ConfigurationError,
154
- "fleet provider instance is not configured: #{instance_id}"
155
- end
156
- raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}" unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
157
-
158
- provider_class.new(deep_symbolize(instance_settings))
159
- end
160
-
105
+ # E3: the response envelope carries the serialized Canonical::Response.
106
+ # E4/G5: thinking never crosses the fleet — excluded exactly once, here.
161
107
  def publish_response(envelope, response)
162
108
  transport_message_class(:FleetResponse).new(
163
109
  protocol_version: envelope.protocol_version,
@@ -171,16 +117,13 @@ module Legion
171
117
  reply_to: envelope.reply_to,
172
118
  message_context: envelope.message_context,
173
119
  trace_context: envelope.trace_context,
174
- content: response_content(response),
175
- tool_calls: response_field(response, :tool_calls) || [],
176
- usage: response_usage(response),
177
- finish_reason: response_field(response, :finish_reason),
178
- metadata: response_metadata(response),
179
- execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
180
- 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
181
123
  ).publish
182
124
  end
183
125
 
126
+ # E6: the error envelope. retryable is derived per F6.
184
127
  def publish_error(envelope, error)
185
128
  transport_message_class(:FleetError).new(
186
129
  protocol_version: envelope.protocol_version,
@@ -199,8 +142,8 @@ module Legion
199
142
  error_class: error.class.name,
200
143
  retryable: retryable_error?(error),
201
144
  metadata: {},
202
- execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
203
- offering_id: exact?(envelope) ? envelope.offering_id : nil
145
+ execution_contract: envelope.execution_contract,
146
+ offering_id: envelope.offering_id
204
147
  ).publish
205
148
  end
206
149
 
@@ -239,20 +182,19 @@ module Legion
239
182
  end
240
183
  end
241
184
 
185
+ # L1: Legion::JSON only (house rule) — the bare ::JSON fallback is
186
+ # deleted; Legion::JSON is a hard dependency of this gem.
242
187
  def parse_json(payload)
243
- if defined?(::Legion::JSON)
244
- ::Legion::JSON.parse(payload)
245
- else
246
- ::JSON.parse(payload)
247
- end
188
+ ::Legion::JSON.parse(payload)
248
189
  end
249
190
 
250
191
  def reject_legacy_fields!(envelope)
251
- LEGACY_FIELDS.each do |field|
252
- 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)
253
194
  end
254
195
  end
255
196
 
197
+ # P3: explicit version — no default fill.
256
198
  def validate_protocol_version!(envelope)
257
199
  return if envelope.protocol_version == Protocol::VERSION
258
200
 
@@ -267,7 +209,7 @@ module Legion
267
209
 
268
210
  def resolve_provider_instances(provider_instances)
269
211
  instances = provider_instances.respond_to?(:call) ? provider_instances.call : provider_instances
270
- deep_symbolize(instances || {})
212
+ Utils.deep_symbolize_keys(instances || {})
271
213
  end
272
214
 
273
215
  def requeue_error?(error)
@@ -275,72 +217,32 @@ module Legion
275
217
  Settings.value(:fleet, :consumer, :requeue_transient, default: true) != false
276
218
  end
277
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.
278
223
  def retryable_error?(error)
279
224
  return false if error.is_a?(ConfigurationError)
280
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)
281
229
 
282
- true
230
+ ::Legion::Extensions::Llm::Routing::ProviderOutcome::RETRYABLE_KINDS.include?(
231
+ ::Legion::Extensions::Llm::Routing::ProviderOutcome.kind_for(error)
232
+ )
283
233
  end
284
234
 
285
235
  def error_code(error)
286
236
  return 'configuration_error' if error.is_a?(ConfigurationError)
287
237
  return 'policy_error' if error.is_a?(WorkerExecution::PolicyError)
238
+ return 'contract_error' if error.is_a?(ContractError)
288
239
 
289
240
  'provider_error'
290
241
  end
291
242
 
292
- def response_content(response)
293
- response_field(response, :content) || response_field(response, :result) || response.to_s
294
- end
295
-
296
- def response_usage(response)
297
- usage = response_field(response, :usage) || response_field(response, :tokens)
298
- return deep_symbolize(usage) if usage.respond_to?(:to_h)
299
-
300
- {
301
- input_tokens: response_field(response, :input_tokens),
302
- output_tokens: response_field(response, :output_tokens),
303
- thinking_tokens: response_field(response, :thinking_tokens)
304
- }.compact
305
- end
306
-
307
- def response_metadata(response)
308
- metadata = response_field(response, :metadata)
309
- metadata.respond_to?(:to_h) ? deep_symbolize(metadata) : {}
310
- end
311
-
312
- def response_field(response, field)
313
- return response[field] if response.respond_to?(:key?) && response.key?(field)
314
- return response[field.to_s] if response.respond_to?(:key?) && response.key?(field.to_s)
315
- return response.public_send(field) if response.respond_to?(field)
316
-
317
- nil
318
- end
319
-
320
- def dig(hash, *keys)
321
- keys.reduce(hash) do |current, key|
322
- break nil unless current.respond_to?(:key?)
323
-
324
- current[key.to_sym] || current[key.to_s]
325
- end
326
- end
327
-
328
243
  def truthy?(value)
329
244
  value == true || value.to_s == 'true'
330
245
  end
331
-
332
- def deep_symbolize(value)
333
- case value
334
- when Hash
335
- value.each_with_object({}) do |(key, child), result|
336
- result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize(child)
337
- end
338
- when Array
339
- value.map { |child| deep_symbolize(child) }
340
- else
341
- value
342
- end
343
- end
344
246
  end
345
247
  end
346
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