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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/llm/utils'
|
|
4
|
+
require_relative 'contract_error'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Llm
|
|
9
|
+
module Fleet
|
|
10
|
+
# The ONE fleet wire-normalization entry (10 U5): typed readers over
|
|
11
|
+
# the raw wire Hash. Every fleet boundary (responder, worker, token
|
|
12
|
+
# validator) reads envelopes through these readers. Data is kept RAW
|
|
13
|
+
# (no eager symbolization) so the worker can still detect duplicate
|
|
14
|
+
# String/Symbol param spellings (W5).
|
|
15
|
+
FleetEnvelope = Struct.new(:data, keyword_init: true) do
|
|
16
|
+
# Wrap a wire payload (Hash) or pass an existing envelope through.
|
|
17
|
+
# L12: a non-Hash payload is a typed ContractError at the wrap
|
|
18
|
+
# boundary — the module is total instead of relying on call-site
|
|
19
|
+
# discipline (an untyped NoMethodError at the first reader).
|
|
20
|
+
def self.wrap(payload)
|
|
21
|
+
return payload if payload.is_a?(FleetEnvelope)
|
|
22
|
+
|
|
23
|
+
raise ContractError, "fleet envelope payload expected Hash, got #{payload.class}" unless payload.is_a?(::Hash)
|
|
24
|
+
|
|
25
|
+
new(data: payload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key)
|
|
29
|
+
symbol_key = key.to_sym
|
|
30
|
+
return data[symbol_key] if data.key?(symbol_key)
|
|
31
|
+
|
|
32
|
+
data[key.to_s]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key?(key)
|
|
36
|
+
data.key?(key.to_sym) || data.key?(key.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch(key, default = nil)
|
|
40
|
+
key?(key) ? self[key] : default
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
Utils.deep_symbolize_keys(data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def protocol_version = self[:protocol_version]
|
|
48
|
+
def request_id = self[:request_id]
|
|
49
|
+
def correlation_id = self[:correlation_id]
|
|
50
|
+
def idempotency_key = self[:idempotency_key]
|
|
51
|
+
def operation = self[:operation]
|
|
52
|
+
def provider = self[:provider]
|
|
53
|
+
def provider_instance = self[:provider_instance]
|
|
54
|
+
def model = self[:model]
|
|
55
|
+
def params = self[:params] || {}
|
|
56
|
+
def reply_to = self[:reply_to]
|
|
57
|
+
def message_context = self[:message_context] || {}
|
|
58
|
+
def trace_context = self[:trace_context] || {}
|
|
59
|
+
def execution_contract = self[:execution_contract]
|
|
60
|
+
def offering_id = self[:offering_id]
|
|
61
|
+
def signed_token = self[:signed_token]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -5,15 +5,19 @@ module Legion
|
|
|
5
5
|
module Llm
|
|
6
6
|
module Fleet
|
|
7
7
|
module Protocol
|
|
8
|
-
|
|
8
|
+
# Fleet protocol v3 — the contract cut (06 P1). v2 envelopes are
|
|
9
|
+
# rejected by design; there is no dual-protocol coexistence.
|
|
10
|
+
VERSION = 3
|
|
9
11
|
REQUEST_TYPE = 'llm.fleet.request'
|
|
10
12
|
RESPONSE_TYPE = 'llm.fleet.response'
|
|
11
13
|
ERROR_TYPE = 'llm.fleet.error'
|
|
12
14
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
15
|
+
# P4: v1 legacy options rejected at both edges (one list).
|
|
16
|
+
LEGACY_FIELDS = %i[schema_version request_type fleet_correlation_id].freeze
|
|
17
|
+
|
|
18
|
+
# Every request is exact-execution (06 P2): the marker is REQUIRED and
|
|
19
|
+
# must equal this value (it names the contract semantics, not the
|
|
20
|
+
# protocol version). Marker absence is rejected.
|
|
17
21
|
EXACT_EXECUTION_CONTRACT = 'exact_offering_v1'
|
|
18
22
|
EXACT_REQUIRED_FIELDS = %i[
|
|
19
23
|
execution_contract offering_id provider provider_instance model operation
|
|
@@ -21,6 +25,15 @@ module Legion
|
|
|
21
25
|
EXACT_SIGNED_SCALAR_CLAIMS = %i[
|
|
22
26
|
execution_contract offering_id
|
|
23
27
|
].freeze
|
|
28
|
+
|
|
29
|
+
# The complete required envelope field set (06 E2) — the 16 protocol
|
|
30
|
+
# fields plus the two exact fields. Both edges (FleetRequest and
|
|
31
|
+
# ProviderResponder) consume this one list.
|
|
32
|
+
REQUIRED_FIELDS = %i[
|
|
33
|
+
request_id correlation_id idempotency_key operation provider provider_instance model params reply_to
|
|
34
|
+
message_context caller trace_context signed_token timeout_seconds expires_at protocol_version
|
|
35
|
+
execution_contract offering_id
|
|
36
|
+
].freeze
|
|
24
37
|
end
|
|
25
38
|
end
|
|
26
39
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
|
|
3
|
+
require 'legion/extensions/llm/utils'
|
|
4
|
+
require 'legion/extensions/llm/inventory/errors'
|
|
5
|
+
require 'legion/extensions/llm/routing/provider_outcome'
|
|
5
6
|
require_relative 'protocol'
|
|
6
7
|
require_relative 'settings'
|
|
8
|
+
require_relative 'contract_error'
|
|
9
|
+
require_relative 'fleet_envelope'
|
|
7
10
|
require_relative 'worker_execution'
|
|
8
11
|
require 'legion/extensions/llm/inventory/registry'
|
|
9
12
|
|
|
@@ -22,60 +25,28 @@ module Legion
|
|
|
22
25
|
|
|
23
26
|
module Fleet
|
|
24
27
|
# Shared implementation for provider-owned fleet responder runners.
|
|
28
|
+
# Protocol v3 (06 W9): parse → legacy-field rejection → required fields
|
|
29
|
+
# (one Protocol::REQUIRED_FIELDS list) → explicit version →
|
|
30
|
+
# provider-family match → exact execution contract (required, P2) →
|
|
31
|
+
# WorkerExecution.call → publish FleetResponse (E3/E4) → ack. On error:
|
|
32
|
+
# publish FleetError (E6), reject per F6, re-raise.
|
|
25
33
|
module ProviderResponder
|
|
26
34
|
include Legion::Logging::Helper
|
|
27
35
|
extend Legion::Logging::Helper
|
|
28
36
|
|
|
29
37
|
class ConfigurationError < StandardError; end
|
|
30
38
|
|
|
31
|
-
REQUIRED_FIELDS = %i[
|
|
32
|
-
request_id correlation_id idempotency_key operation provider provider_instance model params reply_to
|
|
33
|
-
message_context caller trace_context signed_token timeout_seconds expires_at protocol_version
|
|
34
|
-
].freeze
|
|
35
|
-
LEGACY_FIELDS = %i[schema_version request_type fleet_correlation_id].freeze
|
|
36
|
-
|
|
37
|
-
FleetEnvelope = Struct.new(:data, keyword_init: true) do
|
|
38
|
-
def [](key)
|
|
39
|
-
symbol_key = key.to_sym
|
|
40
|
-
return data[symbol_key] if data.key?(symbol_key)
|
|
41
|
-
|
|
42
|
-
data[key.to_s]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def key?(key)
|
|
46
|
-
data.key?(key.to_sym) || data.key?(key.to_s)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def fetch(key, default = nil)
|
|
50
|
-
key?(key) ? self[key] : default
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def to_h = data
|
|
54
|
-
def protocol_version = self[:protocol_version]
|
|
55
|
-
def request_id = self[:request_id]
|
|
56
|
-
def correlation_id = self[:correlation_id]
|
|
57
|
-
def idempotency_key = self[:idempotency_key]
|
|
58
|
-
def operation = self[:operation]
|
|
59
|
-
def provider = self[:provider]
|
|
60
|
-
def provider_instance = self[:provider_instance]
|
|
61
|
-
def model = self[:model]
|
|
62
|
-
def params = self[:params] || {}
|
|
63
|
-
def reply_to = self[:reply_to]
|
|
64
|
-
def message_context = self[:message_context] || {}
|
|
65
|
-
def trace_context = self[:trace_context] || {}
|
|
66
|
-
def execution_contract = self[:execution_contract]
|
|
67
|
-
def offering_id = self[:offering_id]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
39
|
module_function
|
|
71
40
|
|
|
72
41
|
# Public runner entry point mirrors AMQP delivery callbacks, which carry both delivery and property metadata.
|
|
73
|
-
#
|
|
74
|
-
|
|
42
|
+
# L6: the dead provider_class/provider_instances params are deleted —
|
|
43
|
+
# v3 dispatch is exact-only and never constructs a provider; passing
|
|
44
|
+
# provider objects here was a latent second execution truth.
|
|
45
|
+
def call(payload:, provider_family:,
|
|
75
46
|
registry: ::Legion::Extensions::Llm::Inventory::Registry, delivery: nil, properties: nil)
|
|
76
47
|
envelope = parse_payload(payload)
|
|
77
48
|
check_envelope!(envelope, provider_family:)
|
|
78
|
-
response =
|
|
49
|
+
response = WorkerExecution.call(envelope: envelope, registry:)
|
|
79
50
|
publish_response(envelope, response)
|
|
80
51
|
ack(delivery || properties)
|
|
81
52
|
response
|
|
@@ -86,30 +57,30 @@ module Legion
|
|
|
86
57
|
reject(delivery || properties, requeue: requeue_error?(e))
|
|
87
58
|
raise
|
|
88
59
|
end
|
|
89
|
-
# rubocop:enable Metrics/ParameterLists
|
|
90
60
|
|
|
91
61
|
def enabled_for?(provider_instances)
|
|
92
62
|
instances = resolve_provider_instances(provider_instances)
|
|
93
63
|
instances.any? do |_instance_id, settings|
|
|
94
|
-
truthy?(dig(
|
|
64
|
+
truthy?(Utils.deep_symbolize_keys(settings).dig(:fleet, :respond_to_requests))
|
|
95
65
|
end
|
|
96
66
|
end
|
|
97
67
|
|
|
68
|
+
# E1: the single wire-normalization entry. Wrong-shape payloads
|
|
69
|
+
# (neither Hash, String, nor envelope) raise — the silent {} fallback
|
|
70
|
+
# is deleted.
|
|
98
71
|
def parse_payload(payload)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
end
|
|
107
|
-
FleetEnvelope.new(data: deep_symbolize(hash))
|
|
72
|
+
case payload
|
|
73
|
+
when FleetEnvelope then payload
|
|
74
|
+
when String then FleetEnvelope.new(data: parse_json(payload))
|
|
75
|
+
when Hash then FleetEnvelope.new(data: payload)
|
|
76
|
+
else
|
|
77
|
+
raise ContractError, "fleet payload expected Hash or String, got #{payload.class}"
|
|
78
|
+
end
|
|
108
79
|
end
|
|
109
80
|
|
|
110
81
|
def check_envelope!(envelope, provider_family:)
|
|
111
82
|
reject_legacy_fields!(envelope)
|
|
112
|
-
REQUIRED_FIELDS.each do |field|
|
|
83
|
+
Protocol::REQUIRED_FIELDS.each do |field|
|
|
113
84
|
raise ArgumentError, "#{field} is required" unless envelope.key?(field) && !envelope[field].nil?
|
|
114
85
|
end
|
|
115
86
|
|
|
@@ -118,46 +89,21 @@ module Legion
|
|
|
118
89
|
validate_execution_contract!(envelope)
|
|
119
90
|
end
|
|
120
91
|
|
|
121
|
-
#
|
|
122
|
-
# the exact marker
|
|
92
|
+
# P2: exact execution only — the marker is required and must equal
|
|
93
|
+
# the exact marker; absence is rejected. The exact fields are
|
|
94
|
+
# additionally required by the marker.
|
|
123
95
|
def validate_execution_contract!(envelope)
|
|
124
96
|
marker = envelope.execution_contract
|
|
125
|
-
|
|
126
|
-
|
|
97
|
+
raise ContractError, "execution_contract must be #{Protocol::EXACT_EXECUTION_CONTRACT}" unless
|
|
98
|
+
marker == Protocol::EXACT_EXECUTION_CONTRACT
|
|
127
99
|
|
|
128
100
|
Protocol::EXACT_REQUIRED_FIELDS.each do |field|
|
|
129
|
-
raise
|
|
101
|
+
raise ContractError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
|
|
130
102
|
end
|
|
131
103
|
end
|
|
132
104
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Exact requests dispatch through the registry and never call
|
|
138
|
-
# build_provider; legacy v2 keeps the provider-object path.
|
|
139
|
-
def dispatch_request(envelope, provider_class, provider_instances, registry)
|
|
140
|
-
if exact?(envelope)
|
|
141
|
-
WorkerExecution.call(envelope: envelope, registry: registry)
|
|
142
|
-
else
|
|
143
|
-
provider = build_provider(envelope:, provider_class:, provider_instances:)
|
|
144
|
-
WorkerExecution.call(envelope: envelope, provider: provider)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def build_provider(envelope:, provider_class:, provider_instances:)
|
|
149
|
-
instances = resolve_provider_instances(provider_instances)
|
|
150
|
-
instance_id = envelope.provider_instance.to_s
|
|
151
|
-
instance_settings = instances[instance_id.to_sym] || instances[instance_id]
|
|
152
|
-
unless instance_settings
|
|
153
|
-
raise ConfigurationError,
|
|
154
|
-
"fleet provider instance is not configured: #{instance_id}"
|
|
155
|
-
end
|
|
156
|
-
raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}" unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
|
|
157
|
-
|
|
158
|
-
provider_class.new(deep_symbolize(instance_settings))
|
|
159
|
-
end
|
|
160
|
-
|
|
105
|
+
# E3: the response envelope carries the serialized Canonical::Response.
|
|
106
|
+
# E4/G5: thinking never crosses the fleet — excluded exactly once, here.
|
|
161
107
|
def publish_response(envelope, response)
|
|
162
108
|
transport_message_class(:FleetResponse).new(
|
|
163
109
|
protocol_version: envelope.protocol_version,
|
|
@@ -171,16 +117,13 @@ module Legion
|
|
|
171
117
|
reply_to: envelope.reply_to,
|
|
172
118
|
message_context: envelope.message_context,
|
|
173
119
|
trace_context: envelope.trace_context,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
finish_reason: response_field(response, :finish_reason),
|
|
178
|
-
metadata: response_metadata(response),
|
|
179
|
-
execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
|
|
180
|
-
offering_id: exact?(envelope) ? envelope.offering_id : nil
|
|
120
|
+
response: response.to_h.except(:thinking),
|
|
121
|
+
execution_contract: envelope.execution_contract,
|
|
122
|
+
offering_id: envelope.offering_id
|
|
181
123
|
).publish
|
|
182
124
|
end
|
|
183
125
|
|
|
126
|
+
# E6: the error envelope. retryable is derived per F6.
|
|
184
127
|
def publish_error(envelope, error)
|
|
185
128
|
transport_message_class(:FleetError).new(
|
|
186
129
|
protocol_version: envelope.protocol_version,
|
|
@@ -199,8 +142,8 @@ module Legion
|
|
|
199
142
|
error_class: error.class.name,
|
|
200
143
|
retryable: retryable_error?(error),
|
|
201
144
|
metadata: {},
|
|
202
|
-
execution_contract:
|
|
203
|
-
offering_id:
|
|
145
|
+
execution_contract: envelope.execution_contract,
|
|
146
|
+
offering_id: envelope.offering_id
|
|
204
147
|
).publish
|
|
205
148
|
end
|
|
206
149
|
|
|
@@ -239,20 +182,19 @@ module Legion
|
|
|
239
182
|
end
|
|
240
183
|
end
|
|
241
184
|
|
|
185
|
+
# L1: Legion::JSON only (house rule) — the bare ::JSON fallback is
|
|
186
|
+
# deleted; Legion::JSON is a hard dependency of this gem.
|
|
242
187
|
def parse_json(payload)
|
|
243
|
-
|
|
244
|
-
::Legion::JSON.parse(payload)
|
|
245
|
-
else
|
|
246
|
-
::JSON.parse(payload)
|
|
247
|
-
end
|
|
188
|
+
::Legion::JSON.parse(payload)
|
|
248
189
|
end
|
|
249
190
|
|
|
250
191
|
def reject_legacy_fields!(envelope)
|
|
251
|
-
LEGACY_FIELDS.each do |field|
|
|
252
|
-
raise ArgumentError, "#{field} is not supported by fleet protocol
|
|
192
|
+
Protocol::LEGACY_FIELDS.each do |field|
|
|
193
|
+
raise ArgumentError, "#{field} is not supported by fleet protocol v3" if envelope.key?(field)
|
|
253
194
|
end
|
|
254
195
|
end
|
|
255
196
|
|
|
197
|
+
# P3: explicit version — no default fill.
|
|
256
198
|
def validate_protocol_version!(envelope)
|
|
257
199
|
return if envelope.protocol_version == Protocol::VERSION
|
|
258
200
|
|
|
@@ -267,7 +209,7 @@ module Legion
|
|
|
267
209
|
|
|
268
210
|
def resolve_provider_instances(provider_instances)
|
|
269
211
|
instances = provider_instances.respond_to?(:call) ? provider_instances.call : provider_instances
|
|
270
|
-
|
|
212
|
+
Utils.deep_symbolize_keys(instances || {})
|
|
271
213
|
end
|
|
272
214
|
|
|
273
215
|
def requeue_error?(error)
|
|
@@ -275,72 +217,32 @@ module Legion
|
|
|
275
217
|
Settings.value(:fleet, :consumer, :requeue_transient, default: true) != false
|
|
276
218
|
end
|
|
277
219
|
|
|
220
|
+
# F6: retryability is derived from the one ProviderOutcome kind table
|
|
221
|
+
# (05 O6) — transient kinds retry; contract/policy/auth/classification
|
|
222
|
+
# kinds never do. The default-true catch-all is deleted.
|
|
278
223
|
def retryable_error?(error)
|
|
279
224
|
return false if error.is_a?(ConfigurationError)
|
|
280
225
|
return false if error.is_a?(WorkerExecution::PolicyError)
|
|
226
|
+
return false if error.is_a?(ContractError)
|
|
227
|
+
return false if error.is_a?(TokenError)
|
|
228
|
+
return false if error.is_a?(Inventory::Errors::ExactOfferingMismatchError)
|
|
281
229
|
|
|
282
|
-
|
|
230
|
+
::Legion::Extensions::Llm::Routing::ProviderOutcome::RETRYABLE_KINDS.include?(
|
|
231
|
+
::Legion::Extensions::Llm::Routing::ProviderOutcome.kind_for(error)
|
|
232
|
+
)
|
|
283
233
|
end
|
|
284
234
|
|
|
285
235
|
def error_code(error)
|
|
286
236
|
return 'configuration_error' if error.is_a?(ConfigurationError)
|
|
287
237
|
return 'policy_error' if error.is_a?(WorkerExecution::PolicyError)
|
|
238
|
+
return 'contract_error' if error.is_a?(ContractError)
|
|
288
239
|
|
|
289
240
|
'provider_error'
|
|
290
241
|
end
|
|
291
242
|
|
|
292
|
-
def response_content(response)
|
|
293
|
-
response_field(response, :content) || response_field(response, :result) || response.to_s
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def response_usage(response)
|
|
297
|
-
usage = response_field(response, :usage) || response_field(response, :tokens)
|
|
298
|
-
return deep_symbolize(usage) if usage.respond_to?(:to_h)
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
input_tokens: response_field(response, :input_tokens),
|
|
302
|
-
output_tokens: response_field(response, :output_tokens),
|
|
303
|
-
thinking_tokens: response_field(response, :thinking_tokens)
|
|
304
|
-
}.compact
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def response_metadata(response)
|
|
308
|
-
metadata = response_field(response, :metadata)
|
|
309
|
-
metadata.respond_to?(:to_h) ? deep_symbolize(metadata) : {}
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def response_field(response, field)
|
|
313
|
-
return response[field] if response.respond_to?(:key?) && response.key?(field)
|
|
314
|
-
return response[field.to_s] if response.respond_to?(:key?) && response.key?(field.to_s)
|
|
315
|
-
return response.public_send(field) if response.respond_to?(field)
|
|
316
|
-
|
|
317
|
-
nil
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def dig(hash, *keys)
|
|
321
|
-
keys.reduce(hash) do |current, key|
|
|
322
|
-
break nil unless current.respond_to?(:key?)
|
|
323
|
-
|
|
324
|
-
current[key.to_sym] || current[key.to_s]
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
243
|
def truthy?(value)
|
|
329
244
|
value == true || value.to_s == 'true'
|
|
330
245
|
end
|
|
331
|
-
|
|
332
|
-
def deep_symbolize(value)
|
|
333
|
-
case value
|
|
334
|
-
when Hash
|
|
335
|
-
value.each_with_object({}) do |(key, child), result|
|
|
336
|
-
result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize(child)
|
|
337
|
-
end
|
|
338
|
-
when Array
|
|
339
|
-
value.map { |child| deep_symbolize(child) }
|
|
340
|
-
else
|
|
341
|
-
value
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
246
|
end
|
|
345
247
|
end
|
|
346
248
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'concurrent'
|
|
4
4
|
require 'time'
|
|
5
5
|
|
|
6
|
+
require 'legion/extensions/llm/utils'
|
|
6
7
|
require_relative 'protocol'
|
|
7
8
|
require_relative 'settings'
|
|
8
9
|
require_relative 'token_error'
|
|
@@ -21,19 +22,22 @@ module Legion
|
|
|
21
22
|
|
|
22
23
|
module_function
|
|
23
24
|
|
|
25
|
+
# The envelope is a plain (already symbolized) Hash — the single
|
|
26
|
+
# wire-normalization entry (FleetEnvelope) happens before this
|
|
27
|
+
# boundary.
|
|
24
28
|
def validate!(token:, envelope:, record_replay: true)
|
|
25
29
|
raise TokenError, 'fleet token is required' if token.to_s.empty?
|
|
26
30
|
|
|
27
|
-
claims =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
claims = Utils.deep_symbolize_keys(jwt_module.verify(
|
|
32
|
+
token,
|
|
33
|
+
verification_key: signing_key,
|
|
34
|
+
issuer: issuer,
|
|
35
|
+
algorithm: algorithm,
|
|
36
|
+
verify_issuer: true
|
|
37
|
+
))
|
|
34
38
|
validate_registered_claims!(claims)
|
|
35
39
|
validate_request_expiry!(claims)
|
|
36
|
-
validate_envelope_claims!(claims,
|
|
40
|
+
validate_envelope_claims!(claims, Utils.deep_symbolize_keys(envelope || {}))
|
|
37
41
|
record_replay ? reserve_replay!(claims[:jti]) : ensure_not_replayed!(claims[:jti])
|
|
38
42
|
claims
|
|
39
43
|
rescue TokenError
|
|
@@ -96,12 +100,10 @@ module Legion
|
|
|
96
100
|
validate_exact_execution_claims!(claims, envelope)
|
|
97
101
|
end
|
|
98
102
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
103
|
+
# S3: every request is exact (06 P2), so both exact scalar claims are
|
|
104
|
+
# verified UNCONDITIONALLY — the marker-absence branch is deleted. An
|
|
105
|
+
# unsigned exact marker is never authoritative.
|
|
102
106
|
def validate_exact_execution_claims!(claims, envelope)
|
|
103
|
-
return unless envelope[:execution_contract] == Protocol::EXACT_EXECUTION_CONTRACT
|
|
104
|
-
|
|
105
107
|
Protocol::EXACT_SIGNED_SCALAR_CLAIMS.each do |key|
|
|
106
108
|
raise TokenError, "fleet token missing signed #{key}" if claims[key].to_s.empty?
|
|
107
109
|
raise TokenError, "fleet token #{key} claim mismatch" unless canonical_value(claims[key]) == canonical_value(envelope[key])
|
|
@@ -217,14 +219,6 @@ module Legion
|
|
|
217
219
|
raise TokenError, 'Legion::Crypt::JWT.verify unavailable'
|
|
218
220
|
end
|
|
219
221
|
|
|
220
|
-
def symbolize_keys(hash)
|
|
221
|
-
return {} unless hash.respond_to?(:each)
|
|
222
|
-
|
|
223
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
224
|
-
result[key.respond_to?(:to_sym) ? key.to_sym : key] = value
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
222
|
def canonical_value(value)
|
|
229
223
|
case value
|
|
230
224
|
when Hash
|