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,241 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Routing
|
|
7
|
-
# DEPRECATED (Phase 4 removal): kept unchanged for released consumers.
|
|
8
|
-
# New providers publish Inventory::OfferingDraft; the new registry accepts
|
|
9
|
-
# no default generated offering_id from this class.
|
|
10
|
-
# Describes one concrete model made available by one provider instance.
|
|
11
|
-
class ModelOffering
|
|
12
|
-
CAPABILITY_ALIASES = {
|
|
13
|
-
function_calling: :tools,
|
|
14
|
-
functions: :tools,
|
|
15
|
-
tool: :tools,
|
|
16
|
-
tool_use: :tools,
|
|
17
|
-
stream: :streaming,
|
|
18
|
-
stream_chat: :streaming
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
attr_reader :offering_id, :provider_family, :model_family, :provider_instance, :instance_id, :transport,
|
|
22
|
-
:tier, :model, :canonical_model_alias, :routing_metadata, :usage_type, :capabilities,
|
|
23
|
-
:capability_sources, :limits, :credentials, :health, :cost, :policy_tags, :metadata
|
|
24
|
-
|
|
25
|
-
def initialize(data)
|
|
26
|
-
@metadata = normalize_hash(fetch_value(data, :metadata))
|
|
27
|
-
@provider_family = normalize_symbol(fetch_value(data, :provider_family, fetch_value(data, :provider)))
|
|
28
|
-
@model_family = normalize_symbol(fetch_value(data, :model_family, @metadata[:model_family]))
|
|
29
|
-
@provider_instance = normalize_symbol(fetch_value(data, :provider_instance,
|
|
30
|
-
fetch_value(data, :instance_id, @provider_family)))
|
|
31
|
-
@instance_id = @provider_instance
|
|
32
|
-
@transport = normalize_symbol(fetch_value(data, :transport, :http))
|
|
33
|
-
@tier = normalize_symbol(fetch_value(data, :tier, default_tier))
|
|
34
|
-
@model = fetch_value(data, :model).to_s
|
|
35
|
-
@canonical_model_alias = normalize_model_alias(fetch_value(data, :canonical_model_alias,
|
|
36
|
-
metadata_canonical_model_alias))
|
|
37
|
-
@routing_metadata = normalize_hash(fetch_value(data, :routing_metadata))
|
|
38
|
-
@usage_type = normalize_usage_type(fetch_value(data, :usage_type,
|
|
39
|
-
fetch_value(data, :type) ||
|
|
40
|
-
fetch_value(data, :kind) ||
|
|
41
|
-
infer_usage_type(data)))
|
|
42
|
-
@capabilities = normalize_capabilities(fetch_value(data, :capabilities))
|
|
43
|
-
@capability_sources = normalize_capability_sources(fetch_value(data, :capability_sources))
|
|
44
|
-
@limits = normalize_hash(fetch_value(data, :limits))
|
|
45
|
-
@credentials = fetch_value(data, :credentials)
|
|
46
|
-
@health = normalize_hash(fetch_value(data, :health))
|
|
47
|
-
@cost = normalize_hash(fetch_value(data, :cost))
|
|
48
|
-
@policy_tags = normalize_array(fetch_value(data, :policy_tags)).map(&:to_sym)
|
|
49
|
-
@offering_id = normalize_offering_id(fetch_value(data, :offering_id, default_offering_id))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def enabled?
|
|
53
|
-
!metadata.key?(:enabled) || metadata[:enabled] != false
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def embedding?
|
|
57
|
-
usage_type == :embedding
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def inference?
|
|
61
|
-
%i[chat inference completion].include?(usage_type)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def context_window
|
|
65
|
-
integer_limit(:context_window) || integer_limit(:max_input_tokens)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def max_output_tokens
|
|
69
|
-
integer_limit(:max_output_tokens)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def supports?(capability)
|
|
73
|
-
normalize_capabilities([capability]).any? { |candidate| capabilities.include?(candidate) }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def eligible_for?(usage_type: nil, required_capabilities: [], min_context_window: nil, policy_tags: [])
|
|
77
|
-
return false unless enabled?
|
|
78
|
-
return false unless usage_type_matches?(usage_type)
|
|
79
|
-
return false unless capabilities_match?(required_capabilities)
|
|
80
|
-
return false unless context_window_matches?(min_context_window)
|
|
81
|
-
return false unless policy_tags_match?(policy_tags)
|
|
82
|
-
|
|
83
|
-
true
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def lane_key(prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
|
|
87
|
-
LaneKey.for(self, prefix:, include_context:, include_fingerprint:)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def eligibility_fingerprint
|
|
91
|
-
LaneKey.eligibility_fingerprint(self)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def model_alias?(alias_name)
|
|
95
|
-
normalized = normalize_model_alias(alias_name)
|
|
96
|
-
[canonical_model_alias, model].compact.any? { |candidate| normalize_model_alias(candidate) == normalized }
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def to_h
|
|
100
|
-
{
|
|
101
|
-
offering_id: offering_id,
|
|
102
|
-
provider_family: provider_family,
|
|
103
|
-
model_family: model_family,
|
|
104
|
-
provider_instance: provider_instance,
|
|
105
|
-
instance_id: instance_id,
|
|
106
|
-
transport: transport,
|
|
107
|
-
tier: tier,
|
|
108
|
-
model: model,
|
|
109
|
-
canonical_model_alias: canonical_model_alias,
|
|
110
|
-
routing_metadata: routing_metadata,
|
|
111
|
-
usage_type: usage_type,
|
|
112
|
-
capabilities: capabilities,
|
|
113
|
-
capability_sources: capability_sources,
|
|
114
|
-
limits: limits,
|
|
115
|
-
credentials: credentials,
|
|
116
|
-
health: health,
|
|
117
|
-
cost: cost,
|
|
118
|
-
policy_tags: policy_tags,
|
|
119
|
-
metadata: metadata
|
|
120
|
-
}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def default_tier
|
|
126
|
-
case @transport
|
|
127
|
-
when :local
|
|
128
|
-
:local
|
|
129
|
-
when :rabbitmq
|
|
130
|
-
:fleet
|
|
131
|
-
else
|
|
132
|
-
:private
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def infer_usage_type(data)
|
|
137
|
-
capabilities = normalize_capabilities(fetch_value(data, :capabilities))
|
|
138
|
-
return :embedding if capabilities.include?(:embedding) || capabilities.include?(:embed)
|
|
139
|
-
|
|
140
|
-
:inference
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def normalize_usage_type(value)
|
|
144
|
-
case value.to_sym
|
|
145
|
-
when :embed, :embeddings
|
|
146
|
-
:embedding
|
|
147
|
-
when :completion, :text, :chat
|
|
148
|
-
:inference
|
|
149
|
-
else
|
|
150
|
-
value.to_sym
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def normalize_symbol(value)
|
|
155
|
-
return nil if value.nil?
|
|
156
|
-
|
|
157
|
-
value.to_sym
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def normalize_array(value)
|
|
161
|
-
Array(value).compact.map(&:to_sym)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def normalize_capabilities(value)
|
|
165
|
-
Legion::Extensions::Llm::Capabilities.normalize(value)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def normalize_capability_sources(value)
|
|
169
|
-
normalize_hash(value).to_h do |capability, source_data|
|
|
170
|
-
normalized_source = normalize_hash(source_data)
|
|
171
|
-
[
|
|
172
|
-
Legion::Extensions::Llm::Capabilities.canonical(capability),
|
|
173
|
-
{ value: normalized_source[:value], source: normalized_source[:source]&.to_sym }.compact
|
|
174
|
-
]
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def normalize_hash(value)
|
|
179
|
-
(value || {}).to_h.transform_keys(&:to_sym)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def fetch_value(hash, key, default = nil)
|
|
183
|
-
return default unless hash.respond_to?(:key?)
|
|
184
|
-
|
|
185
|
-
string_key = key.to_s
|
|
186
|
-
return hash[string_key] if hash.key?(string_key)
|
|
187
|
-
|
|
188
|
-
hash.key?(key) ? hash[key] : default
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def usage_type_matches?(expected)
|
|
192
|
-
expected.nil? || normalize_usage_type(expected) == usage_type
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def capabilities_match?(required)
|
|
196
|
-
normalize_capabilities(required).all? { |capability| capabilities.include?(capability) }
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def context_window_matches?(minimum)
|
|
200
|
-
minimum.nil? || (!!context_window && context_window >= minimum.to_i)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def policy_tags_match?(required)
|
|
204
|
-
Array(required).all? { |tag| policy_tags.include?(tag.to_sym) }
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def integer_limit(key)
|
|
208
|
-
value = limits[key]
|
|
209
|
-
return nil if value.nil?
|
|
210
|
-
|
|
211
|
-
Integer(value)
|
|
212
|
-
rescue ArgumentError, TypeError
|
|
213
|
-
nil
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def metadata_canonical_model_alias
|
|
217
|
-
metadata[:canonical_model_alias] || metadata[:alias] ||
|
|
218
|
-
Legion::Extensions::Llm::Aliases.canonical_model_alias(@model, @provider_family)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def normalize_model_alias(value)
|
|
222
|
-
Legion::Extensions::Llm::Aliases.normalize_model_alias(value)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def normalize_offering_id(value)
|
|
226
|
-
value.to_s.strip
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def default_offering_id
|
|
230
|
-
[
|
|
231
|
-
provider_family,
|
|
232
|
-
provider_instance,
|
|
233
|
-
usage_type,
|
|
234
|
-
canonical_model_alias || model
|
|
235
|
-
].compact.map { |part| LaneKey.model_slug(part) }.join(':')
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Routing
|
|
7
|
-
# DEPRECATED (Phase 4 removal): the old mutable registry, kept only for
|
|
8
|
-
# old callers. It is never returned by Inventory::Registry.snapshot.
|
|
9
|
-
# In-memory index of provider-neutral model offerings.
|
|
10
|
-
class OfferingRegistry
|
|
11
|
-
include Enumerable
|
|
12
|
-
|
|
13
|
-
def initialize(offerings = [])
|
|
14
|
-
@offerings = []
|
|
15
|
-
Array(offerings).each { |offering| register(offering) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def register(offering)
|
|
19
|
-
normalized = normalize_offering(offering)
|
|
20
|
-
@offerings.reject! { |existing| existing.offering_id == normalized.offering_id }
|
|
21
|
-
@offerings << normalized
|
|
22
|
-
normalized
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def each(&)
|
|
26
|
-
@offerings.each(&)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def all
|
|
30
|
-
@offerings.dup
|
|
31
|
-
end
|
|
32
|
-
alias list all
|
|
33
|
-
|
|
34
|
-
def find(offering_id)
|
|
35
|
-
@offerings.find { |offering| offering.offering_id == offering_id.to_s }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def find_by_model_alias(alias_name)
|
|
39
|
-
@offerings.find { |offering| offering.model_alias?(alias_name) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def filter(**criteria)
|
|
43
|
-
@offerings.select do |offering|
|
|
44
|
-
matches_symbol?(offering.provider_family, criteria[:provider_family]) &&
|
|
45
|
-
matches_symbol?(offering.model_family, criteria[:model_family]) &&
|
|
46
|
-
matches_symbol?(offering.provider_instance, criteria[:provider_instance]) &&
|
|
47
|
-
matches_capability?(offering, criteria[:capability]) &&
|
|
48
|
-
matches_model_alias?(offering, criteria[:model_alias]) &&
|
|
49
|
-
matches_model?(offering, criteria[:model]) &&
|
|
50
|
-
matches_usage_type?(offering, criteria[:usage_type])
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def by_provider_family(provider_family)
|
|
55
|
-
filter(provider_family:)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def by_model_family(model_family)
|
|
59
|
-
filter(model_family:)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def by_provider_instance(provider_instance)
|
|
63
|
-
filter(provider_instance:)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def by_capability(capability)
|
|
67
|
-
filter(capability:)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def normalize_offering(offering)
|
|
73
|
-
return offering if offering.is_a?(ModelOffering)
|
|
74
|
-
|
|
75
|
-
ModelOffering.new(offering)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def matches_symbol?(actual, expected)
|
|
79
|
-
expected.nil? || actual == expected.to_sym
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def matches_capability?(offering, capability)
|
|
83
|
-
capability.nil? || offering.supports?(capability)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def matches_model_alias?(offering, model_alias)
|
|
87
|
-
model_alias.nil? || offering.model_alias?(model_alias)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def matches_model?(offering, model)
|
|
91
|
-
model.nil? || offering.model == model.to_s
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def matches_usage_type?(offering, usage_type)
|
|
95
|
-
usage_type.nil? || offering.usage_type == usage_type.to_sym
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Routing
|
|
7
|
-
# Serializable provider-neutral envelope for future llm.registry publishing.
|
|
8
|
-
class RegistryEvent
|
|
9
|
-
EVENT_TYPES = %i[
|
|
10
|
-
offering_available
|
|
11
|
-
offering_unavailable
|
|
12
|
-
offering_degraded
|
|
13
|
-
offering_heartbeat
|
|
14
|
-
].freeze
|
|
15
|
-
SENSITIVE_KEYS = %i[
|
|
16
|
-
access_key
|
|
17
|
-
api_key
|
|
18
|
-
authorization
|
|
19
|
-
bearer
|
|
20
|
-
client_secret
|
|
21
|
-
credential
|
|
22
|
-
credentials
|
|
23
|
-
endpoint
|
|
24
|
-
endpoint_url
|
|
25
|
-
password
|
|
26
|
-
path
|
|
27
|
-
private_key
|
|
28
|
-
prompt
|
|
29
|
-
reply_to
|
|
30
|
-
secret
|
|
31
|
-
secrets
|
|
32
|
-
token
|
|
33
|
-
url
|
|
34
|
-
].freeze
|
|
35
|
-
|
|
36
|
-
attr_reader :event_id, :event_type, :occurred_at, :offering, :runtime, :capacity, :health, :lane, :metadata
|
|
37
|
-
|
|
38
|
-
class << self
|
|
39
|
-
def available(offering, **attributes)
|
|
40
|
-
new(event_type: :offering_available, offering:, **attributes)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def unavailable(offering, **attributes)
|
|
44
|
-
new(event_type: :offering_unavailable, offering:, **attributes)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def degraded(offering, **attributes)
|
|
48
|
-
new(event_type: :offering_degraded, offering:, **attributes)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def heartbeat(offering, **attributes)
|
|
52
|
-
new(event_type: :offering_heartbeat, offering:, **attributes)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def initialize(event_type:, offering:, **attributes)
|
|
57
|
-
@event_id = normalize_event_id(attributes.fetch(:event_id, SecureRandom.uuid))
|
|
58
|
-
@event_type = normalize_event_type(event_type)
|
|
59
|
-
@occurred_at = normalize_time(attributes.fetch(:occurred_at, Time.now.utc))
|
|
60
|
-
@offering = normalize_offering(offering)
|
|
61
|
-
@runtime = sanitize_optional_hash(attributes[:runtime], :runtime)
|
|
62
|
-
@capacity = sanitize_optional_hash(attributes[:capacity], :capacity)
|
|
63
|
-
@health = sanitize_optional_hash(attributes[:health], :health)
|
|
64
|
-
@lane = sanitize_optional_value(attributes[:lane], :lane)
|
|
65
|
-
@metadata = sanitize_optional_hash(attributes[:metadata], :metadata)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def to_h
|
|
69
|
-
{
|
|
70
|
-
event_id: event_id,
|
|
71
|
-
event_type: event_type,
|
|
72
|
-
occurred_at: occurred_at.utc.iso8601(6),
|
|
73
|
-
offering: sanitized_offering_hash,
|
|
74
|
-
runtime: runtime,
|
|
75
|
-
capacity: capacity,
|
|
76
|
-
health: health,
|
|
77
|
-
lane: lane,
|
|
78
|
-
metadata: metadata
|
|
79
|
-
}.compact
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def normalize_event_id(value)
|
|
85
|
-
normalized = value.to_s.strip
|
|
86
|
-
raise ArgumentError, 'event_id is required' if normalized.empty?
|
|
87
|
-
|
|
88
|
-
normalized
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def normalize_event_type(value)
|
|
92
|
-
normalized = value.to_sym
|
|
93
|
-
raise ArgumentError, "unsupported registry event type: #{value}" unless EVENT_TYPES.include?(normalized)
|
|
94
|
-
|
|
95
|
-
normalized
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def normalize_time(value)
|
|
99
|
-
return value.utc if value.respond_to?(:utc)
|
|
100
|
-
|
|
101
|
-
Time.parse(value.to_s).utc
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def normalize_offering(value)
|
|
105
|
-
return value if value.is_a?(ModelOffering)
|
|
106
|
-
|
|
107
|
-
ModelOffering.new(value)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def sanitized_offering_hash
|
|
111
|
-
sanitize_hash(offering.to_h, on_sensitive: :drop)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def sanitize_optional_hash(value, label)
|
|
115
|
-
return nil if value.nil?
|
|
116
|
-
|
|
117
|
-
sanitize_hash(value.to_h, label:)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def sanitize_optional_value(value, label)
|
|
121
|
-
return nil if value.nil?
|
|
122
|
-
return sanitize_hash(value.to_h, label:) if value.respond_to?(:to_h)
|
|
123
|
-
return value unless value.is_a?(Array)
|
|
124
|
-
|
|
125
|
-
sanitize_array(value, label:, path: [])
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def sanitize_hash(hash, label: nil, path: [], on_sensitive: :raise)
|
|
129
|
-
hash.each_with_object({}) do |(key, value), sanitized|
|
|
130
|
-
normalized_key = key.to_sym
|
|
131
|
-
key_path = path + [normalized_key]
|
|
132
|
-
if sensitive_key?(normalized_key)
|
|
133
|
-
raise_sensitive_key!(label, key_path) if on_sensitive == :raise
|
|
134
|
-
|
|
135
|
-
next
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
sanitized[normalized_key] = sanitize_value(value, label:, path: key_path, on_sensitive:)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def sanitize_array(array, label:, path:, on_sensitive: :raise)
|
|
143
|
-
array.map { |value| sanitize_value(value, label:, path:, on_sensitive:) }
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def sanitize_value(value, label:, path:, on_sensitive:)
|
|
147
|
-
return sanitize_hash(value, label:, path:, on_sensitive:) if value.is_a?(Hash)
|
|
148
|
-
return sanitize_array(value, label:, path:, on_sensitive:) if value.is_a?(Array)
|
|
149
|
-
|
|
150
|
-
value
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def sensitive_key?(key)
|
|
154
|
-
normalized = key.to_s.downcase.gsub(/[^a-z0-9]+/, '_').to_sym
|
|
155
|
-
SENSITIVE_KEYS.include?(normalized) ||
|
|
156
|
-
normalized.to_s.end_with?('_key', '_secret', '_token', '_password')
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def raise_sensitive_key!(label, path)
|
|
160
|
-
prefix = label ? "#{label} contains" : 'registry event contains'
|
|
161
|
-
raise ArgumentError, "#{prefix} sensitive key: #{path.join('.')}"
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Represents provider thinking output.
|
|
7
|
-
class Thinking
|
|
8
|
-
attr_reader :text, :signature
|
|
9
|
-
|
|
10
|
-
def initialize(text: nil, signature: nil)
|
|
11
|
-
@text = text
|
|
12
|
-
@signature = signature
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.build(text: nil, signature: nil)
|
|
16
|
-
text = nil if text.is_a?(String) && text.empty?
|
|
17
|
-
signature = nil if signature.is_a?(String) && signature.empty?
|
|
18
|
-
|
|
19
|
-
return nil if text.nil? && signature.nil?
|
|
20
|
-
|
|
21
|
-
new(text: text, signature: signature)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def pretty_print(printer)
|
|
25
|
-
printer.object_group(self) do
|
|
26
|
-
printer.breakable
|
|
27
|
-
printer.text 'text='
|
|
28
|
-
printer.pp text
|
|
29
|
-
printer.comma_breakable
|
|
30
|
-
printer.text 'signature='
|
|
31
|
-
printer.pp(signature ? '[REDACTED]' : nil)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
class Thinking
|
|
37
|
-
# Normalized config for thinking across providers.
|
|
38
|
-
class Config
|
|
39
|
-
attr_reader :effort, :budget
|
|
40
|
-
|
|
41
|
-
def initialize(effort: nil, budget: nil)
|
|
42
|
-
@effort = effort.is_a?(Symbol) ? effort.to_s : effort
|
|
43
|
-
@budget = budget
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def enabled?
|
|
47
|
-
!effort.nil? || !budget.nil?
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Represents token usage for a response.
|
|
7
|
-
class Tokens
|
|
8
|
-
attr_reader :input, :output, :cached, :cache_creation, :thinking
|
|
9
|
-
|
|
10
|
-
# rubocop:disable Metrics/ParameterLists
|
|
11
|
-
def initialize(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
|
|
12
|
-
@input = input
|
|
13
|
-
@output = output
|
|
14
|
-
@cached = cached
|
|
15
|
-
@cache_creation = cache_creation
|
|
16
|
-
@thinking = thinking || reasoning
|
|
17
|
-
end
|
|
18
|
-
# rubocop:enable Metrics/ParameterLists
|
|
19
|
-
|
|
20
|
-
# rubocop:disable Metrics/ParameterLists
|
|
21
|
-
def self.build(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
|
|
22
|
-
return nil if [input, output, cached, cache_creation, thinking, reasoning].all?(&:nil?)
|
|
23
|
-
|
|
24
|
-
new(
|
|
25
|
-
input: input,
|
|
26
|
-
output: output,
|
|
27
|
-
cached: cached,
|
|
28
|
-
cache_creation: cache_creation,
|
|
29
|
-
thinking: thinking,
|
|
30
|
-
reasoning: reasoning
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
# rubocop:enable Metrics/ParameterLists
|
|
34
|
-
|
|
35
|
-
def to_h
|
|
36
|
-
{
|
|
37
|
-
input_tokens: input,
|
|
38
|
-
output_tokens: output,
|
|
39
|
-
cached_tokens: cached,
|
|
40
|
-
cache_creation_tokens: cache_creation,
|
|
41
|
-
thinking_tokens: thinking
|
|
42
|
-
}.compact
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def reasoning
|
|
46
|
-
thinking
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Represents a function call from an AI model to a Tool.
|
|
7
|
-
class ToolCall
|
|
8
|
-
attr_reader :id, :name, :arguments, :index
|
|
9
|
-
attr_accessor :thought_signature
|
|
10
|
-
|
|
11
|
-
# index carries the provider's authoritative wire position
|
|
12
|
-
# (delta.tool_calls[N].index) so the StreamAccumulator can correlate
|
|
13
|
-
# interleaved argument fragments to the right parallel call by index
|
|
14
|
-
# rather than by recency. nil for non-streaming/single-call paths.
|
|
15
|
-
def initialize(id:, name:, arguments: {}, thought_signature: nil, index: nil)
|
|
16
|
-
@id = id
|
|
17
|
-
@name = name
|
|
18
|
-
@arguments = arguments
|
|
19
|
-
@thought_signature = thought_signature
|
|
20
|
-
@index = index
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def to_h
|
|
24
|
-
{
|
|
25
|
-
id: @id,
|
|
26
|
-
name: @name,
|
|
27
|
-
arguments: @arguments,
|
|
28
|
-
thought_signature: @thought_signature
|
|
29
|
-
}.compact
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
# Represents a transcription of audio content.
|
|
7
|
-
class Transcription
|
|
8
|
-
attr_reader :text, :model, :language, :duration, :segments, :input_tokens, :output_tokens
|
|
9
|
-
|
|
10
|
-
def initialize(text:, model:, **attributes)
|
|
11
|
-
@text = text
|
|
12
|
-
@model = model
|
|
13
|
-
@language = attributes[:language]
|
|
14
|
-
@duration = attributes[:duration]
|
|
15
|
-
@segments = attributes[:segments]
|
|
16
|
-
@input_tokens = attributes[:input_tokens]
|
|
17
|
-
@output_tokens = attributes[:output_tokens]
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.transcribe(audio_file, **kwargs)
|
|
21
|
-
model = kwargs.delete(:model)
|
|
22
|
-
language = kwargs.delete(:language)
|
|
23
|
-
provider = kwargs.delete(:provider)
|
|
24
|
-
assume_model_exists = kwargs.delete(:assume_model_exists) { false }
|
|
25
|
-
context = kwargs.delete(:context)
|
|
26
|
-
options = kwargs
|
|
27
|
-
|
|
28
|
-
config = context&.config || Legion::Extensions::Llm.config
|
|
29
|
-
model ||= config.default_transcription_model
|
|
30
|
-
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
31
|
-
config: config)
|
|
32
|
-
model_id = model.id
|
|
33
|
-
|
|
34
|
-
provider_instance.transcribe(audio_file, model: model_id, language:, **options)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|