lex-llm-anthropic 0.3.5 → 0.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37a7bf28c47d62711e925d75c0717bf45c952d4b899a5c84794f7544a395ad21
4
- data.tar.gz: caad53642cf6b4123b41a30a0941dae91c587b7c79e89f16c49d737fb76aae13
3
+ metadata.gz: 2ee85fd86d41029ba981f01d33cce9ad7f390f65b61cdd02b2fc15193b718d11
4
+ data.tar.gz: eeec4dfe3bb5d4f0815de6fefc733fa36e7cefed795d2c78462cf8283f44fa0c
5
5
  SHA512:
6
- metadata.gz: 217cbbf2136c44b3a362c78a8d787c646ac6a93746a3bd967a279301023cc5050e39fef902c38e624c1f7080806785784c1df93432c469c96221dbfb059c66b9
7
- data.tar.gz: d6677021c2251c507705c6ada10d5378f54d97e20ea7176b0b00099ca62269063d593fce722c3d051dae53294eca1a8797769b7bf4edb18fb519fea485ef8b5b
6
+ metadata.gz: d7b39fcd77411152616451c234f1994a1ddf42f6278d22ff3fbeb34580fb215130e9035a72944dc5e566fbe3a158f58a6cfaddde3bb6ac81ceba91b6e58508a3
7
+ data.tar.gz: 93f22be2a6a885ead678affd737ca6c0eae9582f341b5eaf278b876fea6bd9f145281a1892f5c5f8139a9bbe612d4609b77ea29eb70eca9c54ee89be9f39ba12
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.7] - 2026-08-25
4
+
5
+ ### Fixed
6
+ - **Thinking budget reconciliation (SSOT bridge)** — `render_thinking_config` now uses `Canonical::Thinking::Config#resolved_budget` as the single source for the Anthropic wire `budget_tokens`, correctly deriving budget from effort-only clients (e.g. OpenAI effort → Anthropic budget). Removed references to deleted `Canonical::Params#max_thinking_tokens` (NoMethodError on lex-llm 0.8.x) and removed the fabricated `default_thinking_budget` config key fallback.
7
+ - **Budget/max_tokens clamp** — The Anthropic API 400s when `budget_tokens >= max_tokens`. The translator now clamps budget to `max_tokens - OUTPUT_RESERVE` when the resolved budget would violate this constraint, with a floor at `MINIMUM_BUDGET_TOKENS` (1024).
8
+ - **`thinking_enabled?` tightened** — Only gates on `Canonical::Thinking::Config#enabled?`; the Hash truthy branch (dead code since tc is always a Config) is removed.
9
+ - **Stale spec migration** — Removed `max_thinking_tokens: nil` from `Params.new` calls in translator specs (member deleted in lex-llm 0.8.x).
10
+
11
+ ### Added
12
+ - `translator_thinking_spec.rb` — effort-only → budget derivation, budget >= max_tokens clamping (including floor at 1024), disabled/absent thinking suppression, and source-level contract assertions (no `max_thinking_tokens` or `default_thinking_budget` references).
13
+
14
+ ## [0.3.6] - 2026-08-20
15
+
16
+ ### Changed
17
+ - **lex-llm 0.8.0 conformance (complete contract cut)** — The provider is migrated off the deleted legacy type set to Canonical end-to-end. The sync parse returns the translator's `Canonical::Response` directly (the `to_legacy_message` / `to_legacy_chunk` re-canonicalizing bridges are ripped); the streaming parse yields `Canonical::Chunk` objects (asserted by type, kit B2); the render seam renders the Anthropic wire payload FROM canonical values only — `Canonical::Message` content (`String` / `ContentBlock` / `Array<ContentBlock>`, thinking as a content block with the signature in block metadata, images as `:image` blocks), `Canonical::Params` (temperature lives only there — the `temperature:` kwarg is gone from the render boundary per 05 O4), and `Canonical::Thinking::Config` (`enabled?` + `resolved_budget`, no fabricated default). The provider-side `build_canonical_messages` re-implementation is ripped — central enforcement is the base funnel's job (08 F2). The fleet callable stays canonical-only: `chat` / `stream_chat` call `Provider#enforce_canonical_messages!` (the one shared helper at the exact-execution boundary) and take messages as the 0.8.0 funnel positional; the fleet wire's Hash `params` / `thinking` are normalized to `Canonical::Params` / `Canonical::Thinking::Config` at the boundary, and in-process canonical values pass through — the render path sees canonical only.
18
+ - **Legacy coordinator wiring removed** — The `LegacyCoordinatorAdapter` compatibility adapter and the `scoped_refresher` require are gone from the discovery actor (the file is deleted in lex-llm 0.8.0); the `Inventory::Publisher` is constructed with the provider family only.
19
+ - **discover_offerings served from the Registry snapshot** — The legacy `offering_from_model` read path (live model listing + filter helpers) is removed with the base; the discovery actor remains the sole publication path (07 C5 / 08 D3).
20
+ - **Streaming tool-call fragments** — `content_block_start` and `input_json_delta` events emit the canonical delta-fragment Hash (`id` / `name` / `arguments` / `index`) the shared `StreamAccumulator` correlates by wire index; `message_start` carries the wire model in chunk metadata (the accumulator's `metadata[:model]` read), so the response model is wire-reported.
21
+ - **lex-llm floor raised to 0.8.0** — Requires the complete 0.8.0 contract cut. A local-tree `lex-llm` path dependency in the test group resolves the adjacent checkout during development.
22
+
23
+ ### Added
24
+ - **Boundary regression coverage (kit B1/B2)** — The dispatch-boundary conformance block asserts the loud reject through the real callable: plain-Hash input and the deleted legacy Message shape (replayed as a plain object with the old interface) raise `ArgumentError` on `chat` and `stream_chat`; `Canonical::Message` input renders through the full dispatch path and returns a `Canonical::Response` asserted by type.
25
+
3
26
  ## [0.3.5] - 2026-08-19
4
27
 
5
28
  ### Changed
data/Gemfile CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'lex-llm'
6
-
7
5
  gemspec
8
6
 
7
+ # Local-tree resolution: use the adjacent lex-llm checkout when present so the
8
+ # 0.8.0 conformance work resolves against the local contract cut.
9
+ group :test do
10
+ lex_llm_path = File.expand_path('../lex-llm', __dir__)
11
+ gem 'lex-llm', path: lex_llm_path if Dir.exist?(lex_llm_path)
12
+ end
13
+
9
14
  group :development do
10
15
  gem 'bundler', '>= 2.0'
11
16
  gem 'rake', '>= 13.0'
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.
@@ -27,5 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency 'legion-logging', '>= 1.3.2'
28
28
  spec.add_dependency 'legion-settings', '>= 1.4.2'
29
29
  spec.add_dependency 'legion-transport', '>= 1.4.14'
30
- spec.add_dependency 'lex-llm', '>= 0.7.6'
30
+ # 0.8.0 is the complete contract cut: Canonical is the only internal language,
31
+ # the legacy type set and the LegacyCoordinatorAdapter wiring are deleted, and
32
+ # the provider funnel enforces canonical messages centrally.
33
+ spec.add_dependency 'lex-llm', '>= 0.8.0'
31
34
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/llm/discovery/actor'
4
+
5
+ # The base discovery actor only exists inside the daemon (it inherits the
6
+ # LegionIO time-based Every actor). In a standalone load, define nothing.
7
+ return unless defined?(Legion::Extensions::Llm::Discovery::Actor)
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Llm
12
+ module Anthropic
13
+ module Actor
14
+ # Anthropic discovery actor: an EMPTY subclass of the shared base. The
15
+ # timer, dispatch, and runner-resolution convention are inherited — this
16
+ # class redefines nothing. The Anthropic-specific work lives in
17
+ # Anthropic::Runners::Discovery, resolved by the base from this
18
+ # namespace.
19
+ class Discovery < Legion::Extensions::Llm::Discovery::Actor; end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ require 'legion/extensions/llm/routing/provider_outcome'
6
+ require 'legion/extensions/llm/canonical'
7
+ require 'legion/extensions/llm/anthropic/provider'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Llm
12
+ module Anthropic
13
+ module Helpers
14
+ # Callable wrapper for an Anthropic provider instance. Implements the
15
+ # fleet dispatch operations (chat, stream_chat, embed, count_tokens) by
16
+ # delegating to a per-instance Anthropic::Provider, plus the
17
+ # `disconnect` and `normalize_dispatch_error(error:)` contracts required
18
+ # by Inventory::CallableHandle and Routing::ProviderOutcome.
19
+ #
20
+ # Dispatch errors propagate unmodified so the coordinator can classify
21
+ # them through normalize_dispatch_error.
22
+ #
23
+ # CRITICAL: Anthropic 529 overloaded_error is ALWAYS :overloaded.
24
+ # It is NEVER :instance_unavailable. Only explicit connection failures
25
+ # at the transport layer map to :connection_failure (which the
26
+ # discovery harness may escalate to :instance_unavailable).
27
+ class Callable
28
+ # Provider dispatch kwargs the 0.8.0 base funnel accepts by name.
29
+ # temperature is not one of them (05 O4) — it lives only in
30
+ # Canonical::Params. Any other fleet param is merged into the
31
+ # provider `params:` passthrough as canonical Params.
32
+ CHAT_PROVIDER_KEYS = %i[tools params headers schema thinking tool_prefs].freeze
33
+ EMBED_PROVIDER_KEYS = %i[dimensions params headers].freeze
34
+
35
+ def initialize(instance_cfg:, logger:)
36
+ @instance_cfg = instance_cfg
37
+ @logger = logger
38
+ @provider_mutex = Mutex.new
39
+ @disconnected = false
40
+ end
41
+
42
+ # The wrapped per-instance provider (built lazily on first dispatch).
43
+ def provider
44
+ @provider_mutex.synchronize do
45
+ @provider ||= build_provider
46
+ end
47
+ end
48
+
49
+ def disconnected?
50
+ @disconnected
51
+ end
52
+
53
+ def chat(messages, model:, **rest)
54
+ # Canonical boundary (N x N law): fleet dispatch delivers
55
+ # Canonical::Message objects only. Native/Hash shapes are the
56
+ # bypass class (the 2026-08-19 incident) — reject loudly, never coerce.
57
+ provider.enforce_canonical_messages!(messages)
58
+ provider.chat(messages, model:,
59
+ **dispatch_kwargs(rest, known: CHAT_PROVIDER_KEYS))
60
+ end
61
+
62
+ def stream_chat(messages, model:, **rest, &)
63
+ provider.enforce_canonical_messages!(messages)
64
+ provider.stream_chat(messages, model:,
65
+ **dispatch_kwargs(rest, known: CHAT_PROVIDER_KEYS), &)
66
+ end
67
+
68
+ def embed(text:, model:, **rest)
69
+ provider.embed(text: text, model:,
70
+ **dispatch_kwargs(rest, known: EMBED_PROVIDER_KEYS))
71
+ end
72
+
73
+ def count_tokens(messages:, model:, **rest)
74
+ provider.count_tokens(messages: messages, model:, params: rest)
75
+ end
76
+
77
+ def disconnect
78
+ @provider_mutex.synchronize do
79
+ @disconnected = true
80
+ @provider&.disconnect
81
+ @provider = nil
82
+ end
83
+ @logger.debug { '[anthropic][callable] disconnected' }
84
+ end
85
+
86
+ def normalize_dispatch_error(error:)
87
+ reason = error.message.to_s[0, 512]
88
+
89
+ kind = case error
90
+ when Faraday::ConnectionFailed
91
+ :connection_failure
92
+ when Faraday::TimeoutError
93
+ :timeout
94
+ when Faraday::ClientError
95
+ classify_client_error(error: error)
96
+ when Faraday::ServerError
97
+ classify_server_error(error: error)
98
+ when Legion::Extensions::Llm::OverloadedError
99
+ :overloaded
100
+ else
101
+ :provider_error
102
+ end
103
+
104
+ Legion::Extensions::Llm::Routing::ProviderOutcome.new(
105
+ kind: kind,
106
+ reason: reason.empty? ? 'unknown dispatch error' : reason
107
+ )
108
+ end
109
+
110
+ private
111
+
112
+ def build_provider
113
+ Legion::Extensions::Llm::Anthropic::Provider.new(@instance_cfg)
114
+ end
115
+
116
+ # The 0.8.0 completion funnel receives canonical values only
117
+ # (08 F3): the folded wire params become a Canonical::Params at
118
+ # the dispatch boundary — temperature is a params member (05 O4),
119
+ # never a kwarg. In-process dispatch already passes canonical
120
+ # values; they converge here.
121
+ def dispatch_kwargs(rest, known:)
122
+ known_part = rest.slice(*known)
123
+ extra = rest.except(*known)
124
+ known_part[:params] = canonical_params(known_part[:params], extra)
125
+ known_part
126
+ end
127
+
128
+ def canonical_params(params, extra)
129
+ base = case params
130
+ when Legion::Extensions::Llm::Canonical::Params then params.to_h
131
+ when Hash then params.transform_keys(&:to_sym)
132
+ else {}
133
+ end
134
+ base = base.merge(extra.transform_keys(&:to_sym)) unless extra.empty?
135
+ return nil if base.empty?
136
+
137
+ Legion::Extensions::Llm::Canonical::Params.from_hash(base)
138
+ end
139
+
140
+ def classify_client_error(error:)
141
+ status = error.respond_to?(:response_status) ? error.response_status : nil
142
+ case status
143
+ when 401 then :authentication
144
+ when 403 then :authorization
145
+ when 404 then :model_missing
146
+ when 429 then :rate_limited
147
+ else :invalid_request
148
+ end
149
+ end
150
+
151
+ def classify_server_error(error:)
152
+ # 529 is Anthropic's overloaded_error status — ALWAYS :overloaded, NEVER :instance_unavailable.
153
+ # 503 from Anthropic also indicates transient overload, not instance loss.
154
+ # Only explicit transport-layer connection failures (classified above as
155
+ # Faraday::ConnectionFailed) can escalate to :instance_unavailable at the harness layer.
156
+ status = error.respond_to?(:response_status) ? error.response_status : nil
157
+ case status
158
+ when 503, 529 then :overloaded
159
+ else :provider_error
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end