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 +4 -4
- data/CHANGELOG.md +18 -0
- data/Gemfile +0 -3
- data/lex-llm-anthropic.gemspec +1 -1
- data/lib/legion/extensions/llm/anthropic/actors/discovery_refresh.rb +3 -0
- data/lib/legion/extensions/llm/anthropic/provider.rb +95 -77
- data/lib/legion/extensions/llm/anthropic/translator.rb +155 -51
- data/lib/legion/extensions/llm/anthropic/version.rb +1 -1
- data/lib/legion/extensions/llm/anthropic.rb +18 -10
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03b981bffa993167ee00d0bb4b122f4a92c55ed978a082d35ee3fb1237afba49
|
|
4
|
+
data.tar.gz: 9c3d7c381087b3eab19acdb504b85670c97833aff725c604c1551cb109fcf677
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lex-llm-anthropic.gemspec
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
339
|
+
to_legacy_chunk(canonical_chunk, data)
|
|
358
340
|
end
|
|
359
341
|
|
|
360
|
-
def
|
|
361
|
-
usage
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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:
|
|
373
|
-
model_id:
|
|
374
|
-
thinking:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
+
tc.id,
|
|
400
387
|
Legion::Extensions::Llm::ToolCall.new(
|
|
401
|
-
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:
|
|
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
|
|
426
|
-
|
|
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
|
|
430
|
-
|
|
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
|
|
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 '
|
|
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'
|
|
123
|
-
|
|
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'
|
|
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'
|
|
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'
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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:
|
|
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).
|
|
@@ -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:
|
|
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]
|
|
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)
|
|
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.
|
|
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.
|
|
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.
|
|
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
|