Module: Legion::LLM::API::DebugFormats
- Extended by:
- Legion::Logging::Helper
- Defined in:
- lib/legion/llm/api/debug_formats.rb
Overview
Shared X-Legion-Format / X-Legion-Debug surface for the three client routes (G21).
Two opt-in debug modes, both gated by llm.api.debug_formats.enabled
(default: true in lite/dev, false otherwise — the envelope leaks
routing/escalation internals):
X-Legion-Format: canonical
Run the full pipeline but skip the client-format translation.
Sync: return Canonical::Response#to_h + contract version.
Streaming: emit each canonical chunk as `data: <chunk-json>\n\n`
followed by `data: [DONE]\n\n` — same envelope across all three
routes (no Anthropic-style typed events, no /v1/responses
sequence_number ceremony) so the canonical layer is its own
bisection point.
X-Legion-Debug: echo-request
The parsed Canonical::Request#to_h is folded into the response
metadata under `_legion_debug.echo_request`. Combined with
X-Legion-Format: canonical, this is the equivalence-invariant
check — the same semantic payload sent to /v1/messages and
/v1/responses must echo back IDENTICAL Canonical::Request hashes.
Modes are independent: format=canonical works without the echo, and
echo-request works on a normal client-format response.
Defined Under Namespace
Classes: CanonicalEvents
Constant Summary collapse
- Canonical =
Legion::Extensions::Llm::Canonical
- FORMAT_HEADER =
'HTTP_X_LEGION_FORMAT'- DEBUG_HEADER =
'HTTP_X_LEGION_DEBUG'- FORMAT_CANONICAL =
'canonical'- DEBUG_ECHO_REQUEST =
'echo-request'
Class Method Summary collapse
-
.attach_echo_request(client_format_body, canonical_request) ⇒ Object
Sync: fold the parsed canonical request into a client-format response's metadata.
-
.canonical_event_emitter(out) ⇒ Object
Streaming canonical SSE — emit each chunk via the assembler-equivalent stream interface, then a final
data: [DONE]\n\n. - .canonical_format?(env) ⇒ Boolean
-
.canonical_stop_reason(pipeline_response, tool_calls) ⇒ Object
M7: the debug projection honors the one stop-reason policy — the provider-declared stop (or the tool-call-derived :tool_use) when it is a canonical enum value, nil when absent or unmapped.
-
.canonical_thinking(value) ⇒ Object
G3: the envelope already carries canonical types — the debug projection is the identity for thinking and tool calls (the former Hash-OR-canonical reconstruction was the split-world seam).
- .canonical_tool_call(tool_call) ⇒ Object
- .canonical_usage(tokens, _pipeline_response) ⇒ Object
-
.canonicalize_response(pipeline_response) ⇒ Object
Convert an Inference::Response (the executor's envelope) into a Canonical::Response (the provider-boundary contract).
- .echo_request?(env) ⇒ Boolean
-
.emit_echo_request_sse(out, canonical_request) ⇒ Object
Streaming: emit a one-shot SSE event with the canonical request echo so the client can correlate the two endpoints.
-
.enabled? ⇒ Boolean
Settings dig — debug surface enabled?.
-
.render_canonical_response(pipeline_response, canonical_request:, env:) ⇒ Object
Render a non-streaming canonical response as JSON.
- .sanitize_routing(routing) ⇒ Object
- .token_value(tokens, *keys) ⇒ Object
Class Method Details
.attach_echo_request(client_format_body, canonical_request) ⇒ Object
Sync: fold the parsed canonical request into a client-format response's metadata. Mutates a copy of the body to keep callers simple. Returns the merged hash.
86 87 88 89 90 91 |
# File 'lib/legion/llm/api/debug_formats.rb', line 86 def self.attach_echo_request(client_format_body, canonical_request) { **client_format_body, _legion_debug: { echo_request: canonical_request.to_h } } end |
.canonical_event_emitter(out) ⇒ Object
Streaming canonical SSE — emit each chunk via the assembler-equivalent
stream interface, then a final data: [DONE]\n\n. Same envelope on
every route.
79 80 81 |
# File 'lib/legion/llm/api/debug_formats.rb', line 79 def self.canonical_event_emitter(out) CanonicalEvents.new(out) end |
.canonical_format?(env) ⇒ Boolean
52 53 54 |
# File 'lib/legion/llm/api/debug_formats.rb', line 52 def self.canonical_format?(env) enabled? && env[FORMAT_HEADER].to_s.downcase == FORMAT_CANONICAL end |
.canonical_stop_reason(pipeline_response, tool_calls) ⇒ Object
M7: the debug projection honors the one stop-reason policy — the provider-declared stop (or the tool-call-derived :tool_use) when it is a canonical enum value, nil when absent or unmapped. Never a fabricated :end_turn.
292 293 294 295 296 297 298 299 300 301 |
# File 'lib/legion/llm/api/debug_formats.rb', line 292 def self.canonical_stop_reason(pipeline_response, tool_calls) return :tool_use if tool_calls.any? { |tc| tc.source != :special && tc.source != :registry && tc.source != :extension && tc.source != :mcp && tc.result.nil? } stop = pipeline_response.respond_to?(:stop) ? pipeline_response.stop : nil reason = stop.is_a?(Hash) ? (stop[:reason] || stop['reason']) : nil sym = reason&.to_sym return sym if Canonical::Response::STOP_REASONS.include?(sym) nil end |
.canonical_thinking(value) ⇒ Object
G3: the envelope already carries canonical types — the debug projection is the identity for thinking and tool calls (the former Hash-OR-canonical reconstruction was the split-world seam).
266 267 268 269 270 |
# File 'lib/legion/llm/api/debug_formats.rb', line 266 def self.canonical_thinking(value) return nil if value.nil? value.is_a?(Canonical::Thinking) ? value : Canonical::Thinking.from_hash(value) end |
.canonical_tool_call(tool_call) ⇒ Object
272 273 274 |
# File 'lib/legion/llm/api/debug_formats.rb', line 272 def self.canonical_tool_call(tool_call) tool_call.is_a?(Canonical::ToolCall) ? tool_call : Canonical::ToolCall.from_hash(tool_call) end |
.canonical_usage(tokens, _pipeline_response) ⇒ Object
276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/legion/llm/api/debug_formats.rb', line 276 def self.canonical_usage(tokens, _pipeline_response) return nil if tokens.nil? || (tokens.respond_to?(:empty?) && tokens.empty?) Canonical::Usage.from_hash( input_tokens: token_value(tokens, :input, :input_tokens) || 0, output_tokens: token_value(tokens, :output, :output_tokens) || 0, cache_read_tokens: token_value(tokens, :cache_read, :cache_read_tokens) || 0, cache_write_tokens: token_value(tokens, :cache_write, :cache_write_tokens) || 0, thinking_tokens: token_value(tokens, :thinking, :thinking_tokens) || 0 ) end |
.canonicalize_response(pipeline_response) ⇒ Object
Convert an Inference::Response (the executor's envelope) into a Canonical::Response (the provider-boundary contract). Inference::Response carries the executor envelope — text, tool_calls, thinking, usage live on it; Canonical::Response is the projection.
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 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/legion/llm/api/debug_formats.rb', line 106 def self.canonicalize_response(pipeline_response) = pipeline_response.respond_to?(:message) ? pipeline_response. : nil text = if .is_a?(Hash) ([:content] || ['content']).to_s else .to_s end tokens = pipeline_response.respond_to?(:tokens) ? pipeline_response.tokens || {} : {} usage = canonical_usage(tokens, pipeline_response) tool_calls = (pipeline_response.respond_to?(:tools) ? Array(pipeline_response.tools) : []).map do |tc| canonical_tool_call(tc) end thinking = canonical_thinking(pipeline_response.respond_to?(:thinking) ? pipeline_response.thinking : nil) stop_reason = canonical_stop_reason(pipeline_response, tool_calls) routing = pipeline_response.respond_to?(:routing) ? pipeline_response.routing || {} : {} model = (routing[:model] || routing['model']).to_s = {} [:request_id] = pipeline_response.request_id if pipeline_response.respond_to?(:request_id) [:conversation_id] = pipeline_response.conversation_id if pipeline_response.respond_to?(:conversation_id) [:routing] = sanitize_routing(routing) if routing.any? Canonical::Response.build( text: text, thinking: thinking, tool_calls: tool_calls, usage: usage, stop_reason: stop_reason, model: model, routing: sanitize_routing(routing), metadata: ) end |
.echo_request?(env) ⇒ Boolean
56 57 58 |
# File 'lib/legion/llm/api/debug_formats.rb', line 56 def self.echo_request?(env) enabled? && env[DEBUG_HEADER].to_s.downcase == DEBUG_ECHO_REQUEST end |
.emit_echo_request_sse(out, canonical_request) ⇒ Object
Streaming: emit a one-shot SSE event with the canonical request echo so the client can correlate the two endpoints. Best-effort; non-fatal on write failure.
96 97 98 99 100 |
# File 'lib/legion/llm/api/debug_formats.rb', line 96 def self.emit_echo_request_sse(out, canonical_request) out << "event: legion.debug.echo_request\ndata: #{Legion::JSON.dump(canonical_request.to_h)}\n\n" rescue IOError, Errno::EPIPE nil end |
.enabled? ⇒ Boolean
Settings dig — debug surface enabled?
47 48 49 50 |
# File 'lib/legion/llm/api/debug_formats.rb', line 47 def self.enabled? settings = Legion::Settings[:llm][:api][:debug_formats] settings.is_a?(Hash) ? settings[:enabled] == true : false end |
.render_canonical_response(pipeline_response, canonical_request:, env:) ⇒ Object
Render a non-streaming canonical response as JSON. Returns [status, headers, body_string] suitable for use in a Sinatra action.
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/legion/llm/api/debug_formats.rb', line 62 def self.render_canonical_response(pipeline_response, canonical_request:, env:) canonical_response = canonicalize_response(pipeline_response) payload = { object: 'canonical.response', contract_version: Canonical::CONTRACT_VERSION, response: canonical_response.to_h } payload[:_legion_debug] = { echo_request: canonical_request.to_h } if echo_request?(env) [200, { 'Content-Type' => 'application/json', 'X-Legion-Contract-Version' => Canonical::CONTRACT_VERSION }, Legion::JSON.dump(payload)] end |
.sanitize_routing(routing) ⇒ Object
303 304 305 306 307 308 309 310 311 312 |
# File 'lib/legion/llm/api/debug_formats.rb', line 303 def self.sanitize_routing(routing) return {} if routing.nil? || routing.empty? { provider: routing[:provider] || routing['provider'], model: routing[:model] || routing['model'], tier: routing[:tier] || routing['tier'], instance: routing[:instance] || routing['instance'] }.compact end |
.token_value(tokens, *keys) ⇒ Object
314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/legion/llm/api/debug_formats.rb', line 314 def self.token_value(tokens, *keys) return nil if tokens.nil? keys.each do |key| value = if tokens.is_a?(Hash) tokens[key] || tokens[key.to_s] elsif tokens.respond_to?(key) tokens.public_send(key) end return value.to_i unless value.nil? end nil end |