lex-llm-anthropic 0.2.17 → 0.2.21

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: d0c7cf1313510c485ef6ee26fffcd14bec94d99d7d10b38a9857e9635c5cf717
4
- data.tar.gz: e0e3623a3199753ae17b554c3671d41572320d55348e4af8ce04634ebc9e360d
3
+ metadata.gz: 03b981bffa993167ee00d0bb4b122f4a92c55ed978a082d35ee3fb1237afba49
4
+ data.tar.gz: 9c3d7c381087b3eab19acdb504b85670c97833aff725c604c1551cb109fcf677
5
5
  SHA512:
6
- metadata.gz: 6481188c16aca43c757a5d39a044186ff27da95b90b7691048ba6164e6830bb2d2f94a32a968aff02362406402c6cb24f9b831cbb51a4b1b74f3a82310f40462
7
- data.tar.gz: 83a1479648dfdd1c47ed50af21285f96030e60f19bc7b70cc02c5f781e3cc898393aeed6c08ee27a6244e90fff916ef03ceba6c1c0b023d0f426f273c40b9951
6
+ metadata.gz: 7dab8f8bd7972ea48ce388f3fe72688d21ad2a7c960fef5112673f71bbf2f356aa233a5e00d73c99e766e8ae1b6003a86e1afb258f31cbc153a4c861dcb60f06
7
+ data.tar.gz: 1e1eed5d32c804b2a030ead997aa1a1be86692e13333842a0ad6517a78e94a1307fa919e2791d0c3b2a917778735deb0b35f63638ffcc486d806709eb091c217
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.21 - 2026-06-17
4
+
5
+ ### Changed
6
+ - **Policy-aware default model** — `default_model` is no longer a hardcoded literal forced via `||=`. The `claude-sonnet-4-6` fallback is now a named `DEFAULT_MODEL` constant applied through `Provider.policy_safe_default_model`, so a configured `model_whitelist`/`model_blacklist` is never overridden: if neither the configured default nor the fallback is permitted, `default_model` is left unset and routing resolves an allowed discovered model instead. (Fixes the case where a haiku-only whitelist still surfaced a sonnet default.) Requires lex-llm >= 0.5.4.
7
+
8
+ ## 0.2.20 - 2026-06-16
9
+
10
+ - dependency updates, code quality improvements
11
+
12
+ ## 0.2.19 - 2026-06-15
13
+
14
+ - **CapabilityPolicy integration** — Streaming and tools from `:provider_envelope`; vision/thinking default false unless explicitly enabled via settings. Settings overrides at provider/instance/model level supported.
15
+
16
+ ## 0.2.18 - 2026-06-13
17
+
18
+ - **Gemfile cleanup** — Remove local path overrides; all dependencies resolve from gemspec via rubygems.
19
+ - 135 examples, 0 failures; 20 files, 0 rubocop offenses.
20
+
3
21
  ## 0.2.17 - 2026-06-10
4
22
 
5
23
  - **Canonical provider translator (Phase 3)** — New `Translator` class implementing the Anthropic↔canonical boundary per N×N routing design. Public interface: `render_request(canonical_request)`, `parse_response(wire)`, `parse_chunk(raw)`, `capabilities`. Extracted from existing `Provider` render/parse methods — behaviour preserved, not rewritten (translator.rb).
data/Gemfile CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- transport_path = ENV.fetch('LEGION_TRANSPORT_PATH', File.expand_path('../../legion-transport', __dir__))
6
- gem 'legion-transport', path: transport_path if File.directory?(transport_path)
7
-
8
5
  gemspec
9
6
 
10
7
  group :development do
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency 'legion-logging', '>= 1.3.2'
28
28
  spec.add_dependency 'legion-settings', '>= 1.3.14'
29
29
  spec.add_dependency 'legion-transport', '>= 1.4.14'
30
- spec.add_dependency 'lex-llm', '>= 0.5.0'
30
+ spec.add_dependency 'lex-llm', '>= 0.5.4'
31
31
  end
