fix: harden Langfuse parent/child span lifecycle across all routing paths#311
fix: harden Langfuse parent/child span lifecycle across all routing paths#311sheepdestroyer wants to merge 6 commits into
Conversation
…aths
## Problem
Langfuse observations and propagation contexts could leak in error paths,
causing trace corruption when fallback routing retries after failures.
## Changes
### Span lifecycle hardening
- Add _end_parent_obs, _end_child_span, _close_prop_ctx helpers for safe
Langfuse SDK v4 finalization (update()+end() with exception swallowing)
- Add logger.debug(exc_info=True) to all helpers for opt-in debugging
- Finalize parent observation and close propagation context at correct
scope level (chat_completions, not execute_proxy) so fallback retries
don't operate on already-closed traces
- Add parent finalization safety nets at all terminal call sites:
Ollama cooldown fallback, HTTPException fallback, unexpected error
fallback, default proxy path, and non-transient re-raise paths
### Missed cleanup paths (5 sites)
- Backend misconfigured (500) — finalize child span before raise
- Context window exceeded (400) — finalize child span before raise
- Stream connection failure — finalize child span before raise
- Non-streaming non-200 — finalize child span before raise
- Proxy connection error (502) — finalize child span before raise
### Stream generator hardening
- Add propagate_attributes context in streaming generators for
session_id/user_id propagation across asyncio task boundaries
- Finalize spans on stream error and cancellation (finally block)
- Handle GeneratorExit gracefully in streaming generators
### LLAMA_SERVER_URL hardening
- Use config.yaml placeholder instead of hardcoded URL
- Add defensive str() guard: str(_raw_llama_url).rstrip('/')
### Code quality
- Extract _resolve_verify() helper for CA-bundle TLS verification
(deduplicates get_classifier_client/get_llama_client)
- Remove unnecessary f-string prefix (Ruff F541)
### E2E verification
- Add test_langfuse_session_propagation: validates session_id/user_id
propagation and cross-request leak isolation
- Fix leak detection when step-1 polling fails: report inconclusive
instead of false positive with degraded t.get('id') != None check
- Add poll-based Langfuse SDK auto-flush wait (up to 10s)
## Verification
- 101/101 unit tests pass
- 28/28 E2E pass (incl. Langfuse session propagation + leak isolation)
There was a problem hiding this comment.
Sorry @sheepdestroyer, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. 📝 WalkthroughWalkthroughThe router now resolves the llama endpoint from environment-backed configuration, centralizes TLS and llama health handling, and manages Langfuse observations and propagation contexts across completion, streaming, fallback, and error paths. Verification tooling adds Langfuse session propagation checks. ChangesLlama endpoint and client lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Router
participant ModelBackend
participant Langfuse
Client->>Router: POST /v1/chat/completions with session_id and user_id
Router->>Langfuse: create parent observation and propagation context
Router->>ModelBackend: route completion or stream request
ModelBackend-->>Router: completion result or stream events
Router->>Langfuse: finalize child span and parent observation
Router-->>Client: completion response or stream
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces Langfuse session and user propagation to the LLM router, refactors client creation to support dynamic TLS verification, and adds a verification script to test session propagation. The review feedback highlights two important issues: a critical resource and context leak in the non-streaming path of chat_completions during client cancellations or unexpected exceptions, and a race condition in the verification script's leak test that could lead to false positives.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| # --- Langfuse parent trace: create early so child spans can reference it --- | ||
| langfuse_trace_id = None | ||
| parent_obs = None | ||
| _prop_ctx = None | ||
| _is_streaming = body.get("stream", False) | ||
| lf = get_langfuse() | ||
| if lf: | ||
| try: | ||
| langfuse_trace_id = lf.create_trace_id( | ||
| seed=f"triage_{stats['total_requests']}" | ||
| ) | ||
| # Propagate session_id/user_id via Langfuse's native session mechanism. | ||
| # For non-streaming: enter here (same asyncio task, contextvars work). | ||
| # For streaming: each generator creates its own context in its own task | ||
| # because OpenTelemetry contextvars are task-isolated. | ||
| if propagate_attributes and (_trace_session_id or _trace_user_id): | ||
| if not _is_streaming: | ||
| _prop_ctx = propagate_attributes( | ||
| session_id=_trace_session_id or None, | ||
| user_id=_trace_user_id or None, | ||
| tags=[os.getenv("ENVIRONMENT", "production"), "llm-routing"], | ||
| ) | ||
| _prop_ctx.__enter__() |
There was a problem hiding this comment.
🔴 Critical Leak on Client Cancellation / Unexpected Exceptions in Non-Streaming Path
In the non-streaming path of chat_completions, if a client disconnects or an unexpected exception occurs during any of the await calls (such as await classify_request(...), await execute_proxy(...), or await try_agy_proxy(...)), an asyncio.CancelledError or other exception will propagate up.
Because there is no global try...finally block wrapping the non-streaming execution in chat_completions, the following cleanups will be completely bypassed:
_end_parent_obs(parent_obs)will never be called, leaving the parent trace hanging/unfinalized in Langfuse._close_prop_ctx(_prop_ctx)will never be called, leaking the context variables.
While the streaming generators correctly use finally blocks to handle cancellation, the non-streaming path is currently vulnerable.
Recommended Fix:
Wrap the non-streaming routing and execution logic in a try...finally block to guarantee cleanup:
# After initializing parent_obs and entering _prop_ctx:
try:
# ... existing routing and execution logic ...
finally:
if not _is_streaming:
_end_parent_obs(parent_obs, output=...)
_close_prop_ctx(_prop_ctx)| # Check for traces created after the session request | ||
| recent_ids = {t["id"] for t in traces2} | ||
| if session_trace_id and session_trace_id in recent_ids: | ||
| no_session_trace_found = True | ||
| break |
There was a problem hiding this comment.
⚡ Race Condition / False Positive in Session Propagation Test
In Step 2 of test_langfuse_session_propagation, the test attempts to verify that no session leak occurred by polling Langfuse traces. However, the current condition session_trace_id in recent_ids will immediately evaluate to True on the very first iteration of the loop because session_trace_id was already successfully flushed and stored in Langfuse during Step 1.
This causes the polling loop to exit prematurely (usually on the first attempt) without actually waiting for the second request's trace to be flushed. If the second trace is flushed slowly, the test will assert on an incomplete list of traces and pass falsely.
To fix this, we should check if there is a new trace in the list that is newer than session_trace_id. Since the traces are returned in descending order of timestamp, any trace newer than session_trace_id will appear before it in the list (i.e., its index will be greater than 0).
| # Check for traces created after the session request | |
| recent_ids = {t["id"] for t in traces2} | |
| if session_trace_id and session_trace_id in recent_ids: | |
| no_session_trace_found = True | |
| break | |
| # Check for traces created after the session request | |
| recent_ids = [t["id"] for t in traces2] | |
| if session_trace_id and session_trace_id in recent_ids: | |
| if recent_ids.index(session_trace_id) > 0: | |
| no_session_trace_found = True | |
| break |
There was a problem hiding this comment.
Fixed: Leak test waits for recent_ids.index(session_trace_id) > 0 to confirm flush before checking (commit 0f62620).
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
scripts/verification/verify_canonical_endpoints.py (1)
432-449: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLog swallowed exceptions in the poll loops.
Both polling loops (here and the leak-check loop at Lines 492-514) do
except Exception: continue, silently discarding every error. For a diagnostic/verification script this hides the actual cause when Langfuse polling fails (auth error, connection refused, JSON decode), leaving only a generic "not found after 10s" result. Capture and print the exception on the final attempt.♻️ Example: surface the error on the last attempt
- except Exception: - continue + except Exception as e: + if _attempt == 9: + print(f" ⚠ trace poll error: {str(e)[:100]}") + continue🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/verification/verify_canonical_endpoints.py` around lines 432 - 449, Update the polling loops around the trace lookup and leak-check logic to capture exceptions and print the error on the final retry instead of silently continuing. Preserve retries for earlier attempts, and include the exception details in the final-attempt diagnostic output before the existing failure result.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@router/main.py`:
- Around line 2388-2474: Add an exception handler to agy_stream_generator,
alongside its existing try/finally, that records genuine exceptions with the
actual error type and closes the propagation context before re-raising. Keep the
existing finally cancellation finalization only for interrupted streams, and
mirror the error-finalization behavior of native_agy_stream_generator.
- Around line 1995-2048: The Langfuse trace seed in the early parent-trace
initialization must be unique per request. Update the create_trace_id call
within the Langfuse setup block to use a freshly generated uuid.uuid4() value
instead of stats['total_requests'], reusing the module’s existing UUID import or
adding the required import.
In `@scripts/verification/verify_canonical_endpoints.py`:
- Around line 520-535: Update the step-2 handling in the session leak
verification flow so a known session trace missing from the polling window is
treated as inconclusive with a warning, matching the step-1 behavior, instead of
recording a failed check. Preserve leak detection when traces2 contains a
different trace for the same session and retain the existing counters and return
flow.
---
Nitpick comments:
In `@scripts/verification/verify_canonical_endpoints.py`:
- Around line 432-449: Update the polling loops around the trace lookup and
leak-check logic to capture exceptions and print the error on the final retry
instead of silently continuing. Preserve retries for earlier attempts, and
include the exception details in the final-attempt diagnostic output before the
existing failure result.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1650ed65-3bf4-4761-b808-354510b41e95
📒 Files selected for processing (4)
.env.devrouter/config.yamlrouter/main.pyscripts/verification/verify_canonical_endpoints.py
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent Code Review
Scope: 4 files, +623 / -138. Comprehensive Langfuse span lifecycle hardening across all 26+ routing code paths.
Items to address before merge
-
Production config needs
llama_server_urlfield — the new config resolution readsllama_server_url: "os.environ/LLAMA_SERVER_URL"fromconfig.yamlinstead of reading the env var directly.~/prod/LLM-Routing/router/config.yamlmust be updated before this reaches production. -
DRY: propagate_attributes block duplicated 4 times — the exact same condition+builder block appears at lines 2025, 2223, 2390, and 2612 of
main.py. Extract to a helper like_make_prop_ctx(session_id, user_id)to avoid copy-paste drift. -
Verification script: misleading variable name —
no_session_trace_found(line 490) is set toTruewhen the session trace IS found. Rename tosession_trace_visibleto match its actual semantics. -
Verification script: inconsistent limit between search passes — step 1 fetches with
limit=20(line 436) but the leak check useslimit=10(line 496). If 10+ traces arrive between the two requests, the session trace could be missed in the leak window. Align both to 20.
Non-blocking observations
- All parent_obs/prop_ctx exit paths are verified covered — every raise, return, and fallback has matching
_end_parent_obs+_close_prop_ctx. - Child-before-parent span ordering is consistent across all error paths. The CodeRabbit-flagged LiteLLM stream error at line 2680 is correctly fixed.
- The
finalizedguard in all 3 streaming generators prevents double-finalization on cancellation. - LiteLLM stream connection failure (r.status_code != 200) finalizes only
_end_child_spanlocally;parent_obs+_prop_ctxfinalization depends on outer HTTPException handlers 200+ lines away. A brief comment would help future maintainers.
Verdict
No approve/request-changes — all findings above are concrete and actionable. Merge decision is yours.
| token_count = 0 | ||
| finalized = False | ||
| _native_agy_prop = ( | ||
| propagate_attributes( |
There was a problem hiding this comment.
Suggestion: The propagate_attributes condition+builder block is duplicated 4 times (lines 2025, 2223, 2390, 2612). Extract to a small helper:
def _make_prop_ctx(session_id, user_id):
if not propagate_attributes or not (session_id or user_id):
return nullcontext()
return propagate_attributes(
session_id=session_id or None,
user_id=user_id or None,
tags=[os.getenv('ENVIRONMENT', 'production'), 'llm-routing'],
)
This would eliminate ~36 lines of copy-paste and keep the tags/env string in one place.
There was a problem hiding this comment.
Fixed: Extracted _make_prop_ctx(session_id, user_id) helper, used at all 4 propagation sites (commit 0f62620).
| ) | ||
| await r.aclose() | ||
| # Finalize child span before raising on stream connection failure | ||
| _end_child_span(litellm_span_obj, |
There was a problem hiding this comment.
Note: On LiteLLM stream connection failure (r.status_code != 200), only _end_child_span is called here. parent_obs and _prop_ctx finalization depend on the HTTPException propagating to outer handlers (ollama except block at ~2855 or default proxy catch at ~2933). Correct, but a one-liner comment would help: # parent_obs finalized by outer handler.
There was a problem hiding this comment.
Fixed: Comment added at L2731: 'parent_obs finalized by outer handler (HTTPException -> except block)' (commit 0f62620).
| return passed, total | ||
|
|
||
| # Wait for Langfuse SDK auto-flush (poll up to 10s for leak check) | ||
| no_session_trace_found = False |
There was a problem hiding this comment.
Naming: no_session_trace_found is misleading — it is set to True when the session trace IS found (line 505: no_session_trace_found = True). Rename to session_trace_visible or session_trace_flushed.
There was a problem hiding this comment.
Fixed: Renamed to session_trace_visible matching actual semantics (commit 0f62620).
| time.sleep(1) | ||
| try: | ||
| resp2 = httpx.get( | ||
| f"{lf_base}/api/public/traces?page=1&limit=10&orderBy=timestamp.desc", |
There was a problem hiding this comment.
Inconsistency: step 1 fetches traces with limit=20 (line 436) but the leak check uses limit=10. If 10+ traces arrive between requests, the session trace could fall outside the leak window. Align both to 20.
There was a problem hiding this comment.
Fixed: Both trace polls use limit=20 (commit 0f62620).
…e_id uniqueness, DRY helpers, leak test fixes Gemini Code Assist (critical): - Add try/finally around non-streaming execution body in chat_completions to guarantee _end_parent_obs + _close_prop_ctx on CancelledError Gemini Code Assist (high): - Fix race condition in leak test: wait for a NEWER trace to appear before checking for session leaks (check session_trace_id index > 0 in poll list) CodeRabbit (major): - Replace deterministic trace_id seed (stats counter) with uuid.uuid4() to prevent concurrent request trace collision CodeRabbit (minor): - Make step-2 poll timeout INCONCLUSIVE (warn) instead of FAILURE, consistent with step-1 behavior Owner review: - DRY: extract _make_prop_ctx() helper, replacing 4 duplicate propagate_attributes condition+builder blocks across 3 streaming generators + non-streaming init - Add comment at LiteLLM stream connection failure noting parent_obs is finalized by outer HTTPException handler - Rename no_session_trace_found → session_trace_visible (clearer semantics) - Align leak-check poll limit from 10 → 20 (consistent with step 1) Nitpick: - Log swallowed exceptions on final poll attempt for easier debugging Verification: 193/193 unit tests pass, 28/28 E2E pass (incl. Langfuse session propagation + leak isolation)
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent Review — Round 2
Verdict: 1 critical issue found, 1 minor suggestion. All 9 prior review comments addressed correctly.
🔴 Critical: Non-streaming finally block overwrites successful trace output
router/main.py:2940-2944 — The new finally block fires on ALL non-streaming paths. Python executes finally BEFORE returning the value, so _end_parent_obs(parent_obs, output={"error": "cancelled"}) overwrites the success output set by the return path.
Fix: _non_streaming_finalized = False flag + guard. See inline comment for details.
💡 Minor: LLAMA_SERVER_URL missing from main .env
Added to .env.dev but not .env. Router falls back safely, but add for consistency.
✅ 8 patterns verified correct (see inline comments for details)
The only blocker is the finally-block overwrite. Fix and this is ready.,
| client=get_http_client(), | ||
| cooldown_persistence=ValkeyCooldownPersistence(), | ||
| ) | ||
| if agy_response: |
There was a problem hiding this comment.
🔴 Critical: finally block fires on successful non-streaming returns, corrupting trace output.
When a non-streaming request succeeds:
- The success path calls
_end_parent_obs(parent_obs, output={"model": ..., "tier": ...})(e.g. line 2463, 2781) - Python queues the
returnvalue - Python executes this
finallyblock BEFORE the return _end_parent_obs(parent_obs, output={"error": "cancelled"})calls.update()→ overwrites the success output- Then
.end()is called again (benign)
Result: every non-streaming Langfuse trace shows output={"error": "cancelled"} instead of the real result.
Fix: Add a _non_streaming_finalized = False flag (same pattern as the 3 streaming generators), set it to True at every explicit _end_parent_obs call site, and guard the finally block:
finally:
if not _is_streaming and not _non_streaming_finalized:
_end_parent_obs(parent_obs,
output={"error": "cancelled", "route": "non_streaming"})
_prop_ctx = _close_prop_ctx(_prop_ctx)…flag Prevents the finally block from overwriting successful/error trace output with "cancelled" on normal non-streaming returns/raises. The flag is set to True at all 8 explicit _end_parent_obs + _close_prop_ctx exit points. Matches the same finalized-flag pattern used in the 3 streaming generators. 193/193 unit + 28/28 E2E pass (incl. Langfuse session propagation)
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent Review — Round 3
Verdict: APPROVE — all prior review comments addressed, zero blocking items remain.
All fixes verified
| Source | Severity | Finding | Status |
|---|---|---|---|
| Gemini CA | Critical | Non-streaming CancelledError cleanup | Fixed — _non_streaming_finalized flag guards finally block; set True at all 8 exit points |
| Gemini CA | High | Leak test race condition | Fixed — waits for recent_ids.index(session_trace_id) > 0 |
| CodeRabbit | Major | Deterministic trace_id collision | Fixed — uuid.uuid4() |
| CodeRabbit | Minor | Asymmetric INCONCLUSIVE handling | Fixed — step-2 timeout warns instead of failing; both polls use limit=20 |
| Owner | DRY | 4 duplicate propagate_attributes blocks | _make_prop_ctx() helper |
| Owner | Naming | Misleading variable name | no_session_trace_found -> session_trace_visible |
| Owner | Comment | LiteLLM stream connection failure | Comment added |
Test results
- 193/193 unit tests pass
- 28/28 E2E pass (incl. Langfuse session propagation + zero session leak)
Non-blocking
- LLAMA_SERVER_URL in .env.dev but not .env — code falls back safely.
Ready to merge.
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent Review — Round 3 (Commit 2d5fd05)
Verdict: 1 critical issue found.
🔴 Critical: 4 nested fallback exit points missing _non_streaming_finalized = True
The _non_streaming_finalized flag correctly guards the outer finally block at 8 explicit exit points. However, 4 additional nested except HTTPException handlers also call _end_parent_obs + _close_prop_ctx + raise without setting the flag. These re-raises propagate through the outer try/finally, which then overwrites the trace output with "cancelled".
The 4 missing sites all follow the same pattern: nested fallback-retry except HTTPException: that finalizes the parent before re-raising.
Fix: add _non_streaming_finalized = True before raise at all 4 sites.
✅ What is correct
- Flag initialization, 8 exit-point sets, and finally guard correctly placed
execute_proxynon-streaming 200 path (inside closure) correctly sets flag- Streaming generators use own
finalizedguards (unaffected) - Pattern matches existing 3 streaming generators
⚠️ Working tree note
Staged changes on pr-311 appear to revert parts of 2d5fd05. Resolve before merge.
Reviewed by Hermes Agent
| _end_parent_obs(parent_obs, | ||
| output={"error": "all_backends_failed", "route": "ollama_cooldown_fallback"}) | ||
| _close_prop_ctx(_prop_ctx) | ||
| raise |
There was a problem hiding this comment.
🔴 Missing _non_streaming_finalized = True — this raise propagates through the outer try/finally, which overwrites the parent observation output with "cancelled". Add _non_streaming_finalized = True before raise.
There was a problem hiding this comment.
Fixed: Added _non_streaming_finalized = True before raise at ollama_cooldown fallback (commit 47ffae5).
| _end_parent_obs(parent_obs, | ||
| output={"error": "all_backends_failed", "route": "ollama_fallback"}) | ||
| _close_prop_ctx(_prop_ctx) | ||
| raise |
There was a problem hiding this comment.
🔴 Missing _non_streaming_finalized = True — same pattern: nested fallback except HTTPException finalizes parent then re-raises. Add the flag before raise.
There was a problem hiding this comment.
Fixed: Added _non_streaming_finalized = True before raise at ollama_fallback (commit 47ffae5).
| _end_parent_obs(parent_obs, | ||
| output={"error": "all_backends_failed", "route": "ollama_unexpected_fallback"}) | ||
| _close_prop_ctx(_prop_ctx) | ||
| raise |
There was a problem hiding this comment.
🔴 Missing _non_streaming_finalized = True — same pattern. Add before raise.
There was a problem hiding this comment.
Fixed: Added _non_streaming_finalized = True before raise at ollama_unexpected_fallback (commit 47ffae5).
| _end_parent_obs(parent_obs, | ||
| output={"error": "all_backends_failed", "route": "default_proxy"}) | ||
| _close_prop_ctx(_prop_ctx) | ||
| raise |
There was a problem hiding this comment.
🔴 Missing _non_streaming_finalized = True — default proxy else-branch raises after finalizing parent. Add flag before raise.
There was a problem hiding this comment.
Fixed: Added _non_streaming_finalized = True before raise at default_proxy (commit 47ffae5).
…e sites in non-streaming proxy
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent Code Review — PR #311
Verdict: ✅ Zero issues found — recommend merge. All checklist items pass. (Cannot self-APPROVE via GitHub API; this COMMENT serves as the approval recommendation.)
Review Scope
Hardens Langfuse parent/child span lifecycle across all routing paths — streaming, non-streaming, fallback chains, and error/cancellation paths. The most recent commit (47ffae5) fixes the 4 nested fallback re-raise sites that were previously missing the _non_streaming_finalized = True guard.
Verified Items
✅ Non-Streaming Exit Points (12/12 guarded)
All 12 non-streaming exit points correctly set _non_streaming_finalized = True before raising or returning:
- L2095 — Unknown model guard → raise HTTPException
- L2471 — Agy non-streaming success → return
- L2791 — execute_proxy non-streaming success → return (internal)
- L2846 — Ollama cooldown → fallback fails → re-raise
- L2854 — Ollama cooldown → direct 429 → raise HTTPException
- L2886 — Ollama transient → fallback fails → re-raise
- L2892 — Ollama non-transient → raise e
- L2903 — Direct ollama transient → raise HTTPException
- L2912 — Direct ollama non-transient → raise e
- L2934 — Ollama unexpected → fallback fails → re-raise
- L2940 — Direct ollama unexpected → raise HTTPException
- L2951 — Default proxy failure → raise
✅ Finally Guard (L2954)
Guard correctly checks if not _is_streaming and not _non_streaming_finalized:. The critical insight: execute_proxy at L2791 internally sets _non_streaming_finalized = True on non-streaming success, so the finally guard does not fire on successful requests. The guard only catches truly unhandled non-streaming exits.
✅ _make_prop_ctx() DRY Helper
Well-structured at L389-403. Used consistently at all 4 propagation sites (L2045, L2241, L2403, L2621) — no duplicated condition+builder blocks. Returns None when propagate_attributes is unavailable or no session/user data.
✅ trace_id Uses uuid4
L2038: seed=str(uuid.uuid4()) — non-deterministic, not a stats counter.
✅ Variable Naming
session_trace_visible correctly used in verification script (L492, L510, L532).
✅ Limit Alignment
Both trace polls use limit=20 (L436 and L498).
✅ INCONCLUSIVE Handling Symmetric
- Step 1 (L528): "session trace not found in step 1 polling"
- Step 2 (L539): "second request trace not flushed within 10s"
Both correctly return without counting as failures.
✅ No Debug Artifacts
No commented-out code, debug prints, or TODOs in the changed files. Searched across all changed files for TODO, FIXME, HACK, XXX, print(, debugger — zero hits in the PR diff.
Config Changes (ancillary)
.env.dev: AddedLLAMA_SERVER_URLrouter/config.yaml: Addedllama_server_urlbinding- Both are environmental configuration additions, not logic changes.
Test Results (previously verified)
- ✅ 193/193 unit tests pass
- ✅ 28/28 E2E tests pass
Reviewed by Hermes Agent
…ype instead of 'cancelled'
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent FRESH GLOBAL Review — PR #311
Verdict: APPROVE — All prior review findings addressed. Zero blocking issues remain. One non-blocking code-clarity observation noted below.
Scope: 4 files, +1228/-705. Comprehensive Langfuse span lifecycle hardening across all routing paths.
Checklist Results
✅ _non_streaming_finalized Flag (13 sites)
All 12 = True exit points verified against current code at commit 0c4169c:
- L2095 — Unknown model guard → raise HTTPException
- L2481 — Non-streaming agy success → return
- L2801 — execute_proxy non-streaming success → return (closure)
- L2856 — Ollama cooldown → fallback fails → re-raise
- L2864 — Ollama cooldown → direct 429 → raise HTTPException
- L2896 — Ollama transient → fallback fails → re-raise
- L2902 — Ollama non-transient auto → raise
- L2913 — Direct ollama transient → raise HTTPException
- L2922 — Direct ollama non-transient → raise
- L2944 — Ollama unexpected → fallback fails → re-raise
- L2950 — Direct ollama unexpected → raise
- L2961 — Default proxy failure → raise
Init at L2066. Finally guard at L2964: if not _is_streaming and not _non_streaming_finalized:
✅ Streaming Generators (3 generators)
- native_agy_stream_generator (L2232–2347): try/except/finally with
finalizedguard. Child-before-parent ordering on all paths._make_prop_ctxwith nullcontext() fallback. - agy_stream_generator (L2394–2468): try/except/finally with
finalizedguard.except Exception as ehandler (added in 0c4169c) recordstype(e).__name__instead of'cancelled'._make_prop_ctxwith nullcontext(). - LiteLLM stream_generator (L2621–2729): try/except/finally with
finalizedguard. Child-before-parent on all paths.r.aclose()in finally._make_prop_ctxwith nullcontext(). Comment at L2741 correctly notes parent finalized by outer handler.
✅ Helper Functions (L331–403)
_end_parent_obs/_end_child_span: Non-fatal,.update()before.end(), swallows exceptions_close_prop_ctx: Safe context exit, returns None for idempotent cleanup_make_prop_ctx: DRY helper used at all 4 propagation sites (L2045, L2241, L2403, L2631)
✅ Error Paths
- All raises in chat_completions correctly clean up parent_obs + prop_ctx before propagating
- execute_proxy error paths finalize child spans locally; parent finalization by outer handlers
- Ollama fallback chains (retry → free tier) handle parent finalization at both success and failure levels
- LiteLLM stream connection failure (L2734–2749): child span finalized, parent handled by outer HTTPException handler (comment at L2741)
✅ Verification Script
session_trace_visiblenaming correct (L492)limit=20on both trace polls (L436, L498)- INCONCLUSIVE handling symmetric: step 1 missing trace → warn + return (L527–529), step 2 timeout → warn + return (L537–542)
- No session leak false positives
✅ Code Quality
trace_idusesuuid.uuid4()(L2038) — non-deterministic, unique per request_resolve_verify()DRY helper for CA bundle resolution- No debug prints, TODOs, FIXMEs, commented-out code in changed files
- No hardcoded secrets — all credentials read from
os.getenv()oros.environ/config pattern
✅ Config Changes
.env.dev: AddedLLAMA_SERVER_URL(non-secret infrastructure URL)router/config.yaml: Addedllama_server_url: "os.environ/LLAMA_SERVER_URL"binding
Observations (Non-Blocking)
💡 _non_streaming_finalized inside execute_proxy closure — missing nonlocal
At L2801, _non_streaming_finalized = True is assigned inside the execute_proxy nested function without a nonlocal declaration. In Python 3, this creates a local shadow variable — the outer scope's _non_streaming_finalized (initialized at L2066) is never modified by this assignment.
Practical effect: The finally guard at L2964 fires on ALL successful execute_proxy non-streaming returns. It calls _end_parent_obs(parent_obs, output={"error": "cancelled"}) on an already-ended observation (ended at L2794 via the first _end_parent_obs call inside execute_proxy). Langfuse SDK v4 rejects .update() on ended observations, and _end_parent_obs swallows the exception — so the output is preserved correctly, but by accident rather than by design.
Recommendation: Add nonlocal _non_streaming_finalized at the top of execute_proxy (after L2523) for correctness. This makes the guard's intent explicit and eliminates the unnecessary duplicate finalization call.
💡 classify_request except block (pre-existing, not from this PR)
At L1290–1293, the except Exception handler returns a fallback classification without finalizing class_span_obj. This is pre-existing code unchanged by this PR, but worth noting for a future cleanup pass.
Prior Review Comments — All Resolved
| Source | Finding | Resolution |
|---|---|---|
| Gemini CA | Non-streaming CancelledError cleanup | Fixed — _non_streaming_finalized flag + finally guard |
| Gemini CA | Leak test race condition | Fixed — recent_ids.index(session_trace_id) > 0 |
| CodeRabbit | Deterministic trace_id collision | Fixed — uuid.uuid4() |
| CodeRabbit | Asymmetric INCONCLUSIVE handling | Fixed — step-2 inconclusive + limit=20 both polls |
| CodeRabbit | agy_stream_generator missing except handler | Fixed — commit 0c4169c |
| Owner | 4 duplicate propagate_attributes blocks | Fixed — _make_prop_ctx() helper |
| Owner | Misleading variable name | Fixed — session_trace_visible |
| Owner | Missing _non_streaming_finalized at 4 nested sites |
Fixed — commit 47ffae5 |
Test Results (previously verified)
- ✅ 193/193 unit tests pass
- ✅ 28/28 E2E tests pass (incl. session propagation + zero leak)
Reviewed by Hermes Agent — FRESH GLOBAL review against commit 0c4169c
…_proxy closure — prevents local variable shadow
|
Addressed fresh global review finding: added |
sheepdestroyer
left a comment
There was a problem hiding this comment.
Hermes Agent — Final Fresh Global Code Review
Verdict: ✅ APPROVE — Zero issues. Code is merge-ready.
All 10 checklist items verified against current code at commit 1644d9c:
✅ 1. Langfuse Span Lifecycle
All code paths have correct parent/child finalization. Every try/except/finally block properly calls _end_child_span before _end_parent_obs. The streaming generators finalize telemetry BEFORE any fallible await calls in except blocks (e.g., cooldown save).
✅ 2. _non_streaming_finalized Guard
nonlocaldeclared at L2525 insideexecute_proxy✅- 1 init:
_non_streaming_finalized = Falseat L2066 ✅ - 12
= Truesites (L2095, 2481, 2802, 2857, 2865, 2897, 2903, 2914, 2923, 2945, 2951, 2962) — all correct ✅ - 1 finally guard at L2964-2968 ✅
✅ 3. Streaming Generators (3 async defs)
- native_agy_stream_generator (L2232): try/except/finally with
finalizedflag ✅ - agy_stream_generator (L2394): try/except/finally with
finalizedflag ✅ - LiteLLM stream_generator (L2622): try/except/finally with
finalizedflag; ollama cooldown path at L2706 properly sequenced (telemetry finalized before cooldown save) ✅
✅ 4. Propagation Contexts
_make_prop_ctx DRY helper used at all 4 sites (L2045, 2241, 2403, 2632) ✅
_close_prop_ctx safely handles both real context managers and nullcontext() ✅
✅ 5. Child-Before-Parent Ordering
Every dual-finalization site calls _end_child_span BEFORE _end_parent_obs ✅
✅ 6. Error Paths
All raise sites clean up spans before propagating:
- LiteLLM non-200 (L2809 → parent finalized by caller handler) ✅
- Context window exceeded (L2596 → parent by caller) ✅
- Unknown model (L2092 → parent + prop_ctx + finalized) ✅
- All 4 nested ollama fallback
except HTTPExceptionblocks finalize parent/prop_ctx ✅
✅ 7. Verification Script (verify_canonical_endpoints.py)
session_trace_visibleguard at L492 ✅- API polls use
limit=20at L436 and L498 ✅ - Symmetric INCONCLUSIVE handling at L528 (session trace not found) and L538 (second trace not flushed) ✅
- Leak check correctly excludes the known session trace ✅
✅ 8. Code Quality
Zero debug artifacts: no TODO, FIXME, XXX, debugger, or stray print() statements ✅
✅ 9. Security
No hardcoded secrets — all credentials read from environment variables ✅
.env.dev addition is an internal HTTPS URL, not a credential ✅
✅ 10. Documentation
Non-obvious paths have clear comments:
- Contextvars task isolation for streaming vs non-streaming (L2040-2043) ✅
- Classification gating rationale (L2150-2152) ✅
- Ollama cooldown flow notes ✅
- Leak test INCONCLUSIVE rationale in verification script ✅
Test Results (verified from prior runs)
- 193/193 unit tests pass ✅
- 28/28 E2E pass (session propagation + zero leak) ✅
Prior Reviews — All Addressed
- Gemini CA (2): non-streaming cleanup, leak test ✅
- CodeRabbit (4): trace_id uuid, agy_stream except, INCONCLUSIVE symmetry, poll logging ✅
- Owner rounds (8): _non_streaming_finalized, DRY, naming, limits, comment, 4 nested fallbacks ✅
- Fresh review round 1 (1): nonlocal in execute_proxy ✅
Reviewed by Hermes Agent (fresh global review of commit 1644d9c)
|
Closing to replace with a new PR consolidating all review findings and fixes. |
What
Harden Langfuse observation and propagation context lifecycle across all routing paths — streaming, non-streaming, fallback chains, and error/cancellation paths. Incorporates review feedback from PR#305 (Gemini Code Assist, owner self-review, CodeRabbit).
Changes
Parent/child span lifecycle (Gemini CA critical)
chat_completionslevel, not insideexecute_proxyMissed cleanup paths
Streaming generator hardening
propagate_attributescontext entered inside generators for session/user propagation across asyncio tasksDefensive fixes
str()guard onLLAMA_SERVER_URLto preventAttributeErroron non-string config valuesCode quality
_resolve_verify()helper to DRY CA-bundle resolutionlogger.debug(exc_info=True)on all finalization helpers for opt-in debuggingE2E verification
test_langfuse_session_propagation: validates session/user propagation and cross-request leak isolationVerification
Summary by CodeRabbit