lex-llm-bedrock 0.3.6 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba5cbf8b2adeb7ce05f969bfef32582c03d238febed07790345841d1cf038eba
4
- data.tar.gz: 96fd62bc8575afa30a836c084e3d4f8745dfd59566f7210ec6226f4a466444f5
3
+ metadata.gz: 5ef7a8a909bb86688882fc9ffca3c7fd96955d75cb01e5048efa954909599c49
4
+ data.tar.gz: 90d58f5f3e6f976b2332341e6f5624064a5b70f7ce5381d4df94b082d7398ab7
5
5
  SHA512:
6
- metadata.gz: 4cf60483077635545837818151d76674d3a8f69550dce991f3c8def75f1c9fabe882b30efedbeffe57c051040b53e7b473a92d34199c9edf7da2f6441fecddcc
7
- data.tar.gz: 9098f7257c42b29f48ea2cb6d212ef737b8473f7d53e3f9bddedbde7e4be7c2887ce1d82e998905fbf86621975537fe49bd3181792d8a23925dbada75fa5fc33
6
+ metadata.gz: ab5e3feccaee75a608ba7032721bd899e74e9059bfee3704ebc45b1f1733a5341eb2e1f1f99be48bdc2b0eee4555aaf8985e1c84a2c673474dbc4ae018abf6b7
7
+ data.tar.gz: 61b3a978d40a10d91f725ffd3ebc7b937596fa60740fb9e3d168cbba6f19fc2a50f27af56a637612c4980b1e4408dcae61e2148fff6b819026ed35811fbdcf6c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8 - 2026-05-13
4
+
5
+ - Auto-prefix `us.` on `inference_profile_id` for Anthropic, Meta, Mistral, Cohere, and AI21 models at API call time.
6
+ - Filter empty content blocks from messages to satisfy Bedrock validation.
7
+ - Wire Bearer token into AWS SDK via `Aws::StaticTokenProvider` to eliminate IMDS timeout on startup.
8
+ - Add `source` and `credential_fingerprint` fields to all discovered instances.
9
+ - Inject default capabilities into all discovered instances.
10
+ - Add static `CONTEXT_WINDOWS` map; `infer_limits` reads from `model_detail` cache instead of live API.
11
+ - Override `fetch_model_detail` to return static context window data without a network call.
12
+ - Cache live results in `discover_offerings`.
13
+ - Add `unresolved_credential?` filter — instances with `vault://` or `env://` credential refs are skipped during registration.
14
+ - Inject `default_model` into all discovered instances.
15
+
16
+ ## 0.3.7 - 2026-05-12
17
+
18
+ - Use `Legion::Logging::Helper` explicitly across Bedrock provider, actor, and fleet runner logging surfaces.
19
+ - Add non-sensitive debug logging for Bedrock tool configuration and fleet request routing.
20
+ - Report optional actor runtime load failures through `handle_exception` instead of direct warning output.
21
+
3
22
  ## 0.3.6 - 2026-05-08
4
23
 
5
24
  - Accept keyword arguments in `list_models` to match the base provider contract called by `discover_offerings`.
@@ -1,16 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/extensions/llm/bedrock'
4
+
3
5
  begin
4
6
  require 'legion/extensions/actors/subscription'
5
7
  rescue LoadError => e
6
- warn(e.message) if $VERBOSE
8
+ Legion::Extensions::Llm::Bedrock.handle_exception(
9
+ e,
10
+ level: :debug,
11
+ handled: true,
12
+ operation: 'bedrock.actor.fleet_worker.load_subscription'
13
+ )
7
14
  end
8
15
 
9
16
  unless defined?(Legion::Extensions::Actors::Subscription)
10
17
  raise LoadError, 'LegionIO actor runtime is required for Bedrock fleet worker'
11
18
  end
12
19
 
13
- require 'legion/extensions/llm/bedrock'
14
20
  require 'legion/extensions/llm/fleet/provider_responder'