@@ -36,6 +36,9 @@ module Legion
36
36
  return unless defined?(Legion::LLM::Discovery)
37
37
 
38
38
  Legion::LLM::Discovery.refresh_discovered_models!(provider: :anthropic)
39
+
40
+ Legion::LLM::Router.populate_auto_rules(Legion::LLM::Discovery.discovered_instances) if defined?(Legion::LLM::Router) && Legion::LLM::Router.respond_to?(:populate_auto_rules)
41
+ Legion::LLM::Inventory.invalidate_offerings_cache! if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
39
42
  rescue StandardError => e
40
43
  handle_exception(e, level: :warn, handled: true, operation: 'anthropic.actor.discovery_refresh')
41
44
  end
@@ -20,7 +20,7 @@ module Legion
20
20
  def capabilities = Capabilities
21
21
 
22
22
  def registry_publisher
23
- @registry_publisher ||= RegistryPublisher.new
23
+ @registry_publisher ||= Legion::Extensions::Llm::RegistryPublisher.new(provider_family: :anthropic)
24
24
  end
25
25
  end
26
26
 
@@ -54,6 +54,10 @@ module Legion
54
54
  def stream_url = completion_url
55
55
  def models_url = '/v1/models'
56
56
 
57
+ def translator
58
+ @translator ||= Translator.new(config)
59
+ end
60
+
57
61
  def embed(**_provider_options)
58
62
  raise NotImplementedError, 'Anthropic does not expose embeddings through this provider'
59
63
  end
@@ -76,6 +80,8 @@ module Legion
76
80
  'claude-3-haiku' => 200_000
77
81
  }.freeze
78
82
 
83
+ COMPLETION_BASE = [:completion].freeze
84
+
79
85
  private
80
86
 
81
87
  def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:)
@@ -186,7 +192,11 @@ module Legion
186
192
 
187
193
  def format_tool_call_message(message, thinking:, cache:)
188
194
  blocks = content_blocks(message.content, thinking:, message:, cache:)
189
- message.tool_calls.each_value { |tool_call| blocks << tool_use_block(tool_call, cache:) }
195
+ # tool_calls is an Array of ToolCall since the adapter stopped
196
+ # name-keying them (name-keyed hashes silently dropped parallel
197
+ # same-name calls); tolerate the legacy Hash shape from old callers.
198
+ calls = message.tool_calls.is_a?(Hash) ? message.tool_calls.values : Array(message.tool_calls)
199
+ calls.each { |tool_call| blocks << tool_use_block(tool_call, cache:) }
190
200
  { role: 'assistant', content: blocks }
191
201
  end
192
202
 
@@ -274,8 +284,8 @@ module Legion
274
284
  def tool_schema(tool)
275
285
  return tool.params_schema if tool.respond_to?(:params_schema) && tool.params_schema
276
286
 
277
- { type: 'object',
278
- properties: tool.respond_to?(:parameters) ? tool.parameters : (tool[:parameters] || tool['parameters'] || {}), required: [] }
287
+ raw = tool.respond_to?(:parameters) ? tool.parameters : (tool[:parameters] || tool['parameters'])
288
+ Legion::Extensions::Llm::Canonical::ToolDefinition.normalize_parameters(raw)
279
289
  end
280
290
 
281
291
  def tool_choice(tool_prefs)
@@ -318,116 +328,124 @@ module Legion
318
328
 
319
329
  def parse_completion_response(response)
320
330
  body = response.body
321
- content_blocks = body['content'] || []
322
- usage = body['usage'] || {}
323
-
324
- Legion::Extensions::Llm::Message.new(
325
- role: :assistant,
326
- content: text_from(content_blocks),
327
- model_id: body['model'],
328
- thinking: thinking_from(content_blocks),
329
- tool_calls: parse_tool_calls(content_blocks),
330
- input_tokens: usage['input_tokens'],
331
- output_tokens: usage['output_tokens'],
332
- cached_tokens: usage['cache_read_input_tokens'],
333
- cache_creation_tokens: cache_creation_tokens(usage),
334
- thinking_tokens: thinking_tokens(usage),
335
- raw: body
336
- )
331
+ canonical = translator.parse_response(body)
332
+ to_legacy_message(canonical, body)
337
333
  end
