Lift aiohttp<3.14 cap with a vcrpy aiohttp-stub compat shim (closes #1920)#1948
Conversation
aiohttp is an unpinned transitive dep (via llama-index-core); #1914 capped it <3.14 because aiohttp 3.14 removed aiohttp.streams.AsyncStreamReaderMixin, which vcrpy 8.1.1's aiohttp stub (vcr/stubs/aiohttp_stubs.py) subclasses when vcrpy lazily loads it on cassette entry — making every VCR-using test raise AttributeError. vcrpy 8.1.1 is the latest release and has no fix released yet (kevin1024/vcrpy#995; unreleased PR #996), so the issue's "bump vcrpy" step is not yet possible. Since this codebase only records/replays httpx (LLM) cassettes, the aiohttp MockStream is never instantiated. Add a small, idempotent import-time shim, ensure_aiohttp_vcr_compat(), that restores the removed symbol as an empty mixin so cassette entry works under aiohttp >=3.14; it is a no-op under aiohttp <3.14 (the real mixin is left untouched). The shim is applied from the root conftest.py (before any test runs) and from maybe_vcr_cassette() (for the non-pytest E2E harness). Remove the cap so aiohttp floats back to 3.14+. Verified against aiohttp 3.14.0 + vcrpy 8.1.1 (bug reproduced without the shim; cassette entry succeeds with it) and aiohttp 3.13.5 (no-op path). Drop the shim and bump vcrpy once a release ships the vcrpy#996 fix. Closes #1920
Code ReviewOverviewThis PR lifts the temporary What's Good
IssuesCLAUDE.md Violation — Claude Code attribution in PR bodyThe PR description ends with: CLAUDE.md is explicit:
Please edit the PR description to remove this line before merging. Minor Observations (non-blocking)
SummaryTechnically sound and well-executed. The one required change is removing the Claude Code attribution from the PR body per project convention. Code itself is ready to merge once that is addressed. |
vcr_replay.py and test_vcr_replay.py are not in the mypy baseline, so they must type-check cleanly. Under aiohttp >= 3.14 (which CI now resolves after lifting the cap) mypy reads aiohttp's real types, where streams.AsyncStreamReaderMixin no longer exists — direct attribute assignment/access would raise attr-defined. Use setattr()/getattr() with the string name so mypy doesn't check the removed member. Runtime behavior is unchanged; verified with mypy (aiohttp 3.14 real types) and the regression tests under aiohttp 3.14 and 3.13.
Code ReviewOverviewThis PR lifts the temporary Code Quality
Test CoverageThe
One minor observation: Potential Issues
SecurityNo concerns. The shim only adds a missing name to an existing module object at import time and cannot affect runtime behavior for the httpx cassettes this project actually uses. SummaryVerdict: Approve with minor notes. The approach is correct and the tradeoff is well-reasoned — a small, explicitly temporary shim is better than keeping a transitive dependency pinned to an older minor version. The breadcrumbs in Blocking: Remove the Claude Code attribution from the PR description (CLAUDE.md requirement). |
Code ReviewOverviewThis PR lifts the temporary What Works Well
Minor Observations
Security / PerformanceNo security concerns. The shim only adds a name to a module's namespace; it does not alter existing symbols or expose new attack surface. The SummaryThis is a clean, well-reasoned workaround for a genuine upstream blocker. The implementation is correct, all call sites are covered, and the breadcrumbs for eventual cleanup are in place. The observations above are non-blocking. Good to merge. |
Closes #1920.
Context
requirements/base.txtcarried a temporaryaiohttp>=3.13,<3.14cap (added in #1914).aiohttpis an unpinned transitive dep (viallama-index-core), and aiohttp 3.14 removedaiohttp.streams.AsyncStreamReaderMixin, which vcrpy 8.1.1's aiohttp stub (vcr/stubs/aiohttp_stubs.py:18) subclasses:The blocker on the issue's literal checklist
The issue's action items are gated on "when vcrpy is bumped to a release that supports the aiohttp 3.14 stream API." No such release exists yet — vcrpy 8.1.1 (Jan 2026) is still the latest, the incompatibility is tracked in kevin1024/vcrpy#995, and the proposed fix (PR #996) is unreleased. So "bump vcrpy" is not currently possible, and pinning to an unmerged upstream PR would violate the repo's clean-pinning conventions.
Approach — surgical, removable shim
This codebase only records/replays httpx (LLM provider) cassettes; the aiohttp stub is pulled in incidentally because aiohttp is importable. vcrpy imports that stub lazily when a cassette is entered (
vcr/patch.pybuilds its patchers), so under aiohttp ≥3.14 every VCR-using test errors withAttributeError. BecauseMockStreamis never instantiated here, restoring the removed symbol as an empty mixin is sufficient.requirements/base.txt— removed theaiohttp<3.14cap (back to unpinned-transitive; floats to 3.14+).opencontractserver/utils/vcr_replay.py— newensure_aiohttp_vcr_compat(): idempotent, restoresaiohttp.streams.AsyncStreamReaderMixinas an empty mixin when missing; no-op under aiohttp <3.14 (real mixin left untouched). Also called insidemaybe_vcr_cassette()for the non-pytest E2E harness.conftest.py— applies the shim at conftest import, before any test runs.requirements/local.txt— vcrpy stays pinned at8.1.1with a breadcrumb pointing at the shim and Add Redis integration tests and CI workflow #996.opencontractserver/tests/test_vcr_replay.py::EnsureAiohttpVcrCompatTests.Every vcr entry point is covered: the 7 integration test files (via conftest) and both runtime call sites (
data_extract_tasks.py,unified_agent_conversation.py) route throughmaybe_vcr_cassette().Verification
Reproduced and verified in throwaway venvs against the exact dependency combos:
use_cassette()raisesAttributeError: module 'aiohttp.streams' has no attribute 'AsyncStreamReaderMixin'(bug reproduced)maybe_vcr_cassette()works end-to-end; regression tests passblack,flake8, and the changelog--checkall pass.Follow-up
Delete the shim and bump
vcrpyonce a release ships the vcrpy#996 fix. The breadcrumbs inrequirements/local.txtand the shim docstring point there.