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
@@ -4,33 +4,43 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  # Handles streaming responses from AI providers.
7
+ # The provider's build_chunk yields Canonical::Chunk objects; the caller's
8
+ # block receives Canonical::Chunk objects and the sequence ends in exactly
9
+ # one done chunk (or an error chunk followed by the raise, 05 O5).
10
+ # FaradayHandlers / SSE parsing / error paths are unchanged plumbing; the
11
+ # status→error mapping delegates to the ONE mapper (ErrorMiddleware, 10 U8).
7
12
  module Streaming
8
13
  include Legion::Logging::Helper
9
14
  extend Legion::Logging::Helper
10
15
 
11
16
  module_function
12
17
 
13
- def stream_response(connection, payload, additional_headers = {}, &block)
18
+ def stream_response(connection, payload, additional_headers = {}, model: nil, &block)
14
19
  accumulator = StreamAccumulator.new
15
20
 
16
- response = connection.post stream_url, payload do |req|
17
- req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
18
- on_chunk = build_stream_callback(accumulator, block)
19
- log.debug { "Stream callback prepared: #{on_chunk.inspect}" } if Legion::Extensions::Llm.config.log_stream_debug
20
- if faraday_1?
21
- req.options[:on_data] = handle_stream(&on_chunk)
22
- else
23
- req.options.on_data = handle_stream(&on_chunk)
21
+ begin
22
+ connection.post stream_url, payload do |req|
23
+ req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
24
+ on_chunk = build_stream_callback(accumulator, block)
25
+ log.debug { "Stream callback prepared: #{on_chunk.inspect}" } if Legion::Extensions::Llm.config.log_stream_debug
26
+ if faraday_1?
27
+ req.options[:on_data] = handle_stream(&on_chunk)
28
+ else
29
+ req.options.on_data = handle_stream(&on_chunk)
30
+ end
24
31
  end
32
+ rescue StandardError => e
33
+ block&.call(Canonical::Chunk.error_chunk(error: e, request_id: nil))
34
+ raise
25
35
  end
26
36
 
27
37
  # Release any text held by the untagged-preamble heuristic so short
28
38
  # responses still stream at least one delta to the caller.
29
- final_chunk = accumulator.flush_pending_chunk
30
- block&.call(final_chunk) if final_chunk
39
+ accumulator.flush_pending_chunk.each { |chunk| block&.call(chunk) }
31
40
 
32
- message = accumulator.to_message(response)
33
- log.debug { "Stream completed: #{message.content}" }
41
+ message = accumulator.to_response(model:)
42
+ log.debug { "Stream completed: #{message.text}" }
43
+ block&.call(Canonical::Chunk.done(request_id: nil, usage: message.usage, stop_reason: message.stop_reason))
34
44
  message
35
45
  end
36
46
 
@@ -38,9 +48,7 @@ module Legion
38
48
  proc do |chunk|
39
49
  next unless chunk
40
50
 
41
- accumulator.add chunk
42
- filtered = accumulator.filtered_chunk(chunk)
43
- block.call(filtered) if filtered
51
+ accumulator.add(chunk).each { |emitted| block&.call(emitted) }
44
52
  end
45
53
  end
46
54
 
@@ -151,27 +159,13 @@ module Legion
151
159
  raise_streaming_status_error(status, msg)
152
160
  end
153
161
 
162
+ # 10 U8: the streaming path delegates to the ONE status→error mapper
163
+ # (ErrorMiddleware.parse_error) — the inlined case table is deleted.
164
+ # The provider's parse_error extracts the message from the synthesized
165
+ # body.
154
166
  def raise_streaming_status_error(status, message)
155
167
  response = Struct.new(:body, :status).new({ 'error' => { 'message' => message } }, status)
156
- case status
157
- when 400
158
- raise Legion::Extensions::Llm::BadRequestError.new(response, message)
159
- when 401
160
- raise Legion::Extensions::Llm::UnauthorizedError.new(response, message)
161
- when 403
162
- raise Legion::Extensions::Llm::ForbiddenError.new(response, message)
163
- when 429
164
- raise Legion::Extensions::Llm::RateLimitError.new(response, message)
165
- when 500
166
- raise Legion::Extensions::Llm::ServerError.new(response, message)
167
- when 502..504
168
- raise Legion::Extensions::Llm::ServiceUnavailableError.new(response, message)
169
- when 529
170
- raise Legion::Extensions::Llm::OverloadedError.new(response, message)
171
- else
172
- provider = respond_to?(:parse_error) ? self : nil
173
- Legion::Extensions::Llm::ErrorMiddleware.parse_error(provider: provider, response: response)
174
- end
168
+ ErrorMiddleware.parse_error(provider: respond_to?(:parse_error) ? self : nil, response:)
175
169
  end
