Module: Legion::LLM::Router::RejectionDiagnostics

Extended by:
Legion::Logging::Helper
Defined in:
lib/legion/llm/router/rejection_diagnostics.rb

Overview

Total typed no-candidate reduction (SSOT v3 §11). Called only when Ranker returns nil (no ready candidate). Applies the exact ordered partition to produce the single most specific Rejection.

NEVER infers :attempts_exhausted — RoutingSession constructs that before candidate evaluation begins and it is not a candidate-set verdict. NEVER infers :stale_selection — that is an AttemptContext/RoutingSession concern, not a no-candidate signal.

Class Method Summary collapse

Class Method Details

.call(requirements:, evaluation_set:) ⇒ Object

rubocop:disable Metrics/AbcSize



21
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
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
87
88
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
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/legion/llm/router/rejection_diagnostics.rb', line 21

def self.call(requirements:, evaluation_set:, **)
  candidates   = evaluation_set.candidates
  pub_statuses = evaluation_set.publication_statuses
  gen          = evaluation_set.inventory_generation
  counts       = build_counts(candidates, pub_statuses)
  pins         = build_explicit_pins(requirements)

  # ---------------------------------------------------------------- #
  # Step 0 — malformed/missing routing context                        #
  # Guard: server-created routing_seed absent or not 32-char hex.    #
  # ---------------------------------------------------------------- #
  seed = requirements.routing_seed
  unless seed.is_a?(String) && seed.match?(SEED_PATTERN)
    log.warn('[llm][rejection_diagnostics] action=diagnose result=invalid_routing_context')
    return rejection(:invalid_routing_context, 500,
                     'routing context absent or malformed', gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Steps 1–2 — explicit-pin checks (skipped when no pins supplied)  #
  # ---------------------------------------------------------------- #
  if pins.any?
    # Step 1 — pin proven absent: every candidate mismatches and every
    # relevant publication scope is complete (authoritative evidence
    # proves the pin does not exist).
    if candidates.any? && candidates.all? { |c| c.pin_state == :mismatch } && (pub_statuses.empty? || pub_statuses.all? { |s| s.state == :complete })
      log.debug('[llm][rejection_diagnostics] action=diagnose result=invalid_request ' \
                "reason=pin_nonexistent pins=#{pins.keys.join(',')}")
      return rejection(:invalid_request, 400,
                       'explicit pin not found in any complete publication scope',
                       gen, counts, pins)
    end

    # Step 2 — pin not provable: no complete scope exists; cannot
    # confirm or deny the pinned identity.
    unless pub_statuses.any? { |s| s.state == :complete }
      log.debug('[llm][rejection_diagnostics] action=diagnose result=too_early ' \
                'reason=pin_authority_incomplete')
      return rejection(:too_early, 425,
                       'explicit pin resolution blocked; all publication scopes are initializing or absent',
                       gen, counts, pins)
    end
  end

  # ---------------------------------------------------------------- #
  # Cold/empty catalog — no candidates regardless of cause            #
  # ---------------------------------------------------------------- #
  if candidates.empty?
    log.debug('[llm][rejection_diagnostics] action=diagnose result=too_early ' \
              "reason=cold_catalog pub_scopes=#{pub_statuses.size}")
    return rejection(:too_early, 425,
                     'no selectable candidates; catalog is cold or all scopes are initializing',
                     gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Step 3 — policy_denied 403                                        #
  # Every candidate is policy denied or weight disabled.              #
  # ---------------------------------------------------------------- #
  if candidates.all? { |c| c.policy_state == :denied || c.weight_state == :disabled }
    log.debug('[llm][rejection_diagnostics] action=diagnose result=policy_denied ' \
              "count=#{candidates.size}")
    return rejection(:policy_denied, 403,
                     'all candidates are policy denied or weight disabled',
                     gen, counts, pins)
  end

  # "Otherwise relevant" from step 4 onwards: not policy denied, not disabled.
  policy_eligible = candidates.reject { |c| c.policy_state == :denied || c.weight_state == :disabled }

  # ---------------------------------------------------------------- #
  # Step 4 — failed_dependency 424                                    #
  # Complete catalog; every policy-eligible candidate conclusively    #
  # lacks the requested operation or capability (no :unknown among    #
  # the relevant op/cap axes).                                        #
  # ---------------------------------------------------------------- #
  all_scopes_complete    = pub_statuses.empty? || pub_statuses.all? { |s| s.state == :complete }
  has_op_cap_unknown     = policy_eligible.any? do |c|
    c.operation_state == :unknown || c.capability_state == :unknown
  end
  all_op_cap_unsupported = policy_eligible.all? do |c|
    c.operation_state == :unsupported || c.capability_state == :unsupported
  end

  if all_scopes_complete && all_op_cap_unsupported && !has_op_cap_unknown
    log.debug('[llm][rejection_diagnostics] action=diagnose result=failed_dependency ' \
              "count=#{policy_eligible.size}")
    return rejection(:failed_dependency, 424,
                     'all eligible candidates conclusively lack required operation or capability',
                     gen, counts, pins)
  end

  has_tripped     = policy_eligible.any? { |c| c.availability_state == :unavailable }
  # Pin-aware: only a pin-MATCHING fit+available candidate can satisfy a
  # pinned request. A pin-mismatched fit sibling (vllm/bedrock under an
  # ollama pin) cannot be selected, so it must not suppress the tripped
  # 503 (step 5) or the settled-unknown 400 (step 6). Unpinned requests
  # are unaffected — every candidate is pin_state :match with no pin.
  fit_available   = policy_eligible.any? { |c| conclusively_fit?(c) && c.availability_state == :available && c.pin_state == :match }

  # ---------------------------------------------------------------- #
  # Step 5 — service_unavailable 503 (tripped before unknown)         #
  # An UNAVAILABLE (tripped) candidate reports before an unknown-     #
  # evidence one: a tripped instance is 503 (recoverable without a    #
  # restart), never 529 (unbounded overload retry). Skipped when a    #
  # conclusively fit and available candidate exists (its not-ready    #
  # state — e.g. a consumed attempt target — is the step 9 state).    #
  # ---------------------------------------------------------------- #
  if has_tripped && !fit_available
    log.debug('[llm][rejection_diagnostics] action=diagnose result=service_unavailable ' \
              "reason=tripped_before_unknown count=#{policy_eligible.size}")
    return rejection(:service_unavailable, 503,
                     'tripped instance reported before unknown evidence; recovers without a restart',
                     gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Step 6 — invalid_request 400 (terminal settled-unknown)           #
  # A SETTLED (complete) publication scope with an unsatisfied        #
  # :unknown required capability is TERMINAL: the evidence will not   #
  # settle on its own, so the request fails as a typed no-lane (400), #
  # never an unbounded too_early/529 retry loop. too_early (step 7)   #
  # is bounded to genuinely-initializing scopes and to unknowns on    #
  # the non-capability hard-filter axes.                              #
  # ---------------------------------------------------------------- #
  if all_scopes_complete && !fit_available &&
     policy_eligible.any? { |c| c.capability_state == :unknown }
    log.debug('[llm][rejection_diagnostics] action=diagnose result=invalid_request ' \
              "reason=settled_unknown_capability count=#{policy_eligible.size}")
    return rejection(:invalid_request, 400,
                     'no lane can attest the required capabilities; published evidence is unknown and no operator enable_* override is set',
                     gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Step 7 — too_early 425 (unknown evidence)                         #
  # Genuinely-initializing publication scopes, or unknown evidence    #
  # on a non-capability hard-filter axis: operation, context,        #
  # dimension, availability, or fleet contract.                       #
  # ---------------------------------------------------------------- #
  has_any_unknown = policy_eligible.any? do |c|
    c.operation_state == :unknown ||
      c.capability_state     == :unknown ||
      c.context_state        == :unknown ||
      c.dimension_state      == :unknown ||
      c.availability_state   == :unknown ||
      c.fleet_contract_state == :unknown
  end

  if has_any_unknown
    log.debug('[llm][rejection_diagnostics] action=diagnose result=too_early reason=unknown_evidence')
    return rejection(:too_early, 425,
                     'some candidates have unknown evidence; system may still be initializing',
                     gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Step 8 — context_rejected 400                                     #
  # A conclusive context or dimension constraint blocks selection.   #
  # Only when an authoritative context/dimension rejection is the    #
  # actual cause — NOT when the sole blocker is request-local         #
  # exclusion (a consumed attempt identity).                          #
  # ---------------------------------------------------------------- #
  if policy_eligible.any? { |c| c.context_state == :rejected || c.dimension_state == :rejected }
    log.debug('[llm][rejection_diagnostics] action=diagnose result=context_rejected ' \
              "count=#{policy_eligible.size}")
    return rejection(:context_rejected, 400,
                     'all candidates fail context or dimension constraints',
                     gen, counts, pins)
  end

  # ---------------------------------------------------------------- #
  # Step 9 — service_unavailable 503 (retriable)                      #
  # Candidates were otherwise eligible (capable, fit, not policy-     #
  # denied, no unknown evidence) but every one is request-locally     #
  # excluded (its exact provider+instance+model was already consumed  #
  # this request) or on an unavailable instance. This is the "tried   #
  # every eligible target, none left" state — retriable, never a      #
  # 400 caller error and never a fabricated default. Maps to 503      #
  # (native/OpenAI) / 529 (Anthropic) with Retry-After.               #
  # ---------------------------------------------------------------- #
  log.debug('[llm][rejection_diagnostics] action=diagnose result=service_unavailable ' \
            "reason=all_eligible_consumed_or_unavailable count=#{policy_eligible.size}")
  rejection(:service_unavailable, 503,
            'all eligible candidates are consumed or unavailable for this request',
            gen, counts, pins)
end