338
334
 
339
- def text_from(blocks)
340
- blocks.select { |block| block['type'] == 'text' }.map { |block| block['text'] }.join
341
- end
342
-
343
- def thinking_from(blocks)
344
- thinking_block = blocks.find { |block| block['type'] == 'thinking' }
345
- redacted_block = blocks.find { |block| block['type'] == 'redacted_thinking' }
346
-
347
- Legion::Extensions::Llm::Thinking.build(
348
- text: thinking_block&.dig('thinking') || thinking_block&.dig('text'),
349
- signature: thinking_block&.dig('signature') || redacted_block&.dig('data')
350
- )
351
- end
352
-
353
- def cache_creation_tokens(usage)
354
- cache_creation = usage['cache_creation']
355
- cache_creation_values = cache_creation.values if cache_creation
335
+ def build_chunk(data)
336
+ canonical_chunk = translator.parse_chunk(data)
337
+ return nil if canonical_chunk.nil?
356
338
 
357
- usage['cache_creation_input_tokens'] || cache_creation_values&.compact&.sum
339
+ to_legacy_chunk(canonical_chunk, data)
358
340
  end
359
341
 
360
- def thinking_tokens(usage)
361
- usage.dig('output_tokens_details', 'thinking_tokens') ||
362
- usage.dig('output_tokens_details', 'reasoning_tokens') ||
363
- usage['thinking_tokens'] ||
364
- usage['reasoning_tokens']
342
+ def to_legacy_message(canonical, raw_body)
343
+ usage = canonical.usage
344
+ Legion::Extensions::Llm::Message.new(
345
+ role: :assistant,
346
+ content: canonical.text,
347
+ model_id: canonical.model,
348
+ thinking: if canonical.thinking
349
+ Legion::Extensions::Llm::Thinking.build(
350
+ text: canonical.thinking.content,
351
+ signature: canonical.thinking.signature
352
+ )
353
+ end,
354
+ tool_calls: legacy_tool_calls(canonical.tool_calls),
355
+ input_tokens: usage&.input_tokens,
356
+ output_tokens: usage&.output_tokens,
357
+ cached_tokens: usage&.cache_read_tokens,
358
+ cache_creation_tokens: usage&.cache_write_tokens,
359
+ thinking_tokens: usage&.thinking_tokens,
360
+ raw: raw_body
361
+ )
365
362
  end
366
363
 
367
- def build_chunk(data)
368
- delta_type = data.dig('delta', 'type')
369
-
364
+ def to_legacy_chunk(canonical_chunk, raw_data)
370
365
  Legion::Extensions::Llm::Chunk.new(
371
366
  role: :assistant,
372
- content: delta_type == 'text_delta' ? data.dig('delta', 'text') : nil,
373
- model_id: data.dig('message', 'model'),
374
- thinking: Legion::Extensions::Llm::Thinking.build(
375
- text: delta_type == 'thinking_delta' ? data.dig('delta', 'thinking') : nil,
376
- signature: delta_type == 'signature_delta' ? data.dig('delta', 'signature') : nil
377
- ),
378
- input_tokens: data.dig('message', 'usage', 'input_tokens'),
379
- output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens'),
380
- tool_calls: extract_streaming_tool_calls(data, delta_type)
367
+ content: canonical_chunk.text_delta? ? canonical_chunk.delta : nil,
368
+ model_id: raw_data.dig('message', 'model'),
369
+ thinking: if canonical_chunk.thinking_delta?
370
+ Legion::Extensions::Llm::Thinking.build(
371
+ text: canonical_chunk.delta,
372
+ signature: canonical_chunk.signature
373
+ )
374
+ end,
375
+ input_tokens: canonical_chunk.usage&.input_tokens,
376
+ output_tokens: canonical_chunk.usage&.output_tokens,
377
+ tool_calls: legacy_streaming_tool_calls(canonical_chunk)
381
378
  )
