lex-llm 0.7.6 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -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 +107 -94
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +53 -69
  8. data/lib/legion/extensions/llm/canonical/params.rb +58 -34
  9. data/lib/legion/extensions/llm/canonical/request.rb +68 -56
  10. data/lib/legion/extensions/llm/canonical/response.rb +62 -78
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
  13. data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
  14. data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
  15. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  16. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  17. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  18. data/lib/legion/extensions/llm/canonical.rb +7 -5
  19. data/lib/legion/extensions/llm/configuration.rb +40 -10
  20. data/lib/legion/extensions/llm/connection.rb +8 -30
  21. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  22. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  23. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  24. data/lib/legion/extensions/llm/error.rb +0 -14
  25. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  26. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  27. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  28. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  29. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
  30. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  31. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
  32. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  33. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  34. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  35. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  36. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  37. data/lib/legion/extensions/llm/inventory/records.rb +36 -100
  38. data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
  39. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  40. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
  41. data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
  42. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -182
  43. data/lib/legion/extensions/llm/provider.rb +192 -315
  44. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  45. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  46. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  47. data/lib/legion/extensions/llm/responses/tool_arguments.rb +48 -0
  48. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  49. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  50. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  51. data/lib/legion/extensions/llm/streaming.rb +50 -35
  52. data/lib/legion/extensions/llm/taxonomies.rb +14 -26
  53. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  54. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  55. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  56. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  57. data/lib/legion/extensions/llm/utils.rb +23 -5
  58. data/lib/legion/extensions/llm/version.rb +1 -1
  59. data/lib/legion/extensions/llm.rb +8 -98
  60. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  61. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  62. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  63. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  64. data/spec/legion/extensions/llm/canonical/params_spec.rb +55 -136
  65. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  66. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  67. data/spec/legion/extensions/llm/canonical/thinking/config_spec.rb +222 -0
  68. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
  69. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  70. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  71. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  72. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  73. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  74. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  75. data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +1 -2
  76. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  77. data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
  78. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
  79. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  80. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
  81. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -0
  82. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  83. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  84. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  85. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  86. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
  87. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  88. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
  89. data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
  90. data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
  91. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  92. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  93. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  94. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  95. data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
  96. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
  97. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  98. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  99. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
  100. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
  101. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -68
  102. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  103. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  104. data/spec/legion/extensions/llm/provider_spec.rb +97 -78
  105. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  106. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  107. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  108. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
  109. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  110. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  111. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  112. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  113. data/spec/support/fake_llm_provider.rb +45 -39
  114. data/spec/support/fake_ssot_harness.rb +7 -2
  115. metadata +13 -54
  116. data/lib/legion/extensions/llm/agent.rb +0 -366
  117. data/lib/legion/extensions/llm/aliases.json +0 -436
  118. data/lib/legion/extensions/llm/aliases.rb +0 -67
  119. data/lib/legion/extensions/llm/attachment.rb +0 -229
  120. data/lib/legion/extensions/llm/chat.rb +0 -354
  121. data/lib/legion/extensions/llm/chunk.rb +0 -10
  122. data/lib/legion/extensions/llm/content.rb +0 -81
  123. data/lib/legion/extensions/llm/context.rb +0 -33
  124. data/lib/legion/extensions/llm/embedding.rb +0 -33
  125. data/lib/legion/extensions/llm/image.rb +0 -109
  126. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  127. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
  128. data/lib/legion/extensions/llm/message.rb +0 -118
  129. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  130. data/lib/legion/extensions/llm/model/info.rb +0 -286
  131. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  132. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  133. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  134. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  135. data/lib/legion/extensions/llm/model.rb +0 -11
  136. data/lib/legion/extensions/llm/models.json +0 -57313
  137. data/lib/legion/extensions/llm/models.rb +0 -530
  138. data/lib/legion/extensions/llm/models_schema.json +0 -168
  139. data/lib/legion/extensions/llm/moderation.rb +0 -60
  140. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  141. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  142. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  143. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  144. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  145. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  146. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  147. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  148. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  149. data/lib/legion/extensions/llm/thinking.rb +0 -53
  150. data/lib/legion/extensions/llm/tokens.rb +0 -51
  151. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  152. data/lib/legion/extensions/llm/transcription.rb +0 -39
  153. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  154. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  155. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  156. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  157. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  158. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  159. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
  160. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  161. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  162. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  163. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  164. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  165. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  166. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  167. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  168. data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
