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
|
@@ -1,108 +1,87 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ParameterLists -- factory methods have many params
|
|
3
4
|
module Legion
|
|
4
5
|
module Extensions
|
|
5
6
|
module Llm
|
|
6
7
|
module Canonical
|
|
7
8
|
# Typed content block with media_type support per G20a.
|
|
8
|
-
# Ports field vocabulary from Legion::LLM::Types::ContentBlock.
|
|
9
|
-
# rubocop:disable Lint/ConstantDefinitionInBlock -- required for Data.define block scope
|
|
9
|
+
# Ports field vocabulary from Legion::LLM::Types::ContentBlock. # -- required for Data.define block scope
|
|
10
10
|
ContentBlock = ::Data.define(
|
|
11
11
|
:type, :text, :data, :source_type, :media_type,
|
|
12
12
|
:detail, :name, :file_id,
|
|
13
13
|
:id, :input, :tool_use_id, :is_error,
|
|
14
14
|
:source, :start_index, :end_index,
|
|
15
|
-
:code, :message, :cache_control
|
|
15
|
+
:code, :message, :cache_control, :metadata
|
|
16
16
|
) do
|
|
17
|
-
|
|
17
|
+
# Build from keyword args (primary constructor).
|
|
18
|
+
def self.build(
|
|
19
|
+
type: nil, text: nil, data: nil, source_type: nil, media_type: nil,
|
|
20
|
+
detail: nil, name: nil, file_id: nil,
|
|
21
|
+
id: nil, input: nil, tool_use_id: nil, is_error: nil,
|
|
22
|
+
source: nil, start_index: nil, end_index: nil,
|
|
23
|
+
code: nil, message: nil, cache_control: nil, metadata: {}
|
|
24
|
+
)
|
|
25
|
+
new(
|
|
26
|
+
type: normalize_type!(type, self::BUILD_SITE),
|
|
27
|
+
text:, data:, source_type:, media_type:,
|
|
28
|
+
detail:, name:, file_id:,
|
|
29
|
+
id:, input:, tool_use_id:, is_error:,
|
|
30
|
+
source:, start_index:, end_index:,
|
|
31
|
+
code:, message:, cache_control:,
|
|
32
|
+
metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
37
|
+
# Canonical keys only (O03a); unknown keys fold into metadata (L5).
|
|
38
|
+
def self.from_hash(source)
|
|
39
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
40
|
+
hash = Strict.symbolize_keys(source)
|
|
41
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
42
|
+
build(
|
|
43
|
+
type: hash[:type], text: hash[:text], data: hash[:data],
|
|
44
|
+
source_type: hash[:source_type], media_type: hash[:media_type],
|
|
45
|
+
detail: hash[:detail], name: hash[:name], file_id: hash[:file_id],
|
|
46
|
+
id: hash[:id], input: hash[:input], tool_use_id: hash[:tool_use_id],
|
|
47
|
+
is_error: hash[:is_error], source: hash[:source],
|
|
48
|
+
start_index: hash[:start_index], end_index: hash[:end_index],
|
|
49
|
+
code: hash[:code], message: hash[:message],
|
|
50
|
+
cache_control: hash[:cache_control], metadata:
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# L6: validate against the declared block types when present.
|
|
55
|
+
def self.normalize_type!(type, site)
|
|
56
|
+
return nil if type.nil?
|
|
57
|
+
|
|
58
|
+
type_sym = type.is_a?(::String) ? type.to_sym : type
|
|
59
|
+
Strict.enum!(type_sym, self::CONTENT_BLOCK_TYPES, site, :type)
|
|
60
|
+
end
|
|
18
61
|
|
|
19
62
|
# Build a text content block.
|
|
20
63
|
def self.text(content, cache_control: nil)
|
|
21
|
-
|
|
22
|
-
type: :text, text: content, data: nil, source_type: nil, media_type: nil,
|
|
23
|
-
detail: nil, name: nil, file_id: nil, id: nil, input: nil,
|
|
24
|
-
tool_use_id: nil, is_error: nil, source: nil, start_index: nil,
|
|
25
|
-
end_index: nil, code: nil, message: nil, cache_control: cache_control
|
|
26
|
-
)
|
|
64
|
+
build(type: :text, text: content, cache_control:)
|
|
27
65
|
end
|
|
28
66
|
|
|
29
67
|
# Build a thinking content block.
|
|
30
68
|
def self.thinking(content)
|
|
31
|
-
|
|
32
|
-
type: :thinking, text: content, data: nil, source_type: nil, media_type: nil,
|
|
33
|
-
detail: nil, name: nil, file_id: nil, id: nil, input: nil,
|
|
34
|
-
tool_use_id: nil, is_error: nil, source: nil, start_index: nil,
|
|
35
|
-
end_index: nil, code: nil, message: nil, cache_control: nil
|
|
36
|
-
)
|
|
69
|
+
build(type: :thinking, text: content)
|
|
37
70
|
end
|
|
38
71
|
|
|
39
72
|
# Build a tool_use content block.
|
|
40
73
|
def self.tool_use(id:, name:, input:)
|
|
41
|
-
|
|
42
|
-
type: :tool_use, text: nil, data: nil, source_type: nil, media_type: nil,
|
|
43
|
-
detail: nil, name: name, file_id: nil, id: id, input: input,
|
|
44
|
-
tool_use_id: nil, is_error: nil, source: nil, start_index: nil,
|
|
45
|
-
end_index: nil, code: nil, message: nil, cache_control: nil
|
|
46
|
-
)
|
|
74
|
+
build(type: :tool_use, id:, name:, input:)
|
|
47
75
|
end
|
|
48
76
|
|
|
49
77
|
# Build a tool_result content block.
|
|
50
78
|
def self.tool_result(tool_use_id:, content:, is_error: false)
|
|
51
|
-
|
|
52
|
-
type: :tool_result, text: content, data: nil, source_type: nil, media_type: nil,
|
|
53
|
-
detail: nil, name: nil, file_id: nil, id: nil, input: nil,
|
|
54
|
-
tool_use_id: tool_use_id, is_error: is_error, source: nil, start_index: nil,
|
|
55
|
-
end_index: nil, code: nil, message: nil, cache_control: nil
|
|
56
|
-
)
|
|
79
|
+
build(type: :tool_result, text: content, tool_use_id:, is_error:)
|
|
57
80
|
end
|
|
58
81
|
|
|
59
82
|
# Build an image content block with media_type (G20a).
|
|
60
83
|
def self.image(data:, media_type:, source_type: :base64, detail: nil)
|
|
61
|
-
|
|
62
|
-
type: :image, text: nil, data: data, source_type: source_type, media_type: media_type,
|
|
63
|
-
detail: detail, name: nil, file_id: nil, id: nil, input: nil,
|
|
64
|
-
tool_use_id: nil, is_error: nil, source: nil, start_index: nil,
|
|
65
|
-
end_index: nil, code: nil, message: nil, cache_control: nil
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
70
|
-
# Rescues NoMethodError from corrupted inputs (e.g. String elements from
|
|
71
|
-
# prior serialization bugs where ContentBlock#inspect leaked into storage).
|
|
72
|
-
def self.from_hash(source)
|
|
73
|
-
return nil if source.nil?
|
|
74
|
-
|
|
75
|
-
h = source.transform_keys(&:to_sym)
|
|
76
|
-
type_raw = h.delete(:type)
|
|
77
|
-
if type_raw
|
|
78
|
-
type_sym = type_raw.to_sym
|
|
79
|
-
h[:type] = TEXT_TYPE_ALIASES.include?(type_sym) ? :text : type_sym
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
new(
|
|
83
|
-
type: h[:type],
|
|
84
|
-
text: h[:text],
|
|
85
|
-
data: h[:data],
|
|
86
|
-
source_type: h[:source_type],
|
|
87
|
-
media_type: h[:media_type],
|
|
88
|
-
detail: h[:detail],
|
|
89
|
-
name: h[:name],
|
|
90
|
-
file_id: h[:file_id],
|
|
91
|
-
id: h[:id],
|
|
92
|
-
input: h[:input],
|
|
93
|
-
tool_use_id: h[:tool_use_id],
|
|
94
|
-
is_error: h[:is_error],
|
|
95
|
-
source: h[:source],
|
|
96
|
-
start_index: h[:start_index],
|
|
97
|
-
end_index: h[:end_index],
|
|
98
|
-
code: h[:code],
|
|
99
|
-
message: h[:message],
|
|
100
|
-
cache_control: h[:cache_control]
|
|
101
|
-
)
|
|
102
|
-
rescue NoMethodError => e
|
|
103
|
-
Legion::Logging.log.warn('[canonical][content_block] from_hash received non-Hash input ' \
|
|
104
|
-
"(#{source.class}): #{e.message}")
|
|
105
|
-
text(source.to_s)
|
|
84
|
+
build(type: :image, data:, media_type:, source_type:, detail:)
|
|
106
85
|
end
|
|
107
86
|
|
|
108
87
|
# Serialize to a Hash for AMQP/fleet/wire transport.
|
|
@@ -134,7 +113,7 @@ module Legion
|
|
|
134
113
|
|
|
135
114
|
# Whether this block carries textual content.
|
|
136
115
|
def text?
|
|
137
|
-
|
|
116
|
+
type == :text
|
|
138
117
|
end
|
|
139
118
|
|
|
140
119
|
# Whether this block carries thinking/reasoning content.
|
|
@@ -151,11 +130,23 @@ module Legion
|
|
|
151
130
|
def tool_result?
|
|
152
131
|
type == :tool_result
|
|
153
132
|
end
|
|
133
|
+
|
|
134
|
+
# H1: the single strict constructor — .new runs the same member
|
|
135
|
+
# contract as the factories; the factories fill their defaults and
|
|
136
|
+
# delegate here.
|
|
137
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
138
|
+
values[:type] = normalize_type!(values[:type], site)
|
|
139
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
140
|
+
values
|
|
141
|
+
end
|
|
154
142
|
end
|
|
155
143
|
|
|
156
144
|
ContentBlock::CONTENT_BLOCK_TYPES = %i[text thinking tool_use tool_result image audio video].freeze
|
|
157
|
-
|
|
145
|
+
ContentBlock::BUILD_SITE = 'Canonical::ContentBlock.build'
|
|
146
|
+
ContentBlock::FROM_HASH_SITE = 'Canonical::ContentBlock.from_hash'
|
|
147
|
+
ContentBlock::NEW_SITE = 'Canonical::ContentBlock.new'
|
|
158
148
|
end
|
|
159
149
|
end
|
|
160
150
|
end
|
|
161
151
|
end
|
|
152
|
+
# rubocop:enable Metrics/ParameterLists
|
|
@@ -6,37 +6,33 @@ require 'securerandom'
|
|
|
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 message in a conversation.
|
|
12
|
-
# Ports field vocabulary from Legion::LLM::Types::Message
|
|
12
|
+
# Ports field vocabulary from Legion::LLM::Types::Message.
|
|
13
|
+
# Unknown keys fold into the metadata member (04 L5) — never dropped.
|
|
14
|
+
# :cache_control (prompt-cache breakpoints) IS a member and survives
|
|
15
|
+
# build/to_h/JSON round-trips, including the fleet wire.
|
|
13
16
|
Message = ::Data.define(
|
|
14
17
|
:id, :parent_id, :role, :content, :tool_calls, :tool_call_id,
|
|
15
18
|
:name, :status, :version, :timestamp, :seq,
|
|
16
19
|
:provider, :model, :input_tokens, :output_tokens,
|
|
17
|
-
:conversation_id, :task_id
|
|
20
|
+
:conversation_id, :task_id, :cache_control, :metadata
|
|
18
21
|
) do
|
|
19
|
-
ROLES = %i[system user assistant tool].freeze
|
|
20
|
-
|
|
21
22
|
# Build from keyword args (primary constructor).
|
|
22
23
|
def self.build(
|
|
23
24
|
id: nil, parent_id: nil, role: :user, content: nil, tool_calls: nil,
|
|
24
25
|
tool_call_id: nil, name: nil, status: :created, version: 1,
|
|
25
26
|
timestamp: nil, seq: nil, provider: nil, model: nil,
|
|
26
|
-
input_tokens: nil, output_tokens: nil, conversation_id: nil, task_id: nil
|
|
27
|
+
input_tokens: nil, output_tokens: nil, conversation_id: nil, task_id: nil,
|
|
28
|
+
cache_control: nil, metadata: {}
|
|
27
29
|
)
|
|
28
|
-
role_sym = role.is_a?(String) ? role.to_sym : role
|
|
29
|
-
unless ROLES.include?(role_sym)
|
|
30
|
-
raise ArgumentError,
|
|
31
|
-
"Invalid role: #{role_sym}. Must be one of: #{ROLES.join(', ')}"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
30
|
new(
|
|
35
31
|
id: id || "msg_#{SecureRandom.hex(12)}",
|
|
36
32
|
parent_id: parent_id,
|
|
37
|
-
role:
|
|
38
|
-
content: content,
|
|
39
|
-
tool_calls: tool_calls,
|
|
33
|
+
role: normalize_role!(role, self::BUILD_SITE),
|
|
34
|
+
content: normalize_content!(content, self::BUILD_SITE),
|
|
35
|
+
tool_calls: normalize_tool_calls!(tool_calls, self::BUILD_SITE),
|
|
40
36
|
tool_call_id: tool_call_id,
|
|
41
37
|
name: name,
|
|
42
38
|
status: status,
|
|
@@ -48,64 +44,43 @@ module Legion
|
|
|
48
44
|
input_tokens: input_tokens,
|
|
49
45
|
output_tokens: output_tokens,
|
|
50
46
|
conversation_id: conversation_id,
|
|
51
|
-
task_id: task_id
|
|
47
|
+
task_id: task_id,
|
|
48
|
+
cache_control: cache_control.nil? ? nil : Strict.expect_type!(cache_control, [::Hash], self::BUILD_SITE, :cache_control),
|
|
49
|
+
metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
52
50
|
)
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
56
|
-
def self.from_hash(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
role_raw = h[:role]
|
|
63
|
-
h[:role] = role_raw&.to_sym if role_raw
|
|
64
|
-
|
|
65
|
-
# Parse content blocks if they're an array of hashes
|
|
66
|
-
content = h[:content]
|
|
67
|
-
if content.is_a?(Array)
|
|
68
|
-
h[:content] = content.map do |block|
|
|
69
|
-
block.is_a?(ContentBlock) ? block : ContentBlock.from_hash(block)
|
|
70
|
-
end
|
|
71
|
-
elsif content.is_a?(Hash)
|
|
72
|
-
h[:content] = ContentBlock.from_hash(content)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Parse tool calls — Array is canonical; Hash is legacy lex-llm format (name → ToolCall)
|
|
76
|
-
tool_calls = h[:tool_calls]
|
|
77
|
-
if tool_calls.is_a?(Hash)
|
|
78
|
-
h[:tool_calls] = tool_calls.values.map do |tc|
|
|
79
|
-
next tc if tc.is_a?(ToolCall)
|
|
54
|
+
def self.from_hash(source)
|
|
55
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
56
|
+
hash = Strict.symbolize_keys(source)
|
|
57
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
58
|
+
build(**hash, metadata:)
|
|
59
|
+
end
|
|
80
60
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
next tc if tc.is_a?(ToolCall)
|
|
61
|
+
# L6: role validated at construction, in both factories.
|
|
62
|
+
def self.normalize_role!(role, site)
|
|
63
|
+
role_sym = role.is_a?(::String) ? role.to_sym : role
|
|
64
|
+
Strict.enum!(role_sym, self::ROLES, site, :role)
|
|
65
|
+
end
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
end
|
|
67
|
+
# L2: one content normalizer shared by build and from_hash.
|
|
68
|
+
# String | ContentBlock | Array<ContentBlock> | nil — anything else raises.
|
|
69
|
+
def self.normalize_content!(content, site)
|
|
70
|
+
return nil if content.nil?
|
|
71
|
+
return content if content.is_a?(::String) || content.is_a?(ContentBlock)
|
|
72
|
+
raise ArgumentError, "#{site}: content expected String | ContentBlock | Array, got #{content.class}" unless content.is_a?(::Array)
|
|
95
73
|
|
|
96
|
-
|
|
97
|
-
# injected onto every >=2-message request) by projecting onto the known
|
|
98
|
-
# member set — mirrors Canonical::Request.from_hash, which folds unknown keys
|
|
99
|
-
# instead of raising. Message has no metadata field, so unknown keys are dropped.
|
|
100
|
-
build(**h.slice(*members))
|
|
74
|
+
content.map { |block| block.is_a?(ContentBlock) ? block : ContentBlock.from_hash(block) }
|
|
101
75
|
end
|
|
102
76
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
77
|
+
# L2: one tool-call normalizer shared by build and from_hash.
|
|
78
|
+
# Array<ToolCall> | nil (Array is canonical; the legacy Hash shape is gone).
|
|
79
|
+
def self.normalize_tool_calls!(tool_calls, site)
|
|
80
|
+
return nil if tool_calls.nil?
|
|
107
81
|
|
|
108
|
-
|
|
82
|
+
Strict.expect_type!(tool_calls, [::Array], site, :tool_calls)
|
|
83
|
+
tool_calls.map { |tc| tc.is_a?(ToolCall) ? tc : ToolCall.from_hash(tc) }
|
|
109
84
|
end
|
|
110
85
|
|
|
111
86
|
# Extract plain text from content (String or ContentBlock array).
|
|
@@ -141,14 +116,24 @@ module Legion
|
|
|
141
116
|
text
|
|
142
117
|
end
|
|
143
118
|
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
119
|
+
# H1: the single strict constructor — .new runs the same member
|
|
120
|
+
# contract as the factories (normalize or raise); the factories
|
|
121
|
+
# fill their defaults and delegate here. Missing members follow the
|
|
122
|
+
# nil contract of each member.
|
|
123
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
124
|
+
values[:role] = normalize_role!(values[:role], site)
|
|
125
|
+
values[:content] = normalize_content!(values[:content], site)
|
|
126
|
+
values[:tool_calls] = normalize_tool_calls!(values[:tool_calls], site)
|
|
127
|
+
values[:cache_control] = values[:cache_control].nil? ? nil : Strict.expect_type!(values[:cache_control], [::Hash], site, :cache_control)
|
|
128
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
129
|
+
values
|
|
147
130
|
end
|
|
148
131
|
end
|
|
149
132
|
|
|
150
133
|
Message::ROLES = %i[system user assistant tool].freeze
|
|
151
|
-
|
|
134
|
+
Message::BUILD_SITE = 'Canonical::Message.build'
|
|
135
|
+
Message::FROM_HASH_SITE = 'Canonical::Message.from_hash'
|
|
136
|
+
Message::NEW_SITE = 'Canonical::Message.new'
|
|
152
137
|
end
|
|
153
138
|
end
|
|
154
139
|
end
|
|
@@ -3,49 +3,52 @@
|
|
|
3
3
|
# -- from_hash normalization is intentional
|
|
4
4
|
module Legion
|
|
5
5
|
module Extensions
|
|
6
|
+
# -- module doc is in canonical.rb entry point
|
|
6
7
|
module Llm
|
|
8
|
+
# -- required for Data.define block scope
|
|
7
9
|
module Canonical
|
|
8
|
-
# rubocop:disable Lint/ConstantDefinitionInBlock -- required for Data.define block scope
|
|
9
10
|
# Canonical sampling and limit parameters for a request.
|
|
10
11
|
# Per G18: all standard/useful params are first-class, mapped per provider by translators.
|
|
12
|
+
# Canonical keys only (O03a): provider spellings are translated at the edges.
|
|
11
13
|
Params = ::Data.define(
|
|
12
14
|
:max_tokens, :max_thinking_tokens, :temperature, :top_p, :top_k,
|
|
13
15
|
:stop_sequences, :seed, :frequency_penalty, :presence_penalty,
|
|
14
|
-
:response_format
|
|
16
|
+
:response_format, :metadata
|
|
15
17
|
) do
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
# rubocop:disable Metrics/ParameterLists -- factory methods have many params
|
|
19
|
+
# Build from keyword args (primary constructor).
|
|
20
|
+
def self.build(
|
|
21
|
+
max_tokens: nil, max_thinking_tokens: nil, temperature: nil, top_p: nil, top_k: nil,
|
|
22
|
+
stop_sequences: nil, seed: nil, frequency_penalty: nil, presence_penalty: nil,
|
|
23
|
+
response_format: nil, metadata: {}
|
|
24
|
+
)
|
|
25
|
+
new(
|
|
26
|
+
max_tokens:, max_thinking_tokens:, temperature:, top_p:, top_k:,
|
|
27
|
+
stop_sequences:, seed:, frequency_penalty:, presence_penalty:,
|
|
28
|
+
response_format:, metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
# rubocop:enable Metrics/ParameterLists
|
|
19
32
|
|
|
20
33
|
# Build from a Hash (raw client request or deserialized wire payload).
|
|
21
|
-
#
|
|
34
|
+
# Canonical member keys only; unknown keys fold into metadata (04 L5).
|
|
35
|
+
# from_hash({}) is a valid all-nil Params (the no-params object), never nil.
|
|
22
36
|
def self.from_hash(source)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
new(
|
|
39
|
-
max_tokens: filtered[:max_tokens],
|
|
40
|
-
max_thinking_tokens: filtered[:max_thinking_tokens],
|
|
41
|
-
temperature: filtered[:temperature],
|
|
42
|
-
top_p: filtered[:top_p],
|
|
43
|
-
top_k: filtered[:top_k],
|
|
44
|
-
stop_sequences: filtered[:stop_sequences],
|
|
45
|
-
seed: filtered[:seed],
|
|
46
|
-
frequency_penalty: filtered[:frequency_penalty],
|
|
47
|
-
presence_penalty: filtered[:presence_penalty],
|
|
48
|
-
response_format: filtered[:response_format]
|
|
37
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
38
|
+
hash = Strict.symbolize_keys(source)
|
|
39
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
40
|
+
build(
|
|
41
|
+
max_tokens: hash[:max_tokens],
|
|
42
|
+
max_thinking_tokens: hash[:max_thinking_tokens],
|
|
43
|
+
temperature: hash[:temperature],
|
|
44
|
+
top_p: hash[:top_p],
|
|
45
|
+
top_k: hash[:top_k],
|
|
46
|
+
stop_sequences: hash[:stop_sequences],
|
|
47
|
+
seed: hash[:seed],
|
|
48
|
+
frequency_penalty: hash[:frequency_penalty],
|
|
49
|
+
presence_penalty: hash[:presence_penalty],
|
|
50
|
+
response_format: hash[:response_format],
|
|
51
|
+
metadata:
|
|
49
52
|
)
|
|
50
53
|
end
|
|
51
54
|
|
|
@@ -62,8 +65,31 @@ module Legion
|
|
|
62
65
|
def to_json(*)
|
|
63
66
|
to_h.to_json(*)
|
|
64
67
|
end
|
|
68
|
+
|
|
69
|
+
# H1/M3: the single strict constructor. .new validates every member
|
|
70
|
+
# against its wire type (Integer token counts and seeds, Numeric
|
|
71
|
+
# ratios and penalties, String|Array stop sequences, String|Hash
|
|
72
|
+
# response format) — garbage is rejected here, not on the provider
|
|
73
|
+
# wire. The factories fill their defaults and delegate here.
|
|
74
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
75
|
+
values[:max_tokens] = Strict.expect_type!(values[:max_tokens], [::Integer], site, :max_tokens)
|
|
76
|
+
values[:max_thinking_tokens] = Strict.expect_type!(values[:max_thinking_tokens], [::Integer], site, :max_thinking_tokens)
|
|
77
|
+
values[:temperature] = Strict.expect_type!(values[:temperature], [::Numeric], site, :temperature)
|
|
78
|
+
values[:top_p] = Strict.expect_type!(values[:top_p], [::Numeric], site, :top_p)
|
|
79
|
+
values[:top_k] = Strict.expect_type!(values[:top_k], [::Integer], site, :top_k)
|
|
80
|
+
values[:stop_sequences] = Strict.expect_type!(values[:stop_sequences], [::String, ::Array], site, :stop_sequences)
|
|
81
|
+
values[:seed] = Strict.expect_type!(values[:seed], [::Integer], site, :seed)
|
|
82
|
+
values[:frequency_penalty] = Strict.expect_type!(values[:frequency_penalty], [::Numeric], site, :frequency_penalty)
|
|
83
|
+
values[:presence_penalty] = Strict.expect_type!(values[:presence_penalty], [::Numeric], site, :presence_penalty)
|
|
84
|
+
values[:response_format] = Strict.expect_type!(values[:response_format], [::String, ::Hash], site, :response_format)
|
|
85
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
86
|
+
values
|
|
87
|
+
end
|
|
65
88
|
end
|
|
66
|
-
|
|
89
|
+
|
|
90
|
+
Params::BUILD_SITE = 'Canonical::Params.build'
|
|
91
|
+
Params::FROM_HASH_SITE = 'Canonical::Params.from_hash'
|
|
92
|
+
Params::NEW_SITE = 'Canonical::Params.new'
|
|
67
93
|
end
|
|
68
94
|
end
|
|
69
95
|
end
|
|
@@ -8,7 +8,7 @@ module Legion
|
|
|
8
8
|
module Llm
|
|
9
9
|
module Canonical
|
|
10
10
|
# Canonical request shape — the single contract between client translators
|
|
11
|
-
# and the inference executor. Per R3 and G18.
|
|
11
|
+
# and the inference executor. Per R3 and G18. # -- required for Data.define block scope
|
|
12
12
|
Request = ::Data.define(
|
|
13
13
|
:id, :messages, :system, :tools, :tool_choice,
|
|
14
14
|
:params, :thinking, :stream,
|
|
@@ -18,60 +18,74 @@ module Legion
|
|
|
18
18
|
def self.build(
|
|
19
19
|
id: nil, messages: nil, system: nil, tools: nil, tool_choice: nil,
|
|
20
20
|
params: nil, thinking: nil, stream: false,
|
|
21
|
-
conversation_id: nil, caller: nil, routing: nil, metadata:
|
|
21
|
+
conversation_id: nil, caller: nil, routing: nil, metadata: {}
|
|
22
22
|
)
|
|
23
|
-
# Normalize messages to Canonical::Message array
|
|
24
|
-
msg_array = Array(messages).filter_map do |msg|
|
|
25
|
-
msg.is_a?(Message) ? msg : Message.from_hash(msg)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Normalize tools to Hash<name, ToolDefinition>
|
|
29
|
-
tool_hash = normalize_tools(tools)
|
|
30
|
-
|
|
31
|
-
# Normalize params
|
|
32
|
-
params_obj = case params
|
|
33
|
-
when Params then params
|
|
34
|
-
when Hash then Params.from_hash(params)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Normalize thinking config
|
|
38
|
-
thinking_obj = case thinking
|
|
39
|
-
when Thinking::Config then thinking
|
|
40
|
-
when Hash then Thinking::Config.new(**thinking.transform_keys(&:to_sym))
|
|
41
|
-
end
|
|
42
|
-
|
|
43
23
|
new(
|
|
44
24
|
id: id || "req_#{SecureRandom.hex(12)}",
|
|
45
|
-
messages:
|
|
25
|
+
messages: normalize_messages!(messages, self::BUILD_SITE),
|
|
46
26
|
system: system,
|
|
47
|
-
tools:
|
|
48
|
-
tool_choice: tool_choice.is_a?(String) ? tool_choice.to_sym : tool_choice,
|
|
49
|
-
params:
|
|
50
|
-
thinking:
|
|
27
|
+
tools: normalize_tools(tools, self::BUILD_SITE),
|
|
28
|
+
tool_choice: tool_choice.is_a?(::String) ? tool_choice.to_sym : tool_choice,
|
|
29
|
+
params: normalize_params!(params, self::BUILD_SITE),
|
|
30
|
+
thinking: normalize_thinking!(thinking, self::BUILD_SITE),
|
|
51
31
|
stream: stream,
|
|
52
32
|
conversation_id: conversation_id,
|
|
53
33
|
caller: caller,
|
|
54
34
|
routing: routing || {},
|
|
55
|
-
metadata: metadata
|
|
35
|
+
metadata: Strict.metadata!(metadata, self::BUILD_SITE)
|
|
56
36
|
)
|
|
57
37
|
end
|
|
58
38
|
|
|
59
39
|
# Build from a Hash (raw client request or deserialized wire payload).
|
|
40
|
+
# Unknown keys fold into metadata — the model for 04 L5.
|
|
60
41
|
def self.from_hash(source)
|
|
61
|
-
|
|
42
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
43
|
+
hash = Strict.symbolize_keys(source)
|
|
44
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
45
|
+
build(**hash, metadata:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 04 §9: strict message map — each element must be a Message (pass) or a
|
|
49
|
+
# Hash (normalize); anything else raises. No silent drops (F2 fix).
|
|
50
|
+
def self.normalize_messages!(messages, site)
|
|
51
|
+
return [] if messages.nil?
|
|
52
|
+
|
|
53
|
+
Strict.expect_type!(messages, [::Array], site, :messages)
|
|
54
|
+
messages.map { |msg| msg.is_a?(Message) ? msg : Message.from_hash(msg) }
|
|
55
|
+
end
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
# L2: the single tools normalizer, shared by build and from_hash.
|
|
58
|
+
# Hash<name, ToolDefinition> or Array<ToolDefinition|Hash>; anything else raises.
|
|
59
|
+
def self.normalize_tools(tools, site)
|
|
60
|
+
return {} if tools.nil? || tools.empty?
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
case tools
|
|
63
|
+
when Hash
|
|
64
|
+
tools.transform_values { |tool| tool.is_a?(ToolDefinition) ? tool : ToolDefinition.from_hash(tool) }
|
|
65
|
+
when Array
|
|
66
|
+
tools.each_with_object({}) do |tool, hash|
|
|
67
|
+
td = tool.is_a?(ToolDefinition) ? tool : ToolDefinition.from_hash(tool)
|
|
68
|
+
hash[td.name] = td
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
Strict.expect_type!(tools, [::Hash, ::Array], site, :tools)
|
|
71
72
|
end
|
|
72
|
-
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.normalize_params!(params, site)
|
|
76
|
+
return nil if params.nil?
|
|
77
|
+
return params if params.is_a?(Params)
|
|
78
|
+
|
|
79
|
+
Strict.expect_type!(params, [::Hash], site, :params)
|
|
80
|
+
Params.from_hash(params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.normalize_thinking!(thinking, site)
|
|
84
|
+
return nil if thinking.nil?
|
|
85
|
+
return thinking if thinking.is_a?(Thinking::Config)
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
Strict.expect_type!(thinking, [::Hash], site, :thinking)
|
|
88
|
+
Thinking::Config.from_hash(thinking)
|
|
75
89
|
end
|
|
76
90
|
|
|
77
91
|
# Serialize to a Hash for AMQP/fleet/wire transport.
|
|
@@ -101,24 +115,23 @@ module Legion
|
|
|
101
115
|
to_h.to_json(*)
|
|
102
116
|
end
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
hash[td.name] = td
|
|
116
|
-
end
|
|
117
|
-
else
|
|
118
|
-
{}
|
|
119
|
-
end
|
|
118
|
+
# H1: the single strict constructor — .new runs the same member
|
|
119
|
+
# contract as the factories; the factories fill their defaults and
|
|
120
|
+
# delegate here.
|
|
121
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
122
|
+
values[:messages] = normalize_messages!(values[:messages], site)
|
|
123
|
+
values[:tools] = normalize_tools(values[:tools], site)
|
|
124
|
+
values[:tool_choice] = values[:tool_choice].is_a?(::String) ? values[:tool_choice].to_sym : values[:tool_choice]
|
|
125
|
+
values[:params] = normalize_params!(values[:params], site)
|
|
126
|
+
values[:thinking] = normalize_thinking!(values[:thinking], site)
|
|
127
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
128
|
+
values
|
|
120
129
|
end
|
|
121
130
|
end
|
|
131
|
+
|
|
132
|
+
Request::BUILD_SITE = 'Canonical::Request.build'
|
|
133
|
+
Request::FROM_HASH_SITE = 'Canonical::Request.from_hash'
|
|
134
|
+
Request::NEW_SITE = 'Canonical::Request.new'
|
|
122
135
|
end
|
|
123
136
|
end
|
|
124
137
|
end
|