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
|
@@ -6,23 +6,40 @@ module Legion
|
|
|
6
6
|
# Documents the canonical public provider method signatures shared by provider gems.
|
|
7
7
|
module ProviderContract
|
|
8
8
|
REQUIRED_SIGNATURES = {
|
|
9
|
-
chat: [%i[
|
|
10
|
-
stream_chat: [%i[
|
|
9
|
+
chat: [%i[req messages], %i[keyreq model]],
|
|
10
|
+
stream_chat: [%i[req messages], %i[keyreq model]],
|
|
11
11
|
embed: [%i[keyreq text], %i[keyreq model]],
|
|
12
12
|
image: [%i[keyreq prompt], %i[keyreq model]],
|
|
13
|
-
list_models: [%i[key live], %i[keyrest filters]],
|
|
14
13
|
discover_offerings: [%i[key live], %i[key raise_on_unreachable], %i[keyrest filters]],
|
|
15
14
|
health: [%i[key live]],
|
|
16
15
|
count_tokens: [%i[keyreq messages], %i[keyreq model], %i[key params]]
|
|
17
16
|
}.freeze
|
|
18
17
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# H3: the tools half of the dispatch boundary contract. A non-empty
|
|
19
|
+
# `tools:` value must be Hash<name, Canonical::ToolDefinition>; the
|
|
20
|
+
# base funnel enforces it once (08 F2) and non-canonical tool values
|
|
21
|
+
# raise ArgumentError. Renderers consume Canonical::ToolDefinition
|
|
22
|
+
# only (schema access via Canonical::ToolSchema); Hash and legacy
|
|
23
|
+
# Lex::Llm::Tool tolerance is deleted from the contract.
|
|
21
24
|
TOOL_SUPPORT_CONTRACT = <<~DOC
|
|
22
|
-
- chat and stream_chat accept keyword `tools:` (Hash<name,
|
|
23
|
-
-
|
|
25
|
+
- chat and stream_chat accept keyword `tools:` (Hash<name, Canonical::ToolDefinition> or nil)
|
|
26
|
+
- the base funnel enforces the tool contract once (08 F2): a non-Hash or
|
|
27
|
+
non-ToolDefinition tool value raises ArgumentError before any rendering
|
|
24
28
|
- Renderers must use Canonical::ToolSchema.extract(tool) for schema access
|
|
25
|
-
|
|
29
|
+
DOC
|
|
30
|
+
|
|
31
|
+
# H5: the read-path contract, stated as implemented. The two base
|
|
32
|
+
# read paths perform different amounts of transport and say so.
|
|
33
|
+
READ_PATH_CONTRACT = <<~DOC
|
|
34
|
+
- discover_offerings: the base read path serves the SSOT registry
|
|
35
|
+
snapshot for this provider instance — NO transport occurs.
|
|
36
|
+
`live:` and `raise_on_unreachable:` are accepted for signature
|
|
37
|
+
compatibility and have no effect in the base (there is nothing
|
|
38
|
+
that can become unreachable). `filters` select from the snapshot.
|
|
39
|
+
- Provider gems that override either path with a live fetch MUST
|
|
40
|
+
implement `live:` and (for discover_offerings)
|
|
41
|
+
`raise_on_unreachable:` for real — the base guarantees are not
|
|
42
|
+
inherited semantics.
|
|
26
43
|
DOC
|
|
27
44
|
end
|
|
28
45
|
end
|
|
@@ -11,15 +11,15 @@ module Legion
|
|
|
11
11
|
**legacy_settings)
|
|
12
12
|
validate_provider_defaults!(fleet:, gateways:, legacy_settings:)
|
|
13
13
|
|
|
14
|
-
deep_merge(
|
|
14
|
+
Utils.deep_merge(
|
|
15
15
|
Legion::Extensions::Llm.default_settings,
|
|
16
16
|
{
|
|
17
17
|
enabled: enabled,
|
|
18
18
|
provider_family: family,
|
|
19
|
-
discovery: deep_merge({ enabled: true, interval_seconds: 300 }, discovery || {}),
|
|
20
|
-
instances: deep_merge(
|
|
19
|
+
discovery: Utils.deep_merge({ enabled: true, interval_seconds: 300 }, discovery || {}),
|
|
20
|
+
instances: Utils.deep_merge(
|
|
21
21
|
{
|
|
22
|
-
default: deep_merge(
|
|
22
|
+
default: Utils.deep_merge(
|
|
23
23
|
{ enabled: true, credentials: nil, fleet: { enabled: false, consumer_priority: 0, prefetch: 1 } },
|
|
24
24
|
instance || {}
|
|
25
25
|
)
|
|
@@ -44,31 +44,10 @@ module Legion
|
|
|
44
44
|
raise ArgumentError, "Unsupported top-level provider settings: #{invalid_keys.join(', ')}"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def deep_dup(value)
|
|
48
|
-
case value
|
|
49
|
-
when Hash
|
|
50
|
-
value.to_h { |key, inner_value| [key, deep_dup(inner_value)] }
|
|
51
|
-
when Array
|
|
52
|
-
value.map { |inner_value| deep_dup(inner_value) }
|
|
53
|
-
else
|
|
54
|
-
value
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def deep_merge(left, right)
|
|
59
|
-
deep_dup(left || {}).merge(deep_dup(right || {})) do |_key, left_value, right_value|
|
|
60
|
-
left_value.is_a?(Hash) && right_value.is_a?(Hash) ? deep_merge(left_value, right_value) : right_value
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
47
|
def infer_tier_from_endpoint(url)
|
|
65
48
|
return :direct if url.nil? || url.to_s.empty?
|
|
66
49
|
|
|
67
|
-
|
|
68
|
-
host = URI.parse(url.to_s).host.to_s.downcase
|
|
69
|
-
%w[localhost 127.0.0.1 ::1 [::1]].include?(host) ? :local : :direct
|
|
70
|
-
rescue URI::InvalidURIError
|
|
71
|
-
:direct
|
|
50
|
+
Legion::Extensions::Llm::Utils.localhost_url?(url) ? :local : :direct
|
|
72
51
|
end
|
|
73
52
|
end
|
|
74
53
|
end
|
|
@@ -156,6 +156,9 @@ module Legion
|
|
|
156
156
|
end
|
|
157
157
|
private_class_method :consume_open_think_segment
|
|
158
158
|
|
|
159
|
+
# The shared tag-segment matcher (10 U1): batch extraction and the
|
|
160
|
+
# streaming state machine both match tags through this one
|
|
161
|
+
# implementation.
|
|
159
162
|
def next_tag_match(text, type)
|
|
160
163
|
matches = THINK_TAG_PAIRS.filter_map do |open_tag, close_tag|
|
|
161
164
|
tag = type == :open ? open_tag : close_tag
|
|
@@ -164,7 +167,11 @@ module Legion
|
|
|
164
167
|
end
|
|
165
168
|
matches.min_by { |match| match[:index] }
|
|
166
169
|
end
|
|
167
|
-
|
|
170
|
+
|
|
171
|
+
# All tag tokens (opens + closes) for cross-chunk boundary buffering.
|
|
172
|
+
def tag_tokens
|
|
173
|
+
THINK_TAG_PAIRS.flat_map { |open_tag, close_tag| [open_tag, close_tag] }
|
|
174
|
+
end
|
|
168
175
|
|
|
169
176
|
def untagged_reasoning_preamble?(preamble)
|
|
170
177
|
return false if preamble.length > UNTAGGED_PREAMBLE_MAX_LENGTH
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Responses
|
|
7
|
+
# The ONE shared tool-arguments parser (10 U2). Streaming fragments are
|
|
8
|
+
# assembled into a complete JSON string before parsing; sync providers pass
|
|
9
|
+
# the provider wire string. One strict policy: invalid or non-object JSON
|
|
10
|
+
# raises — corrupted tool arguments never cross the boundary as a
|
|
11
|
+
# fabricated {} (04 L1/L7; the two old rescue-to-{} policies are deleted).
|
|
12
|
+
module ToolArguments
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Parse assembled tool-call arguments JSON into a Hash.
|
|
16
|
+
# nil / empty string means "no arguments" and returns {} (documented
|
|
17
|
+
# default, not tolerance).
|
|
18
|
+
def parse!(raw)
|
|
19
|
+
return {} if raw.nil?
|
|
20
|
+
return {} if raw.is_a?(::String) && raw.strip.empty?
|
|
21
|
+
raise ArgumentError, "tool call arguments expected JSON String, got #{raw.class}" unless raw.is_a?(::String)
|
|
22
|
+
|
|
23
|
+
parsed = ::Legion::JSON.parse(raw, symbolize_names: false)
|
|
24
|
+
raise ArgumentError, "tool call arguments must be a JSON object, got #{parsed.class}" unless parsed.is_a?(::Hash)
|
|
25
|
+
|
|
26
|
+
parsed
|
|
27
|
+
rescue ::Legion::JSON::ParseError => e
|
|
28
|
+
raise ArgumentError, "tool call arguments are not valid JSON: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -47,7 +47,32 @@ module Legion
|
|
|
47
47
|
raise Legion::Extensions::Llm::Inventory::Errors::ValidationError,
|
|
48
48
|
'retry_after must be nil or a finite nonnegative Numeric'
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# The ONE error→kind base table (05 O6 / 08 E1). Providers override
|
|
52
|
+
# #normalize_dispatch_error only when their wire semantics supply
|
|
53
|
+
# stronger evidence; the table itself is frozen here and is also the
|
|
54
|
+
# source of fleet retryability (06 F6).
|
|
55
|
+
def self.kind_for(error)
|
|
56
|
+
case error
|
|
57
|
+
when OverloadedError then :overloaded
|
|
58
|
+
when RateLimitError then :rate_limited
|
|
59
|
+
when UnauthorizedError then :authentication
|
|
60
|
+
when PaymentRequiredError then :billing
|
|
61
|
+
when ForbiddenError then :authorization
|
|
62
|
+
when ContextLengthExceededError then :context_rejected
|
|
63
|
+
when BadRequestError then :invalid_request
|
|
64
|
+
when ModelNotFoundError then :model_missing
|
|
65
|
+
when ModelNotAllowedError then :policy
|
|
66
|
+
when Faraday::TimeoutError, Timeout::Error then :timeout
|
|
67
|
+
when Faraday::ConnectionFailed, Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError then :connection_failure
|
|
68
|
+
else :provider_error
|
|
69
|
+
end
|
|
70
|
+
end
|
|
50
71
|
end
|
|
72
|
+
|
|
73
|
+
# 06 F6: the transient kinds that may be retried at the fleet edge.
|
|
74
|
+
# Classification/contract/auth/policy kinds never retry.
|
|
75
|
+
ProviderOutcome::RETRYABLE_KINDS = %i[overloaded rate_limited timeout connection_failure provider_error].freeze
|
|
51
76
|
end
|
|
52
77
|
end
|
|
53
78
|
end
|
|
@@ -83,9 +83,13 @@ module Legion
|
|
|
83
83
|
|
|
84
84
|
# An immutable selected target produced by legion-llm's selector and
|
|
85
85
|
# consumed by dispatch. lex-llm validates cross-field identity only; it
|
|
86
|
-
# does not compute routing or choose a lane.
|
|
86
|
+
# does not compute routing or choose a lane. lane_id is the 5 tuple
|
|
87
|
+
# tier:provider_family:instance_id:type:model; the tier part is not a
|
|
88
|
+
# Selection member, so cross-field validation covers the four parts the
|
|
89
|
+
# record does carry (family, instance, type-from-operation, model).
|
|
90
|
+
# See section 15.4.
|
|
87
91
|
Selection = ::Data.define(
|
|
88
|
-
:inventory_generation, :lane_id, :instance_key, :
|
|
92
|
+
:inventory_generation, :lane_id, :instance_key, :provider_family, :instance_id,
|
|
89
93
|
:model, :operation, :callable_handle, :publisher_token_id, :capability_evidence, :context_evidence,
|
|
90
94
|
:weight_inputs, :base_weight, :preference_ppm, :effective_weight, :rendezvous_score
|
|
91
95
|
) do
|
|
@@ -103,29 +107,44 @@ module Legion
|
|
|
103
107
|
raise errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(Legion::Extensions::Llm::Inventory::CallableHandle)
|
|
104
108
|
|
|
105
109
|
canonical_model = identity.normalize_text(value: kwargs[:model], field: :model)
|
|
106
|
-
canonical_operation = Legion::Extensions::Llm::Taxonomies.normalize_operation(value: kwargs[:operation]
|
|
110
|
+
canonical_operation = Legion::Extensions::Llm::Taxonomies.normalize_operation(value: kwargs[:operation])
|
|
107
111
|
family = Legion::Extensions::Llm::Inventory::RecordSupport.normalize_provider_family(value: kwargs[:provider_family])
|
|
108
112
|
normalized_instance = identity.normalize_text(value: kwargs[:instance_id], field: :instance_id)
|
|
109
113
|
unless family == instance_key.provider_family && normalized_instance == instance_key.instance_id
|
|
110
114
|
raise errors::ValidationError, 'provider_family and instance_id must equal instance_key'
|
|
111
115
|
end
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
identity.validate_lane_id!(
|
|
116
|
-
value: kwargs[:lane_id], instance_key: instance_key, operation: canonical_operation,
|
|
117
|
-
model: canonical_model, offering_id: offering_id
|
|
118
|
-
)
|
|
117
|
+
identity.validate_lane_id!(value: kwargs[:lane_id])
|
|
118
|
+
validate_lane_id_fields!(kwargs[:lane_id], family, normalized_instance, canonical_operation, canonical_model)
|
|
119
119
|
validate_publisher_token_id!(kwargs[:publisher_token_id])
|
|
120
120
|
|
|
121
121
|
{
|
|
122
|
-
lane_id: kwargs[:lane_id].dup.freeze, instance_key: instance_key,
|
|
122
|
+
lane_id: kwargs[:lane_id].to_s.dup.freeze, instance_key: instance_key,
|
|
123
123
|
provider_family: family, instance_id: normalized_instance, model: canonical_model,
|
|
124
124
|
operation: canonical_operation, callable_handle: callable_handle,
|
|
125
125
|
publisher_token_id: kwargs[:publisher_token_id].dup.freeze
|
|
126
126
|
}
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Cross-field check of the 5 tuple against the parts the Selection
|
|
130
|
+
# carries: provider_family (part 2), instance_id (part 3), type from
|
|
131
|
+
# the operation (part 4), and model (part 5, colons preserved by the
|
|
132
|
+
# bounded parse). Part 1 (tier) is checked against Taxonomies::TIERS
|
|
133
|
+
# by validate_lane_id!.
|
|
134
|
+
def validate_lane_id_fields!(lane_id, family, instance_id, operation, model)
|
|
135
|
+
errors = Legion::Extensions::Llm::Inventory::Errors
|
|
136
|
+
identity = Legion::Extensions::Llm::Inventory::Identity
|
|
137
|
+
parts = identity.parse_lane_id(lane_id)
|
|
138
|
+
raise errors::ValidationError, 'lane_id provider_family does not match the selection' \
|
|
139
|
+
unless parts[1] == family.to_s
|
|
140
|
+
raise errors::ValidationError, 'lane_id instance_id does not match the selection' \
|
|
141
|
+
unless parts[2] == instance_id
|
|
142
|
+
raise errors::ValidationError, 'lane_id type does not match the selection operation' \
|
|
143
|
+
unless parts[3] == Legion::Extensions::Llm::Taxonomies.lane_type_for(operation: operation).to_s
|
|
144
|
+
raise errors::ValidationError, 'lane_id model does not match the selection' \
|
|
145
|
+
unless parts[4] == model
|
|
146
|
+
end
|
|
147
|
+
|
|
129
148
|
def scoring_attributes(kwargs)
|
|
130
149
|
support = Legion::Extensions::Llm::Inventory::RecordSupport
|
|
131
150
|
base_weight = kwargs[:base_weight]
|
|
@@ -145,12 +164,6 @@ module Legion
|
|
|
145
164
|
AttemptTargetKey.new(provider_family: provider_family, instance_id: instance_id, model: model)
|
|
146
165
|
end
|
|
147
166
|
|
|
148
|
-
def validate_offering_id_shape!(offering_id)
|
|
149
|
-
return if offering_id.is_a?(::String) && offering_id.match?(/\Aoff:v1:[0-9a-f]{64}\z/)
|
|
150
|
-
|
|
151
|
-
raise Legion::Extensions::Llm::Inventory::Errors::ValidationError, 'offering_id must have the off:v1: shape'
|
|
152
|
-
end
|
|
153
|
-
|
|
154
167
|
def validate_publisher_token_id!(publisher_token_id)
|
|
155
168
|
return if publisher_token_id.is_a?(::String) && publisher_token_id.start_with?('ptok:v1:') &&
|
|
156
169
|
publisher_token_id.length > 'ptok:v1:'.length
|
|
@@ -158,12 +171,16 @@ module Legion
|
|
|
158
171
|
raise Legion::Extensions::Llm::Inventory::Errors::ValidationError, 'publisher_token_id must be a ptok:v1: String'
|
|
159
172
|
end
|
|
160
173
|
|
|
174
|
+
# U4: structural verification only — same semantics as
|
|
175
|
+
# RecordSupport.validated_weight_pair, never a second weight policy.
|
|
176
|
+
# 0 is a legal value (operator disable, 07 W3); base_weight must
|
|
177
|
+
# equal the product of the inputs.
|
|
161
178
|
def validate_weight_inputs!(weight_inputs, base_weight)
|
|
162
179
|
errors = Legion::Extensions::Llm::Inventory::Errors
|
|
163
180
|
unless weight_inputs.is_a?(::Hash) && weight_inputs.keys.sort == %i[instance model_or_offering provider tier]
|
|
164
181
|
raise errors::ValidationError, 'weight_inputs must have keys tier/provider/instance/model_or_offering'
|
|
165
182
|
end
|
|
166
|
-
raise errors::ValidationError, 'weight_inputs values must be
|
|
183
|
+
raise errors::ValidationError, 'weight_inputs values must be nonnegative Integers' unless weight_inputs.values.all? { |value| value.is_a?(::Integer) && !value.negative? }
|
|
167
184
|
|
|
168
185
|
product = weight_inputs.values.reduce(1, :*)
|
|
169
186
|
raise errors::ValidationError, 'base_weight must equal the product of weight_inputs' unless base_weight == product
|