lex-llm-ledger 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b0e51e42bfec73669b2065b292a856ba029708010d10fc038d086a3442207e5
4
- data.tar.gz: dbfbc4bf7b969362ff0363096fac13d33e8484080eddbccb09051fe3b794c0ff
3
+ metadata.gz: 9dc0c64e738b5fe99d37289f48ce3264b7991f7f521d570df80a63615d419972
4
+ data.tar.gz: a551479f32400fb90df6e0e6bc15423c627d783149f9a17f675421cf3d3f9243
5
5
  SHA512:
6
- metadata.gz: 18e7a33c0c64c8cb6ebc6ef09c21b174ac2a73ef0643947029e2898ec59bf523e5e0d8c157672908755147b136c98b87117be1f7d1e701d6542862fd9b58e4b3
7
- data.tar.gz: f9e869ab4703524b7b054d1c1a44c8ab4d2fd1b53a770aa48f471f69c70f124ad4e4b4a90ecd1ca5399e43817394cdbc05248b00108cdca4ee45b494b8c5d4c9
6
+ metadata.gz: 4cdf842b8a22047e92f2bd1adba9a953eefaf5bf86b255d54b6e8d22fc9f64545a45c03486348c9732539e34739e74fc43ce61c77d52772aeea93e640f0298ee
7
+ data.tar.gz: cd05c364405a72b9f1bfed2e539778fb301fa36f20e646f645755eb87e21a98261f5b9442a3c2b0938662a640f2708eb8886c0632dbe5118848e7616c48ad4c0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## [0.7.0] - 2026-06-09
4
9
 
5
10
  ### Fixed
@@ -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
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Ledger
7
- VERSION = '0.7.0'
7
+ VERSION = '0.7.1'
8
8
  end
9
9
  end
10
10
  end
@@ -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
 
@@ -149,7 +152,7 @@ module Legion
149
152
  budget_key: billing(body)[:budget_id] || billing(body)[:budget_key],
150
153
  injected_tool_count: Array(body.dig(:audit, :injected_tools) || body[:injected_tools]).size,
151
154
  context_tokens: resolve_context_tokens(body),
152
- request_content_hash: compute_content_hash(body.dig(:request, :content) || body.dig(:audit, :request_content)),
155
+ request_content_hash: resolve_request_content_hash(body),
153
156
  curation_strategy: body[:curation_strategy] || body.dig(:audit, :curation_strategy),
154
157
  tool_policy: body[:tool_policy] || body.dig(:audit, :tool_policy),
155
158
  requested_at: recorded_at(body),
@@ -241,7 +244,7 @@ module Legion
241
244
  error_category: body[:error_category] || body.dig(:error, :category),
242
245
  error_code: body[:error_code] || body.dig(:error, :code),
243
246
  error_message: body[:error_message] || body.dig(:error, :message),
244
- response_content_hash: compute_content_hash(body[:response_content] || body.dig(:audit, :response_content)),
247
+ response_content_hash: resolve_response_content_hash(body),
245
248
  route_attempts: (body[:route_attempts] || body.dig(:audit, :route_attempts)).to_i,
246
249
  escalation_chain_ref: body[:escalation_chain_ref],
247
250
  identity_principal_id: caller_identity_refs(db, body)[:principal_id],
@@ -270,6 +273,7 @@ module Legion
270
273
  update_if_missing(updates, existing, :finish_reason, finish_reason(body))
271
274
  update_if_missing(updates, existing, :dispatch_path, body[:dispatch_path] || body[:tier])
272
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))
273
277
 
274
278
  vis = visible_response(body)
275
279
  if vis
@@ -387,6 +391,7 @@ module Legion
387
391
  update_if_missing(updates, existing, :runtime_caller_class, runtime_caller_class(body))
388
392
  update_if_missing(updates, existing, :runtime_caller_client, runtime_caller_client(body))
389
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))
390
395
 
391
396
  request_json = request_payload(body) ? json_dump(request_payload(body)) : nil
392
397
  if request_json
@@ -836,6 +841,22 @@ module Legion
836
841
  Digest::SHA256.hexdigest(json_dump(content))[0..31]
837
842
  end
838
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
+
839
860
  def resolve_context_tokens(body)
840
861
  raw = body[:tokens] || body[:audit] || body
841
862
  val = raw[:input_tokens] || raw[:input] || raw[:context_tokens] || raw[:prompt_tokens]
@@ -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.7.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: