legion-llm 0.9.10 → 0.9.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3155d38fd745698ea1445e6d1d8b1824c4ce57c001f107ab2fc0036ee88c48a
4
- data.tar.gz: 0baebdd6be604ae292502adda2168e5eb599cf4fa00e30e5f82b414ad271d016
3
+ metadata.gz: cc06bf006614fbb7a1052b368912ef58744e5b6509680c163c1b7ce0009e87d8
4
+ data.tar.gz: 91c96c5fee56c7fb4804ea3d58eb908cec12539a75eba11348e842be26509343
5
5
  SHA512:
6
- metadata.gz: 7e3112d5a47dddcfe969867afe3e2cccc2d45029628a6b64c3098b7d8be2ba35908823bd9decd062eface61b2b9fded8e29710f8fcca094b00c19311a82eb211
7
- data.tar.gz: 59f1a067e96bb527f46ed5b68f5e03c50aceeb8141ff91c06e89fdedfb17496359a6d50898a6fb2a8c9fd268b596ea23db858f4ee64f7e86ce22e04aefa0c56d
6
+ metadata.gz: fc3d9e91c66c0128bde4ffc434c19e6e75a21cb14a74cafe206235750ef402380a250c928358dfc7702f11f7259ffc46e5976a2abcf360c508bf8ceb7dca6033
7
+ data.tar.gz: 5d1a42619cc8e0d6e086b82245356b69790df2f7601313046e5d09e301ada70483f83ab53b4f6e592f5294c94f631904c7d46847131981542db173950e8e408c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.9.14] - 2026-05-08
4
+
5
+ ### Fixed
6
+ - Guard discovery-first provider inference when the lightweight discovery namespace is loaded before the full discovery cache API.
7
+ - Clean up publisher-identity review follow-up by making request identity fallback explicit and removing unused caller identity requires.
8
+
9
+ ## [0.9.13] - 2026-05-08
10
+
11
+ ### Fixed
12
+ - Allow trigger-matched registry tools to reach native provider dispatch even when `Settings::Extensions` has no always-loaded tools registered.
13
+ - Pass native dispatch options as top-level fleet request parameters so fleet providers receive `system`, `tools`, and offering metadata consistently with direct dispatch.
14
+
15
+ ## [0.9.12] - 2026-05-07
16
+
17
+ ### Fixed
18
+ - Route LLM AMQP publisher identity, API fallback caller identity, prompt audit attribution, and metering attribution through the local `Legion::Identity::Process` identity instead of trusting request-supplied caller hashes.
19
+ - Preserve request caller context separately from publisher identity headers, including prompt audit request caller type, skill events, escalation events, fleet envelopes, reflection ingest metadata, and privacy-blocked audit events.
20
+
21
+ ## [0.9.11] - 2026-05-07
22
+
23
+ ### Fixed
24
+ - `infer_provider_for_model` now consults `Discovery.cached_discovered_models` before falling back to static regex patterns, so models reported by registered lex-llm-* providers route correctly regardless of naming convention.
25
+ - Add Bedrock vendor prefix detection (`anthropic.`, `meta.`, `mistral.`, etc.) before the Ollama catch-all pattern to prevent Bedrock model IDs like `anthropic.claude-opus-4-5-20251101-v1:0` from being misrouted to Ollama due to the `:` in the version suffix.
26
+ - `inferred_provider_tier` now checks `Call::Registry` metadata for the provider's tier before falling back to the static `PROVIDER_TIER` hash.
27
+ - Restore `thinking` option in `native_dispatch_chat_options` where provider dispatch expects it.
28
+
3
29
  ## [0.9.10] - 2026-05-07
4
30
 
5
31
  ### Fixed
@@ -5,6 +5,7 @@ require 'open3'
5
5
  require 'time'
6
6
  require 'legion/cache/helper'
7
7
  require 'legion/logging/helper'
8
+ require 'legion/llm/publisher_identity'
8
9
  require 'legion/llm/types'
9
10
 
10
11
  begin
@@ -390,8 +391,18 @@ module Legion
390
391
 
391
392
  define_method(:identity_canonical_name) do |rack_env|
392
393
  request_identity = identity_request_from_env(rack_env)
