LLM caching proxy¶
rostam-server llm-proxy runs Rostam as an OpenAI-compatible caching reverse
proxy: point your existing OpenAI SDK or curl calls at it instead of
api.openai.com, and an eligible chat-completions request — one with a
cacheable shape, a matching scope, and a prompt
that hits the cache (byte-identical to a prior prompt in exact mode; a
near-duplicate within the configured threshold in semantic mode) — is
answered from a local semantic cache instead of hitting the upstream API —
zero generation cost, no round trip. Everything else (every other /v1/*
route, and any chat request the cache can't safely answer or that doesn't
match) is forwarded to the real upstream verbatim.
With no embedder configured it still does useful work out of the box: exact byte-identical prompts are cached and served locally, no API key or embedding model required. Point it at an embedder and it upgrades to semantic matching — near-duplicate prompts (different whitespace, minor rewording) hit the cache too.
Quickstart¶
Change one line in the OpenAI Python SDK:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8484/v1", api_key="sk-...")
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What is the capital of France?"}],
)
Or with curl:
rostam-server llm-proxy &
curl http://localhost:8484/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What is the capital of France?"}]}'
The response carries an x-rostam-cache header (hit, miss, uncacheable,
or bypass) so you can see, at a glance, which requests are actually costing
you generation tokens.
Exact vs. semantic mode¶
The mode is decided at startup from whether an embedder is configured — see Embedder configuration below:
- Exact mode (default, no
ROSTAM_EMBED_*env set): prompts are matched with a deterministic stub embedder at a0.999cosine-similarity floor, so only a byte-identical prompt (within the same scope) is ever treated as a hit. - Semantic mode (
ROSTAM_EMBED_ENDPOINTorROSTAM_EMBED_LOCALset): prompts are matched with a real embedder at a0.97floor by default, so a paraphrased or lightly-edited prompt can still hit the cache. The embedder is either a hosted OpenAI-compatible endpoint or the in-process, pure-Go local embedder (ROSTAM_EMBED_LOCAL) — the latter runs semantic caching with no endpoint and no API key.
-threshold overrides either mode's default floor.
What forms the cache key¶
A cache entry is scoped to more than just the prompt text. Two requests only match if both their prompt and their scope agree:
- Prompt: every non-system message's role and content, concatenated in
order. Only the plain-string form of
contentis used — a multimodal message (the array-of-parts form: images, etc.) can't be reduced to text, so it's never cached. - Scope:
model, the concatenatedsystemmessage(s), the requestedtools' names, the effectivetemperature,max_tokens, a hash of the caller's credential headers (tenancy — see below), the embedder identity (so switching embedders never mixes cached answers from a different embedding space), and a hash of every remaining field in the request body.
That last part matters: response_format, seed, top_p, stop,
frequency_penalty and anything else a provider adds all partition the cache,
so a request asking for JSON is never answered with prose cached from the
identical messages. Only messages (that's the prompt) and stream /
stream_options are excluded — the streaming and non-streaming forms of one
call deliberately share an answer. The hash is computed over the parsed body,
so key order and whitespace don't matter.
Two prompts that are textually identical but differ in model, system prompt, tools, any sampling parameter, or caller never share a cache entry.
Flags¶
| Flag | Default | Meaning |
|---|---|---|
-listen |
127.0.0.1:8484 |
HTTP listen address for the proxy. |
-upstream |
https://api.openai.com |
Upstream OpenAI-compatible API base URL. |
-collection |
llm-cache |
Cache collection name (created if absent). |
-threshold |
0 |
Cosine-similarity hit floor. 0 = mode default: 0.999 in exact mode, 0.97 in semantic mode. |
-max-temp |
1.0 |
Do not cache chat requests with a temperature above this. |
-ttl |
168h |
Per-entry cache expiry. |
-data |
auto |
Embedded data directory. auto resolves to ~/.rostam/llmcache (created if missing); "" runs heap/ephemeral mode; any other value is used as given. Mutually exclusive with -connect. |
-connect |
disabled | Connect to a remote rostam-server instead of embedding the engine (host:port). Mutually exclusive with -data. |
-auth-token |
— | Bearer token for -connect. Prefer ROSTAM_AUTH_TOKEN — a flag-passed secret is visible to other local users via /proc and shell history. |
-tls-ca |
— | CA bundle PEM to verify the remote server's certificate (-connect). |
-tls-cert / -tls-key |
— | Client certificate/key PEM for mTLS (-connect; both required together). |
-tls-server-name |
— | Expected server certificate name (SNI + verification) for -connect. |
-insecure |
false |
Acknowledge running with a non-loopback -listen. The proxy has no auth layer of its own (it relays whatever Authorization header the client sends straight to upstream), so binding it to a reachable address without this flag is refused at startup. Dev/trusted-network use only. |
-data and -connect are mutually exclusive: pass one or the other, never
both.
Embedder configuration (environment only)¶
Like the MCP server, the
embedder is configured entirely through environment variables, using the
same ROSTAM_EMBED_* variables:
| Variable | Required | Meaning |
|---|---|---|
ROSTAM_EMBED_LOCAL |
trigger | In-process, pure-Go local embedder — a catalog name (minilm-l6-v2, …) or any Hugging Face org/model id. No endpoint, no API key. See Local embeddings. Mutually exclusive with ROSTAM_EMBED_ENDPOINT. |
ROSTAM_EMBED_ENDPOINT |
trigger | OpenAI-compatible /embeddings URL (hosted embedder). |
ROSTAM_EMBED_MODEL |
if endpoint set | Model id sent to the endpoint. |
ROSTAM_EMBED_DIM |
if endpoint set | Output embedding dimension, as an integer. |
ROSTAM_EMBED_API_KEY |
optional | Bearer token for the endpoint (local endpoints like Ollama typically don't need one). |
With neither trigger set, the zero-config default is exact-match caching, no
embedding API key required. ROSTAM_EMBED_LOCAL and ROSTAM_EMBED_ENDPOINT
are mutually exclusive (setting both fails at startup); setting
ROSTAM_EMBED_ENDPOINT without ROSTAM_EMBED_MODEL or ROSTAM_EMBED_DIM
fails at startup naming the exact missing variable.
Example: the in-process local embedder — semantic caching with no cloud
service or key (weights download from the Hugging Face Hub on first use; see
Local embeddings for the model catalog and
REMBED_CACHE):
Example: a local Ollama embedding model for semantic matching:
ROSTAM_EMBED_ENDPOINT=http://localhost:11434/v1/embeddings \
ROSTAM_EMBED_MODEL=nomic-embed-text \
ROSTAM_EMBED_DIM=768 \
rostam-server llm-proxy
What's never cached¶
The following requests are always forwarded to upstream and never answered from (or written to) the cache:
- A response with
tool_callspresent. ngreater than 1 (multiple choices can't be represented by one cached answer).stream_options(e.g.{"include_usage": true}) — it asks for a stream shape a cache replay cannot reproduce.- Non-string message content — the array-of-parts (multimodal) form.
- A response whose
finish_reasonisn't"stop"(truncated, length-limited, content-filtered, etc.). - A streamed response that aborts mid-stream (never reaches a clean
[DONE]).
Temperature and cacheability¶
OpenAI treats an omitted temperature field as 1.0, and the proxy follows
the same rule for cache scoping. Combined with the default -max-temp 1.0,
that means a typical request with no explicit temperature is cached by
default — high-temperature, "creative" requests are exactly the ones most
API traffic sends without setting the field at all.
If you only want to cache fully-deterministic requests, tighten the ceiling:
With -max-temp 0, only requests that explicitly set "temperature": 0
are cached; anything with a higher (or omitted, i.e. 1.0) temperature
always passes through.
Tenancy¶
Cache entries are scoped by caller: the proxy hashes the request's credential
headers — Authorization, api-key (Azure OpenAI) and x-api-key — never
storing any of them raw, and includes that hash in the scope key, so one
client's cached answers are never served to another client using a different
key. All three are covered because an OpenAI-compatible surface is not only
OpenAI: a proxy that looked at Authorization alone would put every Azure
caller in the same tenant. Requests carrying none of the three share one "no
auth" tenant scope.
/stats¶
GET /stats returns the proxy's running counters as JSON:
{
"hits": 42,
"misses": 130,
"stored": 118,
"uncacheable": 9,
"tokens_saved": 15872,
"mode": "exact"
}
tokens_saved sums the completion-token count of every cache hit — the
generation cost you didn't pay. mode is "exact" or "semantic", fixed
for the life of the process.
Remote mode¶
By default rostam-server llm-proxy embeds the engine and owns a local data
directory. Point it at an already-running server instead with -connect,
the same way as the MCP server:
This is also how multiple proxy processes share one cache: a -data
directory has a single writer (see below), so concurrent proxies sharing a
cache must all -connect to one rostam-server rather than each embedding
their own.
Honest limits¶
- Cache entries do not survive a proxy restart in v1. The semantic-cache
collection backing the proxy is created as an in-memory (non-persistent)
vector collection regardless of
-data— a real-datadirectory still claims the directory and persists other engine state, but the cache itself starts empty on every restart. Don't rely on it as a durable answer store. - One
-datadirectory, one writer. Embedded mode locks the data directory (the same mechanism the MCP server uses); a secondllm-proxyprocess pointed at the same directory is refused at startup. Run one process per-datadirectory, or share a cache across processes with-connectinstead. - OpenAI-compatible surface only. The proxy understands the
/v1/chat/completionsrequest/response shape (including its streaming SSE variant) well enough to cache it; every other route is opaque passthrough. It does not cache/v1/embeddings,/v1/completions, or any provider-specific endpoint. - No single-flight. Concurrent identical requests that all arrive before the first one's answer is stored each reach the upstream once — the cache deduplicates across time, not within a burst. A cold start that fans out N copies of the same prompt pays for N generations.
- Streaming replay doesn't preserve upstream cadence. A cache hit on a
stream: truerequest is replayed as a synthetic SSE stream with a fixed three-chunk shape (role, content, finish) — fewer, larger chunks than the original upstream stream produced. A client reading only the assembled content sees the same answer; a client sensitive to chunk-by-chunk timing or granularity will notice the difference.