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
|
@@ -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,57 +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
|
-
data[key.to_sym] || data[key.to_s]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def key?(key)
|
|
43
|
-
data.key?(key.to_sym) || data.key?(key.to_s)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def fetch(key, default = nil)
|
|
47
|
-
key?(key) ? self[key] : default
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def to_h = data
|
|
51
|
-
def protocol_version = self[:protocol_version]
|
|
52
|
-
def request_id = self[:request_id]
|
|
53
|
-
def correlation_id = self[:correlation_id]
|
|
54
|
-
def idempotency_key = self[:idempotency_key]
|
|
55
|
-
def operation = self[:operation]
|
|
56
|
-
def provider = self[:provider]
|
|
57
|
-
def provider_instance = self[:provider_instance]
|
|
58
|
-
def model = self[:model]
|
|
59
|
-
def params = self[:params] || {}
|
|
60
|
-
def reply_to = self[:reply_to]
|
|
61
|
-
def message_context = self[:message_context] || {}
|
|
62
|
-
def trace_context = self[:trace_context] || {}
|
|
63
|
-
def execution_contract = self[:execution_contract]
|
|
64
|
-
def offering_id = self[:offering_id]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
39
|
module_function
|
|
68
40
|
|
|
69
41
|
# Public runner entry point mirrors AMQP delivery callbacks, which carry both delivery and property metadata.
|
|
70
|
-
#
|
|
71
|
-
|
|
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:,
|
|
72
46
|
registry: ::Legion::Extensions::Llm::Inventory::Registry, delivery: nil, properties: nil)
|
|
73
47
|
envelope = parse_payload(payload)
|
|
74
48
|
check_envelope!(envelope, provider_family:)
|
|
75
|
-
response =
|
|
49
|
+
response = WorkerExecution.call(envelope: envelope, registry:)
|
|
76
50
|
publish_response(envelope, response)
|
|
77
51
|
ack(delivery || properties)
|
|
78
52
|
response
|
|
@@ -83,30 +57,30 @@ module Legion
|
|
|
83
57
|
reject(delivery || properties, requeue: requeue_error?(e))
|
|
84
58
|
raise
|
|
85
59
|
end
|
|
86
|
-
# rubocop:enable Metrics/ParameterLists
|
|
87
60
|
|
|
88
61
|
def enabled_for?(provider_instances)
|
|
89
62
|
instances = resolve_provider_instances(provider_instances)
|
|
90
63
|
instances.any? do |_instance_id, settings|
|
|
91
|
-
truthy?(dig(
|
|
64
|
+
truthy?(Utils.deep_symbolize_keys(settings).dig(:fleet, :respond_to_requests))
|
|
92
65
|
end
|
|
93
66
|
end
|
|
94
67
|
|
|
68
|
+
# E1: the single wire-normalization entry. Wrong-shape payloads
|
|
69
|
+
# (neither Hash, String, nor envelope) raise — the silent {} fallback
|
|
70
|
+
# is deleted.
|
|
95
71
|
def parse_payload(payload)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end
|
|
104
|
-
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
|
|
105
79
|
end
|
|
106
80
|
|
|
107
81
|
def check_envelope!(envelope, provider_family:)
|
|
108
82
|
reject_legacy_fields!(envelope)
|
|
109
|
-
REQUIRED_FIELDS.each do |field|
|
|
83
|
+
Protocol::REQUIRED_FIELDS.each do |field|
|
|
110
84
|
raise ArgumentError, "#{field} is required" unless envelope.key?(field) && !envelope[field].nil?
|
|
111
85
|
end
|
|
112
86
|
|
|
@@ -115,46 +89,21 @@ module Legion
|
|
|
115
89
|
validate_execution_contract!(envelope)
|
|
116
90
|
end
|
|
117
91
|
|
|
118
|
-
#
|
|
119
|
-
# 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.
|
|
120
95
|
def validate_execution_contract!(envelope)
|
|
121
96
|
marker = envelope.execution_contract
|
|
122
|
-
|
|
123
|
-
|
|
97
|
+
raise ContractError, "execution_contract must be #{Protocol::EXACT_EXECUTION_CONTRACT}" unless
|
|
98
|
+
marker == Protocol::EXACT_EXECUTION_CONTRACT
|
|
124
99
|
|
|
125
100
|
Protocol::EXACT_REQUIRED_FIELDS.each do |field|
|
|
126
|
-
raise
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def exact?(envelope)
|
|
131
|
-
envelope.execution_contract == Protocol::EXACT_EXECUTION_CONTRACT
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Exact requests dispatch through the registry and never call
|
|
135
|
-
# build_provider; legacy v2 keeps the provider-object path.
|
|
136
|
-
def dispatch_request(envelope, provider_class, provider_instances, registry)
|
|
137
|
-
if exact?(envelope)
|
|
138
|
-
WorkerExecution.call(envelope: envelope, registry: registry)
|
|
139
|
-
else
|
|
140
|
-
provider = build_provider(envelope:, provider_class:, provider_instances:)
|
|
141
|
-
WorkerExecution.call(envelope: envelope, provider: provider)
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def build_provider(envelope:, provider_class:, provider_instances:)
|
|
146
|
-
instances = resolve_provider_instances(provider_instances)
|
|
147
|
-
instance_id = envelope.provider_instance.to_s
|
|
148
|
-
instance_settings = instances[instance_id.to_sym] || instances[instance_id]
|
|
149
|
-
unless instance_settings
|
|
150
|
-
raise ConfigurationError,
|
|
151
|
-
"fleet provider instance is not configured: #{instance_id}"
|
|
101
|
+
raise ContractError, "#{field} is required for #{Protocol::EXACT_EXECUTION_CONTRACT}" unless envelope.key?(field) && !envelope[field].nil?
|
|
152
102
|
end
|
|
153
|
-
raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}" unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
|
|
154
|
-
|
|
155
|
-
provider_class.new(deep_symbolize(instance_settings))
|
|
156
103
|
end
|
|
157
104
|
|
|
105
|
+
# E3: the response envelope carries the serialized Canonical::Response.
|
|
106
|
+
# E4/G5: thinking never crosses the fleet — excluded exactly once, here.
|
|
158
107
|
def publish_response(envelope, response)
|
|
159
108
|
transport_message_class(:FleetResponse).new(
|
|
160
109
|
protocol_version: envelope.protocol_version,
|
|
@@ -168,16 +117,13 @@ module Legion
|
|
|
168
117
|
reply_to: envelope.reply_to,
|
|
169
118
|
message_context: envelope.message_context,
|
|
170
119
|
trace_context: envelope.trace_context,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
finish_reason: response_field(response, :finish_reason),
|
|
175
|
-
metadata: response_metadata(response),
|
|
176
|
-
execution_contract: exact?(envelope) ? envelope.execution_contract : nil,
|
|
177
|
-
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
|
|
178
123
|
).publish
|
|
179
124
|
end
|
|
180
125
|
|
|
126
|
+
# E6: the error envelope. retryable is derived per F6.
|
|
181
127
|
def publish_error(envelope, error)
|
|
182
128
|
transport_message_class(:FleetError).new(
|
|
183
129
|
protocol_version: envelope.protocol_version,
|
|
@@ -196,8 +142,8 @@ module Legion
|
|
|
196
142
|
error_class: error.class.name,
|
|
197
143
|
retryable: retryable_error?(error),
|
|
198
144
|
metadata: {},
|
|
199
|
-
execution_contract:
|
|
200
|
-
offering_id:
|
|
145
|
+
execution_contract: envelope.execution_contract,
|
|
146
|
+
offering_id: envelope.offering_id
|
|
201
147
|
).publish
|
|
202
148
|
end
|
|
203
149
|
|
|
@@ -236,20 +182,19 @@ module Legion
|
|
|
236
182
|
end
|
|
237
183
|
end
|
|
238
184
|
|
|
185
|
+
# L1: Legion::JSON only (house rule) — the bare ::JSON fallback is
|
|
186
|
+
# deleted; Legion::JSON is a hard dependency of this gem.
|
|
239
187
|
def parse_json(payload)
|
|
240
|
-
|
|
241
|
-
::Legion::JSON.parse(payload)
|
|
242
|
-
else
|
|
243
|
-
::JSON.parse(payload)
|
|
244
|
-
end
|
|
188
|
+
::Legion::JSON.parse(payload)
|
|
245
189
|
end
|
|
246
190
|
|
|
247
191
|
def reject_legacy_fields!(envelope)
|
|
248
|
-
LEGACY_FIELDS.each do |field|
|
|
249
|
-
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)
|
|
250
194
|
end
|
|
251
195
|
end
|
|
252
196
|
|
|
197
|
+
# P3: explicit version — no default fill.
|
|
253
198
|
def validate_protocol_version!(envelope)
|
|
254
199
|
return if envelope.protocol_version == Protocol::VERSION
|
|
255
200
|
|
|
@@ -264,7 +209,7 @@ module Legion
|
|
|
264
209
|
|
|
265
210
|
def resolve_provider_instances(provider_instances)
|
|
266
211
|
instances = provider_instances.respond_to?(:call) ? provider_instances.call : provider_instances
|
|
267
|
-
|
|
212
|
+
Utils.deep_symbolize_keys(instances || {})
|
|
268
213
|
end
|
|
269
214
|
|
|
270
215
|
def requeue_error?(error)
|
|
@@ -272,72 +217,32 @@ module Legion
|
|
|
272
217
|
Settings.value(:fleet, :consumer, :requeue_transient, default: true) != false
|
|
273
218
|
end
|
|
274
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.
|
|
275
223
|
def retryable_error?(error)
|
|
276
224
|
return false if error.is_a?(ConfigurationError)
|
|
277
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)
|
|
278
229
|
|
|
279
|
-
|
|
230
|
+
::Legion::Extensions::Llm::Routing::ProviderOutcome::RETRYABLE_KINDS.include?(
|
|
231
|
+
::Legion::Extensions::Llm::Routing::ProviderOutcome.kind_for(error)
|
|
232
|
+
)
|
|
280
233
|
end
|
|
281
234
|
|
|
282
235
|
def error_code(error)
|
|
283
236
|
return 'configuration_error' if error.is_a?(ConfigurationError)
|
|
284
237
|
return 'policy_error' if error.is_a?(WorkerExecution::PolicyError)
|
|
238
|
+
return 'contract_error' if error.is_a?(ContractError)
|
|
285
239
|
|
|
286
240
|
'provider_error'
|
|
287
241
|
end
|
|
288
242
|
|
|
289
|
-
def response_content(response)
|
|
290
|
-
response_field(response, :content) || response_field(response, :result) || response.to_s
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def response_usage(response)
|
|
294
|
-
usage = response_field(response, :usage) || response_field(response, :tokens)
|
|
295
|
-
return deep_symbolize(usage) if usage.respond_to?(:to_h)
|
|
296
|
-
|
|
297
|
-
{
|
|
298
|
-
input_tokens: response_field(response, :input_tokens),
|
|
299
|
-
output_tokens: response_field(response, :output_tokens),
|
|
300
|
-
thinking_tokens: response_field(response, :thinking_tokens)
|
|
301
|
-
}.compact
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def response_metadata(response)
|
|
305
|
-
metadata = response_field(response, :metadata)
|
|
306
|
-
metadata.respond_to?(:to_h) ? deep_symbolize(metadata) : {}
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def response_field(response, field)
|
|
310
|
-
return response[field] if response.respond_to?(:key?) && response.key?(field)
|
|
311
|
-
return response[field.to_s] if response.respond_to?(:key?) && response.key?(field.to_s)
|
|
312
|
-
return response.public_send(field) if response.respond_to?(field)
|
|
313
|
-
|
|
314
|
-
nil
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def dig(hash, *keys)
|
|
318
|
-
keys.reduce(hash) do |current, key|
|
|
319
|
-
break nil unless current.respond_to?(:key?)
|
|
320
|
-
|
|
321
|
-
current[key.to_sym] || current[key.to_s]
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
243
|
def truthy?(value)
|
|
326
244
|
value == true || value.to_s == 'true'
|
|
327
245
|
end
|
|
328
|
-
|
|
329
|
-
def deep_symbolize(value)
|
|
330
|
-
case value
|
|
331
|
-
when Hash
|
|
332
|
-
value.each_with_object({}) do |(key, child), result|
|
|
333
|
-
result[key.respond_to?(:to_sym) ? key.to_sym : key] = deep_symbolize(child)
|
|
334
|
-
end
|
|
335
|
-
when Array
|
|
336
|
-
value.map { |child| deep_symbolize(child) }
|
|
337
|
-
else
|
|
338
|
-
value
|
|
339
|
-
end
|
|
340
|
-
end
|
|
341
246
|
end
|
|
342
247
|
end
|
|
343
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
|