chore(deps): bump 47 backend dependencies

Bumps the Python backend dependency set, holding back the ones that are
resolver-blocked or that regress behaviour this repo depends on.

Notable upgrades:
  cryptography 46.0.7 -> 50.0.0 (requires msal 1.37.0, which relaxes its cap)
  protobuf 6.33.6 -> 7.35.1 (floats opentelemetry-* to 1.44.0)
  openai 2.32.0 -> 2.53.0, anthropic 0.88.0 -> 0.121.0
  google-genai 1.73.1 -> 2.17.0 (the 2.0 break is scoped to the Interactions
    API, which this repo does not use)
  fastmcp 3.2.4 -> 3.4.6, gunicorn 25.3.0 -> 26.0.0
  starlette 1.0.0 -> 1.6.0, uvicorn 0.42.0 -> 0.52.1
  sentence-transformers 5.3.0 -> 5.7.0, numpy 2.4.4 -> 2.5.1
  faiss-cpu 1.13.2 -> 1.15.0, pillow -> 12.3.0 (pinned; security release)
  pypdf 6.9.2 -> 6.15.0, lxml 6.0.2 -> 6.1.1 (CVE-2026-41066)

Code changes needed by the bumps:
  - openai >= 2.53 rejects a falsy api_key at construction. Keyless
    OpenAI-compatible backends (Ollama, llama.cpp, vLLM) legitimately have
    none, and pydantic-settings yields "" for a bare `API_KEY=` in .env, so
    both call sites now fall back to a placeholder.
  - Flask >= 3.1.2 tears a stream_with_context request down twice, and a
    ContextVar token may only be reset once. The log-context teardown hook
    is now idempotent.
  - sentence-transformers renamed get_sentence_embedding_dimension to
    get_embedding_dimension in 5.4; use the new name with a fallback.

Held back deliberately:
  torch 2.11.0      - 2.13.0 drags torchvision 0.26 -> 0.28 and the whole
                      docling stack; torch is only probed for cuda.empty_cache()
  tokenizers 0.22.2 - transformers pins <=0.23.0 and no stable 0.23.0 exists
  transformers      - capped <5.9.0: 5.9+ breaks docling's PDF layout model on
                      Apple Silicon (MPS float64), matching docling-core's own
                      darwin pin
  docling 2.84.0    - 2.118.1 needs rapidocr >=3.9.1 and conflicts with
                      transformers on macOS; wants its own PR with a
                      golden-corpus diff
  redis 7.4.0       - 8.x defaults socket_timeout to 5s, silently capping the
                      blocking reads in the device broker and SSE tail
  websockets 16.0   - google-genai caps <17.0
  marshmallow       - dataclasses-json hard-caps <4; spec tightened to match
  langchain block   - langchain-community 0.4.2 deletes the Qdrant vectorstore
                      this repo imports; langchain 1.3.x needs websockets <16
This commit is contained in:
Alex committed 2026-08-09 12:30:44 +01:00
1 parent 732f7d1a89
commit 2565906f88
9 files changed
+148 -50

No files matched your search

