lex-llm-bedrock 0.5.5 → 0.5.8

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/Gemfile +7 -2
  4. data/RULES.md +97 -0
  5. data/lex-llm-bedrock.gemspec +5 -1
  6. data/lib/legion/extensions/llm/bedrock/actors/discovery.rb +24 -0
  7. data/lib/legion/extensions/llm/bedrock/credential_discovery.rb +11 -7
  8. data/lib/legion/extensions/llm/bedrock/{callable.rb → helpers/callable.rb} +72 -20
  9. data/lib/legion/extensions/llm/bedrock/provider/class_methods.rb +0 -4
  10. data/lib/legion/extensions/llm/bedrock/provider/client_helpers.rb +11 -7
  11. data/lib/legion/extensions/llm/bedrock/provider/converse_helpers.rb +204 -248
  12. data/lib/legion/extensions/llm/bedrock/provider/dispatch_helpers.rb +113 -71
  13. data/lib/legion/extensions/llm/bedrock/provider/invoke_model_helpers.rb +131 -198
  14. data/lib/legion/extensions/llm/bedrock/provider/model_catalog_helpers.rb +12 -243
  15. data/lib/legion/extensions/llm/bedrock/provider.rb +8 -1
  16. data/lib/legion/extensions/llm/bedrock/render_defaults.rb +30 -0
  17. data/lib/legion/extensions/llm/bedrock/runners/discovery.rb +368 -0
  18. data/lib/legion/extensions/llm/bedrock/runners/fleet_worker.rb +4 -3
  19. data/lib/legion/extensions/llm/bedrock/thinking_modes.rb +171 -13
  20. data/lib/legion/extensions/llm/bedrock/translator/chunk_parsing.rb +7 -9
  21. data/lib/legion/extensions/llm/bedrock/translator/message_rendering.rb +58 -105
  22. data/lib/legion/extensions/llm/bedrock/translator/read_helpers.rb +21 -12
  23. data/lib/legion/extensions/llm/bedrock/translator/request_rendering.rb +67 -38
  24. data/lib/legion/extensions/llm/bedrock/translator/response_parsing.rb +8 -12
  25. data/lib/legion/extensions/llm/bedrock/translator.rb +31 -6
  26. data/lib/legion/extensions/llm/bedrock/version.rb +1 -1
  27. data/lib/legion/extensions/llm/bedrock.rb +13 -8
  28. metadata +8 -5
  29. data/lib/legion/extensions/llm/bedrock/actors/discovery_refresh.rb +0 -955
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dff754b7817098396287ff77ec64205f5dea845b43eddd9c73ddb742b8b638c
4
- data.tar.gz: 754c16f98e51acfcfaee31bf936741616adb92bacb7042956f278d7ff65f9c66
3
+ metadata.gz: 860a45a8f5e161e2d46d3822a895dc659136ab19e9f5ad0233f690ec9742a8ae
4
+ data.tar.gz: f5dac9cdcf27cecc7c7ed319febc79fc3bc390a92eafb3e40e27e768c4cd514f
5
5
  SHA512:
