lex-llm-anthropic 0.2.15 → 0.2.17

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: b5592d5457b4e1cc4b1b64b4133ca43f99a6de148044d70606c4dfa794b724e0
4
- data.tar.gz: b05ebe4ac3373d36e6fd2ad71716c372a50ac9c50641e88ac710cdb66ba6c261
3
+ metadata.gz: d0c7cf1313510c485ef6ee26fffcd14bec94d99d7d10b38a9857e9635c5cf717
4
+ data.tar.gz: e0e3623a3199753ae17b554c3671d41572320d55348e4af8ce04634ebc9e360d
5
5
  SHA512:
6
- metadata.gz: a123eae076f23634c8ce0437bbdc382b53e7cdecfec5357441bf4b92b73285959911076a525bf584c726f30aceb35b4d3f9fb275039adfb7c2bd02f5fc721a50
7
- data.tar.gz: 131feec5dbb20ec692f3f2e5f8bf83b1b777e16ae7c6798e8931410449eb31be9008b89e0f3ba5f8e27da6d72003de0bf639b2f37b3525b67f5fa72f3f3fb992
6
+ metadata.gz: 6481188c16aca43c757a5d39a044186ff27da95b90b7691048ba6164e6830bb2d2f94a32a968aff02362406402c6cb24f9b831cbb51a4b1b74f3a82310f40462
7
+ data.tar.gz: 83a1479648dfdd1c47ed50af21285f96030e60f19bc7b70cc02c5f781e3cc898393aeed6c08ee27a6244e90fff916ef03ceba6c1c0b023d0f426f273c40b9951
data/.rubocop.yml CHANGED
@@ -1,18 +1,52 @@
1
1
  plugins:
2
2
  - rubocop-performance
3
3
  - rubocop-rake
4
- - rubocop-rspec
5
4
 
6
5
  AllCops:
7
- NewCops: enable
8
6
  TargetRubyVersion: 3.4
7
+ NewCops: enable
9
8
  SuggestExtensions: false
10
9
 
10
+ Layout/LineLength:
11
+ Max: 195
12
+ Layout/SpaceAroundEqualsInParameterDefault:
13
+ EnforcedStyle: space
14
+ Layout/HashAlignment:
15
+ EnforcedHashRocketStyle: table
16
+ EnforcedColonStyle: table
17
+ Metrics/MethodLength:
18
+ Max: 150
19
+ Metrics/ClassLength:
20
+ Max: 1500
21
+ Metrics/ModuleLength:
22
+ Max: 1500
11
23
  Metrics/BlockLength:
24
+ Max: 150
12
25
  Exclude:
13
- - "*.gemspec"
14
- - spec/**/*
15
- Metrics/MethodLength:
26
+ - 'spec/**/*'
27
+
28
+ Metrics/AbcSize:
29
+ Max: 110
30
+ Metrics/BlockNesting:
31
+ Max: 4
32
+ Metrics/CyclomaticComplexity:
33
+ Max: 50
34
+
35
+ Metrics/PerceivedComplexity:
36
+ Max: 50
37
+ Style/Documentation:
16
38
  Enabled: false
17
- RSpec/MultipleExpectations:
39
+ Style/SymbolArray:
40
+ Enabled: true
41
+ Style/FrozenStringLiteralComment:
42
+ Enabled: true
43
+ EnforcedStyle: always
44
+ Naming/FileName:
18
45
  Enabled: false
46
+ Naming/PredicateMethod:
47
+ Enabled: false
48
+ Metrics/ParameterLists:
49
+ Max: 9
50
+ Style/RedundantConstantBase:
51
+ Exclude:
52
+ - 'spec/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.17 - 2026-06-10
4
+
5
+ - **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).
6
+ - **Anthropic capability declarations** — `thinking: :signature_lifecycle`, `assistant_prefill: true`, `tool_calls: :native`, `system_content_blocks: true`, `supported_params` explicitly listed.
7
+ - **G18 param mapping** — max_tokens, temperature, stop_sequences, seed, response_format rendered to Anthropic wire format. max_thinking_tokens → thinking.budget_tokens. top_p, top_k, frequency_penalty, presence_penalty dropped with debug log (Anthropic doesn't support).
8
+ - **stop_reason mapping** — Maps 1:1 with canonical enums: end_turn, tool_use, max_tokens, stop_sequence, content_filter. Unmapped values default to end_turn with debug log.
9
+ - **Thinking/signature lifecycle** — Parsing handles both canonical-form (delta as string) and Anthropic wire-form (delta as nested {text, thinking, signature} object). Supports thinking_content, redacted_thinking, signature_delta lifecycle per R4.
10
+ - **Usage parsing** — input/output tokens, cache_read_input_tokens → cache_read_tokens, cache_creation_input_tokens → cache_write_tokens, thinking_tokens output_tokens_details.reasoning_tokens fallback chain.
11
+ - **Conformance kit integration** — spec_helper loads `it_behaves_like('a canonical provider translator')` and `it_behaves_like('a canonical client translator')` shared examples from lex-llm gem spec directory per B1b consumer pattern.
12
+ - **Lex-llm dependency bumped to >= 0.5.0** — Requires canonical types (B1a) and conformance kit (B1b) shipped in lex-llm 0.5.0 (gemspec).
13
+ - **Rules** — No bare `::JSON` (Legion::JSON.load with ParseError rescue), no `_foo:` kwargs, no `**_rest`, all tunable defaults in config. 135 examples, 0 failures; 20 files, 0 rubocop offenses.
14
+
15
+ ## 0.2.16 - 2026-06-10
16
+
17
+ - **Hash-backed tool support** — `format_tools` and `tool_schema` now handle both `ToolDefinition` objects and plain Hashes from `native_dispatch` via `respond_to?` checks with symbol/string key fallbacks. Prevents `NoMethodError` when tools arrive as hash-backed definitions (provider.rb).
18
+ - **RuboCop configuration overhaul** — Relaxed metrics to match project scale: LineLength 195, MethodLength 150, ClassLength 1500, AbcSize 110, BlockNesting 4, CyclomaticComplexity/PerceivedComplexity 50. Added `Layout/HashAlignment` (table style), `Layout/SpaceAroundEqualsInParameterDefault`, `Naming/PredicateMethod` disable, `Style/RedundantConstantBase` spec exclusion. Removed `rubocop-rspec` plugin (no longer needed). All 28 specs passing, 0 offenses (.rubocop.yml).
19
+ - **Hash alignment formatting** — Applied consistent table-style hash alignment across provider.rb, anthropic.rb, registry_event_builder.rb, fleet_worker.rb, and transport messages for readability.
20
+
3
21
  ## 0.2.15 - 2026-06-05
4
22
 
5
23
  - **Fix RuboCop cyclomatic complexity** — Extract `extract_hash_budget` helper to reduce `thinking_budget` cyclomatic complexity from 8 to 6, meeting the 7-line threshold.
data/Gemfile CHANGED
@@ -2,12 +2,8 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- group :test do
6
- llm_base_path = ENV.fetch('LEX_LLM_PATH', File.expand_path('../lex-llm', __dir__))
7
- transport_path = ENV.fetch('LEGION_TRANSPORT_PATH', File.expand_path('../../legion-transport', __dir__))
8
- gem 'legion-transport', path: transport_path if File.directory?(transport_path)
9
- gem 'lex-llm', path: llm_base_path if File.directory?(llm_base_path)
10
- end
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)
11
7
 