+4
View File
@@ -183,6 +183,10 @@ def _reset_log_context(_exc):
# leak into the stream's view of the context.
token = getattr(request, _LOG_CTX_TOKEN_ATTR, None)
if token is not None:
# Flask >= 3.1.2 tears a stream_with_context request down twice: once
# when the view returns, once when the generator is finalized. Clear
# the token first — resetting one twice raises RuntimeError.
setattr(request, _LOG_CTX_TOKEN_ATTR, None)
log_context.reset(token)
+9 -1
View File
@@ -11,6 +11,9 @@ from application.core.settings import settings
from application.llm.base import BaseLLM
from application.storage.storage_creator import StorageCreator
# Placeholder sent to OpenAI-compatible backends that require no credentials.
NO_API_KEY = "sk-no-key"
def _truncate_base64_for_logging(messages):
"""
@@ -124,7 +127,12 @@ class OpenAILLM(BaseLLM):
):
super().__init__(*args, **kwargs)
self.api_key = api_key or settings.OPENAI_API_KEY or settings.API_KEY
# openai>=2.53 rejects a falsy api_key at construction. Keyless
# OpenAI-compatible backends (Ollama, llama.cpp, vLLM) legitimately have
# none, and pydantic-settings yields "" for a bare `API_KEY=` in .env.
self.api_key = (
api_key or settings.OPENAI_API_KEY or settings.API_KEY or NO_API_KEY
)
self.user_api_key = user_api_key
# Priority: 1) Parameter base_url, 2) Settings OPENAI_BASE_URL, 3) Default
+49 -45
View File
@@ -1,16 +1,16 @@
a2wsgi==1.10.10
alembic>=1.13,<2
anthropic==0.88.0
anthropic==0.121.0
asgiref>=3.11.1
boto3==1.42.83
beautifulsoup4==4.14.3
boto3==1.43.67
beautifulsoup4==4.15.0
cel-python==0.5.0
celery==5.6.3
celery-redbeat==2.3.3
croniter==6.2.2
cryptography==46.0.7
celery-redbeat==2.4.2
croniter==6.2.4
cryptography==50.0.0
dataclasses-json==0.6.7
daytona==0.190.1
daytona==0.203.0
defusedxml==0.7.1
docling==2.84.0
rapidocr>=1.4.0
@@ -18,19 +18,19 @@ onnxruntime>=1.19.0
docx2txt==0.9
ddgs>=8.0.0
fast-ebook
elevenlabs==2.43.0
Flask==3.1.1
faiss-cpu==1.13.2
fastmcp==3.2.4
elevenlabs==2.62.0
Flask==3.1.3
faiss-cpu==1.15.0
fastmcp==3.4.6
flask-restx==1.3.2
google-genai==1.73.1
google-api-python-client==2.193.0
google-auth-httplib2==0.3.1
google-auth-oauthlib==1.3.1
google-genai==2.17.0
google-api-python-client==2.198.0
google-auth-httplib2==0.4.1
google-auth-oauthlib==1.4.0
gTTS==2.5.4
gunicorn==25.3.0
gunicorn==26.0.0
jinja2==3.1.6
jiter==0.13.0
jiter==0.16.0
jmespath==1.1.0
joblib==1.5.3
jsonpatch==1.33
@@ -38,21 +38,22 @@ jsonpointer==3.1.1
kombu==5.6.2
langchain==1.2.3
langchain-community==0.4.1
langchain-core==1.2.29
langchain-core==1.2.31
langchain-openai==1.1.12
langchain-text-splitters==1.1.1
langchain-text-splitters==1.1.2
langsmith==0.7.31
lazy-object-proxy==1.12.0
lxml==6.0.2
lxml==6.1.1
markupsafe==3.0.3
marshmallow>=3.18.0,<5.0.0
# dataclasses-json (via langchain-community) hard-caps marshmallow <4.
marshmallow>=3.18.0,<4.0.0
mpmath==1.3.0
multidict==6.7.1
msal==1.35.1
msal==1.37.0
mypy-extensions==1.1.0
networkx==3.6.1
numpy==2.4.4
openai==2.32.0
numpy==2.5.1
openai==2.53.0
openapi3-parser==1.1.22
opentelemetry-distro>=0.50b0,<1
opentelemetry-exporter-otlp>=1.29.0,<2
@@ -64,23 +65,23 @@ opentelemetry-instrumentation-redis>=0.50b0,<1
opentelemetry-instrumentation-requests>=0.50b0,<1
opentelemetry-instrumentation-sqlalchemy>=0.50b0,<1
opentelemetry-instrumentation-starlette>=0.50b0,<1
orjson==3.11.7
packaging==26.0
pandas==3.0.2
orjson==3.11.9
packaging==26.3
pandas==3.0.5
openpyxl==3.1.5
pathable==0.5.0
pathable==0.6.0
pdf2image>=1.17.0
pgvector>=0.2,<1
pillow
pgvector>=0.5,<1
pillow==12.3.0
portalocker>=2.7.0,<4.0.0
prompt-toolkit==3.0.52
protobuf==6.33.6
prompt-toolkit==3.0.53
protobuf==7.35.1
psycopg[binary,pool]>=3.1,<4
py==1.11.0
pydantic
pydantic-core
pydantic-settings
pypdf==6.9.2
pypdf==6.15.0
python-dateutil==2.9.0.post0
python-dotenv
python-jose==3.5.0
@@ -88,28 +89,31 @@ python-pptx==1.0.2
PyYAML
redis==7.4.0
referencing>=0.28.0,<0.38.0
regex==2026.4.4
requests==2.33.1
regex==2026.7.19
requests==2.34.2
retry==0.9.2
sentence-transformers==5.3.0
sentence-transformers==5.7.0
sqlalchemy>=2.0,<3
starlette>=1.0,<2
tiktoken==0.12.0
tiktoken==0.13.0
tokenizers==0.22.2
torch==2.11.0
tqdm==4.67.3
transformers==5.4.0
typing-extensions==4.15.0
# Capped <5.9.0 to match docling-core's own darwin pin: 5.9+ breaks docling's
# PDF layout model on Apple Silicon ("Cannot convert a MPS Tensor to float64").
transformers==5.8.1
typing-extensions==4.16.0
typing-inspect==0.9.0
tzdata==2026.1
urllib3==2.6.3
tzdata==2026.3
urllib3==2.7.0
uvicorn[standard]>=0.30,<1
uvicorn-worker>=0.4,<1
vine==5.1.0
wcwidth==0.6.0
wcwidth==0.8.2
werkzeug>=3.1.0
yarl==1.23.0
markdownify==1.2.2
tldextract==5.3.1
yarl==1.24.5
markdownify==1.2.3
tldextract==5.3.2
# google-genai caps websockets <17.0; 17.x will not resolve.
websockets==16.0
websocket-client==1.9.0
+1 -1
View File
@@ -326,7 +326,7 @@ class DaytonaSandbox(CodeSandbox):
The tools pass a token DIRECTORY (e.g. ``artifacts/{token}``), so the delete is
recursive — ``fs.delete_file`` with ``recursive=True`` removes a folder and its
contents (daytona==0.190.1).
contents (daytona==0.203.0).
"""
try:
handle = self._get_handle(session_id)
+7 -1
View File
@@ -6,6 +6,9 @@ from openai import OpenAI
from application.core.settings import settings
from application.stt.base import BaseSTT
# Placeholder sent to OpenAI-compatible backends that require no credentials.
NO_API_KEY = "sk-no-key"
class OpenAISTT(BaseSTT):
def __init__(
@@ -14,7 +17,10 @@ class OpenAISTT(BaseSTT):
base_url: Optional[str] = None,
model: Optional[str] = None,
):
self.api_key = api_key or settings.OPENAI_API_KEY or settings.API_KEY
# See application/llm/openai.py: openai>=2.53 rejects a falsy api_key.
self.api_key = (
api_key or settings.OPENAI_API_KEY or settings.API_KEY or NO_API_KEY
)
self.base_url = base_url or settings.OPENAI_BASE_URL or "https://api.openai.com/v1"
self.model = model or settings.OPENAI_STT_MODEL
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
+7 -1
View File
@@ -24,7 +24,13 @@ class EmbeddingsWrapper:
raise ValueError(
f"SentenceTransformer model failed to load properly for: {model_name}"
)
self.dimension = self.model.get_sentence_embedding_dimension()
# Renamed in sentence-transformers 5.4; keep the old name as a fallback.
get_dimension = getattr(
self.model,
"get_embedding_dimension",
getattr(self.model, "get_sentence_embedding_dimension", None),
)
self.dimension = get_dimension()
logging.info(f"Successfully loaded model with dimension: {self.dimension}")
except Exception as e:
logging.error(
+31
View File
@@ -2049,3 +2049,34 @@ class TestInlineFilePartResolution:
# Upload happened because the read errored → treated as miss.
assert len(llm.client.files.calls) == 1
assert out[0]["content"][1] == {"type": "file", "file": {"file_id": "file-1"}}
@pytest.mark.unit
class TestKeylessConstruction:
"""openai>=2.53 raises on a falsy api_key at construction.
Keyless OpenAI-compatible backends (Ollama, llama.cpp, vLLM) have no
credential, and pydantic-settings yields "" — not None — for a bare
`API_KEY=` line in .env, so the empty string must not reach the SDK.
"""
@pytest.mark.parametrize("blank", ["", None])
def test_llm_accepts_blank_key(self, blank):
with patch("application.llm.openai.settings") as mock_settings:
mock_settings.OPENAI_API_KEY = blank
mock_settings.API_KEY = blank
mock_settings.OPENAI_BASE_URL = "http://localhost:11434/v1"
llm = OpenAILLM(api_key=blank)
assert llm.api_key == "sk-no-key"
@pytest.mark.parametrize("blank", ["", None])
def test_stt_accepts_blank_key(self, blank):
from application.stt.openai_stt import OpenAISTT
with patch("application.stt.openai_stt.settings") as mock_settings:
mock_settings.OPENAI_API_KEY = blank
mock_settings.API_KEY = blank
mock_settings.OPENAI_BASE_URL = "http://localhost:11434/v1"
mock_settings.OPENAI_STT_MODEL = "whisper-1"
stt = OpenAISTT(api_key=blank)
assert stt.api_key == "sk-no-key"
+23
View File
@@ -19,3 +19,26 @@ def test_app_config():
assert app.config["UPLOAD_FOLDER"] == "inputs"
assert app.config["CELERY_BROKER_URL"] == settings.CELERY_BROKER_URL
assert app.config["CELERY_RESULT_BACKEND"] == settings.CELERY_RESULT_BACKEND
@pytest.mark.unit
class TestLogContextTeardown:
"""Flask >= 3.1.2 tears a stream_with_context request down twice: once when
the view returns, once when the generator is finalized. A ContextVar token
may only be reset once, so the teardown hook has to be idempotent."""
def test_reset_is_idempotent(self):
from flask import request
from application.app import _LOG_CTX_TOKEN_ATTR, _reset_log_context, app
from application.core import log_context
with app.test_request_context("/"):
token = log_context.bind(activity_id="abc", endpoint="stream")
setattr(request, _LOG_CTX_TOKEN_ATTR, token)
_reset_log_context(None)
_reset_log_context(None) # second teardown must not raise
assert getattr(request, _LOG_CTX_TOKEN_ATTR) is None
assert "activity_id" not in log_context.snapshot()
+17 -1
View File
@@ -9,7 +9,7 @@ class TestEmbeddingsWrapper:
def test_init_success(self, mock_st_cls):
mock_model = MagicMock()
mock_model._first_module.return_value = MagicMock()
mock_model.get_sentence_embedding_dimension.return_value = 768
mock_model.get_embedding_dimension.return_value = 768
mock_st_cls.return_value = mock_model
from application.vectorstore.embeddings_local import EmbeddingsWrapper
@@ -19,6 +19,22 @@ class TestEmbeddingsWrapper:
mock_st_cls.assert_called_once()
assert wrapper.dimension == 768
@patch("application.vectorstore.embeddings_local.SentenceTransformer")
def test_init_falls_back_to_legacy_dimension_api(self, mock_st_cls):
"""sentence-transformers < 5.4 only exposes get_sentence_embedding_dimension."""
mock_model = MagicMock()
mock_model._first_module.return_value = MagicMock()
del mock_model.get_embedding_dimension
mock_model.get_sentence_embedding_dimension.return_value = 768
mock_st_cls.return_value = mock_model
from application.vectorstore.embeddings_local import EmbeddingsWrapper
wrapper = EmbeddingsWrapper("test-model")
assert wrapper.dimension == 768
mock_model.get_sentence_embedding_dimension.assert_called_once()
@patch("application.vectorstore.embeddings_local.SentenceTransformer")
def test_init_failure(self, mock_st_cls):
mock_st_cls.side_effect = Exception("model not found")