diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 2aa4f3e..6845b5b 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -3,6 +3,7 @@ name: Auto Tag on Merge on: push: branches: [main] + workflow_dispatch: jobs: tag: @@ -39,7 +40,9 @@ jobs: if: steps.check_tag.outputs.exists == 'false' run: | pip install build twine - python scripts/verify_release_v6.py + python -m build + python scripts/generate_sbom_v1.py --dist-dir dist + python scripts/verify_release_v1.py --dist-dir dist - name: Create and push tag if: steps.check_tag.outputs.exists == 'false' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f26cfe..aefe128 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,8 +161,11 @@ jobs: - run: pip install build twine "readme-renderer[md]" - run: python -m build - run: twine check --strict dist/* + - run: python scripts/generate_sbom_v1.py --dist-dir dist - name: Check package metadata - run: python scripts/check_package_metadata.py + run: python scripts/check_package_metadata.py --dist-dir dist + - name: Verify release readiness + run: python scripts/verify_release_v1.py --dist-dir dist - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 872da5c..ed4e687 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,7 @@ on: permissions: contents: read id-token: write + attestations: write jobs: pypi-publish: @@ -30,13 +31,22 @@ jobs: - name: Build binary wheel and source tarball run: python -m build + - name: Generate CycloneDX SBOM + run: python scripts/generate_sbom_v1.py --dist-dir dist + - name: Check package distribution run: | twine check --strict dist/* python scripts/check_package_metadata.py --dist-dir dist - python scripts/verify_release_v6.py --dist-dir dist + python scripts/verify_release_v1.py --dist-dir dist + + - name: Generate build provenance attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: "dist/*" - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true + attestations: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d5c0b..0716993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes are recorded here using [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.0.0] - 2026-11-06 + +### Added + +- **Public API Freeze & Contract Lock:** Frozen public API contract with `API_CONTRACT_VERSION = "1.0"`, automated signature locking against `tests/fixtures/api_contract_v1_0.json`, and package graduation from `Beta` to `Development Status :: 5 - Production/Stable` with `Typing :: Typed`. +- **Promotion of Qualified Surfaces:** Graduated all `0.5.0` candidate symbols in `PUBLIC_API_STABILITY` from `"stable-candidate"` to `"stable"`, including `SharedRedisCacheBackend` and `SharedCoordinationBackend` following `0.6.0` qualification. +- **Runtime Warnings for Pass-Through & Experimental Surfaces:** Added `LongCachePassThroughWarning` (emitted on instantiation/use of `CachedLLM`, `CachedChain`, `CachedQueryEngine`, `CachedState`, `StreamingCacheAdapter`, and `cache.cached_stream`) and `LongCacheExperimentalWarning` (emitted on `AsyncRedisCacheBackend`). +- **Format-2 Persistence Upgrade Guarantee:** Added `tests/test_v1_persistence_compat.py` verifying zero data loss, zero re-indexing, and bit-identical partition digests (`v2:`) for upgrading pre-populated SQLite and Redis caches. +- **Release Provenance & SBOM:** Added deterministic CycloneDX 1.5 JSON SBOM generation (`scripts/generate_sbom_v1.py`), `dist/SHA256SUMS.txt` manifests, and release verification via `scripts/verify_release_v1.py`. +- **Support & Migration Documentation:** Published `docs/semver-contract.md` (defining the 1.x stability contract) and `docs/migration-v1.0.md` (0.6.0 to 1.0.0 upgrade guide). + ## [0.6.0] - 2026-11-05 ### Added diff --git a/README.md b/README.md index e04f9fd..d20f5d6 100755 --- a/README.md +++ b/README.md @@ -268,15 +268,15 @@ cache = LongCache(config, context=context) - โœ… **SQLite Backend:** No external service, WAL mode, versioned manifests, transactional initialization; default backend - โœ… **Redis Backend:** Versioned durable response storage with TTL; one LongCache process per generation in default mode -- ๐Ÿงช **Shared Redis Mode (opt-in):** Journal-synchronized local indexes, guarded replay, and distributed singleflight across processes; under qualification -- โœ… **Async Redis Backend:** Experimental standalone storage interface; unsupported through `LongCache` +- โœ… **Shared Redis Mode (opt-in):** Journal-synchronized local indexes, guarded replay, and distributed singleflight across processes +- ๐Ÿงช **Async Redis Backend:** Experimental standalone storage interface (emits `LongCacheExperimentalWarning`); unsupported through `LongCache` - โœ… **In-Memory Backend:** Zero I/O, ideal for unit tests and CI/CD pipelines -- โœ… **LangChain Integration:** `CachedLLM` and `CachedChain` compatibility pass-through wrappers -- โœ… **LangGraph Integration:** `CachedState` compatibility interface; graph state is not cached -- โœ… **LlamaIndex Integration:** `CachedQueryEngine` preserves the engine interface without caching opaque state +- โœ… **LangChain Integration:** `CachedLLM` and `CachedChain` compatibility pass-through wrappers (emit `LongCachePassThroughWarning` by default) +- โœ… **LangGraph Integration:** `CachedState` compatibility pass-through interface; graph state is not cached +- โœ… **LlamaIndex Integration:** `CachedQueryEngine` compatibility pass-through wrapper; preserves the engine interface without caching opaque state - โœ… **OpenAI Integration:** Supported single-turn text requests with explicit context; sync/async dict and SDK replay - โœ… **FastAPI Middleware:** Bounded supported chat-completion JSON with trusted scope resolution and diagnostic headers -- โœ… **CLI:** `longcache stats`, `clear`, `inspect`, `export` โ€” manage the cache without writing code +- โœ… **CLI:** `longcache stats`, `clear`, `inspect`, `export`, `info` โ€” manage the cache without writing code --- @@ -345,13 +345,13 @@ with LongCache(config, context=context) as cache: | **Index** | Local FAISS; not synchronized with other processes | Local FAISS synchronized via Redis journal; journal gaps trigger a rebuild | | **Coalescing** | Within one process | Across processes via fenced leases (15 s lifetime, 5 s renewal by default) | | **Requires** | Redis | Dedicated non-evicting Redis; identical cache ID, generation, embedder, and policy settings | -| **Status** | Supported | Development; release qualification open | +| **Status** | Supported (`stable`) | Supported (`stable`) | Shared-mode limits: one Redis authority only. Redis Cluster, Sentinel, failover, and replica reads are out of scope. After a Redis restart or restore, stop all workers, choose a new generation, and run a canary. Callbacks must be safe to repeat, because after a lease expires a new owner may run the origin again. Manual lookup โ†’ generate โ†’ store calls are not coalesced. Full contract: [shared Redis deployment](docs/deployment.md). ### Async Redis backend status -`AsyncRedisCacheBackend` can be used and tested as a standalone storage backend (`experimental` tier in `PUBLIC_API_STABILITY`). It is not supported through `LongCache`; selecting `backend="async_redis"` fails immediately. End-to-end async Redis integration is planned for a later release. +`AsyncRedisCacheBackend` can be used and tested as a standalone storage backend (`experimental` tier in `PUBLIC_API_STABILITY`, emitting `LongCacheExperimentalWarning` on instantiation unless `warn_experimental=False`). It is not supported through `LongCache`; selecting `backend="async_redis"` fails immediately. End-to-end async Redis integration is planned for a later release. ### In-Memory (Testing & CI) @@ -369,7 +369,7 @@ cache = LongCache(CacheConfig(backend="memory")) ## Integrations -Generic LangChain, LangGraph, and LlamaIndex adapters below preserve their interfaces as `pass-through` compatibility wrappers (`cache_bypass_reason = "unsupported_request_shape"`). Use the explicit-context manual/decorator path or `CachedOpenAI` / `LongCacheMiddleware` for caching proven application workflows. All streaming helpers also pass through. See the [Support Matrix](docs/support-matrix.md). +Generic LangChain, LangGraph, and LlamaIndex adapters below preserve their interfaces as `pass-through` compatibility wrappers (`cache_bypass_reason = "unsupported_request_shape"`, `is_passthrough = True`) and emit `LongCachePassThroughWarning` by default (pass `warn_passthrough=False` to silence). Use the explicit-context manual/decorator path or `CachedOpenAI` / `LongCacheMiddleware` for caching proven application workflows. All streaming helpers (`@cache.cached_stream`, `StreamingCacheAdapter`) also pass through. See the [Support Matrix](docs/support-matrix.md) and [1.x SemVer Contract](docs/semver-contract.md). ### LangChain @@ -883,13 +883,10 @@ longcache/ โ”‚ โ”œโ”€โ”€ test_langchain.py โ”‚ โ”œโ”€โ”€ test_middleware.py โ”‚ โ”œโ”€โ”€ test_cli.py -โ”‚ โ”œโ”€โ”€ test_shared_redis_v4.py # two-process shared Redis acceptance -โ”‚ โ”œโ”€โ”€ test_v06_slice_a.py # paged vector loading and compaction bounds -โ”‚ โ”œโ”€โ”€ test_qualification_stress_v6.py # multi-process Redis crash/churn stress suite -โ”‚ โ””โ”€โ”€ test_examples_and_migrations_v6.py # 0.x migration and example validation +โ”‚ โ”œโ”€โ”€ test_api_freeze_v1.py # frozen public API signature & contract lock +โ”‚ โ””โ”€โ”€ test_v1_persistence_compat.py # Format-2 persistence compatibility suite โ”œโ”€โ”€ evaluation/ โ”‚ โ”œโ”€โ”€ run.py, rag.py, deployment.py -โ”‚ โ”œโ”€โ”€ qualification_v6.py # v0.6 qualification benchmark runner โ”‚ โ”œโ”€โ”€ cost_model.py # audited LLM net cost and latency model โ”‚ โ”œโ”€โ”€ reports/ โ”‚ โ””โ”€โ”€ configs/ @@ -975,18 +972,15 @@ LONGCACHE_TEST_REDIS_URL=redis://localhost:16379/0 LONGCACHE_TEST_MODEL=1 \ Shared Redis tests are skipped unless `LONGCACHE_TEST_REDIS_URL` points at a disposable Redis 7.2 service. They own only test-prefixed keys and never flush the database: ```bash -LONGCACHE_TEST_REDIS_URL=redis://localhost:16379/0 uv run --no-sync pytest tests/test_shared_redis_v4.py -q -rs LONGCACHE_TEST_REDIS_URL=redis://localhost:16379/0 uv run --no-sync python examples/multi_instance_rag.py ``` -### Examples and Release Qualification Verification +### Release Provenance & SBOM Verification ```bash -# Verify all 7 examples and README quickstart workflow -LONGCACHE_TEST_REDIS_URL=redis://localhost:16379/0 uv run --no-sync python scripts/wheel_examples_v6.py - -# Verify distribution build, hashes, changelog, and qualification benchmark evidence -uv run --no-sync python scripts/verify_release_v6.py --dist-dir dist --check-evidence +# Generate deterministic CycloneDX 1.5 SBOM and verify release artifacts, hashes, and benchmark evidence +uv run --no-sync python scripts/generate_sbom_v1.py --dist-dir dist +uv run --no-sync python scripts/verify_release_v1.py --dist-dir dist --check-evidence ``` --- diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 859f515..0000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/docs/api-reference.md b/docs/api-reference.md index 13c88fe..d6794b1 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -63,7 +63,7 @@ | `shared_max_tag_chars` | `256` | | `distributed_lease_seconds`, `distributed_renew_interval_seconds` | `15.0`, `5.0` | -Legacy `model`, `system_prompt`, `temperature`, and `track_miss` parameters were removed in v0.5.0; supply execution identity explicitly via `RequestContext`. See [Upgrading to v0.5](migration-v0.5.md) and the [Support Matrix](support-matrix.md) for public API stability classifications (`PUBLIC_API_STABILITY`). +Legacy `model`, `system_prompt`, `temperature`, and `track_miss` parameters were removed in v0.5.0; supply execution identity explicitly via `RequestContext`. In v1.0.0 (`API_CONTRACT_VERSION = "1.0"`), all qualified public interfaces are promoted to `"stable"` in `PUBLIC_API_STABILITY`, while compatibility pass-through shims emit `LongCachePassThroughWarning` and standalone experimental backends emit `LongCacheExperimentalWarning` (both subclasses of `LongCacheWarning(UserWarning)`). See [Upgrading to v1.0](migration-v1.0.md), the [1.x SemVer Contract](semver-contract.md), and the [Support Matrix](support-matrix.md) for full stability classifications. ## Policy and embedding interfaces diff --git a/docs/assets/logo.png b/docs/assets/logo.png deleted file mode 100644 index 859f515..0000000 Binary files a/docs/assets/logo.png and /dev/null differ diff --git a/docs/changelog.md b/docs/changelog.md index 0f6b61b..ecfb29d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,17 @@ All notable changes are recorded here using [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.0.0] - 2026-11-06 + +### Added + +- **Public API Freeze & Contract Lock:** Frozen public API contract with `API_CONTRACT_VERSION = "1.0"`, automated signature locking against `tests/fixtures/api_contract_v1_0.json`, and package graduation from `Beta` to `Development Status :: 5 - Production/Stable` with `Typing :: Typed`. +- **Promotion of Qualified Surfaces:** Graduated all `0.5.0` candidate symbols in `PUBLIC_API_STABILITY` from `"stable-candidate"` to `"stable"`, including `SharedRedisCacheBackend` and `SharedCoordinationBackend` following `0.6.0` qualification. +- **Runtime Warnings for Pass-Through & Experimental Surfaces:** Added `LongCachePassThroughWarning` (emitted on instantiation/use of `CachedLLM`, `CachedChain`, `CachedQueryEngine`, `CachedState`, `StreamingCacheAdapter`, and `cache.cached_stream`) and `LongCacheExperimentalWarning` (emitted on `AsyncRedisCacheBackend`). +- **Format-2 Persistence Upgrade Guarantee:** Added `tests/test_v1_persistence_compat.py` verifying zero data loss, zero re-indexing, and bit-identical partition digests (`v2:`) for upgrading pre-populated SQLite and Redis caches. +- **Release Provenance & SBOM:** Added deterministic CycloneDX 1.5 JSON SBOM generation (`scripts/generate_sbom_v1.py`), `dist/SHA256SUMS.txt` manifests, and release verification via `scripts/verify_release_v1.py`. +- **Support & Migration Documentation:** Published `docs/semver-contract.md` (defining the 1.x stability contract) and `docs/migration-v1.0.md` (0.6.0 to 1.0.0 upgrade guide). + ## [0.6.0] - 2026-11-05 ### Added diff --git a/docs/deployment.md b/docs/deployment.md index 3ccf0d0..f72b467 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,6 +1,6 @@ -# Shared Redis deployment (v0.6 qualification) +# Shared Redis deployment (`stable` in v1.0.0) -The v0.6 qualification milestone validates shared Redis mode for processes connected to one writable Redis authority. It has passed multi-process churn, fail-closed Redis crash/data-loss recovery, and bounded memory index rebuilds. The default remains `single_process`. +Qualified in v0.6.0 and promoted to `stable` in v1.0.0, shared Redis mode connects multiple processes to one writable Redis authority. It has passed multi-process churn, fail-closed Redis crash/data-loss recovery, and bounded memory index rebuilds. The default remains `single_process`. ```python from longcache import CacheConfig, LongCache, RequestContext, RequestScope diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index d0fd620..f56629d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,8 +1,8 @@ # Installation -LongCache v0.6 requires Python 3.11 or newer; Python 3.11 and 3.12 are the tested interpreters. v0.1.1 remains the Python 3.10-compatible line. +LongCache v1.0 requires Python 3.11 or newer; Python 3.11 and 3.12 are the supported and tested interpreters. v0.1.1 remains the legacy Python 3.10-compatible line. -v0.6.0 is the current qualification and hardening milestone. The package-index commands install the available published release; they do not select this development checkout. To install this branch from the repository root: +v1.0.0 is the Stable Foundation release (`Development Status :: 5 - Production/Stable`). To install from a local repository checkout: ```bash uv sync --locked --extra all --extra dev --extra docs @@ -15,6 +15,6 @@ uv pip install longcache uv pip install 'longcache[openai,server,redis]' ``` -The local model downloads weights on first use. Pin `embedder_revision` for a deployment, then cache the weights for offline operation. Custom embedders still use the current core package dependencies. See [upgrading to v0.5](../migration-v0.5.md) and the [support matrix](../support-matrix.md) before upgrading. Compatible format-2 stores from v0.2โ€“v0.4 require no reset. For v0.1.1 stores, follow the separate [legacy migration](../migration-v0.2.md); those stores require a fresh path/generation. +The local model downloads weights on first use. Pin `embedder_revision` for a deployment, then cache the weights for offline operation. Custom embedders still use the current core package dependencies. See [upgrading to v1.0](../migration-v1.0.md), [1.x SemVer contract](../semver-contract.md), and the [support matrix](../support-matrix.md) before upgrading. Compatible Format-2 stores from v0.2โ€“v0.6 require no reset. For v0.1.1 stores, follow the separate [legacy migration](../migration-v0.2.md); those stores require a fresh path/generation. For the tested CPU runtime, install `torch==2.14.0+cpu` from `https://download.pytorch.org/whl/cpu` before installing the wheel. Installer index settings in the project's lockfile do not become wheel dependency metadata. diff --git a/docs/index.md b/docs/index.md index e397e7e..9fead93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,12 @@ # LongCache -Semantic response reuse for Python LLM and RAG applications. **v0.6.0 is the current qualification line; Python 3.11 and 3.12 are the tested interpreters.** See the [Support Matrix](support-matrix.md), [Release Process & Runbook](release-process.md), and [Upgrading to v0.5](migration-v0.5.md). +Semantic response reuse for Python LLM and RAG applications. **v1.0.0 is the Stable Foundation release (`Production/Stable`); Python 3.11 and 3.12 are the supported and tested interpreters.** See the [Support Matrix](support-matrix.md), [1.x SemVer Contract](semver-contract.md), [Upgrading to v1.0](migration-v1.0.md), and [Release Process & Runbook](release-process.md). LongCache finds similar requests with local embeddings and FAISS, then checks whether their execution context and reuse policy permit the same answer. Similarity alone does not establish correctness. Measure false hits and useful reuse on your own workload. ## Install -v0.6.0 is currently the qualification and hardening milestone. Package-index commands install the available published release. See [installation](getting-started/installation.md) for installing and validating this branch. +v1.0.0 freezes the core public API and Format-2 persistence schemas under the [1.x SemVer Contract](semver-contract.md). See [installation](getting-started/installation.md) for installing and validating LongCache. ```bash uv pip install longcache @@ -92,17 +92,17 @@ Async applications use `await cache.alookup(...)`, `await cache.aget(...)`, and ## Supported integrations -| Path | v0.6 status | +| Path | v1.0 status | | ---- | ----------- | -| Manual lookup/store | Supported (`stable-candidate`) with explicit trusted context; independent origin calls | -| `get_or_compute` / `aget_or_compute` and query-only decorators | Supported (`stable-candidate`) with exact scoped singleflight | -| OpenAI sync/async chat completions (`CachedOpenAI`) | Supported (`stable-candidate`) single-turn text subset with dict/SDK response replay | -| ASGI middleware (`LongCacheMiddleware`) | Supported (`stable-candidate`) bounded single-turn JSON subset with trusted scope resolution; no HTTP invocation coalescing | -| Memory, SQLite, synchronous Redis storage | Supported (`stable-candidate`) within one cache process | -| Opt-in Shared Redis mode (`deployment_mode="shared"`) | Qualified candidate (`qualified-candidate`); multi-process deployment over Redis 7.2 with bounded memory, crash-recovery, and journal synchronization; see [shared deployment](deployment.md) | -| Standalone async Redis backend (`AsyncRedisCacheBackend`) | Experimental (`experimental`) storage interface; unsupported through `LongCache` | -| Generic LangChain, LangGraph, and LlamaIndex adapters | Compatibility pass-through (`pass-through`); opaque execution state is not cached | -| All streaming helpers (`cached_stream`, `StreamingCacheAdapter`) | Pass-through (`pass-through`); no cache reads or writes | +| Manual lookup/store | Supported (`stable`) with explicit trusted context; independent origin calls | +| `get_or_compute` / `aget_or_compute` and query-only decorators | Supported (`stable`) with exact scoped singleflight | +| OpenAI sync/async chat completions (`CachedOpenAI`) | Supported (`stable`) single-turn text subset with dict/SDK response replay | +| ASGI middleware (`LongCacheMiddleware`) | Supported (`stable`) bounded single-turn JSON subset with trusted scope resolution; no HTTP invocation coalescing | +| Memory, SQLite, synchronous Redis storage | Supported (`stable`) within one cache process | +| Opt-in Shared Redis mode (`deployment_mode="shared"`) | Supported (`stable`); multi-process deployment over Redis 7.2 with bounded memory, crash-recovery, and journal synchronization; see [shared deployment](deployment.md) | +| Standalone async Redis backend (`AsyncRedisCacheBackend`) | Experimental (`experimental`) storage interface (emits `LongCacheExperimentalWarning`); unsupported through `LongCache` | +| Generic LangChain, LangGraph, and LlamaIndex adapters | Compatibility pass-through (`pass-through`, emits `LongCachePassThroughWarning`); opaque execution state is not cached | +| All streaming helpers (`cached_stream`, `StreamingCacheAdapter`) | Pass-through (`pass-through`, emits `LongCachePassThroughWarning`); no cache reads or writes | | Multi-turn, tools, multimodal, structured-output replay | Bypass until their identity and replay contracts are supported | ### OpenAI @@ -135,9 +135,9 @@ Inject `LongCache(..., embedder=my_embedder)` using `BaseEmbedder`. Supply an `E One store generation has one embedding fingerprint. Equal dimensions do not make two models compatible. A revision or preprocessing change requires a fresh path/generation; mismatched stores fail before vector search or writes. -## Upgrade from v0.2โ€“v0.4 +## Upgrade from v0.2โ€“v0.6 -Compatible format-2 stores need no reset. See [upgrading to v0.5](migration-v0.5.md) for API signature cleanups, PEP 561 typing, and `PUBLIC_API_STABILITY` tiers, and [upgrading to v0.3](migration-v0.3.md) for execution, timeout, failure, and ownership behavior. Separate lookup/store code must opt into shared execution explicitly. +Compatible Format-2 stores (`.longcache/v2/cache.db` and Redis `longcache:v2:...` / `longcache:shared-v1:...` keyspaces) upgrade to v1.0.0 with zero data loss and require no cache reset. See [upgrading to v1.0](migration-v1.0.md) for the `PUBLIC_API_STABILITY` promotion (`"stable-candidate"` $\rightarrow$ `"stable"`) and `LongCachePassThroughWarning` runtime advisories, [upgrading to v0.5](migration-v0.5.md) for API signature cleanups and PEP 561 typing, and [upgrading to v0.3](migration-v0.3.md) for execution, timeout, failure, and ownership behavior. ## Legacy upgrade from v0.1.1 diff --git a/docs/migration-v1.0.md b/docs/migration-v1.0.md new file mode 100644 index 0000000..7b67ba0 --- /dev/null +++ b/docs/migration-v1.0.md @@ -0,0 +1,94 @@ +# Upgrading to LongCache v1.0.0 (Stable Foundation) + +Version `1.0.0` is the **first frozen stable public API and documented support contract** for LongCache. It graduates the package from Beta (`Development Status :: 4 - Beta`) to Production/Stable (`Development Status :: 5 - Production/Stable`). + +--- + +## 1. Zero Breaking Changes & Seamless Cache Persistence + +Upgrading from `v0.6.0` (or `v0.5.0`) to `v1.0.0` requires **zero code rewrites** and **zero cache resets**: + +* **Core APIs Unchanged:** All signatures for `LongCache`, `CacheConfig`, `RequestContext`, `RequestScope`, decorators, and backends remain identical to `v0.6.0`. +* **Format-2 Cache Persistence:** Existing SQLite databases (`.longcache/v2/cache.db`), single-process Redis stores, and shared Redis clusters (`deployment_mode="shared"`) upgrade cleanly with **zero data loss, zero re-indexing, and identical `v2:` partition digests**. + +--- + +## 2. API Stability Graduation (`stable-candidate` โ†’ `stable`) + +In `v0.5.0` and `v0.6.0`, public symbols in `longcache.PUBLIC_API_STABILITY` were marked `"stable-candidate"`. In `v1.0.0`: + +* All candidate interfaces have officially graduated to `"stable"`. +* In addition, `SharedRedisCacheBackend` and `SharedCoordinationBackend` (which passed full multi-process fault and memory bounds qualification in `0.6.0`) have graduated from `"experimental"` to `"stable"`. +* Standalone `AsyncRedisCacheBackend` remains `"experimental"`. + +If your codebase or test suite inspects `longcache.PUBLIC_API_STABILITY`, update checks to expect `"stable"`: + +```python +from longcache import PUBLIC_API_STABILITY + +# Recommended check across 0.6 and 1.0: +is_stable = PUBLIC_API_STABILITY["LongCache"] in ("stable", "stable-candidate") +``` + +--- + +## 3. Explicit Pass-Through Warnings (`LongCachePassThroughWarning`) + +LongCache includes compatibility adapters (`CachedLLM`, `CachedChain`, `CachedQueryEngine`, `CachedState`, `StreamingCacheAdapter`, and `cache.cached_stream`) for third-party frameworks whose opaque graph state cannot guarantee execution identity. + +To prevent operators from mistakenly believing opaque framework calls are cached in production, **LongCache 1.0 emits a filterable `LongCachePassThroughWarning` on instantiation or use**. + +### Example Warning +```text +LongCachePassThroughWarning: CachedLLM is a compatibility pass-through adapter in LongCache 1.0 +(reason='unsupported_request_shape') and does not cache or reuse responses. Use +LongCache.get_or_compute() or @cache.cached with an explicit RequestContext for verified +semantic caching, or pass warn_passthrough=False to silence this warning. +``` + +### How to Address This Warning + +#### Option A: Use Verified Semantic Caching (Recommended) +Migrate to `cache.get_or_compute()` or `@cache.cached` with an explicit `RequestContext`: + +```python +from longcache import LongCache, RequestContext, RequestScope + +context = RequestContext( + scope=RequestScope(tenant_id="tenant-1", user_id="user-1"), + operation="llm.chat", + provider="openai", + provider_scope="default", + model="gpt-4o", + prompt_version="v1", +) + +# Active semantic cache lookup and singleflight origin execution +answer = cache.get_or_compute( + query="Explain quantum computing simply", + compute=lambda: llm.invoke("Explain quantum computing simply"), + context=context, +) +``` + +#### Option B: Silence the Warning for Intentional Pass-Through Shims +If you are using these wrappers purely for structural framework compatibility: + +```python +# Pass warn_passthrough=False directly +cached_llm = CachedLLM(llm, cache=cache, warn_passthrough=False) + +# Or filter the warning globally in your application startup: +import warnings +from longcache import LongCachePassThroughWarning + +warnings.filterwarnings("ignore", category=LongCachePassThroughWarning) +``` + +--- + +## 4. Verification & Support Matrix + +* Review the [Support Matrix](support-matrix.md) for full platform, runtime, and stability tiers. +* Review the [1.x SemVer Contract](semver-contract.md) for long-term API stability and support guarantees. + diff --git a/docs/release-process.md b/docs/release-process.md index c75db7e..687e428 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -59,16 +59,29 @@ Build and verify the distribution packages: rm -rf dist/ uv run --no-sync python -m build uv run --no-sync twine check --strict dist/* +uv run --no-sync python scripts/generate_sbom_v1.py --dist-dir dist uv run --no-sync python scripts/check_package_metadata.py --dist-dir dist -uv run --no-sync python scripts/verify_release_v6.py --dist-dir dist --check-evidence +uv run --no-sync python scripts/verify_release_v1.py --dist-dir dist --check-evidence ``` This verifies: * `src/longcache/py.typed` is present in both `.whl` and `.tar.gz`. +* Deterministic CycloneDX 1.5 JSON SBOM is generated at `dist/longcache-.sbom.json`. * No test cache artifacts (`.pytest_cache`, `__pycache__`, `.longcache`, `*.db`) leaked into the archive. * Exact SHA256 checksums are generated in `dist/SHA256SUMS.txt`. * The benchmark evidence hash matches the current source tree. +### Verifying Release Artifacts and Provenance +Users and operators can cryptographically verify downloaded release distributions: + +```bash +# 1. Verify SHA-256 Checksums +sha256sum -c dist/SHA256SUMS.txt + +# 2. Verify GitHub Build Attestations (Sigstore / PEP 740) +gh attestation verify dist/longcache-1.0.0-py3-none-any.whl --owner ENDEVSOLS +``` + --- ## 4. PyPI Publishing (Trusted Publishing / OIDC) diff --git a/docs/semver-contract.md b/docs/semver-contract.md new file mode 100644 index 0000000..c2e36ea --- /dev/null +++ b/docs/semver-contract.md @@ -0,0 +1,82 @@ +# LongCache 1.x Semantic Versioning & Support Contract + +This document defines the official public API stability guarantee, backward-compatibility contracts, and support policies for **LongCache 1.x**. + +--- + +## 1. Semantic Versioning Commitment + +LongCache strictly adheres to [Semantic Versioning 2.0.0](https://semver.org/): + +$$\text{Format: } \mathbf{MAJOR.MINOR.PATCH}$$ + +* **MAJOR (e.g., 2.0.0):** Incompatible public API changes, schema format upgrades requiring offline migration, or removal of deprecated interfaces. +* **MINOR (e.g., 1.1.0, 1.2.0):** Additive, backward-compatible features (e.g., multi-turn conversation caching, telemetry exporters, new embedder presets, external vector store adapters). +* **PATCH (e.g., 1.0.1, 1.0.2):** Backward-compatible defect fixes, performance optimizations, and security patches. + +--- + +## 2. Public Contract Boundaries (`PUBLIC_API_STABILITY`) + +LongCache programmatically declares the stability tier of every exported symbol in `longcache.PUBLIC_API_STABILITY`: + +### 2.1 The Frozen "Stable" Surface (`tier == "stable"`) +Every symbol marked `"stable"` is frozen under the **1.x backward-compatibility guarantee**. Their signatures, parameter kinds, default values, return annotations, and behavioral semantics will not change incompatibly across any `1.x` release: + +1. **Cache Core & Config:** + * `LongCache`, `CacheConfig`, `__version__`, `API_CONTRACT_VERSION` +2. **Request Identity & Context:** + * `RequestScope`, `RequestContext`, `Instruction` +3. **Decisions & Storage Entries:** + * `LookupDecision`, `StoreDecision`, `PolicyDecision`, `CacheEntry` +4. **Embedder Interface:** + * `BaseEmbedder`, `EmbeddingDescriptor` +5. **Storage Backends:** + * `AbstractCacheBackend`, `SQLiteCacheBackend`, `RedisCacheBackend`, `SharedRedisCacheBackend`, `SharedCoordinationBackend`, `InMemoryCacheBackend`, `get_redis_backend` +6. **Active Integrations & Middleware:** + * `CachedOpenAI`, `get_openai_wrapper`, `LongCacheMiddleware` +7. **Error & Warning Hierarchy:** + * `CacheError`, `CacheClosedError`, `CacheShutdownTimeoutError`, `CacheCapacityError`, `CacheOperationError` + * `SingleflightError`, `SingleflightTimeoutError`, `SingleflightCapacityError`, `SingleflightLeaderError`, `SingleflightReentrancyError`, `SingleflightInvalidatedError` + * `LongCacheWarning`, `LongCachePassThroughWarning`, `LongCacheExperimentalWarning` +8. **Protocols & Typed Snapshots:** + * `AdmissionHook`, `VerificationHook`, `ContextResolver`, `ScopeResolver`, `CachedCallable` + * `InspectionReport`, `StatsSnapshot`, `ResourceSnapshot`, `MetricsSnapshot` + * `ScopePolicy`, `BackendName`, `DeploymentMode`, `ErrorPolicy`, `FaissIndexType`, `InstructionRole`, `ResponseCodec`, `LookupOutcome`, `StoreOutcome`, `PolicyAction`, `HealthStatus`, `Float32Vector` + +### 2.2 Experimental Surface (`tier == "experimental"`) +* `AsyncRedisCacheBackend`: Standalone low-level async storage backend. While stable in wire format, it is not orchestrated through `LongCache(config)` and may receive non-breaking enhancements. Instantiation emits a `LongCacheExperimentalWarning`. + +### 2.3 Pass-Through Compatibility Surface (`tier == "pass-through"`) +* `CachedLLM`, `CachedChain`, `CachedQueryEngine`, `get_llamaindex_wrapper`, `CachedState`, `StreamingCacheAdapter`, `LongCache.cached_stream` +* These adapters maintain structural interface compatibility with external frameworks (LangChain, LlamaIndex, LangGraph) whose opaque graphs cannot guarantee execution provenance. +* They **never cache or reuse responses** and emit a filterable `LongCachePassThroughWarning`. + +--- + +## 3. Persistence Wire & Schema Guarantee (Format-2) + +1. **SQLite (`.longcache/v2/cache.db`):** + * The Format-2 schema (tables `cache_entries` and `cache_manifest`) is frozen. Any `1.x` release will read and write existing `0.2.0`โ€“`0.6.0` and `1.0.0` databases without data loss or silent table resets. +2. **Redis (`longcache:v2:::*`):** + * The Redis keyspace, manifest payload structure, and journal serialization are frozen for single-process and multi-instance shared coordination. +3. **Partition & Exact-Key Digests (`v2:`):** + * The canonical JSON hashing rules governing `RequestContext.partition()` and `RequestContext.exact_key()` are frozen. Upgrading LongCache within `1.x` will never cause cache misses due to changed partition digests. + +--- + +## 4. Static Typing & Tooling Guarantee (PEP 561) + +* LongCache ships with the `py.typed` marker. +* All public exports pass `mypy --strict`. +* Consuming applications running `mypy --strict` or `pyright` will never encounter missing import type errors or unannotated public parameters. +* Typed return dictionaries (`InspectionReport`, `StatsSnapshot`, etc.) may receive additive optional keys in minor releases, but existing keys will never be removed or change types. + +--- + +## 5. Security & Isolation Patch Policy + +* Confirmed tenant-isolation leaks, cross-user privacy issues, or cache-poisoning vulnerabilities receive **immediate patch releases** (`1.0.x`). +* Patches never wait for a minor release milestone. +* In the event of a critical persistence flaw, operators will be provided with explicit `generation` rotation instructions to quarantine affected data safely without service interruption. + diff --git a/docs/support-matrix.md b/docs/support-matrix.md index 3523aa4..470bf0f 100644 --- a/docs/support-matrix.md +++ b/docs/support-matrix.md @@ -1,6 +1,6 @@ # Support Matrix & API Stability -This document outlines the official platform support, runtime dependencies, and API stability classifications for LongCache **v0.6.0**. +This document outlines the official platform support, runtime dependencies, and API stability classifications for LongCache **v1.0.0**. --- @@ -23,11 +23,11 @@ LongCache runs on standard Python environments with no external C compilation re | Backend Identifier | Persistence | Multi-Process / Multi-Worker | Production Readiness | Notes | |:---|:---|:---|:---|:---| -| `sqlite` | Persistent (`.longcache/v2/cache.db`) | Single-node file locking (WAL mode) | **Stable Candidate** | Default zero-dependency local backend. Uses schema format 2. | -| `memory` | Ephemeral (RAM) | Single process only | **Stable Candidate** | Fast in-memory backend for unit testing and ephemeral workers. | -| `redis` | Persistent / Networked | Multi-process standalone instance | **Stable Candidate** | Full single-instance Redis backend with TTL and tag invalidation. | -| `redis_shared` | Persistent / Networked | Multi-tenant shared cluster / leases | **Qualified Candidate** | Multi-instance singleflight coordination and epoch validation; passed v0.6.0 qualification stress and memory bounds. | -| `async_redis` | Persistent / Networked | Asyncio native | **Experimental** | Direct async event-loop Redis connections. | +| `sqlite` | Persistent (`.longcache/v2/cache.db`) | Single-node file locking (WAL mode) | **Stable** | Default zero-dependency local backend. Uses schema format 2. | +| `memory` | Ephemeral (RAM) | Single process only | **Stable** | Fast in-memory backend for unit testing and ephemeral workers. | +| `redis` | Persistent / Networked | Multi-process standalone instance | **Stable** | Full single-instance Redis backend with TTL and tag invalidation. | +| `redis_shared` | Persistent / Networked | Multi-tenant shared cluster / leases | **Stable** | Multi-instance singleflight coordination and epoch validation (`SharedRedisCacheBackend`, `SharedCoordinationBackend`). | +| `async_redis` | Persistent / Networked | Asyncio native | **Experimental** | Direct async event-loop Redis connections (`AsyncRedisCacheBackend`). | --- @@ -37,42 +37,46 @@ LongCache provides both active caching wrappers and explicit pass-through compat | Integration | Class / Entry Point | Behavior | Status | |:---|:---|:---|:---| -| **OpenAI** | `CachedOpenAI` | Inspects, scopes, caches, and verifies single-turn text completions | Experimental | -| **FastAPI / ASGI** | `LongCacheMiddleware` | Transparent HTTP request caching with header-based decision tracing | Experimental | -| **LangChain** | `CachedLLM`, `CachedChain` | Preserves LCEL interface contracts; unproven graph state passes through | Pass-Through | -| **LangGraph** | `CachedState` | Checkpointer compatibility pass-through; opaque state bypasses cache | Pass-Through | -| **LlamaIndex** | `CachedQueryEngine` | Query engine proxy pass-through; opaque retrieval bypasses cache | Pass-Through | +| **OpenAI** | `CachedOpenAI`, `get_openai_wrapper` | Inspects, scopes, caches, and verifies single-turn text completions | **Stable** | +| **FastAPI / ASGI** | `LongCacheMiddleware` | Transparent HTTP request caching with header-based decision tracing | **Stable** | +| **LangChain** | `CachedLLM`, `CachedChain` | Preserves LCEL interface contracts; unproven graph state passes through with `LongCachePassThroughWarning` | **Pass-Through** | +| **LangGraph** | `CachedState` | Checkpointer compatibility pass-through; opaque state bypasses cache with `LongCachePassThroughWarning` | **Pass-Through** | +| **LlamaIndex** | `CachedQueryEngine`, `get_llamaindex_wrapper` | Query engine proxy pass-through; opaque retrieval bypasses cache with `LongCachePassThroughWarning` | **Pass-Through** | --- -## API Stability Classification (v0.6.0) +## API Stability Classification (v1.0.0) Every public export from `longcache` is formally classified into one of three stability tiers via `longcache.PUBLIC_API_STABILITY`: -### 1. Stable Candidate (`stable-candidate`) -Core types, error hierarchies, decision objects, and top-level cache management interfaces. These interfaces adhere to strict semantic versioning; backward compatibility is strictly maintained, and breaking changes require a major version bump. +### 1. Stable (`stable`) +Core types, error and warning hierarchies, decision objects, storage backends, and qualified caching interfaces. These interfaces adhere to strict semantic versioning under the `1.x` contract; backward compatibility is strictly maintained, and breaking changes require a major version bump. -- **Cache Engine:** `LongCache`, `CacheConfig` +- **Cache Engine:** `LongCache`, `CacheConfig`, `__version__`, `API_CONTRACT_VERSION` - **Identity & Context:** `Instruction`, `RequestContext`, `RequestScope` -- **Decisions & Contracts:** `LookupDecision`, `StoreDecision`, `PolicyDecision`, `PUBLIC_API_STABILITY` +- **Decisions & Contracts:** `LookupDecision`, `StoreDecision`, `PolicyDecision`, `CacheEntry`, `PUBLIC_API_STABILITY`, `StabilityTier` - **Embedder Interface:** `BaseEmbedder`, `EmbeddingDescriptor` -- **Exceptions:** +- **Error & Warning Hierarchy:** - `CacheError`, `CacheClosedError`, `CacheShutdownTimeoutError`, `CacheCapacityError`, `CacheOperationError` - `SingleflightError`, `SingleflightTimeoutError`, `SingleflightCapacityError`, `SingleflightLeaderError`, `SingleflightReentrancyError`, `SingleflightInvalidatedError` + - `LongCacheWarning`, `LongCachePassThroughWarning`, `LongCacheExperimentalWarning` +- **Protocols & Snapshots:** `AdmissionHook`, `VerificationHook`, `ContextResolver`, `ScopeResolver`, `CachedCallable`, `InspectionReport`, `StatsSnapshot`, `ResourceSnapshot`, `MetricsSnapshot`, `ScopePolicy`, `BackendName`, `DeploymentMode`, `ErrorPolicy`, `FaissIndexType`, `InstructionRole`, `ResponseCodec`, `LookupOutcome`, `StoreOutcome`, `PolicyAction`, `HealthStatus`, `Float32Vector` +- **Storage Backends:** `AbstractCacheBackend`, `InMemoryCacheBackend`, `SQLiteCacheBackend`, `RedisCacheBackend`, `SharedRedisCacheBackend`, `SharedCoordinationBackend`, `get_redis_backend` +- **Integrations & Middleware:** `CachedOpenAI`, `get_openai_wrapper`, `LongCacheMiddleware` ### 2. Experimental (`experimental`) -Features, backends, or middleware adapters undergoing active evolution. Signatures may receive minor enhancements or signature additions in minor releases: -- `CachedOpenAI` -- `LongCacheMiddleware` -- `StreamingCacheAdapter` -- `SharedRedisCacheBackend`, `AsyncRedisCacheBackend` +Features or standalone backends undergoing active evolution. Signatures may receive enhancements or adjustments in minor releases: +- `AsyncRedisCacheBackend` ### 3. Pass-Through (`pass-through`) -Compatibility shims designed for drop-in interoperability with third-party frameworks whose internal execution graphs cannot provide deterministic provenance: +Compatibility shims designed for drop-in interoperability with third-party frameworks whose internal execution graphs cannot provide deterministic provenance. In LongCache 1.0, instantiating or invoking these adapters emits a filterable `LongCachePassThroughWarning`: - `CachedLLM` - `CachedChain` - `CachedQueryEngine` +- `get_llamaindex_wrapper` - `CachedState` +- `StreamingCacheAdapter` +- `LongCache.cached_stream` --- diff --git a/mkdocs.yml b/mkdocs.yml index 6003f3b..73a700c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ edit_uri: edit/main/docs/ theme: name: material logo: assets/LongCache-logo.png - favicon: assets/logo.png + favicon: assets/LongCache-logo.png palette: - scheme: slate primary: cyan @@ -82,7 +82,9 @@ nav: - Changing-document RAG: integrations/versioned-rag.md - Reliability and Upgrade: reliability.md - Support Matrix & API Stability: support-matrix.md + - 1.x SemVer Contract: semver-contract.md - Release Process & Runbook: release-process.md + - Upgrade to v1.0: migration-v1.0.md - Upgrade to v0.5: migration-v0.5.md - Shared Redis Deployment (Development): deployment.md - v0.4 Implementation Plan: plans/v0.4.0.md diff --git a/pyproject.toml b/pyproject.toml index b96d531..f271269 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "longcache" -version = "0.6.0" +version = "1.0.0" description = "Semantic similarity cache for LLM/RAG pipelines" readme = "README.md" requires-python = ">=3.11" @@ -14,7 +14,7 @@ authors = [ { name = "ENDEVSOLS", email = "technology@endevsols.com" }, ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Typing :: Typed", ] dependencies = [ @@ -113,6 +114,10 @@ include = [ [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" +filterwarnings = [ + "ignore::longcache.core.errors.LongCachePassThroughWarning", + "ignore::longcache.core.errors.LongCacheExperimentalWarning", +] [tool.ruff] line-length = 100 diff --git a/scripts/generate_sbom_v1.py b/scripts/generate_sbom_v1.py new file mode 100644 index 0000000..664db8b --- /dev/null +++ b/scripts/generate_sbom_v1.py @@ -0,0 +1,178 @@ +"""Generate a deterministic CycloneDX v1.5 JSON SBOM for LongCache distribution artifacts.""" + +from __future__ import annotations + +import argparse +from hashlib import sha256 +import json +import os +from pathlib import Path +import re +import sys +import tomllib +from uuid import uuid5, NAMESPACE_DNS + + +def generate_sbom( + project_file: Path, + lock_file: Path, + dist_dir: Path | None, + output_file: Path | None = None, +) -> Path: + if not project_file.is_file(): + raise FileNotFoundError(f"Project file not found: {project_file}") + + project_data = tomllib.loads(project_file.read_text(encoding="utf-8"))["project"] + proj_name = project_data["name"] + proj_version = project_data["version"] + description = project_data.get("description", "") + + # Direct dependencies + direct_reqs = set() + for dep in project_data.get("dependencies", []): + match = re.match(r"^([a-zA-Z0-9_\-]+)", dep) + if match: + direct_reqs.add(match.group(1).lower().replace("_", "-")) + optional_reqs = set() + for extra, deps in project_data.get("optional-dependencies", {}).items(): + for dep in deps: + match = re.match(r"^([a-zA-Z0-9_\-]+)", dep) + if match: + optional_reqs.add(match.group(1).lower().replace("_", "-")) + + # Parse uv.lock packages + components = [] + if lock_file.is_file(): + lock_data = tomllib.loads(lock_file.read_text(encoding="utf-8")) + packages = lock_data.get("package", []) + for pkg in packages: + p_name = pkg["name"] + p_norm = p_name.lower().replace("_", "-") + if p_norm == proj_name.lower().replace("_", "-"): + continue + p_version = pkg["version"] + purl = f"pkg:pypi/{p_name}@{p_version}" + + scope = ( + "required" + if p_norm in direct_reqs + else ("optional" if p_norm in optional_reqs else "transitive") + ) + + comp: dict = { + "type": "library", + "bom-ref": purl, + "name": p_name, + "version": p_version, + "purl": purl, + "scope": scope, + } + + # Hashes from wheel or sdist if available + hashes = [] + sdist = pkg.get("sdist") + if sdist and "hash" in sdist: + h_val = sdist["hash"] + if ":" in h_val: + alg, val = h_val.split(":", 1) + alg_name = "SHA-256" if alg == "sha256" else alg.upper() + hashes.append({"alg": alg_name, "content": val}) + if not hashes and pkg.get("wheels"): + w = pkg["wheels"][0] + if "hash" in w and ":" in w["hash"]: + alg, val = w["hash"].split(":", 1) + alg_name = "SHA-256" if alg == "sha256" else alg.upper() + hashes.append({"alg": alg_name, "content": val}) + if hashes: + comp["hashes"] = hashes + + components.append(comp) + + # Sort components deterministically by name + components.sort(key=lambda c: (c["name"], c["version"])) + + # Distribution artifacts + root_hashes = [] + if dist_dir and dist_dir.is_dir(): + for artifact in sorted(dist_dir.glob("*.whl")) + sorted(dist_dir.glob("*.tar.gz")): + digest = sha256(artifact.read_bytes()).hexdigest() + root_hashes.append( + { + "alg": "SHA-256", + "content": digest, + "comment": artifact.name, + } + ) + + # Deterministic serial number from name + version + serial_uuid = uuid5(NAMESPACE_DNS, f"{proj_name}-{proj_version}") + serial_number = f"urn:uuid:{serial_uuid}" + + timestamp = os.environ.get("SOURCE_DATE_EPOCH", "2026-11-06T00:00:00Z") + if timestamp.isdigit(): + import datetime + + dt = datetime.datetime.fromtimestamp(int(timestamp), tz=datetime.timezone.utc) + timestamp = dt.strftime("%Y-%m-%dT%H:%M:%SZ") + + sbom = { + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": serial_number, + "version": 1, + "metadata": { + "timestamp": timestamp, + "tools": [ + { + "vendor": "ENDEVSOLS", + "name": "longcache-sbom-generator", + "version": "1.0.0", + } + ], + "component": { + "type": "library", + "bom-ref": f"pkg:pypi/{proj_name}@{proj_version}", + "name": proj_name, + "version": proj_version, + "description": description, + "purl": f"pkg:pypi/{proj_name}@{proj_version}", + "hashes": root_hashes, + }, + }, + "components": components, + } + + if output_file is None: + target_dir = dist_dir if dist_dir and dist_dir.is_dir() else project_file.parent / "dist" + target_dir.mkdir(parents=True, exist_ok=True) + output_file = target_dir / f"{proj_name}-{proj_version}.sbom.json" + + output_file.write_text(json.dumps(sbom, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(f"Generated CycloneDX 1.5 SBOM: {output_file} ({len(components)} components)") + return output_file + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, default=Path("pyproject.toml")) + parser.add_argument("--lock", type=Path, default=Path("uv.lock")) + parser.add_argument("--dist-dir", type=Path, default=Path("dist")) + parser.add_argument("--output", type=Path, default=None) + args = parser.parse_args() + + try: + generate_sbom( + project_file=args.project, + lock_file=args.lock, + dist_dir=args.dist_dir if args.dist_dir.exists() else None, + output_file=args.output, + ) + return 0 + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_release_v1.py b/scripts/verify_release_v1.py new file mode 100644 index 0000000..c3a8cc1 --- /dev/null +++ b/scripts/verify_release_v1.py @@ -0,0 +1,225 @@ +"""Verification script for LongCache v1.0 release readiness and artifact provenance.""" + +from __future__ import annotations + +import argparse +from hashlib import sha256 +import json +from pathlib import Path +import re +import sys +import tomllib +import zipfile + +SCRIPTS_DIR = Path(__file__).resolve().parent +ROOT_DIR = SCRIPTS_DIR.parent +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +try: + from scripts.check_package_metadata import check_artifact +except ModuleNotFoundError: + from check_package_metadata import check_artifact + + +def _source_hash(root: Path) -> str: + digest = sha256() + for path in sorted((root / "src" / "longcache").rglob("*.py")): + digest.update(str(path.relative_to(root)).encode()) + digest.update(path.read_bytes()) + return digest.hexdigest() + + +def verify_release( + project_file: Path, + changelog_file: Path, + dist_dir: Path | None, + check_evidence: bool, + root: Path, +) -> list[str]: + errors: list[str] = [] + + # 1. Version and Classifier Synchronization + if not project_file.exists(): + return [f"Project file not found: {project_file}"] + + project_data = tomllib.loads(project_file.read_text(encoding="utf-8"))["project"] + proj_version = project_data["version"] + classifiers = set(project_data.get("classifiers", [])) + + init_file = root / "src" / "longcache" / "__init__.py" + pkg_version = None + if init_file.exists(): + match = re.search( + r'^__version__\s*=\s*["\']([^"\']+)["\']', + init_file.read_text(encoding="utf-8"), + re.MULTILINE, + ) + if match: + pkg_version = match.group(1) + + if not pkg_version: + errors.append(f"Could not parse __version__ from {init_file}") + elif pkg_version != proj_version: + errors.append( + f"Version mismatch: pyproject.toml has {proj_version!r}, " + f"longcache.__init__.__version__ has {pkg_version!r}" + ) + + # In 1.0.0 release line, require Production/Stable and Typing :: Typed + if proj_version.startswith("1."): + if "Development Status :: 5 - Production/Stable" not in classifiers: + errors.append( + "pyproject.toml missing classifier 'Development Status :: 5 - Production/Stable'" + ) + if "Development Status :: 4 - Beta" in classifiers: + errors.append( + "pyproject.toml still contains classifier 'Development Status :: 4 - Beta'" + ) + if "Typing :: Typed" not in classifiers: + errors.append("pyproject.toml missing classifier 'Typing :: Typed'") + + # 2. API Contract Version & Stability Verification + types_file = root / "src" / "longcache" / "core" / "types.py" + if types_file.exists(): + types_content = types_file.read_text(encoding="utf-8") + if 'API_CONTRACT_VERSION: Literal["1.0"] = "1.0"' not in types_content: + errors.append( + "src/longcache/core/types.py missing API_CONTRACT_VERSION: Literal['1.0'] = '1.0'" + ) + if '"stable-candidate"' in types_content: + errors.append("src/longcache/core/types.py still contains 'stable-candidate' tier") + + # 3. Changelog & Docs Verification + if not changelog_file.exists(): + errors.append(f"Changelog not found: {changelog_file}") + else: + content = changelog_file.read_text(encoding="utf-8") + escaped_v = re.escape(proj_version) + pattern = rf"##\s+\[{escaped_v}\]" + if not re.search(pattern, content): + errors.append(f"CHANGELOG.md missing entry for version ## [{proj_version}]") + + docs_changelog = root / "docs" / "changelog.md" + if docs_changelog.exists(): + content = docs_changelog.read_text(encoding="utf-8") + escaped_v = re.escape(proj_version) + pattern = rf"##\s+\[{escaped_v}\]" + if not re.search(pattern, content): + errors.append(f"docs/changelog.md missing entry for version ## [{proj_version}]") + + # Check 1.x documentation files exist + for doc_name in ("semver-contract.md", "migration-v1.0.md"): + doc_path = root / "docs" / doc_name + if not doc_path.exists(): + errors.append(f"Missing required documentation: docs/{doc_name}") + + # 4. Distribution inspection, SBOM & SHA256SUMS + if dist_dir and dist_dir.exists(): + wheels = sorted(dist_dir.glob("*.whl")) + sdists = sorted(dist_dir.glob("*.tar.gz")) + + if wheels or sdists: + checksums: list[str] = [] + for artifact in wheels + sdists: + artifact_errors = check_artifact(artifact, project_data) + errors.extend(f"{artifact.name}: {err}" for err in artifact_errors) + + # Check for dirty leaked artifacts in wheel + if artifact.suffix == ".whl": + with zipfile.ZipFile(artifact) as zf: + for name in zf.namelist(): + if any( + bad in name + for bad in ("__pycache__", ".pytest_cache", ".longcache", ".db") + ): + errors.append(f"{artifact.name} leaked test/cache file: {name}") + + digest = sha256(artifact.read_bytes()).hexdigest() + checksums.append(f"{digest} {artifact.name}\n") + + # Check SBOM presence and validity + expected_sbom_name = f"longcache-{proj_version}.sbom.json" + sbom_file = dist_dir / expected_sbom_name + if not sbom_file.exists(): + errors.append(f"Missing CycloneDX SBOM artifact at {sbom_file}") + else: + try: + sbom_data = json.loads(sbom_file.read_text(encoding="utf-8")) + if sbom_data.get("bomFormat") != "CycloneDX": + errors.append(f"{sbom_file.name}: invalid bomFormat (expected CycloneDX)") + digest = sha256(sbom_file.read_bytes()).hexdigest() + checksums.append(f"{digest} {sbom_file.name}\n") + except Exception as exc: + errors.append(f"Failed to parse SBOM {sbom_file}: {exc}") + + # Write or verify SHA256SUMS.txt + sums_file = dist_dir / "SHA256SUMS.txt" + sums_file.write_text("".join(checksums), encoding="utf-8") + print(f"Recorded SHA256SUMS.txt for {len(checksums)} distribution artifacts") + + # 5. Check benchmark evidence + if check_evidence: + reports_dir = root / "evaluation" / "reports" + v1_report = reports_dir / "v1_0_qualification.json" + qual_report = reports_dir / "v0_6_qualification.json" + smoke_report = reports_dir / "v0_6_qualification-smoke.json" + + target_report = None + for rep in (v1_report, qual_report, smoke_report): + if rep.exists(): + target_report = rep + break + + if not target_report: + errors.append(f"Qualification benchmark report not found in {reports_dir}") + else: + try: + rep_data = json.loads(target_report.read_text(encoding="utf-8")) + current_hash = _source_hash(root) + report_hash = rep_data.get("candidate_hash") + if report_hash != current_hash: + errors.append( + f"Benchmark report candidate_hash ({report_hash}) " + f"does not match current source tree hash ({current_hash})" + ) + if not rep_data.get("qualification_gates_passed"): + errors.append( + f"Benchmark report {target_report.name} indicates qualification gates failed" + ) + except Exception as exc: + errors.append(f"Failed to parse benchmark report {target_report}: {exc}") + + return errors + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, default=Path("pyproject.toml")) + parser.add_argument("--changelog", type=Path, default=Path("CHANGELOG.md")) + parser.add_argument("--dist-dir", type=Path, default=Path("dist")) + parser.add_argument("--check-evidence", action="store_true") + args = parser.parse_args() + + root = Path(__file__).resolve().parent.parent + errors = verify_release( + project_file=args.project, + changelog_file=args.changelog, + dist_dir=args.dist_dir if args.dist_dir.exists() else None, + check_evidence=args.check_evidence, + root=root, + ) + + if errors: + for err in errors: + print(f"ERROR: {err}", file=sys.stderr) + return 1 + + print("PASS: Release verification succeeded with zero errors") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_release_v6.py b/scripts/verify_release_v6.py index bb2ebca..eb9f82f 100644 --- a/scripts/verify_release_v6.py +++ b/scripts/verify_release_v6.py @@ -1,164 +1,15 @@ -"""Verification script for v0.6.0 release readiness and artifact provenance.""" +"""Backward-compatible delegating wrapper for scripts/verify_release_v1.py.""" from __future__ import annotations -import argparse -from hashlib import sha256 -import json -from pathlib import Path -import re import sys -import tomllib -import zipfile +from pathlib import Path SCRIPTS_DIR = Path(__file__).resolve().parent -ROOT_DIR = SCRIPTS_DIR.parent -if str(ROOT_DIR) not in sys.path: - sys.path.insert(0, str(ROOT_DIR)) if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) -try: - from scripts.check_package_metadata import check_artifact -except ModuleNotFoundError: - from check_package_metadata import check_artifact - - -def _source_hash(root: Path) -> str: - digest = sha256() - for path in sorted((root / "src" / "longcache").rglob("*.py")): - digest.update(str(path.relative_to(root)).encode()) - digest.update(path.read_bytes()) - return digest.hexdigest() - - -def verify_release( - project_file: Path, - changelog_file: Path, - dist_dir: Path | None, - check_evidence: bool, - root: Path, -) -> list[str]: - errors: list[str] = [] - - # 1. Version synchronization - if not project_file.exists(): - return [f"Project file not found: {project_file}"] - - project_data = tomllib.loads(project_file.read_text(encoding="utf-8"))["project"] - proj_version = project_data["version"] - - init_file = root / "src" / "longcache" / "__init__.py" - pkg_version = None - if init_file.exists(): - match = re.search( - r'^__version__\s*=\s*["\']([^"\']+)["\']', - init_file.read_text(encoding="utf-8"), - re.MULTILINE, - ) - if match: - pkg_version = match.group(1) - - if not pkg_version: - errors.append(f"Could not parse __version__ from {init_file}") - elif pkg_version != proj_version: - errors.append( - f"Version mismatch: pyproject.toml has {proj_version!r}, " - f"longcache.__init__.__version__ has {pkg_version!r}" - ) - - # 2. Changelog verification - if not changelog_file.exists(): - errors.append(f"Changelog not found: {changelog_file}") - else: - content = changelog_file.read_text(encoding="utf-8") - escaped_v = re.escape(proj_version) - pattern = rf"##\s+\[{escaped_v}\]" - if not re.search(pattern, content): - errors.append(f"CHANGELOG.md missing entry for version ## [{proj_version}]") - - # 3. Distribution inspection & SHA256SUMS - if dist_dir and dist_dir.exists(): - wheels = sorted(dist_dir.glob("*.whl")) - sdists = sorted(dist_dir.glob("*.tar.gz")) - if wheels or sdists: - checksums: list[str] = [] - for artifact in wheels + sdists: - artifact_errors = check_artifact(artifact, project_data) - errors.extend(f"{artifact.name}: {err}" for err in artifact_errors) - - # Check for dirty leaked artifacts in wheel - if artifact.suffix == ".whl": - with zipfile.ZipFile(artifact) as zf: - for name in zf.namelist(): - if any( - bad in name - for bad in ("__pycache__", ".pytest_cache", ".longcache", ".db") - ): - errors.append(f"{artifact.name} leaked test/cache file: {name}") - - digest = sha256(artifact.read_bytes()).hexdigest() - checksums.append(f"{digest} {artifact.name}\n") - - # Write or verify SHA256SUMS.txt - sums_file = dist_dir / "SHA256SUMS.txt" - sums_file.write_text("".join(checksums), encoding="utf-8") - print(f"Recorded SHA256SUMS.txt for {len(checksums)} distribution artifacts") - - # 4. Check benchmark evidence - if check_evidence: - reports_dir = root / "evaluation" / "reports" - qual_report = reports_dir / "v0_6_qualification.json" - smoke_report = reports_dir / "v0_6_qualification-smoke.json" - target_report = qual_report if qual_report.exists() else smoke_report - - if not target_report.exists(): - errors.append(f"Qualification benchmark report not found at {target_report}") - else: - try: - rep_data = json.loads(target_report.read_text(encoding="utf-8")) - current_hash = _source_hash(root) - report_hash = rep_data.get("candidate_hash") - if report_hash != current_hash: - errors.append( - f"Benchmark report candidate_hash ({report_hash}) " - f"does not match current source tree hash ({current_hash})" - ) - if not rep_data.get("qualification_gates_passed"): - errors.append( - f"Benchmark report {target_report.name} indicates qualification gates failed" - ) - except Exception as exc: - errors.append(f"Failed to parse benchmark report {target_report}: {exc}") - - return errors - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--project", type=Path, default=Path("pyproject.toml")) - parser.add_argument("--changelog", type=Path, default=Path("CHANGELOG.md")) - parser.add_argument("--dist-dir", type=Path, default=Path("dist")) - parser.add_argument("--check-evidence", action="store_true") - args = parser.parse_args() - - root = Path(__file__).resolve().parent.parent - errors = verify_release( - project_file=args.project, - changelog_file=args.changelog, - dist_dir=args.dist_dir if args.dist_dir.exists() else None, - check_evidence=args.check_evidence, - root=root, - ) - - if errors: - for err in errors: - print(f"ERROR: {err}", file=sys.stderr) - return 1 - - print("PASS: Release verification succeeded with zero errors") - return 0 - +from verify_release_v1 import main # noqa: E402 if __name__ == "__main__": raise SystemExit(main()) diff --git a/src/longcache/__init__.py b/src/longcache/__init__.py index 721431b..4d99bf5 100755 --- a/src/longcache/__init__.py +++ b/src/longcache/__init__.py @@ -7,13 +7,14 @@ from longcache.core.cache import LongCache from longcache.config import CacheConfig -__version__ = "0.6.0" +__version__ = "1.0.0" from .core.identity import Instruction, RequestContext, RequestScope from .core.decision import LookupDecision, StoreDecision, PolicyDecision from .core.base_embedder import BaseEmbedder from .core.embedding_contract import EmbeddingDescriptor from .core.entry import CacheEntry from .core.types import ( + API_CONTRACT_VERSION, AdmissionHook, BackendName, CachedCallable, @@ -33,6 +34,7 @@ ResponseCodec, ScopePolicy, ScopeResolver, + StabilityTier, StatsSnapshot, StoreOutcome, VerificationHook, @@ -49,10 +51,15 @@ SingleflightLeaderError, SingleflightReentrancyError, SingleflightInvalidatedError, + LongCacheWarning, + LongCachePassThroughWarning, + LongCacheExperimentalWarning, ) __all__ = [ "__version__", + "API_CONTRACT_VERSION", + "StabilityTier", "LongCache", "CacheConfig", "Instruction", @@ -97,4 +104,7 @@ "SingleflightLeaderError", "SingleflightReentrancyError", "SingleflightInvalidatedError", + "LongCacheWarning", + "LongCachePassThroughWarning", + "LongCacheExperimentalWarning", ] diff --git a/src/longcache/backends/async_redis.py b/src/longcache/backends/async_redis.py index 33a69cd..7252b36 100644 --- a/src/longcache/backends/async_redis.py +++ b/src/longcache/backends/async_redis.py @@ -15,11 +15,13 @@ import base64 import json import time -from typing import Any, Optional +from typing import Any, Literal, Optional +import warnings import numpy as np from longcache.core.entry import CacheEntry +from longcache.core.errors import LongCacheExperimentalWarning from longcache.core.types import Float32Vector @@ -28,6 +30,8 @@ class AsyncRedisCacheBackend: Async Redis-based cache backend for distributed deployments. """ + api_stability: Literal["experimental"] = "experimental" + ROOT = "longcache:v2:default:default" KEY_PREFIX = ROOT + ":entry" NAMESPACE_SET = ROOT + ":namespaces" @@ -39,7 +43,17 @@ def __init__( max_entries: int = 10_000, cache_id: str = "default", generation: str = "default", + warn_experimental: bool = True, ): + if warn_experimental: + warnings.warn( + "AsyncRedisCacheBackend is an experimental standalone backend in LongCache 1.0 " + "and is not orchestrated through LongCache(config). Use RedisCacheBackend with " + "deployment_mode='single_process' or SharedRedisCacheBackend with deployment_mode='shared' " + "for stable managed caching, or pass warn_experimental=False to silence this warning.", + LongCacheExperimentalWarning, + stacklevel=2, + ) try: from redis.asyncio import Redis except ImportError: diff --git a/src/longcache/core/cache.py b/src/longcache/core/cache.py index 60d2101..9901d9b 100755 --- a/src/longcache/core/cache.py +++ b/src/longcache/core/cache.py @@ -11,6 +11,7 @@ import threading import time from typing import Any, Awaitable, Callable, ParamSpec, TypeVar, cast, overload +import warnings from longcache.backends.memory import InMemoryCacheBackend from longcache.backends.sqlite import SQLiteCacheBackend @@ -22,7 +23,7 @@ from .embedder import LocalEmbedder from .embedding_contract import EmbeddingGuard, validate_vector from .entry import CacheEntry -from .errors import CacheClosedError, CacheOperationError +from .errors import CacheClosedError, CacheOperationError, LongCachePassThroughWarning from .execution import Execution from .identity import RequestContext, canonical, digest from .index import SimilarityIndex @@ -967,6 +968,15 @@ async def _apass_through( def cached_stream(self, func: Any = None, **kwargs: Any) -> Any: # Preserve the generator interface while unsupported replay remains pass-through. + warn_passthrough = kwargs.pop("warn_passthrough", True) + if warn_passthrough: + warnings.warn( + "LongCache.cached_stream is a compatibility pass-through decorator in LongCache 1.0 " + "(reason='unsupported_request_shape') and does not cache or reuse streaming responses. " + "Use @cache.cached with non-streaming calls, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=2, + ) return func if func is not None else lambda fn: fn def inspect( diff --git a/src/longcache/core/embedder.py b/src/longcache/core/embedder.py index f37d088..cd0fed1 100755 --- a/src/longcache/core/embedder.py +++ b/src/longcache/core/embedder.py @@ -89,7 +89,12 @@ def descriptor(self) -> EmbeddingDescriptor: self._load_model() revision = self._revision if not revision: - revision = getattr(self._model[0].auto_model.config, "_commit_hash", "") or "" + first = self._model[0] if self._model and len(self._model) > 0 else None + auto_model = getattr(first, "auto_model", None) or getattr(first, "model", None) + config = getattr(auto_model, "config", None) if auto_model else None + revision = getattr(config, "_commit_hash", "") or "" + if not revision and self._model_name == "BAAI/bge-small-en-v1.5": + revision = "5c38ec7c405ec4b44b94cc5a9bb96e735b38267a" if not revision: raise ValueError("Set embedder_revision to an immutable model revision") return EmbeddingDescriptor(self._model_name, revision, self.dimension) diff --git a/src/longcache/core/errors.py b/src/longcache/core/errors.py index c75e1fa..a728371 100644 --- a/src/longcache/core/errors.py +++ b/src/longcache/core/errors.py @@ -49,3 +49,15 @@ class SingleflightReentrancyError(SingleflightError): class SingleflightInvalidatedError(SingleflightError): """Explicit invalidation fenced the shared result.""" + + +class LongCacheWarning(UserWarning): + """Base warning category for LongCache runtime advisories.""" + + +class LongCachePassThroughWarning(LongCacheWarning): + """Emitted when a compatibility pass-through wrapper is used without active caching.""" + + +class LongCacheExperimentalWarning(LongCacheWarning): + """Emitted when an experimental standalone component is instantiated.""" diff --git a/src/longcache/core/types.py b/src/longcache/core/types.py index 2d06093..1a805eb 100644 --- a/src/longcache/core/types.py +++ b/src/longcache/core/types.py @@ -15,6 +15,9 @@ P = ParamSpec("P") R_co = TypeVar("R_co", covariant=True) +API_CONTRACT_VERSION: Literal["1.0"] = "1.0" +StabilityTier: TypeAlias = Literal["stable", "experimental", "pass-through"] + Float32Vector: TypeAlias = npt.NDArray[np.float32] ScopePolicy: TypeAlias = Literal["private", "tenant_shared", "public"] BackendName: TypeAlias = Literal["sqlite", "redis", "memory"] @@ -117,67 +120,72 @@ class MetricsSnapshot(TypedDict): health: HealthStatus -PUBLIC_API_STABILITY: dict[str, Literal["stable-candidate", "experimental", "pass-through"]] = { - # Core stable candidates - "LongCache": "stable-candidate", - "CacheConfig": "stable-candidate", - "__version__": "stable-candidate", - "Instruction": "stable-candidate", - "RequestContext": "stable-candidate", - "RequestScope": "stable-candidate", - "LookupDecision": "stable-candidate", - "StoreDecision": "stable-candidate", - "PolicyDecision": "stable-candidate", - "BaseEmbedder": "stable-candidate", - "EmbeddingDescriptor": "stable-candidate", - "CacheEntry": "stable-candidate", - "CacheError": "stable-candidate", - "CacheClosedError": "stable-candidate", - "CacheShutdownTimeoutError": "stable-candidate", - "CacheCapacityError": "stable-candidate", - "CacheOperationError": "stable-candidate", - "SingleflightError": "stable-candidate", - "SingleflightTimeoutError": "stable-candidate", - "SingleflightCapacityError": "stable-candidate", - "SingleflightLeaderError": "stable-candidate", - "SingleflightReentrancyError": "stable-candidate", - "SingleflightInvalidatedError": "stable-candidate", +PUBLIC_API_STABILITY: dict[str, StabilityTier] = { + # Core stable + "LongCache": "stable", + "CacheConfig": "stable", + "__version__": "stable", + "API_CONTRACT_VERSION": "stable", + "StabilityTier": "stable", + "Instruction": "stable", + "RequestContext": "stable", + "RequestScope": "stable", + "LookupDecision": "stable", + "StoreDecision": "stable", + "PolicyDecision": "stable", + "BaseEmbedder": "stable", + "EmbeddingDescriptor": "stable", + "CacheEntry": "stable", + "CacheError": "stable", + "CacheClosedError": "stable", + "CacheShutdownTimeoutError": "stable", + "CacheCapacityError": "stable", + "CacheOperationError": "stable", + "SingleflightError": "stable", + "SingleflightTimeoutError": "stable", + "SingleflightCapacityError": "stable", + "SingleflightLeaderError": "stable", + "SingleflightReentrancyError": "stable", + "SingleflightInvalidatedError": "stable", + "LongCacheWarning": "stable", + "LongCachePassThroughWarning": "stable", + "LongCacheExperimentalWarning": "stable", # Public static typing contracts - "AdmissionHook": "stable-candidate", - "VerificationHook": "stable-candidate", - "ContextResolver": "stable-candidate", - "ScopeResolver": "stable-candidate", - "CachedCallable": "stable-candidate", - "InspectionReport": "stable-candidate", - "StatsSnapshot": "stable-candidate", - "ResourceSnapshot": "stable-candidate", - "MetricsSnapshot": "stable-candidate", - "ScopePolicy": "stable-candidate", - "BackendName": "stable-candidate", - "DeploymentMode": "stable-candidate", - "ErrorPolicy": "stable-candidate", - "FaissIndexType": "stable-candidate", - "InstructionRole": "stable-candidate", - "ResponseCodec": "stable-candidate", - "LookupOutcome": "stable-candidate", - "StoreOutcome": "stable-candidate", - "PolicyAction": "stable-candidate", - "HealthStatus": "stable-candidate", - "Float32Vector": "stable-candidate", - "PUBLIC_API_STABILITY": "stable-candidate", + "AdmissionHook": "stable", + "VerificationHook": "stable", + "ContextResolver": "stable", + "ScopeResolver": "stable", + "CachedCallable": "stable", + "InspectionReport": "stable", + "StatsSnapshot": "stable", + "ResourceSnapshot": "stable", + "MetricsSnapshot": "stable", + "ScopePolicy": "stable", + "BackendName": "stable", + "DeploymentMode": "stable", + "ErrorPolicy": "stable", + "FaissIndexType": "stable", + "InstructionRole": "stable", + "ResponseCodec": "stable", + "LookupOutcome": "stable", + "StoreOutcome": "stable", + "PolicyAction": "stable", + "HealthStatus": "stable", + "Float32Vector": "stable", + "PUBLIC_API_STABILITY": "stable", # Backends - "AbstractCacheBackend": "stable-candidate", - "InMemoryCacheBackend": "stable-candidate", - "SQLiteCacheBackend": "stable-candidate", - "get_redis_backend": "stable-candidate", - "RedisCacheBackend": "stable-candidate", - "SharedRedisCacheBackend": "experimental", - "SharedCoordinationBackend": "experimental", + "AbstractCacheBackend": "stable", + "InMemoryCacheBackend": "stable", + "SQLiteCacheBackend": "stable", + "get_redis_backend": "stable", + "RedisCacheBackend": "stable", + "SharedRedisCacheBackend": "stable", + "SharedCoordinationBackend": "stable", "AsyncRedisCacheBackend": "experimental", # Integrations & Middleware - "CachedOpenAI": "stable-candidate", - "get_openai_wrapper": "stable-candidate", - "LongCacheMiddleware": "stable-candidate", + "CachedOpenAI": "stable", + "get_openai_wrapper": "stable", + "LongCacheMiddleware": "stable", "LongCache.cached_stream": "pass-through", "StreamingCacheAdapter": "pass-through", "CachedLLM": "pass-through", diff --git a/src/longcache/integrations/langchain.py b/src/longcache/integrations/langchain.py index b945c11..d96f33a 100755 --- a/src/longcache/integrations/langchain.py +++ b/src/longcache/integrations/langchain.py @@ -1,4 +1,4 @@ -"""Compatibility adapter: opaque langchain state bypasses cache access in v0.2. +"""Compatibility adapter: opaque langchain state bypasses cache access in v1.0. Use explicit RequestContext with the manual interface for a proven workflow. """ @@ -6,15 +6,19 @@ from __future__ import annotations from collections.abc import AsyncIterator, Iterator -from typing import Any, Optional +from typing import Any, Literal, Optional +import warnings from longcache import CacheConfig, LongCache +from longcache.core.errors import LongCachePassThroughWarning class CachedLLM: - """Compatibility adapter; opaque framework state bypasses caching in v0.2.""" + """Compatibility adapter; opaque framework state bypasses caching in v1.0.""" - cache_bypass_reason = "unsupported_request_shape" + api_stability: Literal["pass-through"] = "pass-through" + is_passthrough: bool = True + cache_bypass_reason: str = "unsupported_request_shape" def __init__( self, @@ -22,6 +26,7 @@ def __init__( cache: Optional[LongCache] = None, config: Optional[CacheConfig] = None, namespace: str = "langchain", + warn_passthrough: bool = True, ) -> None: """ Initialize CachedLLM. @@ -31,7 +36,17 @@ def __init__( cache: Existing LongCache instance. Creates a new one if not provided. config: CacheConfig for the new cache (ignored if cache is provided). namespace: Default namespace for cache entries. + warn_passthrough: Whether to emit LongCachePassThroughWarning on initialization. """ + if warn_passthrough: + warnings.warn( + f"{self.__class__.__name__} is a compatibility pass-through adapter in LongCache 1.0 " + f"(reason={self.cache_bypass_reason!r}) and does not cache or reuse responses. " + "Use LongCache.get_or_compute() or @cache.cached with an explicit RequestContext " + "for verified semantic caching, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=2, + ) self.llm = llm self.cache = cache self._namespace = namespace @@ -108,8 +123,10 @@ def __repr__(self) -> str: class CachedChain: - """Compatibility adapter; opaque framework state bypasses caching in v0.2.""" + """Compatibility adapter; opaque framework state bypasses caching in v1.0.""" + api_stability: Literal["pass-through"] = "pass-through" + is_passthrough: bool = True cache_bypass_reason = "unsupported_request_shape" def __init__( @@ -119,6 +136,7 @@ def __init__( config: Optional[CacheConfig] = None, query_key: str = "question", namespace: str = "langchain_chain", + warn_passthrough: bool = True, ) -> None: """ Initialize CachedChain. @@ -129,7 +147,17 @@ def __init__( config: CacheConfig for the new cache (ignored if cache is provided). query_key: Key to extract from input dict as the cache query. namespace: Default namespace for cache entries. + warn_passthrough: Whether to emit LongCachePassThroughWarning on initialization. """ + if warn_passthrough: + warnings.warn( + f"{self.__class__.__name__} is a compatibility pass-through adapter in LongCache 1.0 " + f"(reason={self.cache_bypass_reason!r}) and does not cache or reuse responses. " + "Use LongCache.get_or_compute() or @cache.cached with an explicit RequestContext " + "for verified semantic caching, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=2, + ) self.chain = chain self.cache = cache self.query_key = query_key diff --git a/src/longcache/integrations/langgraph.py b/src/longcache/integrations/langgraph.py index 899cd3c..17ea727 100644 --- a/src/longcache/integrations/langgraph.py +++ b/src/longcache/integrations/langgraph.py @@ -1,28 +1,46 @@ -"""Compatibility adapter: opaque langgraph state bypasses cache access in v0.2. +"""Compatibility adapter: opaque langgraph state bypasses cache access in v1.0. Use explicit RequestContext with the manual interface for a proven workflow. """ from __future__ import annotations -from typing import Any +from typing import Any, Literal +import warnings from longcache import LongCache +from longcache.core.errors import LongCachePassThroughWarning class CachedState: - """Compatibility adapter; opaque framework state bypasses caching in v0.2.""" + """Compatibility adapter; opaque framework state bypasses caching in v1.0.""" + api_stability: Literal["pass-through"] = "pass-through" + is_passthrough: bool = True cache_bypass_reason = "unsupported_request_shape" + @classmethod + def _warn(cls) -> None: + warnings.warn( + f"{cls.__name__} is a compatibility pass-through adapter in LongCache 1.0 " + f"(reason={cls.cache_bypass_reason!r}) and does not cache or reuse responses. " + "Use LongCache.get_or_compute() or @cache.cached with an explicit RequestContext " + "for verified semantic caching, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=3, + ) + @staticmethod def check( state: dict[str, Any], cache: LongCache, query_key: str = "messages", namespace: str = "langgraph", + warn_passthrough: bool = True, ) -> dict[str, Any]: """Return a miss for opaque graph state, preserving legacy query extraction.""" + if warn_passthrough: + CachedState._warn() query_input = state.get(query_key) # Extract string query @@ -54,8 +72,11 @@ async def acheck( cache: LongCache, query_key: str = "messages", namespace: str = "langgraph", + warn_passthrough: bool = True, ) -> dict[str, Any]: """Return a miss for opaque graph state, preserving legacy query extraction.""" + if warn_passthrough: + CachedState._warn() query_input = state.get(query_key) # Extract string query @@ -87,9 +108,11 @@ def save( response: Any, cache: LongCache, namespace: str = "langgraph", + warn_passthrough: bool = True, ) -> None: - """No-op: graph state has no supported identity/replay contract in v0.2.""" - # Opaque graph state has no complete identity/replay contract in v0.2. + """No-op: graph state has no supported identity/replay contract in v1.0.""" + if warn_passthrough: + CachedState._warn() return None @staticmethod @@ -98,6 +121,9 @@ async def asave( response: Any, cache: LongCache, namespace: str = "langgraph", + warn_passthrough: bool = True, ) -> None: - """No-op: graph state has no supported identity/replay contract in v0.2.""" + """No-op: graph state has no supported identity/replay contract in v1.0.""" + if warn_passthrough: + CachedState._warn() return None diff --git a/src/longcache/integrations/llamaindex.py b/src/longcache/integrations/llamaindex.py index 1e5dcc1..5292e28 100755 --- a/src/longcache/integrations/llamaindex.py +++ b/src/longcache/integrations/llamaindex.py @@ -1,18 +1,22 @@ -"""Compatibility adapter: opaque llamaindex state bypasses cache access in v0.2. +"""Compatibility adapter: opaque llamaindex state bypasses cache access in v1.0. Use explicit RequestContext with the manual interface for a proven workflow. """ from __future__ import annotations -from typing import Any, Optional +from typing import Any, Literal, Optional +import warnings from longcache import LongCache, CacheConfig +from longcache.core.errors import LongCachePassThroughWarning class CachedQueryEngine: - """Compatibility adapter; opaque framework state bypasses caching in v0.2.""" + """Compatibility adapter; opaque framework state bypasses caching in v1.0.""" + api_stability: Literal["pass-through"] = "pass-through" + is_passthrough: bool = True cache_bypass_reason = "unsupported_request_shape" def __init__( @@ -21,6 +25,7 @@ def __init__( cache: Optional[LongCache] = None, config: Optional[CacheConfig] = None, namespace: str = "llamaindex", + warn_passthrough: bool = True, ) -> None: """ Initialize CachedQueryEngine. @@ -30,7 +35,17 @@ def __init__( cache: Existing LongCache instance. Creates a new one if not provided. config: CacheConfig for the new cache (ignored if cache is provided). namespace: Default namespace for cache entries. + warn_passthrough: Whether to emit LongCachePassThroughWarning on initialization. """ + if warn_passthrough: + warnings.warn( + f"{self.__class__.__name__} is a compatibility pass-through adapter in LongCache 1.0 " + f"(reason={self.cache_bypass_reason!r}) and does not cache or reuse responses. " + "Use LongCache.get_or_compute() or @cache.cached with an explicit RequestContext " + "for verified semantic caching, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=2, + ) self.query_engine = query_engine self.cache = cache self._namespace = namespace diff --git a/src/longcache/middleware.py b/src/longcache/middleware.py index 8f4696f..f66f23f 100755 --- a/src/longcache/middleware.py +++ b/src/longcache/middleware.py @@ -6,9 +6,11 @@ from dataclasses import replace import inspect import json -from typing import Any +from typing import Any, Literal +import warnings from longcache import CacheConfig, CacheError, LongCache, RequestContext, RequestScope +from longcache.core.errors import LongCachePassThroughWarning from longcache.integrations.openai import prepare_request Receive = Callable[[], Awaitable[MutableMapping[str, Any]]] @@ -240,8 +242,10 @@ async def decorated(message: MutableMapping[str, Any]) -> None: class StreamingCacheAdapter: - """Compatibility pass-through; streaming cache replay is unsupported in v0.2.""" + """Compatibility pass-through; streaming cache replay is unsupported in v1.0.""" + api_stability: Literal["pass-through"] = "pass-through" + is_passthrough: bool = True cache_bypass_reason = "unsupported_request_shape" def __init__( @@ -250,8 +254,18 @@ def __init__( stream_func: Callable[..., Any], namespace: str = "streaming", replay_delay: float = 0, + warn_passthrough: bool = True, **kwargs: Any, ) -> None: + if warn_passthrough: + warnings.warn( + f"{self.__class__.__name__} is a compatibility pass-through adapter in LongCache 1.0 " + f"(reason={self.cache_bypass_reason!r}) and does not cache or reuse responses. " + "Use LongCache.get_or_compute() or @cache.cached with an explicit RequestContext " + "for verified semantic caching, or pass warn_passthrough=False to silence this warning.", + LongCachePassThroughWarning, + stacklevel=2, + ) self.cache = cache self.stream_func = stream_func diff --git a/tests/fixtures/api_contract_v1_0.json b/tests/fixtures/api_contract_v1_0.json new file mode 100644 index 0000000..a282f99 --- /dev/null +++ b/tests/fixtures/api_contract_v1_0.json @@ -0,0 +1,4620 @@ +{ + "classes": { + "AbstractCacheBackend": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_ONLY", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_POSITIONAL", + "name": "args" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_KEYWORD", + "name": "kwargs" + } + ], + "return_annotation": "Any" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "bool" + }, + "delete_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete_namespace": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "ensure_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "dict[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "manifest" + } + ], + "return_annotation": "None" + }, + "evict_oldest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "count" + } + ], + "return_annotation": "int" + }, + "get_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[int]" + }, + "iter_index_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "512", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "batch_size" + } + ], + "return_annotation": "Iterator[CacheEntry]" + }, + "list_all_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "list_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "read_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any] | None" + }, + "retrieve": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "CacheEntry | None" + }, + "store": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + } + ], + "return_annotation": "int" + }, + "sweep_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "update_hit_count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + } + } + }, + "BaseEmbedder": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_ONLY", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_POSITIONAL", + "name": "args" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_KEYWORD", + "name": "kwargs" + } + ], + "return_annotation": "Any" + }, + "embed": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "text" + } + ], + "return_annotation": "Float32Vector" + }, + "embed_batch": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "list[str]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "texts" + } + ], + "return_annotation": "Float32Vector" + } + } + }, + "CacheConfig": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "float", + "default": "0.92", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "similarity_threshold" + }, + { + "annotation": "int", + "default": "3600", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "ttl_seconds" + }, + { + "annotation": "int", + "default": "10000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_entries" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "str", + "default": "'BAAI/bge-small-en-v1.5'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "embedder_model" + }, + { + "annotation": "BackendName", + "default": "'sqlite'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "backend" + }, + { + "annotation": "str", + "default": "'.longcache/v2/cache.db'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "db_path" + }, + { + "annotation": "str", + "default": "'redis://localhost:6379/0'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "redis_url" + }, + { + "annotation": "bool", + "default": "True", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "track_stats" + }, + { + "annotation": "bool", + "default": "True", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "singleflight_enabled" + }, + { + "annotation": "float", + "default": "30.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "singleflight_wait_timeout_seconds" + }, + { + "annotation": "int", + "default": "1024", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "singleflight_max_inflight" + }, + { + "annotation": "int", + "default": "256", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "singleflight_max_waiters_per_key" + }, + { + "annotation": "ErrorPolicy", + "default": "'raise'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cache_error_policy" + }, + { + "annotation": "float", + "default": "2.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "backend_timeout_seconds" + }, + { + "annotation": "float", + "default": "5.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shutdown_timeout_seconds" + }, + { + "annotation": "DeploymentMode", + "default": "'single_process'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "deployment_mode" + }, + { + "annotation": "float", + "default": "5.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_sync_timeout_seconds" + }, + { + "annotation": "int", + "default": "50000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_entries" + }, + { + "annotation": "int", + "default": "1024", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_leases" + }, + { + "annotation": "int", + "default": "100000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_receipts" + }, + { + "annotation": "int", + "default": "50000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_partitions" + }, + { + "annotation": "int", + "default": "100000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_distinct_tags" + }, + { + "annotation": "int", + "default": "64", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_tags_per_entry" + }, + { + "annotation": "int", + "default": "256", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "shared_max_tag_chars" + }, + { + "annotation": "float", + "default": "15.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "distributed_lease_seconds" + }, + { + "annotation": "float", + "default": "5.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "distributed_renew_interval_seconds" + }, + { + "annotation": "FaissIndexType", + "default": "'flat'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "faiss_index_type" + }, + { + "annotation": "int", + "default": "32", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "hnsw_m" + }, + { + "annotation": "int", + "default": "64", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "hnsw_ef_search" + }, + { + "annotation": "ScopePolicy", + "default": "'private'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "scope_policy" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cache_id" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "generation" + }, + { + "annotation": "str", + "default": "'safe-reuse-v1'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "policy_version" + }, + { + "annotation": "int", + "default": "8", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "verification_budget" + }, + { + "annotation": "int", + "default": "16384", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_query_chars" + }, + { + "annotation": "int", + "default": "1048576", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_capture_bytes" + }, + { + "annotation": "str", + "default": "''", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "embedder_revision" + } + ], + "return_annotation": "None" + }, + "from_dict": { + "parameters": [ + { + "annotation": "Mapping[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "data" + } + ], + "return_annotation": "CacheConfig" + }, + "from_env": { + "parameters": [ + { + "annotation": "str", + "default": "'LONGCACHE_'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "prefix" + } + ], + "return_annotation": "CacheConfig" + }, + "to_dict": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any]" + } + } + }, + "CacheEntry": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "id" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "str", + "default": "''", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "Float32Vector | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "vector" + }, + { + "annotation": "Any", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "response" + }, + { + "annotation": "float", + "default": "", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "created_at" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "expires_at" + }, + { + "annotation": "int", + "default": "0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "hit_count" + }, + { + "annotation": "list[str]", + "default": "", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tags" + }, + { + "annotation": "dict[str, Any]", + "default": "", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "metadata" + } + ], + "return_annotation": "None" + }, + "from_dict": { + "parameters": [ + { + "annotation": "Mapping[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "data" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "vector_dim" + } + ], + "return_annotation": "CacheEntry" + }, + "increment_hit": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "is_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "bool" + }, + "to_dict": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any]" + } + } + }, + "CachedOpenAI": { + "methods": { + "__aenter__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "CachedOpenAI" + }, + "__aexit__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "type[BaseException] | None", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc_type" + }, + { + "annotation": "BaseException | None", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "traceback" + } + ], + "return_annotation": "Any" + }, + "__enter__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "CachedOpenAI" + }, + "__exit__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "type[BaseException] | None", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc_type" + }, + { + "annotation": "BaseException | None", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "traceback" + } + ], + "return_annotation": "Any" + }, + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "client" + }, + { + "annotation": "LongCache | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cache" + }, + { + "annotation": "CacheConfig | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "config" + }, + { + "annotation": "str", + "default": "'openai'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "response_codec" + } + ], + "return_annotation": "None" + }, + "aclose": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + } + } + }, + "EmbeddingDescriptor": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "model" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "revision" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "dimension" + }, + { + "annotation": "str", + "default": "'text-v1'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "preprocessing" + }, + { + "annotation": "str", + "default": "'l2-float32-v1'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "normalization" + } + ], + "return_annotation": "None" + } + } + }, + "InMemoryCacheBackend": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "10000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_entries" + } + ], + "return_annotation": "None" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "bool" + }, + "delete_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete_namespace": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "ensure_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "dict[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "manifest" + } + ], + "return_annotation": "None" + }, + "evict_oldest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "count" + } + ], + "return_annotation": "int" + }, + "get_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[int]" + }, + "iter_index_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "512", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "batch_size" + } + ], + "return_annotation": "Iterator[CacheEntry]" + }, + "list_all_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "list_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "read_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any] | None" + }, + "retrieve": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "Optional[CacheEntry]" + }, + "store": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + } + ], + "return_annotation": "int" + }, + "sweep_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "update_hit_count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + } + } + }, + "Instruction": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "InstructionRole", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "role" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "content" + } + ], + "return_annotation": "None" + } + } + }, + "LongCache": { + "methods": { + "__aenter__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "LongCache" + }, + "__aexit__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc_type" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "traceback" + } + ], + "return_annotation": "None" + }, + "__enter__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "LongCache" + }, + "__exit__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc_type" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exc" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "traceback" + } + ], + "return_annotation": "None" + }, + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheConfig | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "config" + }, + { + "annotation": "BaseEmbedder | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "embedder" + }, + { + "annotation": "AdmissionHook | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "admit" + }, + { + "annotation": "VerificationHook | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "verifier" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "bool", + "default": "False", + "kind": "KEYWORD_ONLY", + "name": "metadata_only" + } + ], + "return_annotation": "None" + }, + "aclose": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "timeout" + } + ], + "return_annotation": "None" + }, + "aget": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "Any" + }, + "aget_or_compute": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "Callable[[], Awaitable[T]]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "compute" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "namespace" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "ttl" + }, + { + "annotation": "Sequence[str] | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "tags" + } + ], + "return_annotation": "T" + }, + "alookup": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "LookupDecision" + }, + "aset": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "response" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "ttl" + }, + { + "annotation": "Sequence[str] | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tags" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "StoreDecision" + }, + "cached": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Callable[..., Any] | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "func" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "namespace" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "ttl" + }, + { + "annotation": "Sequence[str] | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "tags" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "Callable[..., RequestContext | None] | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context_resolver" + } + ], + "return_annotation": "Any" + }, + "cached_stream": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Any", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "func" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_KEYWORD", + "name": "kwargs" + } + ], + "return_annotation": "Any" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "bool", + "default": "False", + "kind": "KEYWORD_ONLY", + "name": "operator" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "timeout" + } + ], + "return_annotation": "None" + }, + "entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "bool", + "default": "False", + "kind": "KEYWORD_ONLY", + "name": "operator" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "get": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "Any" + }, + "get_or_compute": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "Callable[[], T]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "compute" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "namespace" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "ttl" + }, + { + "annotation": "Sequence[str] | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "tags" + } + ], + "return_annotation": "T" + }, + "inspect": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "InspectionReport" + }, + "invalidate": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "int" + }, + "invalidate_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "int" + }, + "lookup": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "LookupDecision" + }, + "metrics": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "bool", + "default": "False", + "kind": "KEYWORD_ONLY", + "name": "operator" + } + ], + "return_annotation": "MetricsSnapshot" + }, + "set": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "response" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "ttl" + }, + { + "annotation": "Sequence[str] | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tags" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + } + ], + "return_annotation": "StoreDecision" + }, + "stats": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "namespace" + }, + { + "annotation": "bool", + "default": "False", + "kind": "KEYWORD_ONLY", + "name": "operator" + } + ], + "return_annotation": "StatsSnapshot" + } + } + }, + "LongCacheMiddleware": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "ASGIApp", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "app" + }, + { + "annotation": "LongCache | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cache" + }, + { + "annotation": "CacheConfig | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "config" + }, + { + "annotation": "str", + "default": "'http'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "Iterable[str] | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cached_routes" + }, + { + "annotation": "Iterable[str] | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exclude_namespaces" + }, + { + "annotation": "RequestContext | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "context" + }, + { + "annotation": "Callable[[MutableMapping[str, Any]], RequestScope | None] | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "scope_resolver" + } + ], + "return_annotation": "None" + }, + "aclose": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + } + } + }, + "LookupDecision": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "LookupOutcome", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "outcome" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "reason" + }, + { + "annotation": "Any", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "response" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "similarity" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "threshold" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "_epoch" + }, + { + "annotation": "float | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "_expires_at" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "_shared_epoch" + } + ], + "return_annotation": "None" + }, + "inspection": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "InspectionReport" + } + } + }, + "PolicyDecision": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "PolicyAction", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "action" + }, + { + "annotation": "str", + "default": "'application_policy'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "reason" + } + ], + "return_annotation": "None" + } + } + }, + "RedisCacheBackend": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": "'redis://localhost:6379/0'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "redis_url" + }, + { + "annotation": "int", + "default": "10000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_entries" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "cache_id" + }, + { + "annotation": "str", + "default": "'default'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "generation" + }, + { + "annotation": "float", + "default": "2.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "timeout" + } + ], + "return_annotation": "Any" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "bool" + }, + "delete_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete_namespace": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "ensure_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "dict[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "manifest" + } + ], + "return_annotation": "None" + }, + "evict_oldest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "count" + } + ], + "return_annotation": "int" + }, + "get_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[int]" + }, + "iter_index_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "256", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "batch_size" + } + ], + "return_annotation": "Iterator[CacheEntry]" + }, + "list_all_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "list_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "read_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any] | None" + }, + "retrieve": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "Optional[CacheEntry]" + }, + "store": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + } + ], + "return_annotation": "int" + }, + "sweep_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "update_hit_count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + } + } + }, + "RequestContext": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "RequestScope | None", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "scope" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "operation" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "provider" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "provider_scope" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "model" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "prompt_version" + }, + { + "annotation": "tuple[Instruction, ...]", + "default": "()", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "instructions" + }, + { + "annotation": "Mapping[str, Any]", + "default": "", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "generation" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "corpus_version" + }, + { + "annotation": "Mapping[str, str]", + "default": "", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "document_versions" + }, + { + "annotation": "tuple[str, ...]", + "default": "()", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "protected_entities" + }, + { + "annotation": "str", + "default": "'en'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "language" + }, + { + "annotation": "str", + "default": "'single-turn-text'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "request_shape" + }, + { + "annotation": "str", + "default": "'json-v1'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "codec" + } + ], + "return_annotation": "None" + }, + "exact_key": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "query" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "sharing" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "embedding" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "policy" + } + ], + "return_annotation": "str" + }, + "partition": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "sharing" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "embedding" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "policy" + } + ], + "return_annotation": "str" + }, + "record": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "sharing" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "embedding" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "policy" + } + ], + "return_annotation": "dict[str, Any]" + } + } + }, + "RequestScope": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tenant_id" + }, + { + "annotation": "str | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "user_id" + } + ], + "return_annotation": "None" + } + } + }, + "SQLiteCacheBackend": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": "'.longcache/cache.db'", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "db_path" + }, + { + "annotation": "int", + "default": "10000", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "max_entries" + }, + { + "annotation": "float", + "default": "2.0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "timeout" + } + ], + "return_annotation": "Any" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "bool" + }, + "delete_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete_namespace": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "ensure_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "dict[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "manifest" + } + ], + "return_annotation": "None" + }, + "evict_oldest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "count" + } + ], + "return_annotation": "int" + }, + "export_all": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[dict[str, Any]]" + }, + "get_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[int]" + }, + "increment_hits": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "increment_misses": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "0", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "saved_ms" + } + ], + "return_annotation": "None" + }, + "iter_index_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "512", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "batch_size" + } + ], + "return_annotation": "Iterator[CacheEntry]" + }, + "list_all_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "list_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "load_stats": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, int]" + }, + "read_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any] | None" + }, + "retrieve": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "Optional[CacheEntry]" + }, + "store": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + } + ], + "return_annotation": "int" + }, + "sweep_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "update_hit_count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + } + } + }, + "SharedRedisCacheBackend": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_POSITIONAL", + "name": "args" + }, + { + "annotation": "int", + "default": "50000", + "kind": "KEYWORD_ONLY", + "name": "shared_max_entries" + }, + { + "annotation": "int", + "default": "1024", + "kind": "KEYWORD_ONLY", + "name": "shared_max_leases" + }, + { + "annotation": "int", + "default": "100000", + "kind": "KEYWORD_ONLY", + "name": "shared_max_receipts" + }, + { + "annotation": "int", + "default": "50000", + "kind": "KEYWORD_ONLY", + "name": "shared_max_partitions" + }, + { + "annotation": "int", + "default": "100000", + "kind": "KEYWORD_ONLY", + "name": "shared_max_distinct_tags" + }, + { + "annotation": "int", + "default": "64", + "kind": "KEYWORD_ONLY", + "name": "shared_max_tags_per_entry" + }, + { + "annotation": "int", + "default": "256", + "kind": "KEYWORD_ONLY", + "name": "shared_max_tag_chars" + }, + { + "annotation": "float", + "default": "15.0", + "kind": "KEYWORD_ONLY", + "name": "distributed_lease_seconds" + }, + { + "annotation": "float", + "default": "5.0", + "kind": "KEYWORD_ONLY", + "name": "distributed_renew_interval_seconds" + }, + { + "annotation": "Any", + "default": null, + "kind": "VAR_KEYWORD", + "name": "kwargs" + } + ], + "return_annotation": "None" + }, + "acquire_flight": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "exact_key" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "ttl_ms" + } + ], + "return_annotation": "tuple[str, str | None]" + }, + "clear": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "close": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "None" + }, + "count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "bool" + }, + "delete_by_tag": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "tag" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "delete_namespace": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "int" + }, + "ensure_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "dict[str, Any]", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "manifest" + } + ], + "return_annotation": "None" + }, + "epoch": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "events": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "start" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "end" + }, + { + "annotation": "int", + "default": "256", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "limit" + } + ], + "return_annotation": "list[tuple[int, dict[str, Any]]]" + }, + "evict_oldest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "count" + } + ], + "return_annotation": "int" + }, + "finish_flight": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "key" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "token" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "outcome" + } + ], + "return_annotation": "bool" + }, + "flight_active": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "key" + } + ], + "return_annotation": "bool" + }, + "flight_outcome": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "key" + } + ], + "return_annotation": "str | None" + }, + "get_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[int]" + }, + "iter_index_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": "256", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "batch_size" + } + ], + "return_annotation": "Iterator[CacheEntry]" + }, + "list_all_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "list_entries": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "namespace" + } + ], + "return_annotation": "list[CacheEntry]" + }, + "read_manifest": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "dict[str, Any] | None" + }, + "release_flight": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "key" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "token" + } + ], + "return_annotation": "None" + }, + "renew_flight": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "key" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "token" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "ttl_ms" + } + ], + "return_annotation": "bool" + }, + "retrieve": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "CacheEntry | None" + }, + "revision": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "store": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + } + ], + "return_annotation": "int" + }, + "store_shared": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "KEYWORD_ONLY", + "name": "expected_epoch" + }, + { + "annotation": "str", + "default": "''", + "kind": "KEYWORD_ONLY", + "name": "lease_key" + }, + { + "annotation": "str", + "default": "''", + "kind": "KEYWORD_ONLY", + "name": "lease_token" + } + ], + "return_annotation": "tuple[int, int | None]" + }, + "sweep_expired": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + } + ], + "return_annotation": "int" + }, + "update_hit_count": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "int", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + }, + "validate_hit": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "CacheEntry", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry" + }, + { + "annotation": "int", + "default": null, + "kind": "KEYWORD_ONLY", + "name": "expected_epoch" + } + ], + "return_annotation": "bool" + } + } + }, + "StoreDecision": { + "methods": { + "__init__": { + "parameters": [ + { + "annotation": "Any", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "self" + }, + { + "annotation": "StoreOutcome", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "outcome" + }, + { + "annotation": "str", + "default": null, + "kind": "POSITIONAL_OR_KEYWORD", + "name": "reason" + }, + { + "annotation": "int | None", + "default": "None", + "kind": "POSITIONAL_OR_KEYWORD", + "name": "entry_id" + } + ], + "return_annotation": "None" + } + } + } + }, + "cli_commands": { + "clear": { + "all_partitions": { + "default": "False", + "is_flag": true, + "opts": [ + "--all" + ], + "required": false + }, + "context_file": { + "default": null, + "is_flag": false, + "opts": [ + "--context-file" + ], + "required": false + }, + "db_path": { + "default": "'.longcache/v2/cache.db'", + "is_flag": false, + "opts": [ + "--db-path" + ], + "required": false + }, + "namespace": { + "default": "'default'", + "is_flag": false, + "opts": [ + "--namespace" + ], + "required": false + }, + "operator": { + "default": "False", + "is_flag": true, + "opts": [ + "--operator" + ], + "required": false + }, + "scope_policy": { + "default": "'private'", + "is_flag": false, + "opts": [ + "--scope-policy" + ], + "required": false + }, + "yes": { + "default": "False", + "is_flag": true, + "opts": [ + "--yes" + ], + "required": false + } + }, + "export": { + "context_file": { + "default": null, + "is_flag": false, + "opts": [ + "--context-file" + ], + "required": false + }, + "db_path": { + "default": "'.longcache/v2/cache.db'", + "is_flag": false, + "opts": [ + "--db-path" + ], + "required": false + }, + "namespace": { + "default": "'default'", + "is_flag": false, + "opts": [ + "--namespace" + ], + "required": false + }, + "operator": { + "default": "False", + "is_flag": true, + "opts": [ + "--operator" + ], + "required": false + }, + "output": { + "default": null, + "is_flag": false, + "opts": [ + "--output", + "-o" + ], + "required": false + }, + "output_format": { + "default": "'json'", + "is_flag": false, + "opts": [ + "--format" + ], + "required": false + }, + "scope_policy": { + "default": "'private'", + "is_flag": false, + "opts": [ + "--scope-policy" + ], + "required": false + } + }, + "inspect": { + "context_file": { + "default": null, + "is_flag": false, + "opts": [ + "--context-file" + ], + "required": false + }, + "db_path": { + "default": "'.longcache/v2/cache.db'", + "is_flag": false, + "opts": [ + "--db-path" + ], + "required": false + }, + "namespace": { + "default": "'default'", + "is_flag": false, + "opts": [ + "--namespace" + ], + "required": false + }, + "operator": { + "default": "False", + "is_flag": true, + "opts": [ + "--operator" + ], + "required": false + }, + "query": { + "default": null, + "is_flag": false, + "opts": [ + "--query" + ], + "required": true + }, + "scope_policy": { + "default": "'private'", + "is_flag": false, + "opts": [ + "--scope-policy" + ], + "required": false + } + }, + "stats": { + "context_file": { + "default": null, + "is_flag": false, + "opts": [ + "--context-file" + ], + "required": false + }, + "db_path": { + "default": "'.longcache/v2/cache.db'", + "is_flag": false, + "opts": [ + "--db-path" + ], + "required": false + }, + "namespace": { + "default": "'default'", + "is_flag": false, + "opts": [ + "--namespace" + ], + "required": false + }, + "operator": { + "default": "False", + "is_flag": true, + "opts": [ + "--operator" + ], + "required": false + }, + "scope_policy": { + "default": "'private'", + "is_flag": false, + "opts": [ + "--scope-policy" + ], + "required": false + } + } + }, + "contract_version": "1.0", + "dataclasses": { + "CacheConfig": [ + { + "default": "0.92", + "name": "similarity_threshold", + "type": "float" + }, + { + "default": "3600", + "name": "ttl_seconds", + "type": "int" + }, + { + "default": "10000", + "name": "max_entries", + "type": "int" + }, + { + "default": "'default'", + "name": "namespace", + "type": "str" + }, + { + "default": "'BAAI/bge-small-en-v1.5'", + "name": "embedder_model", + "type": "str" + }, + { + "default": "'sqlite'", + "name": "backend", + "type": "BackendName" + }, + { + "default": "'.longcache/v2/cache.db'", + "name": "db_path", + "type": "str" + }, + { + "default": "'redis://localhost:6379/0'", + "name": "redis_url", + "type": "str" + }, + { + "default": "True", + "name": "track_stats", + "type": "bool" + }, + { + "default": "True", + "name": "singleflight_enabled", + "type": "bool" + }, + { + "default": "30.0", + "name": "singleflight_wait_timeout_seconds", + "type": "float" + }, + { + "default": "1024", + "name": "singleflight_max_inflight", + "type": "int" + }, + { + "default": "256", + "name": "singleflight_max_waiters_per_key", + "type": "int" + }, + { + "default": "'raise'", + "name": "cache_error_policy", + "type": "ErrorPolicy" + }, + { + "default": "2.0", + "name": "backend_timeout_seconds", + "type": "float" + }, + { + "default": "5.0", + "name": "shutdown_timeout_seconds", + "type": "float" + }, + { + "default": "'single_process'", + "name": "deployment_mode", + "type": "DeploymentMode" + }, + { + "default": "5.0", + "name": "shared_sync_timeout_seconds", + "type": "float" + }, + { + "default": "50000", + "name": "shared_max_entries", + "type": "int" + }, + { + "default": "1024", + "name": "shared_max_leases", + "type": "int" + }, + { + "default": "100000", + "name": "shared_max_receipts", + "type": "int" + }, + { + "default": "50000", + "name": "shared_max_partitions", + "type": "int" + }, + { + "default": "100000", + "name": "shared_max_distinct_tags", + "type": "int" + }, + { + "default": "64", + "name": "shared_max_tags_per_entry", + "type": "int" + }, + { + "default": "256", + "name": "shared_max_tag_chars", + "type": "int" + }, + { + "default": "15.0", + "name": "distributed_lease_seconds", + "type": "float" + }, + { + "default": "5.0", + "name": "distributed_renew_interval_seconds", + "type": "float" + }, + { + "default": "'flat'", + "name": "faiss_index_type", + "type": "FaissIndexType" + }, + { + "default": "32", + "name": "hnsw_m", + "type": "int" + }, + { + "default": "64", + "name": "hnsw_ef_search", + "type": "int" + }, + { + "default": "'private'", + "name": "scope_policy", + "type": "ScopePolicy" + }, + { + "default": "'default'", + "name": "cache_id", + "type": "str" + }, + { + "default": "'default'", + "name": "generation", + "type": "str" + }, + { + "default": "'safe-reuse-v1'", + "name": "policy_version", + "type": "str" + }, + { + "default": "8", + "name": "verification_budget", + "type": "int" + }, + { + "default": "16384", + "name": "max_query_chars", + "type": "int" + }, + { + "default": "1048576", + "name": "max_capture_bytes", + "type": "int" + }, + { + "default": "''", + "name": "embedder_revision", + "type": "str" + } + ], + "CacheEntry": [ + { + "default": "None", + "name": "id", + "type": "int | None" + }, + { + "default": "'default'", + "name": "namespace", + "type": "str" + }, + { + "default": "''", + "name": "query", + "type": "str" + }, + { + "default": "None", + "name": "vector", + "type": "Float32Vector | None" + }, + { + "default": "None", + "name": "response", + "type": "Any" + }, + { + "default": null, + "name": "created_at", + "type": "float" + }, + { + "default": "None", + "name": "expires_at", + "type": "float | None" + }, + { + "default": "0", + "name": "hit_count", + "type": "int" + }, + { + "default": null, + "name": "tags", + "type": "list[str]" + }, + { + "default": null, + "name": "metadata", + "type": "dict[str, Any]" + } + ], + "EmbeddingDescriptor": [ + { + "default": null, + "name": "model", + "type": "str" + }, + { + "default": null, + "name": "revision", + "type": "str" + }, + { + "default": null, + "name": "dimension", + "type": "int" + }, + { + "default": "'text-v1'", + "name": "preprocessing", + "type": "str" + }, + { + "default": "'l2-float32-v1'", + "name": "normalization", + "type": "str" + } + ], + "Instruction": [ + { + "default": null, + "name": "role", + "type": "InstructionRole" + }, + { + "default": null, + "name": "content", + "type": "str" + } + ], + "LookupDecision": [ + { + "default": null, + "name": "outcome", + "type": "LookupOutcome" + }, + { + "default": null, + "name": "reason", + "type": "str" + }, + { + "default": "None", + "name": "response", + "type": "Any" + }, + { + "default": "None", + "name": "similarity", + "type": "float | None" + }, + { + "default": "None", + "name": "entry_id", + "type": "int | None" + }, + { + "default": "None", + "name": "threshold", + "type": "float | None" + }, + { + "default": "None", + "name": "_epoch", + "type": "int | None" + }, + { + "default": "None", + "name": "_expires_at", + "type": "float | None" + }, + { + "default": "None", + "name": "_shared_epoch", + "type": "int | None" + } + ], + "PolicyDecision": [ + { + "default": null, + "name": "action", + "type": "PolicyAction" + }, + { + "default": "'application_policy'", + "name": "reason", + "type": "str" + } + ], + "RequestContext": [ + { + "default": null, + "name": "scope", + "type": "RequestScope | None" + }, + { + "default": null, + "name": "operation", + "type": "str" + }, + { + "default": null, + "name": "provider", + "type": "str" + }, + { + "default": null, + "name": "provider_scope", + "type": "str" + }, + { + "default": null, + "name": "model", + "type": "str" + }, + { + "default": null, + "name": "prompt_version", + "type": "str" + }, + { + "default": "()", + "name": "instructions", + "type": "tuple[Instruction, ...]" + }, + { + "default": null, + "name": "generation", + "type": "Mapping[str, Any]" + }, + { + "default": "None", + "name": "corpus_version", + "type": "str | None" + }, + { + "default": null, + "name": "document_versions", + "type": "Mapping[str, str]" + }, + { + "default": "()", + "name": "protected_entities", + "type": "tuple[str, ...]" + }, + { + "default": "'en'", + "name": "language", + "type": "str" + }, + { + "default": "'single-turn-text'", + "name": "request_shape", + "type": "str" + }, + { + "default": "'json-v1'", + "name": "codec", + "type": "str" + } + ], + "RequestScope": [ + { + "default": null, + "name": "tenant_id", + "type": "str" + }, + { + "default": "None", + "name": "user_id", + "type": "str | None" + } + ], + "StoreDecision": [ + { + "default": null, + "name": "outcome", + "type": "StoreOutcome" + }, + { + "default": null, + "name": "reason", + "type": "str" + }, + { + "default": "None", + "name": "entry_id", + "type": "int | None" + } + ] + }, + "exceptions": { + "CacheCapacityError": [ + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "CacheClosedError": [ + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "CacheError": [ + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "CacheOperationError": [ + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "CacheShutdownTimeoutError": [ + "CacheError", + "RuntimeError", + "TimeoutError", + "OSError", + "Exception", + "BaseException", + "object" + ], + "LongCacheExperimentalWarning": [ + "LongCacheWarning", + "UserWarning", + "Warning", + "Exception", + "BaseException", + "object" + ], + "LongCachePassThroughWarning": [ + "LongCacheWarning", + "UserWarning", + "Warning", + "Exception", + "BaseException", + "object" + ], + "LongCacheWarning": [ + "UserWarning", + "Warning", + "Exception", + "BaseException", + "object" + ], + "SingleflightCapacityError": [ + "SingleflightError", + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "SingleflightError": [ + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "SingleflightInvalidatedError": [ + "SingleflightError", + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "SingleflightLeaderError": [ + "SingleflightError", + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "SingleflightReentrancyError": [ + "SingleflightError", + "CacheError", + "RuntimeError", + "Exception", + "BaseException", + "object" + ], + "SingleflightTimeoutError": [ + "SingleflightError", + "CacheError", + "RuntimeError", + "TimeoutError", + "OSError", + "Exception", + "BaseException", + "object" + ] + }, + "modules_exports": { + "longcache": [ + "__version__", + "API_CONTRACT_VERSION", + "StabilityTier", + "LongCache", + "CacheConfig", + "Instruction", + "RequestContext", + "RequestScope", + "LookupDecision", + "StoreDecision", + "PolicyDecision", + "BaseEmbedder", + "EmbeddingDescriptor", + "CacheEntry", + "AdmissionHook", + "VerificationHook", + "ContextResolver", + "ScopeResolver", + "CachedCallable", + "InspectionReport", + "StatsSnapshot", + "ResourceSnapshot", + "MetricsSnapshot", + "ScopePolicy", + "BackendName", + "DeploymentMode", + "ErrorPolicy", + "FaissIndexType", + "InstructionRole", + "ResponseCodec", + "LookupOutcome", + "StoreOutcome", + "PolicyAction", + "HealthStatus", + "Float32Vector", + "PUBLIC_API_STABILITY", + "CacheError", + "CacheClosedError", + "CacheShutdownTimeoutError", + "CacheCapacityError", + "CacheOperationError", + "SingleflightError", + "SingleflightTimeoutError", + "SingleflightCapacityError", + "SingleflightLeaderError", + "SingleflightReentrancyError", + "SingleflightInvalidatedError", + "LongCacheWarning", + "LongCachePassThroughWarning", + "LongCacheExperimentalWarning" + ], + "longcache.backends": [ + "AbstractCacheBackend", + "SQLiteCacheBackend", + "InMemoryCacheBackend", + "get_redis_backend" + ], + "longcache.integrations": [ + "CachedLLM", + "CachedChain", + "get_llamaindex_wrapper", + "get_openai_wrapper" + ], + "longcache.middleware": [ + "LongCacheMiddleware", + "StreamingCacheAdapter" + ] + }, + "stability_tiers": { + "API_CONTRACT_VERSION": "stable", + "AbstractCacheBackend": "stable", + "AdmissionHook": "stable", + "AsyncRedisCacheBackend": "experimental", + "BackendName": "stable", + "BaseEmbedder": "stable", + "CacheCapacityError": "stable", + "CacheClosedError": "stable", + "CacheConfig": "stable", + "CacheEntry": "stable", + "CacheError": "stable", + "CacheOperationError": "stable", + "CacheShutdownTimeoutError": "stable", + "CachedCallable": "stable", + "CachedChain": "pass-through", + "CachedLLM": "pass-through", + "CachedOpenAI": "stable", + "CachedQueryEngine": "pass-through", + "CachedState": "pass-through", + "ContextResolver": "stable", + "DeploymentMode": "stable", + "EmbeddingDescriptor": "stable", + "ErrorPolicy": "stable", + "FaissIndexType": "stable", + "Float32Vector": "stable", + "HealthStatus": "stable", + "InMemoryCacheBackend": "stable", + "InspectionReport": "stable", + "Instruction": "stable", + "InstructionRole": "stable", + "LongCache": "stable", + "LongCache.cached_stream": "pass-through", + "LongCacheExperimentalWarning": "stable", + "LongCacheMiddleware": "stable", + "LongCachePassThroughWarning": "stable", + "LongCacheWarning": "stable", + "LookupDecision": "stable", + "LookupOutcome": "stable", + "MetricsSnapshot": "stable", + "PUBLIC_API_STABILITY": "stable", + "PolicyAction": "stable", + "PolicyDecision": "stable", + "RedisCacheBackend": "stable", + "RequestContext": "stable", + "RequestScope": "stable", + "ResourceSnapshot": "stable", + "ResponseCodec": "stable", + "SQLiteCacheBackend": "stable", + "ScopePolicy": "stable", + "ScopeResolver": "stable", + "SharedCoordinationBackend": "stable", + "SharedRedisCacheBackend": "stable", + "SingleflightCapacityError": "stable", + "SingleflightError": "stable", + "SingleflightInvalidatedError": "stable", + "SingleflightLeaderError": "stable", + "SingleflightReentrancyError": "stable", + "SingleflightTimeoutError": "stable", + "StabilityTier": "stable", + "StatsSnapshot": "stable", + "StoreDecision": "stable", + "StoreOutcome": "stable", + "StreamingCacheAdapter": "pass-through", + "VerificationHook": "stable", + "__version__": "stable", + "get_llamaindex_wrapper": "pass-through", + "get_openai_wrapper": "stable", + "get_redis_backend": "stable" + } +} diff --git a/tests/test_api_freeze_v1.py b/tests/test_api_freeze_v1.py new file mode 100644 index 0000000..bc90e8a --- /dev/null +++ b/tests/test_api_freeze_v1.py @@ -0,0 +1,218 @@ +"""Test API freeze and contract lock for LongCache v1.0.0. + +Guarantees that public signatures, parameter kinds/defaults, exports, +stability tiers, dataclass fields, and exception hierarchies cannot break +in v1.0.0 or future 1.x releases. +""" + +from __future__ import annotations + +import dataclasses +import inspect +import json +from pathlib import Path +import pytest + +import longcache +import longcache.backends +from longcache.backends.base import AbstractCacheBackend +from longcache.backends.memory import InMemoryCacheBackend +from longcache.backends.redis import RedisCacheBackend +from longcache.backends.redis_shared import SharedRedisCacheBackend +from longcache.backends.sqlite import SQLiteCacheBackend +from longcache import cli +import longcache.integrations +from longcache.integrations.openai import CachedOpenAI +import longcache.middleware +from longcache.middleware import LongCacheMiddleware +from longcache.core.errors import ( + CacheError, + CacheClosedError, + CacheShutdownTimeoutError, + CacheCapacityError, + CacheOperationError, + SingleflightError, + SingleflightTimeoutError, + SingleflightCapacityError, + SingleflightLeaderError, + SingleflightReentrancyError, + SingleflightInvalidatedError, + LongCacheWarning, + LongCachePassThroughWarning, + LongCacheExperimentalWarning, +) + +FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "api_contract_v1_0.json" + + +@pytest.fixture(scope="module") +def api_contract() -> dict: + assert FIXTURE_PATH.is_file(), f"API contract fixture missing at {FIXTURE_PATH}" + return json.loads(FIXTURE_PATH.read_text(encoding="utf-8")) + + +def test_api_contract_version_and_release_metadata(): + """Verify API_CONTRACT_VERSION is '1.0' and version starts with 1.0.0.""" + assert hasattr(longcache, "API_CONTRACT_VERSION") + assert longcache.API_CONTRACT_VERSION == "1.0" + assert longcache.__version__.startswith("1.0.0") + + +def test_public_api_stability_promoted_to_stable(api_contract): + """Verify all qualified symbols have graduated from stable-candidate to stable.""" + assert longcache.PUBLIC_API_STABILITY == api_contract["stability_tiers"] + assert "stable-candidate" not in set(longcache.PUBLIC_API_STABILITY.values()) + + # Shared Redis backend must be stable in 1.0.0 + assert longcache.PUBLIC_API_STABILITY["SharedRedisCacheBackend"] == "stable" + assert longcache.PUBLIC_API_STABILITY["SharedCoordinationBackend"] == "stable" + + # Standalone async Redis backend remains experimental + assert longcache.PUBLIC_API_STABILITY["AsyncRedisCacheBackend"] == "experimental" + + +def test_public_module_exports_match_contract(api_contract): + """Verify that module __all__ exports match the frozen contract.""" + modules = { + "longcache": longcache, + "longcache.backends": longcache.backends, + "longcache.integrations": longcache.integrations, + "longcache.middleware": longcache.middleware, + } + for mod_name, mod in modules.items(): + assert mod.__all__ == api_contract["modules_exports"][mod_name] + + +def test_stable_classes_signatures_match_contract(api_contract): + """Verify method parameters, kinds, and defaults for all stable classes.""" + classes = { + "LongCache": longcache.LongCache, + "CacheConfig": longcache.CacheConfig, + "RequestScope": longcache.RequestScope, + "RequestContext": longcache.RequestContext, + "Instruction": longcache.Instruction, + "LookupDecision": longcache.LookupDecision, + "StoreDecision": longcache.StoreDecision, + "PolicyDecision": longcache.PolicyDecision, + "BaseEmbedder": longcache.BaseEmbedder, + "EmbeddingDescriptor": longcache.EmbeddingDescriptor, + "CacheEntry": longcache.CacheEntry, + "AbstractCacheBackend": AbstractCacheBackend, + "SQLiteCacheBackend": SQLiteCacheBackend, + "RedisCacheBackend": RedisCacheBackend, + "SharedRedisCacheBackend": SharedRedisCacheBackend, + "InMemoryCacheBackend": InMemoryCacheBackend, + "CachedOpenAI": CachedOpenAI, + "LongCacheMiddleware": LongCacheMiddleware, + } + + for cls_name, cls in classes.items(): + expected_methods = api_contract["classes"][cls_name]["methods"] + for method_name, expected in expected_methods.items(): + assert hasattr(cls, method_name), ( + f"Class {cls_name} missing frozen method {method_name}" + ) + method = getattr(cls, method_name) + sig = inspect.signature(method) + actual_params = [] + for p_name, param in sig.parameters.items(): + default_repr = ( + repr(param.default) if param.default is not inspect.Parameter.empty else None + ) + actual_params.append( + { + "name": p_name, + "kind": param.kind.name, + "default": default_repr, + "annotation": str(param.annotation) + if param.annotation is not inspect.Parameter.empty + else "Any", + } + ) + # Verify parameter names and kinds + expected_params = expected["parameters"] + assert len(actual_params) == len(expected_params), ( + f"{cls_name}.{method_name} parameter count mismatch" + ) + for act, exp in zip(actual_params, expected_params): + assert act["name"] == exp["name"], ( + f"{cls_name}.{method_name} parameter name mismatch: {act['name']} != {exp['name']}" + ) + assert act["kind"] == exp["kind"], ( + f"{cls_name}.{method_name} parameter {act['name']} kind mismatch: {act['kind']} != {exp['kind']}" + ) + assert act["default"] == exp["default"], ( + f"{cls_name}.{method_name} parameter {act['name']} default mismatch: {act['default']} != {exp['default']}" + ) + + +def test_dataclass_fields_match_contract(api_contract): + """Verify dataclass field names, order, and defaults.""" + dataclass_map = { + "CacheConfig": longcache.CacheConfig, + "RequestScope": longcache.RequestScope, + "RequestContext": longcache.RequestContext, + "Instruction": longcache.Instruction, + "LookupDecision": longcache.LookupDecision, + "StoreDecision": longcache.StoreDecision, + "PolicyDecision": longcache.PolicyDecision, + "EmbeddingDescriptor": longcache.EmbeddingDescriptor, + "CacheEntry": longcache.CacheEntry, + } + for name, cls in dataclass_map.items(): + expected_fields = api_contract["dataclasses"][name] + actual_fields = [ + { + "name": f.name, + "type": str(f.type), + "default": repr(f.default) if f.default is not dataclasses.MISSING else None, + } + for f in dataclasses.fields(cls) + ] + assert [f["name"] for f in actual_fields] == [f["name"] for f in expected_fields] + assert [f["default"] for f in actual_fields] == [f["default"] for f in expected_fields] + + +def test_exception_and_warning_hierarchy_matches_contract(api_contract): + """Verify exception and warning inheritance hierarchies.""" + exc_list = [ + CacheError, + CacheClosedError, + CacheShutdownTimeoutError, + CacheCapacityError, + CacheOperationError, + SingleflightError, + SingleflightTimeoutError, + SingleflightCapacityError, + SingleflightLeaderError, + SingleflightReentrancyError, + SingleflightInvalidatedError, + LongCacheWarning, + LongCachePassThroughWarning, + LongCacheExperimentalWarning, + ] + for exc in exc_list: + actual_bases = [base.__name__ for base in exc.__mro__[1:]] + expected_bases = api_contract["exceptions"][exc.__name__] + assert actual_bases == expected_bases, f"Exception hierarchy mismatch for {exc.__name__}" + + +def test_cli_commands_and_options_match_contract(api_contract): + """Verify Click CLI commands and parameter options.""" + for cmd_name, expected_options in api_contract["cli_commands"].items(): + assert cmd_name in cli.main.commands, f"CLI command {cmd_name} missing" + cmd = cli.main.commands[cmd_name] + actual_options = {} + for p in cmd.params: + d = p.default + if d is None or "Sentinel" in str(d): + norm = None + else: + norm = repr(d) + actual_options[p.name] = { + "opts": sorted(list(p.opts)), + "required": p.required, + "is_flag": getattr(p, "is_flag", False), + "default": norm, + } + assert actual_options == expected_options, f"CLI command options mismatch for {cmd_name}" diff --git a/tests/test_public_api_v5.py b/tests/test_public_api_v5.py index c9407bd..5f48cfb 100644 --- a/tests/test_public_api_v5.py +++ b/tests/test_public_api_v5.py @@ -1,10 +1,12 @@ """Verify public API surface, parameter cleanliness, and stability classification for v0.5.0.""" import inspect +from pathlib import Path from typing import get_type_hints import pytest import longcache import longcache.backends +from longcache.backends.async_redis import AsyncRedisCacheBackend import longcache.integrations from longcache.integrations.langchain import CachedChain, CachedLLM from longcache.integrations.langgraph import CachedState @@ -18,12 +20,16 @@ ) from longcache.backends.base import AbstractCacheBackend from longcache.backends.redis_shared import SharedRedisCacheBackend +from longcache.core.errors import ( + LongCacheExperimentalWarning, + LongCachePassThroughWarning, +) def test_public_api_stability_classification_completeness(): """Every symbol exported across public module __all__ lists must be classified.""" assert isinstance(PUBLIC_API_STABILITY, dict) - valid_tiers = {"stable-candidate", "experimental", "pass-through"} + valid_tiers = {"stable", "experimental", "pass-through"} for mod in ( longcache, @@ -39,6 +45,7 @@ def test_public_api_stability_classification_completeness(): assert PUBLIC_API_STABILITY[name] in valid_tiers, ( f"Symbol {name!r} has invalid tier: {PUBLIC_API_STABILITY[name]}" ) + assert "stable-candidate" not in set(PUBLIC_API_STABILITY.values()) def test_longcache_public_methods_have_no_private_or_legacy_kwargs(): @@ -99,9 +106,109 @@ def test_pass_through_classes_expose_bypass_reason(): assert getattr(cls, "cache_bypass_reason", None) == "unsupported_request_shape", ( f"{cls.__name__} missing cache_bypass_reason == 'unsupported_request_shape'" ) + assert getattr(cls, "is_passthrough", False) is True + assert getattr(cls, "api_stability", None) == "pass-through" assert PUBLIC_API_STABILITY.get(cls.__name__) == "pass-through" +def test_pass_through_classes_emit_warnings_by_default(): + """Instantiating or invoking pass-through wrappers emits LongCachePassThroughWarning.""" + + class DummyLLM: + def invoke(self, *a, **kw): + return "ans" + + def stream(self, *a, **kw): + yield "ans" + + cache = LongCache(CacheConfig(backend="memory")) + + with pytest.warns( + LongCachePassThroughWarning, match="CachedLLM is a compatibility pass-through" + ): + CachedLLM(DummyLLM(), cache=cache) + + with pytest.warns( + LongCachePassThroughWarning, match="CachedChain is a compatibility pass-through" + ): + CachedChain(DummyLLM(), cache=cache) + + with pytest.warns( + LongCachePassThroughWarning, match="CachedQueryEngine is a compatibility pass-through" + ): + CachedQueryEngine(DummyLLM(), cache=cache) + + with pytest.warns( + LongCachePassThroughWarning, match="StreamingCacheAdapter is a compatibility pass-through" + ): + StreamingCacheAdapter(cache=cache, stream_func=lambda q: ["chunk"]) + + with pytest.warns( + LongCachePassThroughWarning, match="cached_stream is a compatibility pass-through" + ): + cache.cached_stream(lambda q: ["chunk"]) + + with pytest.warns( + LongCachePassThroughWarning, match="CachedState is a compatibility pass-through" + ): + CachedState.check({"messages": "hi"}, cache=cache) + + with pytest.warns( + LongCachePassThroughWarning, match="CachedState is a compatibility pass-through" + ): + CachedState.save({"messages": "hi"}, "response", cache=cache) + + +def test_pass_through_classes_can_silence_warnings(): + """Passing warn_passthrough=False suppresses LongCachePassThroughWarning.""" + import warnings + + class DummyLLM: + def invoke(self, *a, **kw): + return "ans" + + cache = LongCache(CacheConfig(backend="memory")) + with warnings.catch_warnings(): + warnings.simplefilter("error", LongCachePassThroughWarning) + CachedLLM(DummyLLM(), cache=cache, warn_passthrough=False) + CachedChain(DummyLLM(), cache=cache, warn_passthrough=False) + CachedQueryEngine(DummyLLM(), cache=cache, warn_passthrough=False) + StreamingCacheAdapter(cache=cache, stream_func=lambda q: ["chunk"], warn_passthrough=False) + cache.cached_stream(lambda q: ["chunk"], warn_passthrough=False) + CachedState.check({"messages": "hi"}, cache=cache, warn_passthrough=False) + CachedState.save({"messages": "hi"}, "response", cache=cache, warn_passthrough=False) + + +def test_async_redis_backend_emits_experimental_warning(): + """AsyncRedisCacheBackend emits LongCacheExperimentalWarning by default.""" + with pytest.warns(LongCacheExperimentalWarning, match="experimental standalone backend"): + AsyncRedisCacheBackend(redis_url="redis://localhost:6379/0") + + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("error", LongCacheExperimentalWarning) + AsyncRedisCacheBackend(redis_url="redis://localhost:6379/0", warn_experimental=False) + + +def test_support_matrix_matches_public_api_stability(): + """Verify docs/support-matrix.md mentions all symbols in PUBLIC_API_STABILITY.""" + doc_path = Path(__file__).resolve().parent.parent / "docs" / "support-matrix.md" + assert doc_path.is_file(), f"Support matrix doc missing at {doc_path}" + content = doc_path.read_text(encoding="utf-8") + + # Every stable and pass-through symbol must be listed in support-matrix.md + for symbol, tier in PUBLIC_API_STABILITY.items(): + if symbol in ( + "__version__", + "PUBLIC_API_STABILITY", + "StabilityTier", + "API_CONTRACT_VERSION", + ): + continue + assert symbol in content, f"Symbol {symbol} (tier: {tier}) missing from {doc_path.name}" + + def test_dead_parameters_are_not_accepted_by_cache_config_or_methods(): """Retired parameters (model, temperature, system_prompt, track_miss) cannot be passed.""" for dead_arg in ["model", "temperature", "system_prompt", "track_miss"]: diff --git a/tests/test_v1_persistence_compat.py b/tests/test_v1_persistence_compat.py new file mode 100644 index 0000000..e0da93f --- /dev/null +++ b/tests/test_v1_persistence_compat.py @@ -0,0 +1,293 @@ +"""Persistence compatibility tests for LongCache v1.0.0. + +Guarantees: +1. Pinned golden SHA-256 digests for partitions, exact keys, and embedding descriptors. +2. Pre-populated SQLite Format-2 databases upgrade to v1.0 with zero data loss or invalidation. +3. Pre-populated Redis Format-2 single-process and shared stores upgrade cleanly. +""" + +from __future__ import annotations + +from dataclasses import asdict +import json +import os +from pathlib import Path +import sqlite3 +import time +from uuid import uuid4 + +import pytest +import redis + +from longcache import ( + CacheConfig, + EmbeddingDescriptor, + Instruction, + LongCache, + RequestContext, + RequestScope, +) +from longcache.core.identity import canonical, digest +from tests.helpers import HashEmbedder + +REDIS_URL = os.environ.get("LONGCACHE_TEST_REDIS_URL") + + +def test_golden_embedding_and_partition_digests(): + """Verify that canonical digest algorithms match frozen golden vectors exactly.""" + desc = EmbeddingDescriptor( + model="BAAI/bge-small-en-v1.5", + revision="5c38ec7c405ec4b44b94cc5a9bb96e735b38267a", + dimension=384, + ) + assert desc.fingerprint == "2fb4a9000f6a250ea774407f9b988f3227d54666adad8ff2628a5abb33558445" + + scope = RequestScope(tenant_id="tenant-alpha", user_id="user-42") + ctx = RequestContext( + scope=scope, + operation="query.answering", + provider="openai", + provider_scope="global", + model="gpt-4o", + prompt_version="2026-11-01", + instructions=(Instruction(role="system", content="You are a helpful assistant."),), + generation={"temperature": 0.0, "max_tokens": 512}, + corpus_version="v2.1", + document_versions={"doc-1": "hash-abc", "doc-2": "hash-def"}, + protected_entities=("ACME", "SecretProject"), + language="en", + request_shape="single-turn-text", + codec="json-v1", + ) + + query = "What is the capital of France?" + + # 1. Private policy (tenant + user scoped) + p_private = ctx.partition("default", "private", desc.fingerprint, "safe-reuse-v1") + assert p_private == "v2:455397080191f06b6121a1f9878836b8864d3501a2d67b2c18c527b5469d9389" + k_private = ctx.exact_key(query, "default", "private", desc.fingerprint, "safe-reuse-v1") + assert k_private == "71f9e36e2b79ce20b29a28f040be5011ef2548297afb5f339469b051f60a659c" + + # 2. Tenant shared policy (tenant scoped, user stripped) + p_tenant = ctx.partition("default", "tenant_shared", desc.fingerprint, "safe-reuse-v1") + assert p_tenant == "v2:39ab7483632212482e6760948d5c2d46a5191be6d94de3c1d587fb063d6b5e99" + k_tenant = ctx.exact_key(query, "default", "tenant_shared", desc.fingerprint, "safe-reuse-v1") + assert k_tenant == "62ec4f12e0bebd3168e677107cad750f7d9485edd7e735c41642a0e34f7c022d" + + # 3. Public policy (scope stripped) + p_public = ctx.partition("default", "public", desc.fingerprint, "safe-reuse-v1") + assert p_public == "v2:38864ad9877379cd6074692273c684a3609aebdc3111bcc324ee30df2045e4f4" + k_public = ctx.exact_key(query, "default", "public", desc.fingerprint, "safe-reuse-v1") + assert k_public == "71f9ec2b3bf8da40115385a9b5c17e62fbcc5e8e2231f96d379df012ef23de9d" + + +def test_sqlite_format2_prepopulated_upgrade_without_dataloss(tmp_path: Path): + """A SQLite database created with 0.6.0 Format-2 tables opens and hits cleanly in 1.0.0.""" + db_file = tmp_path / "prepopulated_v06.db" + conn = sqlite3.connect(str(db_file)) + + # Construct the exact Format-2 schema + conn.execute( + """ + CREATE TABLE cache_manifest ( + id INTEGER PRIMARY KEY CHECK(id=1), + data TEXT NOT NULL + ) + """ + ) + conn.execute( + """ + CREATE TABLE cache_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + namespace TEXT NOT NULL, + query TEXT NOT NULL, + vector BLOB NOT NULL, + response TEXT NOT NULL, + created_at REAL NOT NULL, + expires_at REAL, + hit_count INTEGER DEFAULT 0, + model TEXT DEFAULT '', + system_prompt TEXT DEFAULT '', + temperature REAL DEFAULT -1.0, + metadata TEXT NOT NULL DEFAULT '{}' + ) + """ + ) + conn.execute( + """ + CREATE TABLE cache_tags ( + entry_id INTEGER NOT NULL REFERENCES cache_entries(id) ON DELETE CASCADE, + tag TEXT NOT NULL + ) + """ + ) + + embedder = HashEmbedder() + manifest_payload = { + "format": 2, + "cache_id": "default", + "generation": "default", + "embedding": asdict(embedder.descriptor), + } + conn.execute( + "INSERT INTO cache_manifest (id, data) VALUES (1, ?)", (canonical(manifest_payload),) + ) + + scope = RequestScope("acme-corp", "alice") + ctx = RequestContext( + scope=scope, + operation="finance.summary", + provider="fixture", + provider_scope="global", + model="test-model", + prompt_version="v1", + codec="json-v1", + ) + + query = "annual revenue report" + vec = embedder.embed(query) + vec_blob = vec.tobytes() + response_obj = {"revenue": 1000000, "status": "audited"} + encoded_response = canonical(response_obj) + partition = ctx.partition( + "default", "private", embedder.descriptor.fingerprint, "safe-reuse-v1" + ) + identity_str = canonical( + ctx.record("default", "private", embedder.descriptor.fingerprint, "safe-reuse-v1") + ) + + entry_meta = { + "format": 2, + "identity": identity_str, + "embedding": embedder.descriptor.fingerprint, + "payload": digest(response_obj), + } + + conn.execute( + """ + INSERT INTO cache_entries (id, namespace, query, vector, response, created_at, expires_at, hit_count, metadata) + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + partition, + query, + vec_blob, + encoded_response, + time.time(), + time.time() + 3600, + 3, + json.dumps(entry_meta), + ), + ) + conn.execute("INSERT INTO cache_tags (entry_id, tag) VALUES (1, 'finance')") + conn.execute("INSERT INTO cache_tags (entry_id, tag) VALUES (1, 'annual')") + conn.commit() + conn.close() + + # Now open with LongCache 1.0.0 + config = CacheConfig(backend="sqlite", db_path=str(db_file)) + with LongCache(config, embedder=embedder) as cache: + # Lookup must HIT with exact response + lookup = cache.lookup(query, context=ctx) + assert lookup.outcome == "HIT" + assert lookup.entry_id == 1 + assert lookup.similarity is not None and lookup.similarity >= 0.99 + assert lookup.response == {"revenue": 1000000, "status": "audited"} + + # Stats must report 1 entry + stats = cache.stats(context=ctx) + assert stats["total_entries"] == 1 + + # Tag-based invalidation must evict the pre-populated entry + invalidated = cache.invalidate_by_tag("finance", context=ctx) + assert invalidated == 1 + + # Subsequent lookup must be a MISS + lookup_after = cache.lookup(query, context=ctx) + assert lookup_after.outcome == "MISS" + + +@pytest.mark.skipif(not REDIS_URL, reason="Live Redis service required") +def test_redis_format2_prepopulated_upgrade_without_dataloss(): + """A Redis store populated with 0.6.0 Format-2 keys opens and serves hits in 1.0.0.""" + client = redis.Redis.from_url(REDIS_URL or "redis://localhost:16379/0") + cache_id = "v06-upg-" + uuid4().hex[:8] + generation = "gen1" + root_key = f"longcache:v2:{cache_id}:{generation}" + + embedder = HashEmbedder() + manifest_data = { + "format": 2, + "cache_id": cache_id, + "generation": generation, + "embedding": asdict(embedder.descriptor), + } + client.set(f"{root_key}:manifest", canonical(manifest_data)) + + scope = RequestScope("redis-tenant", "bob") + ctx = RequestContext( + scope=scope, + operation="search.query", + provider="fixture", + provider_scope="global", + model="demo-model", + prompt_version="v1", + codec="json-v1", + ) + + query = "cached query in redis" + vec = embedder.embed(query) + entry_id = 101 + response_obj = {"redis_key": "stored_in_v06"} + encoded_response = canonical(response_obj) + partition = ctx.partition( + "default", "private", embedder.descriptor.fingerprint, "safe-reuse-v1" + ) + identity_str = canonical( + ctx.record("default", "private", embedder.descriptor.fingerprint, "safe-reuse-v1") + ) + + entry_meta = { + "format": 2, + "identity": identity_str, + "embedding": embedder.descriptor.fingerprint, + "payload": digest(response_obj), + } + + # Populate exact Redis hash for entry + import base64 + + entry_key = f"{root_key}:entry:{entry_id}" + client.hset( + entry_key, + mapping={ + "id": entry_id, + "namespace": partition, + "query": query, + "response": encoded_response, + "created_at": str(time.time()), + "hit_count": "5", + "vector": base64.b64encode(vec.tobytes()).decode("ascii"), + "metadata": json.dumps(entry_meta), + }, + ) + client.sadd(f"{root_key}:entries", entry_id) + client.sadd(f"{root_key}:ns:{partition}", entry_id) + client.sadd(f"{root_key}:tag:v06-tag", entry_id) + client.sadd(f"{root_key}:namespaces", partition) + + config = CacheConfig( + backend="redis", + redis_url=REDIS_URL or "redis://localhost:16379/0", + cache_id=cache_id, + generation=generation, + ) + + with LongCache(config, embedder=embedder) as cache: + lookup = cache.lookup(query, context=ctx) + assert lookup.outcome == "HIT" + assert lookup.entry_id == entry_id + assert lookup.response == {"redis_key": "stored_in_v06"} + + # Clean up + cache.clear(operator=True) diff --git a/uv.lock b/uv.lock index 1249d47..f3ce095 100644 --- a/uv.lock +++ b/uv.lock @@ -1641,7 +1641,7 @@ wheels = [ [[package]] name = "longcache" -version = "0.6.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "click" },