15
21
 
16
22
  module Legion
@@ -20,6 +26,8 @@ module Legion
20
26
  module Actor
21
27
  # Subscription actor for Bedrock fleet request consumption.
22
28
  class FleetWorker < Legion::Extensions::Actors::Subscription
29
+ include Legion::Logging::Helper
30
+
23
31
  def runner_class
24
32
  'Legion::Extensions::Llm::Bedrock::Runners::FleetWorker'
25
33
  end
@@ -33,7 +41,10 @@ module Legion
33
41
  end
34
42
 
35
43
  def enabled?
36
- Legion::Extensions::Llm::Fleet::ProviderResponder.enabled_for?(Bedrock.discover_instances)
44
+ instances = Bedrock.discover_instances
45
+ enabled = Legion::Extensions::Llm::Fleet::ProviderResponder.enabled_for?(instances)
46
+ log.debug { "bedrock.actor.fleet_worker.enabled?: instances=#{instances.size} enabled=#{enabled}" }
47
+ enabled
37
48
  end
38
49
  end
39
50
  end
@@ -3,7 +3,7 @@
3
3
  require 'aws-sdk-bedrock'
4
4
  require 'aws-sdk-bedrockruntime'
5
5
  require 'legion/json'
6
- require 'legion/logging'
6
+ require 'legion/logging/helper'
7
7
  require 'legion/extensions/llm'
8
8
 
9
9
  module Legion
@@ -26,6 +26,28 @@ module Legion
26
26
 
27
27
  ALIASES = STATIC_MODELS.to_h { |entry| [entry.fetch(:alias), entry.fetch(:model)] }.freeze
28
28
 
29
+ CONTEXT_WINDOWS = {
30
+ 'anthropic.claude-sonnet-4' => 200_000,
31
+ 'anthropic.claude-haiku-4' => 200_000,
32
+ 'anthropic.claude-opus-4' => 200_000,
33
+ 'anthropic.claude-3-5-sonnet' => 200_000,
34
+ 'anthropic.claude-3-5-haiku' => 200_000,
35
+ 'anthropic.claude-3-haiku' => 200_000,
36
+ 'anthropic.claude-3-opus' => 200_000,
37
+ 'anthropic.claude-3-sonnet' => 200_000,
38
+ 'meta.llama3' => 128_000,
39
+ 'meta.llama3-1' => 128_000,
40
+ 'meta.llama3-2' => 128_000,
41
+ 'meta.llama3-3' => 128_000,
42
+ 'mistral.mistral-large' => 128_000,
43
+ 'mistral.mistral-small' => 128_000,
44
+ 'amazon.titan-text-express' => 8_192,
45
+ 'amazon.titan-text-premier' => 32_000,
46
+ 'amazon.nova-pro' => 300_000,
47
+ 'amazon.nova-lite' => 300_000,
48
+ 'amazon.nova-micro' => 128_000
49
+ }.freeze
50
+
29
51
  class << self
30
52
  def slug = 'bedrock'
31
53
 
@@ -38,6 +60,7 @@ module Legion
38
60
  bedrock_session_token
39
61
  bedrock_profile
40
62
  bedrock_stub_responses
63
+ bearer_token
41
64
  ]
42
65
  end
43
66
 
@@ -51,6 +74,15 @@ module Legion
51
74
  def resolve_model_id(model_id, config: nil) # rubocop:disable Lint/UnusedMethodArgument
52
75
  ALIASES.fetch(model_id.to_s, model_id.to_s)
53
76
  end
77
+
78
+ INFERENCE_PROFILE_PREFIXES = %w[anthropic. meta. mistral. cohere. ai21.].freeze
79
+
80
+ def inference_profile_id(model)
81
+ return model if model.start_with?('us.', 'eu.', 'ap.', 'arn:')
82
+ return model unless INFERENCE_PROFILE_PREFIXES.any? { |p| model.start_with?(p) }
83
+
84
+ "us.#{model}"
85
+ end
54
86
  end