393
- name = request_identity&.canonical_name if request_identity.respond_to?(:canonical_name)
394
- return name if name && name.to_s != ''
394
+ if request_identity.respond_to?(:to_caller_hash)
395
+ caller_hash = request_identity.to_caller_hash
396
+ requested_by = nil
397
+ requested_by = caller_hash[:requested_by] || caller_hash['requested_by'] if caller_hash.is_a?(Hash)
398
+ unless Legion::LLM::PublisherIdentity.generic_requested_by?(requested_by)
399
+ name = requested_by[:identity] || requested_by['identity'] if requested_by.respond_to?(:key?)
400
+ return name if name && name.to_s != ''
401
+ end
402
+ end
403
+
404
+ publisher_identity = Legion::LLM::PublisherIdentity.requested_by[:identity]
405
+ return publisher_identity if publisher_identity && publisher_identity.to_s != ''
395
406
 
396
407
  if defined?(Legion::Identity::Process) && Legion::Identity::Process.respond_to?(:canonical_name)
397
408
  process_name = Legion::Identity::Process.canonical_name
@@ -408,16 +419,12 @@ module Legion
408
419
  caller_hash = request_identity.to_caller_hash
409
420
  if caller_hash.is_a?(Hash)
410
421
  requested_by = caller_hash[:requested_by] || caller_hash['requested_by']
411
- return { requested_by: requested_by } if requested_by
422
+ return { requested_by: requested_by } if requested_by && !Legion::LLM::PublisherIdentity.generic_requested_by?(requested_by)
412
423
  end
413
424
  end
414
425
 
415
426
  {
416
- requested_by: {
417
- identity: identity_canonical_name(rack_env),
418
- type: :process,
419
- credential: :system
420
- }
427
+ requested_by: Legion::LLM::PublisherIdentity.requested_by
421
428
  }
422
429
  end
423
430
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/logging/helper'
4
+ require_relative 'publisher_identity'
4
5
 
5
6
  module Legion
6
7
  module LLM
@@ -19,6 +20,7 @@ module Legion
19
20
  module_function
20
21
 
21
22
  def emit_prompt(event)
23
+ event = attributed_event(event)
22
24
  if transport_connected? && defined?(Legion::LLM::Transport::Messages::PromptEvent)
23
25
  Legion::LLM::Transport::Messages::PromptEvent.new(**event).publish
24
26
  log.info('[llm][audit] published prompt audit')
@@ -33,6 +35,7 @@ module Legion
33
35
  end
34
36
 
35
37
  def emit_tools(event)
38
+ event = attributed_event(event)
36
39
  if transport_connected? && defined?(Legion::LLM::Transport::Messages::ToolEvent)
37
40
  Legion::LLM::Transport::Messages::ToolEvent.new(**event).publish
38
41
  log.info('[llm][audit] published tool audit')
@@ -47,6 +50,7 @@ module Legion
47
50
  end
48
51
 
49
52
  def emit_skill(**event)
53
+ event = attributed_event(event)
50
54
  if transport_connected? && defined?(Legion::LLM::Transport::Messages::SkillEvent)
51
55
  Legion::LLM::Transport::Messages::SkillEvent.new(**event).publish
52
56
  log.info('[llm][audit] published skill audit')
@@ -64,6 +68,13 @@ module Legion
64
68
  Legion::LLM::Settings.transport_connected?
65
69
  end
66
70
 
71
+ def attributed_event(event)
72
+ source = event.is_a?(Hash) ? event.dup : {}
73
+ source[:identity] = Legion::LLM::PublisherIdentity.current
74
+ source[:caller] ||= Legion::LLM::PublisherIdentity.caller_hash
75
+ source
76
+ end
77
+
67
78
  # Backward-compat: resolve old Legion::LLM::Audit::Exchange, ::PromptEvent, etc.
68
79
  def self.const_missing(name)
69
80
  case name
@@ -6,6 +6,7 @@ require 'time'
6
6
  require 'legion/extensions/llm/fleet/protocol'
7
7
  require 'legion/logging/helper'
8
8
 
9
+ require_relative '../publisher_identity'
9
10
  require_relative 'token_issuer'
10
11
 
11
12
  module Legion
@@ -15,7 +16,7 @@ module Legion
15
16
  extend Legion::Logging::Helper
16
17
 