176
170
 
177
171
  def handle_sse(chunk, parser, env, &)
@@ -186,12 +180,19 @@ module Legion
186
180
  end
187
181
 
188
182
  def handle_data(data, env)
183
+ # An empty data frame carries nothing — it is not a parse failure.
184
+ return if data.to_s.strip.empty?
185
+
189
186
  parsed = Legion::JSON.parse(data, symbolize_names: false)
190
187
  return parsed unless parsed.is_a?(Hash) && parsed.key?('error')
191
188
 
192
189
  handle_parsed_error(parsed, env)
193
190
  rescue Legion::JSON::ParseError => e
191
+ # M1: an unparseable mid-stream data frame is still a provider
192
+ # error — a classified failure through the one mapper, never a
193
+ # silent drop (the stream must not complete "successfully").
194
194
  handle_exception(e, level: :warn, handled: true, operation: 'llm.streaming.handle_data')
195
+ raise_unparseable_streaming_error(env, data, 'data frame')
195
196
  end
196
197
 
197
198
  def handle_error_event(data, env)
@@ -216,7 +217,21 @@ module Legion
216
217
  parsed_data = Legion::JSON.parse(data, symbolize_names: false)
217
218
  handle_parsed_error(parsed_data, env)
218
219
  rescue Legion::JSON::ParseError => e
220
+ # M1: an error event that cannot be parsed is STILL an error event
221
+ # — a classified failure, never a silent nil (the pre-M1 fail-open
222
+ # completed the stream "successfully" on the provider's explicit
223
+ # error signal).
219
224
  handle_exception(e, level: :warn, handled: true, operation: 'llm.streaming.parse_error_from_json')
225
+ raise_unparseable_streaming_error(env, data, 'error event')
226
+ end
227
+
228
+ # One classified failure for unparseable stream error content — the
229
+ # same 500 classification the PARSEABLE in-band error path uses
230
+ # (parse_streaming_error): an in-band provider error event is a
231
+ # failure regardless of the stream's HTTP status. The raw payload is
232
+ # bounded (200 chars) — never the full body.
233
+ def raise_unparseable_streaming_error(_env, data, kind)
234
+ raise_streaming_status_error(500, "Provider error: unparseable #{kind} (#{data.to_s[0, 200].inspect})")
220
235
  end
221
236
 
222
237
  def build_stream_error_response(parsed_data, env, status)
@@ -12,12 +12,12 @@ module Legion
12
12
  TIERS = %i[direct local fleet cloud frontier].freeze
13
13
  TYPES = %i[inference embedding image audio].freeze
14
14
 
15
- # The current authoritative operation → Inventory lane-type mapping (the 4th
16
- # field of the 5-tuple lane id). NON-LEGACY home: the deletion-scheduled
17
- # LegacyCoordinatorAdapter consumes `lane_type_for` below until deletion;
18
- # it must not retain a second mapping. moderate/count_tokens
19
- # are non-generative inference operations — no moderation/count type exists in
20
- # TYPES, so they map to :inference.
15
+ # The authoritative operation → Inventory lane-type mapping (the 4th
16
+ # field of the 5-tuple lane id). Single home for the mapping: the
17
+ # identity composer consumes `lane_type_for` below; no layer retains a
18
+ # second table. moderate/count_tokens are non-generative inference
19
+ # operations — no moderation/count type exists in TYPES, so they map to
20
+ # :inference.
21
21
  OPERATION_TO_LANE_TYPE = {
22
22
  chat: :inference,
23
23
  stream_chat: :inference,
@@ -30,9 +30,6 @@ module Legion
30
30
  count_tokens: :inference
31
31
  }.freeze
32
32
 
33
- CIRCUIT_STATES = %i[closed half_open open].freeze
34
- HEALTH_KEYS = %i[circuit_state denied available adjustment].freeze
35
-
36
33
  # --- SSOT v3 runtime-contract enums (phase-1-lex-llm-additive.md section 6) ---
37
34
  OPERATIONS = %i[
38
35
  chat stream_chat embed image transcribe translate speak moderate count_tokens
@@ -87,27 +84,19 @@ module Legion
87
84
  failed_probe inconclusive_probe
88
85
  ].freeze
89
86
 
90
- # Only these aliases are accepted, and only when allow_aliases: true.
91
- OPERATION_ALIASES = {
92
- stream: :stream_chat,
93
- embedding: :embed,
94
- moderation: :moderate,
95
- audio_translation: :translate,
96
- speech: :speak
97
- }.freeze
98
-
99
87
  module_function
100
88
 
89
+ # The one operation → lane-type resolution: canonicalizes the operation,
90
+ # then reads the single OPERATION_TO_LANE_TYPE table. Unknown operations
91
+ # raise via normalize_operation.
101
92
  def lane_type_for(operation:)
102
- canonical = normalize_operation(value: operation, allow_aliases: false)
103
- OPERATION_TO_LANE_TYPE.fetch(canonical)
93
+ OPERATION_TO_LANE_TYPE.fetch(normalize_operation(value: operation))
104
94
  end
105
95
 
106
- # Normalize an operation to one of OPERATIONS. Ingress and protocol-v2
107
- # compatibility adapters pass allow_aliases: true; registry records and
108
- # exact-execution envelopes pass false. Unknown, empty, or invalid UTF-8
109
- # input always raises Inventory::Errors::ValidationError.
110
- def normalize_operation(value:, allow_aliases: false)
96
+ # The one operation spelling (06 P5): canonical operations only, no
97
+ # aliases. Unknown, empty, or invalid UTF-8 input always raises
98
+ # Inventory::Errors::ValidationError.
99
+ def normalize_operation(value:)
111
100
  raise Inventory::Errors::ValidationError, 'operation must be a String or Symbol' unless value.is_a?(::String) || value.is_a?(::Symbol)
112
101
 
113
102
  string = value.to_s
@@ -118,7 +107,6 @@ module Legion
118
107
 
119
108
  candidate = trimmed.to_sym
120
109
  return candidate if OPERATIONS.include?(candidate)
121
- return OPERATION_ALIASES[candidate] if allow_aliases && OPERATION_ALIASES.key?(candidate)
122
110
 
123
111
  raise Inventory::Errors::ValidationError, "operation is not a canonical operation (#{candidate})"
124
112
  end
@@ -4,20 +4,18 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Transport
7
- # Shared RabbitMQ live-work lane defaults for provider fleet workers.
7
+ # Shared RabbitMQ live-work lane construction for provider fleet
8
+ # workers. The queue defaults live in ONE home — Llm.default_settings
9
+ # (10 U10: no inline literal defaults).
8
10
  module FleetLane
9
- DEFAULTS = {
10
- queue_expires_ms: 60_000,
11
- message_ttl_ms: 120_000,
12
- queue_max_length: 100,
13
- delivery_limit: 3,
14
- consumer_ack_timeout_ms: 300_000
15
- }.freeze
16
-
17
11
  module_function
18
12
 
13
+ def queue_defaults
14
+ Legion::Extensions::Llm.default_settings.dig(:fleet, :consumer) || {}
15
+ end
16
+
19
17
  def queue_options(settings = {})
20
- config = DEFAULTS.merge((settings || {}).compact.transform_keys(&:to_sym))
18
+ config = queue_defaults.merge((settings || {}).compact.transform_keys(&:to_sym))
21
19
  {
22
20
  durable: true,
23
21
  auto_delete: false,
@@ -11,7 +11,8 @@ module Legion
11
11
  module Llm
12
12
  module Transport
13
13
  module Messages
14
- # Correlated protocol-v2 error envelope for fleet reply queues.
14
+ # Correlated protocol-v3 error envelope for fleet reply queues (E6).
15
+ # E5: no aliases — provider_instance only.
15
16
  class FleetError < ::Legion::Transport::Message
16
17
  include Fleet::DefaultExchangeReply
17
18
  include Fleet::EnvelopeValidation
@@ -45,7 +46,7 @@ module Legion
45
46
  idempotency_key: @options[:idempotency_key],
46
47
  operation: @options[:operation],
47
48
  provider: @options[:provider],
48
- provider_instance: @options[:provider_instance] || @options[:instance],
49
+ provider_instance: @options[:provider_instance],
49
50
  model: @options[:model],
50
51
  reply_to: reply_to,
51
52
  message_context: @options[:message_context],
@@ -11,7 +11,8 @@ module Legion
11
11
  module Llm
12
12
  module Transport
13
13
  module Messages
14
- # Strict protocol-v2 request envelope for outbound fleet work.
14
+ # Strict protocol-v3 request envelope for outbound fleet work.
15
+ # E2: required fields come from the one Fleet::Protocol::REQUIRED_FIELDS list.
15
16
  class FleetRequest < ::Legion::Transport::Message
16
17
  include Fleet::EnvelopeValidation
17
18
  include Fleet::PublishSafety
@@ -23,11 +24,6 @@ module Legion
23
24
  spool: false,
24
25
  return_result: true
25
26
  }.freeze
