lex-llm-bedrock 0.3.7 → 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: d0046ccd6a590eef46ec16fa6092134f7b63eb5d2512d6c75249ee51a63633ec
4
- data.tar.gz: 4944f376912c69fc937856c81630996eeaa2e1504f94dd5ea06d0d8ab8d0ff66
3
+ metadata.gz: 5ef7a8a909bb86688882fc9ffca3c7fd96955d75cb01e5048efa954909599c49
4
+ data.tar.gz: 90d58f5f3e6f976b2332341e6f5624064a5b70f7ce5381d4df94b082d7398ab7
5
5
  SHA512:
6
- metadata.gz: a2fd955d35142f9d765d8059cb3a793f4453eab7dfefb171c4223a0b88ad59d520e653a3841c86bbfa9ecc193f9e8b85549176ab4ff559b673cfe4393a7aa519
7
- data.tar.gz: bfeeee234144ba6b45cb7bd39495533c7c18c998d1c44e9c4ce6ac17d4a2b95403f28de6f0d25d07a49d5a2f413689cb8f4a2ee2aa59e5716f443daff80c5a0a
6
+ metadata.gz: ab5e3feccaee75a608ba7032721bd899e74e9059bfee3704ebc45b1f1733a5341eb2e1f1f99be48bdc2b0eee4555aaf8985e1c84a2c673474dbc4ae018abf6b7
7
+ data.tar.gz: 61b3a978d40a10d91f725ffd3ebc7b937596fa60740fb9e3d168cbba6f19fc2a50f27af56a637612c4980b1e4408dcae61e2148fff6b819026ed35811fbdcf6c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## 0.3.7 - 2026-05-12
4
17
 
5
18
  - Use `Legion::Logging::Helper` explicitly across Bedrock provider, actor, and fleet runner logging surfaces.
@@ -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)
@@ -194,7 +230,7 @@ module Legion
194
230
  log.debug { "bedrock.provider.count_tokens: model=#{model_id(model)}" }
195
231
  request = Utils.deep_merge(
196
232
  {
197
- model_id: model_id(model),
233
+ model_id: self.class.inference_profile_id(model_id(model)),
198
234
  input: { converse: { messages: format_messages(messages), system: system_blocks(system) }.compact }
199
235
  },
200
236
  params
@@ -283,6 +319,7 @@ module Legion
283
319
 
284
320
  def build_offering(model:, model_family:, usage_type:, instance_id: :default, alias_name: nil,
285
321
  capabilities: nil, metadata: {})
322
+ limits = infer_limits(model)
286
323
  Legion::Extensions::Llm::Routing::ModelOffering.new(
287
324
  provider_family: :bedrock,
288
325
  instance_id: instance_id,
@@ -291,10 +328,24 @@ module Legion
291
328
  model: model,
292
329
  usage_type: usage_type,
293
330
  capabilities: capabilities || default_capabilities(model),
331
+ limits: limits,
294
332
  metadata: metadata.merge(model_family: model_family, alias: alias_name).compact
295
333
  )
296
334
  end
297
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
+
298
349
  def configured_transport(default)
299
350
  config.respond_to?(:transport) ? config.transport : default
300
351
  end
@@ -305,7 +356,7 @@ module Legion
305
356
 
306
357
  def converse_request(messages, model:, temperature:, max_tokens:, tools:, tool_prefs:)
307
358
  {
308
- model_id: model_id(model),
359
+ model_id: self.class.inference_profile_id(model_id(model)),
309
360
  messages: format_messages(messages.reject { |message| message.role == :system }),
310
361
  system: format_system(messages),
311
362
  inference_config: { temperature: temperature, max_tokens: max_tokens || model_max_tokens(model) }.compact,
@@ -314,11 +365,11 @@ module Legion
314
365
  end
315
366
 
316
367
  def format_messages(messages)
317
- messages.map do |message|
318
- {
319
- role: bedrock_role(message.role),
320
- content: content_blocks(message.content)
321
- }
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 }
322
373
  end
323
374
  end
324
375
 
@@ -339,7 +390,13 @@ module Legion
339
390
  end
340
391
 
341
392
  def content_blocks(content)
342
- 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 }]
343
400
  end
344
401
 
345
402
  def raw_content(content)
@@ -477,12 +534,23 @@ module Legion
477
534
  end
478
535
 
479
536
  def client_options
480
- {
537
+ opts = {
481
538
  region: region,
482
539
  endpoint: config.bedrock_endpoint,
483
- credentials: credentials,
484
540
  stub_responses: config.bedrock_stub_responses
485
- }.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?
486
554
  end
487
555
 
488
556
  def credentials
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Bedrock
7
- VERSION = '0.3.7'
7
+ VERSION = '0.3.8'
8
8
  end
9
9
  end
10
10
  end
@@ -59,6 +59,8 @@ module Legion
59
59
  @registry_publisher ||= Legion::Extensions::Llm::RegistryPublisher.new(provider_family: PROVIDER_FAMILY)
60
60
  end
61
61
 
62
+ DEFAULT_CAPABILITIES = %i[completion streaming embedding].freeze
63
+
62
64
  def self.discover_instances
63
65
  candidates = {}
64
66
  discover_env_bearer(candidates)
@@ -66,7 +68,23 @@ module Legion
66
68
  discover_env_sigv4(candidates)
67
69
  discover_settings(candidates)
68
70
  discover_broker(candidates)
69
- 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)://})
70
88
  end
71
89
 
72
90
  def self.discover_env_bearer(candidates)
@@ -76,7 +94,9 @@ module Legion
76
94
  candidates[:env_bearer] = {
77
95
  bearer_token: bearer,
78
96
  bedrock_region: CredentialSources.env('AWS_DEFAULT_REGION') || DEFAULT_REGION,
79
- tier: :cloud
97
+ tier: :cloud,
98
+ source: CredentialSources.source_tag(:env, 'AWS_BEARER_TOKEN_BEDROCK'),
99
+ credential_fingerprint: CredentialSources.credential_fingerprint(bearer)
80
100
  }
81
101
  end
82
102
 
@@ -88,7 +108,9 @@ module Legion
88
108
  candidates[:claude] = {
89
109
  bearer_token: claude_bearer,
90
110
  bedrock_region: CredentialSources.claude_env_value('AWS_DEFAULT_REGION') || DEFAULT_REGION,
91
- 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)
92
114
  }
93
115
  end
94
116
 
@@ -100,7 +122,10 @@ module Legion
100
122
  candidates[:env_sigv4] = {
101
123
  api_key: akid, bedrock_access_key_id: akid, bedrock_secret_access_key: skey,
102
124
  bedrock_session_token: CredentialSources.env('AWS_SESSION_TOKEN'),
103
- 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)
104
129
  }.compact
105
130
  end
106
131
 
@@ -109,12 +134,19 @@ module Legion
109
134
  return unless settings.is_a?(Hash) && !settings.empty?
110
135
 
111
136
  default_config = dedup_config(normalize_instance_config(settings))
112
- 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
113
142
 
114
143
  settings_instances(settings).each do |name, config|
115
144
  next unless config.is_a?(Hash)
116
145
 
117
- 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)
118
150
  end
119
151
  end
120
152
 
@@ -122,7 +154,11 @@ module Legion
122
154
  return unless defined?(Legion::Identity::Broker)
123
155
 
124
156
  broker_creds = broker_aws_credentials
125
- 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)
126
162
  end
127
163
 
128
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.7
4
+ version: 0.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO