A drop-in, Rust-native embedding cache. Point your existing OpenAI-compatible embedding client at Zerocache instead of the real provider, and identical text/image inputs stop costing you a second API call.
No SDK to install. No framework plugin. No server-side provider credentials for Zerocache to leak — every request brings its own API key.
Your app ──▶ Zerocache ──▶ Real provider (only on a cache miss)
(drop-in base_url swap)
- Why Zerocache exists
- How it works
- Architecture
- Supported providers
- Quickstart
- Configuration reference
- API reference
- Deployment
- Observability
- Testing
- Project status
- Non-goals (v1)
- Contributing / further reading
- License
RAG ingestion pipelines re-embed text that's already been embedded before — during re-indexing, pipeline re-runs, CI test suites, or overlapping corpora across projects and teams. Every re-embed is pure waste: it costs real input tokens and adds real latency for a result that's byte-identical to something already computed.
Zerocache eliminates that waste at the wire level, transparently, independent of which language or framework produced the request. A real measured example from this repo's own agentic battle-test: re-indexing a 9-document corpus after editing one document and adding another cost exactly 2 provider calls out of 9 — the other 7, byte-identical between versions, were served from cache for free.
- Your embedding client sends a normal
POST /v1/embeddings-shaped request — except the URL now points at Zerocache, with a provider name in the path (/openai/v1/embeddings,/gemini/v1/embeddings, etc.). - Zerocache derives a content-addressed cache key from
owner_id + provider + cache_scope + model + model_version + text, using your forwarded API key (hashed, never stored raw) to keep your cache private to you. - Any input already in the store is returned instantly. Anything new is batched into a single call to the real upstream provider, using your forwarded key — Zerocache never sees or stores a provider credential beyond the duration of that one request.
- New vectors are written back to the store and returned alongside the cache hits, in the original request order.
- Concurrent requests that miss on the exact same text are automatically coalesced into one upstream call, not one per request.
Dependencies point inward only — a hard, structurally-enforced rule via Cargo workspace crate boundaries, not just convention:
flowchart TB
subgraph layerInterface["Interface / Transport"]
nodeHttp["zerocache-http<br/>axum · wire-shape translation · provider registry"]
end
subgraph layerApplication["Application"]
nodeApp["orchestration: split hits/misses,<br/>call provider for misses only,<br/>write back, reassemble in order"]
end
subgraph layerPorts["Ports"]
nodePorts["EmbeddingStore · EmbeddingProvider ·<br/>ImageEmbeddingProvider trait contracts"]
end
subgraph layerAdapters["Adapters"]
nodeStore["Store adapters<br/>sled · redis"]
nodeProvider["Provider adapters<br/>openai · mistral · gemini · huggingface<br/>bedrock · vertexai · azure"]
end
subgraph layerCore["Core (domain)"]
nodeKey["CacheKey derivation (blake3)<br/>hit/miss reconciliation<br/>zero I/O, zero async runtime"]
end
nodeClient(["Any OpenAI-compatible<br/>embedding client"]) -->|"POST /{provider}/v1/embeddings"| nodeHttp
nodeHttp --> nodeApp
nodeApp --> nodePorts
nodePorts --> nodeStore
nodePorts --> nodeProvider
nodeApp --> nodeKey
nodeProvider -->|"BYOK: your forwarded key"| nodeUpstream(["Real provider API"])
nodeHttp -->|"ordered response +<br/>X-Zerocache-Hits/-Misses"| nodeClient
| Crate | Responsibility |
|---|---|
zerocache-core |
Domain logic: CacheKey/CacheKey::derive_image derivation, hit/miss reconciliation. No I/O, no async runtime, no framework awareness. |
zerocache-ports |
EmbeddingStore / EmbeddingProvider / ImageEmbeddingProvider trait contracts, StoreError/ProviderError/ProviderUsage. |
zerocache-adapters-sled |
EmbeddingStore backed by sled — embedded, single-process. Local dev / single-instance. |
zerocache-adapters-redis |
EmbeddingStore backed by Redis — shared, network-accessible, connection-pooled. Use for any multi-replica deployment. |
zerocache-adapters-openai |
EmbeddingProvider for OpenAI. |
zerocache-adapters-mistral |
EmbeddingProvider for Mistral. |
zerocache-adapters-gemini |
EmbeddingProvider and ImageEmbeddingProvider for Gemini — the only provider with image-embedding support. |
zerocache-adapters-huggingface |
EmbeddingProvider for HuggingFace Inference Providers. |
zerocache-adapters-cloud |
Shared kit for the three cloud adapters below: HTTP transport driver (client, timeouts, retry, chunking, usage accounting) plus a CloudRouter/TextWireStrategy strategy-pattern abstraction, since each cloud is one API in front of several independent model vendors. |
zerocache-adapters-bedrock |
EmbeddingProvider for Amazon Bedrock (Titan, Cohere). |
zerocache-adapters-vertexai |
EmbeddingProvider for GCP Vertex AI's native :predict endpoint. |
zerocache-adapters-azure |
EmbeddingProvider for Azure — both the GA OpenAI /openai/v1 surface and Foundry Models. |
zerocache-http |
axum HTTP server, wire-shape translation, provider registry, application wiring. Registers all seven provider adapters. |
The cache key is blake3(owner_id, provider, cache_scope, model, model_version, text):
owner_id— a hash of your forwarded API key (never the raw key), so two different callers never share a cache entry even for identical text.provider+model+model_version— so a different provider, model, or adapter version can never silently return a stale-but-plausible vector.cache_scope— provider-specific routing identity: the configured base URL for the four "simple" adapters (so repointingZEROCACHE_OPENAI_BASE_URLat a self-hosted vLLM instance starts from a cold cache, never a wrong hit), and{endpoint_base}\0{canonical}\0kit{version}for the three cloud adapters (sous-east-1andeu-west-1Bedrock, or two different GCP Vertex AI projects, can never collide even when the caller'smodelstring looks identical).
{provider} |
Text embeddings | Image embeddings | Auth to Zerocache | Notes |
|---|---|---|---|---|
openai |
✅ | — | Authorization: Bearer <key> |
Configurable base URL — self-hosted vLLM/LM Studio work too. |
mistral |
✅ | — | Authorization: Bearer <key> |
Configurable base URL. |
gemini |
✅ | ✅ | Authorization: Bearer <key> |
Only provider with image embeddings. Never reports token usage. |
huggingface |
✅ | — | Authorization: Bearer <key> |
Model is part of the URL path, not the JSON body — a genuine wire-shape difference from the other three. |
bedrock |
✅ | — | Authorization: Bearer <key> |
Amazon's own bearer API keys — no AWS SigV4. Titan and Cohere vendor models behind one router. |
vertexai |
✅ | — | Authorization: Bearer <key> |
GCP's native :predict endpoint (Vertex's OpenAI-compatible surface is chat-only). |
azure |
✅ | — | Authorization: Bearer <key> |
Two surfaces in one adapter: Azure OpenAI GA /openai/v1 and Foundry Models — routed by a foundry: model prefix. Only registers if at least one of its two base-URL env vars is set. |
Every caller brings their own key for whichever provider they call — Zerocache holds no provider credentials of its own (BYOK: bring-your-own-key).
docker run -d --name zerocache -p 8080:8080 ghcr.io/shramanb113/zerocache:latestThat's it — no provider key needed to start the container; you supply one per request (see API reference). Confirm it's up:
curl http://localhost:8080/healthThe image runs as a non-root user, ships a built-in HEALTHCHECK against /health, and defaults to the embedded sled store at /data inside the container — mount a volume there if you want the cache to survive a restart:
docker run -d --name zerocache -p 8080:8080 -v zerocache-data:/data ghcr.io/shramanb113/zerocache:latestBind mounts vs named volumes: the container runs as uid
10001. A named volume (-v zerocache-data:/data, as above) inherits the image's ownership and just works. A bind mount to a host directory (-v ./local-data:/data) takes the host directory's ownership instead — if that directory isn't writable by uid10001, sled fails to open and the process exits. Prefer named volumes unless you specifically need the data on a host path, in which casechown -R 10001:10001that directory first.
For local development against the Redis backend (what you'd run in a multi-replica deployment):
docker compose up -d --buildThis brings up zerocache-http wired to a redis service (ZEROCACHE_STORAGE_BACKEND=redis), both on the same compose network. See docker-compose.yml.
Prerequisites: Rust via rustup, toolchain 1.97.1 (edition 2021).
cargo build --workspace
cargo test --workspace
cargo run -p zerocache-httpReal-Redis integration tests are #[ignore]d by default (they spin up an ephemeral container via testcontainers, so they need Docker running):
cargo test -p zerocache-adapters-redis -- --ignoredEvery setting is an environment variable — there is no config file. Everything is optional; Zerocache starts with sensible defaults and zero provider keys.
Core
| Variable | Default | Notes |
|---|---|---|
ZEROCACHE_PORT |
8080 |
HTTP listen port (binds 0.0.0.0). |
ZEROCACHE_STORAGE_BACKEND |
sled |
sled or redis. |
ZEROCACHE_STORAGE_PATH |
./data (/data in the Docker image) |
sled only. |
ZEROCACHE_REDIS_URL |
redis://127.0.0.1:6379 |
redis only. |
ZEROCACHE_TTL_SECONDS |
unset (never expires) | Per-store-instance expiry. 0 or an unparseable value is treated as unset, with a startup warning. |
Simple provider base-URL overrides — a bare origin (scheme + host + optional port), no /v1 suffix, no trailing slash:
| Variable | Default |
|---|---|
ZEROCACHE_OPENAI_BASE_URL |
https://api.openai.com |
ZEROCACHE_MISTRAL_BASE_URL |
https://api.mistral.ai |
ZEROCACHE_GEMINI_BASE_URL |
https://generativelanguage.googleapis.com |
ZEROCACHE_HUGGINGFACE_BASE_URL |
https://router.huggingface.co/hf-inference |
Azure
| Variable | Default | Notes |
|---|---|---|
ZEROCACHE_AZURE_OPENAI_BASE_URL |
unset | e.g. https://my-resource.openai.azure.com. Setting either this or the Foundry URL below registers the azure provider. |
ZEROCACHE_AZURE_FOUNDRY_BASE_URL |
unset | e.g. https://my-resource.services.ai.azure.com. |
ZEROCACHE_AZURE_FOUNDRY_API_VERSION |
2024-05-01-preview |
Foundry surface only — the GA /openai/v1 path takes no api-version. |
ZEROCACHE_AZURE_AUTH_MODE |
bearer |
bearer (Entra ID token, recommended) or api-key. An unrecognized value warns and falls back to bearer. |
Amazon Bedrock
| Variable | Default |
|---|---|
ZEROCACHE_BEDROCK_REGION |
us-east-1 |
ZEROCACHE_BEDROCK_ENDPOINT_TEMPLATE |
https://bedrock-runtime.{region}.amazonaws.com |
GCP Vertex AI
| Variable | Default | Notes |
|---|---|---|
ZEROCACHE_VERTEX_PROJECT |
unset | If unset, every vertexai request's model must carry <location>/<project>/ itself. |
ZEROCACHE_VERTEX_LOCATION |
us-central1 |
|
ZEROCACHE_VERTEX_ENDPOINT_TEMPLATE |
https://{location}-aiplatform.googleapis.com |
global/us/eu multi-region locations resolve to the correct real Google host automatically. |
Observability (optional, off by default)
| Variable | Default |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
unset — no OTLP export, console logging only |
RUST_LOG |
info |
POST /{provider}/v1/embeddings
Authorization: Bearer <your own API key for that provider>
Content-Type: application/json
{ "model": "<real upstream model name>", "input": ["text 1", "text 2"] }
→ 200 OK
{
"object": "list",
"data": [ { "embedding": [0.001, -0.02, ...], "index": 0 }, ... ],
"model": "...",
"usage": { "prompt_tokens": 12, "total_tokens": 12 }
}
{provider} is one of openai, mistral, gemini, huggingface, bedrock, vertexai, azure. input accepts either a JSON array of strings or a single bare string, matching OpenAI's real input: string | string[] contract — so embedQuery()-style single-string calls (LangChain, LlamaIndex, etc.) work without modification.
Example, OpenAI:
curl https://your-zerocache-host/openai/v1/embeddings \
-H "Authorization: Bearer sk-your-real-openai-key" \
-H "Content-Type: application/json" \
-d '{"model": "text-embedding-3-small", "input": "hello world"}'Example, Bedrock (region + Cohere input_type encoded in model):
curl https://your-zerocache-host/bedrock/v1/embeddings \
-H "Authorization: Bearer your-bedrock-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "us-east-1/cohere.embed-english-v3#search_query", "input": ["find me something"]}'A matching DELETE /{provider}/v1/embeddings (identical body shape) removes the cache entries a matching POST would have hit, scoped to your own owner_id. Response: {"deleted": <count>} — the count of keys requested, not how many actually existed (deletion is idempotent).
POST /gemini/v1/images/embeddings
Authorization: Bearer <your Gemini API key>
Content-Type: application/json
{ "model": "gemini-embedding-001", "input": ["data:image/png;base64,<...>", ...] }
Every other provider 404s on this route with {"error": "provider '<name>' does not support image embeddings"}. Same DELETE, error-shape, and per-caller isolation semantics as the text endpoint.
Azure, Bedrock, and Vertex AI encode routing coordinates into the model string itself rather than a new wire field — model is already free-form per-request and already part of the cache key, so this needs no framework-specific changes on the client side:
{provider} |
Grammar | Example |
|---|---|---|
bedrock |
[<region>/]<modelId>[#<input_type>] |
us-east-1/cohere.embed-english-v3#search_query |
vertexai |
[<location>/<project>/]<modelId>[#<task_type>] |
us-central1/my-proj/text-embedding-005#RETRIEVAL_DOCUMENT |
azure |
[foundry:]<deployment>[#<input_type>] |
foundry:cohere-embed-v3-english#document |
The #<input_type>/#<task_type> qualifier is included deliberately: for the vendors that accept it, it's a required-or-near-required parameter that changes the output vector (e.g. document vs. query embeddings). Hardcoding one value would silently give every caller the same embedding style regardless of use case — a failure mode that's invisible until retrieval quality degrades. It's caller-controlled and folded into the cache key so document- and query-style embeddings of the same text are never confused with each other.
| Condition | Status | Body |
|---|---|---|
Missing/malformed Authorization |
401 |
{"error": "..."} |
Unknown {provider} |
404 |
{"error": "..."} |
| Malformed JSON | 400 |
{"error": "..."} |
| Valid JSON, missing/wrong-typed field | 422 |
{"error": "..."} |
| Malformed image data URI | 400 |
{"error": "..."} |
Every error path — including axum's own body-rejection errors — returns the same {"error": "..."} shape, never a bare plain-text response.
X-Zerocache-Hits/X-Zerocache-Misses— counts for this request's batch.usagein the body reflects only what was actually billed for this request:0for an all-hit batch,0for a request that piggybacked on another in-flight identical request (in-process coalescing — see below), and always0for Gemini, which never reports token usage on any endpoint.
Concurrent requests that miss on the exact same cache key within one Zerocache instance are coalesced into a single upstream call — proven with a dedicated test asserting exactly one provider call across 5 genuinely-overlapping concurrent requests. This is in-process only; two different replicas behind a load balancer each still fetch independently (cross-replica coalescing would need a distributed lock and isn't built).
GET /health liveness — 200 OK means only the process/router is up, zero I/O
GET /ready readiness — 200 OK if the configured store answers a get(); 503 otherwise
GET /metrics Prometheus text format
Built from the repo-root Dockerfile: a multi-stage build using cargo-chef for dependency-layer caching (so a source-only change doesn't force a full dependency recompile), a debian:bookworm-slim runtime stage, a non-root zerocache user (uid 10001), and a curl-based HEALTHCHECK against /health that respects a custom ZEROCACHE_PORT. Published to GitHub Container Registry:
docker pull ghcr.io/shramanb113/zerocache:latest
# or pin to an exact commit:
docker pull ghcr.io/shramanb113/zerocache:<commit-sha>ZEROCACHE_STORAGE_BACKEND=sled (the default) is embedded and single-process — each replica keeps its own private cache, which is fine for a single instance but means replicas never share hits. For any deployment with more than one instance, set ZEROCACHE_STORAGE_BACKEND=redis and point ZEROCACHE_REDIS_URL at a shared Redis: it's connection-pooled with no distributed locking, since content-addressed keys mean two replicas racing to fill the same key both compute the same value — a last-write-wins SET is always safe.
GET /health / GET /ready are wired for standard liveness/readiness probes; GET /metrics is Prometheus text format with provider/content_type labels — scrape every pod and aggregate with sum() for a fleet-wide hit rate.
Two GitHub Actions workflows, both under .github/workflows/:
ci.yml— on every push and pull request tomaster:cargo build --workspace,cargo test --workspace, the real-Redis integration suite (cargo test -p zerocache-adapters-redis -- --ignored, using the runner's built-in Docker),cargo clippy --workspace --all-targets -- -D warnings, andcargo fmt --check— five independent, individually-required jobs.docker-publish.yml— afterci.ymlsucceeds on a genuine push tomaster(explicitly not on pull requests, closing off a fork-PR path to an unreviewed publish), builds and pushes the image toghcr.io/shramanb113/zerocache, tagged with bothlatestand the commit SHA.
GET /health— zero-I/O liveness.200means only "the process and router are up."GET /ready— real readiness: calls the configured store'sget()against a reserved sentinel key.200on a miss (healthy — the key was never written),503on a genuine store-level error.GET /metrics— Prometheus counters, labeled byproviderandcontent_type(text/image):zerocache_cache_hits_total,zerocache_cache_misses_total,zerocache_provider_prompt_tokens_total. Deliberately no owner/tenant label — that would leak tenant identity into a monitoring system and create unbounded cardinality.- OpenTelemetry tracing — set
OTEL_EXPORTER_OTLP_ENDPOINTto enable OTLP/gRPC export; unset means console-only logging, no collector required to run locally. Every HTTP request gets its own span, withstore_lookup/provider_call/store_write_backnested underneath andhits/misses/claimed/piggybackedrecorded as fields.
Ordered so cheap, deterministic layers run first:
- Core — pure unit tests, no I/O (key derivation, owner/provider/cache-scope isolation, image domain-separation).
- Application — orchestration logic against mock ports (hit/miss splitting, ordering, coalescing, within-batch dedup, failure propagation).
- Adapters —
sledagainst a real embedded store; every provider adapter against a stubbed HTTP server (httpmock); Redis against a genuine ephemeral container viatestcontainers(#[ignore]d by default so the documentedcargo test --workspaceneeds no external services — run explicitly with-- --ignored). - End-to-end, real consumers — not synthetic examples: a TypeScript/Mastra RAG pipeline (including an agentic battle-test driving Zerocache through
Agenttool calls, not a direct embedding client), a second independent TypeScript project on LangChain, and a Python/LlamaIndex pipeline against a different provider (Gemini) — proving the "any framework, any language" neutrality claim rather than just asserting it.
At the time of writing: 196 tests passing + 7 real-Redis integration tests, zero cargo clippy -- -D warnings findings, across 13 crates.
The three cloud provider adapters (Azure, Bedrock, Vertex AI) ship mock-only — none has had a live-key smoke test, since real credentials for those three clouds aren't available in this project's development environment. Every wire shape was verified directly against each vendor's own current documentation at implementation time, with a follow-up re-verification pass that caught and fixed a real stale-docs defect (a wrong Vertex AI endpoint-host derivation for global/us/eu locations). Treat this caveat as real: if you deploy one of these three, a first live smoke test against your own credentials before production traffic is a reasonable precaution.
Phase 1 complete. Validated against three independently-built, real consumers across two languages (see Testing above). Production-trust basics are in place: provider timeouts, graceful shutdown (SIGTERM-aware), /health + /ready, request coalescing, retry/backoff with exponential backoff, OpenTelemetry tracing. All seven provider adapters (OpenAI, Mistral, Gemini, HuggingFace, Azure, Bedrock, Vertex AI) are implemented and registered. Docker image and CI/CD pipeline are live.
See PRD.md for the full product spec and success criteria, CLAUDE.md for the complete architecture and decision log (every deviation from the original spec, with rationale), and decisions.md for the reasoning behind the multi-tenant, multi-provider, BYOK design.
- Live/conversational query embedding caching.
- Semantic/fuzzy similarity matching — exact-match only.
- Vector quantization/compression, eviction.
- Multi-provider failover (automatic fallback to a second provider if the first fails) — multi-provider support itself is fully implemented; failover is a different, separate feature.
- Per-tenant rate limiting or quota enforcement.
- Any Zerocache-specific SDK or client package — if a consumer needs to install one, the neutrality goal has failed.
See PRD.md §4 for the full rationale.
CLAUDE.md— architecture notes and a full, dated log of every deviation from the original spec, aimed at any future contributor (human or AI) who needs to understand why the code looks the way it does before changing it.decisions.md— the reasoning behind major design calls (multi-tenancy, BYOK, storage backend choice, the cloud-adapter strategy pattern).PRD.md— the original product spec and phasing.
Development loop:
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --checkAll four are exactly what CI runs on every push and pull request — a green local run is a strong (though not complete, since the real-Redis job also needs Docker) predictor of a green CI run.
No license file exists in this repository yet — until one is added, treat the code as all-rights-reserved rather than assuming permissive reuse.