382
379
  end
383
380
 
384
- def extract_streaming_tool_calls(data, _delta_type)
385
- content_block = data['content_block']
386
- return nil unless content_block && content_block['type'] == 'tool_use'
381
+ def legacy_tool_calls(canonical_tool_calls)
382
+ return nil if canonical_tool_calls.nil? || canonical_tool_calls.empty?
387
383
 
388
- { content_block['id'] => Legion::Extensions::Llm::ToolCall.new(
389
- id: content_block['id'], name: content_block['name'], arguments: ''
390
- ) }
391
- end
392
-
393
- def parse_tool_calls(content_blocks)
394
- blocks = Array(content_blocks).select { |block| block && block['type'] == 'tool_use' }
395
- return nil if blocks.empty?
396
-
397
- blocks.to_h do |block|
384
+ canonical_tool_calls.to_h do |tc|
398
385
  [
399
- block['id'],
386
+ tc.id,
400
387
  Legion::Extensions::Llm::ToolCall.new(
401
- id: block['id'],
402
- name: block['name'],
403
- arguments: block['input'] || {}
388
+ id: tc.id, name: tc.name, arguments: tc.arguments || {}
404
389
  )
405
390
  ]
406
391
  end
407
392
  end
408
393
 
394
+ def legacy_streaming_tool_calls(canonical_chunk)
395
+ return nil unless canonical_chunk.tool_call_delta?
396
+
397
+ tc = canonical_chunk.tool_call
398
+ return nil unless tc
399
+
400
+ { tc.id => Legion::Extensions::Llm::ToolCall.new(
401
+ id: tc.id, name: tc.name, arguments: tc.arguments || ''
402
+ ) }
403
+ end
404
+
409
405
  def parse_list_models_response(response, provider, _capabilities)
410
406
  Array(response.body['data']).map do |model|
411
407
  model_id = model.fetch('id')
412
408
  detail = model_detail(model_id)
413
409
  ctx = detail&.dig(:context_window) || infer_context_window(model_id)
410
+ resolved = resolve_model_capabilities(model_id)
414
411
  Legion::Extensions::Llm::Model::Info.new(
415
412
  id: model_id,
416
413
  name: model['display_name'] || model_id,
417
414
  provider: provider,
418
- capabilities: %i[completion streaming tools],
415
+ capabilities: COMPLETION_BASE + resolved[:capabilities],
419
416
  context_length: ctx,
420
417
  metadata: model.merge('created_at' => model['created_at']).compact
421
418
  )
422
419
  end
423
420
  end
424
421
 
425
- def infer_context_window(model_id)
426
- CONTEXT_WINDOWS.find { |prefix, _| model_id.start_with?(prefix) }&.last
422
+ def resolve_model_capabilities(model_id)
423
+ provider_settings = CredentialSources.setting(:extensions, :llm, :anthropic)
424
+ provider_cfg = provider_settings.is_a?(Hash) ? provider_settings.except(:instances) : {}
425
+ model_cfg = model_config_for(model_id, provider_settings)
426
+
427
+ Legion::Extensions::Llm::CapabilityPolicy.resolve(
428
+ real: {},
429
+ provider_catalog: {},
430
+ probe: {},
431
+ provider_envelope: { streaming: true, tools: true },
432
+ provider_config: provider_cfg,
433
+ instance_config: config.respond_to?(:to_h) ? config.to_h : {},
434
+ model_config: model_cfg
435
+ )
427
436
  end
428
437
 
