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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +107 -94
- data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
- data/lib/legion/extensions/llm/canonical/message.rb +53 -69
- data/lib/legion/extensions/llm/canonical/params.rb +58 -34
- data/lib/legion/extensions/llm/canonical/request.rb +68 -56
- data/lib/legion/extensions/llm/canonical/response.rb +62 -78
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
- data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
- 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 +7 -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 -156
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
- 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 +36 -100
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -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 +48 -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 +14 -26
- 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 +8 -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 +55 -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/config_spec.rb +222 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
- 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/client_translator_examples.rb +1 -2
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -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 +173 -95
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
- data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
- 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 +82 -47
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
- 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 +7 -3
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -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 +97 -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 -43
- 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
- metadata +13 -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 -310
- 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 -340
- 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)
|
|
@@ -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
|
|
16
|
-
# field of the 5-tuple lane id).
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
-
|
|
103
|
-
OPERATION_TO_LANE_TYPE.fetch(canonical)
|
|
93
|
+
OPERATION_TO_LANE_TYPE.fetch(normalize_operation(value: operation))
|
|
104
94
|
end
|
|
105
95
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
|
|
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
|
|
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'
|
|
@@ -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
|
-
# ---
|
|
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
|
-
# ---
|
|
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
|
|
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 '
|
|
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
|
|