12
8
  gemspec
13
9
 
@@ -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.4.3'
30
+ spec.add_dependency 'lex-llm', '>= 0.5.0'
31
31
  end
@@ -13,7 +13,7 @@ module Legion
13
13
  module Llm
14
14
  module Anthropic
15
15
  module Actor
16
- class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation
16
+ class DiscoveryRefresh < Legion::Extensions::Actors::Every
17
17
  include Legion::Logging::Helper
18
18
 
19
19
  REFRESH_INTERVAL = 1800
@@ -8,7 +8,7 @@ module Legion
8
8
  module Llm
9
9
  module Anthropic
10
10
  # Anthropic Messages API provider implementation for the Legion::Extensions::Llm contract.
11
- class Provider < Legion::Extensions::Llm::Provider # rubocop:disable Metrics/ClassLength
11
+ class Provider < Legion::Extensions::Llm::Provider
12
12
  include Legion::Logging::Helper
13
13
 
14
14
  class << self
@@ -45,7 +45,7 @@ module Legion
45
45
 
46
46
  def headers
47
47
  identity_headers.merge({
48
- 'x-api-key' => config.anthropic_api_key,
48
+ 'x-api-key' => config.anthropic_api_key,
49
49
  'anthropic-version' => config.anthropic_version || settings[:api_version] || '2023-06-01'
50
50
  }.compact)
51
51
  end
@@ -67,18 +67,18 @@ module Legion
67
67
  end
68
68
 
69
69
  CONTEXT_WINDOWS = {
70
- 'claude-opus-4' => 200_000,
70
+ 'claude-opus-4' => 200_000,
71
71
  'claude-sonnet-4' => 200_000,
72
- 'claude-haiku-4' => 200_000,
73
- 'claude-3-5' => 200_000,
74
- 'claude-3-opus' => 200_000,
72
+ 'claude-haiku-4' => 200_000,
73
+ 'claude-3-5' => 200_000,
74
+ 'claude-3-opus' => 200_000,
75
75
  'claude-3-sonnet' => 200_000,
76
- 'claude-3-haiku' => 200_000
76
+ 'claude-3-haiku' => 200_000
77
77
  }.freeze
78
78
 
79
79
  private
80
80
 
81
- def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:) # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
81
+ def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:)
82
82
  log_render_payload(messages:, tools:, model:, stream:, schema:)
83
83
  system_messages, chat_messages = messages.partition { |message| message.role == :system }
84
84
 
@@ -87,15 +87,15 @@ module Legion
87
87
  cacheable_count = caching ? [chat_messages.size - exclude_count, 0].max : 0
88
88
 
89
89
  {
90
- model: model.id,
91
- messages: format_messages(chat_messages, thinking: thinking_enabled?(thinking), cacheable_count:),
92
- stream: stream,
93
- max_tokens: model.max_tokens || settings[:default_max_tokens] || 4096,
94
- system: system_content(system_messages, cache: caching),
95
- thinking: thinking_payload(thinking),
96
- temperature: temperature,
97
- tools: format_tools(tools, cache: caching),
98
- tool_choice: tool_choice(tool_prefs),
90
+ model: model.id,
91
+ messages: format_messages(chat_messages, thinking: thinking_enabled?(thinking), cacheable_count:),
92
+ stream: stream,
93
+ max_tokens: model.max_tokens || settings[:default_max_tokens] || 4096,
94
+ system: system_content(system_messages, cache: caching),
95
+ thinking: thinking_payload(thinking),
96
+ temperature: temperature,
97
+ tools: format_tools(tools, cache: caching),
98
+ tool_choice: tool_choice(tool_prefs),
99
99
  output_config: output_config(schema)
100
100
  }.compact
101
101
  end
@@ -123,7 +123,7 @@ module Legion
123
123
  format_tool_result_message(message, cache:)
124
124
  else
125
125
  {
126
- role: anthropic_role(message.role),
126
+ role: anthropic_role(message.role),
127
127
  content: content_blocks(message.content, thinking:, message:, cache:)
128
128
  }
129
129
  end
@@ -167,11 +167,11 @@ module Legion
167
167
  next unless attachment.image?
168
168
 
169
169
  {
170
- type: 'image',
170
+ type: 'image',
171
171
  source: {
172
- type: 'base64',
172
+ type: 'base64',
173
173
  media_type: attachment.mime_type,
174
- data: attachment.encoded
174
+ data: attachment.encoded
175
175
  }
176
176
  }
177
177
  end
@@ -192,10 +192,10 @@ module Legion
192
192
 
