LLM Caching
A Redis-based caching library for PydanticAI LLM agents with cost tracking support.
Caching responses is particularly useful in testing and development scenarios.
Typically for tests, developers mock LLM results to avoid latency and cost issues. However this can result in tests not detecting incorrect schemas for mocked data nor potential changes in LLM response schemas.
A cached response allows us to run the same prompts time and again without the cost or latency while being sure of real-world LLM responses.
Simply use cached_agent_run (async) or cached_agent_run_sync (sync) as a drop-in replacements for PydanticAI's agent.run() and agent.run_sync() respectively, to add support for caching, rate-limiting, and cost tracking.
NOTE: cached_agent_run and cached_agent_run_sync always return the complete result object, including data, usage information, and metadata.
Features
- Redis-based caching for PydanticAI Agent responses
- Flexible expense tracking
- Rate limit handling with exponential backoff
- Customizable cost tables for different models
- Type-safe implementation
- Comprehensive test coverage
Installation
pip install pyai-caching
Quick Start
Set an Environment variable to point to your redis cache:
export LLM_CACHE_REDIS_URL="redis://localhost:6379/0"
import os
from pydantic import BaseModel, Field
from pydantic_ai import Agent
from pyai_caching import cached_agent_run
from typing import List
class UserProfile(BaseModel):
name: str
age: int
interests: List[str]
profiler_agent = Agent(
model="anthropic:claude-haiku-4-5",
output_type=UserProfile,
name="profiler",
system_prompt="You read transcripts and extract pertinent details for a profile record on a person."
)
# The function returns the complete result object
result = await cached_agent_run(
agent=profiler_agent,
prompt="Make a profile on the user",
task_name="make_profile",
message_history=[{
"role": "user",
"content": "Hi, my name is Alex. I'm 30 years old and I enjoy hiking and reading science fiction."
}]
)
# Access the typed data from the result
profile = result.output
print(type(profile))
# <class '__main__.UserProfile'> (or similar based on execution context)
print(profile)
# name='Alex' age=30 interests=['hiking', 'reading science fiction']
# Access metadata from the result object
print(result.model) # The model used
print(result.usage) # Token usage information
print(result.cost) # The cost of the request
Configuration
Redis Configuration
The library requires a Redis URL to be configured. You can provide it in two ways:
- Environment variable (recommended):
export LLM_CACHE_REDIS_URL="redis://localhost:6379/0"
- Direct configuration in code:
# Example using async version
result = await cached_agent_run(
agent=your_agent,
prompt="Hello",
task_name="chat",
redis_url="redis://localhost:6379/0"
)
Supported URL formats:
redis://[[username]:[password]]@localhost:6379/0rediss://hostname:port/0# SSL/TLS connectionredis+sentinel://localhost:26379/mymaster/0
Cost Configuration
The library comes with default cost tables for popular models. You can provide custom costs for your models:
custom_costs = {
"my-custom-model": ModelCosts(
cost_per_million_input_tokens=1.0,
cost_per_million_output_tokens=2.0,
cost_per_million_caching_input_tokens=0.5,
cost_per_million_caching_hit_tokens=0.1,
)
}
# Use custom costs
result = await cached_agent_run(
agent=your_agent,
prompt="Hello",
task_name="chat",
custom_costs=custom_costs
)
Advanced Usage
Rate Limit Handling
The library includes built-in rate limit handling with exponential backoff:
result = await cached_agent_run(
agent=your_agent,
prompt="Hello",
task_name="chat",
max_wait=30.0, # Maximum wait time before giving up
initial_wait=1.0 # Initial wait time for exponential backoff
)
Expense Tracking
Implement custom expense tracking:
import logging
from datetime import datetime
async def expense_tracker(model: str, task_name: str, cost: float) -> None:
logging.info(f"Expense: {datetime.now()} - Model: {model}, Task: {task_name}, Cost: ${cost}")
# Add your expense tracking logic here
# e.g., save to database, send to monitoring service, etc.
result = await cached_agent_run(
agent=your_agent,
prompt="Hello",
task_name="chat",
expense_recorder=expense_tracker
)
Migration Guide
Version 0.2.0 Changes
-
Complete Result Objects
- Both
cached_agent_runandcached_agent_run_syncnow always return the complete result object - The result object includes:
data: The typed response datausage: Token usage informationmetadata: Any additional model-specific metadata
- Both
-
Simplified Parameter Structure
- Removed
transcript_historyparameter (usemessage_historyinstead) - Removed
message_converterparameter (message conversion is now handled internally) - All additional parameters are passed directly to
agent.runvia**kwargs
- Removed
-
Message History Handling
- Message history is now passed directly via the
message_historyparameter - Messages are automatically converted to the appropriate format
- The cache key incorporates the message history to ensure unique caching per conversation context
- Message history is now passed directly via the
Example of migrating from 0.1.x to 0.2.0:
# Old code (0.1.x)
result = await cached_agent_run(
agent=agent,
prompt="Hello",
task_name="chat",
transcript_history=["User: Hi", "Assistant: Hello!"],
message_converter=my_converter,
full_result=True
)
# New code (0.2.0)
result = await cached_agent_run(
agent=agent,
prompt="Hello",
task_name="chat",
message_history=[
ModelRequest(parts=[UserPromptPart(content="Hi")]),
ModelResponse(parts=[TextPart(content="Hello!")])
]
)
Error Handling
The library provides specific exceptions for different error cases:
from pyai_caching.exceptions import UsageLimitExceeded, ConfigurationError
try:
result = await cached_agent_run(
agent=your_agent,
prompt="Hello",
task_name="chat"
)
except UsageLimitExceeded:
print("Rate limit exceeded and max wait time reached")
except ConfigurationError:
print("Redis URL not configured")
except ValueError as e:
print(f"Invalid input: {e}")
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
For local development, install uv, then:
uv sync
uv run pre-commit install
Run the test suite with uv run pytest. See CONTRIBUTING.md for the full workflow.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for version history.
Running Tests
# All tests
uv run pytest
Metadata
Release files for pyai-caching 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyai_caching-0.4.1.tar.gz | 21.0 kB | With provenance | Details for pyai_caching-0.4.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyai_caching-0.4.1-py3-none-any.whl | Python 3 | none | any | Details for pyai_caching-0.4.1-py3-none-any.whl |
Total release size: 37.6 kB
Release files / pyai_caching-0.4.1.tar.gz
| Download URL | pyai_caching-0.4.1.tar.gz |
|---|---|
| Size | 21.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
04a968274a7b44a4d32374e65ad188a4ae9d2d8bd5ad078b7683ce43981c2142
|
|
BLAKE2b-256 checksum How to use checksums |
fcf771e383d61d43f1cbcab60007440df4287f7a1061462788c69d4e2a7abae0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / pyai_caching-0.4.1-py3-none-any.whl
| Download URL | pyai_caching-0.4.1-py3-none-any.whl |
|---|---|
| Size | 16.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0046a97735142548d9b923179b0857531453f761d7c4d9c4ba3d466693051ea
|
|
BLAKE2b-256 checksum How to use checksums |
5f0509d9c90374b24b2348b3c27bef547eab3fc564c82c9450649aa4c8c41e01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logSecurity for pyai-caching 0.4.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyai_caching-0.4.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.