6
- metadata.gz: 3a674c63e6fd3f99855f89dcbd63319148751ff4dea85596a1cf6024a90d5de015ba35eb8c7bff563143f40608ced83dbda1196794569560c7df79337c545e17
7
- data.tar.gz: 645ed19662a6b3097202b494a4ef941f08b7fdc39d8af8feed201fa13b341697736f0b517294b0980d0bad7009dc81f60e6f3ad95abf2987cc4ed36971c1c064
6
+ metadata.gz: e075da15662be51f02f72286c894ebbc26fa09251f2cb8e62ff09801a5a869947886dbce7e5fa35b6fb9ca08aa2269618af28ff38803bcafc6b53ce579a71b6a
7
+ data.tar.gz: 8bae12f419ea1f3b6bc13233a98a35ef988e0953a6149c55f419ea8a5a0461c0d00a12b8186061773d732c629efaf40c4658f2f1bd8e59a219262d72e645fc38
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.8] - 2026-08-25
4
+
5
+ ### Added
6
+ - **Model-aware adaptive/effort thinking wire** — Claude opus-4-6, opus-4-7, opus-4-8, opus-5, sonnet-4-6, and sonnet-5 on Bedrock now emit `{ type: 'adaptive' }` + `output_config: { effort: <low|medium|high|xhigh|max> }` with the `effort-2025-11-24` beta header, instead of the budgeted `{ type: 'enabled', budget_tokens: N }` shape which these models reject with `ValidationException`. Effort is passed through directly from the canonical `Thinking::Config#resolved_effort` (the full ladder maps 1:1); `none` or nil effort omits `output_config` so the API defaults to high. Both dispatch paths (invoke_model and Converse) emit the correct wire shape based on model classification. Precedence logic ensures `claude-opus-4-7` matches adaptive before `claude-opus-4` matches budgeted.
7
+
8
+ ## [0.5.7] - 2026-08-25
9
+
10
+ ### Fixed
11
+ - **Budget/max_tokens reconciliation** — `ThinkingModes.thinking_wire` now clamps `budget_tokens` so it is strictly less than the effective `max_tokens` sent on the wire (Bedrock constraint: `max_tokens > budget_tokens`). When Codex sends `effort=high` (resolving to `budget_tokens=16384`) but the client's `max_output_tokens` is only 2048, the budget is clamped to `max_tokens - OUTPUT_RESERVE` (floor: 1024) instead of producing an HTTP 400 `ValidationException`. If `max_tokens` is too small to fit even the minimum budget, thinking is omitted to keep the request valid rather than overriding the client's cap.
12
+ - **Removed latent NoMethodError** — Dropped the `|| params&.max_thinking_tokens` fallback in `thinking_wire`; `Canonical::Params#max_thinking_tokens` was deleted in lex-llm 0.8.3 and `&.` does not guard a deleted method on a non-nil receiver. `thinking.resolved_budget` is the sole budget source.
13
+
14
+ ## [0.5.6] - 2026-08-19
15
+
16
+ ### Changed
17
+ - **Canonical dispatch boundary (N x N law)** — The production `BedrockCallable#chat` / `#stream_chat` / `#count_tokens` operations now call `Provider#enforce_canonical_messages!` before dispatch, and the provider's message-conversion seam (`DispatchHelpers#build_provider_messages`) accepts only `Canonical::Message` (pipeline dispatch) or the provider-native `Legion::Extensions::Llm::Message` (Chat facade); anything else raises a loud `ArgumentError`. The lenient message-level hash re-canonicalization that masked the 2026-08-19 hash-bypass defect is removed from the invoke_model render path. Client request formats and the Bedrock wire payload shape are unchanged.
18
+ - **Dependency floor** — Requires `lex-llm >= 0.7.7` for `Provider#enforce_canonical_messages!` (the N x N dispatch boundary). The Gemfile adds a local-tree `lex-llm` path dependency to the test group so the adjacent checkout resolves against 0.7.7 during development.
19
+
20
+ ### Added
21
+ - **Dispatch-boundary regression guards** — The SSOT v3 conformance spec now asserts that plain-Hash messages are rejected loudly at both the callable dispatch boundary and the provider render seam, and the model-policy / streaming specs that previously fed incidental Hash messages now use canonical inputs.
22
+
23
+ ### 0.8.0 conformance (SSOT v4 provider wave, 2026-08-20)
24
+
25
+ #### Changed
26
+ - **Legacy types migrated to Canonical** — Every provider parse/build path now renders FROM `Canonical::Message` / `Canonical::ContentBlock` and parses TO `Canonical::Response` / `Canonical::Chunk` / `Canonical::ToolCall` / `Canonical::Usage` / `Canonical::Thinking`. The deleted legacy `Llm::Message` / `Llm::Chunk` / `Llm::ToolCall` / `Llm::Content::Raw` / `Llm::Content::ImageAttachment` constructions and the `to_provider_message` re-canonicalization bridge are gone; the provider dispatch seam enforces `Canonical::Message` only.
27
+ - **Callable boundary is the 0.8.0 contract** — `BedrockCallable#chat` / `#stream_chat` take messages positionally, matching the base `Provider#chat` signature and the fleet `WorkerExecution` dispatch. The `enforce_canonical_messages!` calls (the one shared lex-llm helper at the exact-execution boundary) and `normalize_dispatch_error(error:)` are kept.
28
+ - **Offering read path (07 C5)** — The legacy `Routing::ModelOffering` production chain in the provider (`discover_offerings` override, `offering_for`, `offering_from_model`, `offering_from_summary`, `build_offering`, `static_offerings` and their filter helpers) is deleted; the base `Provider#discover_offerings` serves the activated inventory offerings from `Registry.snapshot`. The discovery actor's `OfferingDraft` writer path is the sole publication path.
29
+ - **Streaming is canonical end-to-end** — Both streaming paths (Converse events and invoke_model Anthropic events) yield `Canonical::Chunk` objects (`text_delta` / `thinking_delta` / `tool_call_delta`) and the sequence ends in exactly one `done` chunk carrying usage + stop_reason; the accumulated state builds a `Canonical::Response`. Tool-input JSON fragments travel on the `tool_call_delta` chunk and are parsed once at stream end.
30
+ - **Sync parse boundary** — The Converse and invoke_model sync parsers delegate to the gem's canonical `Translator` (one parse boundary); the `content_filtered` / `content_filter` wire stop-reason spellings map to canonical `:content_filter` at that single edge.
31
+ - **Embed artifact (05 §3 / O07)** — `parse_embedding_response` returns the documented Hash artifact `{ text:, model:, embedding:, usage: Canonical::Usage }` (the deleted `Llm::Embedding` type is gone).
32
+
33
+ #### Removed
34
+ - **Legacy coordinator wiring** — The `ScopedRefresher::LegacyCoordinatorAdapter` compatibility adapter (and the `scoped_refresher` require) is removed from the discovery actor's `Publisher` construction; the mixed-version window is over with the lex-llm 0.8.0 cut.
35
+
36
+ #### Added
37
+ - **Conformance kit B1/B2** — The SSOT v3 conformance spec now loads the 0.8.0 boundary kit (`ssot_contract_examples.rb`) and runs the B1 (central canonical enforcement) and B2 (canonical outputs) shared example groups against the real `BedrockCallable` -> `Bedrock::Provider` -> stubbed-AWS-SDK boundary.
38
+ - **RULES.md** — The 0.8.0 architecture law (`references/01-rules-draft.md`, byte-for-byte) ships at the repo root.
39
+
40
+ #### Dependency
41
+ - **Floor bump** — Requires `lex-llm >= 0.8.0` (the contract cut: canonical strictification, legacy rip, provider funnel, fleet v3, conformance kit).
42
+
3
43
  ## [0.5.5] - 2026-08-19
