Repository navigation
feat(lens): show end-user feedback on traces, stored in ClickHouse - #45171
Conversation
ReplacingMergeTree(UpdatedAt, IsDeleted) keyed like agent_traces_by_key so feedback joins traces in sort order, with a bloom filter on TraceId, a score CHECK, and the same retention as traces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feedback_target resolves a visible trace's team, key and trace_ref before a write; feedback lists the latest live entry per author; feedback_summary returns count, average and lowest score for a batch of traces Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PUT saves or replaces the caller's score and comment, GET lists every entry, DELETE removes the caller's, POST /summary feeds the trace list. Callers can target a trace_id or a Claude Code session_id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Feedback column shows each run's average score and rating count, marked red when any score is 4 or below, and a filter narrows to rated or low-score runs through the URL Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
|
| <FeedbackForm | ||
| key={view.mine?.updated_at ?? "new"} | ||
| mine={view.mine} | ||
| saving={save.isPending} | ||
| removing={remove.isPending} | ||
| onSave={(score, comment) => save.mutate({ score, comment })} | ||
| onRemove={() => remove.mutate()} |
|
|
||
| export function matchesFeedback(state: TraceFeedbackState | undefined, filter: FeedbackFilter): boolean { | ||
| if (filter === "all") return true; | ||
| if (state?.status !== "ready" || state.summary.count === 0) return false; |
There was a problem hiding this comment.
Under “Has feedback” or “Low score,” a failed summary request hides every affected run as though none matched. The table only receives errors from the trace request, so the reviewer sees an empty result instead of the feedback error. Show the failed lookup and a retry action
| <Button type="button" variant="ghost" size="xs" disabled={removing} onClick={onRemove}> | ||
| Remove | ||
| </Button> | ||
| )} | ||
| <Button type="submit" size="xs" disabled={score === null || saving}> |
There was a problem hiding this comment.
| ("otel_traces", "toDateTime(Timestamp)"), | ||
| ("agent_traces_by_key", "toDateTime(StartTs)"), | ||
| ("spend_logs", "toDateTime(start_time)"), | ||
| ("lens_feedback", "toDateTime(CreatedAt)"), |
There was a problem hiding this comment.
| ); | ||
| } | ||
|
|
||
| /** Human 0–10 scores and comments on one run, plus the caller's own editable entry. */ |
There was a problem hiding this comment.
Routine comments break repository rules
This new comment repeats the function’s purpose. AGENTS.md prohibits routine source comments unless they meet a listed exception. Remove it before merging. The same issue appears in feedbackView and stubPost
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| **access_parameters(scope).model_dump(), trace_ids=sorted({t.trace_id for t in traces}) | ||
| ), | ||
| ) | ||
| return tuple(summary for trace in traces for summary in _summaries(trace, rows)) |
There was a problem hiding this comment.
Comprehensions exceed the allowed limit
This comprehension uses two for clauses. AGENTS.md allows only one per comprehension, so split it into a helper before merging. The fake summary query in test_feedback_endpoints.py repeats the same violation
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Any key can now PUT/DELETE feedback on traces its team or key sent, naming the end user in an optional user field. Reading feedback stays with Lens admins Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red rows Replace the edit popover and feedback filter with a read-only view: the list shows each run's score in a fixed-width badge and tints the row red when a user scored it 4 or below, and opening a run shows what users said with their score right under the header Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| return Scope(all_teams=True) | ||
| if auth.user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY: | ||
| raise HTTPException(403, "Admin viewers cannot write feedback") | ||
| return Scope(team_id=auth.team_id or "", api_key_hash="" if auth.team_id else auth.token or "") |
There was a problem hiding this comment.
Medium: Wildcard feedback access for teamless JWTs
JWTAuthManager.user_api_key_auth_from_result() leaves auth.token unset, so a caller without a resolved team receives Scope(team_id="", api_key_hash=""); lens_feedback_target.sql treats the empty key hash as a wildcard. A non-admin JWT user can therefore overwrite or delete another key's feedback by supplying a known teamless trace/session ID and the victim's user; reject callers lacking both a team and a key hash.
| return Scope(team_id=auth.team_id or "", api_key_hash="" if auth.team_id else auth.token or "") | |
| if not auth.team_id and not auth.token: | |
| raise HTTPException(403, "Feedback requires a team or API key") | |
| return Scope(team_id=auth.team_id or "", api_key_hash="" if auth.team_id else auth.token or "") |
PR overviewAdds end-user feedback to trace views, backed by ClickHouse storage and endpoints for managing feedback. One access-control issue remains: a non-admin JWT caller without a resolved team or API-key hash can overwrite or delete another key’s feedback by supplying a known teamless trace or session ID and the victim’s user value. The demonstrated impact is limited to unauthorized modification or removal of feedback; no issues have yet been addressed. Open issues (1)
Fixed/addressed: 0 · PR risk: 6/10 |
… a list A proxy without the feedback route or an error body made the list crash on query.data.map; treat it as unavailable feedback instead Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker image copies lens/models.py without the litellm package, so the litellm.constants import broke it; move the proxy-only feedback models into feedback_models.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… failed one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…was recorded Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2848bf7 to
466f42b
Compare
…n_feedback # Conflicts: # litellm-rust/crates/traces-clickhouse/src/query/lens.rs # litellm/rust_bridge/trace/generated/types.py # ui/litellm-dashboard/src/components/lens/data/demo/createLensDemo.ts # ui/litellm-dashboard/src/components/lens/traces/api.ts # ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx # ui/litellm-dashboard/src/lib/http/schema.d.ts
…n_feedback # Conflicts: # ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx
TLDR
Problem this solves:
How it solves it:
/lens/feedbackAPI: an app posts its end user's 0 to 10 score and commentlens_feedbacktable next to the tracesUser Flow
Before: an app's users rate the agent's answers, but the developer debugging in Lens never sees those ratings
After: the rating flows into Lens and is the first thing the developer sees on that run
PUT /lens/feedbackwith its own key, thetrace_id(or Claude Codesession_id), the enduser,score2 and the comment★ 2/10badge and its whole row is tinted red2/10, the user's quote, who said it and whenPre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Setup: local proxy on :4000 with
tracing:on, ClickHouse on :18123, Postgres on :15432, and real Lens traces already ingested for team9319c223….$APP_KEYis an ordinary virtual key minted for that team withPOST /key/generate, standing in for the customer's app. The proxy createslens_feedbackitself at bootBefore (46d440a)
API
/lens/feedbackonly matches the existing/lens/{lens_id}investigation routes and an app has nowhere to send a ratingUI
After (18fd204)
API
curl -s -X PUT http://localhost:4000/lens/feedback -H "Authorization: Bearer $APP_KEY" -H 'Content-Type: application/json' -d '{"trace_id":"a2b2ab7f16eaaec662eebdea0a2b63be","user":"customer-1042","score":2,"comment":"I asked for the sum of 1 to 100 and it ran the wrong command first. Took two tries."}'{"trace_id":"a2b2ab7f16eaaec662eebdea0a2b63be","trace_ref":"482C5101…","score":2,"comment":"I asked for the sum of 1 to 100 and it ran the wrong command first. Took two tries.","author":"customer-1042","created_at":"2026-10-07T21:57:01.414000Z","updated_at":"2026-10-07T21:57:01.414000Z"}customer-2210, 7 "Right answer, a bit slow." fromcustomer-3077): both HTTP 200curl -s -X PUT http://localhost:4000/lens/feedback -H "Authorization: Bearer $APP_KEY" ... -d '{"trace_id":"94816cc030f363944412a825c8c991b7","user":"customer-1042","score":5,"comment":"other tenant trace"}'{"detail":"Trace not found"}ClickHouse table
SHOW CREATE TABLE litellm.lens_feedbackafter proxy bootUI
Type
🆕 New Feature
Caveats (if any)
Medium
useris whatever the app sends; LiteLLM does not verify end-user identitytrace_id+trace_ref), Lens's unit for a sessionsession_idmaps to the same id ingest derives, pinned by matching Rust and Python testsLow
Final Attestation