lex-llm 0.6.3 → 0.6.8
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 +31 -0
- data/lib/legion/extensions/llm/actors/metering_flush.rb +60 -0
- data/lib/legion/extensions/llm/canonical/chunk.rb +9 -0
- data/lib/legion/extensions/llm/canonical/content_block.rb +9 -0
- data/lib/legion/extensions/llm/canonical/message.rb +9 -0
- data/lib/legion/extensions/llm/canonical/params.rb +9 -0
- data/lib/legion/extensions/llm/canonical/request.rb +9 -0
- data/lib/legion/extensions/llm/canonical/response.rb +9 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +9 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +10 -0
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +9 -0
- data/lib/legion/extensions/llm/canonical/usage.rb +9 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/spec/legion/extensions/llm/actor/metering_flush_spec.rb +64 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5613f655c5743be588ec5030535de96dc998cb8f03ffb8f8b2be21c95dc55b84
|
|
4
|
+
data.tar.gz: cba342c0f0662d7612b2d8647791149ddb716878e79e487e84a640dbcd3582cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2eb4840e8f1f39d65690dc9226387d6fecbcc9352845cc796d2421b56b660fddae35e2373abdbb88e5c3b127a5b5a6ba621c33682762bbf2eeb271809ec2beb
|
|
7
|
+
data.tar.gz: 7cce383baa60095d03479240c16076624706d56d605200efee5943d3de290ba96c5c18c58e7ada252572966657d846107a3f55945d9f95aa278f553c31ac82a2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.8 - 2026-07-03
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Version bump to publish the `MeteringFlush` actor (0.6.5). Versions 0.6.5–0.6.7 tagged and created GitHub releases but never reached RubyGems: 0.6.5/0.6.6 failed `gem push` on a revoked API key, and 0.6.7 failed because the replacement API key was created with per-key MFA enabled (`gem push` prompted for an OTP a CI runner can't provide). The API key has been recreated without per-key MFA. Because the release workflow skips publishing when a version's tag/release already exists, each failed version must be superseded by a fresh one. No functional changes since 0.6.5.
|
|
7
|
+
|
|
8
|
+
## 0.6.7 - 2026-07-03
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Version bump attempting to publish the `MeteringFlush` actor. Did not reach RubyGems — the replacement API key carried a per-key MFA requirement, so `gem push` demanded an OTP (superseded by 0.6.8). No functional changes.
|
|
12
|
+
|
|
13
|
+
## 0.6.6 - 2026-07-03
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Version bump attempting to re-trigger the release pipeline. Did not publish to RubyGems — the `gem push` step failed on a revoked API key (superseded by 0.6.7). No functional changes.
|
|
17
|
+
|
|
18
|
+
## 0.6.5 - 2026-07-03
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`MeteringFlush` actor drains the LLM metering spool back into RabbitMQ.** legion-llm's `Legion::LLM::Metering` spools metering events to `~/.legionio/data/spool/metering/events.jsonl` whenever transport is down at emit time. Nothing had been draining that spool since `lex-llm-gateway` was retired, so events (chat, embeddings, skills — all funnel through `Legion::LLM::Metering.emit`) accumulated indefinitely. This `Legion::Extensions::Actors::Every` actor ticks every 60s and calls `Legion::LLM::Metering.flush_spool`, which republishes each spooled event to the `llm.metering` exchange and truncates the file. It is a no-op while transport is unavailable, so it is safe to tick continuously. Runs on every node (not a singleton) because each node owns its own spool file and must drain it locally. References legion-llm by string (`'Legion::LLM::Metering'`) so lex-llm — the lower gem — avoids a circular dependency; the constant is resolved at tick time.
|
|
22
|
+
|
|
23
|
+
## 0.6.4 - 2026-06-30
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Canonical Data structs now serialize correctly via MultiJson/Oj/::JSON** — all 10 `Data.define` canonical structs (`ToolCall`, `Message`, `ContentBlock`, `Chunk`, `Params`, `Request`, `Response`, `Thinking`, `ToolDefinition`, `Usage`) now implement `as_json` and `to_json`, delegating to their existing `#to_h` method. Without these callbacks, `MultiJson.dump` (and any JSON encoder) fell back to `obj.to_s` on canonical structs, producing the Ruby `#inspect` dump (e.g. `#<data Legion::Extensions::Llm::Canonical::ToolCall id="toolu_bdrk_...", ...>`) wherever a struct appeared inside a Hash/Array being serialized. This leaked into:
|
|
27
|
+
- Client responses (`/v1/chat/completions`, `/v1/messages`, `/v1/responses`) — `tool_calls` in assistant message history appeared as inspect strings instead of structured JSON objects, breaking LangGraph/Freelens supervisors that expect structured routing decisions.
|
|
28
|
+
- Ledger persistence (`llm_message_inference_requests.request_json`) — tool_calls stored as unparseable Ruby inspect strings, breaking history reconstruction on subsequent turns.
|
|
29
|
+
- AMQP wire payloads — any consumer receiving a message containing canonical structs saw inspect strings instead of structured data.
|
|
30
|
+
- Debug echo-request (`X-Legion-Debug: echo-request`) — canonical request snapshot contained inspect strings for any `tool_calls` in message history.
|
|
31
|
+
|
|
32
|
+
The fix is structural: canonical structs now self-enforce correct JSON serialization at the architectural boundary (per Amendment A of the N×N routing design), so every downstream consumer — ledger, AMQP publisher, client translator, debug formatter — serializes correctly without needing to call `.to_h` explicitly.
|
|
33
|
+
|
|
3
34
|
## 0.6.3 - 2026-06-25
|
|
4
35
|
|
|
5
36
|
### Fixed
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Actor
|
|
7
|
+
# Periodically drains the LLM metering spool.
|
|
8
|
+
#
|
|
9
|
+
# Metering events are emitted by legion-llm's Legion::LLM::Metering. When
|
|
10
|
+
# transport is down at emit time they are appended to a durable JSONL
|
|
11
|
+
# spool (~/.legionio/data/spool/metering/events.jsonl) instead of being
|
|
12
|
+
# published to the `llm.metering` exchange. Nothing was draining that
|
|
13
|
+
# spool after lex-llm-gateway was retired, so events accumulated forever.
|
|
14
|
+
#
|
|
15
|
+
# flush_spool reads every spooled event — chat, embeddings, skills, all
|
|
16
|
+
# funnel through Legion::LLM::Metering.emit into the same file — and
|
|
17
|
+
# republishes each to the exchange, then truncates. It is a no-op when
|
|
18
|
+
# transport is unavailable, so it is safe to tick every minute.
|
|
19
|
+
#
|
|
20
|
+
# runner_class points at legion-llm (the higher gem, always loaded at
|
|
21
|
+
# full boot). lex-llm must not require legion-llm — that would be circular
|
|
22
|
+
# (legion-llm depends on lex-llm) — so we reference it by string and let
|
|
23
|
+
# the actor base resolve the constant at tick time via const_get.
|
|
24
|
+
#
|
|
25
|
+
# NOT a singleton actor: every node runs LegionIO independently and owns
|
|
26
|
+
# its own spool file, so each node must drain its own spool. Leader
|
|
27
|
+
# election would strand every non-leader node's spool.
|
|
28
|
+
class MeteringFlush < Legion::Extensions::Actors::Every
|
|
29
|
+
def runner_class
|
|
30
|
+
'Legion::LLM::Metering'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def runner_function
|
|
34
|
+
'flush_spool'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def time
|
|
38
|
+
60
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_now?
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def use_runner?
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_subtask?
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_task?
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -162,6 +162,15 @@ module Legion
|
|
|
162
162
|
}.compact
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
166
|
+
def as_json(*)
|
|
167
|
+
to_h
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def to_json(*)
|
|
171
|
+
to_h.to_json(*)
|
|
172
|
+
end
|
|
173
|
+
|
|
165
174
|
# Type predicate helpers.
|
|
166
175
|
def text_delta? = type == :text_delta
|
|
167
176
|
def thinking_delta? = type == :thinking_delta
|
|
@@ -110,6 +110,15 @@ module Legion
|
|
|
110
110
|
super.compact
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
114
|
+
def as_json(*)
|
|
115
|
+
to_h
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def to_json(*)
|
|
119
|
+
to_h.to_json(*)
|
|
120
|
+
end
|
|
121
|
+
|
|
113
122
|
# Human-readable string — prevents #inspect leaking into user-facing output.
|
|
114
123
|
def to_s
|
|
115
124
|
return "[tool_use:#{name}]" if type == :tool_use
|
|
@@ -123,6 +123,15 @@ module Legion
|
|
|
123
123
|
super.compact
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
127
|
+
def as_json(*)
|
|
128
|
+
to_h
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_json(*)
|
|
132
|
+
to_h.to_json(*)
|
|
133
|
+
end
|
|
134
|
+
|
|
126
135
|
# Human-readable string — prevents #inspect leaking into user-facing output.
|
|
127
136
|
def to_s
|
|
128
137
|
text
|
|
@@ -53,6 +53,15 @@ module Legion
|
|
|
53
53
|
def to_h
|
|
54
54
|
super.compact
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
58
|
+
def as_json(*)
|
|
59
|
+
to_h
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_json(*)
|
|
63
|
+
to_h.to_json(*)
|
|
64
|
+
end
|
|
56
65
|
end
|
|
57
66
|
# rubocop:enable Lint/ConstantDefinitionInBlock
|
|
58
67
|
end
|
|
@@ -92,6 +92,15 @@ module Legion
|
|
|
92
92
|
}.compact
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
96
|
+
def as_json(*)
|
|
97
|
+
to_h
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_json(*)
|
|
101
|
+
to_h.to_json(*)
|
|
102
|
+
end
|
|
103
|
+
|
|
95
104
|
def self.normalize_tools(tools)
|
|
96
105
|
return {} if tools.nil? || tools.empty?
|
|
97
106
|
|
|
@@ -104,6 +104,15 @@ module Legion
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
108
|
+
def as_json(*)
|
|
109
|
+
to_h
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_json(*)
|
|
113
|
+
to_h.to_json(*)
|
|
114
|
+
end
|
|
115
|
+
|
|
107
116
|
# Whether the response includes tool calls.
|
|
108
117
|
def tool_call?
|
|
109
118
|
!tool_calls.nil? && !tool_calls.empty?
|
|
@@ -31,6 +31,15 @@ module Legion
|
|
|
31
31
|
super.compact
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
35
|
+
def as_json(*)
|
|
36
|
+
to_h
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_json(*)
|
|
40
|
+
to_h.to_json(*)
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
# Whether this thinking block has any content.
|
|
35
44
|
def empty?
|
|
36
45
|
content.nil? && signature.nil?
|
|
@@ -106,6 +106,16 @@ module Legion
|
|
|
106
106
|
super.compact
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# MultiJson/Oj/::JSON callback for unknown types — without this, fallback is
|
|
110
|
+
# obj.to_s which for Data.define returns the #inspect dump and leaks into JSON.
|
|
111
|
+
def as_json(*)
|
|
112
|
+
to_h
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_json(*)
|
|
116
|
+
to_h.to_json(*)
|
|
117
|
+
end
|
|
118
|
+
|
|
109
119
|
# Subset for audit/ledger emission.
|
|
110
120
|
def to_audit_hash
|
|
111
121
|
{
|
|
@@ -91,6 +91,15 @@ module Legion
|
|
|
91
91
|
parameters: parameters
|
|
92
92
|
}.compact.reject { |k, v| k == :description && v == '' }
|
|
93
93
|
end
|
|
94
|
+
|
|
95
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
96
|
+
def as_json(*)
|
|
97
|
+
to_h
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_json(*)
|
|
101
|
+
to_h.to_json(*)
|
|
102
|
+
end
|
|
94
103
|
end
|
|
95
104
|
end
|
|
96
105
|
end
|
|
@@ -61,6 +61,15 @@ module Legion
|
|
|
61
61
|
super.compact
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
|
|
65
|
+
def as_json(*)
|
|
66
|
+
to_h
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_json(*)
|
|
70
|
+
to_h.to_json(*)
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
# Total tokens across all categories.
|
|
65
74
|
def total_tokens
|
|
66
75
|
[input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Stub the actor base class before requiring the real actor — the concrete
|
|
4
|
+
# Legion::Extensions::Actors::Every lives in LegionIO, which lex-llm does not
|
|
5
|
+
# depend on (lex-llm is the lower gem).
|
|
6
|
+
unless defined?(Legion::Extensions::Actors::Every)
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Actors
|
|
10
|
+
class Every; end # rubocop:disable Lint/EmptyClass
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
$LOADED_FEATURES << 'legion/extensions/actors/every'
|
|
17
|
+
|
|
18
|
+
require_relative '../../../../../lib/legion/extensions/llm/actors/metering_flush'
|
|
19
|
+
|
|
20
|
+
RSpec.describe Legion::Extensions::Llm::Actor::MeteringFlush do
|
|
21
|
+
subject(:actor) { described_class.new }
|
|
22
|
+
|
|
23
|
+
describe '#runner_class' do
|
|
24
|
+
it 'targets the legion-llm metering module that owns the spool' do
|
|
25
|
+
expect(actor.runner_class).to eq('Legion::LLM::Metering')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#runner_function' do
|
|
30
|
+
it 'returns flush_spool' do
|
|
31
|
+
expect(actor.runner_function).to eq('flush_spool')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '#time' do
|
|
36
|
+
it 'flushes once per minute' do
|
|
37
|
+
expect(actor.time).to eq(60)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#run_now?' do
|
|
42
|
+
it 'returns false so it does not fire during boot' do
|
|
43
|
+
expect(actor.run_now?).to be false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#use_runner?' do
|
|
48
|
+
it 'returns false so it calls the module directly (no AMQP round-trip)' do
|
|
49
|
+
expect(actor.use_runner?).to be false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '#check_subtask?' do
|
|
54
|
+
it 'returns false' do
|
|
55
|
+
expect(actor.check_subtask?).to be false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#generate_task?' do
|
|
60
|
+
it 'returns false' do
|
|
61
|
+
expect(actor.generate_task?).to be false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -255,6 +255,7 @@ files:
|
|
|
255
255
|
- README.md
|
|
256
256
|
- lex-llm.gemspec
|
|
257
257
|
- lib/legion/extensions/llm.rb
|
|
258
|
+
- lib/legion/extensions/llm/actors/metering_flush.rb
|
|
258
259
|
- lib/legion/extensions/llm/agent.rb
|
|
259
260
|
- lib/legion/extensions/llm/aliases.json
|
|
260
261
|
- lib/legion/extensions/llm/aliases.rb
|
|
@@ -347,6 +348,7 @@ files:
|
|
|
347
348
|
- spec/fixtures/ruby.wav
|
|
348
349
|
- spec/fixtures/ruby.xml
|
|
349
350
|
- spec/fixtures/sample.pdf
|
|
351
|
+
- spec/legion/extensions/llm/actor/metering_flush_spec.rb
|
|
350
352
|
- spec/legion/extensions/llm/agent_spec.rb
|
|
351
353
|
- spec/legion/extensions/llm/attachment_spec.rb
|
|
352
354
|
- spec/legion/extensions/llm/auto_registration_spec.rb
|