26
- REQUIRED_OPTIONS = %i[
27
- request_id correlation_id operation provider provider_instance model params reply_to
28
- message_context caller trace_context signed_token timeout_seconds expires_at protocol_version
29
- idempotency_key
30
- ].freeze
31
27
 
32
28
  def exchange = Exchanges::Fleet
33
29
  def type = Fleet::Protocol::REQUEST_TYPE
@@ -78,7 +74,7 @@ module Legion
78
74
  def validate
79
75
  reject_legacy_options!
80
76
  require_option!(:routing_key)
81
- REQUIRED_OPTIONS.each { |key| require_option!(key) }
77
+ Fleet::Protocol::REQUIRED_FIELDS.each { |key| require_option!(key) }
82
78
  require_protocol_version!
83
79
  @valid = true
84
80
  end
@@ -100,7 +96,9 @@ module Legion
100
96
  trace_context: @options[:trace_context],
101
97
  signed_token: @options[:signed_token],
102
98
  timeout_seconds: @options[:timeout_seconds],
103
- expires_at: @options[:expires_at]
99
+ expires_at: @options[:expires_at],
100
+ execution_contract: @options[:execution_contract],
101
+ offering_id: @options[:offering_id]
104
102
  ).compact
105
103
  end
106
104
 
@@ -11,7 +11,11 @@ module Legion
11
11
  module Llm
12
12
  module Transport
13
13
  module Messages
14
- # Correlated protocol-v2 response envelope for fleet reply queues.
14
+ # Correlated protocol-v3 response envelope for fleet reply queues.
15
+ # E3: carries the serialized Canonical::Response under :response.
16
+ # E4: thinking exclusion happens exactly once at the responder
17
+ # (ProviderResponder#publish_response); this envelope is a pass-through.
18
+ # E5: no aliases — provider_instance only.
15
19
  class FleetResponse < ::Legion::Transport::Message
16
20
  include Fleet::DefaultExchangeReply
17
21
  include Fleet::EnvelopeValidation
@@ -32,28 +36,25 @@ module Legion
32
36
  require_option!(:request_id)
33
37
  require_option!(:correlation_id)
34
38
  require_option!(:reply_to)
39
+ require_option!(:response)
35
40
  require_protocol_version!
36
41
  @valid = true
37
42
  end
38
43
 
39
44
  def message
