lex-llm-bedrock 0.4.9 → 0.4.10
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 +6 -0
- data/lib/legion/extensions/llm/bedrock/provider.rb +34 -14
- data/lib/legion/extensions/llm/bedrock/thinking_modes.rb +65 -0
- data/lib/legion/extensions/llm/bedrock/translator.rb +10 -8
- data/lib/legion/extensions/llm/bedrock/version.rb +1 -1
- data/lib/legion/extensions/llm/bedrock.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee19df358e0fe9b9c6020be38aca9500e1053aee94afd929e83b6a041dca82d5
|
|
4
|
+
data.tar.gz: 6d1ccabe1fa7709a941b1f679dad26ec49fa62950ca764d2fb07ffed1fa63c7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9849e1efcf3a3d35ce2c9ea5c4b2fa61c5232a361b0a33c29f3deb6d98c1aad66672ffccafbebfdd94b4c353218befc12f39f0f13eba2b7429a0ab000d6ea4ab
|
|
7
|
+
data.tar.gz: db563c25558e547ac90bacc8d8dd4d0eb8628d8e3135063b32eaff6c48eddbcb128967cda735b4204eee6946b91d912852268f16167e261d8dfcbbfbcedd0f6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.10] - 2026-08-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Claude models now advertise the `:thinking` capability.** Discovery passed `provider_catalog: {}` to `CapabilityPolicy.resolve`, so per-model capabilities from the shared lex-llm catalog (which correctly tags Claude 3.7 / 4+ models `reasoning` → `:thinking`) were ignored — every Bedrock Claude model reported no thinking capability, so the router's thinking filter could not route thinking requests correctly. `offering_from_model` now consults the shared catalog via `catalog_capabilities`.
|
|
7
|
+
- **Thinking payload no longer sends unsupported `adaptive` mode.** `invoke_model_thinking` / `build_invoke_thinking` (and the converse-path `bedrock_additional_fields` / `build_additional_fields`) previously emitted `{type: 'adaptive'}` for every non-`claude-sonnet-4` model, which Bedrock rejects with `ValidationException: adaptive thinking is not supported on this model` (HTTP 500, e.g. claude-opus-4-5). A new `ThinkingModes` module is the single source of truth shared by provider and translator: budgeted-thinking models emit `{type: 'enabled', budget_tokens: N}`; known non-thinking models omit thinking entirely. `adaptive` is never emitted.
|
|
8
|
+
|
|
3
9
|
## [0.4.9] - 2026-06-20
|
|
4
10
|
|
|
5
11
|
### Fixed
|
|
@@ -6,6 +6,7 @@ require 'aws-sdk-bedrockruntime'
|
|
|
6
6
|
require 'legion/json'
|
|
7
7
|
require 'legion/logging/helper'
|
|
8
8
|
require 'legion/extensions/llm'
|
|
9
|
+
require_relative 'thinking_modes'
|
|
9
10
|
|
|
10
11
|
module Legion
|
|
11
12
|
module Extensions
|
|
@@ -621,8 +622,8 @@ module Legion
|
|
|
621
622
|
body[:tool_choice] = tool_format[:tool_choice] if tool_format[:tool_choice]
|
|
622
623
|
end
|
|
623
624
|
if thinking
|
|
624
|
-
|
|
625
|
-
|
|
625
|
+
thinking_cfg = invoke_model_thinking(model: rest[:model] || model_id(rest[:model]), thinking: thinking)
|
|
626
|
+
body[:thinking] = thinking_cfg if thinking_cfg
|
|
626
627
|
end
|
|
627
628
|
body
|
|
628
629
|
end
|
|
@@ -643,17 +644,19 @@ module Legion
|
|
|
643
644
|
parts.map { |t| { type: 'text', text: t } }
|
|
644
645
|
end
|
|
645
646
|
|
|
647
|
+
# Emit the thinking wire shape the model actually supports.
|
|
648
|
+
# Budgeted-thinking Claude models get { type: 'enabled', budget_tokens: N }.
|
|
649
|
+
# Every other model returns nil so the caller OMITS the thinking field —
|
|
650
|
+
# Bedrock rejects { type: 'adaptive' } with a ValidationException (HTTP 500).
|
|
646
651
|
def invoke_model_thinking(model:, thinking:)
|
|
647
652
|
mid = model_id(model)
|
|
648
|
-
if
|
|
649
|
-
budget = if thinking.is_a?(Hash)
|
|
650
|
-
thinking[:budget_tokens] || thinking['budget_tokens'] ||
|
|
651
|
-
thinking[:budget] || thinking['budget']
|
|
652
|
-
end
|
|
653
|
-
return { type: 'enabled', budget_tokens: budget }.compact
|
|
654
|
-
end
|
|
653
|
+
return nil if ThinkingModes.known_non_thinking?(mid)
|
|
655
654
|
|
|
656
|
-
|
|
655
|
+
budget = if thinking.is_a?(Hash)
|
|
656
|
+
thinking[:budget_tokens] || thinking['budget_tokens'] ||
|
|
657
|
+
thinking[:budget] || thinking['budget']
|
|
658
|
+
end
|
|
659
|
+
{ type: 'enabled', budget_tokens: budget }.compact
|
|
657
660
|
end
|
|
658
661
|
|
|
659
662
|
def format_invoke_model_messages(messages)
|
|
@@ -919,7 +922,7 @@ module Legion
|
|
|
919
922
|
metadata = model_info.respond_to?(:metadata) && model_info.metadata.is_a?(Hash) ? model_info.metadata : {}
|
|
920
923
|
policy = Legion::Extensions::Llm::CapabilityPolicy.resolve(
|
|
921
924
|
real: real,
|
|
922
|
-
provider_catalog:
|
|
925
|
+
provider_catalog: catalog_capabilities(model),
|
|
923
926
|
probe: {},
|
|
924
927
|
provider_envelope: provider_envelope_capabilities,
|
|
925
928
|
provider_config: provider_capability_config,
|
|
@@ -1001,13 +1004,13 @@ module Legion
|
|
|
1001
1004
|
inference_config: { temperature: temperature, max_tokens: max_tokens || model_max_tokens(model) }.compact,
|
|
1002
1005
|
tool_config: format_tool_config(tools, tool_prefs),
|
|
1003
1006
|
guardrail_config: guardrail_config,
|
|
1004
|
-
additional_model_request_fields: bedrock_additional_fields(thinking)
|
|
1007
|
+
additional_model_request_fields: bedrock_additional_fields(thinking, model: model_id(model))
|
|
1005
1008
|
}.compact
|
|
1006
1009
|
end
|
|
1007
1010
|
|
|
1008
|
-
def bedrock_additional_fields(thinking)
|
|
1011
|
+
def bedrock_additional_fields(thinking, model:)
|
|
1009
1012
|
fields = {}
|
|
1010
|
-
if thinking
|
|
1013
|
+
if thinking && !ThinkingModes.known_non_thinking?(model)
|
|
1011
1014
|
fields[:thinking] = {
|
|
1012
1015
|
type: 'enabled',
|
|
1013
1016
|
budget_tokens: if thinking.is_a?(Hash)
|
|
@@ -1607,6 +1610,23 @@ module Legion
|
|
|
1607
1610
|
{ tools: true }
|
|
1608
1611
|
end
|
|
1609
1612
|
|
|
1613
|
+
# Capability truth from the shared lex-llm catalog (models.json), keyed by
|
|
1614
|
+
# bedrock model id. This is the source of truth for per-model capabilities
|
|
1615
|
+
# (e.g. :thinking for Claude 4+) that the live AWS ListFoundationModels
|
|
1616
|
+
# summary does not expose. Surfaced to CapabilityPolicy as :provider_catalog
|
|
1617
|
+
# so operator/instance/model overrides still win, and the boolean map is fed
|
|
1618
|
+
# through the shared Capabilities alias table (reasoning -> :thinking).
|
|
1619
|
+
def catalog_capabilities(model)
|
|
1620
|
+
info = Legion::Extensions::Llm::Models.find(model.to_s, :bedrock)
|
|
1621
|
+
caps = Legion::Extensions::Llm::Capabilities.normalize(info.capabilities)
|
|
1622
|
+
caps.to_h { |cap| [cap, true] }
|
|
1623
|
+
rescue Legion::Extensions::Llm::ModelNotFoundError
|
|
1624
|
+
{}
|
|
1625
|
+
rescue StandardError => e
|
|
1626
|
+
handle_exception(e, level: :warn, handled: true, operation: 'bedrock.provider.catalog_capabilities')
|
|
1627
|
+
{}
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1610
1630
|
def model_family_for(model)
|
|
1611
1631
|
normalize_provider(model.to_s.split('.').first)
|
|
1612
1632
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Llm
|
|
6
|
+
module Bedrock
|
|
7
|
+
# Single source of truth for how each Bedrock model expresses extended
|
|
8
|
+
# thinking on the wire. Shared by both the Provider (invoke_model /
|
|
9
|
+
# converse paths) and the Translator (canonical render path) so the two
|
|
10
|
+
# never diverge.
|
|
11
|
+
#
|
|
12
|
+
# Bedrock supports exactly one thinking wire shape for Anthropic Claude:
|
|
13
|
+
# { type: 'enabled', budget_tokens: N } (native Anthropic Messages API)
|
|
14
|
+
#
|
|
15
|
+
# There is NO Bedrock Claude model that accepts { type: 'adaptive' } —
|
|
16
|
+
# sending adaptive raises `ValidationException: adaptive thinking is not
|
|
17
|
+
# supported on this model` (observed live on opus-4-5) and surfaces as an
|
|
18
|
+
# HTTP 500. So a model either supports budgeted thinking (emit `enabled`)
|
|
19
|
+
# or it does not (OMIT the thinking field entirely — never `adaptive`).
|
|
20
|
+
#
|
|
21
|
+
# Budgeted extended thinking arrived with Claude 3.7 Sonnet and is
|
|
22
|
+
# supported across the entire Claude 4 family (sonnet-4, opus-4.x,
|
|
23
|
+
# haiku-4.5). The match is a substring so it tolerates the many Bedrock
|
|
24
|
+
# model-id decorations (geo prefixes `us.`/`eu.`/`ap.`, `anthropic.`
|
|
25
|
+
# provider prefix, `-vN:0` version suffixes, `:200k` context suffixes).
|
|
26
|
+
module ThinkingModes
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Model-id fragments for Claude families that support explicit budgeted
|
|
30
|
+
# extended thinking via { type: 'enabled', budget_tokens: N }.
|
|
31
|
+
BUDGETED_THINKING_FRAGMENTS = %w[
|
|
32
|
+
claude-3-7-sonnet
|
|
33
|
+
claude-sonnet-4
|
|
34
|
+
claude-opus-4
|
|
35
|
+
claude-haiku-4
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] true when the model supports { type: 'enabled', budget_tokens: N }
|
|
39
|
+
def budgeted_thinking?(model_id)
|
|
40
|
+
return false if model_id.nil? || model_id.to_s.strip.empty?
|
|
41
|
+
|
|
42
|
+
mid = model_id.to_s
|
|
43
|
+
# Substring scan (String#include?), NOT array intersection: intersect?
|
|
44
|
+
# raises TypeError on a String argument.
|
|
45
|
+
BUDGETED_THINKING_FRAGMENTS.any? { |fragment| mid.include?(fragment) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true when the model is KNOWN not to support thinking.
|
|
49
|
+
# Distinct from `!budgeted_thinking?`: a nil/blank/unknown model id is
|
|
50
|
+
# NOT known-unsupported — we then honor an explicit thinking request and
|
|
51
|
+
# emit the (safe) `enabled` shape rather than dropping it. The router's
|
|
52
|
+
# capability filter (fed by the shared catalog) is the real guard that
|
|
53
|
+
# keeps thinking requests off non-thinking models; this method only
|
|
54
|
+
# strips thinking for a positively-identified non-thinking Claude model
|
|
55
|
+
# so we never emit an unsupported shape and 500.
|
|
56
|
+
def known_non_thinking?(model_id)
|
|
57
|
+
return false if model_id.nil? || model_id.to_s.strip.empty?
|
|
58
|
+
|
|
59
|
+
!budgeted_thinking?(model_id)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'legion/json'
|
|
4
4
|
require 'legion/logging/helper'
|
|
5
5
|
require 'legion/extensions/llm/canonical'
|
|
6
|
+
require_relative 'thinking_modes'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module Extensions
|
|
@@ -178,6 +179,7 @@ module Legion
|
|
|
178
179
|
|
|
179
180
|
def build_additional_fields(canonical)
|
|
180
181
|
return nil unless canonical.thinking
|
|
182
|
+
return nil if ThinkingModes.known_non_thinking?(model_from_request(canonical))
|
|
181
183
|
|
|
182
184
|
budget = canonical_thinking_budget(canonical)
|
|
183
185
|
budget ||= DEFAULT_MAX_TOKENS / 4
|
|
@@ -253,17 +255,17 @@ module Legion
|
|
|
253
255
|
body.compact
|
|
254
256
|
end
|
|
255
257
|
|
|
258
|
+
# Emit the thinking wire shape the model supports. Budgeted-thinking
|
|
259
|
+
# Claude models get { type: 'enabled', budget_tokens: N }; every other
|
|
260
|
+
# model returns nil so render_invoke_model OMITS the thinking field.
|
|
261
|
+
# Bedrock rejects { type: 'adaptive' } (ValidationException -> HTTP 500).
|
|
256
262
|
def build_invoke_thinking(canonical)
|
|
257
263
|
return nil unless canonical.thinking
|
|
264
|
+
return nil if ThinkingModes.known_non_thinking?(model_from_request(canonical))
|
|
258
265
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
budget ||= DEFAULT_MAX_TOKENS / 4
|
|
263
|
-
return { type: 'enabled', budget_tokens: budget }
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
{ type: 'adaptive' }
|
|
266
|
+
budget = canonical_thinking_budget(canonical)
|
|
267
|
+
budget ||= DEFAULT_MAX_TOKENS / 4
|
|
268
|
+
{ type: 'enabled', budget_tokens: budget }
|
|
267
269
|
end
|
|
268
270
|
|
|
269
271
|
def render_invoke_system(canonical)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/llm'
|
|
4
|
+
require 'legion/extensions/llm/bedrock/thinking_modes'
|
|
4
5
|
require 'legion/extensions/llm/bedrock/provider'
|
|
5
6
|
require 'legion/extensions/llm/bedrock/translator'
|
|
6
7
|
require 'legion/extensions/llm/bedrock/version'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-bedrock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -129,6 +129,7 @@ files:
|
|
|
129
129
|
- lib/legion/extensions/llm/bedrock/actors/fleet_worker.rb
|
|
130
130
|
- lib/legion/extensions/llm/bedrock/provider.rb
|
|
131
131
|
- lib/legion/extensions/llm/bedrock/runners/fleet_worker.rb
|
|
132
|
+
- lib/legion/extensions/llm/bedrock/thinking_modes.rb
|
|
132
133
|
- lib/legion/extensions/llm/bedrock/translator.rb
|
|
133
134
|
- lib/legion/extensions/llm/bedrock/version.rb
|
|
134
135
|
homepage: https://github.com/LegionIO/lex-llm-bedrock
|