lex-llm-ledger 0.6.0 → 0.7.1
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 +14 -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/registry_availability.rb +41 -0
- 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 +98 -28
- data/lib/legion/extensions/llm/ledger/writers/official_route_attempt_writer.rb +111 -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: 9dc0c64e738b5fe99d37289f48ce3264b7991f7f521d570df80a63615d419972
|
|
4
|
+
data.tar.gz: a551479f32400fb90df6e0e6bc15423c627d783149f9a17f675421cf3d3f9243
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cdf842b8a22047e92f2bd1adba9a953eefaf5bf86b255d54b6e8d22fc9f64545a45c03486348c9732539e34739e74fc43ce61c77d52772aeea93e640f0298ee
|
|
7
|
+
data.tar.gz: cd05c364405a72b9f1bfed2e539778fb301fa36f20e646f645755eb87e21a98261f5b9442a3c2b0938662a640f2708eb8886c0632dbe5118848e7616c48ad4c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **IDENTITY-07**: `OfficialRouteAttemptWriter` called non-existent `CallerIdentity.resolve_principal_id` and `CallerIdentity.resolve_identity_id`. Delegated to `OfficialRecordWriter.caller_identity_refs` which already implements full identity resolution with header fallbacks and DB lookups.
|
|
7
|
+
|
|
8
|
+
## [0.7.0] - 2026-06-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **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.
|
|
12
|
+
- **RECONCILIATION-01**: Fixed `UniqueConstraintViolation` in `link_orphaned_tool_calls` by recalculating `tool_call_index` based on existing max index before linking, preventing index collisions.
|
|
13
|
+
- **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`.
|
|
14
|
+
- **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.
|
|
15
|
+
- **ENRICHMENT-02**: Expanded `update_if_placeholder` to recognize both `'{}'` and `'null'` as stale markers for retroactive cleanup. Guarded `response_content(body)` against `nil` returns.
|
|
16
|
+
|
|
3
17
|
## [0.6.0] - 2026-05-31
|
|
4
18
|
|
|
5
19
|
### 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
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative '../helpers/caller_identity'
|
|
4
4
|
require_relative '../helpers/json'
|
|
5
5
|
require_relative '../helpers/persistence_logging'
|
|
6
|
+
require_relative '../writers/official_record_writer'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module Extensions
|
|
@@ -66,6 +67,8 @@ module Legion
|
|
|
66
67
|
worker_id: runtime[:worker_id] || runtime[:worker],
|
|
67
68
|
node_id: runtime[:node_id] || runtime[:host_id],
|
|
68
69
|
identity_canonical_name: extract_canonical_name(body, headers),
|
|
70
|
+
identity_principal_id: extract_identity_principal_id(body, headers),
|
|
71
|
+
identity_id: extract_identity_id(body, headers),
|
|
69
72
|
offering_json: json_dump(offering),
|
|
70
73
|
runtime_json: json_dump(runtime),
|
|
71
74
|
capacity_json: json_dump(body[:capacity] || {}),
|
|
@@ -89,6 +92,44 @@ module Legion
|
|
|
89
92
|
raw.to_s
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
# Resolve identity_principal_id from AMQP headers or body.
|
|
96
|
+
# Uses OfficialRecordWriter.identity_tables_available? to check if FK resolution is possible.
|
|
97
|
+
def extract_identity_principal_id(body, headers)
|
|
98
|
+
raw = headers['x-legion-identity'] ||
|
|
99
|
+
body.dig(:identity, :identity) ||
|
|
100
|
+
body.dig(:identity, :canonical_name)
|
|
101
|
+
return nil unless raw && !raw.to_s.empty? # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
102
|
+
|
|
103
|
+
db = ::Legion::Data.connection
|
|
104
|
+
return nil unless Writers::OfficialRecordWriter.identity_tables_available?(db) # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
105
|
+
|
|
106
|
+
body_with_identity = body.merge(caller_identity: raw)
|
|
107
|
+
refs = Writers::OfficialRecordWriter.resolve_identity(db, body_with_identity)
|
|
108
|
+
refs[:principal_id]
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
handle_exception(e, level: :warn, handled: true, operation: 'write_registry_availability_record.identity_principal')
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Resolve identity_id from AMQP headers or body.
|
|
115
|
+
# Uses OfficialRecordWriter.identity_tables_available? to check if FK resolution is possible.
|
|
116
|
+
def extract_identity_id(body, headers)
|
|
117
|
+
raw = headers['x-legion-identity'] ||
|
|
118
|
+
body.dig(:identity, :identity) ||
|
|
119
|
+
body.dig(:identity, :canonical_name)
|
|
120
|
+
return nil unless raw && !raw.to_s.empty? # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
121
|
+
|
|
122
|
+
db = ::Legion::Data.connection
|
|
123
|
+
return nil unless Writers::OfficialRecordWriter.identity_tables_available?(db) # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
124
|
+
|
|
125
|
+
body_with_identity = body.merge(caller_identity: raw)
|
|
126
|
+
refs = Writers::OfficialRecordWriter.resolve_identity(db, body_with_identity)
|
|
127
|
+
refs[:identity_id]
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
handle_exception(e, level: :warn, handled: true, operation: 'write_registry_availability_record.identity')
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
92
133
|
def lane_key(lane)
|
|
93
134
|
if lane.is_a?(String)
|
|
94
135
|
lane
|
|
@@ -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]
|
|
@@ -6,6 +6,7 @@ require 'legion/logging'
|
|
|
6
6
|
require 'legion/extensions/llm/responses/thinking_extractor'
|
|
7
7
|
require_relative '../helpers/json'
|
|
8
8
|
require_relative '../helpers/persistence_logging'
|
|
9
|
+
require_relative 'official_route_attempt_writer'
|
|
9
10
|
|
|
10
11
|
module Legion
|
|
11
12
|
module Extensions
|
|
@@ -30,6 +31,7 @@ module Legion
|
|
|
30
31
|
response = find_or_create_response(db, request, response_message, body)
|
|
31
32
|
link_response_message!(db, response_message, response)
|
|
32
33
|
metric = find_or_create_metric(db, request, response, body)
|
|
34
|
+
OfficialRouteAttemptWriter.write_route_attempts(db, request, response, body)
|
|
33
35
|
result = { result: :ok, request_id: request[:id], response_id: response[:id], metric_id: metric[:id] }
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -46,6 +48,7 @@ module Legion
|
|
|
46
48
|
request = find_or_create_request(db, conversation, nil, body)
|
|
47
49
|
response = find_or_create_response(db, request, nil, body)
|
|
48
50
|
metric = find_or_create_metric(db, request, response, body)
|
|
51
|
+
OfficialRouteAttemptWriter.write_route_attempts(db, request, response, body)
|
|
49
52
|
result = { result: :ok, request_id: request[:id], response_id: response[:id], metric_id: metric[:id] }
|
|
50
53
|
end
|
|
51
54
|
|
|
@@ -112,7 +115,7 @@ module Legion
|
|
|
112
115
|
end
|
|
113
116
|
end
|
|
114
117
|
|
|
115
|
-
def find_or_create_request(db, conversation, latest_message, body)
|
|
118
|
+
def find_or_create_request(db, conversation, latest_message, body)
|
|
116
119
|
request_id = request_ref(body)
|
|
117
120
|
existing = db[:llm_message_inference_requests].where(request_ref: request_id).first
|
|
118
121
|
return enrich_request!(db, existing, body, latest_message) if existing
|
|
@@ -140,13 +143,16 @@ module Legion
|
|
|
140
143
|
status: 'responded',
|
|
141
144
|
context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
|
|
142
145
|
request_capture_mode: 'full',
|
|
143
|
-
request_json:
|
|
146
|
+
request_json: if request_payload(body)
|
|
147
|
+
phi_protect(json_dump(request_payload(body)),
|
|
148
|
+
contains_phi?(body))
|
|
149
|
+
end,
|
|
144
150
|
classification_level: classification_level(body),
|
|
145
151
|
cost_center: billing(body)[:cost_center],
|
|
146
152
|
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
147
153
|
injected_tool_count: Array(body.dig(:audit, :injected_tools) || body[:injected_tools]).size,
|
|
148
154
|
context_tokens: resolve_context_tokens(body),
|
|
149
|
-
request_content_hash:
|
|
155
|
+
request_content_hash: resolve_request_content_hash(body),
|
|
150
156
|
curation_strategy: body[:curation_strategy] || body.dig(:audit, :curation_strategy),
|
|
151
157
|
tool_policy: body[:tool_policy] || body.dig(:audit, :tool_policy),
|
|
152
158
|
requested_at: recorded_at(body),
|
|
@@ -194,15 +200,29 @@ module Legion
|
|
|
194
200
|
end
|
|
195
201
|
end
|
|
196
202
|
|
|
197
|
-
def find_or_create_response(db, request, response_message, body)
|
|
203
|
+
def find_or_create_response(db, request, response_message, body)
|
|
198
204
|
response_uuid = stable_uuid(reference(body, :provider_response_ref) || "response:#{request_ref(body)}:#{body[:provider] || 'unknown'}")
|
|
199
205
|
existing = db[:llm_message_inference_responses].where(uuid: response_uuid).first
|
|
206
|
+
|
|
207
|
+
# Fallback: if we couldn't find a response by UUID, check if a response
|
|
208
|
+
# already exists for this request (e.g., metering arrived first and created
|
|
209
|
+
# a response with a different UUID). Enrich it instead of creating a duplicate.
|
|
210
|
+
unless existing
|
|
211
|
+
existing = db[:llm_message_inference_responses]
|
|
212
|
+
.where(message_inference_request_id: request[:id])
|
|
213
|
+
.first
|
|
214
|
+
log.debug("[ledger] response fallback: found existing response id=#{existing[:id]} for request_id=#{request[:id]}") if existing
|
|
215
|
+
end
|
|
216
|
+
|
|
200
217
|
if existing
|
|
201
218
|
enrich_response!(db, existing, response_message, body)
|
|
202
219
|
return existing
|
|
203
220
|
end
|
|
204
221
|
|
|
222
|
+
vis_resp = visible_response(body)
|
|
223
|
+
think_resp = thinking_response(body)
|
|
205
224
|
phi = contains_phi?(body)
|
|
225
|
+
|
|
206
226
|
id = insert_with_savepoint(db, :llm_message_inference_responses, {
|
|
207
227
|
uuid: response_uuid,
|
|
208
228
|
message_inference_request_id: request[:id],
|
|
@@ -218,13 +238,13 @@ module Legion
|
|
|
218
238
|
latency_ms: integer(body[:latency_ms]),
|
|
219
239
|
wall_clock_ms: integer(body[:wall_clock_ms]),
|
|
220
240
|
response_capture_mode: 'full',
|
|
221
|
-
response_json: phi_protect(json_dump(
|
|
222
|
-
response_thinking_json: phi_protect(json_dump(
|
|
241
|
+
response_json: vis_resp ? phi_protect(json_dump(vis_resp), phi) : nil,
|
|
242
|
+
response_thinking_json: think_resp ? phi_protect(json_dump(think_resp), phi) : nil,
|
|
223
243
|
dispatch_path: body[:dispatch_path] || body[:tier],
|
|
224
244
|
error_category: body[:error_category] || body.dig(:error, :category),
|
|
225
245
|
error_code: body[:error_code] || body.dig(:error, :code),
|
|
226
246
|
error_message: body[:error_message] || body.dig(:error, :message),
|
|
227
|
-
response_content_hash:
|
|
247
|
+
response_content_hash: resolve_response_content_hash(body),
|
|
228
248
|
route_attempts: (body[:route_attempts] || body.dig(:audit, :route_attempts)).to_i,
|
|
229
249
|
escalation_chain_ref: body[:escalation_chain_ref],
|
|
230
250
|
identity_principal_id: caller_identity_refs(db, body)[:principal_id],
|
|
@@ -253,12 +273,23 @@ module Legion
|
|
|
253
273
|
update_if_missing(updates, existing, :finish_reason, finish_reason(body))
|
|
254
274
|
update_if_missing(updates, existing, :dispatch_path, body[:dispatch_path] || body[:tier])
|
|
255
275
|
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
276
|
+
update_if_missing(updates, existing, :response_content_hash, resolve_response_content_hash(body))
|
|
277
|
+
|
|
278
|
+
vis = visible_response(body)
|
|
279
|
+
if vis
|
|
280
|
+
response_json = json_dump(vis)
|
|
281
|
+
update_if_placeholder(updates, existing, :response_json, response_json)
|
|
282
|
+
elsif existing[:response_json].nil?
|
|
283
|
+
# Nothing to add, but also don't overwrite existing data with nil
|
|
284
|
+
end
|
|
256
285
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
think = thinking_response(body)
|
|
287
|
+
if think
|
|
288
|
+
thinking_json = json_dump(think)
|
|
289
|
+
update_if_placeholder(updates, existing, :response_thinking_json, thinking_json)
|
|
290
|
+
elsif existing[:response_thinking_json].nil?
|
|
291
|
+
# Nothing to add
|
|
292
|
+
end
|
|
262
293
|
|
|
263
294
|
return if updates.empty?
|
|
264
295
|
|
|
@@ -266,12 +297,27 @@ module Legion
|
|
|
266
297
|
log.info("[ledger] enriched response id=#{existing[:id]} fields=#{updates.keys.join(',')}")
|
|
267
298
|
end
|
|
268
299
|
|
|
300
|
+
# Core guard: never upsert a value that is nil, empty string, or empty JSON object.
|
|
301
|
+
# This prevents a leaner message (e.g., metering) from overwriting valid data
|
|
302
|
+
# written by a richer message (e.g., prompt audit).
|
|
303
|
+
def upsert_guard?(value)
|
|
304
|
+
return false if value.nil?
|
|
305
|
+
return false if value.is_a?(String) && value.strip.empty?
|
|
306
|
+
return false if value.to_s == '{}'
|
|
307
|
+
|
|
308
|
+
true
|
|
309
|
+
end
|
|
310
|
+
|
|
269
311
|
def update_if_missing(updates, existing, key, value)
|
|
270
|
-
updates[key] = value if existing[key].nil? &&
|
|
312
|
+
updates[key] = value if existing[key].nil? && upsert_guard?(value)
|
|
271
313
|
end
|
|
272
314
|
|
|
273
315
|
def update_if_placeholder(updates, existing, key, value)
|
|
274
|
-
|
|
316
|
+
return unless upsert_guard?(value)
|
|
317
|
+
|
|
318
|
+
existing_val = existing[key]
|
|
319
|
+
is_placeholder = ['{}', 'null'].include?(existing_val.to_s)
|
|
320
|
+
updates[key] = value if is_placeholder
|
|
275
321
|
end
|
|
276
322
|
|
|
277
323
|
def find_or_create_metric(db, request, response, body)
|
|
@@ -339,15 +385,20 @@ module Legion
|
|
|
339
385
|
updates = {}
|
|
340
386
|
update_if_missing(updates, existing, :latest_message_id, latest_message&.dig(:id))
|
|
341
387
|
caller_refs = caller_identity_refs(db, body)
|
|
342
|
-
updates
|
|
343
|
-
updates
|
|
344
|
-
updates
|
|
388
|
+
update_if_missing(updates, existing, :caller_identity_id, caller_refs[:identity_id])
|
|
389
|
+
update_if_missing(updates, existing, :caller_principal_id, caller_refs[:principal_id])
|
|
390
|
+
update_if_missing(updates, existing, :runtime_caller_type, caller_type(body))
|
|
345
391
|
update_if_missing(updates, existing, :runtime_caller_class, runtime_caller_class(body))
|
|
346
392
|
update_if_missing(updates, existing, :runtime_caller_client, runtime_caller_client(body))
|
|
347
393
|
update_if_missing(updates, existing, :identity_canonical_name, identity_canonical_name(body))
|
|
394
|
+
update_if_missing(updates, existing, :request_content_hash, resolve_request_content_hash(body))
|
|
348
395
|
|
|
349
|
-
request_json = json_dump(request_payload(body))
|
|
350
|
-
|
|
396
|
+
request_json = request_payload(body) ? json_dump(request_payload(body)) : nil
|
|
397
|
+
if request_json
|
|
398
|
+
update_if_placeholder(updates, existing, :request_json, request_json)
|
|
399
|
+
elsif existing[:request_json].nil?
|
|
400
|
+
# Nothing to add
|
|
401
|
+
end
|
|
351
402
|
|
|
352
403
|
msg_count = Array(body.dig(:request, :messages) || body[:messages]).size
|
|
353
404
|
updates[:context_message_count] = msg_count if existing[:context_message_count].to_i.zero? && msg_count.positive?
|
|
@@ -574,7 +625,7 @@ module Legion
|
|
|
574
625
|
end
|
|
575
626
|
|
|
576
627
|
def resolve_parent_request_id(db, body)
|
|
577
|
-
parent_ref = body[:parent_request_id] || body.dig(:context, :parent_request_id)
|
|
628
|
+
parent_ref = body[:parent_request_id] || body.dig(:context, :parent_request_id) || body.dig(:caller, :parent_request_ref)
|
|
578
629
|
return nil unless present?(parent_ref)
|
|
579
630
|
|
|
580
631
|
if parent_ref.is_a?(Integer)
|
|
@@ -630,7 +681,7 @@ module Legion
|
|
|
630
681
|
end
|
|
631
682
|
|
|
632
683
|
def request_payload(body)
|
|
633
|
-
body[:request] || body[:messages]
|
|
684
|
+
body[:request] || body[:messages]
|
|
634
685
|
end
|
|
635
686
|
|
|
636
687
|
def request_content(body)
|
|
@@ -641,7 +692,9 @@ module Legion
|
|
|
641
692
|
end
|
|
642
693
|
|
|
643
694
|
def visible_response(body)
|
|
644
|
-
response = body[:response] || body[:response_content] || body[:content]
|
|
695
|
+
response = body[:response] || body[:response_content] || body[:content]
|
|
696
|
+
return nil if response.nil? || (response.is_a?(Hash) && response.empty?)
|
|
697
|
+
|
|
645
698
|
if response.is_a?(String)
|
|
646
699
|
clean, _thinking = extract_inline_thinking(response)
|
|
647
700
|
return { content: clean }
|
|
@@ -661,10 +714,10 @@ module Legion
|
|
|
661
714
|
end
|
|
662
715
|
|
|
663
716
|
content_str = body[:response_content] || body[:response] || body[:content]
|
|
664
|
-
return
|
|
717
|
+
return nil unless content_str.is_a?(String)
|
|
665
718
|
|
|
666
719
|
_clean, extracted = extract_inline_thinking(content_str)
|
|
667
|
-
extracted ? { content: extracted } :
|
|
720
|
+
extracted ? { content: extracted } : nil
|
|
668
721
|
end
|
|
669
722
|
|
|
670
723
|
def extract_inline_thinking(text)
|
|
@@ -677,7 +730,10 @@ module Legion
|
|
|
677
730
|
end
|
|
678
731
|
|
|
679
732
|
def response_content(body)
|
|
680
|
-
|
|
733
|
+
vis = visible_response(body)
|
|
734
|
+
return nil unless vis
|
|
735
|
+
|
|
736
|
+
stringify_content(vis[:content] || vis.dig(:message, :content))
|
|
681
737
|
end
|
|
682
738
|
|
|
683
739
|
def finish_reason(body)
|
|
@@ -785,12 +841,26 @@ module Legion
|
|
|
785
841
|
Digest::SHA256.hexdigest(json_dump(content))[0..31]
|
|
786
842
|
end
|
|
787
843
|
|
|
844
|
+
# Prefer precomputed hash from emitter (A4: hash ships instead of raw content).
|
|
845
|
+
# Falls back to computing from raw content for backward compatibility.
|
|
846
|
+
def resolve_request_content_hash(body)
|
|
847
|
+
return body[:request_content_hash] if present?(body[:request_content_hash])
|
|
848
|
+
|
|
849
|
+
compute_content_hash(body.dig(:request, :content) || body.dig(:audit, :request_content))
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# Prefer precomputed hash from emitter (A4: hash ships instead of raw content).
|
|
853
|
+
# Falls back to computing from raw content for backward compatibility.
|
|
854
|
+
def resolve_response_content_hash(body)
|
|
855
|
+
return body[:response_content_hash] if present?(body[:response_content_hash])
|
|
856
|
+
|
|
857
|
+
compute_content_hash(body[:response_content] || body.dig(:audit, :response_content))
|
|
858
|
+
end
|
|
859
|
+
|
|
788
860
|
def resolve_context_tokens(body)
|
|
789
861
|
raw = body[:tokens] || body[:audit] || body
|
|
790
862
|
val = raw[:input_tokens] || raw[:input] || raw[:context_tokens] || raw[:prompt_tokens]
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
val.to_i
|
|
863
|
+
present?(val) ? val.to_i : 0
|
|
794
864
|
end
|
|
795
865
|
end
|
|
796
866
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative '../helpers/json'
|
|
5
|
+
require_relative '../helpers/persistence_logging'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Llm
|
|
10
|
+
module Ledger
|
|
11
|
+
module Writers
|
|
12
|
+
module OfficialRouteAttemptWriter
|
|
13
|
+
extend Legion::Logging::Helper
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Persist route attempt details into llm_route_attempts table.
|
|
18
|
+
# Called from write_prompt/write_metering after the response row exists.
|
|
19
|
+
#
|
|
20
|
+
# Maps emitter keys to table columns:
|
|
21
|
+
# provider -> provider
|
|
22
|
+
# instance -> route_target
|
|
23
|
+
# model -> model_key
|
|
24
|
+
# operation -> operation
|
|
25
|
+
# dispatch_path -> dispatch_path
|
|
26
|
+
# status -> status
|
|
27
|
+
# failure_reason -> failure_reason
|
|
28
|
+
# idempotency_key -> idempotency_key
|
|
29
|
+
def write_route_attempts(db, request, response, body)
|
|
30
|
+
attempts = Array(body[:route_attempt_details])
|
|
31
|
+
return if attempts.empty?
|
|
32
|
+
|
|
33
|
+
attempts.each_with_index do |attempt, idx|
|
|
34
|
+
next unless attempt.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
attempt_no = (attempt[:attempt_no] || (idx + 1)).to_i
|
|
37
|
+
uuid = stable_uuid("#{request[:uuid]}:attempt:#{attempt_no}")
|
|
38
|
+
|
|
39
|
+
existing = db[:llm_route_attempts].where(uuid: uuid).first
|
|
40
|
+
next if existing
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
Helpers::PersistenceLogging.insert_row(
|
|
44
|
+
db,
|
|
45
|
+
:llm_route_attempts,
|
|
46
|
+
{
|
|
47
|
+
uuid: uuid,
|
|
48
|
+
message_inference_request_id: request[:id],
|
|
49
|
+
message_inference_response_id: response[:id],
|
|
50
|
+
attempt_no: attempt_no,
|
|
51
|
+
provider: attempt[:provider] || body[:provider],
|
|
52
|
+
model_key: attempt[:model] || attempt[:model_key] || body[:model_id],
|
|
53
|
+
tier: attempt[:tier] || body[:tier],
|
|
54
|
+
route_target: attempt[:route_target] || attempt[:instance],
|
|
55
|
+
status: (attempt[:status] || 'success').to_s,
|
|
56
|
+
failure_reason: attempt[:failure_reason],
|
|
57
|
+
latency_ms: (attempt[:latency_ms] || 0).to_i,
|
|
58
|
+
operation: attempt[:operation],
|
|
59
|
+
dispatch_path: attempt[:dispatch_path],
|
|
60
|
+
idempotency_key: attempt[:idempotency_key],
|
|
61
|
+
started_at: attempt[:started_at],
|
|
62
|
+
ended_at: attempt[:ended_at],
|
|
63
|
+
identity_principal_id: resolve_identity_principal_id(db, body),
|
|
64
|
+
identity_id: resolve_identity_id(db, body),
|
|
65
|
+
identity_canonical_name: identity_canonical_name(body),
|
|
66
|
+
inserted_at: Time.now.utc
|
|
67
|
+
},
|
|
68
|
+
operation: 'official_route_attempt_writer.insert'
|
|
69
|
+
)
|
|
70
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
71
|
+
log.debug("[ledger] route_attempt collision resolved uuid=#{uuid} error=#{e.class}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve_identity_principal_id(db, body)
|
|
77
|
+
OfficialRecordWriter.caller_identity_refs(db, body)[:principal_id]
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
handle_exception(e, level: :warn, handled: true, operation: 'route_attempt_writer.identity_principal')
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_identity_id(db, body)
|
|
84
|
+
OfficialRecordWriter.caller_identity_refs(db, body)[:identity_id]
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
handle_exception(e, level: :warn, handled: true, operation: 'route_attempt_writer.identity')
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def identity_canonical_name(body)
|
|
91
|
+
raw = body.dig(:identity, :canonical_name) ||
|
|
92
|
+
body.dig(:identity, :identity) ||
|
|
93
|
+
body[:caller_identity]
|
|
94
|
+
return nil unless raw && !raw.to_s.empty?
|
|
95
|
+
|
|
96
|
+
raw.to_s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stable_uuid(value)
|
|
100
|
+
raw = value.to_s
|
|
101
|
+
return raw if raw.length <= 36
|
|
102
|
+
|
|
103
|
+
hex = Digest::SHA256.hexdigest(raw)[0, 32]
|
|
104
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
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.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -246,6 +246,7 @@ files:
|
|
|
246
246
|
- lib/legion/extensions/llm/ledger/writers/official_metering_writer.rb
|
|
247
247
|
- lib/legion/extensions/llm/ledger/writers/official_prompt_writer.rb
|
|
248
248
|
- lib/legion/extensions/llm/ledger/writers/official_record_writer.rb
|
|
249
|
+
- lib/legion/extensions/llm/ledger/writers/official_route_attempt_writer.rb
|
|
249
250
|
- lib/lex-llm-ledger.rb
|
|
250
251
|
homepage: https://github.com/LegionIO/lex-llm-ledger
|
|
251
252
|
licenses:
|