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
-
.call(requirements:, evaluation_set:) ⇒ Object
rubocop:disable Metrics/AbcSize.
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 |