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
@@ -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)
@@ -11,8 +11,24 @@ module Legion
11
11
  module Taxonomies
12
12
  TIERS = %i[direct local fleet cloud frontier].freeze
13
13
  TYPES = %i[inference embedding image audio].freeze
14
- CIRCUIT_STATES = %i[closed half_open open].freeze
15
- HEALTH_KEYS = %i[circuit_state denied available adjustment].freeze
14
+
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
+ OPERATION_TO_LANE_TYPE = {
22
+ chat: :inference,
23
+ stream_chat: :inference,
24
+ embed: :embedding,
25
+ image: :image,
26
+ transcribe: :audio,
27
+ translate: :audio,
28
+ speak: :audio,
29
+ moderate: :inference,
30
+ count_tokens: :inference
31
+ }.freeze
16
32
 
17
33
  # --- SSOT v3 runtime-contract enums (phase-1-lex-llm-additive.md section 6) ---
18
34
  OPERATIONS = %i[
@@ -68,22 +84,19 @@ module Legion
68
84
  failed_probe inconclusive_probe
69
85
  ].freeze
70
86
 
71
- # Only these aliases are accepted, and only when allow_aliases: true.
72
- OPERATION_ALIASES = {
73
- stream: :stream_chat,
74
- embedding: :embed,
75
- moderation: :moderate,
76
- audio_translation: :translate,
77
- speech: :speak
78
- }.freeze
79
-
80
87
  module_function
81
88
 
82
- # Normalize an operation to one of OPERATIONS. Ingress and protocol-v2
83
- # compatibility adapters pass allow_aliases: true; registry records and
84
- # exact-execution envelopes pass false. Unknown, empty, or invalid UTF-8
85
- # input always raises Inventory::Errors::ValidationError.
86
- def normalize_operation(value:, allow_aliases: false)
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.
92
+ def lane_type_for(operation:)
93
+ OPERATION_TO_LANE_TYPE.fetch(normalize_operation(value: operation))
94
+ end
95
+
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:)
87
100
  raise Inventory::Errors::ValidationError, 'operation must be a String or Symbol' unless value.is_a?(::String) || value.is_a?(::Symbol)
88
101
 
89
102
  string = value.to_s
@@ -94,7 +107,6 @@ module Legion
94
107
 
95
108
  candidate = trimmed.to_sym
96
109
  return candidate if OPERATIONS.include?(candidate)
97
- return OPERATION_ALIASES[candidate] if allow_aliases && OPERATION_ALIASES.key?(candidate)
98
110
 
99
111
  raise Inventory::Errors::ValidationError, "operation is not a canonical operation (#{candidate})"
100
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.3'
6
+ VERSION = '0.8.0'
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'
@@ -52,6 +50,8 @@ module Legion
52
50
  require_relative 'llm/inventory/errors'
53
51
  require_relative 'llm/inventory/immutable_value'
54
52
  require_relative 'llm/inventory/identity'
53
+ require_relative 'llm/inventory/weight_schema'
54
+ require_relative 'llm/inventory/weight_reconciler'
55
55
  require_relative 'llm/inventory/evidence'
56
56
  require_relative 'llm/inventory/callable_handle'
57
57
  require_relative 'llm/inventory/probe_token'
@@ -64,40 +64,19 @@ module Legion
64
64
  require_relative 'llm/routing/provider_outcome'
65
65
 
66
66
  # --- Base value objects (no internal deps) ---
67
- require_relative 'llm/mime_type'
68
- require_relative 'llm/model/info'
69
- require_relative 'llm/model/modalities'
70
- require_relative 'llm/model/pricing_category'
71
- require_relative 'llm/model/pricing_tier'
72
- require_relative 'llm/model/pricing'
73
67
  require_relative 'llm/configuration'
74
- require_relative 'llm/thinking'
75
- require_relative 'llm/tokens'
76
- require_relative 'llm/message'
77
- require_relative 'llm/tool_call'
78
- require_relative 'llm/content'
79
68
  require_relative 'llm/errors/unsupported_capability'
80
69
  require_relative 'llm/error'
81
70
 
82
- # --- Build on message/base types ---
83
- require_relative 'llm/chunk'
84
- require_relative 'llm/model'
85
- require_relative 'llm/attachment'
86
-
87
71
  # --- Streaming fundamentals (must load before streaming/provider) ---
72
+ require_relative 'llm/responses/thinking_extractor'
73
+ require_relative 'llm/responses/tool_arguments'
88
74
  require_relative 'llm/stream_accumulator'