@@ -13,7 +13,7 @@ module Legion
13
13
  include Legion::Logging::Helper
14
14
 
15
15
  def basic(&)
16
- logger = faraday_logger
16
+ logger = faraday_logger_for(Legion::Extensions::Llm.config, log)
17
17
  Faraday.new do |f|
18
18
  f.response :logger,
19
19
  logger,
@@ -25,22 +25,20 @@ module Legion
25
25
  yield f if block_given?
26
26
  end
27
27
  end
28
+ end
28
29
 
29
- private
30
-
31
- def faraday_logger
32
- config = Legion::Extensions::Llm.config
33
- return config.logger if config.respond_to?(:logger) && config.logger
30
+ # One Faraday logger resolution (10 U9): the configured logger when
31
+ # present, else the caller's fallback log.
32
+ def self.faraday_logger_for(config_source, fallback_log)
33
+ return config_source.logger if config_source.respond_to?(:logger) && config_source.logger
34
34
 
35
- log
36
- end
35
+ fallback_log
37
36
  end
38
37
 
39
38
  def initialize(provider, config)
40
39
  @provider = provider
41
40
  @config = config
42
41
 
43
- ensure_configured!
44
42
  @connection ||= Faraday.new(provider.api_base) do |faraday|
45
43
  faraday.ssl.verify = false
46
44
  setup_timeout(faraday)
@@ -83,7 +81,7 @@ module Legion
83
81
  end
84
82
 
85
83
  def setup_logging(faraday)
86
- logger = faraday_logger
84
+ logger = self.class.faraday_logger_for(config, log)
87
85
  # Enable request body logging when the logger is at DEBUG level,
88
86
  # or when explicitly enabled via fleet request_payload setting.
89
87
  request_payload = Legion::Extensions::Llm.default_settings.dig(:fleet, :request, :logger, :request_payload)
@@ -100,12 +98,6 @@ module Legion
100
98
  end
101
99
  end
102
100
 
103
- def faraday_logger
104
- return config.logger if config.respond_to?(:logger) && config.logger
105
-
106
- log
107
- end
108
-
109
101
  def debug_logger?(logger)
110
102
  return logger.debug? if logger.respond_to?(:debug?)
111
103
  return logger.level.to_i <= Logger::DEBUG if logger.respond_to?(:level)
@@ -164,20 +156,6 @@ module Legion
164
156
  Legion::Extensions::Llm::OverloadedError
165
157
  ]
166
158
  end
167
-
168
- def ensure_configured!
169
- return if @provider.configured?
170
-
171
- missing = @provider.configuration_requirements.reject { |req| @config.send(req) }
172
- config_block = <<~RUBY
173
- Legion::Extensions::Llm.configure do |config|
174
- #{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
175
- end
176
- RUBY
177
-
178
- raise ConfigurationError,
179
- "#{@provider.name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
180
- end
181
159
  end
182
160
  end
183
161
  end
@@ -58,8 +58,12 @@ module Legion
58
58
  env_hash[key.to_sym] || env_hash[key.to_s]
59
59
  end
60
60
 
61
+ # The codex auth file is an optional source: absent means "no codex
62
+ # credential" (probe semantics); present-but-unreadable raises from
63
+ # read_json (O11 fail-closed).
61
64
  def codex_token
62
65
  return nil unless credential_source_probing_enabled?
66
+ return nil unless File.exist?(CODEX_AUTH)
63
67
 
64
68
  data = read_json(CODEX_AUTH)
65
69
  mode = data[:auth_mode] || data['auth_mode']
@@ -74,6 +78,7 @@ module Legion
74
78
 
75
79
  def codex_openai_key
76
80
  return nil unless credential_source_probing_enabled?
81
+ return nil unless File.exist?(CODEX_AUTH)
77
82
 
78
83
  data = read_json(CODEX_AUTH)
79
84
  val = data[:OPENAI_API_KEY] || data['OPENAI_API_KEY']
@@ -198,19 +203,12 @@ module Legion
198
203
  credential_fingerprint(val)
199
204
  end
200
205
 
201
- # Returns true when the URL points to localhost / 127.0.0.1 / ::1.
206
+ # Returns true when the URL points to localhost / 127.0.0.1 / ::1
207
+ # (the one shared host-locality detector — Utils.localhost_url?).
202
208
  def localhost?(url)
