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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba738761ab28ad68c0dfb013b6f13c3d17ae587295a0b53f97e7bb091d8d4a65
4
- data.tar.gz: 3b5d2b5955816d248773ce88d48d6f5719d06bb47bf8cba685eaef6cefb2bee2
3
+ metadata.gz: ee19df358e0fe9b9c6020be38aca9500e1053aee94afd929e83b6a041dca82d5
4
+ data.tar.gz: 6d1ccabe1fa7709a941b1f679dad26ec49fa62950ca764d2fb07ffed1fa63c7b
5
5
  SHA512:
6
- metadata.gz: 99e05143e8194b69cdca989616d2f65f36807cc1d408fa4636eb98b688d76c8b5156c6864c6f7a391b601195aef667fb25e3086d2054f082f62f7dba995dd126
7
- data.tar.gz: fd86abdaca844a0ea28f4b5e32041824fabd9dcf0e5870344045c5379ca776aa7b566a07bc1deb1480bd3c3f007146d1287f906abb66cf5c3df701661bf22e17
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
- body[:thinking] =
625
- invoke_model_thinking(model: rest[:model] || model_id(rest[:model]), thinking: thinking)
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 mid.include?('claude-sonnet-4')
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
- { type: 'adaptive' }
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
- model = model_from_request(canonical)
260
- if model.to_s.include?('claude-sonnet-4')
261
- budget = canonical_thinking_budget(canonical)
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)
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Bedrock
7
- VERSION = '0.4.9'
7
+ VERSION = '0.4.10'
8
8
  end
9
9
  end
10
10
  end
@@ -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.9
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