lex-llm-ledger 0.1.2
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 +7 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +62 -0
- data/Rakefile +7 -0
- data/lex-llm-ledger.gemspec +43 -0
- data/lib/legion/extensions/llm/ledger/actors/metering_writer.rb +25 -0
- data/lib/legion/extensions/llm/ledger/actors/prompt_writer.rb +25 -0
- data/lib/legion/extensions/llm/ledger/actors/spool_flush.rb +42 -0
- data/lib/legion/extensions/llm/ledger/actors/tool_writer.rb +25 -0
- data/lib/legion/extensions/llm/ledger/helpers/decryption.rb +50 -0
- data/lib/legion/extensions/llm/ledger/helpers/queries.rb +39 -0
- data/lib/legion/extensions/llm/ledger/helpers/retention.rb +75 -0
- data/lib/legion/extensions/llm/ledger/migrations/001_create_metering_records.rb +48 -0
- data/lib/legion/extensions/llm/ledger/migrations/002_create_prompt_records.rb +54 -0
- data/lib/legion/extensions/llm/ledger/migrations/003_create_tool_records.rb +47 -0
- data/lib/legion/extensions/llm/ledger/runners/metering.rb +65 -0
- data/lib/legion/extensions/llm/ledger/runners/prompts.rb +88 -0
- data/lib/legion/extensions/llm/ledger/runners/provider_stats.rb +63 -0
- data/lib/legion/extensions/llm/ledger/runners/tools.rb +79 -0
- data/lib/legion/extensions/llm/ledger/runners/usage_reporter.rb +82 -0
- data/lib/legion/extensions/llm/ledger/transport/exchanges/audit.rb +27 -0
- data/lib/legion/extensions/llm/ledger/transport/exchanges/metering.rb +27 -0
- data/lib/legion/extensions/llm/ledger/transport/queues/audit_prompts.rb +23 -0
- data/lib/legion/extensions/llm/ledger/transport/queues/audit_tools.rb +23 -0
- data/lib/legion/extensions/llm/ledger/transport/queues/metering_write.rb +23 -0
- data/lib/legion/extensions/llm/ledger/transport/transport.rb +39 -0
- data/lib/legion/extensions/llm/ledger/version.rb +11 -0
- data/lib/lex-llm-ledger.rb +42 -0
- metadata +229 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do # rubocop:disable Metrics/BlockLength
|
|
4
|
+
change do
|
|
5
|
+
create_table(:metering_records) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
|
|
8
|
+
String :message_id, null: false, unique: true
|
|
9
|
+
String :correlation_id, null: false
|
|
10
|
+
String :conversation_id, null: false
|
|
11
|
+
String :message_id_ctx, null: false
|
|
12
|
+
String :parent_message_id
|
|
13
|
+
Integer :message_seq
|
|
14
|
+
String :request_id, null: false
|
|
15
|
+
String :exchange_id
|
|
16
|
+
String :request_type, null: false
|
|
17
|
+
String :tier, null: false
|
|
18
|
+
String :provider, null: false
|
|
19
|
+
String :model_id, null: false
|
|
20
|
+
String :node_id, null: false
|
|
21
|
+
String :worker_id
|
|
22
|
+
String :agent_id
|
|
23
|
+
String :task_id
|
|
24
|
+
Integer :input_tokens, null: false, default: 0
|
|
25
|
+
Integer :output_tokens, null: false, default: 0
|
|
26
|
+
Integer :thinking_tokens, null: false, default: 0
|
|
27
|
+
Integer :total_tokens, null: false, default: 0
|
|
28
|
+
Integer :latency_ms, null: false, default: 0
|
|
29
|
+
Integer :wall_clock_ms, null: false, default: 0
|
|
30
|
+
Float :cost_usd, null: false, default: 0.0
|
|
31
|
+
String :routing_reason
|
|
32
|
+
String :cost_center
|
|
33
|
+
String :budget_id
|
|
34
|
+
String :recorded_at, null: false
|
|
35
|
+
DateTime :inserted_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
36
|
+
|
|
37
|
+
index [:conversation_id]
|
|
38
|
+
index [:request_id]
|
|
39
|
+
index [:message_id_ctx]
|
|
40
|
+
index [:correlation_id]
|
|
41
|
+
index %i[provider model_id]
|
|
42
|
+
index [:node_id]
|
|
43
|
+
index [:worker_id]
|
|
44
|
+
index [:recorded_at]
|
|
45
|
+
index %i[cost_center recorded_at]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do # rubocop:disable Metrics/BlockLength
|
|
4
|
+
change do # rubocop:disable Metrics/BlockLength
|
|
5
|
+
create_table(:prompt_records) do # rubocop:disable Metrics/BlockLength
|
|
6
|
+
primary_key :id
|
|
7
|
+
|
|
8
|
+
String :message_id, null: false, unique: true
|
|
9
|
+
String :correlation_id, null: false
|
|
10
|
+
String :conversation_id, null: false
|
|
11
|
+
String :message_id_ctx, null: false
|
|
12
|
+
String :parent_message_id
|
|
13
|
+
Integer :message_seq
|
|
14
|
+
String :request_id, null: false
|
|
15
|
+
String :exchange_id
|
|
16
|
+
String :response_message_id
|
|
17
|
+
String :provider, null: false
|
|
18
|
+
String :model_id, null: false
|
|
19
|
+
String :tier
|
|
20
|
+
String :request_type
|
|
21
|
+
String :request_json, null: false, text: true
|
|
22
|
+
String :response_json, null: false, text: true
|
|
23
|
+
Integer :input_tokens, default: 0
|
|
24
|
+
Integer :output_tokens, default: 0
|
|
25
|
+
Integer :total_tokens, default: 0
|
|
26
|
+
Float :cost_usd, default: 0.0
|
|
27
|
+
String :caller_identity
|
|
28
|
+
String :caller_type
|
|
29
|
+
String :agent_id
|
|
30
|
+
String :task_id
|
|
31
|
+
String :classification_level
|
|
32
|
+
TrueClass :contains_phi, null: false, default: false
|
|
33
|
+
TrueClass :contains_pii, null: false, default: false
|
|
34
|
+
String :jurisdictions
|
|
35
|
+
Integer :quality_score
|
|
36
|
+
String :quality_band
|
|
37
|
+
String :retention_policy, null: false, default: 'default'
|
|
38
|
+
DateTime :expires_at
|
|
39
|
+
String :recorded_at, null: false
|
|
40
|
+
DateTime :inserted_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
41
|
+
|
|
42
|
+
index [:conversation_id]
|
|
43
|
+
index [:request_id]
|
|
44
|
+
index [:message_id_ctx]
|
|
45
|
+
index [:correlation_id]
|
|
46
|
+
index [:response_message_id]
|
|
47
|
+
index [:caller_identity]
|
|
48
|
+
index %i[provider model_id]
|
|
49
|
+
index [:contains_phi]
|
|
50
|
+
index [:expires_at]
|
|
51
|
+
index [:inserted_at]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do # rubocop:disable Metrics/BlockLength
|
|
4
|
+
change do
|
|
5
|
+
create_table(:tool_records) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
|
|
8
|
+
String :message_id, null: false, unique: true
|
|
9
|
+
String :correlation_id, null: false
|
|
10
|
+
String :conversation_id, null: false
|
|
11
|
+
String :message_id_ctx, null: false
|
|
12
|
+
String :parent_message_id
|
|
13
|
+
Integer :message_seq
|
|
14
|
+
String :request_id, null: false
|
|
15
|
+
String :exchange_id
|
|
16
|
+
String :tool_call_id, null: false
|
|
17
|
+
String :tool_name, null: false
|
|
18
|
+
String :tool_source_type
|
|
19
|
+
String :tool_source_server
|
|
20
|
+
String :tool_status, null: false
|
|
21
|
+
Integer :tool_duration_ms, default: 0
|
|
22
|
+
String :arguments_json, text: true
|
|
23
|
+
String :result_json, text: true
|
|
24
|
+
String :error_json, text: true
|
|
25
|
+
String :caller_identity
|
|
26
|
+
String :agent_id
|
|
27
|
+
String :classification_level
|
|
28
|
+
TrueClass :contains_phi, null: false, default: false
|
|
29
|
+
String :retention_policy, null: false, default: 'default'
|
|
30
|
+
DateTime :expires_at
|
|
31
|
+
String :tool_start_at
|
|
32
|
+
String :tool_end_at
|
|
33
|
+
DateTime :inserted_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
34
|
+
|
|
35
|
+
index [:conversation_id]
|
|
36
|
+
index [:request_id]
|
|
37
|
+
index [:message_id_ctx]
|
|
38
|
+
index [:correlation_id]
|
|
39
|
+
index [:tool_name]
|
|
40
|
+
index %i[tool_source_server tool_name]
|
|
41
|
+
index [:tool_status]
|
|
42
|
+
index [:contains_phi]
|
|
43
|
+
index [:expires_at]
|
|
44
|
+
index [:inserted_at]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Runners
|
|
8
|
+
module Metering
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def write_metering_record(payload, metadata = {})
|
|
12
|
+
ctx = payload[:message_context] || {}
|
|
13
|
+
props = metadata[:properties] || {}
|
|
14
|
+
|
|
15
|
+
record = build_metering_record(payload, ctx, props)
|
|
16
|
+
::Legion::Data::DB[:metering_records].insert(record)
|
|
17
|
+
{ result: :ok }
|
|
18
|
+
rescue Sequel::UniqueConstraintViolation => _e
|
|
19
|
+
{ result: :duplicate }
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
Legion::Logging.error("[lex-llm-ledger] write_metering_record failed: #{e.message}") # rubocop:disable Legion/HelperMigration/DirectLogging
|
|
22
|
+
{ result: :error, error: e.message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_metering_record(payload, ctx, props)
|
|
28
|
+
billing = payload[:billing] || {}
|
|
29
|
+
{
|
|
30
|
+
message_id: props[:message_id],
|
|
31
|
+
correlation_id: props[:correlation_id],
|
|
32
|
+
conversation_id: ctx[:conversation_id],
|
|
33
|
+
message_id_ctx: ctx[:message_id],
|
|
34
|
+
parent_message_id: ctx[:parent_message_id],
|
|
35
|
+
message_seq: ctx[:message_seq],
|
|
36
|
+
request_id: ctx[:request_id],
|
|
37
|
+
exchange_id: ctx[:exchange_id],
|
|
38
|
+
request_type: payload[:request_type],
|
|
39
|
+
tier: payload[:tier],
|
|
40
|
+
provider: payload[:provider],
|
|
41
|
+
model_id: payload[:model_id],
|
|
42
|
+
node_id: payload[:node_id],
|
|
43
|
+
worker_id: payload[:worker_id],
|
|
44
|
+
agent_id: payload[:agent_id],
|
|
45
|
+
task_id: payload[:task_id],
|
|
46
|
+
input_tokens: payload[:input_tokens].to_i,
|
|
47
|
+
output_tokens: payload[:output_tokens].to_i,
|
|
48
|
+
thinking_tokens: payload[:thinking_tokens].to_i,
|
|
49
|
+
total_tokens: payload[:total_tokens].to_i,
|
|
50
|
+
latency_ms: payload[:latency_ms].to_i,
|
|
51
|
+
wall_clock_ms: payload[:wall_clock_ms].to_i,
|
|
52
|
+
cost_usd: payload[:cost_usd].to_f,
|
|
53
|
+
routing_reason: payload[:routing_reason],
|
|
54
|
+
cost_center: billing[:cost_center],
|
|
55
|
+
budget_id: billing[:budget_id],
|
|
56
|
+
recorded_at: payload[:recorded_at],
|
|
57
|
+
inserted_at: Time.now.utc
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Runners
|
|
8
|
+
module Prompts
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def write_prompt_record(payload, metadata = {})
|
|
12
|
+
headers = metadata[:headers] || {}
|
|
13
|
+
props = metadata[:properties] || {}
|
|
14
|
+
|
|
15
|
+
body = Helpers::Decryption.decrypt_if_needed(payload, metadata)
|
|
16
|
+
ctx = body[:message_context] || {}
|
|
17
|
+
|
|
18
|
+
expires_at = Helpers::Retention.resolve(
|
|
19
|
+
retention: headers['x-legion-retention'],
|
|
20
|
+
contains_phi: headers['x-legion-contains-phi'] == 'true'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
record = build_prompt_record(body, ctx, props, headers, expires_at)
|
|
24
|
+
::Legion::Data::DB[:prompt_records].insert(record)
|
|
25
|
+
{ result: :ok }
|
|
26
|
+
rescue Sequel::UniqueConstraintViolation => _e
|
|
27
|
+
{ result: :duplicate }
|
|
28
|
+
rescue Helpers::DecryptionUnavailable => _e
|
|
29
|
+
raise
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
Legion::Logging.error("[lex-llm-ledger] write_prompt_record failed: #{e.message}") # rubocop:disable Legion/HelperMigration/DirectLogging
|
|
32
|
+
{ result: :error, error: e.message }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_prompt_record(body, ctx, props, headers, expires_at)
|
|
38
|
+
routing = body[:routing] || {}
|
|
39
|
+
tokens = body[:tokens] || {}
|
|
40
|
+
cost = body[:cost] || {}
|
|
41
|
+
caller = body.dig(:caller, :requested_by) || {}
|
|
42
|
+
agent = body[:agent] || {}
|
|
43
|
+
cls = body[:classification] || {}
|
|
44
|
+
quality = body[:quality] || {}
|
|
45
|
+
ts = body[:timestamps] || {}
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
message_id: props[:message_id],
|
|
49
|
+
correlation_id: props[:correlation_id],
|
|
50
|
+
conversation_id: ctx[:conversation_id],
|
|
51
|
+
message_id_ctx: ctx[:message_id],
|
|
52
|
+
parent_message_id: ctx[:parent_message_id],
|
|
53
|
+
message_seq: ctx[:message_seq],
|
|
54
|
+
request_id: ctx[:request_id],
|
|
55
|
+
exchange_id: ctx[:exchange_id],
|
|
56
|
+
response_message_id: body[:response_message_id],
|
|
57
|
+
provider: routing[:provider],
|
|
58
|
+
model_id: routing[:model],
|
|
59
|
+
tier: routing[:tier],
|
|
60
|
+
request_type: headers['x-legion-llm-request-type'],
|
|
61
|
+
request_json: Legion::JSON.dump(body[:request] || {}), # rubocop:disable Legion/HelperMigration/DirectJson
|
|
62
|
+
response_json: Legion::JSON.dump(body[:response] || {}), # rubocop:disable Legion/HelperMigration/DirectJson
|
|
63
|
+
input_tokens: tokens[:input].to_i,
|
|
64
|
+
output_tokens: tokens[:output].to_i,
|
|
65
|
+
total_tokens: tokens[:total].to_i,
|
|
66
|
+
cost_usd: cost[:estimated_usd].to_f,
|
|
67
|
+
caller_identity: caller[:identity],
|
|
68
|
+
caller_type: caller[:type],
|
|
69
|
+
agent_id: agent[:id],
|
|
70
|
+
task_id: agent[:task_id],
|
|
71
|
+
classification_level: cls[:level] || headers['x-legion-classification'],
|
|
72
|
+
contains_phi: Helpers::Queries.phi_flag?(cls, headers),
|
|
73
|
+
contains_pii: cls[:contains_pii] ? true : false,
|
|
74
|
+
jurisdictions: Array(cls[:jurisdictions]).join(','),
|
|
75
|
+
quality_score: quality[:score],
|
|
76
|
+
quality_band: quality[:band],
|
|
77
|
+
retention_policy: headers['x-legion-retention'] || 'default',
|
|
78
|
+
expires_at: expires_at,
|
|
79
|
+
recorded_at: ts[:returned] || ts[:provider_end],
|
|
80
|
+
inserted_at: Time.now.utc
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Runners
|
|
8
|
+
module ProviderStats
|
|
9
|
+
extend self # rubocop:disable Style/ModuleFunction
|
|
10
|
+
|
|
11
|
+
def health_report
|
|
12
|
+
ds = ::Legion::Data::DB[:metering_records]
|
|
13
|
+
.where { inserted_at >= Time.now.utc - 86_400 }
|
|
14
|
+
.select(
|
|
15
|
+
:provider,
|
|
16
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:request_count),
|
|
17
|
+
Sequel.function(:SUM, :total_tokens).as(:total_tokens),
|
|
18
|
+
Sequel.function(:AVG, :latency_ms).as(:avg_latency_ms),
|
|
19
|
+
Sequel.function(:MAX, :latency_ms).as(:max_latency_ms)
|
|
20
|
+
)
|
|
21
|
+
.group(:provider)
|
|
22
|
+
.all
|
|
23
|
+
|
|
24
|
+
ds.map { |row| row.merge(status: Helpers::Queries.latency_status(row[:avg_latency_ms])) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def circuit_summary(period: 'hour')
|
|
28
|
+
since = Helpers::Queries.period_start(period)
|
|
29
|
+
::Legion::Data::DB[:metering_records]
|
|
30
|
+
.where { inserted_at >= since }
|
|
31
|
+
.select(
|
|
32
|
+
:provider, :tier,
|
|
33
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:request_count),
|
|
34
|
+
Sequel.function(:AVG, :latency_ms).as(:avg_latency_ms),
|
|
35
|
+
Sequel.function(:SUM, :cost_usd).as(:cost_usd)
|
|
36
|
+
)
|
|
37
|
+
.group(:provider, :tier)
|
|
38
|
+
.order(Sequel.desc(:request_count))
|
|
39
|
+
.all
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def provider_detail(provider:, period: 'day')
|
|
43
|
+
since = Helpers::Queries.period_start(period)
|
|
44
|
+
::Legion::Data::DB[:metering_records]
|
|
45
|
+
.where(provider: provider)
|
|
46
|
+
.where { inserted_at >= since }
|
|
47
|
+
.select(
|
|
48
|
+
:model_id, :request_type,
|
|
49
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:count),
|
|
50
|
+
Sequel.function(:SUM, :total_tokens).as(:total_tokens),
|
|
51
|
+
Sequel.function(:AVG, :latency_ms).as(:avg_latency_ms),
|
|
52
|
+
Sequel.function(:SUM, :cost_usd).as(:cost_usd)
|
|
53
|
+
)
|
|
54
|
+
.group(:model_id, :request_type)
|
|
55
|
+
.order(Sequel.desc(:count))
|
|
56
|
+
.all
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Runners
|
|
8
|
+
module Tools
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def write_tool_record(payload, metadata = {})
|
|
12
|
+
headers = metadata[:headers] || {}
|
|
13
|
+
props = metadata[:properties] || {}
|
|
14
|
+
|
|
15
|
+
body = Helpers::Decryption.decrypt_if_needed(payload, metadata)
|
|
16
|
+
ctx = body[:message_context] || {}
|
|
17
|
+
tool = body[:tool_call] || {}
|
|
18
|
+
|
|
19
|
+
expires_at = Helpers::Retention.resolve(
|
|
20
|
+
retention: headers['x-legion-retention'],
|
|
21
|
+
contains_phi: headers['x-legion-contains-phi'] == 'true'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
record = build_tool_record(body, ctx, tool, props, headers, expires_at)
|
|
25
|
+
::Legion::Data::DB[:tool_records].insert(record)
|
|
26
|
+
{ result: :ok }
|
|
27
|
+
rescue Sequel::UniqueConstraintViolation => _e
|
|
28
|
+
{ result: :duplicate }
|
|
29
|
+
rescue Helpers::DecryptionUnavailable => _e
|
|
30
|
+
raise
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Legion::Logging.error("[lex-llm-ledger] write_tool_record failed: #{e.message}") # rubocop:disable Legion/HelperMigration/DirectLogging
|
|
33
|
+
{ result: :error, error: e.message }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_tool_record(body, ctx, tool, props, headers, expires_at)
|
|
39
|
+
src = tool[:source] || {}
|
|
40
|
+
cls = body[:classification] || {}
|
|
41
|
+
ts = body[:timestamps] || {}
|
|
42
|
+
caller = body.dig(:caller, :requested_by) || {}
|
|
43
|
+
agent = body[:agent] || {}
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
message_id: props[:message_id],
|
|
47
|
+
correlation_id: props[:correlation_id],
|
|
48
|
+
conversation_id: ctx[:conversation_id],
|
|
49
|
+
message_id_ctx: ctx[:message_id],
|
|
50
|
+
parent_message_id: ctx[:parent_message_id],
|
|
51
|
+
message_seq: ctx[:message_seq],
|
|
52
|
+
request_id: ctx[:request_id],
|
|
53
|
+
exchange_id: ctx[:exchange_id],
|
|
54
|
+
tool_call_id: tool[:id],
|
|
55
|
+
tool_name: tool[:name] || headers['x-legion-tool-name'],
|
|
56
|
+
tool_source_type: src[:type] || headers['x-legion-tool-source-type'],
|
|
57
|
+
tool_source_server: src[:server] || headers['x-legion-tool-source-server'],
|
|
58
|
+
tool_status: tool[:status] || headers['x-legion-tool-status'],
|
|
59
|
+
tool_duration_ms: tool[:duration_ms].to_i,
|
|
60
|
+
arguments_json: Legion::JSON.dump(tool[:arguments] || {}), # rubocop:disable Legion/HelperMigration/DirectJson
|
|
61
|
+
result_json: Legion::JSON.dump(tool[:result]), # rubocop:disable Legion/HelperMigration/DirectJson
|
|
62
|
+
error_json: Legion::JSON.dump(tool[:error]), # rubocop:disable Legion/HelperMigration/DirectJson
|
|
63
|
+
caller_identity: caller[:identity],
|
|
64
|
+
agent_id: agent[:id],
|
|
65
|
+
classification_level: cls[:level] || headers['x-legion-classification'],
|
|
66
|
+
contains_phi: Helpers::Queries.phi_flag?(cls, headers),
|
|
67
|
+
retention_policy: headers['x-legion-retention'] || 'default',
|
|
68
|
+
expires_at: expires_at,
|
|
69
|
+
tool_start_at: ts[:tool_start],
|
|
70
|
+
tool_end_at: ts[:tool_end],
|
|
71
|
+
inserted_at: Time.now.utc
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Runners
|
|
8
|
+
module UsageReporter
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def summary(since: nil, until_: nil, period: nil, group_by: nil)
|
|
12
|
+
dataset = ::Legion::Data::DB[:metering_records]
|
|
13
|
+
dataset = apply_time_window(dataset, since, until_, period)
|
|
14
|
+
dataset = dataset.group_and_count(group_by.to_sym) if group_by
|
|
15
|
+
dataset.select_append(
|
|
16
|
+
Sequel.function(:SUM, :input_tokens).as(:total_input_tokens),
|
|
17
|
+
Sequel.function(:SUM, :output_tokens).as(:total_output_tokens),
|
|
18
|
+
Sequel.function(:SUM, :total_tokens).as(:grand_total_tokens),
|
|
19
|
+
Sequel.function(:SUM, :cost_usd).as(:total_cost_usd),
|
|
20
|
+
Sequel.function(:AVG, :latency_ms).as(:avg_latency_ms),
|
|
21
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:request_count)
|
|
22
|
+
).all
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def worker_usage(worker_id:, since: nil, until_: nil, period: nil)
|
|
26
|
+
dataset = ::Legion::Data::DB[:metering_records].where(worker_id: worker_id)
|
|
27
|
+
dataset = apply_time_window(dataset, since, until_, period)
|
|
28
|
+
dataset.select(
|
|
29
|
+
:provider, :model_id, :request_type,
|
|
30
|
+
Sequel.function(:SUM, :total_tokens).as(:total_tokens),
|
|
31
|
+
Sequel.function(:SUM, :cost_usd).as(:cost_usd),
|
|
32
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:count)
|
|
33
|
+
).group(:provider, :model_id, :request_type).all
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def budget_check(budget_id:, budget_usd:, threshold: 0.8, period: 'month')
|
|
37
|
+
dataset = ::Legion::Data::DB[:metering_records].where(budget_id: budget_id)
|
|
38
|
+
dataset = apply_time_window(dataset, nil, nil, period)
|
|
39
|
+
spent = dataset.sum(:cost_usd).to_f
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
budget_id: budget_id,
|
|
43
|
+
budget_usd: budget_usd.to_f,
|
|
44
|
+
spent_usd: spent,
|
|
45
|
+
remaining_usd: [budget_usd.to_f - spent, 0.0].max,
|
|
46
|
+
exceeded: spent > budget_usd.to_f,
|
|
47
|
+
threshold_reached: spent >= (budget_usd.to_f * threshold.to_f)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def top_consumers(limit: 10, group_by: 'node_id', since: nil, until_: nil, period: 'day')
|
|
52
|
+
col = group_by.to_sym
|
|
53
|
+
dataset = ::Legion::Data::DB[:metering_records]
|
|
54
|
+
dataset = apply_time_window(dataset, since, until_, period)
|
|
55
|
+
dataset.select(
|
|
56
|
+
col,
|
|
57
|
+
Sequel.function(:SUM, :total_tokens).as(:total_tokens),
|
|
58
|
+
Sequel.function(:SUM, :cost_usd).as(:cost_usd),
|
|
59
|
+
Sequel.function(:COUNT, Sequel.lit('*')).as(:request_count)
|
|
60
|
+
).group(col)
|
|
61
|
+
.order(Sequel.desc(:cost_usd))
|
|
62
|
+
.limit(limit)
|
|
63
|
+
.all
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def apply_time_window(dataset, since, until_, period)
|
|
69
|
+
if period
|
|
70
|
+
since = Helpers::Queries.period_start(period)
|
|
71
|
+
until_ = Time.now.utc
|
|
72
|
+
end
|
|
73
|
+
dataset = dataset.where { inserted_at >= since } if since
|
|
74
|
+
dataset = dataset.where { inserted_at <= until_ } if until_
|
|
75
|
+
dataset
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Transport
|
|
8
|
+
module Exchanges
|
|
9
|
+
class Audit < Legion::Transport::Exchange
|
|
10
|
+
def exchange_name
|
|
11
|
+
'llm.audit'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_type
|
|
15
|
+
'topic'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def passive?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Transport
|
|
8
|
+
module Exchanges
|
|
9
|
+
class Metering < Legion::Transport::Exchange
|
|
10
|
+
def exchange_name
|
|
11
|
+
'llm.metering'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def default_type
|
|
15
|
+
'topic'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def passive?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Transport
|
|
8
|
+
module Queues
|
|
9
|
+
class AuditPrompts < Legion::Transport::Queue
|
|
10
|
+
def queue_name
|
|
11
|
+
'llm.audit.prompts'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def queue_options
|
|
15
|
+
{ durable: true }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Transport
|
|
8
|
+
module Queues
|
|
9
|
+
class AuditTools < Legion::Transport::Queue
|
|
10
|
+
def queue_name
|
|
11
|
+
'llm.audit.tools'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def queue_options
|
|
15
|
+
{ durable: true }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LLM
|
|
6
|
+
module Ledger
|
|
7
|
+
module Transport
|
|
8
|
+
module Queues
|
|
9
|
+
class MeteringWrite < Legion::Transport::Queue
|
|
10
|
+
def queue_name
|
|
11
|
+
'llm.metering.write'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def queue_options
|
|
15
|
+
{ durable: true }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|