89
- require_relative 'llm/responses/stream_chunk'
90
75
  require_relative 'llm/streaming'
91
76
 
92
- # --- Context, Connection ---
93
- require_relative 'llm/context'
77
+ # --- Connection ---
94
78
  require_relative 'llm/connection'
95
79
 
96
- # --- Response normalizers ---
97
- require_relative 'llm/responses/chat_response'
98
- require_relative 'llm/responses/embedding_response'
99
- require_relative 'llm/responses/thinking_extractor'
100
-
101
80
  # --- Provider base & allied modules ---
102
81
  require_relative 'llm/provider_contract'
103
82
  require_relative 'llm/stop_reason_mapping'
@@ -109,37 +88,19 @@ module Legion
109
88
 
110
89
  # --- Routing ---
111
90
  require_relative 'llm/routing'
112
- require_relative 'llm/routing/lane_key'
113
- require_relative 'llm/routing/offering_registry'
114
- require_relative 'llm/routing/registry_event'
115
- require_relative 'llm/routing/model_offering'
116
-
117
- # --- Models (scans for Provider subclasses) ---
118
- require_relative 'llm/models'
119
-
120
- # --- Agent & Chat (reference Provider, Context, Chat at method-time) ---
121
- require_relative 'llm/agent'
122
- require_relative 'llm/chat'
123
-
124
- # --- Domain services ---
125
- require_relative 'llm/embedding'
126
- require_relative 'llm/moderation'
127
- require_relative 'llm/image'
128
- require_relative 'llm/transcription'
129
91
 
130
- # --- Registry & misc support ---
131
- require_relative 'llm/registry_event_builder'
132
- require_relative 'llm/registry_publisher'
92
+ # --- Misc support ---
133
93
  require_relative 'llm/auto_registration'
134
94
  require_relative 'llm/credential_sources'
135
95
  require_relative 'llm/tool'
136
96
  require_relative 'llm/utils'
137
- require_relative 'llm/aliases'
138
97
 
139
- # --- Fleet protocol (depends on Provider, Models) ---
98
+ # --- Fleet protocol (depends on Provider) ---
140
99
  require_relative 'llm/fleet/protocol'
141
100
  require_relative 'llm/fleet/settings'
142
101
  require_relative 'llm/fleet/token_error'
102
+ require_relative 'llm/fleet/contract_error'
103
+ require_relative 'llm/fleet/fleet_envelope'
143
104
  require_relative 'llm/fleet/envelope_validation'
144
105
  require_relative 'llm/fleet/publish_safety'
145
106
  require_relative 'llm/fleet/default_exchange_reply'
@@ -173,58 +134,9 @@ module Legion
173
134
 
174
135
  Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
175
136
 
176
- # Provider-neutral value objects exposed under the Legion extension namespace.
177
- module Types
178
- ModelOffering = Routing::ModelOffering unless const_defined?(:ModelOffering, false)
179
- OfferingRegistry = Routing::OfferingRegistry unless const_defined?(:OfferingRegistry, false)
180
- RegistryEvent = Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
181
- end
182
-
183
- # Shared routing helpers exposed under the Legion extension namespace.
184
- module Routing
185
- LaneKey = ::Legion::Extensions::Llm::Routing::LaneKey unless const_defined?(:LaneKey, false)
186
- OfferingRegistry = ::Legion::Extensions::Llm::Routing::OfferingRegistry unless const_defined?(:OfferingRegistry,
187
- false)
188
- RegistryEvent = ::Legion::Extensions::Llm::Routing::RegistryEvent unless const_defined?(:RegistryEvent, false)
189
- end
190
-
191
137
  class << self
192
138
  def remote_invocable? = false
193
139
 
194
- def context
195
- context_config = config.dup
196
- yield context_config if block_given?
197
- Context.new(context_config)
198
- end
199
-
200
- def chat(...)
201
- Chat.new(...)
202
- end
203
-
204
- def embed(...)
205
- Embedding.embed(...)
206
- end
207
-
208
- def moderate(...)
209
- Moderation.moderate(...)
210
- end
211
-
212
- def paint(...)
213
- Image.paint(...)
214
- end
215
-
216
- def transcribe(...)
217
- Transcription.transcribe(...)
218
- end
219
-
220
- def models
221
- Models.instance
222
- end
223
-
224
- def providers
225
- Models.scan_provider_classes.values
226
- end
227
-
228
140
  def configure
229
141
  yield config
230
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