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 =
N8: a provider :error stop is an error state, not a clean completion — it renders as 'error', never 'end_turn'.
{ end_turn: 'end_turn', tool_use: 'tool_use', max_tokens: 'max_tokens', stop_sequence: 'stop_sequence', content_filter: 'content_filter', error: 'error', pause_turn: 'pause_turn' }.freeze
Constants included from SharedExtractors
SharedExtractors::PARAM_SPELLINGS
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_startwithserver_tool_userather thaninput_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
#apply_canonical_params_to_inference, #args_as_json_string, #args_as_object, #canonical_content_block, #canonical_image_block, #canonicalize_content_parts, #extract_content_text, #extract_thinking_text, #legion_routing_explicit_from_env, #legion_routing_from_env, #normalize_canonical_params, #normalize_response_format, #normalize_stop_sequences, #param_spelling, #text_content_type?, #token_value, #tool_fragment_field
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).
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 91 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 # N x N law: the executor receives canonical messages end-to-end. # No canonical-to-hash de-canonicalization (the former # inference_messages bridge is deleted). = canonical_request. request_kwargs = { id: request_id, messages: , system: canonical_request.system, routing: canonical_request.routing, # Body-model routing hint (SSOT v3 D19): the sole input to # BodyModelHintPolicy. The trusted X-Legion-Model pin in # `routing` supersedes it — RequestRequirements keeps that order. client_model: canonical_request.[:client_model], 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, # N2: shared execution carries the canonical Thinking::Config # (or nil) — the Anthropic dialect is re-shaped by the PROVIDER # translator at the provider edge, never in the pipeline. thinking: canonical_request.thinking, cache: { strategy: :default, cacheable: true }, extra: extra, metadata: canonical_request. } apply_canonical_params_to_inference(request_kwargs, canonical_request.params) Legion::LLM::Inference::Request.build(**request_kwargs) 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).
183 184 185 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 183 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
138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 138 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.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 351 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
177 178 179 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 177 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).
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 156 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.
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 382 def format_tool_call_delta_chunk(canonical_chunk) tc = canonical_chunk.tool_call args = tool_fragment_field(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: tool_fragment_field(tc, :id), name: tool_fragment_field(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.
46 47 48 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 46 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.
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 85 86 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 54 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
411 412 413 414 415 416 417 418 419 |
# File 'lib/legion/llm/api/client_translators/anthropic_messages.rb', line 411 def server_tool_chunk?(tool_call) # The chunk fragment's source is the closed dispatch-type enum # (R4 fragment contract) — server-executed tools are the # registry/special/extension/mcp set (G24). source = tool_fragment_field(tool_call, :source) return false if source.nil? %i[special registry extension mcp].include?(source.to_sym) end |