55
87
 
56
88
  # Capability predicates inferred from Bedrock model IDs and API modalities.
@@ -86,15 +118,19 @@ module Legion
86
118
 
87
119
  def discover_offerings(live: false, **filters)
88
120
  unless live
121
+ return @cached_offerings if @cached_offerings&.any?
122
+
89
123
  log.debug { 'bedrock.provider.discover_offerings: returning static catalog' }
90
124
  return static_offerings(**filters)
91
125
  end
92
126
 
93
127
  log.info { "bedrock.provider.discover_offerings: listing foundation models (region=#{region})" }
94
128
  response = bedrock_client.list_foundation_models(**filters)
95
- Array(value(response, :model_summaries)).map { |summary| offering_from_summary(summary) }.tap do |offerings|
96
- log.info { "bedrock.provider.discover_offerings: found #{offerings.size} models" }
129
+ @cached_offerings = Array(value(response, :model_summaries)).map do |summary|
130
+ offering_from_summary(summary)
97
131
  end
132
+ log.info { "bedrock.provider.discover_offerings: found #{@cached_offerings.size} models" }
133
+ @cached_offerings
98
134
  end
99
135
 
100
136
  def offering_for(model:, model_family: nil, instance_id: :default, **metadata)
@@ -164,6 +200,10 @@ module Legion
164
200
  converse_request(messages, model:, temperature:, max_tokens:, tools:, tool_prefs:),
165
201
  params
166
202
  )
203
+ log.debug do
204
+ "bedrock.provider.chat: request prepared model=#{model_id(model)} tools=#{tools.size} " \
205
+ "tool_choice=#{tool_choice_label(tool_prefs)} param_keys=#{params.keys.map(&:to_s).sort.join(',')}"
206
+ end
167
207
  parse_converse_response(runtime_client.converse(**request), model_id(model))
168
208
  end
169
209
 
@@ -174,6 +214,10 @@ module Legion
174
214
  converse_request(messages, model:, temperature:, max_tokens:, tools:, tool_prefs:),
175
215
  params
176
216
  )
217
+ log.debug do
218
+ "bedrock.provider.stream: request prepared model=#{model_id(model)} tools=#{tools.size} " \
219
+ "tool_choice=#{tool_choice_label(tool_prefs)} param_keys=#{params.keys.map(&:to_s).sort.join(',')}"
220
+ end
177
221
  stream_converse(request, model_id(model), &)
178
222
  end
179
223
 
@@ -186,7 +230,7 @@ module Legion
186
230
  log.debug { "bedrock.provider.count_tokens: model=#{model_id(model)}" }
