Class: Legion::LLM::API::ClientTranslators::AnthropicMessages
- Inherits:
-
Object
- Object
- Legion::LLM::API::ClientTranslators::AnthropicMessages
- Extended by:
- Legion::Logging::Helper
- Includes:
- SharedExtractors, Legion::Logging::Helper
- Defined in:
- lib/legion/llm/api/client_translators/anthropic_messages.rb
Overview
Anthropic /v1/messages client translator.
Per Phase 5 (P5):
- parse_request(body, env) -> Canonical::Request
- format_response(canonical) -> Hash (Anthropic Messages shape)
- format_error(error, status_code:, type:) -> Hash
- events_emitter(out, request_id:, model:, conv_id:) -> Events
emitter for StreamAssembler (Anthropic SSE event names).
Absorbs the logic of api/translators/anthropic_request.rb and api/translators/anthropic_response.rb (now deleted in P6).
Defined Under Namespace
Classes: Events
Constant Summary collapse
- Canonical =
Legion::Extensions::Llm::Canonical
- STOP_REASON_MAP =
{ end_turn: 'end_turn', tool_use: 'tool_use', max_tokens: 'max_tokens', stop_sequence: 'stop_sequence', content_filter: 'content_filter', error: 'end_turn', pause_turn: 'pause_turn' }.freeze
Instance Method Summary collapse
-
#build_inference_request(canonical_request, request_id:, server_caller:, modality: nil) ⇒ Object
Build an Inference::Request from a parsed Canonical::Request + the raw env (the executor still consumes Inference::Request as of P4a; this is the Phase-5 bridge).
-
#events_emitter(out, request_id:, model:, conv_id: nil) ⇒ Object
Emitter implementing the StreamAssembler emitter contract for the Anthropic SSE format (event: name + data: payload).
-
#extract_tool_choice(raw) ⇒ Object
Anthropic tool_choice shapes: ‘auto’ → :auto ‘any’ → :any (force-some-tool) ‘tool’, name: ‘X’ → ‘tool’, name: ‘X’ ‘none’ → :none.
-
#format_chunk(canonical_chunk) ⇒ Object
Per the spec equivalence invariant (G21): /v1/messages and /v1/responses with semantically equivalent payloads must parse to canonically equal Canonical::Request structs.
- #format_error(error, status_code: 500, type: 'api_error') ⇒ Object
-
#format_response(pipeline_response, model:, request_id:) ⇒ Object
Non-streaming response formatting — pipeline_response is an Inference::Response (the executor’s envelope; carries the provider-boundary canonical shape inside).
-
#format_tool_call_delta_chunk(canonical_chunk) ⇒ Object
G24 — when the canonical chunk carries a server-executed tool (registry/special/extension/mcp source) the streaming surface is ‘content_block_start` with `server_tool_use` rather than `input_json_delta`.
-
#g24_format ⇒ Object
G24 — declares which execution-proxy contract shape this translator surfaces.
-
#parse_request(body, env = {}) ⇒ Object
Parse an Anthropic Messages API body + Rack env into a Canonical::Request.
- #server_tool_chunk?(tool_call) ⇒ Boolean
Methods included from SharedExtractors
#args_as_json_string, #args_as_object, #extract_content_text, #extract_thinking_text, #legion_routing_explicit_from_env, #legion_routing_from_env, #text_content_type?, #token_value
Instance Method Details
#build_inference_request(canonical_request, request_id:, server_caller:, modality: nil) ⇒ Object
Build an Inference::Request from a parsed Canonical::Request + the raw env (the executor still consumes Inference::Request as of P4a; this is the Phase-5 bridge).
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 89 def build_inference_request(canonical_request, request_id:, server_caller:, modality: nil) tool_defs = build_tool_definitions(canonical_request.tools) extra = {} tier = canonical_request.[:tier] extra[:tier] = tier.to_sym if tier routing_explicit = canonical_request.[:routing_explicit] extra[:routing_explicit] = routing_explicit if routing_explicit = (canonical_request.) Legion::LLM::Inference::Request.build( id: request_id, messages: , system: canonical_request.system, routing: canonical_request.routing, tools: tool_defs, tool_choice: canonical_request.tool_choice, caller: server_caller, conversation_id: canonical_request.conversation_id, stream: canonical_request.stream == true, modality: modality, thinking: thinking_to_inference(canonical_request.thinking), cache: { strategy: :default, cacheable: true }, extra: extra, metadata: canonical_request. ) end |
#events_emitter(out, request_id:, model:, conv_id: nil) ⇒ Object
Emitter implementing the StreamAssembler emitter contract for the Anthropic SSE format (event: name + data: payload).
168 169 170 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 168 def events_emitter(out, request_id:, model:, conv_id: nil) Events.new(out: out, request_id: request_id, model: model.to_s, conv_id: conv_id) end |
#extract_tool_choice(raw) ⇒ Object
Anthropic tool_choice shapes:
{type: 'auto'} → :auto
{type: 'any'} → :any (force-some-tool)
{type: 'tool', name: 'X'} → {type: 'tool', name: 'X'}
{type: 'none'} → :none
123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 123 def extract_tool_choice(raw) return nil if raw.nil? case raw when Hash symbolized = raw.transform_keys(&:to_sym) type = symbolized[:type].to_s return symbolized if type == 'tool' && symbolized[:name] %w[auto any none required].include?(type) ? type.to_sym : symbolized when String, Symbol raw.to_sym end end |
#format_chunk(canonical_chunk) ⇒ Object
Per the spec equivalence invariant (G21): /v1/messages and /v1/responses with semantically equivalent payloads must parse to canonically equal Canonical::Request structs.
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 336 def format_chunk(canonical_chunk) return nil if canonical_chunk.nil? case canonical_chunk.type when :text_delta { type: 'content_block_delta', index: canonical_chunk.block_index || 0, delta: { type: 'text_delta', text: canonical_chunk.delta.to_s } } when :thinking_delta { type: 'content_block_delta', index: canonical_chunk.block_index || 0, delta: { type: 'thinking_delta', thinking: canonical_chunk.delta.to_s } } when :tool_call_delta format_tool_call_delta_chunk(canonical_chunk) when :done { type: 'message_stop' } end end |
#format_error(error, status_code: 500, type: 'api_error') ⇒ Object
162 163 164 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 162 def format_error(error, status_code: 500, type: 'api_error') [status_code, { type: 'error', error: { type: type, message: error.respond_to?(:message) ? error. : error.to_s } }] end |
#format_response(pipeline_response, model:, request_id:) ⇒ Object
Non-streaming response formatting — pipeline_response is an Inference::Response (the executor’s envelope; carries the provider-boundary canonical shape inside).
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 141 def format_response(pipeline_response, model:, request_id:) content = build_content(pipeline_response) tokens = pipeline_response.respond_to?(:tokens) ? pipeline_response.tokens : nil routing = pipeline_response.respond_to?(:routing) ? pipeline_response.routing || {} : {} resolved_model = (routing[:model] || routing['model'] || model).to_s { id: request_id, type: 'message', role: 'assistant', content: content, model: resolved_model, stop_reason: format_stop_reason(pipeline_response), stop_sequence: nil, usage: { input_tokens: token_value(tokens, :input, :input_tokens) || 0, output_tokens: token_value(tokens, :output, :output_tokens) || 0 } } end |
#format_tool_call_delta_chunk(canonical_chunk) ⇒ Object
G24 — when the canonical chunk carries a server-executed tool (registry/special/extension/mcp source) the streaming surface is ‘content_block_start` with `server_tool_use` rather than `input_json_delta`. The accompanying server_tool_result emits as a sibling block on close (handled by the StreamAssembler which owns block-index bookkeeping). This single-chunk shape just surfaces the call shape so consumers can preview names/args without buffering through the assembler.
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 367 def format_tool_call_delta_chunk(canonical_chunk) tc = canonical_chunk.tool_call args = tc.respond_to?(:arguments) ? tc.arguments : {} block_index = canonical_chunk.block_index || 0 if server_tool_chunk?(tc) { type: 'content_block_start', index: block_index, content_block: { type: 'server_tool_use', id: tc.respond_to?(:id) ? tc.id : nil, name: tc.respond_to?(:name) ? tc.name.to_s : '', input: args_as_object(args) } } else { type: 'content_block_delta', index: block_index, # Anthropic streaming partial_json is a String fragment that # accumulates into a JSON object — wire-spec is string here, # the assembled object is what input becomes when the block # closes. args_as_json_string is the right helper. delta: { type: 'input_json_delta', partial_json: args_as_json_string(args) } } end end |
#g24_format ⇒ Object
G24 — declares which execution-proxy contract shape this translator surfaces. Consumed by the lex-llm conformance shared examples.
44 45 46 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 44 def g24_format :claude_messages end |
#parse_request(body, env = {}) ⇒ Object
Parse an Anthropic Messages API body + Rack env into a Canonical::Request. R9: conversation_id is the Legion-internal id; external refs (HTTP_THREAD_ID, claude session ids, body conversation_id) are persisted in metadata.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 52 def parse_request(body, env = {}) log.debug('[llm][client_translator][anthropic] action=parse_request') body = symbolize(body) = (body[:messages] || []) system = extract_system(body[:system]) tools = extract_tools(body[:tools]) params = extract_params(body) thinking = extract_thinking(body[:thinking]) tool_choice = extract_tool_choice(body[:tool_choice]) external_refs = extract_external_refs(body, env) Canonical::Request.build( id: env['HTTP_X_CLIENT_REQUEST_ID'] || "msg_#{SecureRandom.hex(12)}", messages: , system: system, tools: tools, tool_choice: tool_choice, params: params, thinking: thinking, stream: body[:stream] == true, conversation_id: resolve_conversation_id(body, env), caller: nil, routing: legion_routing_from_env(env), metadata: { client_model: body[:model], anthropic_version: body[:anthropic_version], tier: env['HTTP_X_LEGION_TIER'], routing_explicit: legion_routing_explicit_from_env(env), external_refs: external_refs }.compact ) end |
#server_tool_chunk?(tool_call) ⇒ Boolean
396 397 398 399 400 401 402 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 396 def server_tool_chunk?(tool_call) source = tool_call.respond_to?(:source) ? tool_call.source : nil return false if source.nil? type = source.is_a?(Hash) ? (source[:type] || source['type']) : source %i[special registry extension mcp].include?(type&.to_sym) end |