Skip to main content
Not to be confused with prompt caching. Semantic caching is Bifrost replaying a response it has already seen, so the provider is never called. Prompt caching is the provider reusing the prefix of your request, so the call still happens and is still billed: cache reads are cheaper than fresh input, but the turn that writes the cache can cost more than fresh input (on Anthropic, 1.25x for a 5m TTL and 2x for 1h). They are independent and can both be on.

Overview

Bifrost can cache LLM responses and replay them for repeated requests, avoiding a round-trip to the provider. It offers two complementary lookup paths:
  • Direct (hash) matching — deterministic, exact-match replay. The request is normalized and hashed; an identical request is served instantly. No embeddings required.
  • Semantic (similarity) matching — embedding-based lookup that serves a cached answer when a new request is close enough to a previous one, even if the wording differs.
Both paths can run together (direct first, semantic on miss), or you can run direct-only with no embedding provider at all.
In the Web UI this feature is labeled Local Cache (under Settings → Caching). “Semantic caching” refers to the embedding-based mode; “direct” mode is the embedding-free path. They are the same plugin (semantic_cache).
Key benefits:
  • Cost reduction — skip paid LLM calls for repeated or similar prompts.
  • Lower latency — sub-millisecond cache reads vs. multi-second provider calls.
  • Two modes — exact-match deduplication (direct) or fuzzy similarity (semantic).
  • Streaming support — streamed responses are cached and replayed chunk-by-chunk.

How it works

A few things that trip up first-time users — read these before configuring:
  1. A cache key is mandatory. Caching only engages when a request carries a cache key (the x-bf-cache-key header, or the CacheKey context value in the Go SDK). Without one — and without a configured default_cache_key — the request bypasses the cache entirely. This is the single most common reason “nothing is being cached.”
  2. Direct runs before semantic. When both paths are enabled, a direct hash hit is served first; the semantic search only runs on a direct miss. You can narrow a request to one path with the x-bf-cache-type header.
  3. Writes are asynchronous. On a cache miss, Bifrost returns the provider’s response immediately and stores it in the background, so the first request never blocks on a cache write.
  4. Entries persist across restarts. Cache entries live in your vector store with a per-entry expiry (expires_at). They are not purged when Bifrost shuts down — a restart keeps serving warm cache (see Cache lifecycle).
What gets cached: chat completions, text completions, the Responses API (including WebSocket), embeddings, transcriptions, speech, and image generation — including their streaming variants.
Latency overhead. The cache lookup itself adds latency to every cache-enabled request, and the cost differs per path:
  • Direct lookup — one vector store round-trip per request, hit or miss. Sub-millisecond to a few milliseconds with a local Redis/Valkey; higher with remote or managed stores. (Computing the request hash itself is in-process and takes microseconds — the round-trip is the only real cost.)
  • Semantic lookup — runs on every direct miss, and must embed the incoming request before it can search. That means one embedding API call to your provider (typically tens to a few hundred milliseconds) plus a vector similarity search, paid upfront regardless of the outcome. A semantic hit therefore costs roughly an embedding round-trip — not the near-instant replay of a direct hit — and a semantic miss pays the embedding call on top of the full LLM call, making it slower than running without the cache.
  • Cache writes — asynchronous; they add no latency to the response.

Prerequisites

  1. A vector store is required as the storage backend for both modes — even direct-only mode stores its entries there. Bifrost supports:

Redis / Valkey

In-memory, RediSearch-compatible. Recommended for direct-only mode.

Weaviate

Production-ready vector database with gRPC support.

Qdrant

Rust-based vector search engine with advanced filtering.

Pinecone

Managed, serverless vector database service.
  1. An embedding-capable provider — only if you want semantic mode. Direct-only mode needs no provider.
See the Vector Store documentation for per-store setup. The vector store must be enabled in config.json before the Enable Caching toggle becomes available in the UI.
Minimal vector store config (Redis/Valkey):
For Valkey, keep vector_store.type as "redis" and point config.addr at your Valkey endpoint.

Configuration

Local Cache configuration page
  1. Configure and enable a vector store in config.json (see Prerequisites). Without it, the toggle stays disabled.
  2. In the Bifrost UI, go to Settings → Caching. You’ll see the Local Cache panel.
  3. Flip Enable Caching on. The plugin loads live — no server restart needed.
  4. Pick a Cache Mode using the tabs at the top of the panel:
    • Direct only — exact-match caching. No provider or embeddings. Cheapest path; ideal for stable, repeated prompts.
    • Direct + Semantic — adds vector similarity on top of direct matching. Requires an embedding-capable provider. (This tab is disabled until at least one embedding-capable provider is configured.)
  5. For semantic mode, fill in the embedding provider, model, and dimension that appear below the tabs:
    • Configured Providers — an embedding-capable provider already set up in Bifrost. Its API keys are inherited automatically.
    • Embedding Model — e.g. text-embedding-3-small.
    • Dimension — the vector size the model produces. Must match the model exactly (e.g. 1536 for text-embedding-3-small, 3072 for text-embedding-3-large, 768 for many Cohere/Voyage models).
  6. Tune Cache Settings, Storage & Cache Key, Conversation Settings, and Cache Key Composition (all explained in the field reference below).
  7. Click Save Changes. Config changes mutate the live plugin in place.
  8. Send a request with an x-bf-cache-key header to start caching (see Triggering the cache).

Field reference


Direct vs. semantic mode

Direct-only setup