4
44
 
5
45
  ### Added
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
+ # canonical-boundary work resolves against the unreleased 0.7.7.
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.
@@ -29,5 +29,9 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency 'legion-logging', '>= 1.3.2'
30
30
  spec.add_dependency 'legion-settings', '>= 1.4.2'
31
31
  spec.add_dependency 'legion-transport', '>= 1.4.14'
32
- spec.add_dependency 'lex-llm', '>= 0.7.6'
32
+ # 0.8.0 is the contract cut: canonical-only dispatch boundary (the legacy
33
+ # Llm::Message/Chunk/ToolCall/Content types and the ScopedRefresher
34
+ # adapter wiring are deleted), Registry-snapshot offering read path,
35
+ # and the conformance kit this gem runs against.
36
+ spec.add_dependency 'lex-llm', '>= 0.8.0'
33
37
  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 Bedrock
13
+ module Actor
14
+ # Bedrock discovery actor: an EMPTY subclass of the shared base. The
15
+ # timer, dispatch, and runner-resolution convention are inherited —
16
+ # this class redefines nothing. The Bedrock-specific work lives in
17
+ # Bedrock::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
@@ -65,13 +65,10 @@ module Legion
65
65
  settings = CredentialSources.setting(:extensions, :llm, :bedrock)