429
- def model_detail(model_name)
430
- fetch_model_detail(model_name)
438
+ def model_config_for(model_id, provider_settings)
439
+ return {} unless provider_settings.is_a?(Hash)
440
+
441
+ models = provider_settings[:models] || provider_settings['models']
442
+ return {} unless models.is_a?(Hash)
443
+
444
+ models[model_id.to_sym] || models[model_id] || {}
445
+ end
446
+
447
+ def infer_context_window(model_id)
448
+ CONTEXT_WINDOWS.find { |prefix, _| model_id.start_with?(prefix) }&.last
431
449
  end
432
450
 
433
451
  def fetch_model_detail(model_name)
@@ -106,54 +106,68 @@ module Legion
106
106
  )
107
107
  end
108
108
 
109
- # Parse chunk: raw streaming event to Canonical::Chunk.
109
+ # Parse chunk: raw Anthropic SSE event to Canonical::Chunk.
110
+ # Real Anthropic events use top-level types like content_block_delta, message_delta,
111
+ # content_block_start; the delta kind is nested inside delta.type.
110
112
  def parse_chunk(raw)
111
113
  return nil unless raw.is_a?(Hash) && (raw.key?(:type) || raw.key?('type'))
112
114
 
113
- type = raw[:type] || raw['type']
115
+ type = (raw[:type] || raw['type']).to_s
116
+ delta = raw[:delta] || raw['delta'] || {}
117
+ delta = {} unless delta.is_a?(Hash)
118
+ delta_type = (delta[:type] || delta['type']).to_s
114
119
 
115
120
  case type
116
- when 'text_delta', :text_delta
121
+ when 'content_block_delta'
122
+ parse_content_block_delta(raw, delta, delta_type)
123
+ when 'content_block_start'
124
+ parse_content_block_start(raw)
125
+ when 'message_start'
126
+ parse_message_start(raw)
127
+ when 'message_delta'
128
+ parse_message_delta(raw, delta)
129
+ when 'message_stop'
130
+ Canonical::Chunk.done(
131
+ request_id: raw[:request_id] || '',
132
+ stop_reason: map_stop_reason(delta[:stop_reason] || delta['stop_reason'])
133
+ )
134
+ when 'text_delta'
117
135
  Canonical::Chunk.text_delta(
118
136
  delta: extract_delta(raw, 'text_delta'),
119
137
  request_id: raw[:request_id],
120
- block_index: raw[:block_index]
138
+ block_index: raw[:block_index] || raw['index']
121
139
  )
122
- when 'thinking_delta', :thinking_delta
123
- delta_obj = raw[:delta] || raw['delta']
124
- sig_from_delta = (delta_obj[:signature] || delta_obj['signature'] if delta_obj.is_a?(Hash))
125
-
140
+ when 'thinking_delta'
141
+ sig_from_delta = (delta[:signature] || delta['signature'] if delta.any?)
126
142
  Canonical::Chunk.thinking_delta(
127
143
  delta: extract_delta(raw, 'thinking_delta'),
128
144
  request_id: raw[:request_id],
129
- block_index: raw[:block_index],
145
+ block_index: raw[:block_index] || raw['index'],
130
146
  signature: raw[:signature] || raw['signature'] || sig_from_delta
131
147
  )
132
- when 'tool_call_delta', :tool_call_delta
148
+ when 'tool_call_delta'
133
149
  tc = extract_tool_call_from_chunk(raw)
134
150
  return nil unless tc
135
151
 
136
152
  Canonical::Chunk.tool_call_delta(
137
153
  tool_call: tc,
138
154
  request_id: raw[:request_id],
139
- block_index: raw[:block_index]
155
+ block_index: raw[:block_index] || raw['index']
140
156
  )
141
- when 'error', :error
157
+ when 'error'
142
158
  Canonical::Chunk.error_chunk(
143
159
  error: raw[:error] || raw['error'] || 'unknown',
144
160
  request_id: raw[:request_id] || '',
145
161
  metadata: raw[:metadata] || raw['metadata'] || {}
146
162
  )
147
- when 'done', :done
163
+ when 'done'
148
164
  usage = (Canonical::Usage.from_hash(raw[:usage] || raw['usage'] || {}) if raw[:usage] || raw['usage'])
