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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0c19835470c757fe2f6ea3a1708884b0145694e921e34b22fca0b89c8aac69b
4
- data.tar.gz: b0fe049d5182e0760f5740e42fc7488eb80239aabba7a753e34d486d915e3c2d
3
+ metadata.gz: 3aa9de1a3f3c07848fe253d6f8627c54b03bbc74bb96ab5032a407b109aad13a
4
+ data.tar.gz: a3f680546d15739bdfb84f79daa70a45beae2650893aead380e23b0e9086b38e
5
5
  SHA512:
6
- metadata.gz: 23604b77e5b16449e74d08a5fe14e9ee9a0e0bbb3ed164103e42f3005d7c483f3b65ea58d976701429a33632f89269757e1e2e33cf1f0289d04c8a1f231c325f
7
- data.tar.gz: 6b6f898e45dfc824daa1d3ca96381af08b40e7ed72b1fd90f57d378285db0cdecc1ee90ebbeb227c52d4494bbfc7be4b5752e8cbdd1b9bb616019cec09f2db02
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'
@@ -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.0'
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
- log.warn { "bedrock.provider.invoke_model_stream: chunk decode error=#{sanitize_log(e.message)}" }
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:, **_rest)
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: tool[:params_schema] || tool['params_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
- log.warn { "bedrock.provider.invoke_model_stream_json: error=#{e.message}" }
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: capabilities_from_summary(summary),
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
- message.tool_calls.each_value do |call|
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
- return tool.params_schema if tool.respond_to?(:params_schema) && tool.params_schema
1066
-
1067
- { type: 'object', properties: {} }
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) ? value(reasoning_delta, :text) : nil) ||
1277
- (value(delta, :thinking) ? value(thinking_delta, :text) : nil)
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 SecurityError,
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 || { type: 'object', properties: {} } }
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 || { type: 'object', properties: {} }
285
+ input_schema: tool.parameters
267
286
  }
268
287
  end
269
288
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Bedrock
7
- VERSION = '0.4.0'
7
+ VERSION = '0.4.4'
8
8
  end
9
9
  end
10
10
  end
@@ -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: 'anthropic.claude-sonnet-4',
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] ||= 'anthropic.claude-sonnet-4'
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) if
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.0
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.0
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.0
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