66
66
  return unless settings.is_a?(Hash) && !settings.empty?
67
67
 
68
- default_config = dedup_config(normalize_instance_config(settings))
69
- unless default_config.empty?
70
- default_config[:source] = CredentialSources.source_tag(:settings, 'extensions.llm.bedrock')
71
- default_config[:credential_fingerprint] = CredentialSources.config_fingerprint(default_config)
72
- candidates[:settings] = default_config.merge(tier: :cloud)
73
- end
74
-
68
+ # B15: operator-named instances are collected before the
69
+ # top-level :settings (source-named) candidate — with first-wins
70
+ # dedup, the operator's instance keeps its name over the
71
+ # source-named fallback carrying the same credentials.
75
72
  settings_instances(settings).each do |name, config|
76
73
  next unless config.is_a?(Hash)
77
74
 
@@ -81,6 +78,13 @@ module Legion
81
78
  normalized[:credential_fingerprint] = CredentialSources.config_fingerprint(normalized)
82
79
  candidates[name.to_sym] = normalized.merge(tier: :cloud)
83
80
  end
81
+
82
+ default_config = dedup_config(normalize_instance_config(settings))
83
+ return if default_config.empty?
84
+
85
+ default_config[:source] = CredentialSources.source_tag(:settings, 'extensions.llm.bedrock')
86
+ default_config[:credential_fingerprint] = CredentialSources.config_fingerprint(default_config)
87
+ candidates[:settings] = default_config.merge(tier: :cloud)
84
88
  end
85
89
 
86
90
  def discover_broker(candidates)
@@ -10,17 +10,33 @@ module Legion
10
10
  module Extensions
11
11
  module Llm
12
12
  module Bedrock
13
- module Actor
13
+ module Helpers
14
14
  # Per-instance dispatch callable for a Bedrock provider instance.
15
15
  #
16
16
  # Wraps a per-instance Bedrock::Provider built from the instance
17
- # config and implements the fleet dispatch operations the coordinator
18
- # invokes (chat, stream_chat, embed, count_tokens) with ** passthrough,
19
- # plus the disconnect / normalize_dispatch_error(error:) contracts
17
+ # config and implements the 0.8.0 fleet dispatch operations the
18
+ # coordinator invokes (chat, stream_chat, embed, count_tokens) plus
19
+ # the disconnect / normalize_dispatch_error(error:) contracts
20
20
  # required by Inventory::CallableHandle and Routing::ProviderOutcome.
21
+ #
22
+ # 0.8.0 callable boundary (WorkerExecution.dispatch_operation):
23
+ # chat/stream_chat take messages POSITIONALLY (base Provider#chat
24
+ # form); count_tokens takes the messages: kwarg. temperature and
25
+ # max_tokens are Canonical::Params members (05 O4), never named
26
+ # completion keys — the folded wire params become a Canonical::Params
27
+ # at this boundary, because the 0.8.0 renderer reads params.temperature
28
+ # / params.max_tokens (a raw Hash would NoMethodError).
21
29
  # Provider and AWS SDK errors propagate unchanged so
22
30
  # normalize_dispatch_error can classify them.
23
- class BedrockCallable
31
+ class Callable
32
+ # Keys the base Provider exposes as named kwargs for the completion
33
+ # operations. Anything else the fleet passes (sampling scalars,
34
+ # `temperature` — a Canonical::Params member, 05 O4) is folded into
35
+ # Canonical::Params at the dispatch boundary.
36
+ COMPLETION_NAMED_KEYS = %i[tools tool_prefs thinking].freeze
37
+ COUNT_TOKENS_NAMED_KEYS = %i[system].freeze
38
+ EMBED_NAMED_KEYS = %i[dimensions].freeze
39
+
24
40
  def initialize(instance_cfg:, logger:)
25
41
  @instance_cfg = instance_cfg
26
42
  @logger = logger
@@ -45,41 +61,58 @@ module Legion
45
61
  @logger.debug { '[bedrock][callable] disconnected' }
46
62
  end
47
63
 
48
- def chat(messages:, model:, temperature: nil, max_tokens: nil, tools: {}, tool_prefs: nil,
49
- thinking: nil, params: {}, **opts)
64
+ def chat(messages, model:, **rest)
50
65
  dispatch! do
51
- provider.chat(messages: messages, model: model, temperature: temperature, max_tokens: max_tokens,
52
- tools: tools, tool_prefs: tool_prefs, thinking: thinking, params: params.merge(opts))
66
+ # Canonical boundary (N x N law): pipeline dispatch delivers
67
+ # Canonical::Message objects only. Hash/legacy shapes are the
68
+ # bypass class — reject loudly, never coerce.
69
+ provider.enforce_canonical_messages!(messages)
70
+ named, params = split_fleet_kwargs(rest, COMPLETION_NAMED_KEYS)
71
+ provider.chat(messages: messages, model: model, params: canonical_params(params), **named)
53
72
  end
54
73
  end
55
74
 
56
- def stream_chat(messages:, model:, temperature: nil, max_tokens: nil, tools: {}, tool_prefs: nil,
57
- thinking: nil, params: {}, **opts, &)
75
+ def stream_chat(messages, model:, **rest, &)
58
76
  dispatch! do
59
- provider.stream(messages: messages, model: model, temperature: temperature, max_tokens: max_tokens,
60
- tools: tools, tool_prefs: tool_prefs, thinking: thinking,
61
- params: params.merge(opts), &)
77
+ provider.enforce_canonical_messages!(messages)
78
+ named, params = split_fleet_kwargs(rest, COMPLETION_NAMED_KEYS)
79
+ provider.stream(messages: messages, model: model, params: canonical_params(params), **named, &)
62
80
  end
63
81
  end
64
82
 
65
- def embed(text:, model:, dimensions: nil, params: {}, **opts)
66
- dispatch! { provider.embed(text: text, model: model, dimensions: dimensions, params: params.merge(opts)) }
83
+ # B19: unowned fleet params are not forwarded — the exact
84
+ # execution binding carries no payload the operation does not own
85
+ # (the old params passthrough reached the InvokeModel body
86
+ # silently). Only the operation's named keys cross the boundary.
87
+ def embed(text:, model:, **rest)
88
+ dispatch! do
89
+ named, _params = split_fleet_kwargs(rest, EMBED_NAMED_KEYS)
90
+ provider.embed(text: text, model: model, **named)
91
+ end
67
92
  end
68
93
 
69
- def count_tokens(messages:, model:, system: nil, params: {}, **opts)
94
+ # The fleet WorkerExecution calls count_tokens with the messages:
95
+ # KWARG (worker_execution.rb) — only chat/stream_chat are positional.
96
+ def count_tokens(messages:, model:, **rest)
70
97
  dispatch! do
71
- provider.count_tokens(messages: messages, model: model, system: system, params: params.merge(opts))
98
+ provider.enforce_canonical_messages!(messages)
99
+ named, _params = split_fleet_kwargs(rest, COUNT_TOKENS_NAMED_KEYS)
100
+ provider.count_tokens(messages: messages, model: model, **named)
72
101
  end
73
102
  end
74
103
 
75
104
  def normalize_dispatch_error(error:)
76
- reason = error.message.to_s[0, 512]
105
+ # B9: the base reason policy (10 §1E) — the bounded exception
106
+ # CLASS NAME; never a response body, credential, endpoint, or
107
+ # exception message (AWS SDK messages embed request context).
108
+ reason = error.class.name
109
+ reason = 'UnknownError' if reason.nil? || reason.empty?
77
110
 
78
111
  kind = classify_error(error: error)
79
112
 
80
113
  Legion::Extensions::Llm::Routing::ProviderOutcome.new(
81
114
  kind: kind,
82
- reason: reason.empty? ? 'unknown dispatch error' : reason
115
+ reason:
83
116
  )
84
117
  end
85
118
 
@@ -91,6 +124,25 @@ module Legion
91
124
  @provider ||= Legion::Extensions::Llm::Bedrock::Provider.new(@instance_cfg)
92
125
  end
93
126
 
127
+ # The 0.8.0 completion funnel receives canonical values only
128
+ # (08 F3): the folded wire params become a Canonical::Params at the
129
+ # dispatch boundary — temperature is a params member (05 O4), never
130
+ # a kwarg. from_hash accepts canonical keys and folds unknowns into
131
+ # the metadata member (04 L5) — nothing is dropped.
132
+ def canonical_params(params)
133
+ Legion::Extensions::Llm::Canonical::Params.from_hash(params)
134
+ end
135
+
136
+ # Split the fleet's **rest into the provider's named completion
137
+ # kwargs and a payload params hash (any passed :params merged with
138
+ # the remaining unknown keys).
139
+ def split_fleet_kwargs(rest, named_keys)
140
+ named = rest.slice(*named_keys)
141
+ extra = rest.reject { |key, _| named.key?(key) }
142
+ params = (extra.delete(:params) || {}).to_h.merge(extra)
143
+ [named, params]
144
+ end
145
+
94
146
  def dispatch!
95
147
  if disconnected?
96
148
  raise Legion::Extensions::Llm::Inventory::Errors::CallableDisposedError,
@@ -30,10 +30,6 @@ module Legion
30
30
  def configuration_requirements = []
31
31
  def capabilities = Capabilities
32
32
 
33
- def registry_publisher
34
- Legion::Extensions::Llm::Bedrock.registry_publisher
35
- end
36
-
37
33
  def resolve_model_id(model_id, **)
38
34
  ALIASES.fetch(model_id.to_s, model_id.to_s)
39
35
  end
@@ -65,11 +65,19 @@ module Legion
65
65
  :aws_sdk_default_chain
66
66
  end
67
67
 
68
- def parse_embedding_response(response, model:)
68
+ # Non-conversation artifact (05 §3, O07): a documented Hash shape,
69
+ # not a canonical type.
70
+ def parse_embedding_response(response, model:, text:)
69
71
  body = parse_body(value(response, :body))
70
72
  vectors = body['embedding'] || body['embeddings'] || body.dig('data', 0, 'embedding')
71
- Legion::Extensions::Llm::Embedding.new(vectors: vectors, model: model,
72
- input_tokens: body['inputTextTokenCount'])
73
+ {
74
+ text:,
75
+ model:,
76
+ embedding: vectors,
77
+ usage: Legion::Extensions::Llm::Canonical::Usage.build(
78
+ input_tokens: body['inputTextTokenCount']
79
+ )
80
+ }
73
81
  end
74
82
 
75
83
  def parse_body(body)
@@ -120,10 +128,6 @@ module Legion
120
128
  rescue NameError
121
129
  nil
122
130
  end
123
-
124
- def safe_event_data(evt)
125
- evt.respond_to?(:to_h) ? evt.to_h : evt.inspect[0, 500]
126
- end
127
131
  end
128
132
  end
129
133
  end