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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +106 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
- data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
- data/lib/legion/extensions/llm/canonical/message.rb +52 -67
- data/lib/legion/extensions/llm/canonical/params.rb +59 -33
- data/lib/legion/extensions/llm/canonical/request.rb +67 -54
- data/lib/legion/extensions/llm/canonical/response.rb +61 -76
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
- data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
- data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
- data/lib/legion/extensions/llm/canonical.rb +6 -5
- data/lib/legion/extensions/llm/configuration.rb +40 -10
- data/lib/legion/extensions/llm/connection.rb +8 -30
- data/lib/legion/extensions/llm/credential_sources.rb +32 -49
- data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
- data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
- data/lib/legion/extensions/llm/error.rb +0 -14
- data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
- data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
- data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -153
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
- data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
- data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
- data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
- data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
- data/lib/legion/extensions/llm/inventory/records.rb +82 -99
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
- data/lib/legion/extensions/llm/provider.rb +192 -315
- data/lib/legion/extensions/llm/provider_contract.rb +25 -8
- data/lib/legion/extensions/llm/provider_settings.rb +5 -26
- data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
- data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
- data/lib/legion/extensions/llm/routing/records.rb +34 -17
- data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
- data/lib/legion/extensions/llm/streaming.rb +50 -35
- data/lib/legion/extensions/llm/taxonomies.rb +29 -17
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
- data/lib/legion/extensions/llm/utils.rb +23 -5
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +10 -98
- data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
- data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
- data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
- data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
- data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
- data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
- data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
- data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
- data/spec/legion/extensions/llm/error_spec.rb +2 -12
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +178 -67
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
- data/spec/legion/extensions/llm/inventory/records_spec.rb +129 -41
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +312 -0
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
- data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
- data/spec/legion/extensions/llm/provider_spec.rb +98 -78
- data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
- data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -16
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
- data/spec/legion/extensions/llm/utils_spec.rb +26 -7
- data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
- data/spec/legion/extensions/llm_extension_spec.rb +5 -5
- data/spec/support/fake_llm_provider.rb +45 -39
- data/spec/support/fake_ssot_harness.rb +7 -2
- data/spec/support/ssot_registry_helpers.rb +3 -2
- metadata +15 -54
- data/lib/legion/extensions/llm/agent.rb +0 -366
- data/lib/legion/extensions/llm/aliases.json +0 -436
- data/lib/legion/extensions/llm/aliases.rb +0 -67
- data/lib/legion/extensions/llm/attachment.rb +0 -229
- data/lib/legion/extensions/llm/chat.rb +0 -354
- data/lib/legion/extensions/llm/chunk.rb +0 -10
- data/lib/legion/extensions/llm/content.rb +0 -81
- data/lib/legion/extensions/llm/context.rb +0 -33
- data/lib/legion/extensions/llm/embedding.rb +0 -33
- data/lib/legion/extensions/llm/image.rb +0 -109
- data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -311
- data/lib/legion/extensions/llm/message.rb +0 -118
- data/lib/legion/extensions/llm/mime_type.rb +0 -75
- data/lib/legion/extensions/llm/model/info.rb +0 -286
- data/lib/legion/extensions/llm/model/modalities.rb +0 -26
- data/lib/legion/extensions/llm/model/pricing.rb +0 -52
- data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
- data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
- data/lib/legion/extensions/llm/model.rb +0 -11
- data/lib/legion/extensions/llm/models.json +0 -57313
- data/lib/legion/extensions/llm/models.rb +0 -530
- data/lib/legion/extensions/llm/models_schema.json +0 -168
- data/lib/legion/extensions/llm/moderation.rb +0 -60
- data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
- data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
- data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
- data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
- data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
- data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
- data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
- data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
- data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
- data/lib/legion/extensions/llm/thinking.rb +0 -53
- data/lib/legion/extensions/llm/tokens.rb +0 -51
- data/lib/legion/extensions/llm/tool_call.rb +0 -34
- data/lib/legion/extensions/llm/transcription.rb +0 -39
- data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
- data/spec/legion/extensions/llm/agent_spec.rb +0 -179
- data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
- data/spec/legion/extensions/llm/context_spec.rb +0 -127
- data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -337
- data/spec/legion/extensions/llm/message_spec.rb +0 -64
- data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
- data/spec/legion/extensions/llm/models_spec.rb +0 -104
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
block&.call(final_chunk) if final_chunk
|
|
39
|
+
accumulator.flush_pending_chunk.each { |chunk| block&.call(chunk) }
|
|
31
40
|
|
|
32
|
-
message = accumulator.
|
|
33
|
-
log.debug { "Stream completed: #{message.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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 =
|
|
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-
|
|
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]
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
41
|
-
protocol_version: @options[: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]
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
# ---
|
|
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
|
-
# ---
|
|
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
|
|
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 '
|
|
23
|
-
it '
|
|
24
|
-
expect(provider_module
|
|
25
|
-
|
|
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
|
|