Agent Cache (Python)
betterdb-agent-cache is the Python counterpart to @betterdb/agent-cache. Same architecture, same three cache tiers, same Valkey key format, same Monitor integration — different language. A TypeScript app and a Python app can share the same cache. It pairs with betterdb-agent-memory as the short-term (llm/tool/session) cache layer.
For the full design rationale (tradeoffs, storage layout, cluster behavior), see the TypeScript page — this page focuses on Python usage and the Python-only differences.
Prerequisites
- Valkey 7+ or Redis 6.2+ (no modules, no RediSearch, no RedisJSON)
- Or Amazon ElastiCache for Valkey / Redis
- Or Google Cloud Memorystore for Valkey
- Or Amazon MemoryDB
- Python >= 3.11
Installation
pip install betterdb-agent-cache
Optional extras install the provider/framework SDKs alongside the library:
pip install "betterdb-agent-cache[openai]"
pip install "betterdb-agent-cache[anthropic]"
pip install "betterdb-agent-cache[langchain]"
pip install "betterdb-agent-cache[langgraph]"
pip install "betterdb-agent-cache[llamaindex]"
pip install "betterdb-agent-cache[openai_agents]"
# Everything:
pip install "betterdb-agent-cache[all]"
Quick start
import asyncio
import json
import valkey.asyncio as valkey_client
from betterdb_agent_cache import AgentCache, TierDefaults
from betterdb_agent_cache.types import AgentCacheOptions
client = valkey_client.Valkey(host="localhost", port=6379)
cache = AgentCache(AgentCacheOptions(
client=client,
tier_defaults={
"llm": TierDefaults(ttl=3600),
"tool": TierDefaults(ttl=300),
"session": TierDefaults(ttl=1800),
},
# cost_table is pre-loaded for GPT-4o, Claude, Gemini, and 1,900+ others
))
async def main():
# LLM response caching
params = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": "What is Valkey?"}],
"temperature": 0,
}
result = await cache.llm.check(params)
if not result.hit:
response = await call_llm(params)
await cache.llm.store(params, response)
# Tool result caching
weather = await cache.tool.check("get_weather", {"city": "Sofia"})
if not weather.hit:
data = await get_weather(city="Sofia")
await cache.tool.store("get_weather", {"city": "Sofia"}, json.dumps(data))
# Session state
await cache.session.set("thread-1", "last_intent", "book_flight")
intent = await cache.session.get("thread-1", "last_intent")
asyncio.run(main())
The client owns the connection lifecycle — AgentCache does not open or close it, and there is no separate initialize() step.
Configuration reference
| Option | Type | Default | Description |
|---|---|---|---|
client | valkey.asyncio.Valkey | ValkeyCluster | required | Valkey async client instance |
name | str | 'betterdb_ac' | Key prefix for all Valkey keys |
default_ttl | int \| None | None | Default TTL in seconds. None = no expiry |
tier_defaults["llm"].ttl | int \| None | None | Default TTL for LLM cache entries |
tier_defaults["tool"].ttl | int \| None | None | Default TTL for tool cache entries |
tier_defaults["session"].ttl | int \| None | None | Default TTL for session entries |
cost_table | dict[str, ModelCost] | {} | Model pricing overrides. Merged on top of the built-in default table |
use_default_cost_table | bool | True | Use bundled default cost table sourced from LiteLLM. Set to False to disable |
telemetry.tracer_name | str | '@betterdb/agent-cache' | OpenTelemetry tracer name |
telemetry.metrics_prefix | str | 'agent_cache' | Prometheus metric name prefix |
telemetry.registry | CollectorRegistry \| None | default registry | prometheus_client registry to register metrics on |
ModelCost format
from betterdb_agent_cache import ModelCost
cost_table = {
"gpt-4o": ModelCost(input_per_1k=0.0025, output_per_1k=0.01),
"gpt-4o-mini": ModelCost(input_per_1k=0.00015, output_per_1k=0.0006),
}
A default cost table sourced from LiteLLM’s model_prices_and_context_window.json is bundled and refreshed on every release, so cost tracking works out of the box for 1,900+ models. cost_table entries override defaults for matching keys; set use_default_cost_table=False to disable the defaults entirely.
Cache tiers
Three tiers behind one connection, all async. Method names are the Python (snake_case) equivalents of the TypeScript API.
LLM cache — cache.llm
# Check for a cached response
result = await cache.llm.check(params)
# Store a response with token counts for cost tracking
await cache.llm.store(params, response, LlmStoreOptions(tokens={"input": 10, "output": 50}))
# Store a multi-part response (text + tool calls)
await cache.llm.store_multipart(params, blocks, LlmStoreOptions(...))
# Invalidate all entries for a model
deleted = await cache.llm.invalidate_by_model("gpt-4o")
Tool cache — cache.tool
result = await cache.tool.check("get_weather", {"city": "Sofia"})
await cache.tool.store("get_weather", {"city": "Sofia"}, json_result, ToolStoreOptions(ttl=300, cost=0.001))
await cache.tool.set_policy("get_weather", ToolPolicy(ttl=600))
deleted = await cache.tool.invalidate_by_tool("get_weather")
existed = await cache.tool.invalidate("get_weather", {"city": "Sofia"})
Session store — cache.session
Key-value storage for agent session state with a sliding-window TTL (fields are individual Valkey keys, enabling per-field TTL).
await cache.session.set("thread-1", "last_intent", "book_flight")
intent = await cache.session.get("thread-1", "last_intent") # refreshes TTL on hit
allf = await cache.session.get_all("thread-1")
await cache.session.delete("thread-1", "last_intent")
deleted = await cache.session.destroy_thread("thread-1") # includes LangGraph checkpoints
await cache.session.touch("thread-1")
Stats and self-optimization
stats = await cache.stats()
# AgentCacheStats(llm=TierStats(hits=..., misses=...), tool=..., session=...,
# cost_saved_micros=..., per_tool={...})
ranked = await cache.tool_effectiveness()
# [ToolEffectivenessEntry(tool="get_weather", hit_rate=0.85, cost_saved=5.00,
# recommendation="increase_ttl"), ...]
Adapters
All adapters are submodule imports under betterdb_agent_cache.adapters with optional peer dependencies. The prepare_params(...) adapters normalize provider request shapes into LlmCacheParams that you pass to cache.llm.check() / store(); the BetterDB* classes plug directly into a framework’s cache/checkpoint interface.
| Adapter | Import | Kind |
|---|---|---|
| OpenAI Chat Completions | adapters.openai | prepare_params |
| OpenAI Responses API | adapters.openai_responses | prepare_params |
| OpenAI Agents SDK | adapters.openai_agents | CachedModel / CachedModelProvider |
| Anthropic Messages | adapters.anthropic | prepare_params |
| LlamaIndex | adapters.llamaindex | prepare_params |
| LangChain | adapters.langchain | BetterDBLlmCache |
| LangGraph | adapters.langgraph | BetterDBSaver |
Note on parity: the Python package ships the OpenAI Agents SDK adapter, which has no TypeScript equivalent (the Agents SDK is Python). Conversely, the TypeScript package ships a Vercel AI SDK adapter, which is JavaScript-only. Every other adapter exists in both languages.
OpenAI Chat Completions
from betterdb_agent_cache.adapters.openai import prepare_params
cache_params = await prepare_params(openai_params)
result = await cache.llm.check(cache_params)
OpenAI Responses API
from betterdb_agent_cache.adapters.openai_responses import prepare_params
cache_params = await prepare_params(responses_params)
OpenAI Agents SDK
Caches at the Agents SDK Model.get_response() level, so agent workloads that replay the same tool-call sequences (evaluation, testing, multi-agent orchestration) skip the API entirely. Requires the openai-agents peer dependency (pip install "betterdb-agent-cache[openai_agents]").
Recommended — wrap the model provider so every model an agent run resolves is cache-enabled:
from agents import Agent, Runner, RunConfig
from betterdb_agent_cache.adapters.openai_agents import CachedModelProvider
cached_provider = CachedModelProvider(provider, cache=cache)
result = await Runner.run(
agent,
"Hello",
run_config=RunConfig(model_provider=cached_provider),
)
Or wrap a single model directly:
from agents import Agent
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from betterdb_agent_cache.adapters.openai_agents import CachedModel
base_model = OpenAIChatCompletionsModel(model="gpt-4o", openai_client=client)
agent = Agent(name="Assistant", model=CachedModel(base_model, cache=cache))
get_response() is checked against the cache before calling the model and stored on a miss (fail-open: a store failure logs and returns the live response, never crashing the run). stream_response() is delegated uncached, matching the BetterDB streaming convention. Tools, handoffs, output_schema, and server-side context references (previous_response_id, conversation_id) are excluded from the cache key — safe when one CachedModel wraps a single agent whose tools don’t change between calls. If server-side context affects your responses, use a separate CachedModel per conversation thread.
Anthropic Messages
from betterdb_agent_cache.adapters.anthropic import prepare_params
cache_params = await prepare_params(anthropic_params)
LlamaIndex
from betterdb_agent_cache.adapters.llamaindex import prepare_params
cache_params = await prepare_params(messages)
LangChain
from betterdb_agent_cache.adapters.langchain import BetterDBLlmCache
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini", cache=BetterDBLlmCache(cache=cache))
LangGraph
Works on vanilla Valkey 7+ with no modules. Unlike langgraph-checkpoint-redis, it does not require Redis 8.0+, RedisJSON, or RediSearch.
from betterdb_agent_cache.adapters.langgraph import BetterDBSaver
from langgraph.graph import StateGraph
checkpointer = BetterDBSaver(cache=cache)
graph = StateGraph(schema).add_node("agent", agent_node).compile(checkpointer=checkpointer)
Binary normalizer
Controls how binary content (images, audio, documents) is reduced to a stable string before hashing. Zero-latency by default — no network calls.
from betterdb_agent_cache import compose_normalizer, hash_base64, fetch_and_hash
normalizer = compose_normalizer({"base64": hash_base64}) # hash base64 bytes
normalizer = compose_normalizer({"url": fetch_and_hash}) # fetch + hash URLs (needs aiohttp)
Cluster support
Pass a ValkeyCluster client and all SCAN-based operations (flush, invalidate_by_model, invalidate_by_tool, destroy_thread, touch) automatically iterate all master nodes. No configuration changes needed.
from valkey.asyncio.cluster import ValkeyCluster
client = ValkeyCluster(host="my-cluster.example.com", port=6379)
cache = AgentCache(AgentCacheOptions(client=client))
Observability
Every public method emits an OpenTelemetry span (snake_case names, e.g. agent_cache.llm.check, agent_cache.session.destroy_thread) and updates Prometheus metrics prefixed with agent_cache_ (configurable via telemetry.metrics_prefix). Spans require an OpenTelemetry SDK configured in the host application. See the TypeScript page for the full span and metric tables — the names match.
BetterDB Monitor integration
Connect BetterDB Monitor to the same Valkey instance and it will automatically detect the agent cache stats hash ({name}:__stats) and surface hit rates, cost savings, and per-tool effectiveness in the dashboard. No additional configuration is required.
Known limitations
- Session
get_all()is SCAN-based — fine for dozens of fields per thread; consider a Redis HASH for thousands. - LangGraph
list()loads all checkpoint data for a thread into memory before filtering. For millions of checkpoints, uselanggraph-checkpoint-rediswith Redis 8+ instead. active_sessionsgauge is approximate and does not survive process restarts.- Streaming responses are not cached by any Python adapter — accumulate the full response before storing. (The TypeScript Vercel AI SDK adapter is the only one that caches streams.)