17
18
  ENVELOPE_KEYS = %i[
18
- app_id caller correlation_id expires_at idempotency_key message_context operation
19
+ app_id caller correlation_id expires_at idempotency_key identity message_context operation
19
20
  model priority protocol_version provider provider_instance reply_to request_id routing_key
20
21
  signed_token timeout timeout_seconds trace_context ttl
21
22
  ].freeze
@@ -89,6 +90,7 @@ module Legion
89
90
  reply_to: reply_to,
90
91
  message_context: message_context || {},
91
92
  caller: fetch_option(request_opts, :caller) || default_caller,
93
+ identity: Legion::LLM::PublisherIdentity.current,
92
94
  trace_context: fetch_option(request_opts, :trace_context) || {},
93
95
  timeout_seconds: timeout,
94
96
  expires_at: (Time.now.utc + timeout).iso8601,
@@ -293,7 +295,11 @@ module Legion
293
295
  end
294
296
 
295
297
  def default_caller
296
- { source: 'legion-llm', component: 'fleet_dispatcher' }
298
+ {
299
+ source: 'legion-llm',
300
+ component: 'fleet_dispatcher',
301
+ requested_by: Legion::LLM::PublisherIdentity.requested_by
302
+ }
297
303
  end
298
304
  end
299
305
  end
@@ -4,6 +4,7 @@ require 'legion/extensions/llm/fleet/protocol'
4
4
  require 'legion/logging/helper'
5
5
 
6
6
  require_relative '../call/registry'
7
+ require_relative '../publisher_identity'
7
8
  require_relative 'worker_execution'
8
9
 
9
10
  module Legion
@@ -73,6 +74,8 @@ module Legion
73
74
  reply_to: envelope[:reply_to],
74
75
  message_context: envelope[:message_context] || {},
75
76
  trace_context: envelope[:trace_context] || {},
77
+ caller: envelope[:caller],
78
+ identity: Legion::LLM::PublisherIdentity.current,
76
79
  content: response_content(response),
77
80
  tool_calls: response_tool_calls(response),
78
81
  usage: response_usage(response),
@@ -96,12 +99,14 @@ module Legion
96
99
  reply_to: envelope[:reply_to],
97
100
  message_context: envelope[:message_context] || {},
98
101
  trace_context: envelope[:trace_context] || {},
102
+ caller: envelope[:caller],
103
+ identity: Legion::LLM::PublisherIdentity.current,
99
104
  message: error.message,
100
105
  error_class: error.class.name
101
106
  }.compact
102
107
  end
103
108
 
104
- def publish_response(_envelope, result)
109
+ def publish_response(envelope, result)
105
110
  require 'legion/extensions/llm/transport/messages/fleet_response'
106
111
  publish_result = ::Legion::Extensions::Llm::Transport::Messages::FleetResponse.new(
107
112
  protocol_version: result[:protocol_version],
@@ -115,6 +120,8 @@ module Legion
115
120
  reply_to: result[:reply_to],
116
121
  message_context: result[:message_context],
117
122
  trace_context: result[:trace_context],
123
+ caller: envelope[:caller],
124
+ identity: Legion::LLM::PublisherIdentity.current,
118
125
  content: result[:content],
119
126
  tool_calls: result[:tool_calls],
120
127
  usage: result[:usage],
@@ -127,7 +134,7 @@ module Legion
127
134
  handle_exception(e, level: :warn, operation: 'llm.fleet.handler.publish_response')
128
135
  end
129
136
 
130
- def publish_error(_envelope, result)
137
+ def publish_error(envelope, result)
131
138
  require 'legion/extensions/llm/transport/messages/fleet_error'
132
139
  publish_result = ::Legion::Extensions::Llm::Transport::Messages::FleetError.new(
133
140
  protocol_version: result[:protocol_version],
@@ -141,6 +148,8 @@ module Legion
141
148
  reply_to: result[:reply_to],
142
149
  message_context: result[:message_context],
143
150
  trace_context: result[:trace_context],
151
+ caller: envelope[:caller],
152
+ identity: Legion::LLM::PublisherIdentity.current,
144
153
  code: result[:error],
145
154
  message: result[:message],
146
155
  error_class: result[:error_class],
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/logging/helper'
4
+ require_relative '../publisher_identity'
4
5
  module Legion
5
6
  module LLM
6
7
  module Hooks
@@ -166,7 +167,11 @@ module Legion
166
167
  knowledge_domain: 'reflection',
167
168
  confidence: entry[:confidence],
168
169
  source_agent: "llm:#{model}",
169
- metadata: { context: entry[:context], source: 'reflection_hook' }
170
+ metadata: {
171
+ context: entry[:context],
172
+ source: 'reflection_hook',
173
+ submitted_by: Legion::LLM::PublisherIdentity.requested_by
174
+ }
170
175
  })
171
176
  )
172
177
  log.info("[llm][reflection] published via=transport model=#{model} type=#{entry[:type]}")
@@ -176,7 +181,8 @@ module Legion
176
181
  content_type: entry[:type].to_s,
177
182
  knowledge_domain: 'reflection',
178
183
  confidence: entry[:confidence],
179
- source_agent: "llm:#{model}"
184
+ source_agent: "llm:#{model}",
185
+ metadata: { submitted_by: Legion::LLM::PublisherIdentity.requested_by }
180
186
  )
181
187
  log.info("[llm][reflection] published via=direct model=#{model} type=#{entry[:type]}")
182
188
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/logging/helper'
4
- require_relative '../caller_identity'
4
+ require_relative '../publisher_identity'
5
5
  module Legion
6
6
  module LLM
7
7
  module Inference
@@ -65,8 +65,8 @@ module Legion
65
65
  nil
66
66
  end
67
67
 
68
- def extract_identity(caller)
69
- Legion::LLM::CallerIdentity.normalize(caller: caller)
68
+ def extract_identity(_caller)
69
+ Legion::LLM::PublisherIdentity.current
70
70
  end
71
71
 
72
72
  def serialize_tokens(tokens)
@@ -3,7 +3,7 @@
3
3
  require 'concurrent'
4
4
  require 'faraday'
5
5
 
6
- require_relative '../caller_identity'
6
+ require_relative '../publisher_identity'
7
7
  require_relative 'route_attempts'
8
8
 
9
9
  module Legion
@@ -142,6 +142,9 @@ module Legion
142
142
 
143
143
  def inferred_provider_tier(provider)
144
144
  return nil unless provider
145
+
146
+ meta = Call::Registry.metadata_for(provider, @resolved_instance || :default)
147
+ return meta[:tier].to_sym if meta.is_a?(Hash) && meta[:tier]
145
148
  return Router.provider_tier(provider) if defined?(Router) && Router.respond_to?(:provider_tier)
146
149
 
147
150
  Router::PROVIDER_TIER.fetch(provider.to_sym, :cloud) if defined?(Router::PROVIDER_TIER)
@@ -650,10 +653,7 @@ module Legion
650
653
  end
651
654
 
652
655
  def native_dispatch_chat_options
653
- opts = {
654
- model: @resolved_model,
655
- provider: @resolved_provider
656
- }
656
+ opts = { model: @resolved_model, provider: @resolved_provider }
657
657
  opts[:instance] = @resolved_instance if @resolved_instance
658
658
  opts[:thinking] = @request.thinking if @request.thinking
659
659
  opts.compact
@@ -729,8 +729,8 @@ module Legion
729
729
 
730
730
  def add_registry_tool_definitions(definitions)
731
731
  return unless Legion::Settings::Extensions.respond_to?(:tools) &&
732
- Legion::Settings::Extensions.respond_to?(:filter_tools) &&
733
- Array(Legion::Settings::Extensions.tools).any?
732
+ Legion::Settings::Extensions.respond_to?(:filter_tools)
733
+ return unless Array(Legion::Settings::Extensions.tools).any? || @triggered_tools.any?
734
734
 
735
735
  add_settings_extensions_tool_definitions(definitions)
736
736
  rescue StandardError => e
@@ -1418,8 +1418,7 @@ module Legion
1418
1418
  end
1419
1419
 
1420
1420
  def metering_identity
1421
- top_id = @request.respond_to?(:metadata) ? @request.metadata[:identity] || @request.metadata['identity'] : nil
1422
- Legion::LLM::CallerIdentity.normalize(caller: @request.caller, identity: top_id)
1421
+ Legion::LLM::PublisherIdentity.current
1423
1422
  end
1424
1423
 
1425
1424
  def step_context_store
@@ -96,11 +96,10 @@ module Legion
96
96
  model: @resolved_model,
97
97
  idempotency_key: idempotency_key,
98
98
  messages: messages,
99
- options: native_dispatch_options,
100
99
  caller: @request.caller,
101
100
  trace_context: @tracing || {},
102
101
  timeout: @request.ttl
103
- }.compact
102
+ }.merge(native_dispatch_options).compact
104
103
  end
105
104
 
106
105
  def normalize_fleet_result(result)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/logging/helper'
4
+ require_relative 'publisher_identity'
4
5
  require_relative 'metering/usage'
5
6
  require_relative 'inference/request'
6
7
  require_relative 'inference/response'
@@ -589,7 +590,7 @@ module Legion
589
590
  return response if response
590
591
  end
591
592
 
592
- publish_escalation_event(history, :exhausted) if history.size > 1
593
+ publish_escalation_event(history, :exhausted, caller: kwargs[:caller]) if history.size > 1
593
594
  message = "All #{history.size} escalation attempts failed"
594
595
  if last_error
595
596
  providers = history.filter_map { |attempt| attempt[:provider] }.uniq.join(', ')
@@ -608,7 +609,8 @@ module Legion
608
609
  duration_ms = ((Time.now - start_time) * 1000).round
609
610
  result = Quality::Checker.check(response, quality_threshold: threshold, quality_check: quality_check)
610
611
 
611
- return [response, nil] if escalation_attempt_passed?(response, result, resolution, duration_ms, history, chain)
612
+ return [response, nil] if escalation_attempt_passed?(response, result, resolution, duration_ms, history, chain,
613
+ caller: kwargs[:caller])
612
614
 
613
615
  report_health(:quality_failure, resolution, duration_ms, failures: result.failures)
614
616
  history << build_attempt(resolution, :quality_failure, result.failures, duration_ms)
@@ -630,13 +632,13 @@ module Legion
630
632
  **opts.except(:model, :provider))
631
633
  end
632
634
 
633
- def escalation_attempt_passed?(response, result, resolution, duration_ms, history, chain)
635
+ def escalation_attempt_passed?(response, result, resolution, duration_ms, history, chain, caller: nil)
634
636
  return false unless result.passed
635
637
 
636
638
  report_health(:success, resolution, duration_ms)
637
639
  history << build_attempt(resolution, :success, [], duration_ms)
638
640
  attach_escalation_history(response, history, resolution, chain)
639
- publish_escalation_event(history, :success) if history.size > 1
641
+ publish_escalation_event(history, :success, caller: caller) if history.size > 1
640
642
  log.debug "[llm][inference] chat_with_escalation success attempts=#{history.size}"
641
643
  true
642
644
  end
@@ -683,11 +685,12 @@ module Legion
683
685
  signal: :latency, value: duration_ms, metadata: {})
684
686
  end
685
687
 
686
- def publish_escalation_event(history, final_outcome)
688
+ def publish_escalation_event(history, final_outcome, caller: nil)
687
689
  payload = {
688
690
  outcome: final_outcome,
689
691
  attempts: history.size,
690
692
  history: history,
693
+ caller: caller || Legion::LLM::PublisherIdentity.caller_hash,
691
694
  timestamp: Time.now.utc.iso8601
692
695
  }
693
696
 
@@ -775,7 +778,7 @@ module Legion
775
778
 
776
779
  def emit_privacy_blocked_audit