187
231
  request = Utils.deep_merge(
188
232
  {
189
- model_id: model_id(model),
233
+ model_id: self.class.inference_profile_id(model_id(model)),
190
234
  input: { converse: { messages: format_messages(messages), system: system_blocks(system) }.compact }
191
235
  },
192
236
  params
@@ -275,6 +319,7 @@ module Legion
275
319
 
276
320
  def build_offering(model:, model_family:, usage_type:, instance_id: :default, alias_name: nil,
277
321
  capabilities: nil, metadata: {})
322
+ limits = infer_limits(model)
278
323
  Legion::Extensions::Llm::Routing::ModelOffering.new(
279
324
  provider_family: :bedrock,
280
325
  instance_id: instance_id,
@@ -283,10 +328,24 @@ module Legion
283
328
  model: model,
284
329
  usage_type: usage_type,
285
330
  capabilities: capabilities || default_capabilities(model),
331
+ limits: limits,
286
332
  metadata: metadata.merge(model_family: model_family, alias: alias_name).compact
287
333
  )
288
334
  end
289
335
 
336
+ def infer_limits(model)
337
+ detail = model_detail(model.to_s)
338
+ return detail if detail.is_a?(Hash) && detail[:context_window]
339
+
340
+ ctx = CONTEXT_WINDOWS.find { |prefix, _| model.to_s.start_with?(prefix) }&.last
341
+ ctx ? { context_window: ctx } : {}
342
+ end
343
+
344
+ def fetch_model_detail(model_name)
345
+ ctx = CONTEXT_WINDOWS.find { |prefix, _| model_name.start_with?(prefix) }&.last
346
+ ctx ? { context_window: ctx } : nil
347
+ end
348
+
290
349
  def configured_transport(default)
291
350
  config.respond_to?(:transport) ? config.transport : default
292
351
  end
@@ -297,7 +356,7 @@ module Legion
297
356
 
298
357
  def converse_request(messages, model:, temperature:, max_tokens:, tools:, tool_prefs:)
299
358
  {
300
- model_id: model_id(model),
359
+ model_id: self.class.inference_profile_id(model_id(model)),
301
360
  messages: format_messages(messages.reject { |message| message.role == :system }),
302
361
  system: format_system(messages),
303
362
  inference_config: { temperature: temperature, max_tokens: max_tokens || model_max_tokens(model) }.compact,
@@ -306,11 +365,11 @@ module Legion
306
365
  end
307
366
 
308
367
  def format_messages(messages)
309
- messages.map do |message|
310
- {
311
- role: bedrock_role(message.role),
312
- content: content_blocks(message.content)
313
- }
368
+ messages.filter_map do |message|
369
+ blocks = content_blocks(message.content)
370
+ next if blocks.empty?
371
+
372
+ { role: bedrock_role(message.role), content: blocks }
314
373
  end
315
374
  end
316
375
 
@@ -331,7 +390,13 @@ module Legion
331
390
  end
332
391
 
333
392
  def content_blocks(content)
334
- raw_content(content) || [{ text: content_text(content) }]
393
+ raw = raw_content(content)
394
+ return raw if raw
395
+
396
+ text = content_text(content)
397
+ return [] if text.strip.empty?
398
+
399
+ [{ text: text }]
335
400
  end
336
401
 
337
402
  def raw_content(content)
@@ -349,6 +414,10 @@ module Legion
349
414
  def format_tool_config(tools, tool_prefs)
350
415
  return nil if tools.empty?
351
416
 
417
+ log.debug do
418
+ "bedrock.provider.tools: formatting tools=#{tools.keys.map(&:to_s).sort.join(',')} " \
419
+ "tool_choice=#{tool_choice_label(tool_prefs)}"
420
+ end
352
421
  { tools: tools.values.map { |tool| tool_definition(tool) }, tool_choice: tool_choice(tool_prefs) }.compact
353
422
  end
354
423
 
@@ -382,6 +451,12 @@ module Legion
382
451
  end
383
452
  end
384
453
 
454
+ def tool_choice_label(tool_prefs)
455
+ return 'none' unless tool_prefs
456
+
457
+ (tool_prefs[:choice] || tool_prefs['choice'] || 'unspecified').to_s
458
+ end
459
+
385
460
  def parse_converse_response(response, fallback_model)
386
461
  output = value(response, :output)
387
462
  message = value(output, :message)
@@ -459,12 +534,23 @@ module Legion
459
534
  end
460
535
 
461
536
  def client_options
462
- {
537
+ opts = {
463
538
  region: region,
464
539
  endpoint: config.bedrock_endpoint,
465
- credentials: credentials,
466
540
  stub_responses: config.bedrock_stub_responses
467
- }.compact
541
+ }
542
+
543
+ if bearer_token_configured?
544
+ opts[:token_provider] = Aws::StaticTokenProvider.new(config.bearer_token)
545
+ else
546
+ opts[:credentials] = credentials
547
+ end
548
+
549
+ opts.compact
550
+ end
551
+
552
+ def bearer_token_configured?
553
+ config.respond_to?(:bearer_token) && !config.bearer_token.to_s.empty?
468
554
  end
469
555
 
470
556
  def credentials
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'legion/extensions/llm/fleet/provider_responder'
4
4
  require 'legion/extensions/llm/bedrock'
5
+ require 'legion/logging/helper'
5
6
 
6
7
  module Legion
7
8
  module Extensions
@@ -10,9 +11,15 @@ module Legion
10
11
  module Runners
11
12
  # Runner entrypoint for Bedrock fleet request execution.
12
13
  module FleetWorker
14
+ extend Legion::Logging::Helper
15
+
13
16
  module_function
14
17
 
15
18
  def handle_fleet_request(payload, delivery: nil, properties: nil)
19
+ log.debug do
20
+ "bedrock.runner.fleet_worker.handle_fleet_request: request_id=#{payload_value(payload, :request_id)} " \
21
+ "provider_instance=#{payload_value(payload, :provider_instance) || 'default'}"
22
+ end
16
23
  Legion::Extensions::Llm::Fleet::ProviderResponder.call(
17
24
  payload: payload,
18
25
  provider_family: Bedrock::PROVIDER_FAMILY,
@@ -22,6 +29,12 @@ module Legion
22
29
  properties: properties
23
30
  )
24
31
  end
32
+
33
+ def payload_value(payload, key)
34
+ return nil unless payload.respond_to?(:[])
35
+
36
+ payload[key] || payload[key.to_s]
37
+ end
25
38
  end
26
39
  end
27
40
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Bedrock
7
- VERSION = '0.3.6'
7
+ VERSION = '0.3.8'
8
8
  end
9
9
  end
10
10
  end
@@ -3,6 +3,7 @@
3
3
  require 'legion/extensions/llm'
4
4
  require 'legion/extensions/llm/bedrock/provider'
5
5
  require 'legion/extensions/llm/bedrock/version'
6
+ require 'legion/logging/helper'
6
7
 
7
8
  module Legion
8
9
  module Extensions
@@ -58,6 +59,8 @@ module Legion
58
59
  @registry_publisher ||= Legion::Extensions::Llm::RegistryPublisher.new(provider_family: PROVIDER_FAMILY)
59
60
  end
60
61
 
62
+ DEFAULT_CAPABILITIES = %i[completion streaming embedding].freeze
63
+
61
64
  def self.discover_instances
62
65
  candidates = {}
63
66
  discover_env_bearer(candidates)
@@ -65,7 +68,23 @@ module Legion
65
68
  discover_env_sigv4(candidates)
66
69
  discover_settings(candidates)
67
70
  discover_broker(candidates)
68
- CredentialSources.dedup_credentials(candidates).transform_values { |config| sanitize_instance_config(config) }
71
+ CredentialSources.dedup_credentials(candidates)
72
+ .reject { |_, config| unresolved_credential?(config) }
73
+ .transform_values do |config|
74
+ sanitized = sanitize_instance_config(config)
75
+ sanitized[:capabilities] ||= DEFAULT_CAPABILITIES.dup
76
+ sanitized[:default_model] ||= 'us.anthropic.claude-sonnet-4-6'
77
+ sanitized
78
+ end
79
+ end
80
+
81
+ def self.unresolved_credential?(config)
82
+ return false if config[:bedrock_profile]
83
+
84
+ cred = config[:bearer_token] || config[:bedrock_access_key_id] || config[:api_key]
85
+ return true if cred.nil?
86
+
87
+ cred.to_s.match?(%r{\A(vault|env)://})
69
88
  end
70
89
 
71
90
  def self.discover_env_bearer(candidates)
@@ -75,7 +94,9 @@ module Legion
75
94
  candidates[:env_bearer] = {
76
95
  bearer_token: bearer,
77
96
  bedrock_region: CredentialSources.env('AWS_DEFAULT_REGION') || DEFAULT_REGION,
78
- tier: :cloud
97
+ tier: :cloud,
98
+ source: CredentialSources.source_tag(:env, 'AWS_BEARER_TOKEN_BEDROCK'),
99
+ credential_fingerprint: CredentialSources.credential_fingerprint(bearer)
79
100
  }
80
101
  end
81
102
 
@@ -87,7 +108,9 @@ module Legion
87
108
  candidates[:claude] = {
88
109
  bearer_token: claude_bearer,
89
110
  bedrock_region: CredentialSources.claude_env_value('AWS_DEFAULT_REGION') || DEFAULT_REGION,
90
- tier: :cloud
111
+ tier: :cloud,
112
+ source: CredentialSources.source_tag(:file, '~/.claude/settings.json', 'AWS_BEARER_TOKEN_BEDROCK'),
113
+ credential_fingerprint: CredentialSources.credential_fingerprint(claude_bearer)
91
114
  }
92
115
  end
93
116
 
@@ -99,7 +122,10 @@ module Legion
99
122
  candidates[:env_sigv4] = {
100
123
  api_key: akid, bedrock_access_key_id: akid, bedrock_secret_access_key: skey,
101
124
  bedrock_session_token: CredentialSources.env('AWS_SESSION_TOKEN'),
102
- bedrock_region: CredentialSources.env('AWS_DEFAULT_REGION') || DEFAULT_REGION, tier: :cloud
125
+ bedrock_region: CredentialSources.env('AWS_DEFAULT_REGION') || DEFAULT_REGION,
126
+ tier: :cloud,
127
+ source: CredentialSources.source_tag(:env, 'AWS_ACCESS_KEY_ID'),
128
+ credential_fingerprint: CredentialSources.credential_fingerprint(akid)
103
129
  }.compact
104
130
  end
105
131
 
@@ -108,12 +134,19 @@ module Legion
108
134
  return unless settings.is_a?(Hash) && !settings.empty?
109
135
 
110
136
  default_config = dedup_config(normalize_instance_config(settings))
111
- candidates[:settings] = default_config.merge(tier: :cloud) unless default_config.empty?
137
+ unless default_config.empty?
138
+ default_config[:source] = CredentialSources.source_tag(:settings, 'extensions.llm.bedrock')
139
+ default_config[:credential_fingerprint] = CredentialSources.config_fingerprint(default_config)
140
+ candidates[:settings] = default_config.merge(tier: :cloud)
141
+ end
112
142
 
113
143
  settings_instances(settings).each do |name, config|
114
144
  next unless config.is_a?(Hash)
115
145
 
116
- candidates[name.to_sym] = dedup_config(normalize_instance_config(config)).merge(tier: :cloud)
146
+ normalized = dedup_config(normalize_instance_config(config))
147
+ normalized[:source] = CredentialSources.source_tag(:settings, "extensions.llm.bedrock.instances.#{name}")
148
+ normalized[:credential_fingerprint] = CredentialSources.config_fingerprint(normalized)
149
+ candidates[name.to_sym] = normalized.merge(tier: :cloud)
117
150
  end
118
151
  end
119
152
 
@@ -121,7 +154,11 @@ module Legion
121
154
  return unless defined?(Legion::Identity::Broker)
122
155
 
123
156
  broker_creds = broker_aws_credentials
124
- candidates[:broker] = broker_creds.merge(tier: :cloud) if broker_creds
157
+ return unless broker_creds
158
+
159
+ broker_creds[:source] = CredentialSources.source_tag(:broker, 'identity', 'aws')
160
+ broker_creds[:credential_fingerprint] = CredentialSources.config_fingerprint(broker_creds)
161
+ candidates[:broker] = broker_creds.merge(tier: :cloud)
125
162
  end
126
163
 
127
164
  # Scan Claude config env hash for any key containing all of
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.3.6
4
+ version: 0.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO