Cache
The gateway caches completions in two layers. An exact layer in Redis keyed on a hash of the normalised request, and a semantic layer in MongoDB that matches requests which are similar rather than identical.
The Cache page lets you inspect what is stored, remove a single answer, and flush everything.
How a lookup works
- Exact. A Redis
GETon the scoped key. A hit returns immediately. - Semantic. On a miss, the request text is embedded and compared against stored entries, racing a three-second timeout — a cache that takes longer than the provider would have is not a cache.
- Promotion. A semantic hit is written back into Redis under the exact key, so the same question asked twice more is answered from the fast layer.
- Otherwise, a miss.
The semantic pass compares against the fifty most recently used candidates for the requested model. Anything outside that window is not considered, which bounds the work done on the request path.
Candidates are narrowed on the model the caller asked for, not the model
that answered. Routing may serve a gpt-4o request from a different provider,
and a cached answer belongs to the question, not to whoever happened to answer
it that time.
The cache key
A SHA-256 of the normalised request: model, message roles and contents, system prompt, temperature, top-p, max tokens, sorted tool names, tool choice, response format and seed.
The key alone is not the partition. The Redis key is:
aigw:cache:<deployment>:<organizationId|->:<departmentId|->:<cacheKey>A - marks an absent level, so two different scope paths cannot collide. A
caller with no organization shares the deployment-wide namespace; an
organization’s answers are not served to another organization.
When the cache is skipped entirely
A request bypasses both layers if any of these hold:
stream: truetemperature > 1.0toolsare presentn > 1seedis set
Each of these describes a request whose answer is either not reusable or not deterministic enough to be worth reusing.
Threshold and TTL
Both are deployment-level environment variables. Neither is editable from the console.
| Variable | Default | Meaning |
|---|---|---|
CACHE_SIMILARITY_THRESHOLD | 0.96 | Minimum cosine similarity for a semantic hit, 0–1 |
CACHE_DEFAULT_TTL_SECONDS | 86400 | Entry lifetime, 24 hours |
The Redis TTL is written with ±10% jitter, so a burst of entries created together does not expire together and dump the whole load onto providers in one second.
cachingEnabled and cacheTTLSeconds also exist as policy on the deployment
and each scope, and feed the per-request TTL.
Embeddings are local
Semantic matching uses an ONNX all-MiniLM-L6-v2 model loaded from disk, with
remote model fetching disabled. No text leaves the deployment to be embedded,
which is what keeps the semantic cache usable in an air-gapped install.
If the model cannot be loaded, an entry is stored with an empty embedding and is simply never returned by a semantic match. Exact matching is unaffected.
This is not the same embedding stack the document corpus uses — see Documents.
Reading it in the console
The entries table shows one row per stored answer:
| Column | Content |
|---|---|
| Answer | The model, the provider that produced it, and a truncated cache key |
| Reuses | How many times this entry has been served |
| Match | The similarity score recorded on it |
| Stored | When it was created, and when it was last served |
| Expires | Remaining lifetime, or “Expired” / “No expiry” |
Only Stored and Expires are sortable — the list endpoint accepts no other sort field. The page says so above the table, and ranks the two derived panels below (most-reused answers, and matches worth reviewing) client-side over a bounded sample of the newest hundred entries, labelled as such.
The Match figure is the similarity of the most recent semantic match against
that entry. An entry only ever served by exact hits keeps its initial 1.0, so
a column of 100% means “not measured by a semantic comparison”, not “all
answers were perfect”.
The detail drawer shows the model, provider, reuse count, match, token split, timestamps, cache key and the stored answer. The request text itself is embedded rather than rendered, so the drawer does not display it.
Flushing
Flush clears the whole deployment. Both layers, every organization, every department, in one sweep — every scope key begins with the deployment identifier, so there is no partial flush.
The console requires you to type the word flush to confirm, and states the
consequence: until the cache refills, every request goes to a provider, latency
returns to a full round trip, and every one of them is billed.
Deleting a single entry removes both its MongoDB row and its Redis twin. Doing only the first was a real earlier defect — the control read as working while every cached answer was still being served.
Both destructive operations require deployment scope, whatever permissions the caller holds.
Endpoints
| Method | Path | Permission |
|---|---|---|
GET | /api/admin/cache/stats | cache:read |
GET | /api/admin/cache/entries | cache:read |
DELETE | /api/admin/cache/entries/:id | cache:delete + deployment scope |
DELETE | /api/admin/cache/flush | cache:delete + deployment scope |
Note that flush and single-entry delete are DELETE, not POST.
GET /entries accepts page, limit (capped at 100), sort (createdAt or
expiresAt) and order. It has no search or filter parameters, and excludes
the stored embedding vector from the response.
cache:read and cache:delete are granted to super-admin and
deployment-admin only. Organization and department administrators have no
cache permission at all, so this page is not reachable for them.
Related
- Settings —
cachingEnabledand the TTL policy - Request Log — requests recorded as From cache
- Configuration — the environment variables