777
780
  Legion::LLM::Audit.emit_prompt(
778
- request_id: nil, conversation_id: nil, caller: nil,
781
+ request_id: nil, conversation_id: nil, caller: Legion::LLM::PublisherIdentity.caller_hash,
779
782
  routing: {}, tokens: {}, status: 'privacy_blocked',
780
783
  error: { class: 'PrivacyModeError', message: 'External tiers blocked by enterprise privacy' },
781
784
  timestamp: Time.now, request_type: 'chat'
@@ -5,6 +5,7 @@ require_relative 'metering/estimator'
5
5
  require_relative 'metering/tracker'
6
6
  require_relative 'metering/tokens'
7
7
  require_relative 'metering/usage'
8
+ require_relative 'publisher_identity'
8
9
 
9
10
  module Legion
10
11
  module LLM
@@ -24,6 +25,7 @@ module Legion
24
25
  module_function
25
26
 
26
27
  def emit(event)
28
+ event = attributed_event(event)
27
29
  event_class = metering_event_class if transport_connected?
28
30
 
29
31
  if event_class
@@ -47,6 +49,13 @@ module Legion
47
49
  :dropped
48
50
  end
49
51
 
52
+ def attributed_event(event)
53
+ source = event.is_a?(Hash) ? event.dup : {}
54
+ source[:identity] = Legion::LLM::PublisherIdentity.current
55
+ source[:caller] ||= Legion::LLM::PublisherIdentity.caller_hash
56
+ source
57
+ end
58
+
50
59
  def flush_spool
51
60
  return 0 unless spool_available? && transport_connected?
52
61
 
@@ -64,7 +73,7 @@ module Legion
64
73
  end
65
74
 
66
75
  def install_hook
67
- Legion::LLM::Hooks.after_chat do |response:, model:, **|
76
+ Legion::LLM::Hooks.after_chat do |response:, model:, caller: nil, **|
68
77
  usage = extract_usage(response)
69
78
  next if usage[:input_tokens].zero? && usage[:output_tokens].zero?
70
79
 
@@ -83,6 +92,7 @@ module Legion
83
92
  model_id: resolved_model,
84
93
  input_tokens: usage[:input_tokens],
85
94
  output_tokens: usage[:output_tokens],
95
+ caller: caller,
86
96
  event_type: 'llm_completion',
87
97
  status: response.is_a?(Hash) && response[:error] ? 'failure' : 'success'
88
98
  )
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'caller_identity'
4
+
5
+ module Legion
6
+ module LLM
7
+ module PublisherIdentity
8
+ GENERIC_PUBLISHER_IDENTITIES = %w[
9
+ anonymous process:anonymous service:system system system:system unknown:anonymous
10
+ ].freeze
11
+
12
+ module_function
13
+
14
+ def current
15
+ process = process_identity_module
16
+ identity = process_identity(process)
17
+ return identity if present_identity?(identity)
18
+
19
+ env_identity
20
+ end
21
+
22
+ def caller_hash
23
+ identity = current
24
+ {
25
+ requested_by: {
26
+ identity: identity[:identity],
27
+ type: identity[:type],
28
+ credential: identity[:credential],
29
+ hostname: identity[:hostname]
30
+ }.compact
31
+ }
32
+ end
33
+
34
+ def requested_by
35
+ caller_hash[:requested_by]
36
+ end
37
+
38
+ def generic_requested_by?(value)
39
+ requested = value.is_a?(Hash) ? value : {}
40
+ raw_id = hash_value(requested, :id).to_s
41
+ return true if raw_id == 'system:system'
42
+
43
+ identity = CallerIdentity.normalize(caller: { requested_by: requested })
44
+ normalized = identity[:identity].to_s
45
+ GENERIC_PUBLISHER_IDENTITIES.include?(normalized)
46
+ end
47
+
48
+ def process_identity_module
49
+ return Legion::Identity::Process if defined?(Legion::Identity::Process)
50
+
51
+ begin
52
+ require 'legion/identity/process'
53
+ rescue LoadError
54
+ nil
55
+ end
56
+
57
+ defined?(Legion::Identity::Process) ? Legion::Identity::Process : nil
58
+ end
59
+
60
+ def process_identity(process)
61
+ return nil unless process
62
+
63
+ canonical = process_value(process, :canonical_name)
64
+ return nil unless present?(canonical)
65
+
66
+ CallerIdentity.normalize(
67
+ caller: {
68
+ requested_by: {
69
+ identity: canonical,
70
+ type: process_value(process, :kind) || :process,
71
+ credential: process_value(process, :source) || :system,
72
+ hostname: process_value(process, :hostname)
73
+ }.compact
74
+ }
75
+ )
76
+ end
77
+
78
+ def env_identity
79
+ raw = ENV.fetch('USER', nil) || ENV.fetch('LOGNAME', nil)
80
+ return CallerIdentity::DEFAULT_IDENTITY.dup unless present?(raw)
81
+
82
+ CallerIdentity.normalize(
83
+ caller: {
84
+ requested_by: {
85
+ identity: raw.to_s,
86
+ type: :human,
87
+ credential: :system
88
+ }
89
+ }
90
+ )
91
+ end
92
+
93
+ def process_value(process, method_name)
94
+ return nil unless process.respond_to?(method_name)
95
+
96
+ process.public_send(method_name)
97
+ rescue StandardError
98
+ nil
99
+ end
100
+
101
+ def hash_value(hash, key)
102
+ return nil unless hash.respond_to?(:key?)
103
+ return hash[key] if hash.key?(key)
104
+
105
+ string_key = key.to_s
106
+ hash[string_key] if hash.key?(string_key)
107
+ end
108
+
109
+ def present_identity?(identity)
110
+ identity.is_a?(Hash) && present?(identity[:identity])
111
+ end
112
+
113
+ def present?(value)
114
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -28,8 +28,12 @@ module Legion
28
28
  def infer_provider_for_model(model)
