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
|
@@ -2,39 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
|
|
5
|
-
# rubocop:disable Metrics/ParameterLists -- factory methods have many params
|
|
5
|
+
# rubocop:disable-next Metrics/ParameterLists -- factory methods have many params
|
|
6
6
|
module Legion
|
|
7
7
|
module Extensions
|
|
8
8
|
module Llm
|
|
9
|
+
# -- required for Data.define block scope
|
|
9
10
|
module Canonical
|
|
10
|
-
# rubocop:disable Lint/ConstantDefinitionInBlock -- required for Data.define block scope
|
|
11
11
|
# Canonical tool call with source enum and compliance fields.
|
|
12
12
|
# Ports field vocabulary from Legion::LLM::Types::ToolCall.
|
|
13
13
|
# Source enum per R7: :client | :registry | :special | :extension | :mcp
|
|
14
14
|
# Compliance fields per R8: data_handling_classification, policy_decision
|
|
15
|
+
# arguments is a Hash only (O03a): JSON-string arguments are a provider
|
|
16
|
+
# wire spelling parsed at the provider translator edge (10 U2).
|
|
15
17
|
ToolCall = ::Data.define(
|
|
16
18
|
:id, :exchange_id, :name, :arguments, :source,
|
|
17
19
|
:status, :duration_ms, :result, :error,
|
|
18
20
|
:started_at, :finished_at, :category,
|
|
19
|
-
:data_handling_classification, :policy_decision
|
|
21
|
+
:data_handling_classification, :policy_decision, :metadata
|
|
20
22
|
) do
|
|
21
|
-
SOURCE_VALUES = %i[client registry special extension mcp].freeze
|
|
22
|
-
STATUS_VALUES = %i[pending running success error].freeze
|
|
23
|
-
|
|
24
23
|
# Build from keyword args (primary constructor).
|
|
24
|
+
# arguments: nil means "no arguments" and normalizes to {} (documented
|
|
25
|
+
# default, not tolerance).
|
|
25
26
|
def self.build(
|
|
26
27
|
name:, id: nil, exchange_id: nil, arguments: nil, source: nil,
|
|
27
28
|
status: nil, duration_ms: nil, result: nil, error: nil,
|
|
28
29
|
started_at: nil, finished_at: nil, category: nil,
|
|
29
|
-
data_handling_classification: nil, policy_decision: nil
|
|
30
|
+
data_handling_classification: nil, policy_decision: nil, metadata: {}
|
|
30
31
|
)
|
|
31
32
|
new(
|
|
32
33
|
id: id || "call_#{SecureRandom.hex(12)}",
|
|
33
34
|
exchange_id: exchange_id,
|
|
34
|
-
name: name,
|
|
35
|
-
arguments: arguments
|
|
36
|
-
source: source,
|
|
37
|
-
status: status,
|
|
35
|
+
name: Strict.expect_type!(name, [::String], self::BUILD_SITE, :name),
|
|
36
|
+
arguments: arguments.nil? ? {} : Strict.expect_type!(arguments, [::Hash], self::BUILD_SITE, :arguments),
|
|
37
|
+
source: normalize_enum!(source, self::SOURCE_VALUES, self::BUILD_SITE, :source),
|
|
38
|
+
status: normalize_enum!(status, self::STATUS_VALUES, self::BUILD_SITE, :status),
|
|
38
39
|
duration_ms: duration_ms,
|
|
39
40
|
result: result,
|
|
40
41
|
error: error,
|
|
@@ -42,38 +43,30 @@ module Legion
|
|
|
42
43
|
finished_at: finished_at,
|
|
43
44
|
category: category,
|
|
44
45
|
data_handling_classification: data_handling_classification,
|
|
45
|
-
policy_decision: policy_decision
|
|
46
|
+
policy_decision: policy_decision,
|
|
47
|
+
metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
46
48
|
)
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
50
|
-
def self.from_hash(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
source_raw = h[:source]
|
|
57
|
-
h[:source] = source_raw&.to_sym if source_raw.is_a?(String)
|
|
58
|
-
|
|
59
|
-
# Normalize status to symbol
|
|
60
|
-
status_raw = h[:status]
|
|
61
|
-
h[:status] = status_raw&.to_sym if status_raw.is_a?(String)
|
|
52
|
+
def self.from_hash(source)
|
|
53
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
54
|
+
hash = Strict.symbolize_keys(source)
|
|
55
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
56
|
+
build(**hash, metadata:)
|
|
57
|
+
end
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
begin
|
|
67
|
-
h[:arguments] = Legion::JSON.load(args)
|
|
68
|
-
rescue Legion::JSON::ParseError => e
|
|
69
|
-
Legion::Logging.debug("[lex-llm][canonical][tool_call] arguments not parseable as JSON, leaving as string: #{e.message}")
|
|
70
|
-
end
|
|
71
|
-
end
|
|
59
|
+
# L6: declared enums validated at construction, in both factories.
|
|
60
|
+
def self.normalize_enum!(value, allowed, site, member)
|
|
61
|
+
return nil if value.nil?
|
|
72
62
|
|
|
73
|
-
|
|
63
|
+
value_sym = value.is_a?(::String) ? value.to_sym : value
|
|
64
|
+
Strict.enum!(value_sym, allowed, site, member)
|
|
74
65
|
end
|
|
75
66
|
|
|
76
|
-
# Return a new ToolCall with execution result attached.
|
|
67
|
+
# Return a new ToolCall with execution result attached. The strict
|
|
68
|
+
# constructor re-validates every carried member (status enum
|
|
69
|
+
# included) — the escape-hatch .new of the pre-H1 world is gone.
|
|
77
70
|
def with_result(result:, status:, duration_ms: nil, finished_at: nil)
|
|
78
71
|
self.class.new(
|
|
79
72
|
id: id,
|
|
@@ -89,7 +82,8 @@ module Legion
|
|
|
89
82
|
finished_at: finished_at || ::Time.now,
|
|
90
83
|
category: category,
|
|
91
84
|
data_handling_classification: data_handling_classification,
|
|
92
|
-
policy_decision: policy_decision
|
|
85
|
+
policy_decision: policy_decision,
|
|
86
|
+
metadata: metadata
|
|
93
87
|
)
|
|
94
88
|
end
|
|
95
89
|
|
|
@@ -116,29 +110,25 @@ module Legion
|
|
|
116
110
|
to_h.to_json(*)
|
|
117
111
|
end
|
|
118
112
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
source: source,
|
|
130
|
-
category: category,
|
|
131
|
-
data_handling_classification: data_handling_classification,
|
|
132
|
-
policy_decision: policy_decision
|
|
133
|
-
}.compact
|
|
113
|
+
# H1: the single strict constructor — .new runs the same member
|
|
114
|
+
# contract as the factories; the factories fill their defaults and
|
|
115
|
+
# delegate here.
|
|
116
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
117
|
+
values[:name] = Strict.expect_type!(values[:name], [::String], site, :name)
|
|
118
|
+
values[:arguments] = values[:arguments].nil? ? {} : Strict.expect_type!(values[:arguments], [::Hash], site, :arguments)
|
|
119
|
+
values[:source] = normalize_enum!(values[:source], self::SOURCE_VALUES, site, :source)
|
|
120
|
+
values[:status] = normalize_enum!(values[:status], self::STATUS_VALUES, site, :status)
|
|
121
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
122
|
+
values
|
|
134
123
|
end
|
|
135
124
|
end
|
|
136
125
|
|
|
137
126
|
ToolCall::SOURCE_VALUES = %i[client registry special extension mcp].freeze
|
|
138
127
|
ToolCall::STATUS_VALUES = %i[pending running success error].freeze
|
|
139
|
-
|
|
128
|
+
ToolCall::BUILD_SITE = 'Canonical::ToolCall.build'
|
|
129
|
+
ToolCall::FROM_HASH_SITE = 'Canonical::ToolCall.from_hash'
|
|
130
|
+
ToolCall::NEW_SITE = 'Canonical::ToolCall.new'
|
|
140
131
|
end
|
|
141
132
|
end
|
|
142
133
|
end
|
|
143
134
|
end
|
|
144
|
-
# rubocop:enable Metrics/ParameterLists
|
|
@@ -4,21 +4,20 @@ module Legion
|
|
|
4
4
|
module Extensions
|
|
5
5
|
module Llm
|
|
6
6
|
module Canonical
|
|
7
|
-
TOOL_NAME_MAX_LENGTH = 64
|
|
8
7
|
OBJECT_SCHEMA_KEYWORDS = %i[properties required additionalProperties].freeze
|
|
9
8
|
COMPOSITE_SCHEMA_KEYWORDS = %i[oneOf anyOf allOf enum $ref $defs definitions].freeze
|
|
10
9
|
|
|
11
10
|
# Canonical tool definition.
|
|
12
|
-
# Ports field vocabulary from Legion::LLM::Types::ToolDefinition.
|
|
13
|
-
ToolDefinition = ::Data.define(:name, :description, :parameters, :source) do
|
|
11
|
+
# Ports field vocabulary from Legion::LLM::Types::ToolDefinition. # -- required for Data.define block scope
|
|
12
|
+
ToolDefinition = ::Data.define(:name, :description, :parameters, :source, :metadata) do
|
|
14
13
|
def self.normalize_parameters(parameters)
|
|
15
14
|
empty = { type: 'object', properties: {} }
|
|
16
15
|
return empty if parameters.nil?
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return empty if schema.
|
|
17
|
+
Strict.expect_type!(parameters, [::Hash], self::NORMALIZE_PARAMETERS_SITE, :parameters) unless parameters.is_a?(::Hash)
|
|
18
|
+
|
|
19
|
+
schema = parameters.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
|
20
|
+
return empty if schema.empty?
|
|
22
21
|
return schema if schema.key?(:type)
|
|
23
22
|
return schema.merge(type: 'object') if OBJECT_SCHEMA_KEYWORDS.any? { |k| schema.key?(k) }
|
|
24
23
|
return schema if COMPOSITE_SCHEMA_KEYWORDS.any? { |k| schema.key?(k) }
|
|
@@ -26,53 +25,50 @@ module Legion
|
|
|
26
25
|
{ type: 'object', properties: schema }
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
# Build from keyword args (primary constructor).
|
|
30
|
-
|
|
28
|
+
# Build from keyword args (primary constructor). M5: the name is the
|
|
29
|
+
# authoritative client/registry fact — never rewritten, stripped,
|
|
30
|
+
# truncated, or fabricated here; per-dialect name constraints belong
|
|
31
|
+
# to the provider translator edge. source is explicit or absent —
|
|
32
|
+
# never fabricated ({ type: :builtin } is deleted).
|
|
33
|
+
def self.build(name:, description: '', parameters: nil, source: nil, metadata: {})
|
|
31
34
|
new(
|
|
32
|
-
|
|
33
|
-
description
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
34
37
|
normalize_parameters(parameters),
|
|
35
|
-
source
|
|
38
|
+
source,
|
|
39
|
+
Strict.metadata!(metadata, self::BUILD_SITE)
|
|
36
40
|
)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
# Canonical keys only (O03a): edges pass `parameters`, not `input_schema`.
|
|
45
|
+
def self.from_hash(source)
|
|
46
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
47
|
+
hash = Strict.symbolize_keys(source)
|
|
48
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
44
49
|
build(
|
|
45
|
-
name:
|
|
46
|
-
description:
|
|
47
|
-
parameters:
|
|
48
|
-
source:
|
|
50
|
+
name: hash[:name],
|
|
51
|
+
description: hash[:description],
|
|
52
|
+
parameters: hash[:parameters],
|
|
53
|
+
source: hash[:source],
|
|
54
|
+
metadata:
|
|
49
55
|
)
|
|
50
56
|
end
|
|
51
57
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
tool_class: entry[:tool_class],
|
|
57
|
-
extension: entry[:extension],
|
|
58
|
-
runner: entry[:runner],
|
|
59
|
-
function: entry[:function]
|
|
60
|
-
}.compact
|
|
58
|
+
# M5: a tool name is authoritative — missing or empty is a contract
|
|
59
|
+
# error, never a fabricated label.
|
|
60
|
+
def self.require_name!(name, site)
|
|
61
|
+
raise ArgumentError, "#{site}: name must be a non-empty String, got #{name.class}: #{name.inspect}" unless name.is_a?(::String) && !name.empty?
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
name: entry[:name],
|
|
64
|
-
description: entry[:description],
|
|
65
|
-
parameters: entry[:input_schema] || entry[:parameters],
|
|
66
|
-
source: source.compact
|
|
67
|
-
)
|
|
63
|
+
name
|
|
68
64
|
end
|
|
69
65
|
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
# M5: description is a String; absence is the empty string (the
|
|
67
|
+
# documented no-description value), wrong class raises.
|
|
68
|
+
def self.description_value!(description, site)
|
|
69
|
+
return '' if description.nil?
|
|
70
|
+
|
|
71
|
+
Strict.expect_type!(description, [::String], site, :description)
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
def params_schema
|
|
@@ -85,11 +81,7 @@ module Legion
|
|
|
85
81
|
|
|
86
82
|
# Serialize to a Hash for AMQP/fleet/wire transport.
|
|
87
83
|
def to_h
|
|
88
|
-
|
|
89
|
-
name: name,
|
|
90
|
-
description: description,
|
|
91
|
-
parameters: parameters
|
|
92
|
-
}.compact.reject { |k, v| k == :description && v == '' }
|
|
84
|
+
super.compact
|
|
93
85
|
end
|
|
94
86
|
|
|
95
87
|
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
@@ -100,7 +92,25 @@ module Legion
|
|
|
100
92
|
def to_json(*)
|
|
101
93
|
to_h.to_json(*)
|
|
102
94
|
end
|
|
95
|
+
|
|
96
|
+
# H1/M5: the single strict constructor — .new runs the same member
|
|
97
|
+
# contract as the factories (authoritative name, String
|
|
98
|
+
# description, normalized parameters, explicit-or-absent source);
|
|
99
|
+
# the factories fill their defaults and delegate here.
|
|
100
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
101
|
+
values[:name] = require_name!(values[:name], site)
|
|
102
|
+
values[:description] = description_value!(values[:description], site)
|
|
103
|
+
values[:parameters] = normalize_parameters(values[:parameters])
|
|
104
|
+
values[:source] = Strict.expect_type!(values[:source], [::Hash], site, :source)
|
|
105
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
106
|
+
values
|
|
107
|
+
end
|
|
103
108
|
end
|
|
109
|
+
|
|
110
|
+
ToolDefinition::BUILD_SITE = 'Canonical::ToolDefinition.build'
|
|
111
|
+
ToolDefinition::FROM_HASH_SITE = 'Canonical::ToolDefinition.from_hash'
|
|
112
|
+
ToolDefinition::NEW_SITE = 'Canonical::ToolDefinition.new'
|
|
113
|
+
ToolDefinition::NORMALIZE_PARAMETERS_SITE = 'Canonical::ToolDefinition.normalize_parameters'
|
|
104
114
|
end
|
|
105
115
|
end
|
|
106
116
|
end
|
|
@@ -4,40 +4,33 @@ module Legion
|
|
|
4
4
|
module Extensions
|
|
5
5
|
module Llm
|
|
6
6
|
module Canonical
|
|
7
|
-
# Extracts and normalizes tool schemas from
|
|
7
|
+
# Extracts and normalizes tool schemas from ToolDefinition ONLY (04 §6).
|
|
8
|
+
# A schema extractor that tolerates raw hashes is a hidden path over the
|
|
9
|
+
# canonical type — wrong-class input raises (L3).
|
|
8
10
|
module ToolSchema
|
|
9
11
|
EMPTY_OBJECT = { type: 'object', properties: {} }.freeze
|
|
10
12
|
|
|
11
13
|
module_function
|
|
12
14
|
|
|
13
|
-
def extract(
|
|
14
|
-
|
|
15
|
-
ToolDefinition.normalize_parameters(
|
|
15
|
+
def extract(tool_definition)
|
|
16
|
+
require_tool_definition!(tool_definition, 'ToolSchema.extract')
|
|
17
|
+
ToolDefinition.normalize_parameters(tool_definition.parameters)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return tool.parameters if tool.respond_to?(:parameters) && tool.parameters
|
|
22
|
-
|
|
23
|
-
return unless tool.respond_to?(:[])
|
|
24
|
-
|
|
25
|
-
tool[:parameters] || tool['parameters'] || tool[:input_schema] || tool['input_schema'] ||
|
|
26
|
-
tool[:params_schema] || tool['params_schema']
|
|
20
|
+
def tool_name(tool_definition)
|
|
21
|
+
require_tool_definition!(tool_definition, 'ToolSchema.tool_name')
|
|
22
|
+
tool_definition.name
|
|
27
23
|
end
|
|
28
24
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'unknown'
|
|
25
|
+
def tool_description(tool_definition)
|
|
26
|
+
require_tool_definition!(tool_definition, 'ToolSchema.tool_description')
|
|
27
|
+
tool_definition.description
|
|
34
28
|
end
|
|
35
29
|
|
|
36
|
-
def
|
|
37
|
-
return
|
|
38
|
-
return (tool[:description] || tool['description'] || '').to_s if tool.respond_to?(:[])
|
|
30
|
+
def require_tool_definition!(tool_definition, site)
|
|
31
|
+
return tool_definition if tool_definition.is_a?(ToolDefinition)
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
raise ArgumentError, "#{site}: expected Canonical::ToolDefinition, got #{tool_definition.class}"
|
|
41
34
|
end
|
|
42
35
|
end
|
|
43
36
|
end
|
|
@@ -4,56 +4,44 @@
|
|
|
4
4
|
module Legion
|
|
5
5
|
module Extensions
|
|
6
6
|
module Llm
|
|
7
|
+
# -- required for Data.define block scope
|
|
7
8
|
module Canonical
|
|
8
|
-
# rubocop:disable Lint/ConstantDefinitionInBlock -- required for Data.define block scope
|
|
9
9
|
# Canonical usage/metering data for a response.
|
|
10
10
|
# Ports field vocabulary from lex-llm Tokens and legion-llm Types.
|
|
11
11
|
# Includes non-token units extension point per G20b.
|
|
12
|
+
# Canonical keys only (O03a): provider spellings are translated at the edges.
|
|
12
13
|
Usage = ::Data.define(
|
|
13
14
|
:input_tokens, :output_tokens, :cache_read_tokens, :cache_write_tokens,
|
|
14
|
-
:thinking_tokens, :units
|
|
15
|
+
:thinking_tokens, :units, :metadata
|
|
15
16
|
) do
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return nil if source.nil? || source.empty?
|
|
23
|
-
|
|
24
|
-
h = source.transform_keys(&:to_sym)
|
|
25
|
-
|
|
26
|
-
# Normalize legacy key names
|
|
27
|
-
h[:input_tokens] ||= h.delete(:input) || h.delete(:prompt_tokens)
|
|
28
|
-
h[:output_tokens] ||= h.delete(:output) || h.delete(:completion_tokens)
|
|
29
|
-
h[:cache_read_tokens] ||= h.delete(:cached) || h.delete(:cache_read)
|
|
30
|
-
h[:cache_write_tokens] ||= h.delete(:cache_creation) || h.delete(:cache_write)
|
|
31
|
-
h[:thinking_tokens] ||= h.delete(:thinking) || h.delete(:reasoning)
|
|
32
|
-
|
|
33
|
-
# Extract nested details (OpenAI prompt_tokens_details / input_tokens_details)
|
|
34
|
-
h[:cache_read_tokens] ||= dig_nested(h, :prompt_tokens_details, :cached_tokens) ||
|
|
35
|
-
dig_nested(h, :input_tokens_details, :cached_tokens)
|
|
36
|
-
h[:thinking_tokens] ||= dig_nested(h, :completion_tokens_details, :reasoning_tokens) ||
|
|
37
|
-
dig_nested(h, :output_tokens_details, :reasoning_tokens)
|
|
38
|
-
|
|
39
|
-
# Extract units (non-token extension point — G20b)
|
|
40
|
-
units = h.delete(:units) || {}
|
|
41
|
-
|
|
17
|
+
# rubocop:disable Metrics/ParameterLists -- factory methods have many params
|
|
18
|
+
# Build from keyword args (primary constructor).
|
|
19
|
+
def self.build(
|
|
20
|
+
input_tokens: nil, output_tokens: nil, cache_read_tokens: nil,
|
|
21
|
+
cache_write_tokens: nil, thinking_tokens: nil, units: nil, metadata: {}
|
|
22
|
+
)
|
|
42
23
|
new(
|
|
43
|
-
input_tokens
|
|
44
|
-
|
|
45
|
-
cache_read_tokens: h[:cache_read_tokens],
|
|
46
|
-
cache_write_tokens: h[:cache_write_tokens],
|
|
47
|
-
thinking_tokens: h[:thinking_tokens],
|
|
48
|
-
units: units
|
|
24
|
+
input_tokens:, output_tokens:, cache_read_tokens:, cache_write_tokens:,
|
|
25
|
+
thinking_tokens:, units: units || {}, metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
49
26
|
)
|
|
50
27
|
end
|
|
28
|
+
# rubocop:enable Metrics/ParameterLists
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
30
|
+
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
31
|
+
# from_hash({}) is a valid all-nil Usage (the no-usage object), never nil.
|
|
32
|
+
def self.from_hash(source)
|
|
33
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
34
|
+
hash = Strict.symbolize_keys(source)
|
|
35
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
36
|
+
build(
|
|
37
|
+
input_tokens: hash[:input_tokens],
|
|
38
|
+
output_tokens: hash[:output_tokens],
|
|
39
|
+
cache_read_tokens: hash[:cache_read_tokens],
|
|
40
|
+
cache_write_tokens: hash[:cache_write_tokens],
|
|
41
|
+
thinking_tokens: hash[:thinking_tokens],
|
|
42
|
+
units: hash[:units] || {},
|
|
43
|
+
metadata:
|
|
44
|
+
)
|
|
57
45
|
end
|
|
58
46
|
|
|
59
47
|
# Serialize to a Hash for AMQP/fleet/wire transport.
|
|
@@ -75,8 +63,27 @@ module Legion
|
|
|
75
63
|
[input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
76
64
|
thinking_tokens].compact.sum
|
|
77
65
|
end
|
|
66
|
+
|
|
67
|
+
# H1/M3: the single strict constructor. .new validates the token
|
|
68
|
+
# members as Integers (a string count would raise deep in
|
|
69
|
+
# StreamAccumulator#total_tokens instead of at construction) and
|
|
70
|
+
# units as a Hash. The factories fill their defaults and delegate
|
|
71
|
+
# here.
|
|
72
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
73
|
+
values[:input_tokens] = Strict.expect_type!(values[:input_tokens], [::Integer], site, :input_tokens)
|
|
74
|
+
values[:output_tokens] = Strict.expect_type!(values[:output_tokens], [::Integer], site, :output_tokens)
|
|
75
|
+
values[:cache_read_tokens] = Strict.expect_type!(values[:cache_read_tokens], [::Integer], site, :cache_read_tokens)
|
|
76
|
+
values[:cache_write_tokens] = Strict.expect_type!(values[:cache_write_tokens], [::Integer], site, :cache_write_tokens)
|
|
77
|
+
values[:thinking_tokens] = Strict.expect_type!(values[:thinking_tokens], [::Integer], site, :thinking_tokens)
|
|
78
|
+
values[:units] = values[:units].nil? ? {} : Strict.expect_type!(values[:units], [::Hash], site, :units)
|
|
79
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
80
|
+
values
|
|
81
|
+
end
|
|
78
82
|
end
|
|
79
|
-
|
|
83
|
+
|
|
84
|
+
Usage::BUILD_SITE = 'Canonical::Usage.build'
|
|
85
|
+
Usage::FROM_HASH_SITE = 'Canonical::Usage.from_hash'
|
|
86
|
+
Usage::NEW_SITE = 'Canonical::Usage.new'
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'canonical/strict'
|
|
3
4
|
require_relative 'canonical/thinking'
|
|
5
|
+
require_relative 'canonical/thinking_config'
|
|
4
6
|
require_relative 'canonical/usage'
|
|
5
7
|
require_relative 'canonical/params'
|
|
6
8
|
require_relative 'canonical/content_block'
|
|
@@ -26,17 +28,17 @@ module Legion
|
|
|
26
28
|
module Canonical
|
|
27
29
|
CONTRACT_VERSION = '1.0.0'
|
|
28
30
|
|
|
29
|
-
# Available canonical types.
|
|
31
|
+
# Available canonical types (the frozen 04 §1 inventory).
|
|
30
32
|
TYPES = %i[
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Request Response Chunk
|
|
33
|
+
Message ContentBlock ToolCall ToolDefinition ToolSchema
|
|
34
|
+
Params Thinking Thinking::Config
|
|
35
|
+
Request Response Chunk Usage
|
|
34
36
|
].freeze
|
|
35
37
|
|
|
36
38
|
class << self
|
|
37
39
|
# List all canonical type classes.
|
|
38
40
|
def types
|
|
39
|
-
TYPES.map { |name| const_get(
|
|
41
|
+
TYPES.map { |name| name.to_s.split('::').reduce(self) { |mod, part| mod.const_get(part) } }
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# Check if a given constant name is a registered canonical type.
|
|
@@ -26,23 +26,49 @@ module Legion
|
|
|
26
26
|
Array(keys).each { |key| option(key.to_sym) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
include Legion::Logging::Helper
|
|
30
|
+
|
|
31
|
+
# L5: the log defaults, read from the settings system (the ENV
|
|
32
|
+
# reads are deleted). A configured log_level Symbol/String names a
|
|
33
|
+
# Logger constant — an unknown name is a configuration error (it
|
|
34
|
+
# raises, it does not fall back).
|
|
35
|
+
def log_settings_defaults
|
|
36
|
+
{
|
|
37
|
+
level: log_level_value(llm_setting(:log_level)),
|
|
38
|
+
stream_debug: llm_setting(:log_stream_debug) == true
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
29
42
|
private
|
|
30
43
|
|
|
31
44
|
def option_keys = @option_keys ||= []
|
|
32
45
|
def defaults = @defaults ||= {}
|
|
33
|
-
|
|
46
|
+
|
|
47
|
+
def log_level_value(value)
|
|
48
|
+
return Logger::INFO if value.nil?
|
|
49
|
+
|
|
50
|
+
value.is_a?(::Integer) ? value : Logger.const_get(value.to_s.upcase)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def llm_setting(key)
|
|
54
|
+
return nil unless defined?(::Legion::Settings) && ::Legion::Settings.respond_to?(:dig)
|
|
55
|
+
|
|
56
|
+
::Legion::Settings.dig(:extensions, :llm, key)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
handle_exception(e, level: :warn, handled: true, operation: 'llm.configuration.setting', key:)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
private :option, :log_level_value, :llm_setting
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
# System-level options are declared here.
|
|
37
65
|
# Provider-specific options are declared in each provider extension via
|
|
38
66
|
# `self.configuration_options`.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
option
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
option :model_registry_file, -> { File.expand_path('models.json', __dir__) }
|
|
67
|
+
# H4: the dormant default_model / default_*_model options are deleted —
|
|
68
|
+
# a model-defaulting authority with no consumer (verified in-repo and
|
|
69
|
+
# in consumer gems). Model selection belongs to the router.
|
|
70
|
+
# The model_registry_file option is deleted with the Models catalog
|
|
71
|
+
# (the second inventory) — the SSOT registry is the only inventory.
|
|
46
72
|
|
|
47
73
|
option :request_timeout, 300
|
|
48
74
|
option :max_retries, 3
|
|
@@ -53,8 +79,12 @@ module Legion
|
|
|
53
79
|
|
|
54
80
|
option :logger, nil
|
|
55
81
|
option :log_file, -> { $stdout }
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
# L5: the ENV reads (LEGION_LLM_DEBUG / LEGION_LLM_STREAM_DEBUG) are
|
|
83
|
+
# deleted — every tunable lives in the settings system:
|
|
84
|
+
# extensions.llm.log_level (Symbol/Integer; Logger::INFO when unset)
|
|
85
|
+
# and extensions.llm.log_stream_debug (boolean; false when unset).
|
|
86
|
+
option :log_level, -> { self.class.log_settings_defaults[:level] }
|
|
87
|
+
option :log_stream_debug, -> { self.class.log_settings_defaults[:stream_debug] }
|
|
58
88
|
option :log_regexp_timeout, -> { Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil }
|
|
59
89
|
|
|
60
90
|
# Prompt caching
|