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 +4 -4
- data/CHANGELOG.md +26 -0
- data/lib/legion/llm/api/native/helpers.rb +15 -8
- data/lib/legion/llm/audit.rb +11 -0
- data/lib/legion/llm/fleet/dispatcher.rb +8 -2
- data/lib/legion/llm/fleet/handler.rb +11 -2
- data/lib/legion/llm/hooks/reflection.rb +8 -2
- data/lib/legion/llm/inference/audit_publisher.rb +3 -3
- data/lib/legion/llm/inference/executor.rb +8 -9
- data/lib/legion/llm/inference/route_attempts.rb +1 -2
- data/lib/legion/llm/inference.rb +9 -6
- data/lib/legion/llm/metering.rb +11 -1
- data/lib/legion/llm/publisher_identity.rb +118 -0
- data/lib/legion/llm/router.rb +15 -0
- data/lib/legion/llm/skills/base.rb +18 -9
- data/lib/legion/llm/transport/message.rb +2 -2
- data/lib/legion/llm/transport/messages/prompt_event.rb +1 -1
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc06bf006614fbb7a1052b368912ef58744e5b6509680c163c1b7ce0009e87d8
|
|
4
|
+
data.tar.gz: 91c96c5fee56c7fb4804ea3d58eb908cec12539a75eba11348e842be26509343
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
394
|
-
|
|
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
|
|
data/lib/legion/llm/audit.rb
CHANGED
|
@@ -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
|
-
{
|
|
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(
|
|
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(
|
|
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: {
|
|
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 '../
|
|
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(
|
|
69
|
-
Legion::LLM::
|
|
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 '../
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/legion/llm/inference.rb
CHANGED
|
@@ -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:
|
|
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'
|
data/lib/legion/llm/metering.rb
CHANGED
|
@@ -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
|
data/lib/legion/llm/router.rb
CHANGED
|
@@ -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 '../
|
|
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::
|
|
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
|
|
data/lib/legion/llm/version.rb
CHANGED
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.
|
|
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
|