40
- super.except(:thinking).merge(
41
- protocol_version: @options[:protocol_version] || Fleet::Protocol::VERSION,
45
+ super.merge(
46
+ protocol_version: @options[:protocol_version],
42
47
  request_id: @options[:request_id],
43
48
  correlation_id: correlation_id,
44
49
  idempotency_key: @options[:idempotency_key],
45
50
  operation: @options[:operation],
46
51
  provider: @options[:provider],
47
- provider_instance: @options[:provider_instance] || @options[:instance],
52
+ provider_instance: @options[:provider_instance],
48
53
  model: @options[:model],
49
54
  reply_to: reply_to,
50
55
  message_context: @options[:message_context],
51
56
  trace_context: @options[:trace_context],
52
- content: @options[:content],
53
- tool_calls: @options[:tool_calls],
54
- usage: @options[:usage] || {},
55
- finish_reason: @options[:finish_reason],
56
- metadata: @options[:metadata] || {},
57
+ response: @options[:response],
57
58
  execution_contract: @options[:execution_contract],
58
59
  offering_id: @options[:offering_id]
59
60
  ).compact
@@ -7,10 +7,6 @@ module Legion
7
7
  module Utils
8
8
  module_function
9
9
 
10
- def hash_get(hash, key)
11
- hash[key.to_sym] || hash[key.to_s]
12
- end
13
-
14
10
  def to_safe_array(item)
15
11
  case item
16
12
  when Array
@@ -34,8 +30,10 @@ module Legion
34
30
  value.is_a?(Date) ? value : Date.parse(value.to_s)
35
31
  end
36
32
 
33
+ # Recursive merge with the safer dup-before-merge policy (10 U7):
34
+ # neither input is mutated.
37
35
  def deep_merge(original, overrides)
38
- original.merge(overrides) do |_key, original_value, overrides_value|
36
+ deep_dup(original || {}).merge(deep_dup(overrides || {})) do |_key, original_value, overrides_value|
39
37
  if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
40
38
  deep_merge(original_value, overrides_value)
41
39
  else
@@ -44,6 +42,26 @@ module Legion
44
42
  end
45
43
  end
46
44
 
45
+ # The one host-locality detector (10 §1E): localhost/loopback by host,
46
+ # accepting full URLs and bare host[:port] values.
47
+ def localhost_url?(url)
48
+ require 'uri'
49
+ text = url.to_s.strip
50
+ return false if text.empty?
51
+
52
+ host =
53
+ begin
54
+ parsed = text.match?(%r{\A[a-z][a-z0-9+.-]*://}) ? URI.parse(text) : URI.parse("http://#{text}")
55
+ parsed.host
56
+ rescue URI::InvalidURIError
57
+ text.split(%r{[:/\s]}, 2).first
58
+ end
59
+ return false if host.nil? || host.empty?
60
+
61
+ normalized = host.delete_prefix('[').delete_suffix(']').downcase
62
+ %w[localhost 127.0.0.1 ::1].include?(normalized)
63
+ end
64
+
47
65
  def deep_dup(value)
48
66
  case value
49
67
  when Hash
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Llm
6
- VERSION = '0.7.6'
6
+ VERSION = '0.8.4'
7
7
  end
8
8
  end
9
9
  end
@@ -36,11 +36,9 @@ module Legion
36
36
  # unqualified constant lookups resolve via Ruby scope. #
37
37
  # ------------------------------------------------------------------ #
38
38
 
39
- # --- P1 SSOT: taxonomy enums, capability normalization, inventory writer mixin ---
39
+ # --- P1 SSOT: taxonomy enums, capability normalization ---
40
40
  require_relative 'llm/taxonomies'
41
41
  require_relative 'llm/capabilities'
42
- require_relative 'llm/inventory/scoped_refresher'
43
- require_relative 'llm/inventory/capabilities'
44
42
 
45
43
  # --- Capability resolution policy (no internal deps) ---
46
44
  require_relative 'llm/capability_policy'
@@ -66,40 +64,19 @@ module Legion
66
64
  require_relative 'llm/routing/provider_outcome'
67
65
 
68
66
  # --- Base value objects (no internal deps) ---
69
- require_relative 'llm/mime_type'
70
- require_relative 'llm/model/info'
71
- require_relative 'llm/model/modalities'
72
- require_relative 'llm/model/pricing_category'
73
- require_relative 'llm/model/pricing_tier'
74
- require_relative 'llm/model/pricing'
75
67
  require_relative 'llm/configuration'
76
- require_relative 'llm/thinking'
77
- require_relative 'llm/tokens'
78
- require_relative 'llm/message'
79
- require_relative 'llm/tool_call'
80
- require_relative 'llm/content'
81
68
  require_relative 'llm/errors/unsupported_capability'
82
69
  require_relative 'llm/error'
83
70
 
84
- # --- Build on message/base types ---
85
- require_relative 'llm/chunk'
86
- require_relative 'llm/model'
87
- require_relative 'llm/attachment'
88
-
89
71
  # --- Streaming fundamentals (must load before streaming/provider) ---
72
+ require_relative 'llm/responses/thinking_extractor'
73
+ require_relative 'llm/responses/tool_arguments'
90
74
  require_relative 'llm/stream_accumulator'
91
- require_relative 'llm/responses/stream_chunk'
92
75
  require_relative 'llm/streaming'
93
76
 
94
- # --- Context, Connection ---
95
- require_relative 'llm/context'
77
+ # --- Connection ---
96
78
  require_relative 'llm/connection'
97
79
 
98
- # --- Response normalizers ---
99
- require_relative 'llm/responses/chat_response'
100
- require_relative 'llm/responses/embedding_response'
101
- require_relative 'llm/responses/thinking_extractor'
102
-
103
80
  # --- Provider base & allied modules ---
104
81
  require_relative 'llm/provider_contract'
105
82
  require_relative 'llm/stop_reason_mapping'
@@ -111,37 +88,19 @@ module Legion
111
88
 
112
89
  # --- Routing ---
113
90
  require_relative 'llm/routing'
114
- require_relative 'llm/routing/lane_key'
115
- require_relative 'llm/routing/offering_registry'
116
- require_relative 'llm/routing/registry_event'
117
- require_relative 'llm/routing/model_offering'
118
-
119
- # --- Models (scans for Provider subclasses) ---
120
- require_relative 'llm/models'
121
-
122
- # --- Agent & Chat (reference Provider, Context, Chat at method-time) ---
123
- require_relative 'llm/agent'
124
- require_relative 'llm/chat'
125
-
126
- # --- Domain services ---
127
- require_relative 'llm/embedding'
128
- require_relative 'llm/moderation'
129
- require_relative 'llm/image'
130
- require_relative 'llm/transcription'
131
91
 
132
- # --- Registry & misc support ---
133
- require_relative 'llm/registry_event_builder'
134
- require_relative 'llm/registry_publisher'
92
+ # --- Misc support ---
135
93
  require_relative 'llm/auto_registration'
136
94
  require_relative 'llm/credential_sources'
137
95
  require_relative 'llm/tool'
138
96
  require_relative 'llm/utils'
139
- require_relative 'llm/aliases'
140
97
 
141
- # --- Fleet protocol (depends on Provider, Models) ---
98
+ # --- Fleet protocol (depends on Provider) ---
142
99
  require_relative 'llm/fleet/protocol'
143
100
  require_relative 'llm/fleet/settings'
144
101
  require_relative 'llm/fleet/token_error'
102
+ require_relative 'llm/fleet/contract_error'
103
+ require_relative 'llm/fleet/fleet_envelope'
145
104
  require_relative 'llm/fleet/envelope_validation'
146
105
  require_relative 'llm/fleet/publish_safety'
147
106
  require_relative 'llm/fleet/default_exchange_reply'
@@ -175,58 +134,9 @@ module Legion
175
134
 
176
135
  Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
177
136
 
178
- # Provider-neutral value objects exposed under the Legion extension namespace.
179
- module Types
180
- ModelOffering = Routing::ModelOffering unless const_defined?(:ModelOffering, false)
181
- OfferingRegistry = Routing::OfferingRegistry unless const_defined?(:OfferingRegistry, false)
182
- RegistryEvent = Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
183
- end
184
-
185
- # Shared routing helpers exposed under the Legion extension namespace.
186
- module Routing
187
- LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
188
- OfferingRegistry = ::Legion::Extensions::Llm::Routing::OfferingRegistry unless const_defined?(:OfferingRegistry,
189
- false)
190
- RegistryEvent = ::Legion::Extensions::Llm::Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
191
- end
192
-
193
137
  class << self
194
138
  def remote_invocable? = false
195
139
 
196
- def context
197
- context_config = config.dup
198
- yield context_config if block_given?
199
- Context.new(context_config)
200
- end
201
-
202
- def chat(...)
203
- Chat.new(...)
204
- end
205
-
206
- def embed(...)
207
- Embedding.embed(...)
208
- end
209
-
210
- def moderate(...)
211
- Moderation.moderate(...)
212
- end
213
-
214
- def paint(...)
215
- Image.paint(...)
216
- end
217
-
218
- def transcribe(...)
219
- Transcription.transcribe(...)
220
- end
221
-
222
- def models
223
- Models.instance
224
- end
225
-
226
- def providers
227
- Models.scan_provider_classes.values
228
- end
229
-
230
140
  def configure
231
141
  yield config
232
142
  end
@@ -19,15 +19,10 @@ RSpec.describe Legion::Extensions::Llm::AutoRegistration do
19
19
  mod
20
20
  end
21
21
 
22
- describe '#discover_instances' do
23
- it 'returns an empty hash by default' do
24
- expect(provider_module.discover_instances).to eq({})
25
- end
26
- end
27
-
28
- describe '#provider_aliases' do
29
- it 'returns an empty alias list by default' do
30
- expect(provider_module.provider_aliases).to eq([])
22
+ describe '0.8.0 rip — legacy defaults deleted' do
23
+ it 'provides no discover_instances or provider_aliases defaults' do
24
+ expect(provider_module).not_to respond_to(:discover_instances)
25
+ expect(provider_module).not_to respond_to(:provider_aliases)
31
26
  end
32
27
  end
33
28