Direct mode hashes each request deterministically from its normalized input, parameters, and stream flag. Identical requests hit; any difference is a miss. The deterministic cache ID keeps repeated lookups consistent across retries, streaming, and restarts. To enable direct-only mode, set dimension: 1 and omit provider and embedding_model. In the UI, pick the Direct only tab.
If you set dimension: 1 and also provide a provider, Bifrost treats the config as semantic mode, not direct-only. To use direct-only mode, omit provider entirely.
In direct-only mode, all requests use hash matching regardless of the x-bf-cache-type header — no embeddings are generated and no embedding credentials are needed. Redis/Valkey-compatible stores are recommended for direct-only mode. They don’t require a vector for metadata-only entries, and all cache fields are indexed as TAG fields for fast exact-match lookups.
Qdrant, Pinecone, and Weaviate are not suitable for direct-only mode. They require a vector for every entry; the plugin’s zero-vector placeholder codepath needs an initialized embedding client, so storage fails when no provider is configured. Use Redis/Valkey for direct-only.

Triggering the cache

A cache key is mandatory. Caching only activates when a request carries a cache key. Without one (and without a configured default_cache_key), the request bypasses caching entirely.
The cache key is the partition every lookup and write is scoped to — it’s part of the cache entry’s identity alongside the model and provider. It exists for two reasons:
  • Isolation (no cross-talk). Entries are only ever matched within the same key. A request under tenant-A can never be served a response cached under tenant-B, even if the prompts are identical. This prevents one user, tenant, or feature from leaking cached answers to another — the key is how you draw that boundary (per user, per session, per feature, per tenant, etc.).
  • Explicit opt-in. Caching changes behavior — a response can be replayed instead of freshly generated. Requiring a key makes that a deliberate choice per request (or per deployment via default_cache_key), so you never accidentally serve a cached answer where you wanted a live one.
Pick a key granularity that matches how much you want to share: a coarse key (e.g. a feature name) maximizes hit rate across users; a fine key (e.g. a per-user or per-session ID) keeps caches private at the cost of fewer hits.
Set the cache key in the x-bf-cache-key header:

Per-request overrides

Every plugin default can be overridden per request via headers (HTTP) or context keys (Go SDK).
In direct-only mode (no embedding provider), x-bf-cache-type and x-bf-cache-threshold have no effect — every request uses direct matching.

Cache management

Every cached or cache-checked response carries debug metadata so you can confirm caching is working and capture the entry’s ID for management. Location: response.ExtraFields.CacheDebug Examples:
On a streamed response, only the final chunk carries the full cache_debug payload.
Cache outcomes also surface in Logs without inspecting the raw response: Log detail sheet showing the Semantic Cache badge and Caching Details block
  • Hit-type badge — a cache hit is tagged with a Direct Cache or Semantic Cache badge on the log entry.
  • Cache row — each cached request shows a Cache (hit) / Cache (miss) row with the copyable cache_id.
  • Caching Details block — expands to the cache_debug fields: cache type, and for semantic hits the embedding provider, embedding model, threshold, similarity score, and embedding input tokens.
  • Local Caching filter — the logs filter sidebar lets you filter requests by hit type (Direct cache / Semantic cache).

Invalidation

Use the cache_id from cache_debug to invalidate entries.

Lifecycle & Cleanup

  • TTL expiration — every entry is stored with an expires_at timestamp. Expired entries are no longer served and are swept out over time.
  • Entries persist across restarts — cache data lives in your vector store and is not purged when Bifrost shuts down. A restart resumes serving the existing (unexpired) cache. To wipe entries, use the cache-clear APIs or clear the namespace in your vector store directly.
  • Namespace isolation — each vector_store_namespace is an independent cache pool. Use distinct namespaces to keep separate caches from colliding.
Changing dimension, provider, or embedding_model: a vector store namespace can hold vectors of one dimension only, and it is not recreated automatically when the dimension changes.On Redis and Qdrant, a dimension mismatch is caught at startup: namespace creation fails with namespace ... already exists with dimension N but config requires M, and Bifrost does not come up until you resolve it. Qdrant skips the check in two cases — the collection uses named vectors (Bifrost only ever creates unnamed ones, so the collection was not created by Bifrost), or the collection-info lookup fails — and logs that it did. On the other backends, namespace creation is a no-op when the class/collection already exists, so a mismatch surfaces later — subsequent writes fail (size mismatch) and reads silently miss.Either way, before saving such a change:
  • point vector_store_namespace at a fresh name, or
  • drop the existing class/index in your vector store.

Troubleshooting

Most common cause: no cache key. Caching only engages when a request sends x-bf-cache-key (or you’ve set a default_cache_key). Confirm the header is present, then check cache_debug on the response.
Expected. The cache is populated after the first response is returned (writes are asynchronous). Send the same request again to see a hit.
  • Verify dimension exactly matches your embedding model’s output size.
  • Lower the threshold (e.g. 0.80.75) if genuinely-similar prompts aren’t matching.
  • Check cache_debug.similarity on a miss to see how close you got.
You changed dimension/provider/embedding_model against an existing namespace. See the dimension-change warning — use a fresh namespace or drop the old class/index.
No embedding-capable provider is configured. Add one under Providers first; its keys are inherited automatically.
No vector store is enabled. Configure and enable one in config.json (see Prerequisites).
You’re likely using Qdrant, Pinecone, or Weaviate, which require a vector per entry. Switch to Redis/Valkey for direct-only mode.

Next steps