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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -0
  3. data/RULES.md +97 -0
  4. data/lib/legion/extensions/llm/auto_registration.rb +4 -13
  5. data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +52 -67
  8. data/lib/legion/extensions/llm/canonical/params.rb +59 -33
  9. data/lib/legion/extensions/llm/canonical/request.rb +67 -54
  10. data/lib/legion/extensions/llm/canonical/response.rb +61 -76
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
  13. data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
  14. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  15. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  16. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  17. data/lib/legion/extensions/llm/canonical.rb +6 -5
  18. data/lib/legion/extensions/llm/configuration.rb +40 -10
  19. data/lib/legion/extensions/llm/connection.rb +8 -30
  20. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  21. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  22. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  23. data/lib/legion/extensions/llm/error.rb +0 -14
  24. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  25. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  26. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  27. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  28. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -153
  29. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  30. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
  31. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  32. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  33. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  34. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  35. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  36. data/lib/legion/extensions/llm/inventory/records.rb +82 -99
  37. data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
  38. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  39. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
  40. data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
  41. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
  42. data/lib/legion/extensions/llm/provider.rb +192 -315
  43. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  44. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  45. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  46. data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
  47. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  48. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  49. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  50. data/lib/legion/extensions/llm/streaming.rb +50 -35
  51. data/lib/legion/extensions/llm/taxonomies.rb +29 -17
  52. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  53. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  54. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  55. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  56. data/lib/legion/extensions/llm/utils.rb +23 -5
  57. data/lib/legion/extensions/llm/version.rb +1 -1
  58. data/lib/legion/extensions/llm.rb +10 -98
  59. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  60. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  61. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  62. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  63. data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
  64. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  65. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  66. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
  67. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  68. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  69. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  70. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  71. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  72. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  73. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  74. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  75. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
  76. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
  77. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  78. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  79. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  80. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  81. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +178 -67
  82. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  83. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
  84. data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
  85. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  86. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  87. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  88. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  89. data/spec/legion/extensions/llm/inventory/records_spec.rb +129 -41
  90. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
  91. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  92. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  93. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +312 -0
  94. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
  95. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
  96. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  97. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  98. data/spec/legion/extensions/llm/provider_spec.rb +98 -78
  99. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  100. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  101. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  102. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -16
  103. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  104. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  105. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  106. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  107. data/spec/support/fake_llm_provider.rb +45 -39
  108. data/spec/support/fake_ssot_harness.rb +7 -2
  109. data/spec/support/ssot_registry_helpers.rb +3 -2
  110. metadata +15 -54
  111. data/lib/legion/extensions/llm/agent.rb +0 -366
  112. data/lib/legion/extensions/llm/aliases.json +0 -436
  113. data/lib/legion/extensions/llm/aliases.rb +0 -67
  114. data/lib/legion/extensions/llm/attachment.rb +0 -229
  115. data/lib/legion/extensions/llm/chat.rb +0 -354
  116. data/lib/legion/extensions/llm/chunk.rb +0 -10
  117. data/lib/legion/extensions/llm/content.rb +0 -81
  118. data/lib/legion/extensions/llm/context.rb +0 -33
  119. data/lib/legion/extensions/llm/embedding.rb +0 -33
  120. data/lib/legion/extensions/llm/image.rb +0 -109
  121. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  122. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -311
  123. data/lib/legion/extensions/llm/message.rb +0 -118
  124. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  125. data/lib/legion/extensions/llm/model/info.rb +0 -286
  126. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  127. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  128. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  129. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  130. data/lib/legion/extensions/llm/model.rb +0 -11
  131. data/lib/legion/extensions/llm/models.json +0 -57313
  132. data/lib/legion/extensions/llm/models.rb +0 -530
  133. data/lib/legion/extensions/llm/models_schema.json +0 -168
  134. data/lib/legion/extensions/llm/moderation.rb +0 -60
  135. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  136. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  137. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  138. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  139. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  140. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  141. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  142. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  143. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  144. data/lib/legion/extensions/llm/thinking.rb +0 -53
  145. data/lib/legion/extensions/llm/tokens.rb +0 -51
  146. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  147. data/lib/legion/extensions/llm/transcription.rb +0 -39
  148. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  149. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  150. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  151. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  152. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  153. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  154. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -337
  155. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  156. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  157. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  158. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  159. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  160. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  161. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  162. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  163. 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