lex-llm-ledger 0.3.0 → 0.3.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 +7 -0
- data/lib/legion/extensions/llm/ledger/actors/metering.rb +2 -0
- data/lib/legion/extensions/llm/ledger/actors/prompts.rb +2 -0
- data/lib/legion/extensions/llm/ledger/actors/tools.rb +2 -0
- data/lib/legion/extensions/llm/ledger/version.rb +1 -1
- data/lib/legion/extensions/llm/ledger/writers/official_record_writer.rb +182 -141
- 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: a69d4ccf5975f45c795e6e54ab6ed59272a269855394bbef4d1149a48bd338e7
|
|
4
|
+
data.tar.gz: 60bfc3f268526d0cf427ccfa90f2e230f9e0491fd88584ff685604c93faf63c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e166ea0dc31b758dd45d5c1595f6a72c78629b7dfd69d09d9928a699ce14f475658d9bf77f08018608bc98f394532f4c19b5887c3b2649bb1761aecbe2b4b5c8
|
|
7
|
+
data.tar.gz: 226be9d0592d67fcca0ff956fe0c62db56a311cc3f6bde5a16523f21801d43079d976a6d6582e2a0f1022396089555495df37ccf81ccfd56ddfd3f2344f9d27f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-05-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Recover cleanly when concurrent ledger consumers create the same conversation, request, response, metric, or identity rows.
|
|
7
|
+
- Keep duplicate insert recovery inside savepoints so PostgreSQL transactions remain usable after unique constraint races.
|
|
8
|
+
- Remove temporary prompt runner debug output while preserving single-message subscription prefetch behavior.
|
|
9
|
+
|
|
3
10
|
## [0.3.0] - 2026-05-08
|
|
4
11
|
|
|
5
12
|
### Changed
|
|
@@ -57,20 +57,26 @@ module Legion
|
|
|
57
57
|
existing = db[:llm_conversations].where(uuid: uuid).first
|
|
58
58
|
return existing if existing
|
|
59
59
|
|
|
60
|
-
id =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
id = insert_with_savepoint(db, :llm_conversations, {
|
|
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
|
+
recorded_at: recorded_at(body),
|
|
69
|
+
inserted_at: Time.now.utc,
|
|
70
|
+
created_at: Time.now.utc,
|
|
71
|
+
updated_at: Time.now.utc
|
|
72
|
+
}, operation: 'official_record_writer.conversation')
|
|
73
73
|
db[:llm_conversations][id: id]
|
|
74
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
75
|
+
log.debug("[ledger] conversation collision resolved uuid=#{uuid} error=#{e.class}")
|
|
76
|
+
existing = db[:llm_conversations].where(uuid: uuid).first
|
|
77
|
+
return existing if existing
|
|
78
|
+
|
|
79
|
+
raise
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
def find_or_create_user_message(db, conversation, body)
|
|
@@ -80,20 +86,18 @@ module Legion
|
|
|
80
86
|
|
|
81
87
|
seq = body[:message_seq] ? integer(body[:message_seq]) : next_message_seq(db, conversation)
|
|
82
88
|
begin
|
|
83
|
-
id = db
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}, operation: 'official_record_writer.user_message')
|
|
96
|
-
end
|
|
89
|
+
id = insert_with_savepoint(db, :llm_messages, {
|
|
90
|
+
uuid: uuid,
|
|
91
|
+
conversation_id: conversation[:id],
|
|
92
|
+
seq: seq,
|
|
93
|
+
role: 'user',
|
|
94
|
+
content_type: 'text',
|
|
95
|
+
content: request_content(body),
|
|
96
|
+
input_tokens: tokens(body)[:input_tokens],
|
|
97
|
+
output_tokens: 0,
|
|
98
|
+
created_at: recorded_at(body),
|
|
99
|
+
inserted_at: Time.now.utc
|
|
100
|
+
}, operation: 'official_record_writer.user_message')
|
|
97
101
|
db[:llm_messages][id: id]
|
|
98
102
|
rescue Sequel::UniqueConstraintViolation => e
|
|
99
103
|
log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
|
|
@@ -112,31 +116,40 @@ module Legion
|
|
|
112
116
|
|
|
113
117
|
operation = operation(body)
|
|
114
118
|
caller_refs = caller_identity_refs(db, body)
|
|
115
|
-
id =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
119
|
+
id = insert_with_savepoint(db, :llm_message_inference_requests, {
|
|
120
|
+
uuid: stable_uuid(request_id),
|
|
121
|
+
conversation_id: conversation[:id],
|
|
122
|
+
latest_message_id: latest_message[:id],
|
|
123
|
+
caller_principal_id: caller_refs[:principal_id],
|
|
124
|
+
caller_identity_id: caller_refs[:identity_id],
|
|
125
|
+
runtime_caller_type: caller_type(body),
|
|
126
|
+
request_ref: request_id,
|
|
127
|
+
correlation_ref: correlation_id(body),
|
|
128
|
+
correlation_id: correlation_id(body),
|
|
129
|
+
exchange_ref: body[:exchange_id],
|
|
130
|
+
request_type: operation,
|
|
131
|
+
operation: operation,
|
|
132
|
+
idempotency_key: body[:idempotency_key] || request_id,
|
|
133
|
+
status: 'responded',
|
|
134
|
+
context_message_count: Array(body.dig(:request, :messages) || body[:messages]).size,
|
|
135
|
+
request_capture_mode: 'full',
|
|
136
|
+
request_json: json_dump(request_payload(body)),
|
|
137
|
+
classification_level: classification_level(body),
|
|
138
|
+
cost_center: billing(body)[:cost_center],
|
|
139
|
+
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
140
|
+
requested_at: recorded_at(body),
|
|
141
|
+
inserted_at: Time.now.utc
|
|
142
|
+
}, operation: 'official_record_writer.inference_request')
|
|
139
143
|
db[:llm_message_inference_requests][id: id]
|
|
144
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
145
|
+
log.debug("[ledger] request collision resolved request_ref=#{request_id} error=#{e.class}")
|
|
146
|
+
existing = db[:llm_message_inference_requests].where(request_ref: request_id).first
|
|
147
|
+
if existing
|
|
148
|
+
enrich_request!(db, existing, body)
|
|
149
|
+
return existing
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
raise
|
|
140
153
|
end
|
|
141
154
|
|
|
142
155
|
def find_or_create_response_message(db, conversation, request, body)
|
|
@@ -147,22 +160,20 @@ module Legion
|
|
|
147
160
|
latest = db[:llm_messages][id: request[:latest_message_id]]
|
|
148
161
|
seq = (latest&.dig(:seq) || 1) + 1
|
|
149
162
|
begin
|
|
150
|
-
id = db
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}, operation: 'official_record_writer.response_message')
|
|
165
|
-
end
|
|
163
|
+
id = insert_with_savepoint(db, :llm_messages, {
|
|
164
|
+
uuid: uuid,
|
|
165
|
+
conversation_id: conversation[:id],
|
|
166
|
+
parent_message_id: latest&.dig(:id),
|
|
167
|
+
message_inference_request_id: request[:id],
|
|
168
|
+
seq: seq,
|
|
169
|
+
role: 'assistant',
|
|
170
|
+
content_type: 'text',
|
|
171
|
+
content: response_content(body),
|
|
172
|
+
input_tokens: 0,
|
|
173
|
+
output_tokens: tokens(body)[:output_tokens],
|
|
174
|
+
created_at: recorded_at(body),
|
|
175
|
+
inserted_at: Time.now.utc
|
|
176
|
+
}, operation: 'official_record_writer.response_message')
|
|
166
177
|
db[:llm_messages][id: id]
|
|
167
178
|
rescue Sequel::UniqueConstraintViolation => e
|
|
168
179
|
log.debug("[ledger] seq collision resolved uuid=#{uuid} conversation_id=#{conversation[:id]} error=#{e.class}")
|
|
@@ -179,28 +190,37 @@ module Legion
|
|
|
179
190
|
return existing
|
|
180
191
|
end
|
|
181
192
|
|
|
182
|
-
id =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
193
|
+
id = insert_with_savepoint(db, :llm_message_inference_responses, {
|
|
194
|
+
uuid: response_uuid,
|
|
195
|
+
message_inference_request_id: request[:id],
|
|
196
|
+
response_message_id: response_message&.dig(:id),
|
|
197
|
+
provider: provider(body),
|
|
198
|
+
provider_instance: provider_instance(body),
|
|
199
|
+
model_key: model_id(body),
|
|
200
|
+
tier: tier(body),
|
|
201
|
+
runner_ref: body[:worker_id] || body[:runner_ref],
|
|
202
|
+
provider_response_ref: body[:provider_response_ref],
|
|
203
|
+
status: body[:error] ? 'error' : 'success',
|
|
204
|
+
finish_reason: finish_reason(body),
|
|
205
|
+
latency_ms: integer(body[:latency_ms]),
|
|
206
|
+
wall_clock_ms: integer(body[:wall_clock_ms]),
|
|
207
|
+
response_capture_mode: 'full',
|
|
208
|
+
response_json: json_dump(visible_response(body)),
|
|
209
|
+
response_thinking_json: json_dump(thinking_response(body)),
|
|
210
|
+
dispatch_path: body[:dispatch_path] || body[:tier],
|
|
211
|
+
responded_at: recorded_at(body),
|
|
212
|
+
inserted_at: Time.now.utc
|
|
213
|
+
}, operation: 'official_record_writer.inference_response')
|
|
203
214
|
db[:llm_message_inference_responses][id: id]
|
|
215
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
216
|
+
log.debug("[ledger] response collision resolved uuid=#{response_uuid} error=#{e.class}")
|
|
217
|
+
existing = db[:llm_message_inference_responses].where(uuid: response_uuid).first
|
|
218
|
+
if existing
|
|
219
|
+
enrich_response!(db, existing, response_message, body)
|
|
220
|
+
return existing
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
raise
|
|
204
224
|
end
|
|
205
225
|
|
|
206
226
|
def enrich_response!(db, existing, response_message, body)
|
|
@@ -237,33 +257,45 @@ module Legion
|
|
|
237
257
|
return existing if existing
|
|
238
258
|
|
|
239
259
|
token_values = tokens(body)
|
|
240
|
-
id =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
id = insert_with_savepoint(db, :llm_message_inference_metrics, {
|
|
261
|
+
uuid: metric_uuid,
|
|
262
|
+
message_inference_request_id: request[:id],
|
|
263
|
+
message_inference_response_id: response[:id],
|
|
264
|
+
provider: provider(body),
|
|
265
|
+
model_key: model_id(body),
|
|
266
|
+
tier: tier(body),
|
|
267
|
+
input_tokens: token_values[:input_tokens],
|
|
268
|
+
output_tokens: token_values[:output_tokens],
|
|
269
|
+
thinking_tokens: token_values[:thinking_tokens],
|
|
270
|
+
total_tokens: token_values[:total_tokens],
|
|
271
|
+
latency_ms: integer(body[:latency_ms]),
|
|
272
|
+
wall_clock_ms: integer(body[:wall_clock_ms]),
|
|
273
|
+
cost_usd: cost_usd(body),
|
|
274
|
+
currency: body[:currency] || 'USD',
|
|
275
|
+
cost_center: billing(body)[:cost_center],
|
|
276
|
+
budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
|
|
277
|
+
recorded_at: recorded_at(body),
|
|
278
|
+
inserted_at: Time.now.utc
|
|
279
|
+
}, operation: 'official_record_writer.inference_metric')
|
|
260
280
|
db[:llm_message_inference_metrics][id: id]
|
|
281
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
282
|
+
log.debug("[ledger] metric collision resolved uuid=#{metric_uuid} error=#{e.class}")
|
|
283
|
+
existing = db[:llm_message_inference_metrics].where(uuid: metric_uuid).first
|
|
284
|
+
return existing if existing
|
|
285
|
+
|
|
286
|
+
raise
|
|
261
287
|
end
|
|
262
288
|
|
|
263
289
|
def insert_row(db, table, attributes, operation:)
|
|
264
290
|
Helpers::PersistenceLogging.insert_row(db, table, attributes, operation: operation)
|
|
265
291
|
end
|
|
266
292
|
|
|
293
|
+
def insert_with_savepoint(db, table, attributes, operation:)
|
|
294
|
+
db.transaction(savepoint: true) do
|
|
295
|
+
insert_row(db, table, attributes, operation: operation)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
267
299
|
def request_ref(body)
|
|
268
300
|
body[:__ledger_request_ref] ||= reference(body, :request_id, :request_ref) ||
|
|
269
301
|
correlation_id(body) ||
|
|
@@ -396,19 +428,22 @@ module Legion
|
|
|
396
428
|
existing = table.where(name: provider_name).first
|
|
397
429
|
return existing if existing
|
|
398
430
|
|
|
399
|
-
id =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
id = insert_with_savepoint(db, :identity_providers, {
|
|
432
|
+
uuid: deterministic_uuid("identity_provider:#{provider_name}"),
|
|
433
|
+
name: provider_name,
|
|
434
|
+
provider_type: provider_name == 'local' ? 'local' : 'external',
|
|
435
|
+
facing: 'internal',
|
|
436
|
+
source: 'ledger',
|
|
437
|
+
created_at: Time.now.utc,
|
|
438
|
+
updated_at: Time.now.utc
|
|
439
|
+
}, operation: 'official_record_writer.identity_provider')
|
|
408
440
|
table[id: id]
|
|
409
441
|
rescue Sequel::UniqueConstraintViolation => e
|
|
410
442
|
handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_provider_race')
|
|
411
|
-
table.where(name: provider_name).first
|
|
443
|
+
existing = table.where(name: provider_name).first
|
|
444
|
+
return existing if existing
|
|
445
|
+
|
|
446
|
+
raise
|
|
412
447
|
end
|
|
413
448
|
|
|
414
449
|
def find_or_create_identity_principal(db, descriptor)
|
|
@@ -416,19 +451,22 @@ module Legion
|
|
|
416
451
|
existing = table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
|
|
417
452
|
return existing if existing
|
|
418
453
|
|
|
419
|
-
id =
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
454
|
+
id = insert_with_savepoint(db, :identity_principals, {
|
|
455
|
+
uuid: deterministic_uuid("identity_principal:#{descriptor[:kind]}:#{descriptor[:canonical_name]}"),
|
|
456
|
+
canonical_name: descriptor[:canonical_name],
|
|
457
|
+
kind: descriptor[:kind],
|
|
458
|
+
display_name: descriptor[:canonical_name],
|
|
459
|
+
last_seen_at: Time.now.utc,
|
|
460
|
+
created_at: Time.now.utc,
|
|
461
|
+
updated_at: Time.now.utc
|
|
462
|
+
}, operation: 'official_record_writer.identity_principal')
|
|
428
463
|
table[id: id]
|
|
429
464
|
rescue Sequel::UniqueConstraintViolation => e
|
|
430
465
|
handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_principal_race')
|
|
431
|
-
table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
|
|
466
|
+
existing = table.where(canonical_name: descriptor[:canonical_name], kind: descriptor[:kind]).first
|
|
467
|
+
return existing if existing
|
|
468
|
+
|
|
469
|
+
raise
|
|
432
470
|
end
|
|
433
471
|
|
|
434
472
|
def find_or_create_identity(db, principal, provider, descriptor)
|
|
@@ -441,22 +479,25 @@ module Legion
|
|
|
441
479
|
return existing if existing
|
|
442
480
|
|
|
443
481
|
uuid_key = "identity:#{principal[:id]}:#{provider[:id]}:#{descriptor[:provider_identity_key]}"
|
|
444
|
-
id =
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
482
|
+
id = insert_with_savepoint(db, :identities, {
|
|
483
|
+
uuid: deterministic_uuid(uuid_key),
|
|
484
|
+
principal_id: principal[:id],
|
|
485
|
+
provider_id: provider[:id],
|
|
486
|
+
provider_identity_key: descriptor[:provider_identity_key],
|
|
487
|
+
last_authenticated_at: Time.now.utc,
|
|
488
|
+
account_type: 'primary',
|
|
489
|
+
is_default: true,
|
|
490
|
+
created_at: Time.now.utc,
|
|
491
|
+
updated_at: Time.now.utc
|
|
492
|
+
}, operation: 'official_record_writer.identity')
|
|
455
493
|
table[id: id]
|
|
456
494
|
rescue Sequel::UniqueConstraintViolation => e
|
|
457
495
|
handle_exception(e, level: :debug, handled: true, operation: 'official_record_writer.identity_race')
|
|
458
|
-
table.where(principal_id: principal[:id], provider_id: provider[:id],
|
|
459
|
-
|
|
496
|
+
existing = table.where(principal_id: principal[:id], provider_id: provider[:id],
|
|
497
|
+
provider_identity_key: descriptor[:provider_identity_key]).first
|
|
498
|
+
return existing if existing
|
|
499
|
+
|
|
500
|
+
raise
|
|
460
501
|
end
|
|
461
502
|
|
|
462
503
|
def identity_tables_available?(db)
|