149
-
150
165
  Canonical::Chunk.done(
151
166
  request_id: raw[:request_id] || '',
152
167
  usage: usage,
153
168
  stop_reason: map_stop_reason(raw[:stop_reason] || raw['stop_reason'])
154
169
  )
155
170
  else
156
- # Per G20d: ignore unknown chunk types on consume
157
171
  log.debug("[anthropic translator] ignoring unknown chunk type: #{type.inspect}")
158
172
  nil
159
173
  end
@@ -233,43 +247,48 @@ module Legion
233
247
  end
234
248
 
235
249
  def content_block_to_wire(block)
236
- case block.type
237
- when :thinking
238
- { type: 'thinking', thinking: block.text || '' }
239
- when :tool_use
240
- { type: 'tool_use', id: block.id, name: block.name, input: block.input || {} }
241
- when :tool_result
242
- { type: 'tool_result', tool_use_id: block.tool_use_id,
243
- content: [{ type: 'text', text: block.text || '' }] }
244
- when :image
245
- { type: 'image', source: { type: block.source_type || 'base64',
246
- media_type: block.media_type, data: block.data } }
247
- else
248
- { type: 'text', text: block.text || '' }
249
- end
250
+ wire = case block.type
251
+ when :thinking
252
+ { type: 'thinking', thinking: block.text || '' }
253
+ when :tool_use
254
+ { type: 'tool_use', id: block.id, name: block.name, input: block.input || {} }
255
+ when :tool_result
256
+ { type: 'tool_result', tool_use_id: block.tool_use_id,
257
+ content: [{ type: 'text', text: block.text || '' }] }
258
+ when :image
259
+ { type: 'image', source: { type: block.source_type || 'base64',
260
+ media_type: block.media_type, data: block.data } }
261
+ else
262
+ { type: 'text', text: block.text || '' }
263
+ end
264
+ wire[:cache_control] = block.cache_control if block.cache_control
265
+ wire
250
266
  end
251
267
 
252
268
  def hash_block_to_wire(block)
253
269
  block_type = block[:type] || block['type']
254
-
255
- case block_type
256
- when 'image'
257
- { type: 'image', source: block[:source] || block['source'] || {} }
258
- when 'tool_result'
259
- {
260
- type: 'tool_result',
261
- tool_use_id: block[:tool_use_id] || block['tool_use_id'],
262
- content: Array(block[:content] || block['content']).map do |item|
263
- if item.is_a?(Hash)
264
- { type: 'text', text: item[:text] || item['text'] || '' }
265
- else
266
- { type: 'text', text: item.to_s }
267
- end
268
- end
269
- }
270
- else
271
- block
272
- end
270
+ cc = block[:cache_control] || block['cache_control']
271
+
272
+ wire = case block_type
273
+ when 'image'
274
+ { type: 'image', source: block[:source] || block['source'] || {} }
275
+ when 'tool_result'
276
+ {
277
+ type: 'tool_result',
278
+ tool_use_id: block[:tool_use_id] || block['tool_use_id'],
279
+ content: Array(block[:content] || block['content']).map do |item|
280
+ if item.is_a?(Hash)
281
+ { type: 'text', text: item[:text] || item['text'] || '' }
282
+ else
283
+ { type: 'text', text: item.to_s }
284
+ end
285
+ end
286
+ }
287
+ else
288
+ return block
289
+ end
290
+ wire[:cache_control] = cc if cc
291
+ wire
273
292
  end
274
293
 
275
294
  # --- system content ---
@@ -303,12 +322,12 @@ module Legion
303
322
  name = tool.is_a?(Canonical::ToolDefinition) ? tool.name : (tool[:name] || tool['name'])
304
323
  desc = tool.is_a?(Canonical::ToolDefinition) ? tool.description : (tool[:description] || tool['description'] || '')
305
324
  params = if tool.is_a?(Canonical::ToolDefinition)