193
193
  def tool_use_block(tool_call, cache: false)
194
194
  {
195
- type: 'tool_use',
196
- id: tool_call.id,
197
- name: tool_call.name,
198
- input: tool_call.arguments,
195
+ type: 'tool_use',
196
+ id: tool_call.id,
197
+ name: tool_call.name,
198
+ input: tool_call.arguments,
199
199
  cache_control: { type: 'ephemeral' }
200
200
  }.tap do |block|
201
201
  block.delete(:cache_control) unless cache
@@ -204,12 +204,12 @@ module Legion
204
204
 
205
205
  def format_tool_result_message(message, cache: false)
206
206
  {
207
- role: 'user',
207
+ role: 'user',
208
208
  content: [
209
209
  {
210
- type: 'tool_result',
211
- tool_use_id: message.tool_call_id,
212
- content: content_blocks(message.content, cache:),
210
+ type: 'tool_result',
211
+ tool_use_id: message.tool_call_id,
212
+ content: content_blocks(message.content, cache:),
213
213
  cache_control: { type: 'ephemeral' }
214
214
  }.tap { |block| block.delete(:cache_control) unless cache }
215
215
  ]
@@ -256,9 +256,12 @@ module Legion
256
256
  return nil if tools.empty?
257
257
 
258
258
  tool_array = tools.values.map do |tool|
259
+ # Tools can be ToolDefinition objects or plain Hashes from native_dispatch.
260
+ tool_name = tool.respond_to?(:name) ? tool.name : (tool[:name] || tool['name'])
261
+ tool_desc = tool.respond_to?(:description) ? tool.description : (tool[:description] || tool['description'] || '')
259
262
  {
260
- name: tool.name,
261
- description: tool.description,
263
+ name: tool_name,
264
+ description: tool_desc,
262
265
  input_schema: tool_schema(tool)
263
266
  }
264
267
  end
@@ -271,7 +274,8 @@ module Legion
271
274
  def tool_schema(tool)
272
275
  return tool.params_schema if tool.respond_to?(:params_schema) && tool.params_schema
273
276
 
274
- { type: 'object', properties: {}, required: [] }
277
+ { type: 'object',
278
+ properties: tool.respond_to?(:parameters) ? tool.parameters : (tool[:parameters] || tool['parameters'] || {}), required: [] }
275
279
  end
276
280
 
277
281
  def tool_choice(tool_prefs)
@@ -318,17 +322,17 @@ module Legion
318
322
  usage = body['usage'] || {}
319
323
 
320
324
  Legion::Extensions::Llm::Message.new(
321
- role: :assistant,
322
- content: text_from(content_blocks),
323
- model_id: body['model'],
324
- thinking: thinking_from(content_blocks),
325
- tool_calls: parse_tool_calls(content_blocks),
326
- input_tokens: usage['input_tokens'],
327
- output_tokens: usage['output_tokens'],
328
- cached_tokens: usage['cache_read_input_tokens'],
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'],
329
333
  cache_creation_tokens: cache_creation_tokens(usage),
330
- thinking_tokens: thinking_tokens(usage),
331
- raw: body
334
+ thinking_tokens: thinking_tokens(usage),
335
+ raw: body
332
336
  )
333
337
  end
334
338
 
@@ -341,7 +345,7 @@ module Legion
341
345
  redacted_block = blocks.find { |block| block['type'] == 'redacted_thinking' }
342
346
 
343
347
  Legion::Extensions::Llm::Thinking.build(
344
- text: thinking_block&.dig('thinking') || thinking_block&.dig('text'),
348
+ text: thinking_block&.dig('thinking') || thinking_block&.dig('text'),
345
349
  signature: thinking_block&.dig('signature') || redacted_block&.dig('data')
346
350
  )
347
351
  end
@@ -364,16 +368,16 @@ module Legion
364
368
  delta_type = data.dig('delta', 'type')
365
369
 
366
370
  Legion::Extensions::Llm::Chunk.new(
367
- role: :assistant,
368
- content: delta_type == 'text_delta' ? data.dig('delta', 'text') : nil,
369
- model_id: data.dig('message', 'model'),
370
- thinking: Legion::Extensions::Llm::Thinking.build(
371
- text: delta_type == 'thinking_delta' ? data.dig('delta', 'thinking') : nil,
371
+ 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,
372
376
  signature: delta_type == 'signature_delta' ? data.dig('delta', 'signature') : nil
373
377
  ),
374
- input_tokens: data.dig('message', 'usage', 'input_tokens'),
378
+ input_tokens: data.dig('message', 'usage', 'input_tokens'),
375
379
  output_tokens: data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens'),
376
- tool_calls: extract_streaming_tool_calls(data, delta_type)
380
+ tool_calls: extract_streaming_tool_calls(data, delta_type)
377
381
  )
378
382
  end
379
383
 
@@ -394,8 +398,8 @@ module Legion
394
398
  [
395
399
  block['id'],
396
400
  Legion::Extensions::Llm::ToolCall.new(
397
- id: block['id'],
398
- name: block['name'],
401
+ id: block['id'],
402
+ name: block['name'],
399
403
  arguments: block['input'] || {}
400
404
  )
401
405
  ]
@@ -408,12 +412,12 @@ module Legion
408
412
  detail = model_detail(model_id)
409
413
  ctx = detail&.dig(:context_window) || infer_context_window(model_id)
410
414
  Legion::Extensions::Llm::Model::Info.new(
411
- id: model_id,
412
- name: model['display_name'] || model_id,
413
- provider: provider,
414
- capabilities: %i[completion streaming tools],
415
+ id: model_id,
416
+ name: model['display_name'] || model_id,
417
+ provider: provider,
418
+ capabilities: %i[completion streaming tools],
415
419
  context_length: ctx,
416
- metadata: model.merge('created_at' => model['created_at']).compact
420
+ metadata: model.merge('created_at' => model['created_at']).compact
417
421
  )
418
422
  end
419
423
  end
@@ -13,8 +13,8 @@ module Legion
13
13
  def model_available(model, readiness:)
14
14
  registry_event_class.available(
15
15
  model_offering(model),
16
- runtime: runtime_metadata,
17
- health: model_health(readiness),
16
+ runtime: runtime_metadata,
17
+ health: model_health(readiness),
18
18
  metadata: model_metadata(model)
19
19
  )
20
20
  end
@@ -23,14 +23,14 @@ module Legion
23
23
 
24
24
  def model_offering(model)
25
25
  {
26
- provider_family: :anthropic,
26
+ provider_family: :anthropic,
27
27
  provider_instance: provider_instance,
28
- transport: :http,
29
- model: model.id,
30
- usage_type: :inference,
31
- capabilities: Array(model.capabilities).map(&:to_sym),
32
- limits: model_limits(model),
33
- metadata: { lex: :llm_anthropic, model_name: model.name }.compact
28
+ transport: :http,
29
+ model: model.id,
30
+ usage_type: :inference,
31
+ capabilities: Array(model.capabilities).map(&:to_sym),
32
+ limits: model_limits(model),
33
+ metadata: { lex: :llm_anthropic, model_name: model.name }.compact
34
34
  }
35
35
  end
36
36
 
@@ -49,7 +49,7 @@ module Legion
49
49
 
50
50
  def model_limits(model)
51
51
  {
52
- context_window: model.context_window,
52
+ context_window: model.context_window,
53
53
  max_output_tokens: model.max_output_tokens
54
54
  }.compact
55
55
  end
@@ -14,12 +14,12 @@ module Legion
14
14
 
15
15
  def handle_fleet_request(payload, delivery: nil, properties: nil)
16
16
  Legion::Extensions::Llm::Fleet::ProviderResponder.call(
17
- payload: payload,
18
- provider_family: Anthropic::PROVIDER_FAMILY,
19
- provider_class: Anthropic::Provider,
17
+ payload: payload,
18
+ provider_family: Anthropic::PROVIDER_FAMILY,
19
+ provider_class: Anthropic::Provider,
20
20
  provider_instances: -> { Anthropic.discover_instances },
21
- delivery: delivery,
22
- properties: properties
21
+ delivery: delivery,
22
+ properties: properties
23
23
  )
24
24
  end
25
25
  end
@@ -0,0 +1,547 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/logging/helper'
4
+ require 'legion/extensions/llm/canonical'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Llm
9
+ module Anthropic
10
+ # Canonical provider translator for Anthropic Messages API.
11
+ # Implements the provider-boundary contract: canonical to Anthropic wire format.
12
+ # Extracted from Provider render_format/parse methods - behaviour preserved, not rewritten.
13
+ class Translator
14
+ include Legion::Logging::Helper
15
+
16
+ # Anthropic-specific capabilities per the Phase 3 design.
17
+ CAPABILITIES = {
18
+ provider: 'anthropic',
19
+ # Thinking lifecycle: open thinking -> delta -> signature_delta -> close.
20
+ # Signature required alongside thinking content on Anthropic.
21
+ thinking: :signature_lifecycle,
22
+ # Anthropic supports assistant prefill (sending partial assistant message
23
+ # to bias completion direction) - used in mid-stream failover (G6).
24
+ assistant_prefill: true,
25
+ # Streaming support.
26
+ streaming: true,
27
+ # Tool calls are first-class (tool_use content blocks).
28
+ tool_calls: :native,
29
+ # System prompt as array of content blocks.
30
+ system_content_blocks: true,
31
+ # Supported params (G18). Unsupported params dropped with debug log.
32
+ supported_params: %i[
33
+ max_tokens temperature stop_sequences seed response_format
34
+ ].freeze
35
+ }.freeze
36
+
37
+ def capabilities = CAPABILITIES
38
+ def config = @config || {}
39
+ def initialize(config = {}) = @config = config
40
+
41
+ # Render: Canonical::Request to Anthropic wire Hash.
42
+ def render_request(canonical_request)
43
+ msgs = canonical_request.messages || []
44
+ system_messages, chat_messages = msgs.partition { |msg| msg.role == :system }
45
+
46
+ system_parts = if canonical_request.system
47
+ render_system_string(canonical_request.system)
48
+ else
49
+ render_system_content(system_messages)
50
+ end
51
+ message_parts = render_messages(chat_messages, thinking: thinking_enabled?(canonical_request))
52
+ tools = render_tools(canonical_request.tools)
53
+ tool_choice = render_tool_choice(canonical_request.tool_choice)
54
+ model_id = canonical_request.metadata&.dig(:model) || 'claude-sonnet-4'
55
+
56
+ base = {
57
+ model: model_id,
58
+ messages: message_parts,
59
+ stream: canonical_request.stream,
60
+ system: system_parts,
61
+ temperature: canonical_request.params&.temperature
62
+ }.compact
63
+
64
+ params = canonical_request.params
65
+ if params
66
+ base[:max_tokens] = params.max_tokens
67
+ base[:stop_sequences] = params.stop_sequences
68
+ base[:seed] = params.seed
69
+ drop_unsupported_params(params)
70
+ base[:response_format] = render_response_format(params.response_format)
71
+ end
72
+
73
+ base[:thinking] = render_thinking_config(canonical_request) if thinking_enabled?(canonical_request)
74
+ base[:tools] = tools if tools && !tools.empty?
75
+ base[:tool_choice] = tool_choice if tool_choice
76
+ base[:max_tokens] ||= canonical_request.metadata&.dig(:default_max_tokens) || settings_default_max_tokens
77
+
78
+ base.compact
79
+ end
80
+
81
+ # Parse: Anthropic wire Hash to Canonical::Response. Accepts both Anthropic wire format
82
+ # (content: array of blocks) and canonical form (text: string) for conformance kit compatibility.
83
+ def parse_response(wire)
84
+ # If the wire has canonical 'text' key, pass through using Canonical::Response factory.
85
+ return Canonical::Response.from_hash(wire) if wire.key?(:text) || wire.key('text')
86
+
87
+ content = Array(wire[:content] || wire['content'] || [])
88
+ usage = wire[:usage] || wire['usage'] || {}
89
+ raw_usage = parse_usage(usage)
90
+
91
+ text = extract_text(content)
92
+ thinking = extract_thinking(content)
93
+ tool_calls = extract_tool_calls(content)
94
+ stop_reason = map_stop_reason(wire[:stop_reason] || wire['stop_reason'])
95
+ model = wire[:model] || wire['model']
96
+
97
+ Canonical::Response.build(
98
+ text: text,
99
+ thinking: thinking,
100
+ tool_calls: tool_calls,
101
+ usage: raw_usage,
102
+ stop_reason: stop_reason,
103
+ model: model,
104
+ routing: {},
105
+ metadata: wire.except(:content, :usage, :stop_reason, :model).compact
106
+ )
107
+ end
108
+
109
+ # Parse chunk: raw streaming event to Canonical::Chunk.
110
+ def parse_chunk(raw)
111
+ return nil unless raw.is_a?(Hash) && (raw.key?(:type) || raw.key?('type'))
112
+
113
+ type = raw[:type] || raw['type']
114
+
115
+ case type
116
+ when 'text_delta', :text_delta
117
+ Canonical::Chunk.text_delta(
118
+ delta: extract_delta(raw, 'text_delta'),
119
+ request_id: raw[:request_id],
120
+ block_index: raw[:block_index]
121
+ )
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
+
126
+ Canonical::Chunk.thinking_delta(
127
+ delta: extract_delta(raw, 'thinking_delta'),
128
+ request_id: raw[:request_id],
129
+ block_index: raw[:block_index],
130
+ signature: raw[:signature] || raw['signature'] || sig_from_delta
131
+ )
132
+ when 'tool_call_delta', :tool_call_delta
133
+ tc = extract_tool_call_from_chunk(raw)
134
+ return nil unless tc
135
+
136
+ Canonical::Chunk.tool_call_delta(
137
+ tool_call: tc,
138
+ request_id: raw[:request_id],
139
+ block_index: raw[:block_index]
140
+ )
141
+ when 'error', :error
142
+ Canonical::Chunk.error_chunk(
143
+ error: raw[:error] || raw['error'] || 'unknown',
144
+ request_id: raw[:request_id] || '',
145
+ metadata: raw[:metadata] || raw['metadata'] || {}
146
+ )
147
+ when 'done', :done
148
+ usage = (Canonical::Usage.from_hash(raw[:usage] || raw['usage'] || {}) if raw[:usage] || raw['usage'])
149
+
150
+ Canonical::Chunk.done(
151
+ request_id: raw[:request_id] || '',
152
+ usage: usage,
153
+ stop_reason: map_stop_reason(raw[:stop_reason] || raw['stop_reason'])
154
+ )
155
+ else
156
+ # Per G20d: ignore unknown chunk types on consume
157
+ log.debug("[anthropic translator] ignoring unknown chunk type: #{type.inspect}")
158
+ nil
159
+ end
160
+ rescue StandardError => e
161
+ handle_exception(e, level: :debug, handled: true, operation: 'anthropic.translator.parse_chunk')
162
+ Canonical::Chunk.error_chunk(
163
+ error: "#{e.class}: #{e.message}",
164
+ request_id: raw[:request_id] || ''
165
+ )
166
+ end
167
+
168
+ private
169
+
170
+ # --- render_messages ---
171
+
172
+ def render_messages(messages, thinking:)
173
+ messages.map do |msg|
174
+ case msg.role
175
+ when :assistant
176
+ render_assistant_message(msg, thinking:)
177
+ when :tool
178
+ render_tool_result_message(msg)
179
+ else
180
+ {
181
+ role: msg.role.to_s,
182
+ content: render_content_blocks(msg.content)
183
+ }
184
+ end
185
+ end
186
+ end
187
+
188
+ def render_assistant_message(msg, thinking:)
189
+ blocks = render_content_blocks(msg.content)
190
+ blocks.unshift({ type: 'text', text: '' }) if thinking && msg.text.to_s.empty? && !msg.tool_calls&.empty?
191
+
192
+ Array(msg.tool_calls).each do |tc|
193
+ args = tc.is_a?(Canonical::ToolCall) ? tc.arguments : (tc[:arguments] || tc['arguments'] || {})
194
+ args = parse_json_or_hash(args)
195
+ blocks << {
196
+ type: 'tool_use',
197
+ id: tc.is_a?(Canonical::ToolCall) ? tc.id : (tc[:id] || tc['id']),
198
+ name: tc.is_a?(Canonical::ToolCall) ? tc.name : (tc[:name] || tc['name']),
199
+ input: args
200
+ }
201
+ end
202
+
203
+ { role: 'assistant', content: blocks }
204
+ end
205
+
206
+ def render_tool_result_message(msg)
207
+ tool_call_id = msg.tool_call_id
208
+ result_content = render_content_blocks(msg.content)
209
+
210
+ {
211
+ role: 'user',
212
+ content: [
213
+ { type: 'tool_result', tool_use_id: tool_call_id, content: result_content }
214
+ ]
215
+ }
216
+ end
217
+
218
+ def render_content_blocks(content)
219
+ return [{ type: 'text', text: content.to_s }] if content.is_a?(String)
220
+ return [] if content.nil?
221
+
222
+ blocks = Array(content).filter_map do |block|
223
+ case block
224
+ when Canonical::ContentBlock
225
+ content_block_to_wire(block)
226
+ when Hash
227
+ hash_block_to_wire(block)
228
+ else
229
+ { type: 'text', text: block.to_s }
230
+ end
231
+ end
232
+ blocks.empty? ? [] : blocks
233
+ end
234
+
235
+ 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
+ end
251
+
252
+ def hash_block_to_wire(block)
253
+ 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
273
+ end
274
+
275
+ # --- system content ---
276
+
277
+ def render_system_string(system_input)
278
+ return system_input if system_input.is_a?(Hash) || system_input.is_a?(Array)
279
+
280
+ [{ type: 'text', text: system_input.to_s }]
281
+ end
282
+
283
+ def render_system_content(messages)
284
+ parts = messages.flat_map do |msg|
285
+ content = msg.content
286
+ if content.is_a?(Canonical::ContentBlock) && content.type == :text
287
+ [{ type: 'text', text: content.text || '' }]
288
+ elsif content.is_a?(Array)
289
+ render_content_blocks(content)
290
+ else
291
+ [{ type: 'text', text: content.to_s }]
292
+ end
293
+ end
294
+ parts.empty? ? nil : parts
295
+ end
296
+
297
+ # --- tools ---
298
+
299
+ def render_tools(tools)
300
+ return nil if tools.nil? || tools.empty?
301
+
302
+ tools.values.map do |tool|
303
+ name = tool.is_a?(Canonical::ToolDefinition) ? tool.name : (tool[:name] || tool['name'])
304
+ desc = tool.is_a?(Canonical::ToolDefinition) ? tool.description : (tool[:description] || tool['description'] || '')
305
+ params = if tool.is_a?(Canonical::ToolDefinition)
306
+ tool.parameters || {}
307
+ else
308
+ tool[:parameters] || tool['parameters'] || {}
309
+ end
310
+
311
+ { name: name, description: desc, input_schema: { type: 'object', properties: params } }
312
+ end
313
+ end
314
+
315
+ # --- tool_choice ---
316
+
317
+ def render_tool_choice(tool_choice)
318
+ return nil unless tool_choice
319
+
320
+ case tool_choice
321
+ when :auto, 'auto'
322
+ { type: 'auto' }
323
+ when :none, 'none'
324
+ nil
325
+ when :required, 'required'
326
+ { type: 'any' }
327
+ when Hash
328
+ { type: 'tool', name: tool_choice[:name] || tool_choice['name'] }
329
+ when Symbol, String
330
+ { type: 'tool', name: tool_choice.to_s }
331
+ end
332
+ end
333
+
334
+ # --- thinking ---
335
+
336
+ def thinking_enabled?(canonical_request)
337
+ thinking = canonical_request.thinking
338
+ return false unless thinking
339
+
340
+ case thinking
341
+ when Canonical::Thinking::Config
342
+ thinking.enabled?
343
+ when Hash
344
+ !!thinking
345
+ else
346
+ true
347
+ end
348
+ end
349
+
350
+ def render_thinking_config(canonical_request)
351
+ tc = canonical_request.thinking
352
+ budget = case tc
353
+ when Canonical::Thinking::Config
354
+ tc.budget
355
+ when Hash
356
+ tc[:budget] || tc['budget'] || tc[:budget_tokens] || tc['budget_tokens']
357
+ end
358
+
359
+ budget ||= canonical_request.params&.max_thinking_tokens
360
+ budget = default_thinking_budget if budget.nil? || budget.zero?
361
+
362
+ { type: 'enabled', budget_tokens: budget }
363
+ end
364
+
365
+ def default_thinking_budget
366
+ @config[:default_thinking_budget] || 1024
367
+ end
368
+
369
+ # --- response_format ---
370
+
371
+ def render_response_format(fmt)
372
+ return nil unless fmt
373
+
374
+ normalized = fmt.is_a?(Hash) ? fmt : {}
375
+ fmt_type = normalized[:type] || normalized['type']
376
+ schema = normalized[:schema] || normalized['schema'] || normalized.except(:type)
377
+
378
+ case fmt_type
379
+ when 'json_object', 'json_schema'
380
+ if schema && !schema.empty?
381
+ { type: 'json_schema', schema: schema }
382
+ else
383
+ { type: 'json_object' }
384
+ end
385
+ when :json, 'json'
386
+ { type: 'json_object' }
387
+ end
388
+ end
389
+
390
+ # --- unsupported params ---
391
+
392
+ def drop_unsupported_params(params)
393
+ # Anthropic Messages API does NOT support: top_p, top_k, frequency_penalty, presence_penalty.
394
+ unsupported = {}
395
+ unsupported[:top_p] = params.top_p if params.top_p
396
+ unsupported[:top_k] = params.top_k if params.top_k
397
+ unsupported[:frequency_penalty] = params.frequency_penalty if params.frequency_penalty
398
+ unsupported[:presence_penalty] = params.presence_penalty if params.presence_penalty
399
+
400
+ return if unsupported.empty?
401
+
402
+ log.debug("[anthropic translator] dropping unsupported params: #{unsupported.keys.join(', ')}")
403
+ end
404
+
405
+ # --- response parsing ---
406
+
407
+ def extract_text(blocks)
408
+ blocks.select { |b| (b[:type] || b['type']) == 'text' }
409
+ .map { |b| b[:text] || b['text'] || '' }
410
+ .join
411
+ end
412
+
413
+ def extract_thinking(blocks)
414
+ thinking_block = blocks.find { |b| (b[:type] || b['type']) == 'thinking' }
415
+ redacted_block = blocks.find { |b| (b[:type] || b['type']) == 'redacted_thinking' }
416
+
417
+ content = thinking_block&.dig(:thinking) || thinking_block&.dig('thinking') ||
418
+ thinking_block&.dig(:text) || thinking_block&.dig('text')
419
+ signature = thinking_block&.dig(:signature) || thinking_block&.dig('signature') ||
420
+ redacted_block&.dig(:data) || redacted_block&.dig('data')
421
+
422
+ Canonical::Thinking.from_hash({ content: content, signature: signature })
423
+ end
424
+
425
+ def extract_tool_calls(blocks)
426
+ tc_blocks = blocks.select { |b| (b[:type] || b['type']) == 'tool_use' }
427
+ return [] if tc_blocks.empty?
428
+
429
+ tc_blocks.map do |block|
430
+ args_input = block[:input] || block['input'] || {}
431
+ args = parse_json_or_hash(args_input)
432
+
433
+ Canonical::ToolCall.build(
434
+ id: block[:id] || block['id'],
435
+ name: block[:name] || block['name'],
436
+ arguments: args,
437
+ source: :client
438
+ )
439
+ end
440
+ end
441
+
442
+ def parse_json_or_hash(input)
443
+ return input if input.is_a?(Hash)
444
+
445
+ if input.is_a?(String)
446
+ begin
447
+ Legion::JSON.load(input)
448
+ rescue Legion::JSON::ParseError
449
+ { raw_json: input }
450
+ end
451
+ else
452
+ {}
453
+ end
454
+ end
455
+
456
+ def parse_usage(usage)
457
+ Canonical::Usage.from_hash(
458
+ input_tokens: usage[:input_tokens] || usage['input_tokens'],
459
+ output_tokens: usage[:output_tokens] || usage['output_tokens'],
460
+ cache_read_tokens: usage[:cache_read_input_tokens] || usage['cache_read_input_tokens'],
461
+ cache_write_tokens: cache_creation_input_tokens(usage),
462
+ thinking_tokens: thinking_tokens_raw(usage)
463
+ )
464
+ end
465
+
466
+ def cache_creation_input_tokens(usage)
467
+ val = usage[:cache_creation_input_tokens] || usage['cache_creation_input_tokens']
468
+ return val if val
469
+
470
+ cache_creation = usage[:cache_creation] || usage['cache_creation']
471
+ return cache_creation.values.sum if cache_creation.is_a?(Hash)
472
+
473
+ val
474
+ end
475
+
476
+ def thinking_tokens_raw(usage)
477
+ usage.dig(:output_tokens_details, :thinking_tokens) ||
478
+ usage.dig('output_tokens_details', 'thinking_tokens') ||
479
+ usage.dig(:output_tokens_details, :reasoning_tokens) ||
480
+ usage.dig('output_tokens_details', 'reasoning_tokens') ||
481
+ usage[:thinking_tokens] || usage['thinking_tokens'] ||
482
+ usage[:reasoning_tokens] || usage['reasoning_tokens']
483
+ end
484
+
485
+ # --- chunk parsing ---
486
+
487
+ def extract_delta(raw, _type)
488
+ delta_val = raw[:delta] || raw['delta']
489
+ # Canonical form: delta is a plain string (e.g. from conformance fixtures).
490
+ return delta_val if delta_val.is_a?(String) && !delta_val.empty?
491
+
492
+ # Anthropic wire form: delta is a nested object with {text:} or {thinking:}.
493
+ raw.dig(:delta, :text) || raw.dig('delta', 'text') ||
494
+ raw.dig(:delta, :thinking) || raw.dig('delta', 'thinking') ||
495
+ ''
496
+ end
497
+
498
+ def extract_tool_call_from_chunk(raw)
499
+ # Canonical form: tool_call is directly in the chunk (e.g. from conformance fixtures).
500
+ tc_data = raw[:tool_call] || raw['tool_call']
501
+ return extract_tc_from_data(tc_data) if tc_data
502
+
503
+ # Anthropic wire form: tool call is in content_block with type 'tool_use'.
504
+ cb = raw[:content_block] || raw['content_block']
505
+ return nil unless cb && ((cb[:type] || cb['type']) == 'tool_use')
506
+
507
+ extract_tc_from_data(cb)
508
+ end
509
+
510
+ def extract_tc_from_data(data)
511
+ Canonical::ToolCall.build(
512
+ id: data[:id] || data['id'],
513
+ name: data[:name] || data['name'],
514
+ arguments: data[:arguments] || data['arguments'] || {}
515
+ )
516
+ end
517
+
518
+ # --- stop_reason mapping ---
519
+
520
+ def map_stop_reason(raw)
521
+ return nil unless raw
522
+
523
+ mapping = {
524
+ 'end_turn' => :end_turn,
525
+ 'tool_use' => :tool_use,
526
+ 'max_tokens' => :max_tokens,
527
+ 'stop_sequence' => :stop_sequence,
528
+ 'content_filter' => :content_filter
529
+ }
530
+
531
+ result = mapping[raw.to_s]
532
+ return result if result
533
+
534
+ log.debug("[anthropic translator] unmapped stop_reason: #{raw.inspect}, defaulting to :end_turn")
535
+ :end_turn
536
+ end
537
+
538
+ # --- settings helpers ---
539
+
540
+ def settings_default_max_tokens
541
+ @config[:default_max_tokens] || 4096
542
+ end
543
+ end
544
+ end
545
+ end
546
+ end
547
+ end
@@ -30,7 +30,7 @@ module Legion
30
30
  @options[:app_id] || RegistryPublisher::APP_ID
31
31
  end
32
32
 
33
- def persistent # rubocop:disable Naming/PredicateMethod
33
+ def persistent
34
34
  false
35
35
  end
36
36
  end
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Anthropic
7
- VERSION = '0.2.15'
7
+ VERSION = '0.2.17'
8
8
  end
9
9
  end
10
10
  end
@@ -5,13 +5,14 @@ require 'legion/logging/helper'
5
5
  require 'legion/extensions/llm/anthropic/registry_event_builder'
6
6
  require 'legion/extensions/llm/anthropic/registry_publisher'
7
7
  require 'legion/extensions/llm/anthropic/provider'
8
+ require 'legion/extensions/llm/anthropic/translator'
8
9
  require 'legion/extensions/llm/anthropic/version'
9
10
 
10
11
  module Legion
11
12
  module Extensions
12
13
  module Llm
13
14
  # Anthropic provider extension namespace.
14
- module Anthropic # rubocop:disable Metrics/ModuleLength
15
+ module Anthropic
15
16
  extend ::Legion::Extensions::Core if ::Legion::Extensions.const_defined?(:Core, false)
16
17
  extend Legion::Logging::Helper
17
18
  extend Legion::Extensions::Llm::AutoRegistration
@@ -20,24 +21,24 @@ module Legion
20
21
 
21
22
  def self.default_settings
22
23
  ::Legion::Extensions::Llm.provider_settings(
23
- family: PROVIDER_FAMILY,
24
+ family: PROVIDER_FAMILY,
24
25
  instance: {
25
- default_model: 'claude-sonnet-4-6',
26
- endpoint: 'https://api.anthropic.com',
27
- api_version: '2023-10-16',
26
+ default_model: 'claude-sonnet-4-6',
27
+ endpoint: 'https://api.anthropic.com',
28
+ api_version: '2023-10-16',
28
29
  default_max_tokens: 4096,
29
- tier: :frontier,
30
- transport: :http,
31
- credentials: { api_key: 'env://ANTHROPIC_API_KEY' },
32
- usage: { inference: true, embedding: false, image: false },
33
- limits: { concurrency: 4 },
34
- fleet: {
35
- enabled: false,
30
+ tier: :frontier,
31
+ transport: :http,
32
+ credentials: { api_key: 'env://ANTHROPIC_API_KEY' },
33
+ usage: { inference: true, embedding: false, image: false },
34
+ limits: { concurrency: 4 },
35
+ fleet: {
36
+ enabled: false,
36
37
  respond_to_requests: false,
37
- capabilities: %i[chat stream_chat],
38
- lanes: [],
39
- concurrency: 4,
40
- queue_suffix: nil
38
+ capabilities: %i[chat stream_chat],
39
+ lanes: [],
40
+ concurrency: 4,
41
+ queue_suffix: nil
41
42
  }
42
43
  }
43
44
  )
@@ -51,16 +52,16 @@ module Legion
51
52
  []
52
53
  end
53
54
 
54
- def self.discover_instances # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
55
+ def self.discover_instances
55
56
  candidates = {}
56
57
 
57
58
  env_key = CredentialSources.env('ANTHROPIC_API_KEY')
58
59
  if env_key
59
60
  candidates[:env] = {
60
- api_key: env_key,
61
- anthropic_api_key: env_key,
62
- tier: :frontier,
63
- source: CredentialSources.source_tag(:env, 'ANTHROPIC_API_KEY'),
61
+ api_key: env_key,
62
+ anthropic_api_key: env_key,
63
+ tier: :frontier,
64
+ source: CredentialSources.source_tag(:env, 'ANTHROPIC_API_KEY'),
64
65
  credential_fingerprint: CredentialSources.credential_fingerprint(env_key)
65
66
  }
66
67
  end
@@ -68,10 +69,10 @@ module Legion
68
69
  claude_key = CredentialSources.claude_config_value(:anthropicApiKey)
69
70
  if claude_key
70
71
  candidates[:claude] = {
71
- api_key: claude_key,
72
- anthropic_api_key: claude_key,
73
- tier: :frontier,
74
- source: CredentialSources.source_tag(:file, '~/.claude/settings.json', 'anthropicApiKey'),
72
+ api_key: claude_key,
73
+ anthropic_api_key: claude_key,
74
+ tier: :frontier,
75
+ source: CredentialSources.source_tag(:file, '~/.claude/settings.json', 'anthropicApiKey'),
75
76
  credential_fingerprint: CredentialSources.credential_fingerprint(claude_key)
76
77
  }
77
78
  end
@@ -81,10 +82,10 @@ module Legion
81
82
  settings_key = settings_config[:api_key] || settings_config['api_key']
82
83
  if settings_key
83
84
  candidates[:settings] = normalize_instance_config(settings_config).merge(
84
- api_key: settings_key,
85
- anthropic_api_key: settings_key,
86
- tier: :frontier,
87
- source: CredentialSources.source_tag(:settings, 'extensions.llm.anthropic'),
85
+ api_key: settings_key,
86
+ anthropic_api_key: settings_key,
87
+ tier: :frontier,
88
+ source: CredentialSources.source_tag(:settings, 'extensions.llm.anthropic'),
88
89
  credential_fingerprint: CredentialSources.credential_fingerprint(settings_key)
89
90
  )
90
91
  end
@@ -108,10 +109,10 @@ module Legion
108
109
  broker_cred = Legion::Identity::Broker.credential_for(:anthropic)
109
110
  if broker_cred
110
111
  candidates[:broker] = {
111
- api_key: broker_cred,
112
- anthropic_api_key: broker_cred,
113
- tier: :frontier,
114
- source: CredentialSources.source_tag(:broker, 'identity', 'anthropic'),
112
+ api_key: broker_cred,
113
+ anthropic_api_key: broker_cred,
114
+ tier: :frontier,
115
+ source: CredentialSources.source_tag(:broker, 'identity', 'anthropic'),
115
116
  credential_fingerprint: CredentialSources.credential_fingerprint(broker_cred)
116
117
  }
117
118
  end
@@ -130,7 +131,7 @@ module Legion
130
131
  instances.is_a?(Hash) ? instances : {}
131
132
  end
132
133
 
133
- def self.normalize_instance_config(config) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
134
+ def self.normalize_instance_config(config)
134
135
  normalized = config.to_h.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
135
136
  normalized[:anthropic_api_key] ||= normalized.delete(:api_key)
136
137
  normalized[:anthropic_api_base] ||= normalized.delete(:base_url)
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.15
4
+ version: 0.2.17
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.4.3
74
+ version: 0.5.0
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.4.3
81
+ version: 0.5.0
82
82
  description: Anthropic provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com
@@ -103,6 +103,7 @@ files:
103
103
  - lib/legion/extensions/llm/anthropic/registry_event_builder.rb
104
104
  - lib/legion/extensions/llm/anthropic/registry_publisher.rb
105
105
  - lib/legion/extensions/llm/anthropic/runners/fleet_worker.rb
106
+ - lib/legion/extensions/llm/anthropic/translator.rb
106
107
  - lib/legion/extensions/llm/anthropic/transport/exchanges/llm_registry.rb
107
108
  - lib/legion/extensions/llm/anthropic/transport/messages/registry_event.rb
108
109
  - lib/legion/extensions/llm/anthropic/version.rb