lex-llm 0.6.16 → 0.7.2
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 +29 -0
- data/lex-llm.gemspec +1 -1
- data/lib/legion/extensions/llm/auto_registration.rb +3 -0
- data/lib/legion/extensions/llm/canonical/message.rb +5 -1
- data/lib/legion/extensions/llm/fleet/protocol.rb +12 -0
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +40 -5
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -0
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +166 -2
- data/lib/legion/extensions/llm/inventory/callable_handle.rb +144 -0
- data/lib/legion/extensions/llm/inventory/errors.rb +34 -0
- data/lib/legion/extensions/llm/inventory/evidence.rb +134 -0
- data/lib/legion/extensions/llm/inventory/identity.rb +141 -0
- data/lib/legion/extensions/llm/inventory/immutable_value.rb +96 -0
- data/lib/legion/extensions/llm/inventory/probe_coordinator.rb +117 -0
- data/lib/legion/extensions/llm/inventory/probe_token.rb +146 -0
- data/lib/legion/extensions/llm/inventory/publisher.rb +116 -0
- data/lib/legion/extensions/llm/inventory/records.rb +599 -0
- data/lib/legion/extensions/llm/inventory/registry.rb +636 -0
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +206 -0
- data/lib/legion/extensions/llm/inventory/snapshot.rb +91 -0
- data/lib/legion/extensions/llm/provider.rb +55 -29
- data/lib/legion/extensions/llm/routing/lane_key.rb +4 -0
- data/lib/legion/extensions/llm/routing/model_offering.rb +3 -0
- data/lib/legion/extensions/llm/routing/offering_registry.rb +2 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +54 -0
- data/lib/legion/extensions/llm/routing/records.rb +288 -0
- data/lib/legion/extensions/llm/settings_cascade.rb +120 -0
- data/lib/legion/extensions/llm/taxonomies.rb +90 -0
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -1
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +3 -1
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +18 -0
- data/spec/legion/extensions/llm/canonical/message_spec.rb +24 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +1 -0
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +84 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_conformance_spec.rb +13 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +196 -0
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +179 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +26 -0
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +14 -0
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +80 -0
- data/spec/legion/extensions/llm/inventory/callable_handle_spec.rb +127 -0
- data/spec/legion/extensions/llm/inventory/evidence_spec.rb +138 -0
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +280 -0
- data/spec/legion/extensions/llm/inventory/immutable_value_spec.rb +132 -0
- data/spec/legion/extensions/llm/inventory/probe_coordinator_spec.rb +107 -0
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +107 -0
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +144 -0
- data/spec/legion/extensions/llm/inventory/records_spec.rb +300 -0
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +140 -0
- data/spec/legion/extensions/llm/inventory/registry_availability_spec.rb +152 -0
- data/spec/legion/extensions/llm/inventory/registry_callable_spec.rb +63 -0
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +69 -0
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +128 -0
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +109 -0
- data/spec/legion/extensions/llm/provider_contract_spec.rb +20 -0
- data/spec/legion/extensions/llm/provider_spec.rb +83 -0
- data/spec/legion/extensions/llm/routing/provider_outcome_spec.rb +73 -0
- data/spec/legion/extensions/llm/routing/records_spec.rb +173 -0
- data/spec/legion/extensions/llm/settings_cascade_spec.rb +208 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +51 -0
- data/spec/support/fake_llm_provider.rb +11 -0
- data/spec/support/fake_ssot_harness.rb +137 -0
- data/spec/support/ssot_registry_helpers.rb +75 -0
- metadata +40 -3
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/llm/inventory/errors'
|
|
4
|
+
require 'legion/extensions/llm/inventory/immutable_value'
|
|
5
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
6
|
+
require 'legion/extensions/llm/inventory/evidence'
|
|
7
|
+
require 'legion/extensions/llm/inventory/callable_handle'
|
|
8
|
+
require 'legion/extensions/llm/taxonomies'
|
|
9
|
+
require 'legion/extensions/llm/capabilities'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module Llm
|
|
14
|
+
module Inventory
|
|
15
|
+
# Documentation namespace anchor for the immutable inventory records
|
|
16
|
+
# defined in this file: OfferingDraft, OfferingRecord, LaneRecord,
|
|
17
|
+
# AvailabilityFact, ReadinessResult, InstanceRecord, PublicationStatus,
|
|
18
|
+
# and MutationResult. The records themselves live directly under
|
|
19
|
+
# Inventory so their canonical constant paths stay flat.
|
|
20
|
+
module Records; end
|
|
21
|
+
|
|
22
|
+
# Shared normalization/validation used by the immutable inventory and
|
|
23
|
+
# routing records. Kept internal to the records so no record implements a
|
|
24
|
+
# competing validator. See phase-1-lex-llm-additive.md sections 10 and 15.
|
|
25
|
+
module RecordSupport
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
SECRET_KEY_TOKENS = %w[
|
|
29
|
+
credential authorization token secret password apikey prompt endpointurl callable
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
PUBLICATION_SOURCES = %i[provider_catalog provider_static_catalog provider_control_plane].freeze
|
|
33
|
+
|
|
34
|
+
def check_unknown_kwargs!(kwargs:, members:)
|
|
35
|
+
unknown = kwargs.keys - members
|
|
36
|
+
raise Errors::ValidationError, "unknown keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def valid_utf8?(string)
|
|
40
|
+
[::Encoding::UTF_8, ::Encoding::US_ASCII].include?(string.encoding) && string.valid_encoding?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sanitized_reason(value:, field:, max_bytes: 1024)
|
|
44
|
+
raise Errors::ValidationError, "#{field} must be a String" unless value.is_a?(::String)
|
|
45
|
+
|
|
46
|
+
# Coerce any source encoding to valid UTF-8 rather than raising: provider
|
|
47
|
+
# error messages can be ASCII-8BIT/binary (raw response bodies, Ruby kernel
|
|
48
|
+
# error messages). A reason is diagnostic text, not operator input to reject —
|
|
49
|
+
# raising here would mask the real dispatch error as an unclassifiable 500 and
|
|
50
|
+
# defeat fail-forward. Undecodable bytes are replaced.
|
|
51
|
+
coerced = value.dup.force_encoding(::Encoding::UTF_8)
|
|
52
|
+
coerced = coerced.scrub('?') unless coerced.valid_encoding?
|
|
53
|
+
|
|
54
|
+
trimmed = coerced.strip
|
|
55
|
+
raise Errors::ValidationError, "#{field} must not be empty" if trimmed.empty?
|
|
56
|
+
raise Errors::ValidationError, "#{field} exceeds #{max_bytes} UTF-8 bytes" if trimmed.bytesize > max_bytes
|
|
57
|
+
|
|
58
|
+
trimmed.b.dup.force_encoding(::Encoding::UTF_8).freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def optional_sanitized_reason(value:, field:, max_bytes: 1024)
|
|
62
|
+
return nil if value.nil?
|
|
63
|
+
|
|
64
|
+
sanitized_reason(value: value, field: field, max_bytes: max_bytes)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def nonnegative_integer(value:, field:)
|
|
68
|
+
raise Errors::ValidationError, "#{field} must be a nonnegative Integer" unless nonnegative_integer?(value)
|
|
69
|
+
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def nonnegative_integer?(value)
|
|
74
|
+
value.is_a?(::Integer) && !value.negative?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def boolean!(value:, field:)
|
|
78
|
+
return value if [true, false].include?(value)
|
|
79
|
+
|
|
80
|
+
raise Errors::ValidationError, "#{field} must be true or false"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_provider_family(value:, field: :provider_family)
|
|
84
|
+
text = Identity.normalize_text(value: value, field: field).downcase
|
|
85
|
+
raise Errors::ValidationError, "#{field} must be snake-case ASCII" unless text.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
86
|
+
|
|
87
|
+
text.to_sym
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def optional_time(value:, field:)
|
|
91
|
+
return nil if value.nil?
|
|
92
|
+
raise Errors::ValidationError, "#{field} must be a Time" unless value.is_a?(::Time)
|
|
93
|
+
|
|
94
|
+
value.dup.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def frozen_metadata(value:, field: :metadata)
|
|
98
|
+
reject_secret_keys!(value, field)
|
|
99
|
+
ImmutableValue.copy_and_freeze(value: value, field: field)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reject_secret_keys!(value, field)
|
|
103
|
+
case value
|
|
104
|
+
when ::Hash
|
|
105
|
+
value.each do |key, nested|
|
|
106
|
+
raise Errors::ValidationError, "#{field} contains a secret-like key" if secret_key?(key)
|
|
107
|
+
|
|
108
|
+
reject_secret_keys!(nested, field)
|
|
109
|
+
end
|
|
110
|
+
when ::Array
|
|
111
|
+
value.each { |element| reject_secret_keys!(element, field) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def secret_key?(key)
|
|
116
|
+
normalized = key.to_s.downcase.gsub(/[^a-z0-9]/, '')
|
|
117
|
+
SECRET_KEY_TOKENS.any? { |token| normalized.include?(token) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_operation_evidence!(evidence)
|
|
121
|
+
raise Errors::ValidationError, 'operation_evidence must be a Hash' unless evidence.is_a?(::Hash)
|
|
122
|
+
|
|
123
|
+
normalized = {}
|
|
124
|
+
evidence.each do |key, value|
|
|
125
|
+
op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
|
|
126
|
+
raise Errors::ValidationError, "operation_evidence[#{op}] must be an OperationEvidence" unless value.is_a?(OperationEvidence)
|
|
127
|
+
raise Errors::ValidationError, "operation_evidence[#{op}] operation must match key" unless value.operation == op
|
|
128
|
+
raise Errors::ValidationError, "duplicate operation_evidence key #{op}" if normalized.key?(op)
|
|
129
|
+
|
|
130
|
+
normalized[op] = value
|
|
131
|
+
end
|
|
132
|
+
raise Errors::ValidationError, 'operation_evidence keys must equal Taxonomies::OPERATIONS exactly' unless normalized.keys.sort == Taxonomies::OPERATIONS.sort
|
|
133
|
+
|
|
134
|
+
normalized.freeze
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def validate_capability_evidence!(evidence)
|
|
138
|
+
raise Errors::ValidationError, 'capability_evidence must be a Hash' unless evidence.is_a?(::Hash)
|
|
139
|
+
|
|
140
|
+
normalized = {}
|
|
141
|
+
evidence.each do |key, value|
|
|
142
|
+
cap = Legion::Extensions::Llm::Capabilities.canonical(key)
|
|
143
|
+
raise Errors::ValidationError, "capability_evidence key #{cap} is not canonical" unless Legion::Extensions::Llm::Capabilities::CANONICAL.include?(cap)
|
|
144
|
+
raise Errors::ValidationError, "capability_evidence[#{cap}] must be a CapabilityEvidence" unless value.is_a?(CapabilityEvidence)
|
|
145
|
+
raise Errors::ValidationError, "capability_evidence[#{cap}] capability must match key" unless value.capability == cap
|
|
146
|
+
raise Errors::ValidationError, "duplicate capability_evidence key #{cap}" if normalized.key?(cap)
|
|
147
|
+
|
|
148
|
+
normalized[cap] = value
|
|
149
|
+
end
|
|
150
|
+
normalized.freeze
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_quota_domains!(quota_domains)
|
|
154
|
+
raise Errors::ValidationError, 'quota_domains must be a Hash' unless quota_domains.is_a?(::Hash)
|
|
155
|
+
|
|
156
|
+
normalized = {}
|
|
157
|
+
quota_domains.each do |key, value|
|
|
158
|
+
op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
|
|
159
|
+
normalized[op] = validate_quota_domain_value!(value: value, field: "quota_domains[#{op}]")
|
|
160
|
+
end
|
|
161
|
+
normalized.freeze
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def validate_quota_domain_value!(value:, field:)
|
|
165
|
+
raise Errors::ValidationError, "#{field} must be a nonempty opaque String" unless value.is_a?(::String) && valid_utf8?(value) && !value.strip.empty?
|
|
166
|
+
|
|
167
|
+
value.dup.freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Validates and returns the five scalar ValueEvidence fields shared by
|
|
171
|
+
# OfferingDraft, OfferingRecord, and LaneRecord as a member Hash.
|
|
172
|
+
def scalar_value_evidences(kwargs)
|
|
173
|
+
{
|
|
174
|
+
context_evidence: positive_int_value_evidence!(kwargs[:context_evidence], :context_evidence),
|
|
175
|
+
max_output_evidence: positive_int_value_evidence!(kwargs[:max_output_evidence], :max_output_evidence),
|
|
176
|
+
embedding_dimensions_evidence: embedding_dimensions_value_evidence!(
|
|
177
|
+
kwargs[:embedding_dimensions_evidence], :embedding_dimensions_evidence
|
|
178
|
+
),
|
|
179
|
+
model_revision_evidence: model_revision_value_evidence!(
|
|
180
|
+
kwargs[:model_revision_evidence], :model_revision_evidence
|
|
181
|
+
),
|
|
182
|
+
tokenizer_evidence: tokenizer_value_evidence!(kwargs[:tokenizer_evidence], :tokenizer_evidence)
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def value_evidence!(evidence, field)
|
|
187
|
+
raise Errors::ValidationError, "#{field} must be a ValueEvidence" unless evidence.is_a?(ValueEvidence)
|
|
188
|
+
|
|
189
|
+
evidence
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def positive_int_value_evidence!(evidence, field)
|
|
193
|
+
value_evidence!(evidence, field)
|
|
194
|
+
raise Errors::ValidationError, "known #{field} must be a positive Integer" if evidence.known? && !(evidence.value.is_a?(::Integer) && evidence.value.positive?)
|
|
195
|
+
|
|
196
|
+
evidence
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def embedding_dimensions_value_evidence!(evidence, field)
|
|
200
|
+
value_evidence!(evidence, field)
|
|
201
|
+
return evidence unless evidence.known?
|
|
202
|
+
|
|
203
|
+
dims = evidence.value
|
|
204
|
+
valid = dims.is_a?(::Array) && !dims.empty? &&
|
|
205
|
+
dims.all? { |dim| dim.is_a?(::Integer) && dim.positive? } &&
|
|
206
|
+
dims == dims.uniq && dims == dims.sort
|
|
207
|
+
raise Errors::ValidationError, "known #{field} must be a sorted, unique Array of positive Integers" unless valid
|
|
208
|
+
|
|
209
|
+
evidence
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def model_revision_value_evidence!(evidence, field)
|
|
213
|
+
value_evidence!(evidence, field)
|
|
214
|
+
return evidence unless evidence.known?
|
|
215
|
+
|
|
216
|
+
revision = evidence.value
|
|
217
|
+
valid = revision.is_a?(::String) && !revision.strip.empty? &&
|
|
218
|
+
valid_utf8?(revision) && revision.unicode_normalized?(:nfc)
|
|
219
|
+
raise Errors::ValidationError, "known #{field} must be a nonempty NFC String" unless valid
|
|
220
|
+
|
|
221
|
+
evidence
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def tokenizer_value_evidence!(evidence, field)
|
|
225
|
+
value_evidence!(evidence, field)
|
|
226
|
+
return evidence unless evidence.known?
|
|
227
|
+
|
|
228
|
+
descriptor = evidence.value
|
|
229
|
+
unless descriptor.is_a?(::Hash) && descriptor.keys.sort == %i[estimator parameters version]
|
|
230
|
+
raise Errors::ValidationError, "known #{field} must be exactly {estimator:, version:, parameters:}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
nonempty_nfc!(value: descriptor[:estimator], field: "#{field}.estimator")
|
|
234
|
+
nonempty_nfc!(value: descriptor[:version], field: "#{field}.version")
|
|
235
|
+
raise Errors::ValidationError, "#{field}.parameters must be a Hash" unless descriptor[:parameters].is_a?(::Hash)
|
|
236
|
+
|
|
237
|
+
evidence
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def nonempty_nfc!(value:, field:)
|
|
241
|
+
valid = value.is_a?(::String) && !value.strip.empty? && valid_utf8?(value) && value.unicode_normalized?(:nfc)
|
|
242
|
+
raise Errors::ValidationError, "#{field} must be a nonempty NFC String" unless valid
|
|
243
|
+
|
|
244
|
+
value
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def publication_source!(value:)
|
|
248
|
+
raise Errors::ValidationError, "publication_source must be one of #{PUBLICATION_SOURCES.inspect}" unless PUBLICATION_SOURCES.include?(value)
|
|
249
|
+
|
|
250
|
+
value
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# An off-registry provider draft of one offering. Carries no provider
|
|
255
|
+
# family, instance ID, offering ID, lane ID, callable, health, weight, or
|
|
256
|
+
# default model. See section 10.1.
|
|
257
|
+
OfferingDraft = ::Data.define(
|
|
258
|
+
:provider_native_key, :model, :tier, :operation_evidence, :capability_evidence,
|
|
259
|
+
:context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
260
|
+
:model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source
|
|
261
|
+
) do
|
|
262
|
+
def initialize(**kwargs)
|
|
263
|
+
kwargs = { capability_evidence: {}, quota_domains: {}, metadata: {} }.merge(kwargs)
|
|
264
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
265
|
+
|
|
266
|
+
super(
|
|
267
|
+
provider_native_key: Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key),
|
|
268
|
+
model: Identity.normalize_text(value: kwargs[:model], field: :model),
|
|
269
|
+
tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
|
|
270
|
+
operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
|
|
271
|
+
capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
|
|
272
|
+
quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
|
|
273
|
+
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
274
|
+
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
275
|
+
**RecordSupport.scalar_value_evidences(kwargs)
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# A registry-owned offering with canonical identity and captured callable.
|
|
281
|
+
# Only Inventory::Publisher/Registry constructs it. See section 10.2.
|
|
282
|
+
OfferingRecord = ::Data.define(
|
|
283
|
+
:offering_id, :provider_native_key, :instance_key, :model, :tier, :operation_evidence,
|
|
284
|
+
:capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
285
|
+
:model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :callable_handle,
|
|
286
|
+
:publication_source
|
|
287
|
+
) do
|
|
288
|
+
def initialize(**kwargs)
|
|
289
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
290
|
+
instance_key = kwargs[:instance_key]
|
|
291
|
+
callable_handle = kwargs[:callable_handle]
|
|
292
|
+
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
293
|
+
raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
|
|
294
|
+
|
|
295
|
+
native_key = Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key)
|
|
296
|
+
Identity.validate_offering_id!(value: kwargs[:offering_id], instance_key: instance_key, provider_native_key: native_key)
|
|
297
|
+
|
|
298
|
+
super(
|
|
299
|
+
offering_id: kwargs[:offering_id].dup.freeze,
|
|
300
|
+
provider_native_key: native_key,
|
|
301
|
+
instance_key: instance_key,
|
|
302
|
+
model: Identity.normalize_text(value: kwargs[:model], field: :model),
|
|
303
|
+
tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
|
|
304
|
+
operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
|
|
305
|
+
capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
|
|
306
|
+
quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
|
|
307
|
+
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
308
|
+
callable_handle: callable_handle,
|
|
309
|
+
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
310
|
+
**RecordSupport.scalar_value_evidences(kwargs)
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def supported_operations
|
|
315
|
+
operation_evidence.select { |_op, evidence| evidence.supported? }.keys.sort
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def unsupported_operations
|
|
319
|
+
operation_evidence.select { |_op, evidence| evidence.unsupported? }.keys.sort
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def unknown_operations
|
|
323
|
+
operation_evidence.select { |_op, evidence| evidence.unknown? }.keys.sort
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def operation_status(operation:)
|
|
327
|
+
operation_evidence.fetch(Taxonomies.normalize_operation(value: operation, allow_aliases: false)).status
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def capability_evidence_for(capability:)
|
|
331
|
+
capability_evidence[Legion::Extensions::Llm::Capabilities.canonical(capability)]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def capability_status(capability:)
|
|
335
|
+
evidence = capability_evidence_for(capability: capability)
|
|
336
|
+
evidence.nil? ? :unknown : evidence.status
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def quota_domain(operation:)
|
|
340
|
+
quota_domains[Taxonomies.normalize_operation(value: operation, allow_aliases: false)]
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# An executable lane derived by the registry from one supported operation
|
|
345
|
+
# of an OfferingRecord. See section 10.3.
|
|
346
|
+
LaneRecord = ::Data.define(
|
|
347
|
+
:lane_id, :offering_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
|
|
348
|
+
:capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
|
|
349
|
+
:model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
|
|
350
|
+
:publication_source
|
|
351
|
+
) do
|
|
352
|
+
def initialize(**kwargs)
|
|
353
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
354
|
+
instance_key = kwargs[:instance_key]
|
|
355
|
+
callable_handle = kwargs[:callable_handle]
|
|
356
|
+
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
357
|
+
raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
|
|
358
|
+
|
|
359
|
+
family = RecordSupport.normalize_provider_family(value: kwargs[:provider_family])
|
|
360
|
+
resolved_instance = Identity.normalize_text(value: kwargs[:instance_id], field: :instance_id)
|
|
361
|
+
unless family == instance_key.provider_family && resolved_instance == instance_key.instance_id
|
|
362
|
+
raise Errors::ValidationError, 'provider_family and instance_id must equal instance_key'
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
canonical_model = Identity.normalize_text(value: kwargs[:model], field: :model)
|
|
366
|
+
canonical_operation = Taxonomies.normalize_operation(value: kwargs[:operation], allow_aliases: false)
|
|
367
|
+
Identity.validate_lane_id!(
|
|
368
|
+
value: kwargs[:lane_id], instance_key: instance_key, operation: canonical_operation,
|
|
369
|
+
model: canonical_model, offering_id: kwargs[:offering_id]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
super(
|
|
373
|
+
lane_id: kwargs[:lane_id].dup.freeze,
|
|
374
|
+
offering_id: kwargs[:offering_id].dup.freeze,
|
|
375
|
+
instance_key: instance_key,
|
|
376
|
+
provider_family: family,
|
|
377
|
+
instance_id: resolved_instance,
|
|
378
|
+
model: canonical_model,
|
|
379
|
+
tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
|
|
380
|
+
operation: canonical_operation,
|
|
381
|
+
capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
|
|
382
|
+
quota_domain: kwargs[:quota_domain].nil? ? nil : RecordSupport.validate_quota_domain_value!(value: kwargs[:quota_domain], field: :quota_domain),
|
|
383
|
+
metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
|
|
384
|
+
callable_handle: callable_handle,
|
|
385
|
+
publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
|
|
386
|
+
**RecordSupport.scalar_value_evidences(kwargs)
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The availability state of an exact instance. Carries no cooldown,
|
|
392
|
+
# half-open, latency, success-count, or clock-expiry field. See 10.4.
|
|
393
|
+
AvailabilityFact = ::Data.define(
|
|
394
|
+
:state, :availability_revision, :source, :reason, :observed_at,
|
|
395
|
+
:last_probe_started_at, :last_probe_completed_at, :last_probe_outcome, :unavailable_revision
|
|
396
|
+
) do
|
|
397
|
+
def initialize(**kwargs)
|
|
398
|
+
kwargs = {
|
|
399
|
+
last_probe_started_at: nil, last_probe_completed_at: nil, last_probe_outcome: nil, unavailable_revision: nil
|
|
400
|
+
}.merge(kwargs)
|
|
401
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
402
|
+
|
|
403
|
+
state = kwargs[:state]
|
|
404
|
+
availability_revision = kwargs[:availability_revision]
|
|
405
|
+
unavailable_revision = kwargs[:unavailable_revision]
|
|
406
|
+
raise Errors::ValidationError, 'state must be a Taxonomies::AVAILABILITY_STATES value' unless Taxonomies::AVAILABILITY_STATES.include?(state)
|
|
407
|
+
raise Errors::ValidationError, 'source must be a Taxonomies::AVAILABILITY_SOURCES value' unless Taxonomies::AVAILABILITY_SOURCES.include?(kwargs[:source])
|
|
408
|
+
|
|
409
|
+
RecordSupport.nonnegative_integer(value: availability_revision, field: :availability_revision)
|
|
410
|
+
validate_unavailable_revision!(state, availability_revision, unavailable_revision)
|
|
411
|
+
resolved_outcome = validate_probe_telemetry!(
|
|
412
|
+
kwargs[:last_probe_started_at], kwargs[:last_probe_completed_at], kwargs[:last_probe_outcome]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
super(
|
|
416
|
+
state: state,
|
|
417
|
+
availability_revision: availability_revision,
|
|
418
|
+
source: kwargs[:source],
|
|
419
|
+
reason: RecordSupport.sanitized_reason(value: kwargs[:reason], field: :reason),
|
|
420
|
+
observed_at: RecordSupport.optional_time(value: kwargs[:observed_at], field: :observed_at),
|
|
421
|
+
last_probe_started_at: RecordSupport.optional_time(value: kwargs[:last_probe_started_at], field: :last_probe_started_at),
|
|
422
|
+
last_probe_completed_at: RecordSupport.optional_time(value: kwargs[:last_probe_completed_at], field: :last_probe_completed_at),
|
|
423
|
+
last_probe_outcome: resolved_outcome,
|
|
424
|
+
unavailable_revision: unavailable_revision
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def validate_unavailable_revision!(state, availability_revision, unavailable_revision)
|
|
429
|
+
if state == :unavailable
|
|
430
|
+
raise Errors::ValidationError, 'unavailable state requires unavailable_revision == availability_revision' unless unavailable_revision == availability_revision
|
|
431
|
+
elsif !unavailable_revision.nil?
|
|
432
|
+
raise Errors::ValidationError, 'non-unavailable state requires unavailable_revision: nil'
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def validate_probe_telemetry!(started_at, completed_at, outcome)
|
|
437
|
+
return nil if outcome.nil?
|
|
438
|
+
raise Errors::ValidationError, 'last_probe_outcome must be nil, :success, or :failure' unless Taxonomies::PROBE_OUTCOMES.include?(outcome)
|
|
439
|
+
raise Errors::ValidationError, 'a non-nil last_probe_outcome requires both probe timestamps' unless started_at.is_a?(::Time) && completed_at.is_a?(::Time)
|
|
440
|
+
raise Errors::ValidationError, 'probe completion cannot precede start' if completed_at < started_at
|
|
441
|
+
|
|
442
|
+
outcome
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# The result of a provider-specific safe readiness operation. Contains no
|
|
447
|
+
# model output, prompt, tokens, callable, exception, or transition
|
|
448
|
+
# instruction. See section 10.5.
|
|
449
|
+
ReadinessResult = ::Data.define(:ready, :reason, :metadata) do
|
|
450
|
+
def initialize(ready:, reason:, metadata: {})
|
|
451
|
+
super(
|
|
452
|
+
ready: RecordSupport.boolean!(value: ready, field: :ready),
|
|
453
|
+
reason: RecordSupport.sanitized_reason(value: reason, field: :reason),
|
|
454
|
+
metadata: RecordSupport.frozen_metadata(value: metadata)
|
|
455
|
+
)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def ready?
|
|
459
|
+
ready
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# An activated exact instance's complete published state. Its availability
|
|
464
|
+
# is available or unavailable, never initializing. See section 10.6.
|
|
465
|
+
InstanceRecord = ::Data.define(
|
|
466
|
+
:instance_key, :callable_handle, :availability, :offerings_by_id,
|
|
467
|
+
:publisher_id, :publisher_token_id, :published_sequence, :published_at
|
|
468
|
+
) do
|
|
469
|
+
def initialize(**kwargs)
|
|
470
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
471
|
+
instance_key = kwargs[:instance_key]
|
|
472
|
+
callable_handle = kwargs[:callable_handle]
|
|
473
|
+
availability = kwargs[:availability]
|
|
474
|
+
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
475
|
+
raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
|
|
476
|
+
unless availability.is_a?(AvailabilityFact) && %i[available unavailable].include?(availability.state)
|
|
477
|
+
raise Errors::ValidationError, 'availability must be an available/unavailable AvailabilityFact'
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
super(
|
|
481
|
+
instance_key: instance_key,
|
|
482
|
+
callable_handle: callable_handle,
|
|
483
|
+
availability: availability,
|
|
484
|
+
offerings_by_id: validate_offerings!(kwargs[:offerings_by_id], instance_key, callable_handle),
|
|
485
|
+
publisher_id: nonempty_id!(kwargs[:publisher_id], :publisher_id),
|
|
486
|
+
publisher_token_id: nonempty_id!(kwargs[:publisher_token_id], :publisher_token_id),
|
|
487
|
+
published_sequence: RecordSupport.nonnegative_integer(value: kwargs[:published_sequence], field: :published_sequence),
|
|
488
|
+
published_at: require_time!(kwargs[:published_at], :published_at)
|
|
489
|
+
)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def validate_offerings!(offerings_by_id, instance_key, callable_handle)
|
|
493
|
+
raise Errors::ValidationError, 'offerings_by_id must be a Hash' unless offerings_by_id.is_a?(::Hash)
|
|
494
|
+
|
|
495
|
+
offerings_by_id.each do |offering_id, offering|
|
|
496
|
+
raise Errors::ValidationError, "offering #{offering_id} must be an OfferingRecord" unless offering.is_a?(OfferingRecord)
|
|
497
|
+
raise Errors::ValidationError, "offering #{offering_id} key mismatch" unless offering.offering_id == offering_id
|
|
498
|
+
unless offering.instance_key == instance_key && offering.callable_handle.equal?(callable_handle)
|
|
499
|
+
raise Errors::ValidationError, "offering #{offering_id} must share the instance callable/key"
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
offerings_by_id.dup.freeze
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def nonempty_id!(value, field)
|
|
506
|
+
raise Errors::ValidationError, "#{field} must be a nonempty String" unless value.is_a?(::String) && !value.empty?
|
|
507
|
+
|
|
508
|
+
value.dup.freeze
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def require_time!(value, field)
|
|
512
|
+
raise Errors::ValidationError, "#{field} must be a Time" unless value.is_a?(::Time)
|
|
513
|
+
|
|
514
|
+
value.dup.freeze
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Publication-scope status distinguishing an initializing claim from an
|
|
519
|
+
# authoritative complete catalog. See section 10.7.
|
|
520
|
+
PublicationStatus = ::Data.define(
|
|
521
|
+
:instance_key, :state, :publisher_token_id, :published_sequence,
|
|
522
|
+
:last_probe_started_at, :last_probe_completed_at, :last_probe_outcome, :last_error
|
|
523
|
+
) do
|
|
524
|
+
def initialize(**kwargs)
|
|
525
|
+
kwargs = {
|
|
526
|
+
last_probe_started_at: nil, last_probe_completed_at: nil, last_probe_outcome: nil, last_error: nil
|
|
527
|
+
}.merge(kwargs)
|
|
528
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
529
|
+
|
|
530
|
+
instance_key = kwargs[:instance_key]
|
|
531
|
+
published_sequence = kwargs[:published_sequence]
|
|
532
|
+
last_probe_outcome = kwargs[:last_probe_outcome]
|
|
533
|
+
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
534
|
+
raise Errors::ValidationError, 'state must be :initializing or :complete' unless Taxonomies::PUBLICATION_STATES.include?(kwargs[:state])
|
|
535
|
+
raise Errors::ValidationError, 'published_sequence must be nil or a nonnegative Integer' unless published_sequence.nil? || RecordSupport.nonnegative_integer?(published_sequence)
|
|
536
|
+
raise Errors::ValidationError, 'last_probe_outcome must be nil, :success, or :failure' unless last_probe_outcome.nil? || Taxonomies::PROBE_OUTCOMES.include?(last_probe_outcome)
|
|
537
|
+
|
|
538
|
+
super(
|
|
539
|
+
instance_key: instance_key,
|
|
540
|
+
state: kwargs[:state],
|
|
541
|
+
publisher_token_id: kwargs[:publisher_token_id].nil? ? nil : kwargs[:publisher_token_id].dup.freeze,
|
|
542
|
+
published_sequence: published_sequence,
|
|
543
|
+
last_probe_started_at: RecordSupport.optional_time(value: kwargs[:last_probe_started_at], field: :last_probe_started_at),
|
|
544
|
+
last_probe_completed_at: RecordSupport.optional_time(value: kwargs[:last_probe_completed_at], field: :last_probe_completed_at),
|
|
545
|
+
last_probe_outcome: last_probe_outcome,
|
|
546
|
+
last_error: RecordSupport.optional_sanitized_reason(value: kwargs[:last_error], field: :last_error)
|
|
547
|
+
)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# The outcome of a registry mutation. Expected stale results use
|
|
552
|
+
# applied: false; every other reason requires applied: true. See 10.8.
|
|
553
|
+
MutationResult = ::Data.define(
|
|
554
|
+
:applied, :reason, :generation, :instance_key, :publication_status, :instance_record
|
|
555
|
+
) do
|
|
556
|
+
def initialize(**kwargs)
|
|
557
|
+
kwargs = { publication_status: nil, instance_record: nil }.merge(kwargs)
|
|
558
|
+
RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
|
|
559
|
+
|
|
560
|
+
applied = kwargs[:applied]
|
|
561
|
+
reason = kwargs[:reason]
|
|
562
|
+
instance_key = kwargs[:instance_key]
|
|
563
|
+
publication_status = kwargs[:publication_status]
|
|
564
|
+
instance_record = kwargs[:instance_record]
|
|
565
|
+
RecordSupport.boolean!(value: applied, field: :applied)
|
|
566
|
+
raise Errors::ValidationError, 'reason must be a Taxonomies::MUTATION_REASONS value' unless Taxonomies::MUTATION_REASONS.include?(reason)
|
|
567
|
+
|
|
568
|
+
validate_applied_reason!(applied, reason)
|
|
569
|
+
raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
|
|
570
|
+
raise Errors::ValidationError, 'publication_status must be a PublicationStatus or nil' unless publication_status.nil? || publication_status.is_a?(PublicationStatus)
|
|
571
|
+
raise Errors::ValidationError, 'instance_record must be an InstanceRecord or nil' unless instance_record.nil? || instance_record.is_a?(InstanceRecord)
|
|
572
|
+
|
|
573
|
+
super(
|
|
574
|
+
applied: applied,
|
|
575
|
+
reason: reason,
|
|
576
|
+
generation: RecordSupport.nonnegative_integer(value: kwargs[:generation], field: :generation),
|
|
577
|
+
instance_key: instance_key,
|
|
578
|
+
publication_status: publication_status,
|
|
579
|
+
instance_record: instance_record
|
|
580
|
+
)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def applied?
|
|
584
|
+
applied
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def validate_applied_reason!(applied, reason)
|
|
588
|
+
non_applied = %i[stale_publisher stale_probe already_removed]
|
|
589
|
+
if applied
|
|
590
|
+
raise Errors::ValidationError, "applied result cannot use reason #{reason}" if non_applied.include?(reason)
|
|
591
|
+
elsif !non_applied.include?(reason)
|
|
592
|
+
raise Errors::ValidationError, "non-applied result requires a stale/already-removed reason, got #{reason}"
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|