lex-llm-ledger 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/lex-llm-ledger.gemspec +1 -1
- data/lib/legion/extensions/llm/ledger/backfill/legacy_llm_records.rb +6 -6
- data/lib/legion/extensions/llm/ledger/data/migrations/011_add_identity_columns_to_registry_availability.rb +30 -0
- data/lib/legion/extensions/llm/ledger/data/migrations/012_archive_legacy_tables.rb +15 -0
- data/lib/legion/extensions/llm/ledger/runners/prompts.rb +25 -1
- data/lib/legion/extensions/llm/ledger/runners/registry_availability.rb +45 -29
- data/lib/legion/extensions/llm/ledger/runners/tools.rb +209 -45
- data/lib/legion/extensions/llm/ledger/version.rb +1 -1
- data/lib/legion/extensions/llm/ledger/writers/official_record_writer.rb +107 -49
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4fb5492195845786b98e664192f84716d049c4144e81e28c5627799cf169b99
|
|
4
|
+
data.tar.gz: fc6b2b70fbfda9e98c6e7a6e942c81f65193e5c65f46856a50b4fa64ec681972
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e85919d09a173cca54608528ceb4c50a0350912a52c81a5948fff7cf483f9a428e078cbf948b7f90237191335e0e1165ff103d22864df6ffc794c2b922394b9a
|
|
7
|
+
data.tar.gz: 6de04faae342441c24754f9b186fa92e49b6ab971ad8e4935175ad7206a58da5e288fef33791e97ed785c01f2ca0198606f82b064812a085e69530cf578dfe8d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Rewrite `Runners::Tools` to write tool audit events to the official `llm_tool_calls` and
|
|
7
|
+
`llm_tool_call_attempts` tables instead of the legacy `llm_tool_records` table.
|
|
8
|
+
- Each tool audit event produces one `llm_tool_calls` row (linked to the parent
|
|
9
|
+
`llm_message_inference_responses` row) and one `llm_tool_call_attempts` row containing
|
|
10
|
+
the execution outcome.
|
|
11
|
+
- Argument and result payloads are stored as SHA-256 fingerprints in `arguments_ref` /
|
|
12
|
+
`result_ref` (the official schema columns are 255-char refs, not full JSON blobs).
|
|
13
|
+
- Idempotency is enforced via UUID derived from `tool_call.id` (or request/message context);
|
|
14
|
+
a second write for the same tool call returns `{ result: :duplicate }`.
|
|
15
|
+
- Tool call writes that cannot be linked to an existing inference response are logged at
|
|
16
|
+
`warn` and dropped gracefully (returns `{ result: :ok }`).
|
|
17
|
+
- Populate `identity_canonical_name` on every insert in `OfficialRecordWriter`:
|
|
18
|
+
`llm_conversations`, `llm_messages` (user and assistant), `llm_message_inference_requests`,
|
|
19
|
+
`llm_message_inference_responses`, and `llm_message_inference_metrics`.
|
|
20
|
+
- Populate `identity_principal_id` and `identity_id` on inserts into `llm_messages`,
|
|
21
|
+
`llm_message_inference_responses`, and `llm_message_inference_metrics`.
|
|
22
|
+
- Backfill `identity_canonical_name` in `enrich_request!` and `enrich_response!` when the
|
|
23
|
+
enrichment opportunity arrives after a metering-first write.
|
|
24
|
+
- `Runners::Tools` extracts identity via `CallerIdentity.normalize` and writes
|
|
25
|
+
`identity_canonical_name`, `identity_principal_id`, and `identity_id` to both
|
|
26
|
+
`llm_tool_calls` and `llm_tool_call_attempts`.
|
|
27
|
+
- `Runners::RegistryAvailability` writes `identity_canonical_name` from AMQP headers or
|
|
28
|
+
body identity fields when present; `identity_principal_id` and `identity_id` FK columns
|
|
29
|
+
are not resolved because node/service identities may not be registered in identity tables.
|
|
30
|
+
|
|
31
|
+
## [0.3.3] - 2026-05-17
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Extract inline `<think>` / `<thinking>` tags from string responses into `response_thinking_json` at write time instead of leaving them in `response_json`.
|
|
35
|
+
- Fall back to `ThinkingExtractor` when `response_thinking` is absent from the audit payload (covers Ollama, vLLM, and OpenAI-compatible gateways that pass thinking inline).
|
|
36
|
+
- Guard `finish_reason` and `thinking_response` against `String#dig` TypeError when `body[:response]` is a plain string.
|
|
37
|
+
|
|
3
38
|
## [0.3.2] - 2026-05-13
|
|
4
39
|
|
|
5
40
|
### Fixed
|
data/lex-llm-ledger.gemspec
CHANGED
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
end
|
|
29
29
|
spec.require_paths = ['lib']
|
|
30
30
|
|
|
31
|
-
spec.add_dependency 'legion-data', '>= 1.8.
|
|
31
|
+
spec.add_dependency 'legion-data', '>= 1.8.7'
|
|
32
32
|
spec.add_dependency 'legion-json', '>= 1.2'
|
|
33
33
|
spec.add_dependency 'legion-logging', '>= 1.3'
|
|
34
34
|
spec.add_dependency 'legion-settings', '>= 1.3'
|
|
@@ -13,9 +13,9 @@ module Legion
|
|
|
13
13
|
extend Legion::Logging::Helper
|
|
14
14
|
|
|
15
15
|
LEGACY_TABLES = %i[
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
z_archive_llm_prompt_records
|
|
17
|
+
z_archive_llm_metering_records
|
|
18
|
+
z_archive_llm_tool_records
|
|
19
19
|
llm_registry_availability_records
|
|
20
20
|
].freeze
|
|
21
21
|
|
|
@@ -43,11 +43,11 @@ module Legion
|
|
|
43
43
|
|
|
44
44
|
def backfill_row(table, row)
|
|
45
45
|
case table
|
|
46
|
-
when :
|
|
46
|
+
when :z_archive_llm_prompt_records
|
|
47
47
|
backfill_prompt(row)
|
|
48
|
-
when :
|
|
48
|
+
when :z_archive_llm_metering_records
|
|
49
49
|
backfill_metering(row)
|
|
50
|
-
when :
|
|
50
|
+
when :z_archive_llm_tool_records
|
|
51
51
|
backfill_tool(row)
|
|
52
52
|
when :llm_registry_availability_records
|
|
53
53
|
backfill_registry(row)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
alter_table(:llm_registry_availability_records) do
|
|
6
|
+
add_column :access_scope, String, size: 20, null: false, default: 'global'
|
|
7
|
+
add_column :identity_principal_id, Integer, null: true
|
|
8
|
+
add_column :identity_id, Integer, null: true
|
|
9
|
+
add_column :identity_canonical_name, String, size: 255, null: true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
alter_table(:llm_registry_availability_records) do
|
|
13
|
+
add_index :identity_principal_id
|
|
14
|
+
add_index :identity_id
|
|
15
|
+
add_index :access_scope
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
down do
|
|
20
|
+
alter_table(:llm_registry_availability_records) do
|
|
21
|
+
drop_index :identity_principal_id
|
|
22
|
+
drop_index :identity_id
|
|
23
|
+
drop_index :access_scope
|
|
24
|
+
drop_column :access_scope
|
|
25
|
+
drop_column :identity_principal_id
|
|
26
|
+
drop_column :identity_id
|
|
27
|
+
drop_column :identity_canonical_name
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
rename_table :llm_metering_records, :z_archive_llm_metering_records
|
|
6
|
+
rename_table :llm_prompt_records, :z_archive_llm_prompt_records
|
|
7
|
+
rename_table :llm_tool_records, :z_archive_llm_tool_records
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
down do
|
|
11
|
+
rename_table :z_archive_llm_metering_records, :llm_metering_records
|
|
12
|
+
rename_table :z_archive_llm_prompt_records, :llm_prompt_records
|
|
13
|
+
rename_table :z_archive_llm_tool_records, :llm_tool_records
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/extensions/llm/responses/thinking_extractor'
|
|
3
4
|
require_relative '../helpers/caller_identity'
|
|
4
5
|
require_relative '../helpers/json'
|
|
5
6
|
|
|
@@ -96,7 +97,30 @@ module Legion
|
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
def response_thinking(body)
|
|
99
|
-
body[:response_thinking] || body[:thinking]
|
|
100
|
+
thinking = body[:response_thinking] || body[:thinking]
|
|
101
|
+
thinking ||= body.dig(:response, :thinking) if body[:response].is_a?(Hash)
|
|
102
|
+
if thinking
|
|
103
|
+
thinking.is_a?(Hash) ? thinking : { content: thinking }
|
|
104
|
+
else
|
|
105
|
+
extract_thinking_from_content(body)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_thinking_from_content(body)
|
|
110
|
+
content_str = body[:response_content] || body[:response] || body[:content]
|
|
111
|
+
return {} unless content_str.is_a?(String)
|
|
112
|
+
|
|
113
|
+
_clean, extracted = extract_inline_thinking(content_str)
|
|
114
|
+
extracted ? { content: extracted } : {}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_inline_thinking(text)
|
|
118
|
+
if defined?(::Legion::Extensions::Llm::Responses::ThinkingExtractor)
|
|
119
|
+
extraction = ::Legion::Extensions::Llm::Responses::ThinkingExtractor.extract(text)
|
|
120
|
+
[extraction.content, extraction.thinking]
|
|
121
|
+
else
|
|
122
|
+
[text, nil]
|
|
123
|
+
end
|
|
100
124
|
end
|
|
101
125
|
|
|
102
126
|
def official_prompt_payload(body, ctx, props, headers, expires_at)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../helpers/caller_identity'
|
|
3
4
|
require_relative '../helpers/json'
|
|
4
5
|
require_relative '../helpers/persistence_logging'
|
|
5
6
|
|
|
@@ -13,10 +14,11 @@ module Legion
|
|
|
13
14
|
|
|
14
15
|
def write_registry_availability_record(payload = nil, metadata = {}, **message)
|
|
15
16
|
payload, metadata = normalize_runner_args(payload, metadata, message)
|
|
16
|
-
|
|
17
|
+
headers = Helpers::SubscriptionMessage.extract_headers(payload, metadata)
|
|
18
|
+
props = metadata[:properties] || {}
|
|
17
19
|
|
|
18
20
|
body = symbolize(payload)
|
|
19
|
-
record = build_registry_availability_record(body, props)
|
|
21
|
+
record = build_registry_availability_record(body, props, headers)
|
|
20
22
|
Helpers::PersistenceLogging.insert_row(
|
|
21
23
|
::Legion::Data.connection,
|
|
22
24
|
:llm_registry_availability_records,
|
|
@@ -38,41 +40,55 @@ module Legion
|
|
|
38
40
|
Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
def build_registry_availability_record(body, props)
|
|
43
|
+
def build_registry_availability_record(body, props, headers)
|
|
42
44
|
offering = body[:offering] || {}
|
|
43
45
|
runtime = body[:runtime] || {}
|
|
44
46
|
lane = body[:lane]
|
|
45
47
|
|
|
46
48
|
{
|
|
47
|
-
event_id:
|
|
48
|
-
message_id:
|
|
49
|
-
correlation_id:
|
|
50
|
-
routing_key:
|
|
51
|
-
event_type:
|
|
52
|
-
occurred_at:
|
|
53
|
-
offering_id:
|
|
54
|
-
provider_family:
|
|
55
|
-
provider_instance:
|
|
56
|
-
instance_id:
|
|
57
|
-
model_family:
|
|
58
|
-
model_id:
|
|
59
|
-
canonical_model:
|
|
60
|
-
provider_model:
|
|
61
|
-
usage_type:
|
|
62
|
-
transport:
|
|
63
|
-
lane_key:
|
|
64
|
-
worker_id:
|
|
65
|
-
node_id:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
49
|
+
event_id: body[:event_id],
|
|
50
|
+
message_id: props[:message_id],
|
|
51
|
+
correlation_id: props[:correlation_id],
|
|
52
|
+
routing_key: props[:routing_key],
|
|
53
|
+
event_type: body[:event_type].to_s,
|
|
54
|
+
occurred_at: body[:occurred_at],
|
|
55
|
+
offering_id: offering[:offering_id],
|
|
56
|
+
provider_family: offering[:provider_family]&.to_s,
|
|
57
|
+
provider_instance: offering[:provider_instance]&.to_s,
|
|
58
|
+
instance_id: offering[:instance_id]&.to_s,
|
|
59
|
+
model_family: offering[:model_family]&.to_s,
|
|
60
|
+
model_id: offering[:model],
|
|
61
|
+
canonical_model: offering[:canonical_model_alias],
|
|
62
|
+
provider_model: offering[:provider_model],
|
|
63
|
+
usage_type: offering[:usage_type]&.to_s,
|
|
64
|
+
transport: offering[:transport]&.to_s,
|
|
65
|
+
lane_key: lane_key(lane),
|
|
66
|
+
worker_id: runtime[:worker_id] || runtime[:worker],
|
|
67
|
+
node_id: runtime[:node_id] || runtime[:host_id],
|
|
68
|
+
identity_canonical_name: extract_canonical_name(body, headers),
|
|
69
|
+
offering_json: json_dump(offering),
|
|
70
|
+
runtime_json: json_dump(runtime),
|
|
71
|
+
capacity_json: json_dump(body[:capacity] || {}),
|
|
72
|
+
health_json: json_dump(body[:health] || {}),
|
|
73
|
+
lane_json: json_dump(lane || {}),
|
|
74
|
+
metadata_json: json_dump(body[:metadata] || {}),
|
|
75
|
+
inserted_at: Time.now.utc
|
|
73
76
|
}
|
|
74
77
|
end
|
|
75
78
|
|
|
79
|
+
# Extract identity_canonical_name from AMQP headers or body.
|
|
80
|
+
# No FK resolution — node/service identities may not be registered in
|
|
81
|
+
# identity tables, but the canonical name string is still valuable for
|
|
82
|
+
# tracking which identity is publishing which model availability.
|
|
83
|
+
def extract_canonical_name(body, headers)
|
|
84
|
+
raw = headers['x-legion-identity'] ||
|
|
85
|
+
body.dig(:identity, :identity) ||
|
|
86
|
+
body.dig(:identity, :canonical_name)
|
|
87
|
+
return nil unless raw && !raw.to_s.empty? # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
88
|
+
|
|
89
|
+
raw.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
76
92
|
def lane_key(lane)
|
|
77
93
|
if lane.is_a?(String)
|
|
78
94
|
lane
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'securerandom'
|
|
3
5
|
require_relative '../helpers/caller_identity'
|
|
4
6
|
require_relative '../helpers/json'
|
|
5
7
|
require_relative '../helpers/persistence_logging'
|
|
@@ -21,19 +23,25 @@ module Legion
|
|
|
21
23
|
ctx = body[:message_context] || {}
|
|
22
24
|
tool = body[:tool_call] || {}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
Helpers::Retention.resolve(
|
|
25
27
|
retention: headers['x-legion-retention'],
|
|
26
28
|
contains_phi: headers['x-legion-contains-phi'] == 'true'
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
db = ::Legion::Data.connection
|
|
32
|
+
write_result = [:ok]
|
|
33
|
+
db.transaction do
|
|
34
|
+
response = find_or_resolve_response(db, body, ctx, props, headers)
|
|
35
|
+
identity_attrs = extract_identity_attrs(body, headers, db)
|
|
36
|
+
tool_call_row, new_tool_call = find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs)
|
|
37
|
+
if tool_call_row && !new_tool_call
|
|
38
|
+
write_result[0] = :duplicate
|
|
39
|
+
elsif new_tool_call
|
|
40
|
+
find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
{ result: write_result[0] }
|
|
37
45
|
rescue Sequel::UniqueConstraintViolation => e
|
|
38
46
|
log.warn("write_tool_record duplicate insert ignored: #{e.message}")
|
|
39
47
|
{ result: :duplicate }
|
|
@@ -54,46 +62,202 @@ module Legion
|
|
|
54
62
|
Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
# Resolve the llm_message_inference_responses row this tool call belongs to.
|
|
66
|
+
# Returns nil if we cannot link at all.
|
|
67
|
+
def find_or_resolve_response(db, body, ctx, props, headers)
|
|
68
|
+
request_ref = ctx[:request_id] || body[:request_id] ||
|
|
69
|
+
props[:correlation_id] || headers['x-legion-llm-request-id']
|
|
70
|
+
return nil unless request_ref # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
71
|
+
|
|
72
|
+
request = db[:llm_message_inference_requests].where(request_ref: request_ref).first
|
|
73
|
+
return nil unless request # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
74
|
+
|
|
75
|
+
db[:llm_message_inference_responses]
|
|
76
|
+
.where(message_inference_request_id: request[:id]).first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs)
|
|
80
|
+
tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
|
|
81
|
+
existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
|
|
82
|
+
return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
83
|
+
|
|
84
|
+
response_id = resolve_response_id(db, response, body, ctx, headers, tool_uuid)
|
|
85
|
+
return [nil, false] unless response_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
86
|
+
|
|
87
|
+
next_index = db[:llm_tool_calls]
|
|
88
|
+
.where(message_inference_response_id: response_id)
|
|
89
|
+
.max(:tool_call_index).to_i + 1
|
|
90
|
+
|
|
91
|
+
src = tool[:source] || {}
|
|
92
|
+
status = tool[:status] || headers['x-legion-tool-status'] || 'success'
|
|
93
|
+
ts = body[:timestamps] || {}
|
|
94
|
+
|
|
95
|
+
id = insert_with_savepoint(db, :llm_tool_calls, {
|
|
96
|
+
uuid: tool_uuid,
|
|
97
|
+
message_inference_response_id: response_id,
|
|
98
|
+
tool_call_index: next_index,
|
|
99
|
+
provider_tool_call_ref: tool[:id],
|
|
100
|
+
tool_name: tool[:name] || headers['x-legion-tool-name'],
|
|
101
|
+
tool_source_type: src[:type] || headers['x-legion-tool-source-type'],
|
|
102
|
+
tool_source_server: src[:server] || headers['x-legion-tool-source-server'],
|
|
103
|
+
status: status,
|
|
104
|
+
requested_at: ts[:tool_start] || tool[:started_at],
|
|
105
|
+
completed_at: ts[:tool_end] || tool[:finished_at],
|
|
106
|
+
**identity_attrs,
|
|
107
|
+
inserted_at: Time.now.utc
|
|
108
|
+
}, operation: 'write_tool_record.tool_call')
|
|
109
|
+
[db[:llm_tool_calls][id: id], true]
|
|
110
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
111
|
+
log.debug("[ledger] tool_call collision resolved uuid=#{tool_uuid} error=#{e.class}")
|
|
112
|
+
row = db[:llm_tool_calls].where(uuid: tool_uuid).first
|
|
113
|
+
raise(e) unless row
|
|
114
|
+
|
|
115
|
+
[row, false]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Extract or fall back to find a response_id for linking the tool call.
|
|
119
|
+
def resolve_response_id(db, response, body, ctx, headers, tool_uuid)
|
|
120
|
+
return response[:id] if response # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
121
|
+
|
|
122
|
+
fallback = fallback_response_for_conversation(db, body, ctx, headers)
|
|
123
|
+
return fallback[:id] if fallback # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
124
|
+
|
|
125
|
+
log.warn("[ledger] write_tool_record: no response row found for tool call uuid=#{tool_uuid}, skipping")
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fallback_response_for_conversation(db, body, ctx, headers)
|
|
130
|
+
conv_id = ctx[:conversation_id] || body[:conversation_id] ||
|
|
131
|
+
headers['x-legion-llm-conversation-id']
|
|
132
|
+
return nil unless conv_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
133
|
+
|
|
134
|
+
conv = db[:llm_conversations].where(uuid: stable_uuid(conv_id)).first ||
|
|
135
|
+
db[:llm_conversations].where(uuid: conv_id).first
|
|
136
|
+
return nil unless conv # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
137
|
+
|
|
138
|
+
db[:llm_message_inference_responses]
|
|
139
|
+
.join(:llm_message_inference_requests,
|
|
140
|
+
id: :message_inference_request_id)
|
|
141
|
+
.where(Sequel[:llm_message_inference_requests][:conversation_id] => conv[:id])
|
|
142
|
+
.order(Sequel.desc(Sequel[:llm_message_inference_responses][:id]))
|
|
143
|
+
.select_all(:llm_message_inference_responses)
|
|
144
|
+
.first
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs) # rubocop:disable Metrics/CyclomaticComplexity
|
|
148
|
+
return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
149
|
+
|
|
150
|
+
tool_call_id = tool_call_row[:id]
|
|
151
|
+
attempt_no = db[:llm_tool_call_attempts]
|
|
152
|
+
.where(tool_call_id: tool_call_id).max(:attempt_no).to_i + 1
|
|
153
|
+
attempt_uuid = derive_attempt_uuid(tool_call_row[:uuid], attempt_no)
|
|
154
|
+
|
|
155
|
+
existing = db[:llm_tool_call_attempts].where(uuid: attempt_uuid).first
|
|
156
|
+
return existing if existing # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
157
|
+
|
|
158
|
+
status = tool[:status] || headers['x-legion-tool-status'] || 'success'
|
|
159
|
+
error_info = tool[:error] || body[:error]
|
|
160
|
+
error_hash = error_info.is_a?(Hash) ? error_info : {}
|
|
161
|
+
ts = body[:timestamps] || {}
|
|
162
|
+
runner_ref = body[:worker_id] || body[:runner_ref] || props[:app_id]
|
|
163
|
+
|
|
164
|
+
id = insert_with_savepoint(db, :llm_tool_call_attempts, {
|
|
165
|
+
uuid: attempt_uuid,
|
|
166
|
+
tool_call_id: tool_call_id,
|
|
167
|
+
attempt_no: attempt_no,
|
|
168
|
+
runner_ref: runner_ref,
|
|
169
|
+
status: status,
|
|
170
|
+
error_category: error_hash[:category] || error_hash[:type],
|
|
171
|
+
error_code: error_hash[:code],
|
|
172
|
+
error_message: error_info.is_a?(String) ? error_info : error_hash[:message],
|
|
173
|
+
duration_ms: tool[:duration_ms].to_i,
|
|
174
|
+
arguments_ref: sha256_ref(tool[:arguments]),
|
|
175
|
+
result_ref: sha256_ref(tool[:result] || body[:result]),
|
|
176
|
+
started_at: ts[:tool_start] || tool[:started_at],
|
|
177
|
+
ended_at: ts[:tool_end] || tool[:finished_at],
|
|
178
|
+
**identity_attrs,
|
|
179
|
+
inserted_at: Time.now.utc
|
|
180
|
+
}, operation: 'write_tool_record.attempt')
|
|
181
|
+
db[:llm_tool_call_attempts][id: id]
|
|
182
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
183
|
+
log.debug("[ledger] tool_call_attempt collision resolved uuid=#{attempt_uuid} error=#{e.class}")
|
|
184
|
+
db[:llm_tool_call_attempts].where(uuid: attempt_uuid).first || raise(e)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def extract_identity_attrs(body, headers, db)
|
|
64
188
|
caller_identity = Helpers::CallerIdentity.normalize(
|
|
65
|
-
caller_raw:
|
|
189
|
+
caller_raw: body[:caller],
|
|
190
|
+
identity: body[:identity],
|
|
191
|
+
headers: headers
|
|
66
192
|
)
|
|
67
|
-
|
|
193
|
+
# raw_identity may carry a "type:value" prefix that OfficialRecordWriter
|
|
194
|
+
# knows how to parse; keep it intact for FK resolution.
|
|
195
|
+
raw_identity = caller_identity[:identity]
|
|
196
|
+
canonical_name = raw_identity
|
|
197
|
+
# Strip "type:" prefix added by CallerIdentity for generic identities
|
|
198
|
+
if canonical_name&.include?(':') && !canonical_name&.include?('@')
|
|
199
|
+
_prefix, remainder = canonical_name.split(':', 2)
|
|
200
|
+
canonical_name = remainder if remainder && !remainder.empty?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
refs = resolve_tool_identity(db, body, raw_identity)
|
|
68
204
|
|
|
69
205
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
206
|
+
identity_canonical_name: canonical_name,
|
|
207
|
+
identity_principal_id: refs[:principal_id],
|
|
208
|
+
identity_id: refs[:identity_id]
|
|
209
|
+
}.compact
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def resolve_tool_identity(db, body, raw_identity)
|
|
213
|
+
return {} unless raw_identity
|
|
214
|
+
return {} unless Writers::OfficialRecordWriter.identity_tables_available?(db)
|
|
215
|
+
|
|
216
|
+
# Merge the header-resolved identity string into the body so that
|
|
217
|
+
# OfficialRecordWriter.resolve_identity can find it via
|
|
218
|
+
# parsed_identity_descriptor even when identity came solely from
|
|
219
|
+
# AMQP headers and is absent from the payload body.
|
|
220
|
+
body_with_identity = raw_identity ? body.merge(caller_identity: raw_identity) : body
|
|
221
|
+
Writers::OfficialRecordWriter.resolve_identity(db, body_with_identity)
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
handle_exception(e, level: :warn, handled: true, operation: 'write_tool_record.identity_resolution')
|
|
224
|
+
{}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def derive_tool_call_uuid(body, ctx, tool, headers)
|
|
228
|
+
ref = tool[:id] ||
|
|
229
|
+
ctx[:request_id] ||
|
|
230
|
+
body[:request_id] ||
|
|
231
|
+
headers['x-legion-llm-request-id'] ||
|
|
232
|
+
ctx[:message_id] ||
|
|
233
|
+
(body[:properties] || {})[:message_id]
|
|
234
|
+
stable_uuid("tool_call:#{ref || SecureRandom.uuid}")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def derive_attempt_uuid(tool_call_uuid, attempt_no)
|
|
238
|
+
stable_uuid("attempt:#{tool_call_uuid}:#{attempt_no}")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def sha256_ref(value)
|
|
242
|
+
return nil if value.nil? # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
243
|
+
|
|
244
|
+
raw = value.is_a?(String) ? value : Helpers::Json.dump(value)
|
|
245
|
+
Digest::SHA256.hexdigest(raw)[0, 64]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def stable_uuid(value)
|
|
249
|
+
raw = value.to_s
|
|
250
|
+
return raw if raw.length <= 36 # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
251
|
+
|
|
252
|
+
hex = Digest::SHA256.hexdigest(raw)[0, 32]
|
|
253
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def insert_with_savepoint(db, table, attributes, operation:)
|
|
257
|
+
db.transaction(savepoint: true) do
|
|
258
|
+
Helpers::PersistenceLogging.insert_row(db, table, attributes,
|
|
259
|
+
operation: operation, warn_on_unique: false)
|
|
260
|
+
end
|
|
97
261
|
end
|
|
98
262
|
|
|
99
263
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require 'securerandom'
|
|
5
5
|
require 'legion/logging'
|
|
6
|
+
require 'legion/extensions/llm/responses/thinking_extractor'
|
|
6
7
|
require_relative '../helpers/json'
|
|
7
8
|
require_relative '../helpers/persistence_logging'
|
|
8
9
|
|
|
@@ -57,17 +58,18 @@ module Legion
|
|
|
57
58
|
return existing if existing
|
|
58
59
|
|
|
59
60
|
id = insert_with_savepoint(db, :llm_conversations, {
|
|
60
|
-
uuid:
|
|
61
|
-
title:
|
|
62
|
-
classification_level:
|
|
63
|
-
contains_phi:
|
|
64
|
-
contains_pii:
|
|
65
|
-
retention_policy:
|
|
66
|
-
expires_at:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
uuid: uuid,
|
|
62
|
+
title: body[:title] || body[:conversation_title],
|
|
63
|
+
classification_level: classification_level(body),
|
|
64
|
+
contains_phi: contains_phi?(body),
|
|
65
|
+
contains_pii: contains_pii?(body),
|
|
66
|
+
retention_policy: body[:retention_policy] || 'default',
|
|
67
|
+
expires_at: body[:expires_at],
|
|
68
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
69
|
+
recorded_at: recorded_at(body),
|
|
70
|
+
inserted_at: Time.now.utc,
|
|
71
|
+
created_at: Time.now.utc,
|
|
72
|
+
updated_at: Time.now.utc
|
|
71
73
|
}, operation: 'official_record_writer.conversation')
|
|
72
74
|
db[:llm_conversations][id: id]
|
|
73
75
|
rescue Sequel::UniqueConstraintViolation => e
|
|
@@ -86,16 +88,19 @@ module Legion
|
|
|
86
88
|
seq = body[:message_seq] ? integer(body[:message_seq]) : next_message_seq(db, conversation)
|
|
87
89
|
begin
|
|
88
90
|
id = insert_with_savepoint(db, :llm_messages, {
|
|
89
|
-
uuid:
|
|
90
|
-
conversation_id:
|
|
91
|
-
seq:
|
|
92
|
-
role:
|
|
93
|
-
content_type:
|
|
94
|
-
content:
|
|
95
|
-
input_tokens:
|
|
96
|
-
output_tokens:
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
uuid: uuid,
|
|
92
|
+
conversation_id: conversation[:id],
|
|
93
|
+
seq: seq,
|
|
94
|
+
role: 'user',
|
|
95
|
+
content_type: 'text',
|
|
96
|
+
content: request_content(body),
|
|
97
|
+
input_tokens: tokens(body)[:input_tokens],
|
|
98
|
+
output_tokens: 0,
|
|
99
|
+
identity_principal_id: caller_identity_refs(db, body)[:principal_id],
|
|
100
|
+
identity_id: caller_identity_refs(db, body)[:identity_id],
|
|
101
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
102
|
+
created_at: recorded_at(body),
|
|
103
|
+
inserted_at: Time.now.utc
|
|
99
104
|
}, operation: 'official_record_writer.user_message')
|
|
100
105
|
db[:llm_messages][id: id]
|
|
101
106
|
rescue Sequel::UniqueConstraintViolation => e
|
|
@@ -113,28 +118,31 @@ module Legion
|
|
|
113
118
|
operation = operation(body)
|
|
114
119
|
caller_refs = caller_identity_refs(db, body)
|
|
115
120
|
id = insert_with_savepoint(db, :llm_message_inference_requests, {
|
|
116
|
-
uuid:
|
|
117
|
-
conversation_id:
|
|
118
|
-
latest_message_id:
|
|
119
|
-
caller_principal_id:
|
|
120
|
-
caller_identity_id:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
uuid: stable_uuid(request_id),
|
|
122
|
+
conversation_id: conversation[:id],
|
|
123
|
+
latest_message_id: latest_message&.dig(:id),
|
|
124
|
+
caller_principal_id: caller_refs[:principal_id],
|
|
125
|
+
caller_identity_id: caller_refs[:identity_id],
|
|
126
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
127
|
+
runtime_caller_type: caller_type(body),
|
|
128
|
+
runtime_caller_class: runtime_caller_class(body),
|
|
129
|
+
runtime_caller_client: runtime_caller_client(body),
|
|
130
|
+
request_ref: request_id,
|
|
131
|
+
correlation_ref: correlation_id(body),
|
|
132
|
+
correlation_id: correlation_id(body),
|
|
133
|
+
exchange_ref: body[:exchange_id],
|
|
134
|
+
request_type: operation,
|
|
135
|
+
operation: operation,
|
|
136
|
+
idempotency_key: body[:idempotency_key] || request_id,
|
|
137
|
+
status: 'responded',
|
|
138
|
+
context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
|
|
139
|
+
request_capture_mode: 'full',
|
|
140
|
+
request_json: json_dump(request_payload(body)),
|
|
141
|
+
classification_level: classification_level(body),
|
|
142
|
+
cost_center: billing(body)[:cost_center],
|
|
143
|
+
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
144
|
+
requested_at: recorded_at(body),
|
|
145
|
+
inserted_at: Time.now.utc
|
|
138
146
|
}, operation: 'official_record_writer.inference_request')
|
|
139
147
|
db[:llm_message_inference_requests][id: id]
|
|
140
148
|
rescue Sequel::UniqueConstraintViolation => e
|
|
@@ -164,6 +172,9 @@ module Legion
|
|
|
164
172
|
content: response_content(body),
|
|
165
173
|
input_tokens: 0,
|
|
166
174
|
output_tokens: tokens(body)[:output_tokens],
|
|
175
|
+
identity_principal_id: caller_identity_refs(db, body)[:principal_id],
|
|
176
|
+
identity_id: caller_identity_refs(db, body)[:identity_id],
|
|
177
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
167
178
|
created_at: recorded_at(body),
|
|
168
179
|
inserted_at: Time.now.utc
|
|
169
180
|
}, operation: 'official_record_writer.response_message')
|
|
@@ -201,6 +212,9 @@ module Legion
|
|
|
201
212
|
response_json: json_dump(visible_response(body)),
|
|
202
213
|
response_thinking_json: json_dump(thinking_response(body)),
|
|
203
214
|
dispatch_path: body[:dispatch_path] || body[:tier],
|
|
215
|
+
identity_principal_id: caller_identity_refs(db, body)[:principal_id],
|
|
216
|
+
identity_id: caller_identity_refs(db, body)[:identity_id],
|
|
217
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
204
218
|
responded_at: recorded_at(body),
|
|
205
219
|
inserted_at: Time.now.utc
|
|
206
220
|
}, operation: 'official_record_writer.inference_response')
|
|
@@ -223,6 +237,7 @@ module Legion
|
|
|
223
237
|
update_if_missing(updates, existing, :provider_instance, provider_instance(body))
|
|
224
238
|
update_if_missing(updates, existing, :finish_reason, finish_reason(body))
|
|
225
239
|
update_if_missing(updates, existing, :dispatch_path, body[:dispatch_path] || body[:tier])
|
|
240
|
+
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
226
241
|
|
|
227
242
|
response_json = json_dump(visible_response(body))
|
|
228
243
|
update_if_placeholder(updates, existing, :response_json, response_json)
|
|
@@ -267,6 +282,9 @@ module Legion
|
|
|
267
282
|
currency: body[:currency] || 'USD',
|
|
268
283
|
cost_center: billing(body)[:cost_center],
|
|
269
284
|
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
285
|
+
identity_principal_id: caller_identity_refs(db, body)[:principal_id],
|
|
286
|
+
identity_id: caller_identity_refs(db, body)[:identity_id],
|
|
287
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
270
288
|
recorded_at: recorded_at(body),
|
|
271
289
|
inserted_at: Time.now.utc
|
|
272
290
|
}, operation: 'official_record_writer.inference_metric')
|
|
@@ -309,6 +327,9 @@ module Legion
|
|
|
309
327
|
updates[:caller_identity_id] = caller_refs[:identity_id] if existing[:caller_identity_id].nil? && caller_refs[:identity_id]
|
|
310
328
|
updates[:caller_principal_id] = caller_refs[:principal_id] if existing[:caller_principal_id].nil? && caller_refs[:principal_id]
|
|
311
329
|
updates[:runtime_caller_type] = caller_type(body) if existing[:runtime_caller_type].nil? && caller_type(body)
|
|
330
|
+
update_if_missing(updates, existing, :runtime_caller_class, runtime_caller_class(body))
|
|
331
|
+
update_if_missing(updates, existing, :runtime_caller_client, runtime_caller_client(body))
|
|
332
|
+
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
312
333
|
|
|
313
334
|
request_json = json_dump(request_payload(body))
|
|
314
335
|
updates[:request_json] = request_json if existing[:request_json].to_s == '{}' && request_json != '{}'
|
|
@@ -341,6 +362,16 @@ module Legion
|
|
|
341
362
|
parsed_identity_descriptor(body)[:kind]
|
|
342
363
|
end
|
|
343
364
|
|
|
365
|
+
def runtime_caller_class(body)
|
|
366
|
+
body.dig(:caller, :class) || body.dig(:caller, :caller_class) ||
|
|
367
|
+
body.dig(:caller, :source_class) || body[:runtime_caller_class]
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def runtime_caller_client(body)
|
|
371
|
+
body.dig(:caller, :client) || body.dig(:caller, :user_agent) ||
|
|
372
|
+
body[:runtime_caller_client]
|
|
373
|
+
end
|
|
374
|
+
|
|
344
375
|
def caller_identity_refs(db, body)
|
|
345
376
|
body[:__ledger_caller_identity_refs] ||= begin
|
|
346
377
|
explicit_identity_id = integer_or_nil(body[:caller_identity_id] || body.dig(:caller, :requested_by, :id))
|
|
@@ -580,18 +611,38 @@ module Legion
|
|
|
580
611
|
|
|
581
612
|
def visible_response(body)
|
|
582
613
|
response = body[:response] || body[:response_content] || body[:content] || {}
|
|
583
|
-
|
|
614
|
+
if response.is_a?(String)
|
|
615
|
+
clean, _thinking = extract_inline_thinking(response)
|
|
616
|
+
return { content: clean }
|
|
617
|
+
end
|
|
584
618
|
return { content: response[:content] } if response.is_a?(Hash) && response.key?(:content)
|
|
585
619
|
|
|
586
620
|
response.is_a?(Hash) ? response.except(:thinking) : { content: response.to_s }
|
|
587
621
|
end
|
|
588
622
|
|
|
589
623
|
def thinking_response(body)
|
|
590
|
-
thinking = body[:response_thinking] || body[:thinking]
|
|
591
|
-
|
|
592
|
-
|
|
624
|
+
thinking = body[:response_thinking] || body[:thinking]
|
|
625
|
+
thinking ||= body.dig(:response, :thinking) if body[:response].is_a?(Hash)
|
|
626
|
+
if thinking
|
|
627
|
+
return { content: thinking } if thinking.is_a?(String)
|
|
628
|
+
|
|
629
|
+
return thinking
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
content_str = body[:response_content] || body[:response] || body[:content]
|
|
633
|
+
return {} unless content_str.is_a?(String)
|
|
593
634
|
|
|
594
|
-
|
|
635
|
+
_clean, extracted = extract_inline_thinking(content_str)
|
|
636
|
+
extracted ? { content: extracted } : {}
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def extract_inline_thinking(text)
|
|
640
|
+
if defined?(::Legion::Extensions::Llm::Responses::ThinkingExtractor)
|
|
641
|
+
extraction = ::Legion::Extensions::Llm::Responses::ThinkingExtractor.extract(text)
|
|
642
|
+
[extraction.content, extraction.thinking]
|
|
643
|
+
else
|
|
644
|
+
[text, nil]
|
|
645
|
+
end
|
|
595
646
|
end
|
|
596
647
|
|
|
597
648
|
def response_content(body)
|
|
@@ -599,7 +650,10 @@ module Legion
|
|
|
599
650
|
end
|
|
600
651
|
|
|
601
652
|
def finish_reason(body)
|
|
602
|
-
body[:finish_reason]
|
|
653
|
+
return body[:finish_reason] if body[:finish_reason]
|
|
654
|
+
return nil unless body[:response].is_a?(Hash)
|
|
655
|
+
|
|
656
|
+
body.dig(:response, :finish_reason) || body.dig(:response, :stop, :reason)
|
|
603
657
|
end
|
|
604
658
|
|
|
605
659
|
def classification_level(body)
|
|
@@ -667,6 +721,10 @@ module Legion
|
|
|
667
721
|
end
|
|
668
722
|
end
|
|
669
723
|
|
|
724
|
+
def identity_canonical_name(body)
|
|
725
|
+
parsed_identity_descriptor(body)[:canonical_name]
|
|
726
|
+
end
|
|
727
|
+
|
|
670
728
|
def present?(value)
|
|
671
729
|
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
|
672
730
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-ledger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.8.
|
|
18
|
+
version: 1.8.7
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.8.
|
|
25
|
+
version: 1.8.7
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: legion-json
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -207,6 +207,8 @@ files:
|
|
|
207
207
|
- lib/legion/extensions/llm/ledger/data/migrations/008_relax_message_id_not_null.rb
|
|
208
208
|
- lib/legion/extensions/llm/ledger/data/migrations/009_add_caller_to_metering.rb
|
|
209
209
|
- lib/legion/extensions/llm/ledger/data/migrations/010_add_response_thinking_json_to_prompt_records.rb
|
|
210
|
+
- lib/legion/extensions/llm/ledger/data/migrations/011_add_identity_columns_to_registry_availability.rb
|
|
211
|
+
- lib/legion/extensions/llm/ledger/data/migrations/012_archive_legacy_tables.rb
|
|
210
212
|
- lib/legion/extensions/llm/ledger/helpers/caller_identity.rb
|
|
211
213
|
- lib/legion/extensions/llm/ledger/helpers/decryption.rb
|
|
212
214
|
- lib/legion/extensions/llm/ledger/helpers/json.rb
|