legion-llm 0.16.8 → 0.16.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/.rubocop.yml +4 -58
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +3 -8
- data/Gemfile +4 -3
- data/RULES.md +97 -0
- data/legion-llm.gemspec +1 -1
- data/lib/legion/llm/api/anthropic/messages.rb +7 -0
- data/lib/legion/llm/api/client_translators/anthropic_messages.rb +86 -73
- data/lib/legion/llm/api/client_translators/openai_chat.rb +62 -54
- data/lib/legion/llm/api/client_translators/openai_responses.rb +91 -90
- data/lib/legion/llm/api/client_translators/shared_extractors.rb +98 -18
- data/lib/legion/llm/api/debug_formats.rb +11 -37
- data/lib/legion/llm/api/inventory_admin.rb +16 -12
- data/lib/legion/llm/api/model_catalog.rb +25 -28
- data/lib/legion/llm/api/namespaces/anthropic/messages/count_tokens.rb +5 -8
- data/lib/legion/llm/api/namespaces/anthropic/messages.rb +6 -1
- data/lib/legion/llm/api/namespaces/native/models.rb +2 -2
- data/lib/legion/llm/api/namespaces/native/providers.rb +9 -12
- data/lib/legion/llm/api/namespaces/native/routing.rb +3 -4
- data/lib/legion/llm/api/namespaces/openai/models.rb +9 -40
- data/lib/legion/llm/api/namespaces/registration.rb +2 -2
- data/lib/legion/llm/api/native/models.rb +139 -5
- data/lib/legion/llm/api/native/offerings.rb +55 -61
- data/lib/legion/llm/api/native/providers.rb +47 -13
- data/lib/legion/llm/api/native/routing.rb +3 -4
- data/lib/legion/llm/api/native/tiers.rb +31 -24
- data/lib/legion/llm/api/openai/chat_completions.rb +8 -0
- data/lib/legion/llm/api/openai/models.rb +2 -1
- data/lib/legion/llm/api/openai/responses.rb +6 -0
- data/lib/legion/llm/api/routing_error_mapper.rb +4 -3
- data/lib/legion/llm/api/stream_assembler.rb +46 -121
- data/lib/legion/llm/api/translators/anthropic_response.rb +13 -4
- data/lib/legion/llm/api/translators/openai_response.rb +5 -0
- data/lib/legion/llm/api.rb +7 -0
- data/lib/legion/llm/cache/response.rb +3 -3
- data/lib/legion/llm/cache.rb +0 -14
- data/lib/legion/llm/call/daemon_client.rb +1 -1
- data/lib/legion/llm/call/dispatch.rb +34 -112
- data/lib/legion/llm/call/embeddings.rb +50 -74
- data/lib/legion/llm/call/lex_llm_adapter.rb +230 -309
- data/lib/legion/llm/call/selection_dispatch.rb +18 -6
- data/lib/legion/llm/compat.rb +6 -21
- data/lib/legion/llm/context/compressor.rb +12 -1
- data/lib/legion/llm/context/curator.rb +2 -2
- data/lib/legion/llm/errors.rb +19 -6
- data/lib/legion/llm/fleet/dispatcher.rb +56 -37
- data/lib/legion/llm/fleet/token_issuer.rb +14 -9
- data/lib/legion/llm/fleet.rb +0 -1
- data/lib/legion/llm/inference/attempt_context.rb +9 -2
- data/lib/legion/llm/inference/audit_publisher.rb +9 -8
- data/lib/legion/llm/inference/executor/context_window.rb +44 -23
- data/lib/legion/llm/inference/executor/escalation.rb +48 -67
- data/lib/legion/llm/inference/executor/payload_builder.rb +16 -5
- data/lib/legion/llm/inference/executor/routing.rb +36 -40
- data/lib/legion/llm/inference/executor/tool_injection.rb +20 -1
- data/lib/legion/llm/inference/executor.rb +198 -164
- data/lib/legion/llm/inference/native_tool_loop.rb +75 -51
- data/lib/legion/llm/inference/prompt.rb +7 -4
- data/lib/legion/llm/inference/request.rb +43 -20
- data/lib/legion/llm/inference/response.rb +4 -22
- data/lib/legion/llm/inference/route_attempts.rb +72 -27
- data/lib/legion/llm/inference/steps/classification.rb +8 -15
- data/lib/legion/llm/inference/steps/debate.rb +22 -17
- data/lib/legion/llm/inference/steps/prompt_cache.rb +20 -2
- data/lib/legion/llm/inference/steps/rag_context.rb +6 -31
- data/lib/legion/llm/inference/steps/rag_guard.rb +4 -1
- data/lib/legion/llm/inference/steps/tier_assigner.rb +8 -12
- data/lib/legion/llm/inference/steps/tool_calls.rb +44 -37
- data/lib/legion/llm/inference/steps/tool_history.rb +5 -1
- data/lib/legion/llm/inference/steps/trigger_match.rb +5 -15
- data/lib/legion/llm/inference.rb +71 -80
- data/lib/legion/llm/inventory/discovery.rb +20 -478
- data/lib/legion/llm/inventory.rb +37 -335
- data/lib/legion/llm/metering.rb +20 -7
- data/lib/legion/llm/quality/checker.rb +4 -2
- data/lib/legion/llm/router.rb +822 -216
- data/lib/legion/llm/routing/TODO.md +1043 -0
- data/lib/legion/llm/{router/candidate_evaluation.rb → routing/evaluation.rb} +14 -17
- data/lib/legion/llm/routing/filter.rb +406 -0
- data/lib/legion/llm/routing/fleet.rb +32 -0
- data/lib/legion/llm/{router → routing}/header_constraints.rb +1 -1
- data/lib/legion/llm/{router/outcome_classifier.rb → routing/outcome.rb} +39 -28
- data/lib/legion/llm/routing/rank.rb +210 -0
- data/lib/legion/llm/{router → routing}/settings_snapshot.rb +20 -14
- data/lib/legion/llm/{router → routing}/settings_state.rb +4 -4
- data/lib/legion/llm/settings/router.rb +98 -0
- data/lib/legion/llm/settings/tools.rb +5 -0
- data/lib/legion/llm/settings.rb +39 -35
- data/lib/legion/llm/tools/dispatcher.rb +6 -1
- data/lib/legion/llm/types.rb +0 -4
- data/lib/legion/llm/version.rb +1 -1
- data/lib/legion/llm.rb +16 -53
- metadata +14 -27
- data/lib/legion/llm/fleet/handler.rb +0 -267
- data/lib/legion/llm/inference/routing_session.rb +0 -144
- data/lib/legion/llm/inventory/capabilities.rb +0 -48
- data/lib/legion/llm/inventory/discovery/memory_gate.rb +0 -55
- data/lib/legion/llm/inventory/discovery/system.rb +0 -138
- data/lib/legion/llm/inventory/settings_observer.rb +0 -61
- data/lib/legion/llm/router/body_model_hint_policy.rb +0 -97
- data/lib/legion/llm/router/candidate_evaluator.rb +0 -399
- data/lib/legion/llm/router/escalation/history.rb +0 -28
- data/lib/legion/llm/router/input_bound.rb +0 -151
- data/lib/legion/llm/router/ranker.rb +0 -231
- data/lib/legion/llm/router/rejection_diagnostics.rb +0 -273
- data/lib/legion/llm/router/request_requirements.rb +0 -162
- data/lib/legion/llm/router/required_capabilities.rb +0 -144
- data/lib/legion/llm/router/resolution.rb +0 -74
- data/lib/legion/llm/types/chunk.rb +0 -45
- data/lib/legion/llm/types/content_block.rb +0 -55
- data/lib/legion/llm/types/message.rb +0 -103
- data/lib/legion/llm/types/tool_call.rb +0 -87
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e9b38f4dad17362f5f987ce0010c05d3bcc030aac669e791eee35cf3e5019e6
|
|
4
|
+
data.tar.gz: 9f7e8c585e0f3decdf9c1607668ed582def66427c9602c4f5858fd9985aea30f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8043043f67b2cba5ba943c8e19892e7483dbf543aa5ec60825fde817101a0df0f0ad674abc83fd3dca2f5310a741ffecba0613ff79699d16cc097991f7989ae
|
|
7
|
+
data.tar.gz: 4d30a159fc67b9c4e35ca21f792df9aca40ae49475f6cd854b1ecdf1eab22f3593635a92a5df1896733a2eabeed0527eb2d363e251dbcf8b0a782715d0710a3b
|
data/.rubocop.yml
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
plugins:
|
|
2
2
|
- rubocop-legion
|
|
3
3
|
|
|
4
|
-
# These rubocop-legion cops surface in the local-path 0.1.7 build but were not
|
|
5
|
-
# in the published 0.1.7 gem the repo previously tracked. They flag broad
|
|
6
|
-
# pre-existing patterns unrelated to the N×N enforcement pass; deferred to
|
|
7
|
-
# their own cleanup task. NoUnderscorePrefixedKwargs / NoInlineSettingDefaults
|
|
8
|
-
# / NoDirectDispatch / NoShapeDuckTyping (the B4 set Phase 6 adopts) remain
|
|
9
|
-
# enabled.
|
|
10
4
|
Legion/RescueLogging/NoCapture:
|
|
11
5
|
Enabled: false
|
|
12
|
-
Legion/ConstantSafety/InheritParam:
|
|
13
|
-
Enabled: false
|
|
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
6
|
Legion/Llm/TaxonomyEnum:
|
|
29
7
|
Enabled: false
|
|
30
8
|
Legion/Llm/RescueLogLevel:
|
|
31
9
|
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
10
|
Legion/Llm/SettingsAccessPath:
|
|
37
11
|
Exclude:
|
|
38
12
|
- 'spec/**/*'
|
|
@@ -41,38 +15,14 @@ AllCops:
|
|
|
41
15
|
TargetRubyVersion: 3.4
|
|
42
16
|
NewCops: enable
|
|
43
17
|
SuggestExtensions: false
|
|
44
|
-
|
|
45
|
-
# N×N routing guard cops (Phase 6 enforcement; defaults from rubocop-legion config/default.yml).
|
|
46
|
-
#
|
|
47
|
-
# - NoUnderscorePrefixedKwargs / NoInlineSettingDefaults / NoDirectDispatch are
|
|
48
|
-
# enabled repo-wide.
|
|
49
|
-
# - NoShapeDuckTyping is enabled on the canonical-only surface where the shape
|
|
50
|
-
# contract is fully established by translators. Code at the HTTP/wire ingress
|
|
51
|
-
# (client_translator parse_request, StreamAssembler chunk adapter, DebugFormats
|
|
52
|
-
# request envelope reader, Response.from_provider_message bridge) legitimately
|
|
53
|
-
# inspects shape because that's the layer responsible for normalising into
|
|
54
|
-
# canonical. The cop's scope expands here as the executor's canonical
|
|
55
|
-
# migration (Phase 4 follow-up) lands.
|
|
56
18
|
Legion/Framework/NoShapeDuckTyping:
|
|
57
|
-
Enabled:
|
|
19
|
+
Enabled: false
|
|
58
20
|
Include:
|
|
21
|
+
#- 'lib/legion/llm/api/**/*.rb'
|
|
22
|
+
# - 'lib/legion/llm/inference/**/*.rb'
|
|
23
|
+
Exclude:
|
|
59
24
|
- 'lib/legion/llm/api/**/*.rb'
|
|
60
25
|
- 'lib/legion/llm/inference/**/*.rb'
|
|
61
|
-
Exclude:
|
|
62
|
-
# Legacy tree deprecated this release (R11); deleted next minor.
|
|
63
|
-
- 'lib/legion/llm/api/translators/**/*.rb'
|
|
64
|
-
- 'lib/legion/llm/api/anthropic/**/*.rb'
|
|
65
|
-
- 'lib/legion/llm/api/openai/**/*.rb'
|
|
66
|
-
- 'lib/legion/llm/api/native/**/*.rb'
|
|
67
|
-
- 'lib/legion/llm/api/shared_helpers.rb'
|
|
68
|
-
# Boundary code that legitimately bridges wire ↔ canonical: parse_request
|
|
69
|
-
# at the HTTP ingress, the StreamAssembler chunk adapter (P5 explicitly
|
|
70
|
-
# accepts both Canonical::Chunk and the legacy StreamChunk shape during
|
|
71
|
-
# migration), DebugFormats (reads raw env / reflects request), and
|
|
72
|
-
# shared_extractors (normalises arbitrary thinking content shapes).
|
|
73
|
-
# Pre-canonical inference steps still navigate raw wire hashes; that
|
|
74
|
-
# scope tightens once the executor finishes the canonical migration
|
|
75
|
-
# (Phase 4 follow-up).
|
|
76
26
|
- 'lib/legion/llm/api/client_translators/anthropic_messages.rb'
|
|
77
27
|
- 'lib/legion/llm/api/client_translators/openai_chat.rb'
|
|
78
28
|
- 'lib/legion/llm/api/client_translators/openai_responses.rb'
|
|
@@ -108,14 +58,12 @@ Metrics/BlockLength:
|
|
|
108
58
|
Max: 150
|
|
109
59
|
Exclude:
|
|
110
60
|
- 'spec/**/*'
|
|
111
|
-
|
|
112
61
|
Metrics/AbcSize:
|
|
113
62
|
Max: 110
|
|
114
63
|
Metrics/BlockNesting:
|
|
115
64
|
Max: 4
|
|
116
65
|
Metrics/CyclomaticComplexity:
|
|
117
66
|
Max: 50
|
|
118
|
-
|
|
119
67
|
Metrics/PerceivedComplexity:
|
|
120
68
|
Max: 50
|
|
121
69
|
Style/Documentation:
|
|
@@ -125,8 +73,6 @@ Style/SymbolArray:
|
|
|
125
73
|
Style/FrozenStringLiteralComment:
|
|
126
74
|
Enabled: true
|
|
127
75
|
EnforcedStyle: always
|
|
128
|
-
Naming/FileName:
|
|
129
|
-
Enabled: false
|
|
130
76
|
Naming/PredicateMethod:
|
|
131
77
|
Enabled: false
|
|
132
78
|
Metrics/ParameterLists:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Legion LLM Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.9] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **lex-llm 0.8.0 contract conformance.** Gemspec floor raised to `lex-llm >= 0.8.0`; the repo conforms to the 0.8.0 contract cut (Canonical-only internal language, fleet protocol v3, documented embed/image artifacts).
|
|
7
|
+
- **Canonical messages end-to-end.** Client translators no longer de-canonicalize (`inference_messages` bridges deleted); `Inference::Request` canonicalizes inbound messages at the pipeline entry (plain strings, wire hashes, and Canonical objects are accepted; anything else raises). Executor `context_window`, `native_tool_loop`, `step_context_store`, and `prompt_cache` consume strict `Canonical::Message` accessors.
|
|
8
|
+
- **Fleet requestor is protocol v3.** Dispatcher emits `protocol_version: 3` (explicit, no default fill), requires `execution_contract: exact_offering_v1` + a nonempty `offering_id` (no default provider/instance fill), signs both exact claims unconditionally (S3), and rejects legacy field names at the edge. The reply path rehydrates the serialized `Canonical::Response` under `response:` (E3) — the v2 `content`/`finish_reason` projection is gone.
|
|
9
|
+
- **Client-edge dialect aliasing (O03a).** The three client translators translate client-wire spellings at the edge: params (`max_output_tokens`/`num_predict`/`max_completion_tokens` → `max_tokens`, `budget_tokens`/`thinking_budget` → `max_thinking_tokens`, `stop` → `stop_sequences`), content-type aliases (`input_text`/`output_text` → `text`, `image_url` → canonical `image` block), and JSON-string tool arguments → Hash. Canonical types accept canonical keys/types only.
|
|
10
|
+
- **`/api/llm/providers` surfaces SSOT-activated instances.** The listing unions the Inventory Registry snapshot (deduped by provider+instance) with the compatibility call registry.
|
|
11
|
+
- **`RULES.md` installed** (architecture law, byte-for-byte from the SSOT v4 reference).
|
|
12
|
+
- **SSOT v4: routing behavior extracted into stateless mixins under `Legion::LLM::Routing::`.** `Filter` (lane-eligibility axes including the body-model-hint decision ladder, policy enforcement, capability/context/dimension checks), `Rank` (weight/affinity/rendezvous ranking with band partition), `Fleet` (fleet-contract evaluation), `Outcome` (provider-outcome classification — renamed from `OutcomeClassifier`), and immutable records `CandidateEvaluation`/`EvaluationSet` (`routing/evaluation.rb`, 10-axis per-lane readiness snapshot). Each mixin is independently unit-testable with no cross-mixin or Router class dependency.
|
|
13
|
+
- **SSOT v4: Router is a per-request CLASS** (written in `router_new.rb`, wiring pending). One `Legion::LLM::Router.new(request:, operation:, body_model:)` per logical request; it holds the request's attempt state (exclusions, consumed targets, attempt budget, last rejection) — absorbing the former `Inference::RoutingSession` role. Request-derived facts (filters, pins, required capabilities, input bound, context budget, the body-model-hint decision) are computed once in `initialize` and are immutable for the request's lifetime. Inventory is fetched live at each lane decision from `Inventory::Registry` — no captured snapshot.
|
|
14
|
+
- **Routing settings read directly from `Legion::Settings[:llm][:router]`.** Defaults centralized in `lib/legion/llm/settings/router.rb`. Per-provider/instance/model cascades (model policy allow/deny, `enable_<capability>` overrides, preferred-context ranges) are resolved live from `Legion::Settings[:extensions][:llm]` via the shared `SettingsCascade`. The per-request `SettingsState`/`SettingsSnapshot` objects are eliminated from the routing path.
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
- **Legacy rip (0.8.0 cut).** The `Responses::StreamChunk` detection in `StreamAssembler` (type deleted upstream) and its tool-call accumulator; dead `Types::Message#to_provider_hash`; the legacy `Fleet::Handler` responder path (replaced by the lex-llm `ProviderResponder` registry topology); provider-native `Embedding` value-object handling in the embed consumer (replaced by the documented 0.8.0 embed artifact `{ text:, model:, embedding:, usage: }`).
|
|
18
|
+
- **Legacy `lib/legion/llm/router/` support folder retired.** 11 files deleted (`candidate_evaluator`, `ranker`, `rejection_diagnostics`, `required_capabilities`, `input_bound`, `request_requirements`, `body_model_hint_policy`, `resolution`, `settings_state`, `settings_snapshot`, `header_constraints`) and 2 renamed into the new namespace (`candidate_evaluation` → `routing/evaluation.rb`, `outcome_classifier` → `routing/outcome.rb`) — their logic reproduced across the `routing/` mixins and the Router class. (`escalation/history.rb` remains until the `llm.rb` require graph is rewired.) The 18 old specs under `spec/legion/llm/router/` were removed: 16 reproduced or subsumed by the new `spec/legion/llm/routing/` suites + `router_new_spec.rb`, and the two behavioral regressions (fail-forward release bar, instance-recovery) re-encoded against the Router class as `spec/legion/llm/router/{fail_forward_release_bar,instance_recovery_regression}_spec.rb`.
|
|
19
|
+
|
|
20
|
+
### Notes
|
|
21
|
+
- **Migration is mid-flight.** The routing mixins (`routing/*.rb`) are landed and spec-covered; the old support folder is deleted. The Router class replacement (`router_new.rb`) is written but not yet wired into the require graph (module→class swap and consumer rewiring are pending). The existing `router.rb` stateless module remains the active entry point until the swap lands.
|
|
22
|
+
|
|
3
23
|
## [0.16.8] - 2026-08-19
|
|
4
24
|
|
|
5
25
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# legion-llm
|
|
1
|
+
# legion-llm
|
|
2
2
|
|
|
3
3
|
Core LegionIO gem: LLM routing, provider dispatch, the inference pipeline, and the
|
|
4
|
-
OpenAI/Anthropic-compatible API surface.
|
|
5
|
-
|
|
4
|
+
OpenAI/Anthropic-compatible API surface. Detailed reference lives in `README.md`; deep history
|
|
5
|
+
lives in git.
|
|
6
6
|
|
|
7
7
|
## What it is (the one-paragraph model)
|
|
8
8
|
|
|
@@ -14,11 +14,6 @@ gems. The daemon is an **execution proxy**, not a passthrough (see Invariants).
|
|
|
14
14
|
|
|
15
15
|
## Build & Test
|
|
16
16
|
|
|
17
|
-
```bash
|
|
18
|
-
bundle exec rspec # ~3200 examples, must be 0 failures before any commit
|
|
19
|
-
bundle exec rubocop # 0 offenses required (rubocop-legion plugin)
|
|
20
|
-
```
|
|
21
|
-
|
|
22
17
|
The **in-process matrix harness** (`spec/legion/llm/api/matrix/`) is the **commit gate**. It boots
|
|
23
18
|
the real Sinatra app, mounts `/v1/messages` `/v1/responses` `/v1/chat/completions`, and replays the
|
|
24
19
|
full client × scenario matrix against a deterministic `FakeProvider` in ~250ms. Any change to
|
data/Gemfile
CHANGED
|
@@ -8,9 +8,10 @@ legion_settings_path = File.expand_path('../legion-settings', __dir__)
|
|
|
8
8
|
gem 'legion-settings', path: legion_settings_path if Dir.exist?(legion_settings_path)
|
|
9
9
|
|
|
10
10
|
group :test do
|
|
11
|
-
# The published lex-llm (>= 0.
|
|
12
|
-
# types and fleet
|
|
13
|
-
# present (development); CI resolves the published gem via
|
|
11
|
+
# The published lex-llm (>= 0.8.0, declared in the gemspec) provides the 0.8.0
|
|
12
|
+
# Canonical types and fleet protocol-v3 behavior these specs require. Use the
|
|
13
|
+
# local checkout when present (development); CI resolves the published gem via
|
|
14
|
+
# the gemspec dependency.
|
|
14
15
|
lex_llm_path = File.expand_path('../extensions-ai/lex-llm', __dir__)
|
|
15
16
|
gem 'lex-llm', path: lex_llm_path if Dir.exist?(lex_llm_path)
|
|
16
17
|
|
data/RULES.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
RULES.md — Legion LLM Architecture Law
|
|
2
|
+
These rules apply to every task, file, repository, agent, model, session, test, refactor, migration, incident, and release.
|
|
3
|
+
The requested task defines what may change. These rules define how the system ALWAYS works.
|
|
4
|
+
Every rule remains active 100% of the time. If requested work conflicts with these rules, stop and surface the conflict before changing code.
|
|
5
|
+
These are architecture laws. Scope, compatibility, urgency, convenience, tests, existing behavior, and model judgment do not change them.
|
|
6
|
+
1. Canonical is the only internal language.
|
|
7
|
+
Every client translates client wire -> Canonical before shared execution.
|
|
8
|
+
Shared execution carries Canonical through context, tools, routing, direct dispatch, fleet dispatch, and response handling.
|
|
9
|
+
Every provider translates Canonical -> provider wire at the provider boundary, then provider wire -> Canonical before returning to shared execution.
|
|
10
|
+
Every internal boundary validates the Canonical type it is defined to receive and raises immediately when that contract is violated.
|
|
11
|
+
Client Wire -> Client Translator -> Canonical -> Shared Execution -> Canonical -> Provider Translator -> Provider Wire.
|
|
12
|
+
2. Serialization preserves Canonical.
|
|
13
|
+
Transport may serialize Canonical state. The receiving transport boundary ALWAYS rehydrates the exact Canonical type before execution continues.
|
|
14
|
+
Fleet follows Canonical -> serialize -> wire -> deserialize -> rehydrate Canonical -> Canonical.
|
|
15
|
+
Serialization changes encoding only. Ownership, identity, model, operation, capability, selection, and meaning remain exactly the same.
|
|
16
|
+
After rehydration, shared execution continues only with Canonical objects.
|
|
17
|
+
3. Every authoritative fact has exactly one owner.
|
|
18
|
+
The owner creates the fact once. Every downstream layer carries, projects, serializes, rehydrates, verifies, or executes that exact fact.
|
|
19
|
+
A downstream layer receiving missing or contradictory authoritative state raises and returns the defect to the owning layer.
|
|
20
|
+
Authority ALWAYS moves forward by preservation.
|
|
21
|
+
Authority is created once and is never recreated downstream.
|
|
22
|
+
4. Requirements describe the request. Inventory describes reality. Router chooses. Dispatch executes.
|
|
23
|
+
Canonical request construction owns request semantics. RequestRequirements expresses operation, capabilities, modality, context, output, tools, and explicit pins.
|
|
24
|
+
Providers publish exact executable facts into Inventory. Inventory owns canonical instance, offering, lane, capability, context, quota, health, and published weight state.
|
|
25
|
+
Router.next_lane consumes Requirements plus one immutable Inventory snapshot and produces one authoritative Selection.
|
|
26
|
+
Dispatch executes that Selection exactly. Once Selection exists, routing is finished.
|
|
27
|
+
5. Inventory facts are immutable executable facts.
|
|
28
|
+
Providers publish exact instances and complete offering snapshots through the Inventory publication contract.
|
|
29
|
+
Identity, capability evidence, context evidence, quota domains, availability, and write-time weights are consumed from published Inventory state.
|
|
30
|
+
A changed fact becomes authoritative only through the owning publication or reconciliation path and a new Inventory snapshot.
|
|
31
|
+
Routing reads Inventory. Dispatch verifies and executes Inventory-backed Selection.
|
|
32
|
+
6. Identity, capabilities, weights, and context policy retain exact ownership.
|
|
33
|
+
Inventory::Identity owns instance, offering, and lane identity; canonical instance identity is provider family plus the operator/configured instance name; physical endpoint data remains secondary.
|
|
34
|
+
Providers publish capability evidence. Requirements state required capabilities. Candidate evaluation compares the two and determines capability eligibility.
|
|
35
|
+
The weight owner computes lane weight at publication time; Inventory stores it; ranking consumes that stored weight; a stored zero disables the lane.
|
|
36
|
+
Preferred-context binning orders eligible candidates into preference bands and preserves eligibility. Capability, health, binning, and weight ALWAYS retain distinct meanings.
|
|
37
|
+
7. Routing chooses exactly once.
|
|
38
|
+
Router.next_lane is the sole routing authority.
|
|
39
|
+
Candidate evaluation determines eligibility from Requirements and Inventory. Ranking orders eligible candidates from published routing facts.
|
|
40
|
+
Selection freezes the exact provider, instance, offering, lane, model, operation, and routing identity required for execution.
|
|
41
|
+
Every downstream component consumes the Selection it receives.
|
|
42
|
+
Selection is preserved, not reconstructed.
|
|
43
|
+
8. Exact execution stays exact through every boundary.
|
|
44
|
+
Direct dispatch executes the exact Selection-derived binding it receives.
|
|
45
|
+
Fleet dispatch serializes and signs that exact binding; fleet validation verifies it; fleet rehydration restores it; worker resolution verifies it against authoritative Inventory.
|
|
46
|
+
The selected provider, instance, offering, lane, model, and operation remain identical through projection, signing, transport, validation, rehydration, resolution, and callable invocation.
|
|
47
|
+
A mismatch raises before provider execution.
|
|
48
|
+
An exact execution request ALWAYS remains exact execution.
|
|
49
|
+
9. Health and errors preserve one authoritative meaning.
|
|
50
|
+
Inventory owns exact-instance availability. An authoritative instance-unavailable result removes that exact instance; readiness probing owns recovery; successful readiness republish re-admits it.
|
|
51
|
+
Overload, timeout, rate limit, model-not-ready, and transient provider failures remain request-local according to ProviderOutcome semantics.
|
|
52
|
+
The first layer that can authoritatively classify an error performs that classification once. Every downstream layer preserves it.
|
|
53
|
+
Programming errors remain programming errors. Contract violations remain contract violations. Routing exhaustion remains the defined typed Rejection.
|
|
54
|
+
10. Compatibility exists only at explicit edges.
|
|
55
|
+
Supported legacy clients and protocols are translated into the current Canonical and SSOT architecture at explicit compatibility boundaries.
|
|
56
|
+
Shared execution remains Canonical. Routing remains SSOT-driven. Exact execution remains exact.
|
|
57
|
+
Compatibility code adapts an external contract to the current internal architecture.
|
|
58
|
+
The current internal architecture ALWAYS has one representation, one routing authority, one identity system, and one execution truth.
|
|
59
|
+
11. Fix every defect at its owner.
|
|
60
|
+
Trace the incorrect value to the layer that owns it, then fix that owner.
|
|
61
|
+
Fix client wire in the client translator; Canonical shape in Canonical construction; Requirements in Requirements construction; provider facts in publication; identity in Inventory identity; weights in publication/reconciliation.
|
|
62
|
+
Fix eligibility in candidate evaluation; ordering in ranking; choice in Router.next_lane; execution preservation in dispatch; provider wire in the provider translator.
|
|
63
|
+
The layer where a defect becomes visible is evidence. The owning layer is where the correction belongs.
|
|
64
|
+
12. A discovered issue remains in its owning domain.
|
|
65
|
+
Complete the requested task inside its stated scope.
|
|
66
|
+
When investigation exposes a separate defect owned by another architectural domain, record and surface it as separate work unless the requested task is explicitly expanded.
|
|
67
|
+
Routing work consumes existing Canonical Requirements and Inventory facts. Canonical work changes Canonical contracts. Provider work changes publication or translation. Transport work changes transport.
|
|
68
|
+
Nearby code never changes ownership. “While we are here” never changes architecture.
|
|
69
|
+
13. N x N ALWAYS converges through Canonical.
|
|
70
|
+
Equivalent client semantics produce equivalent Canonical state before shared execution. Every provider consumes the same Canonical semantics for the same request.
|
|
71
|
+
When two paths disagree, capture the state at every involved boundary and locate the FIRST point where Canonical meaning diverges.
|
|
72
|
+
Fix that first divergent boundary, then run the exact failing path again.
|
|
73
|
+
Client behavior is proven at client-wire <-> Canonical. Provider behavior is proven at Canonical <-> provider-wire. Shared execution is proven with Canonical throughout.
|
|
74
|
+
14. Debug from captured authoritative state.
|
|
75
|
+
Capture the actual input at the failing boundary before reasoning from symptoms.
|
|
76
|
+
For translation or transport defects, capture Canonical immediately before and after every involved boundary.
|
|
77
|
+
For routing or dispatch defects, capture Requirements, relevant Inventory facts, Selection, execution binding, and ProviderOutcome.
|
|
78
|
+
Compare each captured value to the contract owned by that layer. Find the first divergence. Fix its owner. Re-run the exact path.
|
|
79
|
+
Then inspect sibling implementations for the same defect class.
|
|
80
|
+
15. Tests prove the real boundary and the invariant.
|
|
81
|
+
A boundary test exercises the real boundary it claims to protect.
|
|
82
|
+
Fleet tests exercise real serialization, deserialization, Canonical rehydration, signing, validation, exact resolution, and callable dispatch.
|
|
83
|
+
Provider tests exercise the real callable boundary and provider translator. Routing tests exercise real Requirements, Inventory records, candidate evaluation, ranking, and Selection.
|
|
84
|
+
Regression tests prove the violated invariant, not only the observed symptom.
|
|
85
|
+
A green suite is release evidence only when the tested path traverses the real architecture.
|
|
86
|
+
16. Shared contracts are consumed directly.
|
|
87
|
+
Shared Canonical types own execution representation. Shared Inventory types own inventory state. Shared Routing types own routing state.
|
|
88
|
+
Shared taxonomy owns canonical mappings. Shared ProviderOutcome owns provider-neutral outcomes. Shared fleet protocol owns exact execution claims.
|
|
89
|
+
Every repository consumes these shared owners directly.
|
|
90
|
+
A defect in one shared boundary triggers an audit of every sibling implementation of that boundary. Fix the shared owner centrally whenever the defect belongs to a shared contract.
|
|
91
|
+
17. Architecture is the release gate.
|
|
92
|
+
Every change preserves every rule in this file.
|
|
93
|
+
Tests, compatibility, historical behavior, migration phase, patch urgency, nearby code, task wording, and model judgment are evaluated UNDER these rules.
|
|
94
|
+
A contradiction between existing behavior and these rules is surfaced as an architecture conflict and resolved at the owning boundary before release.
|
|
95
|
+
Limited scope means do less. Limited scope NEVER means fewer rules apply.
|
|
96
|
+
These rules apply 100% of the time.
|
|
97
|
+
These are the law.
|
data/legion-llm.gemspec
CHANGED
|
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.add_dependency 'legion-settings', '>= 1.4.2'
|
|
39
39
|
spec.add_dependency 'legion-transport', '>= 1.4.14'
|
|
40
40
|
spec.add_dependency 'lex-knowledge'
|
|
41
|
-
spec.add_dependency 'lex-llm', '>= 0.
|
|
41
|
+
spec.add_dependency 'lex-llm', '>= 0.8.0'
|
|
42
42
|
spec.add_dependency 'pdf-reader'
|
|
43
43
|
spec.add_dependency 'sinatra-contrib', '>= 2.0'
|
|
44
44
|
spec.add_dependency 'tzinfo', '>= 2.0'
|
|
@@ -58,6 +58,13 @@ module Legion
|
|
|
58
58
|
text_delta_lines = [] # buffer in-stream deltas until we know if tool calls exist
|
|
59
59
|
|
|
60
60
|
pipeline_response = executor.call_stream do |chunk|
|
|
61
|
+
# ANNOTATE (G3/L4-family, legacy tree): the provider
|
|
62
|
+
# boundary yields Canonical::Chunk — for a canonical
|
|
63
|
+
# chunk the .to_s fallback leaks the chunk's Ruby
|
|
64
|
+
# inspect string to the client as text. The flat legacy
|
|
65
|
+
# API tree (register_legacy) is a coordinated-wave
|
|
66
|
+
# deletion surface; this chunk read is fixed with the
|
|
67
|
+
# tree, not inline.
|
|
61
68
|
text = chunk.respond_to?(:content) ? chunk.content.to_s : chunk.to_s
|
|
62
69
|
next if text.empty?
|
|
63
70
|
|
|
@@ -29,13 +29,15 @@ module Legion
|
|
|
29
29
|
|
|
30
30
|
Canonical = Legion::Extensions::Llm::Canonical
|
|
31
31
|
|
|
32
|
+
# N8: a provider :error stop is an error state, not a clean
|
|
33
|
+
# completion — it renders as 'error', never 'end_turn'.
|
|
32
34
|
STOP_REASON_MAP = {
|
|
33
35
|
end_turn: 'end_turn',
|
|
34
36
|
tool_use: 'tool_use',
|
|
35
37
|
max_tokens: 'max_tokens',
|
|
36
38
|
stop_sequence: 'stop_sequence',
|
|
37
39
|
content_filter: 'content_filter',
|
|
38
|
-
error: '
|
|
40
|
+
error: 'error',
|
|
39
41
|
pause_turn: 'pause_turn'
|
|
40
42
|
}.freeze
|
|
41
43
|
|
|
@@ -95,7 +97,10 @@ module Legion
|
|
|
95
97
|
routing_explicit = canonical_request.metadata[:routing_explicit]
|
|
96
98
|
extra[:routing_explicit] = routing_explicit if routing_explicit
|
|
97
99
|
|
|
98
|
-
messages
|
|
100
|
+
# N x N law: the executor receives canonical messages end-to-end.
|
|
101
|
+
# No canonical-to-hash de-canonicalization (the former
|
|
102
|
+
# inference_messages bridge is deleted).
|
|
103
|
+
messages = canonical_request.messages
|
|
99
104
|
request_kwargs = {
|
|
100
105
|
id: request_id,
|
|
101
106
|
messages: messages,
|
|
@@ -111,7 +116,10 @@ module Legion
|
|
|
111
116
|
conversation_id: canonical_request.conversation_id,
|
|
112
117
|
stream: canonical_request.stream == true,
|
|
113
118
|
modality: modality,
|
|
114
|
-
|
|
119
|
+
# N2: shared execution carries the canonical Thinking::Config
|
|
120
|
+
# (or nil) — the Anthropic dialect is re-shaped by the PROVIDER
|
|
121
|
+
# translator at the provider edge, never in the pipeline.
|
|
122
|
+
thinking: canonical_request.thinking,
|
|
115
123
|
cache: { strategy: :default, cacheable: true },
|
|
116
124
|
extra: extra,
|
|
117
125
|
metadata: canonical_request.metadata
|
|
@@ -307,9 +315,13 @@ module Legion
|
|
|
307
315
|
end
|
|
308
316
|
|
|
309
317
|
def on_message_delta(stop_reason:, output_tokens:)
|
|
318
|
+
# N8/M7: an absent or unmapped stop state renders as 'error',
|
|
319
|
+
# never a fabricated 'end_turn' — the client must be able to
|
|
320
|
+
# tell a provider-declared completion from a stop the daemon
|
|
321
|
+
# did not observe (matches the sync edge policy).
|
|
310
322
|
emit('message_delta', {
|
|
311
323
|
type: 'message_delta',
|
|
312
|
-
delta: { stop_reason: STOP_REASON_MAP[stop_reason] || '
|
|
324
|
+
delta: { stop_reason: STOP_REASON_MAP[stop_reason] || 'error', stop_sequence: nil },
|
|
313
325
|
usage: { output_tokens: output_tokens.to_i }
|
|
314
326
|
})
|
|
315
327
|
end
|
|
@@ -369,7 +381,7 @@ module Legion
|
|
|
369
381
|
# without buffering through the assembler.
|
|
370
382
|
def format_tool_call_delta_chunk(canonical_chunk)
|
|
371
383
|
tc = canonical_chunk.tool_call
|
|
372
|
-
args = tc
|
|
384
|
+
args = tool_fragment_field(tc, :arguments) || {}
|
|
373
385
|
block_index = canonical_chunk.block_index || 0
|
|
374
386
|
|
|
375
387
|
if server_tool_chunk?(tc)
|
|
@@ -378,8 +390,8 @@ module Legion
|
|
|
378
390
|
index: block_index,
|
|
379
391
|
content_block: {
|
|
380
392
|
type: 'server_tool_use',
|
|
381
|
-
id: tc
|
|
382
|
-
name: tc
|
|
393
|
+
id: tool_fragment_field(tc, :id),
|
|
394
|
+
name: tool_fragment_field(tc, :name).to_s,
|
|
383
395
|
input: args_as_object(args)
|
|
384
396
|
}
|
|
385
397
|
}
|
|
@@ -397,11 +409,13 @@ module Legion
|
|
|
397
409
|
end
|
|
398
410
|
|
|
399
411
|
def server_tool_chunk?(tool_call)
|
|
400
|
-
|
|
412
|
+
# The chunk fragment's source is the closed dispatch-type enum
|
|
413
|
+
# (R4 fragment contract) — server-executed tools are the
|
|
414
|
+
# registry/special/extension/mcp set (G24).
|
|
415
|
+
source = tool_fragment_field(tool_call, :source)
|
|
401
416
|
return false if source.nil?
|
|
402
417
|
|
|
403
|
-
|
|
404
|
-
%i[special registry extension mcp].include?(type&.to_sym)
|
|
418
|
+
%i[special registry extension mcp].include?(source.to_sym)
|
|
405
419
|
end
|
|
406
420
|
|
|
407
421
|
private
|
|
@@ -558,11 +572,14 @@ module Legion
|
|
|
558
572
|
|
|
559
573
|
raw.map do |t|
|
|
560
574
|
ts = symbolize(t)
|
|
575
|
+
# M5/H1: a client-declared tool carries NO source — source is
|
|
576
|
+
# explicit-or-absent, never fabricated. The dispatch-side source
|
|
577
|
+
# (:client) is stamped by build_tool_definitions (the OUTBOUND
|
|
578
|
+
# tool-definition builder), not on the canonical request tools.
|
|
561
579
|
{
|
|
562
580
|
name: ts[:name].to_s,
|
|
563
581
|
description: ts[:description].to_s,
|
|
564
|
-
parameters: ts[:input_schema] || ts[:parameters] || {}
|
|
565
|
-
source: { type: :client, executable: false }
|
|
582
|
+
parameters: ts[:input_schema] || ts[:parameters] || {}
|
|
566
583
|
}
|
|
567
584
|
end
|
|
568
585
|
end
|
|
@@ -580,7 +597,7 @@ module Legion
|
|
|
580
597
|
|
|
581
598
|
# Map an Anthropic-style thinking block ({type:, budget_tokens:}) to
|
|
582
599
|
# the canonical {effort:, budget:} kwargs that
|
|
583
|
-
# Canonical::
|
|
600
|
+
# Canonical::Thinking::Config.new accepts. Anthropic doesn't carry an
|
|
584
601
|
# effort spelling — only a budget — so effort stays nil.
|
|
585
602
|
def extract_thinking(thinking)
|
|
586
603
|
return nil if thinking.nil?
|
|
@@ -595,24 +612,21 @@ module Legion
|
|
|
595
612
|
|
|
596
613
|
{ effort: effort, budget: budget }.compact
|
|
597
614
|
else
|
|
598
|
-
#
|
|
599
|
-
{
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return nil unless h.is_a?(Hash) && !h.empty?
|
|
615
|
+
# Bare (non-Hash) flag: the dialect's formal spelling is the
|
|
616
|
+
# {type:, budget_tokens:} block. A bare value maps only when it
|
|
617
|
+
# is itself a closed-enum effort (the M4 EFFORT_BUDGET keys) —
|
|
618
|
+
# honored 1:1. Anything else (true, 'enabled', 'on', ...)
|
|
619
|
+
# expresses neither effort nor budget and this dialect documents
|
|
620
|
+
# no default — the config drops out, same treatment as
|
|
621
|
+
# {type: 'enabled'} with no budget above. Never fabricate.
|
|
622
|
+
value = thinking.is_a?(Symbol) ? thinking.to_s : thinking
|
|
623
|
+
return nil unless value.is_a?(String)
|
|
624
|
+
|
|
625
|
+
normalized = value.downcase
|
|
626
|
+
return { effort: normalized } if Canonical::Thinking::Config::EFFORT_BUDGET.key?(normalized)
|
|
611
627
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
inference[:effort] = h[:effort] if h[:effort]
|
|
615
|
-
inference
|
|
628
|
+
nil
|
|
629
|
+
end
|
|
616
630
|
end
|
|
617
631
|
|
|
618
632
|
def build_tool_definitions(canonical_tools)
|
|
@@ -635,20 +649,6 @@ module Legion
|
|
|
635
649
|
end
|
|
636
650
|
end
|
|
637
651
|
|
|
638
|
-
# Canonical messages → plain hashes the executor's Inference::Request
|
|
639
|
-
# currently expects. Round-trips text/tool_calls/tool_call_id.
|
|
640
|
-
def inference_messages(canonical_messages)
|
|
641
|
-
canonical_messages.map do |m|
|
|
642
|
-
hash = m.respond_to?(:to_h) ? m.to_h : m
|
|
643
|
-
{
|
|
644
|
-
role: hash[:role],
|
|
645
|
-
content: hash[:content],
|
|
646
|
-
tool_calls: hash[:tool_calls],
|
|
647
|
-
tool_call_id: hash[:tool_call_id]
|
|
648
|
-
}.compact
|
|
649
|
-
end
|
|
650
|
-
end
|
|
651
|
-
|
|
652
652
|
def build_content(pipeline_response)
|
|
653
653
|
tool_calls = extract_tool_calls(pipeline_response)
|
|
654
654
|
blocks = []
|
|
@@ -684,18 +684,15 @@ module Legion
|
|
|
684
684
|
blocks.empty? ? [{ type: 'text', text: '' }] : blocks
|
|
685
685
|
end
|
|
686
686
|
|
|
687
|
+
# G3: the envelope carries the provider's Canonical::Thinking —
|
|
688
|
+
# member reads only (the former Hash-OR-member dual shape was the
|
|
689
|
+
# split-world seam).
|
|
687
690
|
def thinking_content_block(pipeline_response)
|
|
688
691
|
thinking = pipeline_response.respond_to?(:thinking) ? pipeline_response.thinking : nil
|
|
689
692
|
return nil if thinking.nil?
|
|
690
693
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
else
|
|
694
|
-
{ content: thinking.respond_to?(:content) ? thinking.content : thinking.to_s }
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
content = normalized[:content].to_s
|
|
698
|
-
signature = normalized[:signature].to_s
|
|
694
|
+
content = thinking.content.to_s
|
|
695
|
+
signature = thinking.signature.to_s
|
|
699
696
|
|
|
700
697
|
return nil if content.empty? && signature.empty?
|
|
701
698
|
|
|
@@ -704,31 +701,31 @@ module Legion
|
|
|
704
701
|
block
|
|
705
702
|
end
|
|
706
703
|
|
|
704
|
+
# G3: the envelope carries Array<Canonical::ToolCall> — member
|
|
705
|
+
# reads only (the former respond_to?/Hash dual shape was the
|
|
706
|
+
# split-world seam).
|
|
707
707
|
def extract_tool_calls(pipeline_response)
|
|
708
708
|
tools = pipeline_response.respond_to?(:tools) ? pipeline_response.tools : nil
|
|
709
709
|
return [] if tools.nil?
|
|
710
710
|
|
|
711
711
|
Array(tools).map do |tc|
|
|
712
|
-
source = if tc.respond_to?(:source)
|
|
713
|
-
tc.source
|
|
714
|
-
else
|
|
715
|
-
(tc.is_a?(Hash) ? tc[:source] : nil)
|
|
716
|
-
end
|
|
717
712
|
{
|
|
718
|
-
id: tc.
|
|
719
|
-
name: tc.
|
|
720
|
-
arguments: tc.
|
|
721
|
-
result: tc.
|
|
722
|
-
server_tool: server_tool_source?(source)
|
|
713
|
+
id: tc.id,
|
|
714
|
+
name: tc.name,
|
|
715
|
+
arguments: tc.arguments,
|
|
716
|
+
result: tc.result,
|
|
717
|
+
server_tool: server_tool_source?(tc.source)
|
|
723
718
|
}
|
|
724
719
|
end
|
|
725
720
|
end
|
|
726
721
|
|
|
722
|
+
# The canonical ToolCall.source is the closed dispatch-type enum —
|
|
723
|
+
# server-executed tools are the registry/special/extension/mcp set
|
|
724
|
+
# (G24).
|
|
727
725
|
def server_tool_source?(source)
|
|
728
726
|
return false if source.nil?
|
|
729
727
|
|
|
730
|
-
|
|
731
|
-
%i[special registry extension mcp].include?(type&.to_sym)
|
|
728
|
+
%i[special registry extension mcp].include?(source.to_sym)
|
|
732
729
|
end
|
|
733
730
|
|
|
734
731
|
def format_stop_reason(pipeline_response)
|
|
@@ -739,14 +736,29 @@ module Legion
|
|
|
739
736
|
return 'tool_use' if tool_calls.any? { |tc| !tc[:server_tool] && tc[:result].nil? }
|
|
740
737
|
|
|
741
738
|
stop = pipeline_response.respond_to?(:stop) ? pipeline_response.stop : nil
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
739
|
+
# The envelope's stop member is the { reason: } Hash — one shape
|
|
740
|
+
# (G3).
|
|
741
|
+
reason = stop.is_a?(Hash) ? (stop[:reason] || stop['reason']) : nil
|
|
742
|
+
# N8/M7: only known completion states render as completions.
|
|
743
|
+
# :error stays an error, and an absent or unmapped stop state
|
|
744
|
+
# surfaces as 'error' — the client must be able to tell a
|
|
745
|
+
# provider-declared end_turn from a stop the daemon did not
|
|
746
|
+
# observe. Never fabricate :end_turn. (The legacy :stop dialect
|
|
747
|
+
# value is not in the canonical stop vocabulary — it falls
|
|
748
|
+
# through to the error surface like any unmapped state.)
|
|
749
|
+
case reason&.to_sym
|
|
750
|
+
when :end_turn
|
|
751
|
+
'end_turn'
|
|
752
|
+
when :stop_sequence
|
|
753
|
+
'stop_sequence'
|
|
754
|
+
when :tool_use
|
|
755
|
+
'tool_use'
|
|
756
|
+
when :max_tokens
|
|
757
|
+
'max_tokens'
|
|
758
|
+
when :content_filter
|
|
759
|
+
'content_filter'
|
|
760
|
+
else
|
|
761
|
+
'error'
|
|
750
762
|
end
|
|
751
763
|
end
|
|
752
764
|
|
|
@@ -754,7 +766,8 @@ module Legion
|
|
|
754
766
|
return result if result.is_a?(String)
|
|
755
767
|
|
|
756
768
|
Legion::JSON.dump(result)
|
|
757
|
-
rescue StandardError
|
|
769
|
+
rescue StandardError => e
|
|
770
|
+
handle_exception(e, level: :warn, operation: 'llm.client_translator.anthropic.serialize_result')
|
|
758
771
|
result.to_s
|
|
759
772
|
end
|
|
760
773
|
end
|