feat(aio): dedicated AI capture lane and multimodal passthrough#760
Conversation
Prompt To Fix All With AIFix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
posthog/client.py:799-807
**Compatibility Attributes Become Read-Only**
`queue` and `consumers` were writable instance attributes before this change, but they are now getter-only properties. Existing integrations or tests that replace either attribute with a custom queue or consumer list now fail with `AttributeError` during assignment instead of configuring the client.
### Issue 2 of 4
posthog/request.py:365
**Public Event Classifier Is Removed**
This change removes the non-private `is_ai_event` helper from `posthog.request` and its existing re-exports. Downstream callers importing that snapshotted API now fail at module import time, so this minor release needs to retain a compatibility helper or treat the removal as a breaking change.
### Issue 3 of 4
posthog/consumer.py:52-56
**Consumer Keyword Breaks Existing Callers**
`Consumer` previously accepted `dedicated_ai_endpoint`, but the changed signature rejects that keyword before any routing code runs. Direct callers using the snapshotted constructor now receive `TypeError`; accepting the old keyword as a compatibility alias would allow them to migrate to `endpoint` without an immediate runtime break.
### Issue 4 of 4
posthog/client.py:377-385
**Joined Lane Still Reports Started**
After an AI lane has emitted once, `join()` stops all of its consumers but leaves `_started` true. If the client is reused after `join()` or `shutdown()`, the next `_capture_ai()` queues an event without restarting a consumer, so the event is never delivered and a later unbounded flush can wait forever.
Reviews (1): Last reviewed commit: "feat: multimodal passthrough" | Re-trigger Greptile |
| @property | ||
| def queue(self) -> Queue: | ||
| """The analytics lane's queue (kept for backwards compatibility).""" | ||
| return self._analytics_lane.queue | ||
|
|
||
| @property | ||
| def consumers(self) -> Optional[List[Consumer]]: | ||
| """Flat list of the lanes' consumers, analytics first (kept for backwards compatibility).""" | ||
| if self.sync_mode: |
There was a problem hiding this comment.
Compatibility Attributes Become Read-Only
queue and consumers were writable instance attributes before this change, but they are now getter-only properties. Existing integrations or tests that replace either attribute with a custom queue or consumer list now fail with AttributeError during assignment instead of configuring the client.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/client.py
Line: 799-807
Comment:
**Compatibility Attributes Become Read-Only**
`queue` and `consumers` were writable instance attributes before this change, but they are now getter-only properties. Existing integrations or tests that replace either attribute with a custom queue or consumer list now fail with `AttributeError` during assignment instead of configuring the client.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Reads behave exactly as before; assignment was never a supported configuration path and now fails loudly instead of silently desyncing the client from its consumers.
| def join(self) -> None: | ||
| """Pause this lane's consumers and wait for them to exit; a never-started lane is a no-op.""" | ||
| for consumer in self.consumers: | ||
| consumer.pause() | ||
| try: | ||
| consumer.join() | ||
| except RuntimeError: | ||
| # consumer thread has not started | ||
| pass |
There was a problem hiding this comment.
Joined Lane Still Reports Started
After an AI lane has emitted once, join() stops all of its consumers but leaves _started true. If the client is reused after join() or shutdown(), the next _capture_ai() queues an event without restarting a consumer, so the event is never delivered and a later unbounded flush can wait forever.
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/client.py
Line: 377-385
Comment:
**Joined Lane Still Reports Started**
After an AI lane has emitted once, `join()` stops all of its consumers but leaves `_started` true. If the client is reused after `join()` or `shutdown()`, the next `_capture_ai()` queues an event without restarting a consumer, so the event is never delivered and a later unbounded flush can wait forever.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
This matches the pre-existing contract on main: capture() after join() strands events on the analytics queue the same way, since consumers are paused. Capture-after-shutdown isn't supported; resetting _started in join() would make a post-shutdown capture silently respawn threads, which is worse.
There was a problem hiding this comment.
That's a fair point — parity with the analytics lane's existing contract and the hazard of silent post-shutdown respawning both support leaving _started set. The concern is withdrawn.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
posthog-python Compliance ReportDate: 2026-07-22 17:32:02 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
dustinbyrne
left a comment
There was a problem hiding this comment.
overall I think this direction looks great
💡 Motivation and Context
AI Observability needs multimodal (base64 media) capture, which doesn't fit the analytics
/batch/pipeline or its size limits. This PR adds a dedicated AI capture lane and a private multimodal switch, all underscore-private until beta:Client._capture_ai()— a dedicated private method that is the only client-level entry to the AI lane (posts to/i/v0/ai/batch/, 8MiB per-event ceiling vs 900KiB for analytics, lazy-started consumers, fork-safe, lane-aware flush/join/shutdown, pinned to the V0 wire protocol).capture()is never rerouted and no name-based$ai_*routing remains anywhere.Posthog(..., _use_ai_lane=True)routes all AI SDK wrapper events through the AI lane. Wrapper emissions funnel through_capture_ai_event, which uses_capture_aiwhen the client opted in and falls back tocapture()for duck-typed clients (so downstream test suites passingMockclients keep working).posthog._use_ai_lane,posthog._enable_multimodal_capture), synced onto the default client bysetup()on every call — deployments that configure PostHog via module attributes (like our monorepo's lazily auto-instantiatedposthoganalyticsclient) set one attribute instead of mutating a constructed client.Posthog(..., _enable_multimodal_capture=True)switch additionally skips base64 media redaction and implies_use_ai_lane, so unredacted multi-MB events can never land on the analytics lane's size cap. It replaces the_INTERNAL_LLMA_MULTIMODALenv var, which is removed.Mockclients (is Trueguards);Consumeris generalized toendpoint=/max_msg_size=and no longer knows AI events exist; deadis_ai_eventremoved; oversized-event drops log name and byte size only, never event contents.💚 How did you test it?
posthog/test/test_ai_capture_lane.py: lane routing, sync mode, size caps, V0 pinning, lazy start, concurrent first-capture race, fork rebuild, disabled-client behavior, module-attribute config, Mock hardening._capture_aiis used (andcaptureis not) when the client opts in; wrapper-level multimodal passthrough tests for OpenAI and Anthropic.posthog/test/aisuites;ruff format --checkand mypy clean; public API snapshot check passes.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted) — assign @carlos-marchal-ph as DRI.
Built in a Claude Code session directed by Carlos, who made all design decisions (client-level switch over per-wrapper flags, module-attribute config for the monorepo's auto-instantiated default client, removing the
_INTERNAL_LLMA_MULTIMODALenv var, keeping the 8MiB client-side cap). The final branch was verified by two independent whitebox review subagents running the pr-review skill; both confirmed the intended architecture with no blocking findings, and their remaining suggestions (log hygiene on oversized drops, dead code, changeset notes, extra tests) are incorporated.