Module: Legion::LLM::Inference::Executor::PayloadBuilder

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/inference/executor/payload_builder.rb

Overview

Single ingress site for routing_payload construction (C2). Parses x-legion-* headers, validates against Taxonomies, gates body hints, and assembles the dumb hash threaded through Router.request_lane. C2 replaces build_routing_payload_from_resolved from C1.

Class Method Summary collapse

Class Method Details

.body_hints_enabled?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 92

def body_hints_enabled?
  Legion::Settings[:llm][:routing][:allow_body_routing_hints] == true
end

.body_symbol_array(body:, key:) ⇒ Object



96
97
98
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 96

def body_symbol_array(body:, key:, **)
  Array(body[key] || body[key.to_s]).map(&:to_sym).reject { |s| s.to_s.empty? }
end

.build(request:, headers:) ⇒ Object

Build the routing_payload from headers, body, and classification. Called once per request at the executor ingress. H-H / sonnet G4: body model always honored; body tier/provider/instance gated by allow_body_routing_hints setting.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 22

def build(request:, headers:, **)
  body = request.respond_to?(:body) ? (request.body || {}) : {}
  # When called from the executor context, body may be empty; fall back to
  # routing[:model] which was parsed from the HTTP body by the client translator.
  body_model = body[:model] || body['model'] ||
               (request.respond_to?(:routing) ? (request.routing[:model] || request.routing['model']) : nil)

  tiers     = parse_and_validate_header(headers: headers, name: 'x-legion-tiers',
                                        taxonomy: Legion::Extensions::Llm::Taxonomies::TIERS)
  providers = parse_header_array(headers: headers, name: 'x-legion-providers')
  instances = parse_header_array(headers: headers, name: 'x-legion-instances')
  models    = body_model ? [body_model.to_s] : parse_header_array(headers: headers, name: 'x-legion-models')

  if body_hints_enabled?
    tiers     |= body_symbol_array(body: body, key: :tier)
    providers |= body_symbol_array(body: body, key: :provider)
    instances |= body_symbol_array(body: body, key: :instance)
  end

  capabilities = Legion::Extensions::Llm::Capabilities.normalize(
    derive_capabilities(request: request)
  )

  {
    type:              :inference,
    tiers:             tiers,
    providers:         providers,
    instances:         instances,
    models:            models,
    capabilities:      capabilities,
    thinking:          derive_thinking(request: request),
    privacy:           derive_privacy(request: request),
    estimated_context: nil,
    tried_lanes:       [],
    max_attempts:      header_int(headers: headers, name: 'x-legion-max-attempts') ||
      Legion::Settings[:llm][:routing][:max_attempts],
    rng:               nil
  }
end

.derive_capabilities(request:) ⇒ Object



100
101
102
103
104
105
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 100

def derive_capabilities(request:, **)
  caps = []
  tools = request.respond_to?(:tools) ? request.tools : nil
  caps << :tools if tools.is_a?(Array) && tools.any?
  caps
end

.derive_privacy(request:) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 121

def derive_privacy(request:, **)
  classification = request.respond_to?(:classification) ? request.classification : nil
  return :normal unless classification.is_a?(Hash)

  classification[:privacy]&.to_sym == :strict ? :strict : :normal
rescue StandardError => e
  # No-fail-open: a classification fault must never silently downgrade
  # the request's privacy. Fail closed to :strict so routing is
  # constrained, not opened.
  handle_exception(e, level: :error, operation: 'llm.inference.derive_privacy')
  :strict
end

.derive_thinking(request:) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 107

def derive_thinking(request:, **)
  thinking = request.respond_to?(:thinking) ? request.thinking : nil
  return :any if thinking.nil?
  return thinking.any? ? :require : :any if thinking.is_a?(Hash)

  # N2: shared execution carries canonical thinking — a
  # Thinking::Config (or a canonical {effort:, budget:} Hash from
  # the Prompt API), never a client-dialect shape. An explicit
  # effort or budget is a thinking requirement.
  effort = thinking.respond_to?(:effort) ? thinking.effort : nil
  budget = thinking.respond_to?(:budget) ? thinking.budget : nil
  effort || budget ? :require : :any
end

.header_int(headers:, name:) ⇒ Object



83
84
85
86
87
88
89
90
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 83

def header_int(headers:, name:, **)
  raw = headers[name] || headers[name.upcase] || headers[name.tr('-', '_').upcase]
  return nil if raw.nil? || raw.to_s.empty?

  Integer(raw.to_s, 10)
rescue ArgumentError
  nil
end

.parse_and_validate_header(headers:, name:, taxonomy:) ⇒ Object

G31 / opus M7: validate header values against taxonomy. Raises Errors::InvalidHeader (mapped to 400) on unknown values. Returns symbol array of valid values.



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 64

def parse_and_validate_header(headers:, name:, taxonomy:, **)
  values = parse_header_array(headers: headers, name: name)
  return values if values.empty?

  bad = values - taxonomy
  return values if bad.empty?

  raise Legion::LLM::Errors::InvalidHeader.new(
    header: name, got: bad, valid: taxonomy
  )
end

.parse_header_array(headers:, name:) ⇒ Object



76
77
78
79
80
81
# File 'lib/legion/llm/inference/executor/payload_builder.rb', line 76

def parse_header_array(headers:, name:, **)
  raw = headers[name] || headers[name.upcase] || headers[name.tr('-', '_').upcase]
  return [] if raw.nil? || raw.to_s.empty?

  raw.to_s.split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
end