29
29
  return nil if model.nil? || model.to_s.empty?
30
30
 
31
+ discovered = discover_provider_for_model(model)
32
+ return discovered if discovered
33
+
31
34
  model_s = model.to_s
32
35
  return :bedrock if model_s.start_with?('us.')
36
+ return :bedrock if model_s.match?(/\A(anthropic|meta|mistral|cohere|amazon|ai21)\./i)
33
37
  return :openai if model_s.match?(/\Agpt-|\Ao[134]-/)
34
38
  return :anthropic if model_s.start_with?('claude-')
35
39
  return :gemini if model_s.start_with?('gemini-')
@@ -38,6 +42,17 @@ module Legion
38
42
  nil
39
43
  end
40
44
 
45
+ def discover_provider_for_model(model)
46
+ return nil unless defined?(Discovery) && Discovery.respond_to?(:cached_discovered_models)
47
+
48
+ model_s = model.to_s
49
+ entry = Array(Discovery.cached_discovered_models).find do |m|
50
+ dn = m[:model].to_s
51
+ dn == model_s || dn.start_with?("#{model_s}:")
52
+ end
53
+ entry&.dig(:provider)
54
+ end
55
+
41
56
  # Resolve an LLM routing intent to a tier/provider/model decision.
42
57
  #
43
58
  # @param intent [Hash, nil] routing intent (capability, privacy, etc.)
@@ -132,7 +132,7 @@ module Legion
132
132
  total_duration += duration_ms
133
133
  inject_parts << result.inject if result.inject
134
134
 
135
- emit_step_success(conv_id, method_name, step_idx, duration_ms, result, classification)
135
+ emit_step_success(conv_id, method_name, step_idx, duration_ms, result, classification, context)
136
136
 
137
137
  next unless result.gate
138
138
 
@@ -154,14 +154,21 @@ module Legion
154
154
 
155
155
  private
156
156
 
157
+ def context_caller(context)
158
+ return nil unless context.is_a?(Hash)
159
+
160
+ context[:caller] || context['caller']
161
+ end
162
+
157
163
  def execute_step(method_name, step_idx, context, conv_id, classification)
158
164
  t0 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
165
+ caller = context_caller(context)
159
166
  emit_event(conv_id, 'skill.step.started',
160
167
  step_name: method_name, step_index: step_idx)
161
168
  Legion::LLM::Metering.emit(
162
169
  request_type: 'skill.step.start', skill_name: self.class.skill_name,
163
170
  namespace: self.class.namespace, step_name: method_name,
164
- step_index: step_idx, tier: 'local'
171
+ step_index: step_idx, tier: 'local', caller: caller
165
172
  )
166
173
  result = public_send(method_name, context: context)
167
174
  unless result.respond_to?(:inject) && result.respond_to?(:metadata) && result.respond_to?(:gate)
@@ -175,10 +182,11 @@ module Legion
175
182
  [result, duration_ms]
176
183
  rescue StandardError => e
177
184
  duration_ms = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - t0) * 1000).round
178
- handle_step_error(e, method_name, step_idx, conv_id, duration_ms, classification)
185
+ handle_step_error(e, method_name, step_idx, conv_id, duration_ms, classification, context)
179
186
  end
180
187
 
181
- def handle_step_error(err, method_name, step_idx, conv_id, duration_ms, classification)
188
+ def handle_step_error(err, method_name, step_idx, conv_id, duration_ms, classification, context)
189
+ caller = context_caller(context)
182
190
  Legion::LLM::Inference::Conversation.clear_skill_state(conv_id) if conv_id
183
191
  emit_event(conv_id, 'skill.step.failed',
184
192
  step_name: method_name, error: err.message)