306
- tool.parameters || {}
325
+ tool.parameters
307
326
  else
308
- tool[:parameters] || tool['parameters'] || {}
327
+ Canonical::ToolDefinition.normalize_parameters(tool[:parameters] || tool['parameters'])
309
328
  end
310
329
 
311
- { name: name, description: desc, input_schema: { type: 'object', properties: params } }
330
+ { name: name, description: desc, input_schema: params }
312
331
  end
313
332
  end
314
333
 
@@ -484,6 +503,91 @@ module Legion
484
503
 
485
504
  # --- chunk parsing ---
486
505
 
506
+ # --- Anthropic wire-format SSE event parsers ---
507
+
508
+ def parse_content_block_delta(raw, delta, delta_type)
509
+ index = raw[:index] || raw['index']
510
+ case delta_type
511
+ when 'text_delta'
512
+ Canonical::Chunk.text_delta(
513
+ delta: delta[:text] || delta['text'] || '',
514
+ request_id: raw[:request_id],
515
+ block_index: index
516
+ )
517
+ when 'thinking_delta'
518
+ Canonical::Chunk.thinking_delta(
519
+ delta: delta[:thinking] || delta['thinking'] || '',
520
+ request_id: raw[:request_id],
521
+ block_index: index
522
+ )
523
+ when 'signature_delta'
524
+ Canonical::Chunk.thinking_delta(
525
+ delta: '',
526
+ request_id: raw[:request_id],
527
+ block_index: index,
528
+ signature: delta[:signature] || delta['signature']
529
+ )
530
+ when 'input_json_delta'
531
+ tc = Canonical::ToolCall.new(
532
+ id: nil, exchange_id: nil, name: nil, source: nil,
533
+ arguments: delta[:partial_json] || delta['partial_json'] || '',
534
+ status: nil, duration_ms: nil, result: nil, error: nil,
535
+ started_at: nil, finished_at: nil, category: nil,
536
+ data_handling_classification: nil, policy_decision: nil
537
+ )
538
+ Canonical::Chunk.tool_call_delta(
539
+ tool_call: tc,
540
+ request_id: raw[:request_id],
541
+ block_index: index
542
+ )
543
+ else
544
+ log.debug("[anthropic translator] ignoring content_block_delta delta_type: #{delta_type}")
545
+ nil
546
+ end
547
+ end
548
+
549
+ def parse_content_block_start(raw)
550
+ content_block = raw[:content_block] || raw['content_block'] || {}
551
+ return nil unless content_block.is_a?(Hash)
552
+
553
+ block_type = content_block[:type] || content_block['type']
554
+ return nil unless block_type == 'tool_use'
555
+
556
+ tc = Canonical::ToolCall.build(
557
+ id: content_block[:id] || content_block['id'],
558
+ name: content_block[:name] || content_block['name']
559
+ )
560
+ Canonical::Chunk.tool_call_delta(
561
+ tool_call: tc,
562
+ request_id: raw[:request_id],
563
+ block_index: raw[:index] || raw['index']
564
+ )
565
+ end
566
+
567
+ def parse_message_start(raw)
568
+ message = raw[:message] || raw['message'] || {}
569
+ message = {} unless message.is_a?(Hash)
570
+ usage_raw = message[:usage] || message['usage']
571
+ usage = Canonical::Usage.from_hash(usage_raw) if usage_raw.is_a?(Hash) && usage_raw.any?
572
+
573
+ Canonical::Chunk.usage_chunk(
574
+ usage: usage,
575
+ request_id: raw[:request_id] || ''
576
+ )
577
+ end
578
+
579
+ def parse_message_delta(raw, delta)
580
+ usage_raw = raw[:usage] || raw['usage']
581
+ usage = Canonical::Usage.from_hash(usage_raw) if usage_raw.is_a?(Hash) && usage_raw.any?
582
+ stop_reason = delta[:stop_reason] || delta['stop_reason']
583
+
584
+ Canonical::Chunk.done(
585
+ request_id: raw[:request_id] || '',
586
+ usage: usage,
587
+ stop_reason: map_stop_reason(stop_reason)
588
+ )
589
+ end
590
+
487
591
  def extract_delta(raw, _type)