203
209
  return false if url.nil?
204
210
 
205
- uri = URI.parse(url.to_s)
206
- host = uri.host
207
- return false if host.nil?
208
-
209
- normalized = host.delete_prefix('[').delete_suffix(']')
210
- %w[localhost 127.0.0.1 ::1].include?(normalized)
211
- rescue URI::InvalidURIError => e
212
- handle_exception(e, level: :warn, handled: true, operation: 'llm.credential_sources.localhost')
213
- false
211
+ Utils.localhost_url?(url)
214
212
  end
215
213
 
216
214
  module_function :env, :credential_source_probing_enabled?,
@@ -224,69 +222,54 @@ module Legion
224
222
  # --- private helpers -----------------------------------------------
225
223
 
226
224
  # Merge user-level (~/.claude/settings.json) and project-level
227
- # (.claude/settings.json) Claude configs. Project overrides user.
225
+ # (.claude/settings.json) Claude configs. Both files are optional
226
+ # sources: absent means "no config from that level". Project overrides
227
+ # user.
228
228
  def merge_claude_configs
229
- user = read_json(CLAUDE_SETTINGS)
230
- project = read_json(CLAUDE_PROJECT)
231
- deep_merge(user, project)
229
+ user = File.exist?(CLAUDE_SETTINGS) ? read_json(CLAUDE_SETTINGS) : {}
230
+ project = File.exist?(CLAUDE_PROJECT) ? read_json(CLAUDE_PROJECT) : {}
231
+ Utils.deep_merge(user, project)
232
232
  end
233
233
 
234
- # Read and parse a JSON file. Returns an empty hash on any error.
234
+ # Read and parse a JSON file. O11 fail-closed: a missing, empty, or
235
+ # unreadable/unparseable credential file raises — it is a
236
+ # configuration error, never a fabricated empty credential.
235
237
  def read_json(path)
236
- return {} unless File.exist?(path)
238
+ raise ConfigurationError, "credential file is missing: #{path}" unless File.exist?(path)
237
239
 
238
240
  raw = File.read(path)
239
- return {} if raw.strip.empty?
241
+ raise ConfigurationError, "credential file is empty: #{path}" if raw.strip.empty?
240
242
 
241
- if defined?(::Legion::JSON)
242
- ::Legion::JSON.parse(raw, symbolize_names: true)
243
- else
244
- ::JSON.parse(raw, symbolize_names: true)
245
- end
246
- rescue StandardError => e
247
- handle_exception(e, level: :warn, handled: true, operation: 'llm.credential_sources.read_json',
248
- path:)
249
- {}
243
+ # L1: Legion::JSON only (house rule) — the bare ::JSON fallback is
244
+ # deleted; Legion::JSON is a hard dependency of this gem.
245
+ ::Legion::JSON.parse(raw, symbolize_names: true)
250
246
  end
251
247
 
252
248
  # JWT expiry check. Decodes the base64 payload segment and checks
253
- # that exp > now. Returns true on any parse error (benefit of the
254
- # doubt).
249
+ # that exp > now. O11 fail-closed: unreadable or invalid token data
250
+ # is INVALID (the old benefit-of-the-doubt true is deleted). A
251
+ # parseable token with no exp claim has nothing to violate.
255
252
  def token_valid?(token)
256
- return true if token.nil?
257
-
258
253
  require 'base64'
259
- require 'json'
260
254
 
261
255
  parts = token.to_s.split('.')
262
- return true unless parts.length >= 2
256
+ return false if parts.length < 2
263
257
 
264
- payload = ::JSON.parse(Base64.urlsafe_decode64(parts[1]))
258
+ # L1: Legion::JSON only (string keys — the JWT payload is read by
259
+ # string key).
260
+ payload = ::Legion::JSON.parse(Base64.urlsafe_decode64(parts[1]), symbolize_names: false)
265
261
  exp = payload['exp']
266
262
  return true if exp.nil?
267
263
 
268
264
  exp.to_i > Time.now.to_i
269
265
  rescue StandardError => e
270
266
  handle_exception(e, level: :warn, handled: true, operation: 'llm.credential_sources.token_valid')
271
- true
272
- end
273
-
274
- # Simple recursive hash merge (project values override user values).
275
- def deep_merge(base, override)
276
- base.merge(override) do |_key, old_val, new_val|
277
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
278
- deep_merge(old_val, new_val)
279
- else
280
- new_val
281
- end
282
- end
267
+ false
283
268
  end