@@ -186,19 +194,20 @@ module Legion
186
194
  skill_name: self.class.skill_name, namespace: self.class.namespace,
187
195
  step_name: method_name, gate: nil, status: :failed,
188
196
  duration_ms: duration_ms, metadata: { error: err.message },
189
- classification: classification
197
+ classification: classification, caller: caller
190
198
  )
191
199
  Legion::LLM::Metering.emit(
192
200
  request_type: 'skill.step', skill_name: self.class.skill_name,
193
201
  namespace: self.class.namespace, step_name: method_name,
194
- step_index: step_idx, duration_ms: duration_ms, gate: nil, tier: 'local'
202
+ step_index: step_idx, duration_ms: duration_ms, gate: nil, tier: 'local', caller: caller
195
203
  )
196
204
  raise Legion::LLM::Skills::StepError.new(
197
205
  "#{self.class.skill_name}##{method_name} failed: #{err.message}", cause: err
198
206
  )
199
207
  end
200
208
 
201
- def emit_step_success(conv_id, method_name, step_idx, duration_ms, result, classification)
209
+ def emit_step_success(conv_id, method_name, step_idx, duration_ms, result, classification, context)
210
+ caller = context_caller(context)
202
211
  emit_event(conv_id, 'skill.step.completed',
203
212
  step_name: method_name, duration_ms: duration_ms,
204
213
  metadata: result.metadata)
@@ -206,13 +215,13 @@ module Legion
206
215
  skill_name: self.class.skill_name, namespace: self.class.namespace,
207
216
  step_name: method_name, gate: result.gate,
208
217
  status: :completed, duration_ms: duration_ms,
209
- metadata: result.metadata, classification: classification
218
+ metadata: result.metadata, classification: classification, caller: caller
210
219
  )
211
220
  Legion::LLM::Metering.emit(
212
221
  request_type: 'skill.step', skill_name: self.class.skill_name,
213
222
  namespace: self.class.namespace, step_name: method_name,
214
223
  step_index: step_idx, duration_ms: duration_ms,
215
- gate: result.gate&.to_s, tier: 'local'
224
+ gate: result.gate&.to_s, tier: 'local', caller: caller
216
225
  )
217
226
  end
218
227
 
@@ -3,7 +3,7 @@
3
3
  require 'securerandom'
4
4
  require 'uri'
5
5
  require 'legion/logging/helper'
6
- require_relative '../caller_identity'
6
+ require_relative '../publisher_identity'
7
7
 
8
8
  module Legion
9
9
  module LLM
@@ -209,7 +209,7 @@ module Legion
209
209
  end
210
210
 
211
211
  def identity_headers
212
- identity = Legion::LLM::CallerIdentity.normalize(caller: @options[:caller], identity: @options[:identity])
212
+ identity = Legion::LLM::PublisherIdentity.current
213
213
  return {} unless identity
214
214
 
215
215
  h = {}
@@ -43,7 +43,7 @@ module Legion
43
43
  type = caller_info[:type] || caller_info['type'] || top_id[:type] || top_id['type'] ||
44
44
  (extension && 'extension')
45
45
  h = {}
46
- h['x-legion-caller-type'] = type.to_s if type
46
+ h['x-legion-request-caller-type'] = type.to_s if type
47
47
  h
48
48
  end
49
49
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.9.10'
5
+ VERSION = '0.9.14'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'llm/version'
9
9
  require_relative 'llm/errors'
10
10
  require_relative 'llm/settings'
11
11
  require_relative 'llm/caller_identity'
12
+ require_relative 'llm/publisher_identity'
12
13
  require_relative 'llm/call/providers'
13
14
  require_relative 'llm/call/registry'
14
15
  require_relative 'llm/call/lex_llm_adapter'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.10
4
+ version: 0.9.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -294,6 +294,7 @@ files:
294
294
  - lib/legion/llm/metering/tokens.rb
295
295
  - lib/legion/llm/metering/tracker.rb
296
296
  - lib/legion/llm/metering/usage.rb
297
+ - lib/legion/llm/publisher_identity.rb
297
298
  - lib/legion/llm/quality.rb
298
299
  - lib/legion/llm/quality/checker.rb
299
300
  - lib/legion/llm/quality/confidence/score.rb