A living reference of every reason_code string the cache plane emits on the
wire. The tech spec (§4.2) and the gRPC contract design doc
(../design/grpc-contract.md) cover the why; this
file is the what is exposed today.
Update this file in the same change that introduces or retires a code. New codes don't break old clients (see "Forward compatibility" below), but an undocumented code in production is a real friction point for the gateway team and for ops dashboards.
reason_codeis astring, not a protoenum. New codes are an additive server-side change with no client recompile.- Clients MUST treat any unrecognized code as the no-hint default for that
RPC (
NO_HINTforLookupRoute/LookupPDRoute,RENDER_ERRORforRenderTemplate). This is what lets the server roll out new codes without coordinating with every gateway. - The server emits codes only from the Status column "shipped" below. "Spec'd" codes are reserved in the proto comments but not yet returned; promoting one to "shipped" is the trigger to update this doc.
LookupPDRoute remains a fail-open stub today (Phase 2 — prefill/decode
routing) and unconditionally emits NO_HINT. The codes in the table below
that are marked LookupRoute only are emitted by LookupRoute's handler
and will surface from LookupPDRoute when its handler is wired in a later
module — until then, treat NO_HINT as the only LookupPDRoute answer.
| Code | Status | RPC | When the server emits it | Response shape | What the gateway does |
|---|---|---|---|---|---|
PREFIX_MATCH |
shipped | LookupRoute only | The index has at least one replica holding the request's (tenant, model, hash_scheme, adapter) prefix — either the exact prefix_hash (legacy single-blob path) OR the leading run of block_hashes[0..k] (chain longest-prefix path; see ../design/lookuproute-ranking.md §2.5) — the ranker returned a non-empty set, AND at least one replica's realized matched_tokens cleared the per-namespace minimumMatchedTokens floor (default 64) AND the top surviving replica's score cleared the per-namespace routingFloorScore floor (default 0.1). The score includes the distinguishing-power factor 1 − num_matching_replicas / total_replicas so an overlap held by every replica (chat-template framing, RAG corpus headers, custom system prompts) collapses to score 0 and is filtered by the score floor. Both floors can downgrade independently. |
replica_scores non-empty, ranked best-first by matched_tokens × freshness × pressure_factor × slo_bias × distinguishing_power. The pressure / SLO / distinguishing-power factors collapse appropriately when their inputs are absent (no stats → pressure_factor = 1; no SLO hint → slo_bias = 1; single-replica deployment → distinguishing_power = 1). Every qualifying replica is returned today (no top-K limit); the gateway typically uses the top entry. |
Route to the top-ranked replica → prefix-cache hit; lower TTFT. |
NO_HINT |
shipped | both | The fail-open default. LookupPDRoute: every call (the handler is a stub). LookupRoute: the prefix is novel under matching contract keys AND no usable affinity fallback fired (affinityRouting: Disabled on the per-namespace policy, OR no replica known to serve the (tenant, model, hash_scheme) engine domain, OR no usable seed — empty block_hashes and empty prefix_hash, OR structurally malformed input — empty hash_scheme or chain arrays of mismatched length); the ranker found nothing AND the same affinity-disabled / no-replica / no-seed / malformed clause holds; any of tenant_id, model_id, or hash_scheme was unspecified (a contract violation — set-but-wrong values surface as the matching UNKNOWN_* code instead); the index is globally empty (cold-start carve-out); the request was policy-gated below minimumPrefixTokens AND affinityRouting: Disabled (with affinityRouting: Enabled — the default — the same gate surfaces as AFFINITY_HINT); every replica that held the prefix matched fewer tokens than minimumMatchedTokens (result-side per-replica floor, default 64) AND affinityRouting: Disabled (with affinity Enabled the downgrade surfaces as AFFINITY_HINT); the top per-replica score from the distinguishing-power-aware ranker fell below routingFloorScore (result-side score floor, default 0.1) AND affinityRouting: Disabled (with affinity Enabled the downgrade surfaces as AFFINITY_HINT); or an index-disabled state. |
replica_scores empty. Not an error. |
Route per the gateway's default policy (round-robin, least-loaded, …). The cache plane is invisible to this request. |
POLICY_REQUIRES_CHAIN |
shipped | LookupRoute only | The tenant's CachePolicy.spec.strategy.requireChain is true, but the request did not carry a valid block_hashes + block_token_counts chain. The server returns before touching the index. This is a policy-gated empty result, separated from NO_HINT so operators can see legacy/exact callers hitting a chain-only namespace in inferencecache_lookup_route_calls_total{reason_code="POLICY_REQUIRES_CHAIN"}. |
Empty replica_scores. |
Treat as NO_HINT; update the caller to send the chain if it should benefit from the cache plane in this namespace. |
TENANT_HOT |
shipped | LookupRoute only | No exact prefix match for (tenant, model, hash_scheme, adapter, prefix_hash), CachePolicy.spec.strategy.enableTenantHot is not false, and the tenant has at least one replica that (a) has reported stats recently (within ~5 minutes by default), (b) has a hit_rate above a small floor (default 0.1), AND (c) currently has at least one prefix entry in the requested (tenant, model, hash_scheme) in the index — proving the replica serves the requested engine domain. The "in the index" check is sweep-driven (an entry past TTL stays counted until the next sweep removes it), so for at most one sweep interval a recently-stale entry can briefly still satisfy the check; per soft-state semantics that yields at worst a soft hint that turns into a cache miss, never a wrong answer. A coarser locality signal than PREFIX_MATCH — useful when the prefix is novel but the tenant already has servers warm in the cache rotation. When the same fallback is found but the policy disables tenant-hot, the handler downgrades it to NO_HINT. |
replica_scores non-empty (tenant-hot ranked); matched_tokens is 0 because there is no prefix overlap (the gateway must rely on reason_code, not matched_tokens, to recognize this branch). Shape otherwise unchanged. |
Treat as a softer hint than PREFIX_MATCH; gateway free to use or ignore. |
TIMEOUT |
shipped | LookupRoute only | The lookup deadline expired before the index could rank — either the caller's context was already past its deadline on arrival or the per-tenant CachePolicy.spec.lookupTimeoutMs budget elapsed during the lookup. Gateway clients also synthesize this locally when they cancel a slow LookupRoute RPC. |
Server: empty replica_scores. Client-side synth: same. |
Treat as NO_HINT. |
UNKNOWN_TENANT |
shipped | LookupRoute only | After a prefix miss (and the TENANT_HOT fallback when it applies — non-chain requests; chain requests skip TENANT_HOT by design and classify directly), AND the index is not globally empty: the request supplied a non-empty tenant_id and the index has zero prefix entries for that tenant across every model and hash scheme. Cold-start carve-out: a globally empty index (server just started, no ReportCacheState yet) stays on NO_HINT so a fresh deployment does not flood gateways with UNKNOWN_TENANT; the diagnostic resumes the moment any replica has reported state. Canonical asymmetric shape: a gateway-SDK querying with tenant_id="default" while the producer (kvevent-subscriber sidecar) is publishing under tenant_id=$(POD_NAMESPACE). |
Empty replica_scores. |
Treat as NO_HINT for routing (still fail-open — the cache plane is hint-only); surface as a configuration error (log line / metric / SDK warning). Do not retry under a different key — the cache plane will not change between calls. |
UNKNOWN_MODEL |
shipped | LookupRoute only | Same precondition as UNKNOWN_TENANT above. The tenant is known but the (tenant_id, model_id) pair has zero entries. The model has never served traffic in this tenant, or the model identifier disagrees between producer and consumer. |
Empty replica_scores. |
Same as UNKNOWN_TENANT: fail-open, surface as configuration error. |
UNKNOWN_HASH_SCHEME |
shipped | LookupRoute only | Same precondition as UNKNOWN_TENANT above. (tenant_id, model_id) has entries, but none under the request's hash_scheme. Canonical shape: ingest under "vllm", lookup under "vllm-v1". An empty hash_scheme is a contract violation (not a mismatch) and stays on NO_HINT. |
Empty replica_scores. |
Same as UNKNOWN_TENANT: fail-open, surface as configuration error. |
AFFINITY_HINT |
shipped | LookupRoute only | The prefix-match path would otherwise return NO_HINT AND the per-namespace CachePolicy.spec.affinityRouting is Enabled (the kubebuilder default) AND the request has a usable fingerprint AND the index knows at least one replica SERVING the request's (tenant, model, hash_scheme) engine domain. The server returns a single stable replica picked by SHA-256 over the request's length-prefixed block_hashes (fall-back to prefix_hash for legacy callers) modulo the sorted SCHEME-AWARE replica set for (tenant, model, hash_scheme) (read from servingByScope, the same accelerator TENANT_HOT uses, so a vLLM request can never pin to an SGLang replica) — so repeat prompts pin to the same replica and warm T1 on diffuse single-turn workloads. The diagnostic codes (UNKNOWN_TENANT / UNKNOWN_MODEL / UNKNOWN_HASH_SCHEME) and TIMEOUT keep precedence over AFFINITY_HINT; affinity never preempts a real PREFIX_MATCH or TENANT_HOT that cleared the request-side gates (one exception: a tiny request below the per-namespace minimumPrefixTokens gate has its positive-hint result — including TENANT_HOT — downgraded to StrategyNone, so the affinity fallback can still fire on it; the operator intent "tiny prompts don’t surface a positive hint" outranks the TENANT_HOT-vs-affinity precedence); requests with empty hash_scheme or chain arrays of mismatched length stay on NO_HINT (structurally malformed input shouldn't be papered over). |
replica_scores carries exactly one entry whose score, matched_tokens, and estimated_cache_hit_prob are all 0 — there is no cache-evidence claim, only a stable assignment. Shape otherwise identical to PREFIX_MATCH. |
Route to the single returned replica. Treat as PREFIX_MATCH for routing purposes; the distinct reason code is the operator-facing signal for measuring the affinity-vs-real-match share via inferencecache_lookup_route_calls_total{reason_code="AFFINITY_HINT"}. |
Constants in code: reasonPrefixMatch, reasonTenantHot, reasonNoHint, reasonTimeout, reasonPolicyRequiresChain, reasonAffinityHint,
reasonUnknownTenant, reasonUnknownModel, reasonUnknownHashScheme in
internal/server/inferencecache_service.go. See also ../design/lookuproute-diagnostics.md for the design rule and gateway-SDK guidance.
The server-side ranker (internal/index) is configurable via RankerConfig (in-
binary knobs) and CachePolicy.spec (per-namespace knobs). Each RankerConfig
knob defaults to a value that reduces the pressure / SLO layers to the
baseline when its supporting signal is absent — so a deployment without replica
stats or SLO hints sees pressure_factor = 1 and slo_bias = 1. The
cardinality-aware distinguishing-power factor is always on for multi-replica
deployments (no per-knob disable; degrades to 1.0 only for single-replica
deployments), and the two result-side floors (minimumMatchedTokens,
routingFloorScore) still apply on top — see the CachePolicy.spec rows
below for their opt-outs. So a deployment without replica stats or SLO hints
behaves like the original B6 baseline on the pressure/SLO factors only;
the cardinality factor and both floors still run.
| Knob | What it does | Default | Off switch |
|---|---|---|---|
PressureWeight |
Penalty applied to a replica's score from ReplicaStats.pressure: pressure_factor = max(0, 1 - PressureWeight × pressure). Avoids blindly preferring a saturated cache holder over a fresher, lower-pressure peer. |
1.0 |
0 → no penalty |
SLOTightTTFTMs |
TTFT budget (ms) below which the request is "tight" and the SLO bias kicks in. Uses LookupRouteRequest.slo.ttft_ms. |
200 |
0 → bias never fires |
SLOTightBias |
Coefficient in the freshness boost: slo_bias = 1 + freshness × SLOTightBias when the request is tight. Higher → fresher candidates are favored more aggressively. |
1.0 |
0 → no boost |
TenantHotMinHitRate |
Minimum hit_rate for a replica to count as "warm" for the TENANT_HOT fallback. |
0.1 |
n/a (use TenantHotMaxAge = 0 to disable the fallback) |
TenantHotMaxAge |
Maximum stats age for a replica to count as "warm". | 5m |
0 → fallback disabled (a prefix miss whose contract keys all populate the index lands at NO_HINT; mismatched-key misses still diagnose as UNKNOWN_* via the miss-classifier) |
distinguishing_power factor |
Cardinality-aware multiplier: 1 − num_matching_replicas / total_replicas, per-replica depth-aware for chain matches. Discounts overlaps every replica holds (chat-template framing, RAG corpus headers, custom system prompts). Always on for multi-replica deployments; degrades to 1.0 for single-replica deployments. See ../design/lookuproute-ranking.md §2.7. |
always on for multi-replica; 1.0 for single-replica |
none (operators disable the floor it feeds via CachePolicy.spec.routingFloorScore: "0", not the factor itself) |
CachePolicy.spec.minimumMatchedTokens |
Per-replica matched-tokens floor: filters replicas whose realized matched_tokens falls below the threshold. If no replica survives, the response downgrades to StrategyNone, which surfaces as AFFINITY_HINT under affinityRouting: Enabled (the default) with a usable seed + serving replica or as NO_HINT under affinityRouting: Disabled. |
64 (4 KV blocks) |
0 on the CR → opt-out for that namespace |
CachePolicy.spec.routingFloorScore |
Per-response score floor on the top surviving replica's score (after the distinguishing-power factor multiplies in). Below the floor → response downgrades to StrategyNone, with the same AFFINITY_HINT vs NO_HINT split as the matched-tokens row above. |
"0.1" |
"0" on the CR → opt-out for that namespace |
CachePolicy.spec.strategy.enableChainMatching / requireChain / enableTenantHot |
Per-namespace strategy gates. enableChainMatching=false disables block-hash chain matching and uses exact prefix_hash; requireChain=true rejects non-chain requests with POLICY_REQUIRES_CHAIN; enableTenantHot=false downgrades tenant-hot fallbacks to NO_HINT. |
true / false / true |
Set the relevant boolean on the CR; requireChain=true is rejected unless chain matching is enabled |
CachePolicy.spec.affinityRouting |
Per-namespace consistent-hash fallback toggle. When Enabled (the default), StrategyNone responses with a usable seed + serving replica surface as AFFINITY_HINT (single stable replica from servingByScope[(tenant, model, hash_scheme)]). When Disabled, those same responses surface as NO_HINT. Diagnostic codes and TIMEOUT keep precedence over AFFINITY_HINT. |
Enabled |
Disabled on the CR → opt-out for that namespace |
| Code | Status | When the server emits it | Notes |
|---|---|---|---|
OK |
shipped (stub) | The handler currently returns OK unconditionally — the rendering pipeline (Wedge D, D1–D5) isn't wired yet. |
Becomes "real" when D2 (render pipeline) lands. |
TEMPLATE_NOT_FOUND |
spec'd, not emitted | The referenced template_ref doesn't exist. |
Promoted when D5 (RenderTemplate handler) lands. |
RENDER_ERROR |
spec'd, not emitted | Template was found but rendering failed (missing/typed-wrong variables, runtime DSL error). | Promoted with D5. |
Constants in code: reasonOK in internal/server/inferencecache_service.go.
The Ack proto carries an optional reason_code for future use (e.g. partial
acceptance, throttling signals). Today the server returns
Ack{Accepted: true} with reason_code unset on every code path — there are
no Ack reason codes in use.
When the first Ack code ships (e.g. THROTTLED, SCHEMA_DROPPED), add it to
the table below and update this paragraph.
| Code | Status | When the server emits it | Notes |
|---|---|---|---|
| (none) | — | — | Field reserved for future structured acknowledgments. |
reason_code is a label on inferencecache_lookup_route_calls_total,
alongside model and hint_used. The cardinality of values is therefore bounded
by the table above — adding a new code adds one new label value (cheap), but it
does show up as a new time series, so prefer reusing existing codes if the
semantic fits.
inferencecache_lookup_route_calls_total{model="...", reason_code="PREFIX_MATCH", hint_used="true"} 42
inferencecache_lookup_route_calls_total{model="...", reason_code="NO_HINT", hint_used="false"} 318
hint_used="true" ⇔ replica_scores was non-empty in the response. It
correlates with PREFIX_MATCH, TENANT_HOT, or AFFINITY_HINT — all
three shipped codes return non-empty scores. The diagnostic
UNKNOWN_* codes, NO_HINT, and TIMEOUT correlate with
hint_used="false".
See metrics.md for the full metric surface.
- Reserve the string in the proto comment on the response message in
proto/inferencecache/v1alpha1/inferencecache.proto(already done forTENANT_HOT,TIMEOUT,TEMPLATE_NOT_FOUND,RENDER_ERROR). Runmake proto-genif the comment touched the schema. - Add a constant in
internal/server/inferencecache_service.gonext toreasonPrefixMatch/reasonNoHint/reasonOK. Keep the constant namereason<CamelCase>. - Emit it from the handler at the relevant decision point. Keep handlers
side-effect-free apart from metrics;
reason_codeis the only way the server communicates "what kind of answer is this." - Update the table above — move the row from "spec'd" → "shipped", describe the trigger condition, the response shape, and the gateway action.
- Document the metric expectation. If the new code is for
LookupRoute, a newreason_codelabel value will appear automatically ininferencecache_lookup_route_calls_total. Mention this in the PR description so dashboards can be updated. - Backward-compat check. Confirm that an existing client not updated for
the new code degrades to its no-hint default (
NO_HINT/RENDER_ERROR). This is the contract; verify by reading the client adapter, not by guessing.