284
269
 
285
- module_function :merge_claude_configs, :read_json,
286
- :token_valid?, :deep_merge
270
+ module_function :merge_claude_configs, :read_json, :token_valid?
287
271
 
288
- private_class_method :merge_claude_configs, :read_json,
289
- :token_valid?, :deep_merge
272
+ private_class_method :merge_claude_configs, :read_json, :token_valid?
290
273
  end
291
274
  end
292
275
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'legion/extensions/actors/every'
5
+ rescue LoadError => e
6
+ warn(e.message) if $VERBOSE
7
+ end
8
+
9
+ # The base actor inherits the LegionIO time-based Every actor, which only
10
+ # exists inside the daemon. In a standalone lex-llm load (specs, gem require)
11
+ # Every is absent — define nothing rather than crash. Provider actor files
12
+ # carry the same guard.
13
+ #
14
+ # Like the pipeline, this base lives in `discovery/`, NOT the scanned
15
+ # `actors/` dir: a file there would be claimed by the builder as a live
16
+ # per-extension actor (and auto-wrapped in a meta actor). A provider's live
17
+ # actor is its own `<Provider>::Actor::Discovery` (scanned), which subclasses
18
+ # this base with an EMPTY body.
19
+ return unless defined?(Legion::Extensions::Actors::Every)
20
+
21
+ module Legion
22
+ module Extensions
23
+ module Llm
24
+ module Discovery
25
+ # Base periodic discovery actor for EVERY lex-llm-* provider. This is
26
+ # the interface: a provider subclasses it with an EMPTY body and gets
27
+ # discovery for free —
28
+ #
29
+ # module Vllm
30
+ # module Actor
31
+ # class Discovery < Legion::Extensions::Llm::Discovery::Actor; end
32
+ # end
33
+ # end
34
+ #
35
+ # It fires on the configured discovery interval and dispatches each tick
36
+ # to the provider's `Runners::Discovery` module (resolved by convention
37
+ # from this actor's own namespace), which mixes in Discovery::Pipeline
38
+ # and owns the provider-specific fetch/health/evidence/callable. Every
39
+ # generic decision lives in the pipeline; a provider overrides a single
40
+ # method only when its runner lives somewhere non-conventional or its
41
+ # teardown differs.
42
+ #
43
+ # Dispatch contract (Every -> Actors::Base#manual): `use_runner? = false`
44
+ # calls the runner MODULE's `refresh` directly (no Legion::Runner task —
45
+ # the state and the Registry are process-local, so it must run on the
46
+ # owning node); `run_now? = true` fires once on boot then every `time`
47
+ # seconds.
48
+ class Actor < Legion::Extensions::Actors::Every
49
+ include Legion::Extensions::Helpers::Lex
50
+
51
+ def run_now? = true
52
+ def use_runner? = false
53
+ def runner_function = 'refresh'
54
+
55
+ # The provider's discovery runner module, resolved by convention from
56
+ # this actor's namespace: <ProviderModule>::Runners::Discovery. A
57
+ # provider whose runner lives elsewhere overrides this one method.
58
+ def runner_class
59
+ "#{provider_namespace}::Runners::Discovery"
60
+ end
61
+
62
+ # Honor the registered discovery interval. A nil TimerTask interval
63
+ # fires once and then stops, so resolve to the registered default
64
+ # (300s) whenever the setting is missing or non-positive.
65
+ def time
66
+ interval = settings.dig(:instances, :default, :discovery_interval)&.to_i ||
67
+ settings.dig(:discovery, :interval_seconds)&.to_i
68
+ interval&.positive? ? interval : 300
69
+ end
70
+
71
+ def shutdown
72
+ Kernel.const_get(runner_class).remove_all_instances
73
+ rescue StandardError => e
74
+ handle_exception(e, level: :warn, operation: "#{provider_slug}.actor.discovery.shutdown")
75
+ end
76
+
77
+ private
78
+
79
+ # "Legion::Extensions::Llm::Vllm::Actor::Discovery" ->
80
+ # "Legion::Extensions::Llm::Vllm" (drop the trailing Actor::<Name>).
81
+ def provider_namespace
82
+ self.class.name.split('::')[0..-3].join('::')
83
+ end
84
+
85
+ def provider_slug
86
+ provider_namespace.split('::').last.to_s.downcase
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end