lex-llm 0.6.4 → 0.6.9
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 +28 -0
- data/lib/legion/extensions/llm/actors/metering_flush.rb +60 -0
- data/lib/legion/extensions/llm/stop_reason_mapping.rb +55 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +1 -0
- data/spec/legion/extensions/llm/actor/metering_flush_spec.rb +64 -0
- data/spec/legion/extensions/llm/stop_reason_mapping_spec.rb +96 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d325676c4cc272b839d2e0ba4d678a1aab7ec05268e5e7d37ac302234d25ae4e
|
|
4
|
+
data.tar.gz: a8a2475af20b60ea5d53dc2c5da7ceb0ea2f25d8c094079df73b41cce04ea577
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5535f26e2755347ca1024c662eeb7e575515bfa824fe36a2f3958558d21548f393b426f0261a35f3935ccd222099ad8dedaef614af693a0b2d0a74e2353e91ae
|
|
7
|
+
data.tar.gz: 59fd2029653e72366a5384d36f34639348b12ecba38ba3e2ad19ff0a20e5ffcf6d7ea434e02fcfc8994fa0f373264efac90d532800cd3ae14f2036f10f9f64c8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.9 - 2026-07-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `StopReasonMapping` mixin (`legion/extensions/llm/stop_reason_mapping`) — a shared stop_reason vocabulary included by provider translators (and available to legion-llm, which depends on lex-llm). Keys are incoming provider wire strings, values are the canonical `stop_reason` symbol. Provider wire formats spell the same six canonical end-states differently (OpenAI/vLLM emit `tool_calls`, Anthropic `tool_use`; `stop`/`end_turn`/`eos` all mean the same thing), so the common vocabulary now lives in one place instead of being copy-pasted (and drifting) across provider gems.
|
|
7
|
+
- `#stop_reason_map` — the common vocabulary, inherited by all.
|
|
8
|
+
- `#stop_reason_map_additions` — returns `{}` in the base; a provider overrides it to ADD provider-specific strings (merged on top, additions win on collision). No guards needed.
|
|
9
|
+
- `#stop_reason_lookup(key)` — merges additions over the map, coerces the key via `to_s`, returns the canonical symbol or `nil` (caller decides the default). To REPLACE the whole vocabulary, a provider overrides `#stop_reason_map`.
|
|
10
|
+
|
|
11
|
+
## 0.6.8 - 2026-07-03
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
## 0.6.7 - 2026-07-03
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
## 0.6.6 - 2026-07-03
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
## 0.6.5 - 2026-07-03
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **`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.
|
|
30
|
+
|
|
3
31
|
## 0.6.4 - 2026-06-30
|
|
4
32
|
|
|
5
33
|
### 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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
# Shared stop_reason vocabulary, included by every provider translator (and
|
|
7
|
+
# available to legion-llm, which depends on lex-llm). Provider wire formats
|
|
8
|
+
# spell the same canonical end-states differently — OpenAI/vLLM emit
|
|
9
|
+
# "tool_calls", Anthropic "tool_use", and "stop"/"end_turn"/"eos" all mean
|
|
10
|
+
# the same thing. This puts the common vocabulary in one place so it is not
|
|
11
|
+
# copy-pasted (and drifting) across five provider gems.
|
|
12
|
+
#
|
|
13
|
+
# Keys are the incoming provider strings; values are the canonical
|
|
14
|
+
# stop_reason (see Canonical::Response::STOP_REASONS).
|
|
15
|
+
#
|
|
16
|
+
# Provider overrides, all by plain method definition — no guards:
|
|
17
|
+
# * ADD provider-specific strings → override #stop_reason_map_additions
|
|
18
|
+
# * REPLACE the whole vocabulary → override #stop_reason_map
|
|
19
|
+
module StopReasonMapping
|
|
20
|
+
# The common vocabulary every provider inherits for free.
|
|
21
|
+
def stop_reason_map
|
|
22
|
+
{
|
|
23
|
+
'stop' => :end_turn,
|
|
24
|
+
'end_turn' => :end_turn,
|
|
25
|
+
'eos' => :end_turn,
|
|
26
|
+
'complete' => :end_turn,
|
|
27
|
+
'tool_calls' => :tool_use,
|
|
28
|
+
'tool_call' => :tool_use,
|
|
29
|
+
'tool_use' => :tool_use,
|
|
30
|
+
'function_call' => :tool_use,
|
|
31
|
+
'length' => :max_tokens,
|
|
32
|
+
'max_tokens' => :max_tokens,
|
|
33
|
+
'stop_sequence' => :stop_sequence,
|
|
34
|
+
'stop_sequences' => :stop_sequence,
|
|
35
|
+
'content_filter' => :content_filter,
|
|
36
|
+
'error' => :error
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Provider-specific additions, merged on top of the common map. Base
|
|
41
|
+
# returns {} so callers never need an "if defined?" guard. Additions win
|
|
42
|
+
# over the common map on key collision.
|
|
43
|
+
def stop_reason_map_additions
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Resolve a provider wire stop_reason string to its canonical symbol, or
|
|
48
|
+
# nil when unmapped (the caller decides the default — usually :end_turn).
|
|
49
|
+
def stop_reason_lookup(key)
|
|
50
|
+
stop_reason_map.merge(stop_reason_map_additions).fetch(key.to_s, nil)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
# Shared stop_reason vocabulary. Provider wire formats spell the same six
|
|
6
|
+
# canonical end-states differently (OpenAI/vLLM: "tool_calls"; Anthropic:
|
|
7
|
+
# "tool_use"; "stop" vs "end_turn" vs "eos"). Before this module, every
|
|
8
|
+
# provider translator carried its own copy-pasted *_STOP_REASON_MAP, which
|
|
9
|
+
# drifted — vllm/ollama mapped only "tool_use" and silently fell back to
|
|
10
|
+
# :end_turn on the "tool_calls" that OpenAI-compatible backends actually send.
|
|
11
|
+
#
|
|
12
|
+
# StopReasonMapping puts the common vocabulary in one place (inherited by all)
|
|
13
|
+
# with a provider-overridable additions hook and a full-replace hook.
|
|
14
|
+
RSpec.describe Legion::Extensions::Llm::StopReasonMapping do
|
|
15
|
+
let(:host_class) do
|
|
16
|
+
Class.new { include Legion::Extensions::Llm::StopReasonMapping }
|
|
17
|
+
end
|
|
18
|
+
let(:host) { host_class.new }
|
|
19
|
+
|
|
20
|
+
describe 'the common vocabulary (#stop_reason_lookup)' do
|
|
21
|
+
it 'maps OpenAI/vLLM "tool_calls" to :tool_use' do
|
|
22
|
+
expect(host.stop_reason_lookup('tool_calls')).to eq(:tool_use)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'maps Anthropic "tool_use" to :tool_use' do
|
|
26
|
+
expect(host.stop_reason_lookup('tool_use')).to eq(:tool_use)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'maps "stop"/"end_turn"/"eos" to :end_turn' do
|
|
30
|
+
expect(host.stop_reason_lookup('stop')).to eq(:end_turn)
|
|
31
|
+
expect(host.stop_reason_lookup('end_turn')).to eq(:end_turn)
|
|
32
|
+
expect(host.stop_reason_lookup('eos')).to eq(:end_turn)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'maps "length"/"max_tokens" to :max_tokens' do
|
|
36
|
+
expect(host.stop_reason_lookup('length')).to eq(:max_tokens)
|
|
37
|
+
expect(host.stop_reason_lookup('max_tokens')).to eq(:max_tokens)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'coerces non-string keys via to_s' do
|
|
41
|
+
expect(host.stop_reason_lookup(:tool_calls)).to eq(:tool_use)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns nil for an unmapped key (caller decides the default)' do
|
|
45
|
+
expect(host.stop_reason_lookup('totally_unknown')).to be_nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe 'provider additions (#stop_reason_map_additions)' do
|
|
50
|
+
it 'base returns {} so no guard is ever needed' do
|
|
51
|
+
expect(host.stop_reason_map_additions).to eq({})
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'a provider adds provider-specific strings on top of the common map' do
|
|
55
|
+
bedrock_like = Class.new do
|
|
56
|
+
include Legion::Extensions::Llm::StopReasonMapping
|
|
57
|
+
|
|
58
|
+
def stop_reason_map_additions
|
|
59
|
+
{ 'guardrail_intervened' => :content_filter }
|
|
60
|
+
end
|
|
61
|
+
end.new
|
|
62
|
+
|
|
63
|
+
# addition resolves
|
|
64
|
+
expect(bedrock_like.stop_reason_lookup('guardrail_intervened')).to eq(:content_filter)
|
|
65
|
+
# common vocabulary still inherited
|
|
66
|
+
expect(bedrock_like.stop_reason_lookup('tool_calls')).to eq(:tool_use)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'additions win over the common map on key collision' do
|
|
70
|
+
override = Class.new do
|
|
71
|
+
include Legion::Extensions::Llm::StopReasonMapping
|
|
72
|
+
|
|
73
|
+
def stop_reason_map_additions
|
|
74
|
+
{ 'stop' => :stop_sequence }
|
|
75
|
+
end
|
|
76
|
+
end.new
|
|
77
|
+
|
|
78
|
+
expect(override.stop_reason_lookup('stop')).to eq(:stop_sequence)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe 'full replacement (#stop_reason_map)' do
|
|
83
|
+
it 'a provider can replace the whole map, dropping the defaults' do
|
|
84
|
+
replaced = Class.new do
|
|
85
|
+
include Legion::Extensions::Llm::StopReasonMapping
|
|
86
|
+
|
|
87
|
+
def stop_reason_map
|
|
88
|
+
{ 'weird_only' => :end_turn }
|
|
89
|
+
end
|
|
90
|
+
end.new
|
|
91
|
+
|
|
92
|
+
expect(replaced.stop_reason_lookup('weird_only')).to eq(:end_turn)
|
|
93
|
+
expect(replaced.stop_reason_lookup('tool_calls')).to be_nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
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.9
|
|
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
|
|
@@ -323,6 +324,7 @@ files:
|
|
|
323
324
|
- lib/legion/extensions/llm/routing/model_offering.rb
|
|
324
325
|
- lib/legion/extensions/llm/routing/offering_registry.rb
|
|
325
326
|
- lib/legion/extensions/llm/routing/registry_event.rb
|
|
327
|
+
- lib/legion/extensions/llm/stop_reason_mapping.rb
|
|
326
328
|
- lib/legion/extensions/llm/stream_accumulator.rb
|
|
327
329
|
- lib/legion/extensions/llm/streaming.rb
|
|
328
330
|
- lib/legion/extensions/llm/taxonomies.rb
|
|
@@ -347,6 +349,7 @@ files:
|
|
|
347
349
|
- spec/fixtures/ruby.wav
|
|
348
350
|
- spec/fixtures/ruby.xml
|
|
349
351
|
- spec/fixtures/sample.pdf
|
|
352
|
+
- spec/legion/extensions/llm/actor/metering_flush_spec.rb
|
|
350
353
|
- spec/legion/extensions/llm/agent_spec.rb
|
|
351
354
|
- spec/legion/extensions/llm/attachment_spec.rb
|
|
352
355
|
- spec/legion/extensions/llm/auto_registration_spec.rb
|
|
@@ -420,6 +423,7 @@ files:
|
|
|
420
423
|
- spec/legion/extensions/llm/routing/model_offering_spec.rb
|
|
421
424
|
- spec/legion/extensions/llm/routing/offering_registry_spec.rb
|
|
422
425
|
- spec/legion/extensions/llm/routing/registry_event_spec.rb
|
|
426
|
+
- spec/legion/extensions/llm/stop_reason_mapping_spec.rb
|
|
423
427
|
- spec/legion/extensions/llm/stream_accumulator_spec.rb
|
|
424
428
|
- spec/legion/extensions/llm/streaming_spec.rb
|
|
425
429
|
- spec/legion/extensions/llm/taxonomies_spec.rb
|