lex-llm-bedrock 0.4.0 → 0.4.4
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 +22 -0
- data/Gemfile +0 -9
- data/lex-llm-bedrock.gemspec +1 -1
- data/lib/legion/extensions/llm/bedrock/actors/discovery_refresh.rb +7 -0
- data/lib/legion/extensions/llm/bedrock/provider.rb +143 -18
- data/lib/legion/extensions/llm/bedrock/translator.rb +21 -2
- data/lib/legion/extensions/llm/bedrock/version.rb +1 -1
- data/lib/legion/extensions/llm/bedrock.rb +18 -8
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3aa9de1a3f3c07848fe253d6f8627c54b03bbc74bb96ab5032a407b109aad13a
|
|
4
|
+
data.tar.gz: a3f680546d15739bdfb84f79daa70a45beae2650893aead380e23b0e9086b38e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b6ade385af00bbcd329278658f4fe829b312735f0141514353e4da0912d1002a9213cbd6f7411f4ba8dcad9dc9284adefe3518c17ff12297dd18d3f9db36d92
|
|
7
|
+
data.tar.gz: 0ffcee037a6efb318802b3ec1ad1684f0516ee133799dc420bd7a74883320c0c2029fc0df916757c5c4584386d4cc2f7cc5f5f8e7bfca46a8def152adfdb9e1f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.4 - 2026-06-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Model policy enforced at dispatch (compliance)** — Bedrock overrides the base dispatch methods (`chat`, `stream`, `embed`), so the base `enforce_model_allowed!` guard did not apply. Each override now calls `enforce_model_allowed!(model_id(model))`, raising `ModelNotAllowedError` before any Bedrock API call when the model is excluded by `model_whitelist`/`model_blacklist`. Fail-closed, no exceptions.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- **Policy-aware default model** — the `anthropic.claude-sonnet-4` default is no longer a hardcoded literal forced via `||=`; it is a named `DEFAULT_MODEL` constant resolved through `Provider.policy_safe_default_model`, so a configured whitelist/blacklist is never overridden by the fallback. Requires lex-llm >= 0.5.4.
|
|
10
|
+
|
|
11
|
+
## 0.4.3 - 2026-06-16
|
|
12
|
+
|
|
13
|
+
- Dependency updates and code quality improvements.
|
|
14
|
+
|
|
15
|
+
## 0.4.2 - 2026-06-15
|
|
16
|
+
|
|
17
|
+
- **CapabilityPolicy integration** — AWS model summaries used as `:model_metadata`; Converse tool use from `:provider_envelope`. Settings overrides at provider/instance/model level supported.
|
|
18
|
+
|
|
19
|
+
## 0.4.1 - 2026-06-13
|
|
20
|
+
|
|
21
|
+
- **Gemfile cleanup** — Remove local path overrides; dependencies resolve from gemspec via rubygems.
|
|
22
|
+
- **RuboCop fixes** — Auto-corrected 6 offenses (style/layout).
|
|
23
|
+
- 199 examples, 0 failures; 17 files, 0 rubocop offenses.
|
|
24
|
+
|
|
3
25
|
## 0.4.0 - 2026-06-10
|
|
4
26
|
|
|
5
27
|
### Added
|
data/Gemfile
CHANGED
|
@@ -4,15 +4,6 @@ source 'https://rubygems.org'
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
group :test do
|
|
8
|
-
transport_path = ENV.fetch('LEGION_TRANSPORT_PATH', File.expand_path('../../legion-transport', __dir__))
|
|
9
|
-
gem 'legion-transport', path: transport_path if File.directory?(transport_path)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# lex-llm (>= 0.5.0) comes from gemspec with canonical types + conformance kit.
|
|
13
|
-
# Override with a path/branch reference for local development only:
|
|
14
|
-
# gem 'lex-llm', path: ENV.fetch('LEX_LLM_PATH', '../lex-llm')
|
|
15
|
-
|
|
16
7
|
group :development do
|
|
17
8
|
gem 'bundler', '>= 2.0'
|
|
18
9
|
gem 'rake', '>= 13.0'
|
data/lex-llm-bedrock.gemspec
CHANGED
|
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
30
30
|
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
31
31
|
spec.add_dependency 'legion-transport', '>= 1.4.14'
|
|
32
|
-
spec.add_dependency 'lex-llm', '>= 0.5.
|
|
32
|
+
spec.add_dependency 'lex-llm', '>= 0.5.4'
|
|
33
33
|
end
|
|
@@ -36,6 +36,13 @@ module Legion
|
|
|
36
36
|
return unless defined?(Legion::LLM::Discovery)
|
|
37
37
|
|
|
38
38
|
Legion::LLM::Discovery.refresh_discovered_models!(provider: :bedrock)
|
|
39
|
+
|
|
40
|
+
if defined?(Legion::LLM::Router) && Legion::LLM::Router.respond_to?(:populate_auto_rules)
|
|
41
|
+
Legion::LLM::Router.populate_auto_rules(Legion::LLM::Discovery.discovered_instances)
|
|
42
|
+
end
|
|
43
|
+
if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
|
|
44
|
+
Legion::LLM::Inventory.invalidate_offerings_cache!
|
|
45
|
+
end
|
|
39
46
|
rescue StandardError => e
|
|
40
47
|
handle_exception(e, level: :warn, handled: true, operation: 'bedrock.actor.discovery_refresh')
|
|
41
48
|
end
|
|
@@ -11,6 +11,8 @@ module Legion
|
|
|
11
11
|
module Extensions
|
|
12
12
|
module Llm
|
|
13
13
|
module Bedrock
|
|
14
|
+
class StaticCredentialsBlockedError < Legion::Extensions::Llm::ConfigurationError; end
|
|
15
|
+
|
|
14
16
|
# Amazon Bedrock provider implementation for the Legion::Extensions::Llm contract.
|
|
15
17
|
class Provider < Legion::Extensions::Llm::Provider # rubocop:disable Metrics/ClassLength
|
|
16
18
|
include Legion::Logging::Helper
|
|
@@ -18,6 +20,10 @@ module Legion
|
|
|
18
20
|
STATIC_MODELS = [
|
|
19
21
|
{ model: 'anthropic.claude-3-haiku-20240307-v1:0', alias: 'claude-3-haiku' },
|
|
20
22
|
{ model: 'anthropic.claude-sonnet-4-20250514-v1:0', alias: 'anthropic.claude-sonnet-4' },
|
|
23
|
+
{ model: 'anthropic.claude-sonnet-4-20250514-v1:0', alias: 'claude-sonnet-4-6' },
|
|
24
|
+
{ model: 'anthropic.claude-sonnet-4-20250514-v1:0', alias: 'claude-sonnet-4-5-20241022' },
|
|
25
|
+
{ model: 'anthropic.claude-opus-4-20250515-v1:0', alias: 'claude-opus-4-8' },
|
|
26
|
+
{ model: 'anthropic.claude-haiku-4-20250506-v1:0', alias: 'claude-haiku-4-5' },
|
|
21
27
|
{ model: 'amazon.titan-text-express-v1', alias: 'titan-text-express' },
|
|
22
28
|
{ model: 'amazon.titan-embed-text-v2:0', alias: 'titan-embed-text-v2', usage_type: :embedding },
|
|
23
29
|
{ model: 'meta.llama3-2-11b-instruct-v1:0', alias: 'llama-3.2-11b-instruct' },
|
|
@@ -117,6 +123,10 @@ module Legion
|
|
|
117
123
|
end
|
|
118
124
|
end
|
|
119
125
|
|
|
126
|
+
def translator
|
|
127
|
+
@translator ||= Translator.new(region: region)
|
|
128
|
+
end
|
|
129
|
+
|
|
120
130
|
def api_base
|
|
121
131
|
config.bedrock_endpoint || "https://bedrock-runtime.#{region}.amazonaws.com"
|
|
122
132
|
end
|
|
@@ -215,6 +225,7 @@ module Legion
|
|
|
215
225
|
thinking: nil,
|
|
216
226
|
**_provider_options
|
|
217
227
|
)
|
|
228
|
+
enforce_model_allowed!(model_id(model))
|
|
218
229
|
log.info { "bedrock.provider.chat: model=#{model_id(model)} messages=#{messages.size}" }
|
|
219
230
|
|
|
220
231
|
# Bedrock Converse API silently drops thinking config and tool_use blocks
|
|
@@ -301,6 +312,7 @@ module Legion
|
|
|
301
312
|
|
|
302
313
|
def stream(messages:, model:, temperature: nil, max_tokens: nil, tools: {}, tool_prefs: nil, params: {},
|
|
303
314
|
thinking: nil, **_provider_options, &)
|
|
315
|
+
enforce_model_allowed!(model_id(model))
|
|
304
316
|
log.info do
|
|
305
317
|
"bedrock.provider.stream: model=#{model_id(model)} messages=#{messages.size} tools=#{tools.size}"
|
|
306
318
|
end
|
|
@@ -358,6 +370,7 @@ module Legion
|
|
|
358
370
|
**_provider_options
|
|
359
371
|
)
|
|
360
372
|
mid = model_id(model)
|
|
373
|
+
enforce_model_allowed!(mid)
|
|
361
374
|
unless titan_embed?(mid)
|
|
362
375
|
raise NotImplementedError,
|
|
363
376
|
"Bedrock embedding payload for #{mid} is not standardized"
|
|
@@ -499,8 +512,15 @@ module Legion
|
|
|
499
512
|
state[:raw_events] << { event: event_type, data: raw_event } if dump_path
|
|
500
513
|
handle_invoke_model_stream_json(raw_event, state, mid) { |chunk| yield chunk if block_given? }
|
|
501
514
|
end
|
|
515
|
+
rescue Legion::JSON::ParseError => e
|
|
516
|
+
handle_exception(e, level: :warn, handled: true,
|
|
517
|
+
operation: 'bedrock.provider.invoke_model_stream.chunk_decode')
|
|
502
518
|
rescue StandardError => e
|
|
503
|
-
|
|
519
|
+
# Never swallow non-parse errors here — a silent rescue in this
|
|
520
|
+
# event handler previously hid streaming bugs as dead-air streams.
|
|
521
|
+
handle_exception(e, level: :error, handled: false,
|
|
522
|
+
operation: 'bedrock.provider.invoke_model_stream.chunk_event')
|
|
523
|
+
raise
|
|
504
524
|
end
|
|
505
525
|
|
|
506
526
|
stream.on_error_event do |event|
|
|
@@ -555,12 +575,14 @@ module Legion
|
|
|
555
575
|
Legion::Extensions::Llm::Message.new(**msg_attrs)
|
|
556
576
|
end
|
|
557
577
|
|
|
558
|
-
def build_invoke_model_body(messages:, temperature:, max_tokens:, tools:, tool_prefs:, thinking:, **
|
|
578
|
+
def build_invoke_model_body(messages:, temperature:, max_tokens:, tools:, tool_prefs:, thinking:, **rest)
|
|
579
|
+
system_content = extract_invoke_model_system(messages, system: rest[:system])
|
|
559
580
|
body = {
|
|
560
581
|
max_tokens: max_tokens || 4096,
|
|
561
582
|
messages: format_invoke_model_messages(messages),
|
|
562
583
|
anthropic_version: 'bedrock-2023-05-31'
|
|
563
584
|
}
|
|
585
|
+
body[:system] = system_content if system_content
|
|
564
586
|
body[:temperature] = temperature if temperature
|
|
565
587
|
if tools && !tools.empty?
|
|
566
588
|
tool_format = format_invoke_model_tools(tools, tool_prefs)
|
|
@@ -568,11 +590,25 @@ module Legion
|
|
|
568
590
|
body[:tool_choice] = tool_format[:tool_choice] if tool_format[:tool_choice]
|
|
569
591
|
end
|
|
570
592
|
body[:thinking] = invoke_model_thinking(thinking) if thinking
|
|
571
|
-
# NOTE: Don't include body[:stream] = true in the JSON body for invoke_model_with_response_stream.
|
|
572
|
-
# The endpoint itself implies streaming; Bedrock rejects the extra field.
|
|
573
593
|
body
|
|
574
594
|
end
|
|
575
595
|
|
|
596
|
+
def extract_invoke_model_system(messages, system: nil)
|
|
597
|
+
parts = []
|
|
598
|
+
parts << system.to_s unless system.to_s.empty?
|
|
599
|
+
messages.each do |msg|
|
|
600
|
+
role = msg.respond_to?(:role) ? msg.role.to_s : (msg[:role] || msg['role']).to_s
|
|
601
|
+
next unless role == 'system'
|
|
602
|
+
|
|
603
|
+
content = msg.respond_to?(:content) ? msg.content : (msg[:content] || msg['content'])
|
|
604
|
+
text = content.is_a?(Array) ? content.filter_map { |b| b[:text] || b['text'] }.join("\n") : content.to_s
|
|
605
|
+
parts << text unless text.empty?
|
|
606
|
+
end
|
|
607
|
+
return nil if parts.empty?
|
|
608
|
+
|
|
609
|
+
parts.map { |t| { type: 'text', text: t } }
|
|
610
|
+
end
|
|
611
|
+
|
|
576
612
|
# Strip provider-specific keys (e.g. effort from OpenAI) that Bedrock/Anthropic APIs don't accept.
|
|
577
613
|
def invoke_model_thinking(thinking)
|
|
578
614
|
return thinking unless thinking.is_a?(Hash)
|
|
@@ -581,7 +617,7 @@ module Legion
|
|
|
581
617
|
end
|
|
582
618
|
|
|
583
619
|
def format_invoke_model_messages(messages)
|
|
584
|
-
messages.filter_map do |msg|
|
|
620
|
+
formatted = messages.filter_map do |msg|
|
|
585
621
|
role = msg.respond_to?(:role) ? msg.role.to_s : (msg[:role] || msg['role']).to_s
|
|
586
622
|
next if role == 'system'
|
|
587
623
|
|
|
@@ -598,6 +634,19 @@ module Legion
|
|
|
598
634
|
|
|
599
635
|
{ role: role == 'tool' ? 'user' : role, content: content }
|
|
600
636
|
end
|
|
637
|
+
consolidate_adjacent_roles(formatted)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def consolidate_adjacent_roles(messages)
|
|
641
|
+
return messages if messages.size < 2
|
|
642
|
+
|
|
643
|
+
messages.each_with_object([]) do |msg, result|
|
|
644
|
+
if result.last && result.last[:role] == msg[:role]
|
|
645
|
+
result.last[:content] = Array(result.last[:content]) + Array(msg[:content])
|
|
646
|
+
else
|
|
647
|
+
result << msg
|
|
648
|
+
end
|
|
649
|
+
end
|
|
601
650
|
end
|
|
602
651
|
|
|
603
652
|
def format_invoke_model_content(msg)
|
|
@@ -664,11 +713,11 @@ module Legion
|
|
|
664
713
|
|
|
665
714
|
def format_invoke_model_tools(tools, tool_prefs)
|
|
666
715
|
tool_list = tools.values.map do |tool|
|
|
716
|
+
raw_schema = tool[:params_schema] || tool['params_schema'] || tool[:parameters] || tool['parameters']
|
|
667
717
|
{
|
|
668
718
|
name: tool[:name] || tool['name'],
|
|
669
719
|
description: tool[:description] || tool['description'] || '',
|
|
670
|
-
input_schema:
|
|
671
|
-
{ type: 'object', properties: {} }
|
|
720
|
+
input_schema: Legion::Extensions::Llm::Canonical::ToolDefinition.normalize_parameters(raw_schema)
|
|
672
721
|
}
|
|
673
722
|
end
|
|
674
723
|
|
|
@@ -802,7 +851,11 @@ module Legion
|
|
|
802
851
|
state[:stop_reason] = delta['stop_reason']
|
|
803
852
|
end
|
|
804
853
|
rescue StandardError => e
|
|
805
|
-
|
|
854
|
+
# Re-raise — a swallowed error here turns a streaming bug into a
|
|
855
|
+
# silent dead-air stream (message_start then nothing).
|
|
856
|
+
handle_exception(e, level: :error, handled: false,
|
|
857
|
+
operation: 'bedrock.provider.invoke_model_stream_json')
|
|
858
|
+
raise
|
|
806
859
|
end
|
|
807
860
|
|
|
808
861
|
def static_offerings(**filters)
|
|
@@ -816,12 +869,24 @@ module Legion
|
|
|
816
869
|
|
|
817
870
|
def offering_from_summary(summary)
|
|
818
871
|
model = value(summary, :model_id)
|
|
872
|
+
real = real_capabilities_from_summary(summary)
|
|
873
|
+
policy = Legion::Extensions::Llm::CapabilityPolicy.resolve(
|
|
874
|
+
real: real,
|
|
875
|
+
provider_catalog: {},
|
|
876
|
+
probe: {},
|
|
877
|
+
provider_envelope: provider_envelope_capabilities,
|
|
878
|
+
provider_config: provider_capability_config,
|
|
879
|
+
instance_config: instance_capability_config,
|
|
880
|
+
model_config: model_capability_config(model)
|
|
881
|
+
)
|
|
882
|
+
|
|
819
883
|
build_offering(
|
|
820
884
|
model: model,
|
|
821
885
|
alias_name: alias_for(model),
|
|
822
886
|
model_family: normalize_provider(value(summary, :provider_name)) || model_family_for(model),
|
|
823
887
|
usage_type: usage_type_from_modalities(value(summary, :output_modalities)),
|
|
824
|
-
capabilities:
|
|
888
|
+
capabilities: policy[:capabilities],
|
|
889
|
+
capability_sources: policy[:sources],
|
|
825
890
|
metadata: normalize_response(summary)
|
|
826
891
|
)
|
|
827
892
|
end
|
|
@@ -844,7 +909,7 @@ module Legion
|
|
|
844
909
|
end
|
|
845
910
|
|
|
846
911
|
def build_offering(model:, model_family:, usage_type:, instance_id: :default, alias_name: nil,
|
|
847
|
-
capabilities: nil, metadata: {})
|
|
912
|
+
capabilities: nil, capability_sources: nil, metadata: {})
|
|
848
913
|
limits = infer_limits(model)
|
|
849
914
|
Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
850
915
|
provider_family: :bedrock,
|
|
@@ -854,6 +919,7 @@ module Legion
|
|
|
854
919
|
model: model,
|
|
855
920
|
usage_type: usage_type,
|
|
856
921
|
capabilities: capabilities || default_capabilities(model),
|
|
922
|
+
capability_sources: capability_sources,
|
|
857
923
|
limits: limits,
|
|
858
924
|
metadata: metadata.merge(model_family: model_family, alias: alias_name).compact
|
|
859
925
|
)
|
|
@@ -903,13 +969,14 @@ module Legion
|
|
|
903
969
|
|
|
904
970
|
def format_messages(messages)
|
|
905
971
|
total = messages.size
|
|
906
|
-
messages.filter_map.with_index do |message, idx|
|
|
972
|
+
formatted = messages.filter_map.with_index do |message, idx|
|
|
907
973
|
blocks = build_content_blocks(message)
|
|
908
974
|
next if blocks.empty?
|
|
909
975
|
|
|
910
976
|
cache_blocks = should_cache_message?(idx, total) ? add_cache_control_to_blocks(blocks) : blocks
|
|
911
977
|
{ role: bedrock_role(message.role), content: cache_blocks }
|
|
912
978
|
end
|
|
979
|
+
consolidate_adjacent_roles(formatted)
|
|
913
980
|
end
|
|
914
981
|
|
|
915
982
|
def tool_result_blocks(message)
|
|
@@ -967,7 +1034,9 @@ module Legion
|
|
|
967
1034
|
text = content_text(message.content)
|
|
968
1035
|
blocks << { text: text } if text && !text.strip.empty?
|
|
969
1036
|
|
|
970
|
-
|
|
1037
|
+
# Array is canonical (name-keyed hashes dropped parallel same-name calls)
|
|
1038
|
+
calls = message.tool_calls.is_a?(Hash) ? message.tool_calls.values : Array(message.tool_calls)
|
|
1039
|
+
calls.each do |call|
|
|
971
1040
|
blocks << {
|
|
972
1041
|
tool_use: {
|
|
973
1042
|
tool_use_id: call.id,
|
|
@@ -1062,9 +1131,12 @@ module Legion
|
|
|
1062
1131
|
end
|
|
1063
1132
|
|
|
1064
1133
|
def tool_schema(tool)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1134
|
+
raw = if tool.respond_to?(:params_schema) && tool.params_schema
|
|
1135
|
+
tool.params_schema
|
|
1136
|
+
elsif tool.respond_to?(:parameters)
|
|
1137
|
+
tool.parameters
|
|
1138
|
+
end
|
|
1139
|
+
Legion::Extensions::Llm::Canonical::ToolDefinition.normalize_parameters(raw)
|
|
1068
1140
|
end
|
|
1069
1141
|
|
|
1070
1142
|
def tool_choice(tool_prefs)
|
|
@@ -1273,8 +1345,8 @@ module Legion
|
|
|
1273
1345
|
# Bedrock streaming: text blocks use delta.text,
|
|
1274
1346
|
# reasoning/thinking blocks use delta.reasoning.text or delta.thinking.text
|
|
1275
1347
|
text = value(delta, :text) ||
|
|
1276
|
-
(value(delta, :reasoning)
|
|
1277
|
-
(value(delta, :thinking)
|
|
1348
|
+
value(value(delta, :reasoning), :text) ||
|
|
1349
|
+
value(value(delta, :thinking), :text)
|
|
1278
1350
|
if text
|
|
1279
1351
|
if state[:in_thinking]
|
|
1280
1352
|
state[:thinking] << text
|
|
@@ -1399,7 +1471,7 @@ module Legion
|
|
|
1399
1471
|
return nil unless config.bedrock_access_key_id
|
|
1400
1472
|
|
|
1401
1473
|
if static_credentials_blocked?
|
|
1402
|
-
raise
|
|
1474
|
+
raise StaticCredentialsBlockedError,
|
|
1403
1475
|
'Static AWS credentials are disabled (security.block_static_aws_credentials=true); use IAM roles'
|
|
1404
1476
|
end
|
|
1405
1477
|
log.warn('[bedrock] Using static AWS credentials — prefer IAM roles for production')
|
|
@@ -1467,6 +1539,59 @@ module Legion
|
|
|
1467
1539
|
caps
|
|
1468
1540
|
end
|
|
1469
1541
|
|
|
1542
|
+
def real_capabilities_from_summary(summary)
|
|
1543
|
+
caps = {}
|
|
1544
|
+
caps[:streaming] = true if value(summary, :response_streaming_supported)
|
|
1545
|
+
input_mods = Array(value(summary, :input_modalities)).map { |m| m.to_s.upcase }
|
|
1546
|
+
caps[:vision] = true if input_mods.include?('IMAGE')
|
|
1547
|
+
output_mods = Array(value(summary, :output_modalities)).map { |m| m.to_s.upcase }
|
|
1548
|
+
caps[:embeddings] = true if output_mods.include?('EMBEDDING')
|
|
1549
|
+
caps
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
def provider_envelope_capabilities
|
|
1553
|
+
# Bedrock Converse API supports tool use across all active chat model families
|
|
1554
|
+
{ tools: true }
|
|
1555
|
+
end
|
|
1556
|
+
|
|
1557
|
+
def provider_capability_config
|
|
1558
|
+
return {} unless defined?(Legion::Extensions::Llm::CredentialSources)
|
|
1559
|
+
|
|
1560
|
+
conf = Legion::Extensions::Llm::CredentialSources.setting(:extensions, :llm, :bedrock)
|
|
1561
|
+
conf.is_a?(Hash) ? conf.to_h.except(:instances, 'instances') : {}
|
|
1562
|
+
rescue StandardError => e
|
|
1563
|
+
handle_exception(e, level: :debug, handled: true, operation: 'bedrock.provider_capability_config')
|
|
1564
|
+
{}
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
def instance_capability_config
|
|
1568
|
+
cfg = config
|
|
1569
|
+
result = {}
|
|
1570
|
+
%i[capabilities enable_thinking enable_tools enable_streaming enable_vision enable_embeddings
|
|
1571
|
+
thinking_flag tools_flag streaming_flag vision_flag embedding_flag embeddings_flag
|
|
1572
|
+
tool_flag images_flag image_flag].each do |key|
|
|
1573
|
+
next unless cfg.respond_to?(key)
|
|
1574
|
+
|
|
1575
|
+
val = cfg.send(key)
|
|
1576
|
+
result[key] = val unless val.nil?
|
|
1577
|
+
rescue StandardError
|
|
1578
|
+
next
|
|
1579
|
+
end
|
|
1580
|
+
result
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
def model_capability_config(model_id)
|
|
1584
|
+
models_conf = nil
|
|
1585
|
+
models_conf = config.models if config.respond_to?(:models)
|
|
1586
|
+
models_conf ||= config[:models] if config.respond_to?(:[])
|
|
1587
|
+
return {} unless models_conf.respond_to?(:to_h)
|
|
1588
|
+
|
|
1589
|
+
models_conf.to_h[model_id.to_s] || models_conf.to_h[model_id.to_sym] || {}
|
|
1590
|
+
rescue StandardError => e
|
|
1591
|
+
handle_exception(e, level: :debug, handled: true, operation: 'bedrock.model_capability_config')
|
|
1592
|
+
{}
|
|
1593
|
+
end
|
|
1594
|
+
|
|
1470
1595
|
def model_family_for(model)
|
|
1471
1596
|
normalize_provider(model.to_s.split('.').first)
|
|
1472
1597
|
end
|
|
@@ -205,7 +205,7 @@ module Legion
|
|
|
205
205
|
tool_spec: {
|
|
206
206
|
name: tool.name,
|
|
207
207
|
description: tool.description.to_s,
|
|
208
|
-
input_schema: { json: tool.parameters
|
|
208
|
+
input_schema: { json: tool.parameters }
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
end
|
|
@@ -234,6 +234,9 @@ module Legion
|
|
|
234
234
|
anthropic_version: 'bedrock-2023-05-31'
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
sys = render_invoke_system(canonical)
|
|
238
|
+
body[:system] = sys if sys
|
|
239
|
+
|
|
237
240
|
temp = canonical.params&.temperature
|
|
238
241
|
body[:temperature] = temp if temp
|
|
239
242
|
|
|
@@ -256,6 +259,22 @@ module Legion
|
|
|
256
259
|
{ type: 'enabled', budget_tokens: budget }
|
|
257
260
|
end
|
|
258
261
|
|
|
262
|
+
def render_invoke_system(canonical)
|
|
263
|
+
sys = canonical.system
|
|
264
|
+
return nil if sys.nil? || sys.to_s.strip.empty?
|
|
265
|
+
|
|
266
|
+
if sys.is_a?(Array)
|
|
267
|
+
sys.map do |block|
|
|
268
|
+
wire = { type: 'text', text: (block[:text] || block['text'] || block.to_s).to_s }
|
|
269
|
+
cc = block[:cache_control] || block['cache_control']
|
|
270
|
+
wire[:cache_control] = cc if cc
|
|
271
|
+
wire
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
[{ type: 'text', text: sys.to_s }]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
259
278
|
def build_invoke_tools(canonical)
|
|
260
279
|
return nil unless canonical.tools && !canonical.tools.empty?
|
|
261
280
|
|
|
@@ -263,7 +282,7 @@ module Legion
|
|
|
263
282
|
{
|
|
264
283
|
name: tool.name,
|
|
265
284
|
description: (tool.description || '').to_s,
|
|
266
|
-
input_schema: tool.parameters
|
|
285
|
+
input_schema: tool.parameters
|
|
267
286
|
}
|
|
268
287
|
end
|
|
269
288
|
|
|
@@ -5,6 +5,7 @@ require 'legion/extensions/llm/bedrock/provider'
|
|
|
5
5
|
require 'legion/extensions/llm/bedrock/translator'
|
|
6
6
|
require 'legion/extensions/llm/bedrock/version'
|
|
7
7
|
require 'legion/logging/helper'
|
|
8
|
+
require_relative 'bedrock/actors/discovery_refresh'
|
|
8
9
|
|
|
9
10
|
module Legion
|
|
10
11
|
module Extensions
|
|
@@ -18,12 +19,16 @@ module Legion
|
|
|
18
19
|
PROVIDER_FAMILY = :bedrock
|
|
19
20
|
|
|
20
21
|
DEFAULT_REGION = 'us-east-2'
|
|
22
|
+
# Provider's preferred default when the operator configures none. Used only
|
|
23
|
+
# as a fallback and only when the configured model policy permits it
|
|
24
|
+
# (see resolve_default_model) — a whitelist/blacklist is never overridden.
|
|
25
|
+
DEFAULT_MODEL = 'anthropic.claude-sonnet-4'
|
|
21
26
|
|
|
22
27
|
def self.default_settings
|
|
23
28
|
::Legion::Extensions::Llm.provider_settings(
|
|
24
29
|
family: PROVIDER_FAMILY,
|
|
25
30
|
instance: {
|
|
26
|
-
default_model:
|
|
31
|
+
default_model: DEFAULT_MODEL,
|
|
27
32
|
region: 'us-east-1',
|
|
28
33
|
tier: :cloud,
|
|
29
34
|
transport: :aws_sdk,
|
|
@@ -44,10 +49,7 @@ module Legion
|
|
|
44
49
|
fleet: {
|
|
45
50
|
enabled: false,
|
|
46
51
|
respond_to_requests: false,
|
|
47
|
-
capabilities: %i[chat stream_chat embed]
|
|
48
|
-
lanes: [],
|
|
49
|
-
concurrency: 4,
|
|
50
|
-
queue_suffix: nil
|
|
52
|
+
capabilities: %i[chat stream_chat embed tools]
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
)
|
|
@@ -75,11 +77,20 @@ module Legion
|
|
|
75
77
|
.transform_values do |config|
|
|
76
78
|
sanitized = sanitize_instance_config(config)
|
|
77
79
|
sanitized[:capabilities] ||= DEFAULT_CAPABILITIES.dup
|
|
78
|
-
sanitized[:default_model]
|
|
80
|
+
sanitized[:default_model] = resolve_default_model(sanitized)
|
|
79
81
|
sanitized
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
|
|
85
|
+
# Resolve a default_model that never violates the configured model policy
|
|
86
|
+
# (whitelist/blacklist stays authoritative over the DEFAULT_MODEL fallback).
|
|
87
|
+
def self.resolve_default_model(config)
|
|
88
|
+
provider_class.policy_safe_default_model(
|
|
89
|
+
configured: config[:default_model], fallback: DEFAULT_MODEL,
|
|
90
|
+
**provider_class.model_policy(config, PROVIDER_FAMILY)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
83
94
|
def self.unresolved_credential?(config)
|
|
84
95
|
return false if config[:bedrock_profile]
|
|
85
96
|
|
|
@@ -223,8 +234,7 @@ module Legion
|
|
|
223
234
|
config.except(:api_key)
|
|
224
235
|
end
|
|
225
236
|
|
|
226
|
-
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
227
|
-
Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
|
|
237
|
+
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
228
238
|
end
|
|
229
239
|
end
|
|
230
240
|
end
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -99,14 +99,14 @@ dependencies:
|
|
|
99
99
|
requirements:
|
|
100
100
|
- - ">="
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: 0.5.
|
|
102
|
+
version: 0.5.4
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: 0.5.
|
|
109
|
+
version: 0.5.4
|
|
110
110
|
description: Amazon Bedrock provider integration for the LegionIO LLM routing framework.
|
|
111
111
|
email:
|
|
112
112
|
- matthewdiverson@gmail.com
|