lex-llm-anthropic 0.2.17 → 0.2.20
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 +13 -0
- data/Gemfile +0 -3
- 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 +3 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8d65710f4980c8851f9ff9457cadb0053493ec4817e91b0b046ea354297c9a2
|
|
4
|
+
data.tar.gz: d88fda72706b4035cd5ba2866c3b4354c2f36bdcad1d56bd1373684a8c82b0f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a81ea1ad5a04c67d4079af690238308be289c66315f19555a4d5423a92c43f3ffc940356bf52791d6fd504a446e5fc53fc495c798458f42a7e18bc6beaa5863
|
|
7
|
+
data.tar.gz: d53359d7292d05f5719a7412b90d6a8761cc2982f8325a63446dc92ded3c77957c2914a1138eda5eaf52ff56e41408aeea3337cb49c62a00506de860780ad89f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.20 - 2026-06-16
|
|
4
|
+
|
|
5
|
+
- dependency updates, code quality improvements
|
|
6
|
+
|
|
7
|
+
## 0.2.19 - 2026-06-15
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
11
|
+
## 0.2.18 - 2026-06-13
|
|
12
|
+
|
|
13
|
+
- **Gemfile cleanup** — Remove local path overrides; all dependencies resolve from gemspec via rubygems.
|
|
14
|
+
- 135 examples, 0 failures; 20 files, 0 rubocop offenses.
|
|
15
|
+
|
|
3
16
|
## 0.2.17 - 2026-06-10
|
|
4
17
|
|
|
5
18
|
- **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
|
@@ -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
|
|
@@ -35,10 +34,7 @@ module Legion
|
|
|
35
34
|
fleet: {
|
|
36
35
|
enabled: false,
|
|
37
36
|
respond_to_requests: false,
|
|
38
|
-
capabilities: %i[chat stream_chat]
|
|
39
|
-
lanes: [],
|
|
40
|
-
concurrency: 4,
|
|
41
|
-
queue_suffix: nil
|
|
37
|
+
capabilities: %i[chat stream_chat]
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
)
|
|
@@ -145,8 +141,7 @@ module Legion
|
|
|
145
141
|
config.except(:api_key)
|
|
146
142
|
end
|
|
147
143
|
|
|
148
|
-
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
149
|
-
Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
|
|
144
|
+
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
150
145
|
end
|
|
151
146
|
end
|
|
152
147
|
end
|