lex-llm-ledger 0.6.0 → 0.7.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 +9 -0
- data/lib/legion/extensions/llm/ledger/runners/escalations.rb +1 -1
- data/lib/legion/extensions/llm/ledger/runners/reconciliation.rb +9 -1
- data/lib/legion/extensions/llm/ledger/runners/tools.rb +2 -2
- data/lib/legion/extensions/llm/ledger/version.rb +1 -1
- data/lib/legion/extensions/llm/ledger/writers/official_record_writer.rb +74 -25
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b0e51e42bfec73669b2065b292a856ba029708010d10fc038d086a3442207e5
|
|
4
|
+
data.tar.gz: dbfbc4bf7b969362ff0363096fac13d33e8484080eddbccb09051fe3b794c0ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18e7a33c0c64c8cb6ebc6ef09c21b174ac2a73ef0643947029e2898ec59bf523e5e0d8c157672908755147b136c98b87117be1f7d1e701d6542862fd9b58e4b3
|
|
7
|
+
data.tar.gz: f9e869ab4703524b7b054d1c1a44c8ab4d2fd1b53a770aa48f471f69c70f124ad4e4b4a90ecd1ca5399e43817394cdbc05248b00108cdca4ee45b494b8c5d4c9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-06-09
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **ASYNC-RACE-01**: Resolved race condition between metering and prompt audit messages causing duplicate response rows and stale `"{}"`/`"null"` JSON placeholders. Added response UUID fallback to `message_inference_request_id` lookup to enrich metering-created shells instead of creating duplicates.
|
|
7
|
+
- **RECONCILIATION-01**: Fixed `UniqueConstraintViolation` in `link_orphaned_tool_calls` by recalculating `tool_call_index` based on existing max index before linking, preventing index collisions.
|
|
8
|
+
- **ENRICHMENT-01**: Implemented strict upsert guards (`upsert_guard?`) to prevent overwriting valid data with `nil`, `''`, or `'{}'`. Centralized validation across `update_if_missing` and `update_if_placeholder`.
|
|
9
|
+
- **PAYLOAD-01**: Updated `request_payload`, `visible_response`, and `thinking_response` to return `nil` instead of `{}` when content is missing. Insert paths now write `nil` to JSON columns, preventing `"{}"` or `"null"` string artifacts.
|
|
10
|
+
- **ENRICHMENT-02**: Expanded `update_if_placeholder` to recognize both `'{}'` and `'null'` as stale markers for retroactive cleanup. Guarded `response_content(body)` against `nil` returns.
|
|
11
|
+
|
|
3
12
|
## [0.6.0] - 2026-05-31
|
|
4
13
|
|
|
5
14
|
### Added
|
|
@@ -49,7 +49,7 @@ module Legion
|
|
|
49
49
|
Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def build_escalation_record(db, body, props, headers)
|
|
52
|
+
def build_escalation_record(db, body, props, headers)
|
|
53
53
|
history = Array(body[:history])
|
|
54
54
|
identity = Helpers::CallerIdentity.normalize(
|
|
55
55
|
caller_raw: body[:caller], identity: body[:identity], headers: headers
|
|
@@ -28,8 +28,16 @@ module Legion
|
|
|
28
28
|
response = find_response_for_tool_call(db, tool_call)
|
|
29
29
|
next unless response
|
|
30
30
|
|
|
31
|
+
# Recalculate tool_call_index to avoid unique constraint collisions.
|
|
32
|
+
# Orphaned tool calls were inserted with index 0 (no response known),
|
|
33
|
+
# but the response may already have tool calls at those indices.
|
|
34
|
+
next_index = db[:llm_tool_calls]
|
|
35
|
+
.where(message_inference_response_id: response[:id])
|
|
36
|
+
.max(:tool_call_index).to_i + 1
|
|
37
|
+
|
|
31
38
|
db[:llm_tool_calls].where(id: tool_call[:id]).update(
|
|
32
|
-
message_inference_response_id: response[:id]
|
|
39
|
+
message_inference_response_id: response[:id],
|
|
40
|
+
tool_call_index: next_index
|
|
33
41
|
)
|
|
34
42
|
linked += 1
|
|
35
43
|
end
|
|
@@ -101,7 +101,7 @@ module Legion
|
|
|
101
101
|
conv&.[](:id)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs, conversation_id)
|
|
104
|
+
def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs, conversation_id)
|
|
105
105
|
tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
|
|
106
106
|
existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
|
|
107
107
|
return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
@@ -152,7 +152,7 @@ module Legion
|
|
|
152
152
|
[row, false]
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs)
|
|
155
|
+
def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs)
|
|
156
156
|
return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
157
157
|
|
|
158
158
|
tool_call_id = tool_call_row[:id]
|
|
@@ -112,7 +112,7 @@ module Legion
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def find_or_create_request(db, conversation, latest_message, body)
|
|
115
|
+
def find_or_create_request(db, conversation, latest_message, body)
|
|
116
116
|
request_id = request_ref(body)
|
|
117
117
|
existing = db[:llm_message_inference_requests].where(request_ref: request_id).first
|
|
118
118
|
return enrich_request!(db, existing, body, latest_message) if existing
|
|
@@ -140,7 +140,10 @@ module Legion
|
|
|
140
140
|
status: 'responded',
|
|
141
141
|
context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
|
|
142
142
|
request_capture_mode: 'full',
|
|
143
|
-
request_json:
|
|
143
|
+
request_json: if request_payload(body)
|
|
144
|
+
phi_protect(json_dump(request_payload(body)),
|
|
145
|
+
contains_phi?(body))
|
|
146
|
+
end,
|
|
144
147
|
classification_level: classification_level(body),
|
|
145
148
|
cost_center: billing(body)[:cost_center],
|
|
146
149
|
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
@@ -194,15 +197,29 @@ module Legion
|
|
|
194
197
|
end
|
|
195
198
|
end
|
|
196
199
|
|
|
197
|
-
def find_or_create_response(db, request, response_message, body)
|
|
200
|
+
def find_or_create_response(db, request, response_message, body)
|
|
198
201
|
response_uuid = stable_uuid(reference(body, :provider_response_ref) || "response:#{request_ref(body)}:#{body[:provider] || 'unknown'}")
|
|
199
202
|
existing = db[:llm_message_inference_responses].where(uuid: response_uuid).first
|
|
203
|
+
|
|
204
|
+
# Fallback: if we couldn't find a response by UUID, check if a response
|
|
205
|
+
# already exists for this request (e.g., metering arrived first and created
|
|
206
|
+
# a response with a different UUID). Enrich it instead of creating a duplicate.
|
|
207
|
+
unless existing
|
|
208
|
+
existing = db[:llm_message_inference_responses]
|
|
209
|
+
.where(message_inference_request_id: request[:id])
|
|
210
|
+
.first
|
|
211
|
+
log.debug("[ledger] response fallback: found existing response id=#{existing[:id]} for request_id=#{request[:id]}") if existing
|
|
212
|
+
end
|
|
213
|
+
|
|
200
214
|
if existing
|
|
201
215
|
enrich_response!(db, existing, response_message, body)
|
|
202
216
|
return existing
|
|
203
217
|
end
|
|
204
218
|
|
|
219
|
+
vis_resp = visible_response(body)
|
|
220
|
+
think_resp = thinking_response(body)
|
|
205
221
|
phi = contains_phi?(body)
|
|
222
|
+
|
|
206
223
|
id = insert_with_savepoint(db, :llm_message_inference_responses, {
|
|
207
224
|
uuid: response_uuid,
|
|
208
225
|
message_inference_request_id: request[:id],
|
|
@@ -218,8 +235,8 @@ module Legion
|
|
|
218
235
|
latency_ms: integer(body[:latency_ms]),
|
|
219
236
|
wall_clock_ms: integer(body[:wall_clock_ms]),
|
|
220
237
|
response_capture_mode: 'full',
|
|
221
|
-
response_json: phi_protect(json_dump(
|
|
222
|
-
response_thinking_json: phi_protect(json_dump(
|
|
238
|
+
response_json: vis_resp ? phi_protect(json_dump(vis_resp), phi) : nil,
|
|
239
|
+
response_thinking_json: think_resp ? phi_protect(json_dump(think_resp), phi) : nil,
|
|
223
240
|
dispatch_path: body[:dispatch_path] || body[:tier],
|
|
224
241
|
error_category: body[:error_category] || body.dig(:error, :category),
|
|
225
242
|
error_code: body[:error_code] || body.dig(:error, :code),
|
|
@@ -254,11 +271,21 @@ module Legion
|
|
|
254
271
|
update_if_missing(updates, existing, :dispatch_path, body[:dispatch_path] || body[:tier])
|
|
255
272
|
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
256
273
|
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
vis = visible_response(body)
|
|
275
|
+
if vis
|
|
276
|
+
response_json = json_dump(vis)
|
|
277
|
+
update_if_placeholder(updates, existing, :response_json, response_json)
|
|
278
|
+
elsif existing[:response_json].nil?
|
|
279
|
+
# Nothing to add, but also don't overwrite existing data with nil
|
|
280
|
+
end
|
|
259
281
|
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
think = thinking_response(body)
|
|
283
|
+
if think
|
|
284
|
+
thinking_json = json_dump(think)
|
|
285
|
+
update_if_placeholder(updates, existing, :response_thinking_json, thinking_json)
|
|
286
|
+
elsif existing[:response_thinking_json].nil?
|
|
287
|
+
# Nothing to add
|
|
288
|
+
end
|
|
262
289
|
|
|
263
290
|
return if updates.empty?
|
|
264
291
|
|
|
@@ -266,12 +293,27 @@ module Legion
|
|
|
266
293
|
log.info("[ledger] enriched response id=#{existing[:id]} fields=#{updates.keys.join(',')}")
|
|
267
294
|
end
|
|
268
295
|
|
|
296
|
+
# Core guard: never upsert a value that is nil, empty string, or empty JSON object.
|
|
297
|
+
# This prevents a leaner message (e.g., metering) from overwriting valid data
|
|
298
|
+
# written by a richer message (e.g., prompt audit).
|
|
299
|
+
def upsert_guard?(value)
|
|
300
|
+
return false if value.nil?
|
|
301
|
+
return false if value.is_a?(String) && value.strip.empty?
|
|
302
|
+
return false if value.to_s == '{}'
|
|
303
|
+
|
|
304
|
+
true
|
|
305
|
+
end
|
|
306
|
+
|
|
269
307
|
def update_if_missing(updates, existing, key, value)
|
|
270
|
-
updates[key] = value if existing[key].nil? &&
|
|
308
|
+
updates[key] = value if existing[key].nil? && upsert_guard?(value)
|
|
271
309
|
end
|
|
272
310
|
|
|
273
311
|
def update_if_placeholder(updates, existing, key, value)
|
|
274
|
-
|
|
312
|
+
return unless upsert_guard?(value)
|
|
313
|
+
|
|
314
|
+
existing_val = existing[key]
|
|
315
|
+
is_placeholder = ['{}', 'null'].include?(existing_val.to_s)
|
|
316
|
+
updates[key] = value if is_placeholder
|
|
275
317
|
end
|
|
276
318
|
|
|
277
319
|
def find_or_create_metric(db, request, response, body)
|
|
@@ -339,15 +381,19 @@ module Legion
|
|
|
339
381
|
updates = {}
|
|
340
382
|
update_if_missing(updates, existing, :latest_message_id, latest_message&.dig(:id))
|
|
341
383
|
caller_refs = caller_identity_refs(db, body)
|
|
342
|
-
updates
|
|
343
|
-
updates
|
|
344
|
-
updates
|
|
384
|
+
update_if_missing(updates, existing, :caller_identity_id, caller_refs[:identity_id])
|
|
385
|
+
update_if_missing(updates, existing, :caller_principal_id, caller_refs[:principal_id])
|
|
386
|
+
update_if_missing(updates, existing, :runtime_caller_type, caller_type(body))
|
|
345
387
|
update_if_missing(updates, existing, :runtime_caller_class, runtime_caller_class(body))
|
|
346
388
|
update_if_missing(updates, existing, :runtime_caller_client, runtime_caller_client(body))
|
|
347
389
|
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
348
390
|
|
|
349
|
-
request_json = json_dump(request_payload(body))
|
|
350
|
-
|
|
391
|
+
request_json = request_payload(body) ? json_dump(request_payload(body)) : nil
|
|
392
|
+
if request_json
|
|
393
|
+
update_if_placeholder(updates, existing, :request_json, request_json)
|
|
394
|
+
elsif existing[:request_json].nil?
|
|
395
|
+
# Nothing to add
|
|
396
|
+
end
|
|
351
397
|
|
|
352
398
|
msg_count = Array(body.dig(:request, :messages) || body[:messages]).size
|
|
353
399
|
updates[:context_message_count] = msg_count if existing[:context_message_count].to_i.zero? && msg_count.positive?
|
|
@@ -574,7 +620,7 @@ module Legion
|
|
|
574
620
|
end
|
|
575
621
|
|
|
576
622
|
def resolve_parent_request_id(db, body)
|
|
577
|
-
parent_ref = body[:parent_request_id] || body.dig(:context, :parent_request_id)
|
|
623
|
+
parent_ref = body[:parent_request_id] || body.dig(:context, :parent_request_id) || body.dig(:caller, :parent_request_ref)
|
|
578
624
|
return nil unless present?(parent_ref)
|
|
579
625
|
|
|
580
626
|
if parent_ref.is_a?(Integer)
|
|
@@ -630,7 +676,7 @@ module Legion
|
|
|
630
676
|
end
|
|
631
677
|
|
|
632
678
|
def request_payload(body)
|
|
633
|
-
body[:request] || body[:messages]
|
|
679
|
+
body[:request] || body[:messages]
|
|
634
680
|
end
|
|
635
681
|
|
|
636
682
|
def request_content(body)
|
|
@@ -641,7 +687,9 @@ module Legion
|
|
|
641
687
|
end
|
|
642
688
|
|
|
643
689
|
def visible_response(body)
|
|
644
|
-
response = body[:response] || body[:response_content] || body[:content]
|
|
690
|
+
response = body[:response] || body[:response_content] || body[:content]
|
|
691
|
+
return nil if response.nil? || (response.is_a?(Hash) && response.empty?)
|
|
692
|
+
|
|
645
693
|
if response.is_a?(String)
|
|
646
694
|
clean, _thinking = extract_inline_thinking(response)
|
|
647
695
|
return { content: clean }
|
|
@@ -661,10 +709,10 @@ module Legion
|
|
|
661
709
|
end
|
|
662
710
|
|
|
663
711
|
content_str = body[:response_content] || body[:response] || body[:content]
|
|
664
|
-
return
|
|
712
|
+
return nil unless content_str.is_a?(String)
|
|
665
713
|
|
|
666
714
|
_clean, extracted = extract_inline_thinking(content_str)
|
|
667
|
-
extracted ? { content: extracted } :
|
|
715
|
+
extracted ? { content: extracted } : nil
|
|
668
716
|
end
|
|
669
717
|
|
|
670
718
|
def extract_inline_thinking(text)
|
|
@@ -677,7 +725,10 @@ module Legion
|
|
|
677
725
|
end
|
|
678
726
|
|
|
679
727
|
def response_content(body)
|
|
680
|
-
|
|
728
|
+
vis = visible_response(body)
|
|
729
|
+
return nil unless vis
|
|
730
|
+
|
|
731
|
+
stringify_content(vis[:content] || vis.dig(:message, :content))
|
|
681
732
|
end
|
|
682
733
|
|
|
683
734
|
def finish_reason(body)
|
|
@@ -788,9 +839,7 @@ module Legion
|
|
|
788
839
|
def resolve_context_tokens(body)
|
|
789
840
|
raw = body[:tokens] || body[:audit] || body
|
|
790
841
|
val = raw[:input_tokens] || raw[:input] || raw[:context_tokens] || raw[:prompt_tokens]
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
val.to_i
|
|
842
|
+
present?(val) ? val.to_i : 0
|
|
794
843
|
end
|
|
795
844
|
end
|
|
796
845
|
end
|