legion-llm 0.13.0 → 0.14.3
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/.rubocop.yml +26 -0
- data/CHANGELOG.md +180 -0
- data/CLAUDE.md +23 -17
- data/README.md +34 -15
- data/REFACTOR-HANDOFF.md +299 -0
- data/docs/work/planning/p1-results.md +125 -0
- data/docs/work/planning/p2-results.md +101 -0
- data/docs/work/planning/p3-results.md +133 -0
- data/docs/work/planning/p4-results.md +129 -0
- data/docs/work/planning/p5-results.md +86 -0
- data/legion-llm.gemspec +1 -1
- data/lib/legion/llm/api/client_translators/anthropic_messages.rb +6 -3
- data/lib/legion/llm/api/client_translators/openai_chat.rb +6 -3
- data/lib/legion/llm/api/client_translators/openai_responses.rb +29 -33
- data/lib/legion/llm/api/client_translators/shared_extractors.rb +49 -0
- data/lib/legion/llm/api/error_translator.rb +71 -0
- data/lib/legion/llm/api/inventory_admin.rb +42 -0
- data/lib/legion/llm/api/namespaces/anthropic/messages.rb +9 -1
- data/lib/legion/llm/api/namespaces/helpers.rb +29 -0
- data/lib/legion/llm/api/namespaces/native/routing.rb +4 -23
- data/lib/legion/llm/api/namespaces/openai/chat/completions.rb +12 -4
- data/lib/legion/llm/api/namespaces/openai/responses.rb +17 -13
- data/lib/legion/llm/api/native/models.rb +5 -2
- data/lib/legion/llm/api/native/providers.rb +16 -14
- data/lib/legion/llm/api/native/routing.rb +4 -23
- data/lib/legion/llm/api/native/tiers.rb +5 -5
- data/lib/legion/llm/api/shared_helpers.rb +23 -0
- data/lib/legion/llm/api/stream_assembler.rb +88 -5
- data/lib/legion/llm/api.rb +2 -0
- data/lib/legion/llm/call/daemon_client.rb +1 -1
- data/lib/legion/llm/call/embeddings.rb +81 -46
- data/lib/legion/llm/call/lex_llm_adapter.rb +9 -0
- data/lib/legion/llm/call/providers.rb +0 -18
- data/lib/legion/llm/call/registry.rb +2 -2
- data/lib/legion/llm/call/structured_output.rb +1 -1
- data/lib/legion/llm/compat.rb +40 -3
- data/lib/legion/llm/context/compressor.rb +1 -1
- data/lib/legion/llm/context/curator.rb +2 -2
- data/lib/legion/llm/errors.rb +75 -0
- data/lib/legion/llm/fleet/dispatcher.rb +1 -1
- data/lib/legion/llm/helper.rb +10 -10
- data/lib/legion/llm/hooks/budget_guard.rb +1 -1
- data/lib/legion/llm/hooks/rag_guard.rb +1 -1
- data/lib/legion/llm/hooks/reciprocity.rb +2 -2
- data/lib/legion/llm/hooks/reflection.rb +2 -2
- data/lib/legion/llm/inference/context_accounting.rb +27 -7
- data/lib/legion/llm/inference/executor/context_window.rb +21 -5
- data/lib/legion/llm/inference/executor/escalation.rb +263 -360
- data/lib/legion/llm/inference/executor/payload_builder.rb +126 -0
- data/lib/legion/llm/inference/executor/routing.rb +60 -44
- data/lib/legion/llm/inference/executor/tool_injection.rb +1 -1
- data/lib/legion/llm/inference/executor.rb +12 -71
- data/lib/legion/llm/inference/native_tool_loop.rb +6 -101
- data/lib/legion/llm/inference/prompt.rb +7 -8
- data/lib/legion/llm/inference/request.rb +5 -2
- data/lib/legion/llm/inference/route_attempts.rb +4 -36
- data/lib/legion/llm/inference/steps/confidence_scoring.rb +1 -1
- data/lib/legion/llm/inference/steps/gaia_advisory.rb +5 -5
- data/lib/legion/llm/inference/steps/mcp_discovery.rb +1 -1
- data/lib/legion/llm/inference.rb +39 -16
- data/lib/legion/llm/inventory/capabilities.rb +48 -0
- data/lib/legion/llm/inventory/discovery/memory_gate.rb +55 -0
- data/lib/legion/llm/inventory/discovery/system.rb +138 -0
- data/lib/legion/llm/inventory/discovery.rb +565 -0
- data/lib/legion/llm/inventory/settings_observer.rb +61 -0
- data/lib/legion/llm/inventory/sweeper.rb +56 -0
- data/lib/legion/llm/inventory.rb +217 -458
- data/lib/legion/llm/metering/tokens.rb +2 -2
- data/lib/legion/llm/router/availability.rb +6 -159
- data/lib/legion/llm/router/health_tracker.rb +101 -41
- data/lib/legion/llm/router.rb +97 -572
- data/lib/legion/llm/scheduling.rb +1 -1
- data/lib/legion/llm/settings.rb +52 -32
- data/lib/legion/llm/skills/base.rb +1 -1
- data/lib/legion/llm/skills/disk_loader.rb +1 -1
- data/lib/legion/llm/skills/external_discovery.rb +2 -2
- data/lib/legion/llm/tools/confidence.rb +5 -5
- data/lib/legion/llm/tools/dispatcher.rb +1 -1
- data/lib/legion/llm/transport/message.rb +1 -1
- data/lib/legion/llm/types/message.rb +1 -1
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +17 -12
- metadata +18 -14
- data/lib/legion/llm/capabilities.rb +0 -46
- data/lib/legion/llm/discovery/memory_gate.rb +0 -53
- data/lib/legion/llm/discovery/rule_generator.rb +0 -327
- data/lib/legion/llm/discovery/system.rb +0 -136
- data/lib/legion/llm/discovery.rb +0 -703
- data/lib/legion/llm/router/arbitrage.rb +0 -138
- data/lib/legion/llm/router/candidates.rb +0 -263
- data/lib/legion/llm/router/escalation/chain.rb +0 -51
- data/lib/legion/llm/router/escalation/tracker.rb +0 -76
- data/lib/legion/llm/router/registry_lookup.rb +0 -121
- data/lib/legion/llm/router/rule.rb +0 -134
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dac2aadf427ccf4c6e902242ed392c3f9f0f5a3e0ddaeeb6d6c262e3926c87b1
|
|
4
|
+
data.tar.gz: 48ccd139e47330107147d7745ec7e34c7a1de879cc8939d2d1fd33322d04b336
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27c3269042cf0b060f0e402fad967f048319a2a98a425d4b68f39569d07e41842505bf947f32ae62a52125a0c81143e4d1fcf076bee53e247fd0cfe76a9e7f6a
|
|
7
|
+
data.tar.gz: 1fcd99c1a3007b858bc033f57a1cd3ada1fd96e2729d597fd65f8ac8eeb406295f1b73a2d4bcea31793ca6669b0bba545c32ac86474e2c9f1a5adcc60f9550f2
|
data/.rubocop.yml
CHANGED
|
@@ -12,6 +12,31 @@ Legion/RescueLogging/NoCapture:
|
|
|
12
12
|
Legion/ConstantSafety/InheritParam:
|
|
13
13
|
Enabled: false
|
|
14
14
|
|
|
15
|
+
# rubocop-legion 0.1.9 cops — enabled as of P0 but deferred enforcement:
|
|
16
|
+
# these four cops flag broad pre-existing patterns in the codebase that
|
|
17
|
+
# predate the SSOT refactor. They gate NEW code from P1 onward; existing
|
|
18
|
+
# violations are cleaned up within each phase as the code they govern is
|
|
19
|
+
# rewritten. Do not add new violations; do not suppress them with inline
|
|
20
|
+
# rubocop:disable without a tracking comment.
|
|
21
|
+
#
|
|
22
|
+
# TODO(P1): enable Legion/Llm/TaxonomyEnum repo-wide after lane taxonomy
|
|
23
|
+
# is established and all :type/:tier/:circuit_state literals updated.
|
|
24
|
+
# TODO(P1): enable Legion/Llm/RescueLogLevel repo-wide after back-compat
|
|
25
|
+
# :debug rescue handlers are audited and leveled up.
|
|
26
|
+
# TODO(P1): enable Legion/Llm/NoLoopDo repo-wide after cache/drain loops
|
|
27
|
+
# are converted to bounded iteration.
|
|
28
|
+
Legion/Llm/TaxonomyEnum:
|
|
29
|
+
Enabled: false
|
|
30
|
+
Legion/Llm/RescueLogLevel:
|
|
31
|
+
Enabled: false
|
|
32
|
+
Legion/Llm/NoLoopDo:
|
|
33
|
+
Enabled: false
|
|
34
|
+
# SettingsAccessPath is enabled for lib/ only — specs legitimately write
|
|
35
|
+
# settings via the loader path to set up test fixtures.
|
|
36
|
+
Legion/Llm/SettingsAccessPath:
|
|
37
|
+
Exclude:
|
|
38
|
+
- 'spec/**/*'
|
|
39
|
+
|
|
15
40
|
AllCops:
|
|
16
41
|
TargetRubyVersion: 3.4
|
|
17
42
|
NewCops: enable
|
|
@@ -106,6 +131,7 @@ Naming/PredicateMethod:
|
|
|
106
131
|
Enabled: false
|
|
107
132
|
Metrics/ParameterLists:
|
|
108
133
|
Max: 9
|
|
134
|
+
CountKeywordArgs: false
|
|
109
135
|
Style/RedundantConstantBase:
|
|
110
136
|
Exclude:
|
|
111
137
|
- 'spec/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,185 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.3] - 2026-06-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Context window enforcement now accounts for tool definition tokens when deciding whether to compact messages. Previously, 235 injected tools (~50k tokens) were invisible to the threshold check, causing context overflow on dispatch.
|
|
8
|
+
- Context overflow errors are no longer unconditionally terminal during escalation. When a lane with a larger context window exists, the executor retries on that lane instead of raising immediately.
|
|
9
|
+
- `X-Legion-Format: canonical` debug surface enabled by default (was gated to dev/lite mode only). The canonical format is a first-class output format, not a debug feature.
|
|
10
|
+
- Completion log now includes `context_tokens_saved=N` showing total tokens saved by curation, archival, thinking-strip, and context window compaction per request.
|
|
11
|
+
- Embedding dispatch now sends chunks individually instead of as an array, fixing intermittent `ContextOverflow` on RAG embedding queries with ollama.
|
|
12
|
+
- Truncated tool results now instruct the model to make smaller targeted requests instead of silently chopping content.
|
|
13
|
+
- `context_window_threshold` moved from hardcoded 0.90 to `context_curation_defaults` setting.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `tool_result_max_dispatch_chars` raised from 2k to 5k (dispatch-time truncation of oversized tool results before provider call).
|
|
18
|
+
- `context_curation.tool_result_max_chars` lowered from 10k to 2k (curator distills tool results earlier, reducing context growth).
|
|
19
|
+
- Default `tier_weights` rebalanced: direct=105, local=110, fleet=110, cloud=120, frontier=150 (prefer higher-capability tiers when lane_weight tie-breaks).
|
|
20
|
+
|
|
21
|
+
## [0.14.2] - 2026-06-20
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Move auto-routing model aliases into `llm.routing.auto_routing_model_aliases`, so `legionio` and `auto` stay configurable rather than hard-coded.
|
|
26
|
+
- Ignore request-body `model` values as routing hints unless `llm.routing.allow_body_routing_hints` is explicitly enabled; auto-routing aliases still mean "you pick".
|
|
27
|
+
- Stop treating injected special tools as an implicit native-tools routing requirement when the client did not actually request tools.
|
|
28
|
+
|
|
29
|
+
## [0.14.1] - 2026-06-20
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Treat Bedrock region-prefixed model ids such as `us.anthropic.claude-sonnet-4-6` as equivalent to
|
|
34
|
+
the inventory's canonical `anthropic.claude-sonnet-4-6` lane during hard model filtering, so
|
|
35
|
+
routing no longer raises `NoLaneAvailable` for valid Bedrock requests.
|
|
36
|
+
|
|
37
|
+
## [0.14.0] - 2026-06-19
|
|
38
|
+
|
|
39
|
+
### Changed (BREAKING — internal API)
|
|
40
|
+
|
|
41
|
+
- **Inventory is now a single live `Concurrent::Map`** keyed by 5-part lane id
|
|
42
|
+
`tier:provider:instance:type:model`. The catalog is composed on write (by `lex-llm-*` discovery
|
|
43
|
+
actors via the `Inventory::ScopedRefresher` mixin), not recomposed on read. Per-request
|
|
44
|
+
`offerings_calls` collapses from ~4N to ≤1.
|
|
45
|
+
- **`Router.request_lane(**routing_payload)` is the single selection method.** `Router.resolve`,
|
|
46
|
+
`Router.resolve_chain`, `Router::Candidates`, `Router::EscalationChain`, `Arbitrage`, and the
|
|
47
|
+
full chain-building machinery are deleted.
|
|
48
|
+
- **`HealthTracker` writes lane health one-directionally.** The old request-time read API
|
|
49
|
+
(`circuit_state(provider:, instance:)`, `adjustment(...)`, `model_denied?(...)`) is deleted.
|
|
50
|
+
Health is now read from `lane[:health]`.
|
|
51
|
+
- **Dual error classes replace the old `EscalationExhausted`.** `Errors::NoLaneAvailable` (HTTP 400;
|
|
52
|
+
filters excluded all candidates from the start) and `Errors::EscalationExhausted` (HTTP 503 +
|
|
53
|
+
`Retry-After`; max attempts reached mid-flight) are the new error contract. Both inherit from
|
|
54
|
+
`LLMError`.
|
|
55
|
+
- **Embedding selection uses `Router.request_lane(type: :embedding, models: [pinned])`.** Strict
|
|
56
|
+
model pin — no cross-model failover. The bespoke embedding-selection machine is deleted.
|
|
57
|
+
- **`while remaining.positive?` loop replaces `loop do`.** The executor's request lifecycle is
|
|
58
|
+
bounded by construction; `loop do`, `retry`, `redo` are forbidden by the `NoLoopDo` rubocop cop.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `Inventory.write_lane(lane:, ttl:, **)` / `.delete_lane(id:, **)` / `.lane(id:, **)` /
|
|
63
|
+
`.lanes_for(provider:, instance:, type:, model:, **)` / `.lanes(**)` — kwargs-only public API.
|
|
64
|
+
- `Inventory::Sweeper` `::Every` actor — TTL safety net for dead-actor lane orphans.
|
|
65
|
+
- **RANKING v2:** `lane_weight = tier_w × provider_w × instance_w × model_w × health_mult`,
|
|
66
|
+
precomputed at write time, surfaced in `/api/llm/providers/<p>/models`. Operator-tunable via
|
|
67
|
+
settings; all weights default to 100.
|
|
68
|
+
- `Legion::Cache::Local` cooldown circuit for auth failures
|
|
69
|
+
(`llm_auth_failed:<credential_hash>` key). Short-circuits dispatch during the cooldown window
|
|
70
|
+
without tripping the instance circuit.
|
|
71
|
+
- `PayloadBuilder` single ingress site at `inference/executor/payload_builder.rb`. Validates
|
|
72
|
+
`x-legion-tiers`, `x-legion-providers`, `x-legion-instances`, `x-legion-models` headers against
|
|
73
|
+
frozen taxonomies. Unknown values → 400 with `error.type: invalid_header`.
|
|
74
|
+
- `StreamAssembler` mid-stream failover contract: `provider_failover_pending!(from:)` clears the
|
|
75
|
+
canonical buffer; `finalize` emits debug trailers (`x-legion-failover-from`, `-to`, `-count`)
|
|
76
|
+
only when failover occurred. No custom SSE event (N×N invariant 5).
|
|
77
|
+
- Admin endpoint `POST /api/llm/inventory/refresh` — operator-triggered catalog refresh.
|
|
78
|
+
- `:fleet` is a first-class tier in the `Taxonomies::TIERS` enum.
|
|
79
|
+
|
|
80
|
+
### Deprecated
|
|
81
|
+
|
|
82
|
+
- `Router.populate_auto_rules(_)` — no-op stub. Removed in v0.15.0 after call sites in `lex-llm-*`
|
|
83
|
+
gems are cleaned up. Tracking issue: [#154](https://github.com/legion-io/legion-llm/issues/154).
|
|
84
|
+
Remove-stub issue: [#155](https://github.com/legion-io/legion-llm/issues/155).
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `/v1/moderations` 500 error (missing `Call::Registry.providers` method).
|
|
89
|
+
- Compliance leak via discovery path: denied models could enter `/api/llm/offerings` because the
|
|
90
|
+
discovery feeder bypassed `lex-llm-*` whitelist/blacklist filtering. `Inventory.write_lane` is
|
|
91
|
+
now the single fail-closed choke point.
|
|
92
|
+
- Mid-stream provider failover now correctly clears the canonical buffer — no thinking tokens from
|
|
93
|
+
provider A leak into provider B's response context.
|
|
94
|
+
|
|
95
|
+
### Removed
|
|
96
|
+
|
|
97
|
+
- `Legion::LLM::EscalationTracker` (dead code, zero callers).
|
|
98
|
+
- `Inventory#native_provider_offerings`, `discovery_offerings`, `dedupe_offerings`, `build_offering`,
|
|
99
|
+
`add_fleet_lane`, `compose_offerings` — replaced by `lex-llm-*` gem writers via the
|
|
100
|
+
`Inventory::ScopedRefresher` mixin.
|
|
101
|
+
- `Call::Registry.all_provider_families` (duplicate of `.available`).
|
|
102
|
+
- Hardcoded last-resort tier model literals.
|
|
103
|
+
- `Providers.inject_anthropic_cache_control!` — moved to `lex-llm-anthropic` translator (CLAUDE.md
|
|
104
|
+
invariant #3).
|
|
105
|
+
- `lib/legion/llm/discovery.rb`, `lib/legion/llm/capabilities.rb`, `lib/legion/llm/discovery/`
|
|
106
|
+
compat shim forwarders (module paths moved to `inventory/` tree in v0.13.x; shims deleted in
|
|
107
|
+
v0.14.0).
|
|
108
|
+
- `Router::Candidates`, `Router::Arbitrage`, `Router::EscalationChain` (all deleted; use
|
|
109
|
+
`Router.request_lane`).
|
|
110
|
+
|
|
111
|
+
### Breaking change notes
|
|
112
|
+
|
|
113
|
+
- **Embedding single-instance HA:** single-instance Ollama (or any single embedding provider) will
|
|
114
|
+
produce 400 `NoLaneAvailable` during the ~5–10s restart window rather than silently retrying.
|
|
115
|
+
Use two instances for HA.
|
|
116
|
+
- **Rollback requires yanking the entire train.** `lex-llm 0.6.0`'s `ScopedRefresher` calls
|
|
117
|
+
`Inventory.write_lane` which does not exist on `legion-llm 0.13.x`. Yanking `legion-llm 0.14.0`
|
|
118
|
+
alone is insufficient — `lex-llm 0.6.0` and all 9 `lex-llm-*` paired versions must be yanked
|
|
119
|
+
together. See `docs/migration/0.14.0.md` for the 3am rollback procedure.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## [0.13.3] - 2026-06-18
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
|
|
127
|
+
- **OpenAI Responses (`/v1/responses`) tool turns now terminate with `response.completed`.** A turn
|
|
128
|
+
carrying client-callable `function_call` items was emitting a non-standard `response.done` with
|
|
129
|
+
`status: requires_action` — Assistants-API vocabulary the Responses protocol has no concept of. Real
|
|
130
|
+
Responses clients wait for `response.completed`, so each tool turn surfaced to the client as
|
|
131
|
+
"stream disconnected before completion" and forced a reconnect/retry. The terminal event is now
|
|
132
|
+
always `response.completed` / `status: completed` with the `function_call` items in `output[]`
|
|
133
|
+
(streaming **and** non-streaming); `requires_action`/`action_required` removed. Server-executed
|
|
134
|
+
(LegionIO) tools were already `completed` and are unchanged. Specs updated to assert the protocol.
|
|
135
|
+
- **Router no longer manufactures escalation fallbacks the live catalog doesn't offer.**
|
|
136
|
+
`build_fallback_resolutions` enumerated registered instances and paired each with a default model
|
|
137
|
+
without checking the catalog offered it, producing dead candidates (a provider + a model it does
|
|
138
|
+
not serve) that availability rejected on every request — wasted work plus `resolution_unavailable`
|
|
139
|
+
log noise. Fallbacks are now gated against `Inventory` (the catalog SSOT) via
|
|
140
|
+
`fallback_model_offered?`, so an unoffered triple is never proposed.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
|
|
144
|
+
- Removed the per-response `extract_thinking` INFO log spam (it fired 4–6× per request, once per
|
|
145
|
+
extraction site). The extraction is unchanged; only the diagnostic logging was dropped.
|
|
146
|
+
|
|
147
|
+
## [0.13.2] - 2026-06-17
|
|
148
|
+
|
|
149
|
+
### Fixed
|
|
150
|
+
|
|
151
|
+
- **Discovery no longer blocks the request path on a live network refresh.** `Discovery#discovered_models`
|
|
152
|
+
used to refresh synchronously once its 60s TTL lapsed — a serial, per-instance live fetch
|
|
153
|
+
(`adapter.offerings(live: true)`) on the request thread, so one unreachable/slow instance stalled
|
|
154
|
+
routing for its socket timeout (~20s, recurring ~once a minute). It surfaced as a fast `[pipeline][timing]`
|
|
155
|
+
with the time hidden in the `routing` step (which reads candidates via `model_available?`/`model_size`).
|
|
156
|
+
The request path now only **reads** the cache; refresh is owned by the provider `DiscoveryRefresh`
|
|
157
|
+
`::Every` actors (background) + the startup `Discovery.run` warm.
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- **Discovered-models cache is a `Concurrent::Map` keyed by provider.** Each provider's refresh actor
|
|
162
|
+
writes its own key atomically (no read-modify-write across providers, no lock); reads flatten all
|
|
163
|
+
values lock-free. `@discovery_status` is likewise a `Concurrent::Map` (the `@discovery_mutex` is
|
|
164
|
+
removed). Dead read-path TTL machinery (`discovered_models_stale?`, `discovery_refresh_seconds`)
|
|
165
|
+
deleted; the `llm.discovery.refresh_seconds` setting is now inert (actors use their own interval).
|
|
166
|
+
|
|
167
|
+
## [0.13.1] - 2026-06-17
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
|
|
171
|
+
- **Streamed responses no longer leak Ruby object inspect strings to the client.** The
|
|
172
|
+
`StreamAssembler::ChunkAdapter` — the single chunk→wire normalizer — rendered provider value
|
|
173
|
+
objects with `.to_s` when they weren't plain strings, so the client SSE could carry
|
|
174
|
+
`#<Legion::Extensions::Llm::Thinking:0x…>` (Claude Code `/v1/messages`, via the legacy-chunk
|
|
175
|
+
`legacy_thinking` path that only checked `#content` while the legacy `Thinking` exposes `#text`)
|
|
176
|
+
or `[#<data …Canonical::ContentBlock…>]` (Codex `/v1/responses`, via a `text_delta` whose delta
|
|
177
|
+
arrived as a `ContentBlock` array). Both paths now unwrap to text and never `.to_s` a value
|
|
178
|
+
object onto the wire. The metering/audit ledger was already clean — only the streaming wire was
|
|
179
|
+
affected; the in-process matrix did not catch it because the `FakeProvider` emits canonical
|
|
180
|
+
chunks only (the documented provider-shape blind spot), so the regression is locked by direct
|
|
181
|
+
`StreamAssembler` specs.
|
|
182
|
+
|
|
3
183
|
## [0.13.0] - 2026-06-17
|
|
4
184
|
|
|
5
185
|
Consolidated release. This single version bundles every change from `0.12.14` through `0.12.35`
|
data/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# legion-llm (v0.
|
|
1
|
+
# legion-llm (v0.14.0)
|
|
2
2
|
|
|
3
3
|
Core LegionIO gem: LLM routing, provider dispatch, the inference pipeline, and the
|
|
4
4
|
OpenAI/Anthropic-compatible API surface. This file is loaded into **every** session — it is
|
|
@@ -31,8 +31,8 @@ push. If a regression breaks live e2e but not the matrix, the matrix is missing
|
|
|
31
31
|
|------|------|
|
|
32
32
|
| Facade (`start`, `chat`, `ask`, `embed`) | `lib/legion/llm.rb` |
|
|
33
33
|
| **Single source of truth for the catalog** | `lib/legion/llm/inventory.rb` |
|
|
34
|
-
| Router (`
|
|
35
|
-
| Escalation / failover | `lib/legion/llm/router/escalation
|
|
34
|
+
| Router (`request_lane` — single selection) | `lib/legion/llm/router.rb`, `router/{availability,resolution,health_tracker}.rb` |
|
|
35
|
+
| Escalation history / failover | `lib/legion/llm/router/escalation/history.rb`, `inference/executor/escalation.rb` |
|
|
36
36
|
| Pipeline executor (18 steps, streaming) | `lib/legion/llm/inference/executor.rb` (+ `executor/*.rb`) |
|
|
37
37
|
| Pipeline steps | `lib/legion/llm/inference/steps/*.rb` |
|
|
38
38
|
| Client API routes | `lib/legion/llm/api/openai/`, `api/anthropic/`, `api/native/` |
|
|
@@ -75,21 +75,27 @@ These have caused production incidents. They are also enforced by `rubocop-legio
|
|
|
75
75
|
prompt; server-executed tools run server-side; client-passthrough tools surface as pending
|
|
76
76
|
calls for the client. Simplest end-to-end check that the proxy contract holds in both formats.
|
|
77
77
|
|
|
78
|
-
## Routing rules (current behaviour)
|
|
79
|
-
|
|
80
|
-
- **`Inventory
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
## Routing rules (RANKING v2 — current behaviour)
|
|
79
|
+
|
|
80
|
+
- **`Inventory` live `Concurrent::Map` is THE catalog.** Keyed by 5-part lane id
|
|
81
|
+
`tier:provider:instance:type:model`. Written by `lex-llm-*` discovery actors via the
|
|
82
|
+
`Inventory::ScopedRefresher` mixin. `HealthTracker` is the only other writer (owns `health`
|
|
83
|
+
block per lane). Everyone reads the same map, lock-free.
|
|
84
|
+
- **`Router.request_lane(**routing_payload)` is the single selection method.** Returns one lane
|
|
85
|
+
hash or `nil`. Hard filters → soft filter (lane_weight ≤ 0 excluded) → max-weight bucket →
|
|
86
|
+
uniform sample. No pre-built chains.
|
|
87
|
+
- **Escalation = "ask again with the failed lane excluded."** Executor calls `request_lane` in a
|
|
88
|
+
`while remaining.positive?` loop, appending tried lane ids to `tried_lanes`. No `loop do`.
|
|
89
|
+
- **`lane_weight = tier_w × provider_w × instance_w × model_w × health_mult`.** Precomputed on
|
|
90
|
+
write. Negative = open circuit or policy-denied (excluded by soft filter). Surfaced in
|
|
91
|
+
`/api/llm/providers/<p>/models`. Tunable via `settings[:llm][:routing][:weights]`.
|
|
92
|
+
- **`:fleet` is a first-class tier** in `Taxonomies::TIERS`. Fleet lanes written by `lex-llm-*`
|
|
93
|
+
fleet workers appear alongside direct lanes.
|
|
94
|
+
- **`NoLaneAvailable` (400):** hard filters excluded everything before the first attempt.
|
|
95
|
+
**`EscalationExhausted` (503 + `Retry-After`):** max attempts reached mid-flight.
|
|
90
96
|
- **Model policy is compliance.** `model_whitelist`/`model_blacklist` is honored at dispatch,
|
|
91
|
-
fail-closed. A policy-denied model is **terminal** — never escalated, never trips circuits
|
|
92
|
-
Enforced at the daemon layer
|
|
97
|
+
fail-closed. A policy-denied model is **terminal** — never escalated, never trips circuits.
|
|
98
|
+
Enforced at the daemon layer (`call/dispatch.rb` `enforce_model_policy!` →
|
|
93
99
|
`Errors::ModelNotAllowed`) and in each `lex-llm-*` provider.
|
|
94
100
|
|
|
95
101
|
## Coding constraints (enforced in review + cops)
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg">
|
|
14
14
|
<img alt="Ruby" src="https://img.shields.io/badge/ruby-3.4%2B-CC342D.svg">
|
|
15
|
-
<img alt="Version" src="https://img.shields.io/badge/version-0.
|
|
15
|
+
<img alt="Version" src="https://img.shields.io/badge/version-0.14.0-informational.svg">
|
|
16
16
|
<img alt="Tests" src="https://img.shields.io/badge/tests-3200%2B%20examples%20·%200%20failures-success.svg">
|
|
17
17
|
<img alt="RuboCop" src="https://img.shields.io/badge/rubocop-0%20offenses-success.svg">
|
|
18
18
|
</p>
|
|
@@ -642,24 +642,43 @@ session = llm_session(tier: :local)
|
|
|
642
642
|
| `capability` | `:basic`, `:moderate`, `:reasoning` | `:moderate` | Higher prefers larger/cloud models |
|
|
643
643
|
| `cost` | `:minimize`, `:normal` | `:normal` | `:minimize` prefers local/fleet |
|
|
644
644
|
|
|
645
|
-
#### Routing Resolution
|
|
645
|
+
#### Routing Resolution — RANKING v2
|
|
646
646
|
|
|
647
|
+
`Router.request_lane(**routing_payload)` returns one lane hash from the live `Inventory` catalog
|
|
648
|
+
or `nil`. The catalog is a `Concurrent::Map` of 5-part lane ids
|
|
649
|
+
(`tier:provider:instance:type:model`) populated by `lex-llm-*` discovery actors. No recomputation
|
|
650
|
+
on read.
|
|
651
|
+
|
|
652
|
+
**Selection algorithm:**
|
|
653
|
+
```
|
|
654
|
+
1. Hard filters applied (provider/instance/model/tier constraints from routing_payload).
|
|
655
|
+
2. Soft filter: lanes with lane_weight ≤ 0 excluded (open circuit or policy-denied).
|
|
656
|
+
3. Max-weight bucket selected (all lanes with the highest lane_weight value).
|
|
657
|
+
4. One lane sampled uniformly within the bucket (seeded RNG for reproducibility).
|
|
658
|
+
5. Returns the lane, or nil if no lanes survive filters.
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**RANKING v2 lane_weight formula:**
|
|
647
662
|
```
|
|
648
|
-
|
|
649
|
-
2. Router merges with default_intent (fills missing dimensions)
|
|
650
|
-
3. Load rules from settings, filter by:
|
|
651
|
-
a. Intent match (all `when` conditions must match)
|
|
652
|
-
b. Schedule window (valid_from/valid_until, hours, days)
|
|
653
|
-
c. Constraints (e.g., never_cloud strips cloud-tier rules)
|
|
654
|
-
d. Discovery (Ollama model pulled? Model fits in available RAM?)
|
|
655
|
-
e. Tier availability (is Ollama running? is Transport loaded?)
|
|
656
|
-
4. Score remaining candidates:
|
|
657
|
-
effective_priority = rule.priority
|
|
658
|
-
+ health_tracker.adjustment(provider)
|
|
659
|
-
+ (1.0 - cost_multiplier) * 10
|
|
660
|
-
5. Return Resolution for highest-scoring candidate
|
|
663
|
+
lane_weight = tier_weight × provider_weight × instance_weight × model_weight × health_multiplier
|
|
661
664
|
```
|
|
662
665
|
|
|
666
|
+
All weights default to 100. The health multiplier is:
|
|
667
|
+
- `1.0` — closed circuit (full weight)
|
|
668
|
+
- `0.5` — half-open (reduced weight; cautious retry)
|
|
669
|
+
- `-100_000_000` — open circuit (effectively disabled; excluded by soft filter)
|
|
670
|
+
|
|
671
|
+
Weights are operator-tunable via settings and take effect immediately (no restart required).
|
|
672
|
+
Surfaced in `/api/llm/providers/<provider>/models` as `lane_weight`.
|
|
673
|
+
|
|
674
|
+
**Escalation:** "try again with the failed lane excluded." The executor calls `request_lane` in a
|
|
675
|
+
`while remaining.positive?` loop, appending each tried lane to `tried_lanes`. This replaces the
|
|
676
|
+
old pre-built escalation chain.
|
|
677
|
+
|
|
678
|
+
**Errors:**
|
|
679
|
+
- `Errors::NoLaneAvailable` (HTTP 400) — all filters excluded everything before the first attempt.
|
|
680
|
+
- `Errors::EscalationExhausted` (HTTP 503 + `Retry-After`) — attempts exhausted mid-flight.
|
|
681
|
+
|
|
663
682
|
#### Settings
|
|
664
683
|
|
|
665
684
|
Add routing configuration under the `llm` key:
|