Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

The three matchers

What exact, normalized and semantic each match on, what each costs, how each fails, and how to tell which one served a hit.

ReferenceLast updated

On this page

A rule’s strategy names one matcher or an ordered chain of them. Each matcher answers one question, “is there a stored entry for this request?”, and answers it differently. The chain is walked in order and the first matcher that returns a candidate wins; later matchers are not consulted.

Chaining is how you get a cheap fast path in front of an expensive one. [exact, normalized, semantic] costs a hash probe on the common case and only reaches the embedding call when both deterministic tiers have missed.

rules:
  - id: chat-hybrid
    match: { endpoint: chat }
    mode: shadow
    strategy: [exact, normalized, semantic]

Each matcher stores under its own partition, so the same request writes one entry per matcher in the chain. A three-matcher rule writes three entries per miss.

exact

Matches a request whose meaning-bearing content is identical to a stored one after canonical encoding.

strategy: exact

How it decides. It builds the request’s semantic surface (the message array, the system prompt, the tool schema, and the sampling-parameter class), encodes it as canonical JSON, and hashes that together with the dialect, endpoint kind, served provider, served model and stream flag. Two requests that differ only in whitespace between JSON tokens, key order, number formatting or Unicode escaping produce the same bytes and therefore the same key. Any difference in content or in a keyed parameter produces a different one.

Knobs. None. The matcher takes no parameters and rejects any it is given.

What it costs. One hash and one store probe. Key derivation over a four-turn body benchmarks at about 9.9 µs, and an in-memory probe at about 0.25 µs, on the reference machine in benchmarks.

How it fails. It misses. There is no partial match and no fallback inside the matcher; the chain moves on.

How to tell it is working. x-tt-cache-kind: exact on the response, "strategy": "exact" on the row, and an exact entry in hits_by_kind on /tt/api/v1/cache/stats.

Reach for something else when your callers send the same question with cosmetic text differences such as trailing spaces, a doubled space or a newline, because those are content differences to this matcher.

normalized

Matches after a declared text transform, so cosmetic whitespace differences in the content stop fragmenting the cache.

strategy: [exact, normalized]

How it decides. It runs the exact profile’s canonicalization, then applies one transform to every string value in the document: runs of whitespace collapse to a single space, and leading and trailing whitespace is stripped. It hashes the result under a different profile id, so a normalized entry and an exact entry are separate entries even when their material coincides.

It applies nothing that changes meaning. No stopword removal, no punctuation stripping, no stemming. Those belong to the semantic tier, where they are disclosed as a similarity score rather than presented as an identity.

Knobs. A case-folding option exists in the matcher and has no configuration key today: the config-to-engine mapping does not carry per-matcher parameters, so the matcher always compiles with case folding off. Normalized matching is whitespace-only, and Reset my password and reset my password are different requests.

What it costs. One extra string pass over the canonical document, then the same hash and probe as exact. It is scheduled in the same class as exact: no embedding, no deadline.

How it fails. It misses.

How to tell it is working. x-tt-cache-kind: normalized. In a chain behind exact this appears only for requests whose exact key missed, which is what you want to see.

Reach for something else when the variation you are trying to absorb is a rewording rather than whitespace. Two spellings of the same question are not the same string after normalization.

semantic

Matches a request whose recent user text is close in embedding space to a stored one, above a threshold you set.

embedders:
  potion: { kind: static }
rules:
  - id: chat-hybrid
    match: { endpoint: chat }
    mode: shadow
    strategy: [exact, normalized, semantic]
    ttl: 15m
    semantic:
      embedder: potion            # names a cache.embedders entry
      threshold: 0.95             # at or above ⇒ served without a verifier
      low_threshold: 0.85         # below ⇒ miss; between ⇒ the verifier decides
      verifier: kreciprocal       # none | kreciprocal | judge
      history_max_messages: 3     # embed the last N user turns; also an eligibility bound

How it decides. It embeds the last history_max_messages user turns, joined in order, and nothing else. Everything else the request contains (the system prompt, the tool schema, the sampling parameters, the assistant turns, and any earlier user turns outside the window) is hashed into the partition the search runs against, so a candidate stored under different context is not reachable at all. Within that partition it recalls the ten nearest stored vectors, recomputes the exact cosine against each candidate’s stored vector, keeps the best, and applies the dual-threshold decision.

Knobs.

Key Default What it does
embedder — required; names a cache.embedders entry
threshold — the bar a score clears to serve with no verifier; must be in (0,1]
low_threshold 0 the floor below which a score is a miss; 0 means no gray band exists
verifier "" the gray-band gate; without a synchronous one the gray band cannot serve
history_max_messages 0 user turns embedded, and the history bound above which the request is not semantically served; 0 means no bound
verified absent the calibration policy that decides whether these hits may report a dollar

What it costs. One embedding call per lookup that reaches this matcher, on the request path. With an in-process embedder that is CPU time and no money. With an HTTP embedder it is a network round trip and, on the external tier, a real charge that lands on the row as embed_cost. Recall at a ten-thousand-entry working set benchmarks at about 2.9 ms with the default exact index at 384 dimensions.

How it fails. Toward not serving. A score below the floor is a miss; a gray-band candidate with no verifier configured is a miss; a verifier that errors is a miss; an embedding error is a fault that forwards live; a lookup that exceeds the semantic budget is a counted miss with bypass_reason: over_budget on the row. It can also fail by serving the wrong answer, which is what the correctness page is about.

How to tell it is working. x-tt-cache-kind: semantic with x-tt-cache-similarity, -threshold and -verified beside it, and similarity / threshold / zone on the row.

Reach for something else when your traffic is machine-generated and already identical, when answers are personalised or time-sensitive, or when you cannot yet say what wrong-hit rate you are willing to accept.

Choosing a chain

Traffic Chain
identical repeats from a service or an agent loop exact
human-typed repeats with cosmetic whitespace variation [exact, normalized]
the same question in different words, and you can measure wrong hits [exact, normalized, semantic]

Start at the top of that table. The first two tiers cost microseconds, call nothing, and cannot serve an answer to a different question.