488
592
  delta_val = raw[:delta] || raw['delta']
489
593
  # Canonical form: delta is a plain string (e.g. from conformance fixtures).
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Anthropic
7
- VERSION = '0.2.17'
7
+ VERSION = '0.2.21'
8
8
  end
9
9
  end
10
10
  end
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'legion/extensions/llm'
4
4
  require 'legion/logging/helper'
5
- require 'legion/extensions/llm/anthropic/registry_event_builder'
6
- require 'legion/extensions/llm/anthropic/registry_publisher'
7
5
  require 'legion/extensions/llm/anthropic/provider'
8
6
  require 'legion/extensions/llm/anthropic/translator'
9
7
  require 'legion/extensions/llm/anthropic/version'
8
+ require_relative 'anthropic/actors/discovery_refresh'
10
9
 
11
10
  module Legion
12
11
  module Extensions
@@ -18,12 +17,16 @@ module Legion
18
17
  extend Legion::Extensions::Llm::AutoRegistration
19
18
 
20
19
  PROVIDER_FAMILY = :anthropic
20
+ # Provider's preferred default when the operator configures none. Used only
21
+ # as a fallback and only when the configured model policy permits it
22
+ # (see resolve_default_model) — a whitelist/blacklist is never overridden.
23
+ DEFAULT_MODEL = 'claude-sonnet-4-6'
21
24
 
22
25
  def self.default_settings
23
26
  ::Legion::Extensions::Llm.provider_settings(
24
27
  family: PROVIDER_FAMILY,
25
28
  instance: {
26
- default_model: 'claude-sonnet-4-6',
29
+ default_model: DEFAULT_MODEL,
27
30
  endpoint: 'https://api.anthropic.com',
28
31
  api_version: '2023-10-16',
29
32
  default_max_tokens: 4096,
@@ -35,10 +38,7 @@ module Legion
35
38
  fleet: {
36
39
  enabled: false,
37
40
  respond_to_requests: false,
38
- capabilities: %i[chat stream_chat],
39
- lanes: [],
40
- concurrency: 4,
41
- queue_suffix: nil
41
+ capabilities: %i[chat stream_chat]
42
42
  }
43
43
  }
44
44
  )
@@ -121,11 +121,20 @@ module Legion
121
121
  CredentialSources.dedup_credentials(candidates).transform_values do |config|
122
122
  sanitized = sanitize_instance_config(config)
123
123
  sanitized[:capabilities] ||= %i[completion streaming vision tools].freeze
124
- sanitized[:default_model] ||= 'claude-sonnet-4-6'
124
+ sanitized[:default_model] = resolve_default_model(sanitized)
125
125
  sanitized
126
126
  end
127
127
  end
128
128
 
129
+ # Resolve a default_model that never violates the configured model policy
130
+ # (whitelist/blacklist stays authoritative over the DEFAULT_MODEL fallback).
131
+ def self.resolve_default_model(config)
132
+ provider_class.policy_safe_default_model(
133
+ configured: config[:default_model], fallback: DEFAULT_MODEL,
134
+ **provider_class.model_policy(config, PROVIDER_FAMILY)
135
+ )
136
+ end
137
+
129
138
  def self.settings_instances(config)
130
139
  instances = config[:instances] || config['instances']
131
140
  instances.is_a?(Hash) ? instances : {}
@@ -145,8 +154,7 @@ module Legion
145
154
  config.except(:api_key)
146
155
  end
147
156
 
148
- Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options) if
149
- Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
157
+ Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
150
158
  end
151
159
  end
152
160
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-anthropic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.17
4
+ version: 0.2.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 0.5.0
74
+ version: 0.5.4
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 0.5.0
81
+ version: 0.5.4
82
82
  description: Anthropic provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com