Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,40 @@ are in [Docs PR #52](https://github.com/basicmachines-co/docs.basicmemory.com/pu
`docs/archive/` with historical banners. Kept release notes, changelogs, and
benchmark results as records of the versions they actually describe.

## Runtime dependency updates

**Status:** FastAPI upgrade implemented on the development branch; Python 3.15
support deferred pending upstream packages.

- Upgrade FastAPI from 0.139.0 to 0.162.0 and resolve its OpenTelemetry dependencies
with a compatible Logfire version. FastMCP remains at 4.1.3, the latest stable
release checked on 2026-10-24.
- Replace the removed FastAPI `get_flat_dependant` helper with direct traversal of
route dependencies. Cache keys still include nested query parameters and use
validated request bodies; regression tests cover aliases and default values.

### Python 3.15: blocked by ONNX Runtime

Python 3.15 is released, but Basic Memory's default installation depends on
`fastembed`, which depends on `onnxruntime` for local model inference. On 2026-10-24,
installing the locked dependencies with Python 3.15.1 failed because ONNX Runtime
1.27.0 has no compatible wheel or source distribution. The latest ONNX Runtime
1.31.0 also has no Python 3.15 wheels; updating FastEmbed from 0.8.0 to 0.9.0 does
not remove that dependency. The separate `onnx` model-format package is not a
replacement for ONNX Runtime.

Track [upstream Python 3.15 wheels](https://github.com/microsoft/onnxruntime/issues/33271).
Keep Python 3.12–3.14 in the supported test matrix and use Python 3.14 for local
development. Local embeddings remain part of the default installation; do not
silently disable them to claim Python 3.15 support.

Before enabling 3.15:

- [ ] Verify published ONNX Runtime wheels for the supported operating systems.
- [ ] Refresh the lockfile and verify a complete runtime/dev installation on 3.15.
- [ ] Run real local-embedding, SQLite, PostgreSQL, MCP, and package checks.
- [ ] Add Python 3.15 to CI and update the documented support range.

## Remaining 1.0 release work

- [ ] Land the core and docs-site migration PRs and publish the updated packages/docs.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies = [
"unidecode>=1.3.8",
"dateparser>=1.2.0",
"watchfiles>=1.0.4",
"fastapi[standard]==0.139.0",
"fastapi[standard]==0.162.0",
"alembic>=1.14.1",
"pillow>=11.1.0",
"pybars3>=0.9.7",
Expand Down
28 changes: 23 additions & 5 deletions src/basic_memory/read_cache/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
"""

import json
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, override

import logfire
from fastapi import Depends, Request
from fastapi.dependencies.utils import get_flat_dependant
from fastapi.dependencies.models import Dependant
from fastapi.routing import APIRoute
from pydantic import TypeAdapter
from redis_fastapi.cache import CacheHitException, cache
Expand Down Expand Up @@ -300,7 +300,11 @@ async def _request_identity(request: Request) -> str:

def _validated_body(request: Request, raw_body: object) -> object:
"""The QUERY body as the route will see it, raising ValueError when invalid."""
body_params = get_flat_dependant(_api_route(request).dependant).body_params
body_params = [
param
for dependant in _route_dependencies(_api_route(request).dependant)
for param in dependant.body_params
]
if len(body_params) != 1:
raise TypeError("cached QUERY routes take exactly one body model")
adapter = _body_adapter(body_params[0].field_info.annotation)
Expand All @@ -324,8 +328,22 @@ def _api_route(request: Request) -> APIRoute:

def _declared_query_params(request: Request) -> frozenset[str]:
"""Query parameter names the matched route and its dependencies read."""
dependant = get_flat_dependant(_api_route(request).dependant)
return frozenset(param.alias for param in dependant.query_params)
return frozenset(
param.alias
for dependant in _route_dependencies(_api_route(request).dependant)
for param in dependant.query_params
)


def _route_dependencies(dependant: Dependant) -> Iterator[Dependant]:
"""Include nested dependencies so their inputs cannot collide in cache keys.
FastAPI no longer exposes get_flat_dependant; walk the route's dependency
tree directly instead of constructing a synthetic flattened dependant.
"""
yield dependant
for dependency in dependant.dependencies:
yield from _route_dependencies(dependency)


# --- Groups by route ---
Expand Down
64 changes: 64 additions & 0 deletions tests/test_read_cache_request_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Cache identities include inputs declared by nested FastAPI dependencies."""

from typing import Annotated

import pytest
from fastapi import Depends, FastAPI, Query, Request
from httpx import ASGITransport, AsyncClient
from pydantic import BaseModel

from basic_memory.read_cache.routes import _request_identity


class SearchBody(BaseModel):
text: str
limit: int = 10


@pytest.mark.asyncio
async def test_nested_query_parameters_and_aliases_partition_cache_entries() -> None:
app = FastAPI()

def pagination(page: Annotated[int, Query(alias="page-number")] = 1) -> int:
return page

def search_scope(page: Annotated[int, Depends(pagination)]) -> int:
return page

@app.get("/identity")
async def identity(request: Request, page: Annotated[int, Depends(search_scope)]) -> str:
return await _request_identity(request)

async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
first = await client.get("/identity?page-number=1&nonce=a")
ignored = await client.get("/identity?page-number=1&nonce=b")
second = await client.get("/identity?page-number=2&nonce=a")

assert first.status_code == ignored.status_code == second.status_code == 200
assert first.json() == ignored.json()
assert first.json() != second.json()


@pytest.mark.asyncio
async def test_nested_body_model_normalizes_cache_identity() -> None:
app = FastAPI()

def search_body(body: SearchBody) -> SearchBody:
return body

@app.api_route("/identity", methods=["QUERY"])
async def identity(request: Request, body: Annotated[SearchBody, Depends(search_body)]) -> str:
return await _request_identity(request)

async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
first = await client.request("QUERY", "/identity", json={"text": "needle"})
explicit = await client.request(
"QUERY", "/identity", json={"text": "needle", "limit": 10, "nonce": "ignored"}
)
changed = await client.request("QUERY", "/identity", json={"text": "other"})
invalid = await client.request("QUERY", "/identity", json={"limit": 10})

assert first.status_code == explicit.status_code == changed.status_code == 200
assert first.json() == explicit.json()
assert first.json() != changed.json()
assert invalid.status_code == 422
Loading
Loading