lex-llm 0.7.6 → 0.8.0
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 +87 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
- data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
- data/lib/legion/extensions/llm/canonical/message.rb +52 -67
- data/lib/legion/extensions/llm/canonical/params.rb +59 -33
- data/lib/legion/extensions/llm/canonical/request.rb +67 -54
- data/lib/legion/extensions/llm/canonical/response.rb +61 -76
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
- data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
- data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
- data/lib/legion/extensions/llm/canonical.rb +6 -5
- data/lib/legion/extensions/llm/configuration.rb +40 -10
- data/lib/legion/extensions/llm/connection.rb +8 -30
- data/lib/legion/extensions/llm/credential_sources.rb +32 -49
- data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
- data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
- data/lib/legion/extensions/llm/error.rb +0 -14
- data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
- data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
- data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
- data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
- data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
- data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
- data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
- data/lib/legion/extensions/llm/inventory/records.rb +36 -100
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
- data/lib/legion/extensions/llm/provider.rb +192 -315
- data/lib/legion/extensions/llm/provider_contract.rb +25 -8
- data/lib/legion/extensions/llm/provider_settings.rb +5 -26
- data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
- data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
- data/lib/legion/extensions/llm/routing/records.rb +34 -17
- data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
- data/lib/legion/extensions/llm/streaming.rb +50 -35
- data/lib/legion/extensions/llm/taxonomies.rb +14 -26
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
- data/lib/legion/extensions/llm/utils.rb +23 -5
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +8 -98
- data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
- data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
- data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
- data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
- data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
- data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
- data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
- data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
- data/spec/legion/extensions/llm/error_spec.rb +2 -12
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
- data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
- data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
- data/spec/legion/extensions/llm/provider_spec.rb +98 -78
- data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
- data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
- data/spec/legion/extensions/llm/utils_spec.rb +26 -7
- data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
- data/spec/legion/extensions/llm_extension_spec.rb +5 -5
- data/spec/support/fake_llm_provider.rb +45 -39
- data/spec/support/fake_ssot_harness.rb +7 -2
- metadata +11 -54
- data/lib/legion/extensions/llm/agent.rb +0 -366
- data/lib/legion/extensions/llm/aliases.json +0 -436
- data/lib/legion/extensions/llm/aliases.rb +0 -67
- data/lib/legion/extensions/llm/attachment.rb +0 -229
- data/lib/legion/extensions/llm/chat.rb +0 -354
- data/lib/legion/extensions/llm/chunk.rb +0 -10
- data/lib/legion/extensions/llm/content.rb +0 -81
- data/lib/legion/extensions/llm/context.rb +0 -33
- data/lib/legion/extensions/llm/embedding.rb +0 -33
- data/lib/legion/extensions/llm/image.rb +0 -109
- data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
- data/lib/legion/extensions/llm/message.rb +0 -118
- data/lib/legion/extensions/llm/mime_type.rb +0 -75
- data/lib/legion/extensions/llm/model/info.rb +0 -286
- data/lib/legion/extensions/llm/model/modalities.rb +0 -26
- data/lib/legion/extensions/llm/model/pricing.rb +0 -52
- data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
- data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
- data/lib/legion/extensions/llm/model.rb +0 -11
- data/lib/legion/extensions/llm/models.json +0 -57313
- data/lib/legion/extensions/llm/models.rb +0 -530
- data/lib/legion/extensions/llm/models_schema.json +0 -168
- data/lib/legion/extensions/llm/moderation.rb +0 -60
- data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
- data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
- data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
- data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
- data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
- data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
- data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
- data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
- data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
- data/lib/legion/extensions/llm/thinking.rb +0 -53
- data/lib/legion/extensions/llm/tokens.rb +0 -51
- data/lib/legion/extensions/llm/tool_call.rb +0 -34
- data/lib/legion/extensions/llm/transcription.rb +0 -39
- data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
- data/spec/legion/extensions/llm/agent_spec.rb +0 -179
- data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
- data/spec/legion/extensions/llm/context_spec.rb +0 -127
- data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
- data/spec/legion/extensions/llm/message_spec.rb +0 -64
- data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
- data/spec/legion/extensions/llm/models_spec.rb +0 -104
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6482361b65a465a6eb683fb1789cd47a6a1a80c709c344eefb57205321e90c12
|
|
4
|
+
data.tar.gz: b2dd262d310f469fd1c89fa0b00eb1199822397bdb762c17c056b05cbad1b1d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64cb921e6ccf8361704df0fe4846fb80b3d074ed09b6940f4cff2c2440c8260e36258acda18ed786415d8052a111e33dcd17ad0f1cef1240dd634f14c30f0172
|
|
7
|
+
data.tar.gz: e2bb88cc4d672f4a11a64cdc91cc922964e67eab8baa85ad3108c40cc269363dc8009684162c70d1f98dbf8d0a0757d0ca8790838eb066ddab59a1edf37cb28d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-08-20
|
|
4
|
+
|
|
5
|
+
**The SSOT v4 contract cut.** This is a complete contract line, not a patch wave:
|
|
6
|
+
Canonical is the only internal language, every authoritative value is created once
|
|
7
|
+
and executed exactly, and one path / one class / one method exists per function.
|
|
8
|
+
The architecture law for the line ships in `RULES.md` at the repository root.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`RULES.md`** — the 17 architecture laws of the 0.8.x contract line, byte-for-byte
|
|
12
|
+
at the repo root (G5; mirrored to sibling repos at their next releases).
|
|
13
|
+
- **Fleet protocol v3.** `Fleet::Protocol::VERSION = 3`; exact execution only
|
|
14
|
+
(marker required — absence is rejected); explicit `protocol_version` (no default
|
|
15
|
+
fill); one `Fleet::Protocol::REQUIRED_FIELDS` list consumed by both edges;
|
|
16
|
+
`Fleet::ContractError` for envelope/param/message-shape violations; retryability
|
|
17
|
+
derived from the `ProviderOutcome` kind table (contract/policy/auth kinds never
|
|
18
|
+
retry); the response envelope carries the serialized `Canonical::Response` with
|
|
19
|
+
the G5 thinking exclusion performed exactly once at the responder builder.
|
|
20
|
+
- **Conformance kit.** `spec/legion/extensions/llm/conformance/` now carries the
|
|
21
|
+
full 09 oracle: T1-T7 shared examples for all 12 canonical types plus the B1-B9
|
|
22
|
+
(boundary), F1-F7 (fleet), and R1-R5 (registry) shared examples with a kit
|
|
23
|
+
self-test host.
|
|
24
|
+
- **`Canonical::Thinking::Config`** — one name, a `Data` type (the top-level
|
|
25
|
+
`ThinkingConfig` constant and its alias are gone).
|
|
26
|
+
- **`Canonical::Message#metadata`** — unknown keys fold into metadata on every
|
|
27
|
+
canonical type (04 L5); `cache_control` survival through the full JSON wire is
|
|
28
|
+
conformance-pinned (E01).
|
|
29
|
+
- **Strict argument parser** (`Responses::ToolArguments`) — one strict tool-argument
|
|
30
|
+
parse for sync and streaming paths; invalid JSON is a contract error, never a
|
|
31
|
+
fabricated `{}`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **Canonical strictification (04 L1-L10).** No nil-returning factories (typed
|
|
35
|
+
`ArgumentError` naming type, member, and offending class); no rescue-and-repair;
|
|
36
|
+
no silent drops; canonical keys only (provider-dialect aliases moved to the
|
|
37
|
+
translator edges, O03a); declared enums validated in both factories; `build` and
|
|
38
|
+
`from_hash` share one normalization path per member; uniform `to_h`/`as_json`/
|
|
39
|
+
`to_json` serialization; G20d (chunk produce-strict / consume-unknown pass-through)
|
|
40
|
+
is stated law.
|
|
41
|
+
- **Provider funnel (05/08).** `complete` enforces canonical messages centrally
|
|
42
|
+
(F2); the `temperature:` kwarg is deleted (temperature lives only in
|
|
43
|
+
`Canonical::Params`, O4); positional `messages` on the callable contract; the
|
|
44
|
+
provider-object fleet dispatch topology is deleted (exact registry dispatch only);
|
|
45
|
+
the legacy offering production path is replaced by the `Registry.snapshot` read
|
|
46
|
+
path (07 C5); one `ensure_configured!` (the Connection duplicate is deleted); one
|
|
47
|
+
health classifier; `normalize_dispatch_error` reads the shared kind table on
|
|
48
|
+
`Routing::ProviderOutcome`; silent rescues now log.
|
|
49
|
+
- **Streaming (02 FL13 / 10 U1-U3).** The streaming path produces
|
|
50
|
+
`Canonical::Chunk` / `Canonical::Response` through the shared `ThinkingExtractor`
|
|
51
|
+
segment core and the strict tool-argument parser, preserving the wire
|
|
52
|
+
index-first / recency-fallback tool-call correlation law.
|
|
53
|
+
- **`FleetLane` queue defaults** come from the one settings home
|
|
54
|
+
(`Llm.default_settings`, `consumer_ack_timeout_ms: 90_000`); the downgraded
|
|
55
|
+
`CredentialSources` paths fail closed (O11).
|
|
56
|
+
- **`Routing::Selection` weight validation** is structural only — a zero component
|
|
57
|
+
(operator disable) is accepted, matching `RecordSupport` (U4).
|
|
58
|
+
|
|
59
|
+
### Removed
|
|
60
|
+
- **The legacy type set:** `Llm::Message`, `Llm::Chunk`, `Llm::Tokens`,
|
|
61
|
+
`Llm::Thinking` (+ `Thinking::Config`), `Llm::ToolCall`, `Llm::Content`
|
|
62
|
+
(+ `Content::Raw`), `Llm::Attachment`, the `Responses::ChatResponse` /
|
|
63
|
+
`StreamChunk` / `EmbeddingResponse` wrapper family, and `to_internal_h`.
|
|
64
|
+
- **The provider-native facade** (O12): `Llm.chat/context/embed/moderate/paint/
|
|
65
|
+
transcribe/models/providers` entry points, `Chat`, `Agent`, `Context` — zero
|
|
66
|
+
callers in any repo; capability is preserved through the provider base +
|
|
67
|
+
canonical pipeline + fleet.
|
|
68
|
+
- **Legacy offering/lane surface:** `Routing::ModelOffering`, `Routing::OfferingRegistry`,
|
|
69
|
+
`Routing::LaneKey`, `Inventory::ScopedRefresher` (+ `LegacyCoordinatorAdapter` —
|
|
70
|
+
the lex-llm -> legion-llm reverse dependency is gone), the `Llm::Types` alias
|
|
71
|
+
module, `AutoRegistration#discover_instances`/`#provider_aliases`, the
|
|
72
|
+
`UnsupportedCapabilityError` compat alias, `Llm::Aliases`,
|
|
73
|
+
`Taxonomies::OPERATION_ALIASES`/`OPERATION_TO_LANE_TYPE`/`CIRCUIT_STATES`/
|
|
74
|
+
`HEALTH_KEYS`, `Message.wrap`, `Message#to_provider_hash`, `AmbiguousLegacyOfferingError`,
|
|
75
|
+
and the class-level `model_policy`/`resolve_policy_value` policy cascade.
|
|
76
|
+
|
|
77
|
+
## 0.7.7 - 2026-08-19
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- **`Canonical::Message` carries `cache_control`.** Prompt-cache breakpoints are a first-class canonical member (build/from_hash/to_h and the fleet JSON round-trip preserve it) instead of being dropped by member projection.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
- **Fleet worker rehydrates wire messages to canonical objects.** `Fleet::WorkerExecution` rebuilds `params[:messages]` into `Canonical::Message` at the wire boundary for chat, stream, and count_tokens dispatch (exact and local-provider paths), so callables receive canonical input only. Non-Hash wire entries raise `ExactOfferingMismatchError`.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- **Dispatch-boundary contract helper.** `Provider#enforce_canonical_messages!` is the shared loud-reject contract for provider callables: non-canonical message shapes raise `ArgumentError` instead of being coerced or tolerated. `Provider#count_tokens` enforces it and reads canonical content, removing the hash fallback.
|
|
88
|
+
- **Fleet specs assert the canonical boundary.** Worker/responder spec doubles read `.content` off `Canonical::Message` (wire rehydration delivers objects, not hashes).
|
|
89
|
+
|
|
3
90
|
## 0.7.6 - 2026-08-19
|
|
4
91
|
|
|
5
92
|
### Added
|
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.
|
|
@@ -10,20 +10,11 @@ module Legion
|
|
|
10
10
|
# Prerequisites on the extending module:
|
|
11
11
|
# - `PROVIDER_FAMILY` constant (Symbol, e.g. :ollama)
|
|
12
12
|
# - `provider_class` singleton method returning the Provider subclass
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
#
|
|
14
|
+
# The legacy discover_instances/provider_aliases defaults are deleted
|
|
15
|
+
# (Phase 4): providers define their own instance discovery, and
|
|
16
|
+
# publication goes through Inventory::Publisher directly.
|
|
16
17
|
module AutoRegistration
|
|
17
|
-
# Override in each provider. Returns { instance_id => config_hash }.
|
|
18
|
-
def discover_instances
|
|
19
|
-
{}
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Optional provider-family aliases that legion-llm should register
|
|
23
|
-
# against the same discovered provider instances.
|
|
24
|
-
def provider_aliases
|
|
25
|
-
[]
|
|
26
|
-
end
|
|
27
18
|
end
|
|
28
19
|
end
|
|
29
20
|
end
|
|
@@ -7,24 +7,45 @@ module Legion
|
|
|
7
7
|
module Canonical
|
|
8
8
|
# Canonical streaming chunk with full lifecycle support.
|
|
9
9
|
# Per R4: block_index/item_id/signature lifecycle, multi-tool-call deltas.
|
|
10
|
-
# Per G20d: strict on produce
|
|
10
|
+
# Per G20d (04 §11, stated law): strict on produce — the named factories
|
|
11
|
+
# and the generic build validate type against CHUNK_TYPES; lenient on
|
|
12
|
+
# consume — from_hash accepts any type symbol and passes it through. # -- required for Data.define block scope
|
|
11
13
|
Chunk = ::Data.define(
|
|
12
14
|
:request_id, :conversation_id, :exchange_id,
|
|
13
15
|
:index, :type, :block_index,
|
|
14
16
|
:item_id, :delta, :tool_call, :signature,
|
|
15
17
|
:usage, :stop_reason, :metadata, :timestamp
|
|
16
18
|
) do
|
|
19
|
+
# Generic produce path — the only way to construct an arbitrary-type
|
|
20
|
+
# chunk; type is validated against CHUNK_TYPES (G20d).
|
|
21
|
+
def self.build(
|
|
22
|
+
type:, request_id: nil, conversation_id: nil, exchange_id: nil,
|
|
23
|
+
index: nil, block_index: nil, item_id: nil,
|
|
24
|
+
delta: nil, tool_call: nil, signature: nil,
|
|
25
|
+
usage: nil, stop_reason: nil, metadata: {}, timestamp: nil
|
|
26
|
+
)
|
|
27
|
+
type_sym = type.is_a?(::String) ? type.to_sym : type
|
|
28
|
+
Strict.enum!(type_sym, self::CHUNK_TYPES, self::BUILD_SITE, :type)
|
|
29
|
+
new(
|
|
30
|
+
request_id:, conversation_id:, exchange_id:,
|
|
31
|
+
index:, type: type_sym, block_index:,
|
|
32
|
+
item_id:, delta:,
|
|
33
|
+
tool_call: normalize_tool_call!(tool_call, self::BUILD_SITE),
|
|
34
|
+
signature:,
|
|
35
|
+
usage: normalize_usage!(usage, self::BUILD_SITE),
|
|
36
|
+
stop_reason: stop_reason&.to_sym,
|
|
37
|
+
metadata: Strict.metadata!(metadata, self::BUILD_SITE),
|
|
38
|
+
timestamp: timestamp || ::Time.now
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
17
42
|
# Build a text delta chunk.
|
|
18
43
|
def self.text_delta(delta:, request_id:, conversation_id: nil, exchange_id: nil,
|
|
19
44
|
index: 0, block_index: nil, item_id: nil,
|
|
20
45
|
stop_reason: nil, usage: nil)
|
|
21
|
-
|
|
22
|
-
type: :text_delta, delta
|
|
23
|
-
|
|
24
|
-
exchange_id: exchange_id, block_index: block_index,
|
|
25
|
-
item_id: item_id, tool_call: nil, signature: nil,
|
|
26
|
-
usage: usage, stop_reason: stop_reason, metadata: {},
|
|
27
|
-
timestamp: ::Time.now
|
|
46
|
+
build(
|
|
47
|
+
type: :text_delta, delta:, request_id:, conversation_id:, exchange_id:,
|
|
48
|
+
index:, block_index:, item_id:, stop_reason:, usage:
|
|
28
49
|
)
|
|
29
50
|
end
|
|
30
51
|
|
|
@@ -32,117 +53,93 @@ module Legion
|
|
|
32
53
|
def self.thinking_delta(delta:, request_id:, conversation_id: nil, exchange_id: nil,
|
|
33
54
|
index: 0, block_index: nil, item_id: nil, signature: nil,
|
|
34
55
|
stop_reason: nil, usage: nil)
|
|
35
|
-
|
|
36
|
-
type: :thinking_delta, delta
|
|
37
|
-
|
|
38
|
-
exchange_id: exchange_id, block_index: block_index,
|
|
39
|
-
item_id: item_id, tool_call: nil, signature: signature,
|
|
40
|
-
usage: usage, stop_reason: stop_reason, metadata: {},
|
|
41
|
-
timestamp: ::Time.now
|
|
56
|
+
build(
|
|
57
|
+
type: :thinking_delta, delta:, request_id:, conversation_id:, exchange_id:,
|
|
58
|
+
index:, block_index:, item_id:, signature:, stop_reason:, usage:
|
|
42
59
|
)
|
|
43
60
|
end
|
|
44
61
|
|
|
45
|
-
# Build a tool_call_delta chunk (supports multiple in-flight tool calls
|
|
62
|
+
# Build a tool_call_delta chunk (supports multiple in-flight tool calls
|
|
63
|
+
# via the fragment's id/index). tool_call is the delta fragment:
|
|
64
|
+
# { id:, name:, arguments: <String fragment>, index:, signature: }.
|
|
46
65
|
def self.tool_call_delta(tool_call:, request_id:, conversation_id: nil, exchange_id: nil,
|
|
47
66
|
index: 0, block_index: nil, item_id: nil,
|
|
48
67
|
stop_reason: nil, usage: nil)
|
|
49
|
-
|
|
50
|
-
type: :tool_call_delta,
|
|
51
|
-
|
|
52
|
-
exchange_id: exchange_id, block_index: block_index,
|
|
53
|
-
item_id: item_id, delta: nil, tool_call: tool_call, signature: nil,
|
|
54
|
-
usage: usage, stop_reason: stop_reason, metadata: {},
|
|
55
|
-
timestamp: ::Time.now
|
|
68
|
+
build(
|
|
69
|
+
type: :tool_call_delta, tool_call:, request_id:, conversation_id:, exchange_id:,
|
|
70
|
+
index:, block_index:, item_id:, stop_reason:, usage:
|
|
56
71
|
)
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
# Build a usage chunk.
|
|
60
75
|
def self.usage_chunk(usage:, request_id:, conversation_id: nil, exchange_id: nil)
|
|
61
|
-
|
|
62
|
-
type: :usage, request_id: request_id,
|
|
63
|
-
conversation_id: conversation_id, exchange_id: exchange_id,
|
|
64
|
-
index: nil, block_index: nil, item_id: nil,
|
|
65
|
-
delta: nil, tool_call: nil, signature: nil,
|
|
66
|
-
usage: usage, stop_reason: nil, metadata: {},
|
|
67
|
-
timestamp: ::Time.now
|
|
68
|
-
)
|
|
76
|
+
build(type: :usage, request_id:, conversation_id:, exchange_id:, usage:)
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
# Build a done chunk.
|
|
72
80
|
def self.done(request_id:, usage: nil, stop_reason: nil, conversation_id: nil, exchange_id: nil)
|
|
73
|
-
|
|
74
|
-
type: :done, request_id: request_id,
|
|
75
|
-
conversation_id: conversation_id, exchange_id: exchange_id,
|
|
76
|
-
index: nil, block_index: nil, item_id: nil,
|
|
77
|
-
delta: nil, tool_call: nil, signature: nil,
|
|
78
|
-
usage: usage, stop_reason: stop_reason, metadata: {},
|
|
79
|
-
timestamp: ::Time.now
|
|
80
|
-
)
|
|
81
|
+
build(type: :done, request_id:, usage:, stop_reason:, conversation_id:, exchange_id:)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# Build an error chunk.
|
|
84
|
-
def self.error_chunk(error:, request_id:, conversation_id: nil, exchange_id: nil, metadata:
|
|
85
|
-
|
|
86
|
-
type: :error, request_id
|
|
87
|
-
|
|
88
|
-
index: nil, block_index: nil, item_id: nil,
|
|
89
|
-
delta: nil, tool_call: nil, signature: nil,
|
|
90
|
-
usage: nil, stop_reason: :error,
|
|
91
|
-
metadata: (metadata || {}).merge(error: error),
|
|
92
|
-
timestamp: ::Time.now
|
|
85
|
+
def self.error_chunk(error:, request_id:, conversation_id: nil, exchange_id: nil, metadata: {})
|
|
86
|
+
build(
|
|
87
|
+
type: :error, request_id:, conversation_id:, exchange_id:,
|
|
88
|
+
stop_reason: :error, metadata: metadata.merge(error:)
|
|
93
89
|
)
|
|
94
90
|
end
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return nil if source.nil?
|
|
100
|
-
|
|
101
|
-
h = source.transform_keys(&:to_sym)
|
|
92
|
+
def self.shape_symbol!(value, site, member)
|
|
93
|
+
return nil if value.nil?
|
|
94
|
+
return value.to_sym if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
102
95
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
type_sym = type_raw&.to_sym if type_raw
|
|
96
|
+
raise ArgumentError, "#{site}: #{member} expected String or Symbol, got #{value.class}"
|
|
97
|
+
end
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
99
|
+
# Build from a Hash (raw provider response or deserialized wire payload).
|
|
100
|
+
# Per G20d: ignore-unknown on consume — unknown chunk types pass through.
|
|
101
|
+
def self.from_hash(source)
|
|
102
|
+
Strict.require_hash!(source, self::FROM_HASH_SITE)
|
|
103
|
+
hash = Strict.symbolize_keys(source)
|
|
104
|
+
metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
|
|
105
|
+
type_raw = hash.delete(:type)
|
|
106
|
+
tool_call = normalize_tool_call!(hash.delete(:tool_call), self::FROM_HASH_SITE)
|
|
107
|
+
usage = normalize_usage!(hash.delete(:usage), self::FROM_HASH_SITE)
|
|
108
|
+
stop_reason_raw = hash.delete(:stop_reason)
|
|
109
|
+
timestamp = hash.delete(:timestamp)
|
|
110
|
+
# Remaining keys are all members; pass through with consume defaults.
|
|
111
|
+
new(
|
|
112
|
+
request_id: hash[:request_id],
|
|
113
|
+
conversation_id: hash[:conversation_id],
|
|
114
|
+
exchange_id: hash[:exchange_id],
|
|
115
|
+
index: hash[:index],
|
|
116
|
+
type: type_raw&.to_sym,
|
|
117
|
+
block_index: hash[:block_index],
|
|
118
|
+
item_id: hash[:item_id],
|
|
119
|
+
delta: hash[:delta],
|
|
120
|
+
tool_call:,
|
|
121
|
+
signature: hash[:signature],
|
|
122
|
+
usage:,
|
|
123
|
+
stop_reason: stop_reason_raw&.to_sym,
|
|
124
|
+
metadata:,
|
|
125
|
+
timestamp: timestamp || ::Time.now
|
|
126
|
+
)
|
|
127
|
+
end
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Usage.from_hash(usage_raw)
|
|
120
|
-
end
|
|
129
|
+
# tool_call member: the delta fragment (Hash) or a full ToolCall; nil allowed.
|
|
130
|
+
def self.normalize_tool_call!(tool_call, site)
|
|
131
|
+
return nil if tool_call.nil?
|
|
132
|
+
return tool_call if tool_call.is_a?(::Hash) || tool_call.is_a?(ToolCall)
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
h[:stop_reason] = stop_reason_raw&.to_sym if stop_reason_raw
|
|
134
|
+
Strict.expect_type!(tool_call, [::Hash, ToolCall], site, :tool_call)
|
|
135
|
+
end
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
def self.normalize_usage!(usage, site)
|
|
138
|
+
return nil if usage.nil?
|
|
139
|
+
return usage if usage.is_a?(Usage)
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
request_id: h[:request_id],
|
|
132
|
-
conversation_id: h[:conversation_id],
|
|
133
|
-
exchange_id: h[:exchange_id],
|
|
134
|
-
index: h[:index],
|
|
135
|
-
type: type_sym,
|
|
136
|
-
block_index: h[:block_index],
|
|
137
|
-
item_id: h[:item_id],
|
|
138
|
-
delta: h[:delta],
|
|
139
|
-
tool_call: h[:tool_call],
|
|
140
|
-
signature: h[:signature],
|
|
141
|
-
usage: h[:usage],
|
|
142
|
-
stop_reason: h[:stop_reason],
|
|
143
|
-
metadata: h[:metadata],
|
|
144
|
-
timestamp: h[:timestamp] || ::Time.now
|
|
145
|
-
)
|
|
141
|
+
Strict.expect_type!(usage, [::Hash], site, :usage)
|
|
142
|
+
Usage.from_hash(usage)
|
|
146
143
|
end
|
|
147
144
|
|
|
148
145
|
# Serialize to a Hash for AMQP/fleet/wire transport.
|
|
@@ -186,9 +183,26 @@ module Legion
|
|
|
186
183
|
def content?
|
|
187
184
|
%i[text_delta thinking_delta].include?(type)
|
|
188
185
|
end
|
|
186
|
+
|
|
187
|
+
# H1: the single strict constructor. Member shapes are validated
|
|
188
|
+
# (type/stop_reason as String|Symbol, tool_call/usage through the
|
|
189
|
+
# normalizers, metadata as a Hash). Per G20d the TYPE is only
|
|
190
|
+
# shape-checked here — the produce-side enum pin stays in build,
|
|
191
|
+
# and from_hash (consume) passes unknown types through.
|
|
192
|
+
Strict.install_strict_new!(self) do |values, site|
|
|
193
|
+
values[:type] = shape_symbol!(values[:type], site, :type)
|
|
194
|
+
values[:stop_reason] = shape_symbol!(values[:stop_reason], site, :stop_reason)
|
|
195
|
+
values[:tool_call] = normalize_tool_call!(values[:tool_call], site)
|
|
196
|
+
values[:usage] = normalize_usage!(values[:usage], site)
|
|
197
|
+
values[:metadata] = Strict.metadata!(values[:metadata], site)
|
|
198
|
+
values
|
|
199
|
+
end
|
|
189
200
|
end
|
|
190
201
|
|
|
191
202
|
Chunk::CHUNK_TYPES = %i[text_delta thinking_delta tool_call_delta usage done error].freeze
|
|
203
|
+
Chunk::BUILD_SITE = 'Canonical::Chunk.build'
|
|
204
|
+
Chunk::FROM_HASH_SITE = 'Canonical::Chunk.from_hash'
|
|
205
|
+
Chunk::NEW_SITE = 'Canonical::Chunk.new'
|
|
192
206
|
end
|
|
193
207
|
end
|
|
194
208
|
end
|