Skip to content

fix(ui): keep untimed guardrail entries on the request lifecycle - #41374

Merged
yuneng-berri merged 5 commits into
mainfrom
litellm_fix_guardrail_lifecycle_untimed_entries
Sep 17, 2026
Merged

yuneng-berri merged 5 commits into
mainfrom
litellm_fix_guardrail_lifecycle_untimed_entries

Conversation

@yuneng-berri

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Guardrails that report no timing vanished from the log drawer's lifecycle
  • The whole four-row Request Lifecycle panel renders empty
  • Pre-existing shape: the recorder defaults timing to None

How it solves it:

User Flow

Before: an admin looking at a log for a guardrail that reports no timing gets an empty Request Lifecycle panel

  1. They send POST http://localhost:4010/v1/chat/completions with a guardrail enabled that records its decision without start/end times (the conduct guardrail does this today)
  2. They open http://localhost:4010/ui/?page=logs and click the request's row
  3. The drawer says "1 guardrail evaluated" and the guardrail is named under Request Details
  4. The "Request Lifecycle" heading renders with nothing under it, so the panel jumps straight to Evaluation Details, and they cannot see which phase the guardrail ran in

After: the same log shows the lifecycle again, with no invented timings

  1. They send the same POST http://localhost:4010/v1/chat/completions
  2. They open http://localhost:4010/ui/?page=logs and click the request's row
  3. The drawer still says "1 guardrail evaluated"
  4. Request Lifecycle lists "Request received", "Pre-call guardrail: PASSED", "LLM call" and "Response returned", each showing an em dash where an elapsed offset would be, because this guardrail never reported one

Relevant issues

Affected release

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Both legs were captured by driving the real Admin UI, served by a live proxy on :4010 against its own Postgres, opening the same real log row from one real OpenAI call. The guardrail is a custom guardrail that records its decision without timing, which is the shape the conduct guardrail emits today.

Shared setup, config.yaml:

model_list:
  - model_name: gpt-4o-mini
    litellm_params:
      model: openai/gpt-4o-mini
      api_key: os.environ/OPENAI_API_KEY

guardrails:
  - guardrail_name: "conduct-style-untimed"
    litellm_params:
      guardrail: untimed_guardrail.UntimedPreCallGuardrail
      mode: "pre_call"
      default_on: true

general_settings:
  store_prompts_in_spend_logs: true

untimed_guardrail.py, next to the config and on PYTHONPATH:

class UntimedPreCallGuardrail(CustomGuardrail):
    async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
        self.add_standard_logging_guardrail_information_to_request_data(
            guardrail_json_response={"verdict": "allow", "rule_id": "demo-rule"},
            request_data=data,
            guardrail_status="success",
        )
        return data

The one request both legs read back:

curl -sS -X POST http://127.0.0.1:4010/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Say the single word: lifecycle"}]}'
lifecycle

which stored exactly the shape this PR is about:

[{"duration": null, "end_time": null, "start_time": null,
  "guardrail_mode": "pre_call", "guardrail_name": "conduct-style-untimed",
  "guardrail_status": "success"}]

Before (174c1ac)

  1. Open http://127.0.0.1:4010/ui/?page=logs, search the request id, click the row, scroll to Request Lifecycle
  2. The heading is there and the panel below it is empty, reading the rendered rows out of the live page:
{ lifecycleHeadingPresent: true, rowCount: 0, rows: [], header: "1 guardrail evaluated" }

After (51a243e)

  1. Open http://127.0.0.1:4010/ui/?page=logs, search the same request id, click the row, scroll to Request Lifecycle
  2. All four rows are back, each with an em dash in place of an offset this guardrail never reported:
{ rowCount: 4, header: "1 guardrail evaluated", rows: [
  "Request received | —",
  "Pre-call guardrail: conduct-style-untimed | PASSED | —",
  "LLM call | —",
  "Response returned | —" ] }
  1. The same two tests are red on the base component and green here, with every other test in the file untouched and passing on both legs:
$ npx vitest run src/components/view_logs/GuardrailViewer/GuardrailViewer.test.tsx
Tests  2 failed | 15 passed (17)   # base GuardrailViewer.tsx, this PR's tests
Tests  17 passed (17)              # this PR

Type

🐛 Bug Fix

Caveats

Low

  • A guardrail that reports no timing now shows an em dash where an offset would be, rather than being hidden
  • not_run entries that do carry timing still appear, exactly as they do today; this PR does not change that

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

#39050 changed RequestLifecycle from sorting every entry with
(a.start_time ?? 0) to filtering on isTimed, which drops any entry whose
start_time/end_time are null. That was the right call for the not_run entries
the PR introduced, but it also drops entries that DID run and simply carry no
timing, and those are pre-existing: add_standard_logging_guardrail_information_to_request_data
defaults start_time, end_time and duration to None, and the conduct guardrail
passes none of them. One such entry used to draw the whole four-row lifecycle
and now draws nothing, so an admin opening that log sees an empty
Request Lifecycle panel.

An entry now stays on the lifecycle when it is timed OR when it ran, so not_run
keeps the exclusion #39050 wanted and every other shape comes back. Offsets are
number | null and render as an em dash rather than a fabricated T+0ms, which is
what a null minus a null used to produce on the base. Entries without timing
sort after the timed ones and the base time comes from the timed entries, so
real offsets are unchanged.

The two new tests fail on the base component and pass here; #39050's own
not_run tests keep passing untouched, which is what makes this additive rather
than a revert.
@yuneng-berri
yuneng-berri requested a review from a team September 16, 2026 04:45
@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with both earlier ordering findings fixed and no new actionable issues

Summary

This PR restores untimed guardrail entries in the request lifecycle without fabricating elapsed offsets

  • Includes guardrails that ran even when timing data is absent
  • Preserves recorded slots while sorting timed entries within each lifecycle phase
  • Renders missing offsets as an em dash and adds focused regression coverage

Reviews (4) · Last reviewed commit: "fix(ui): order each lifecycle phase on i..."

Comment thread ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx Outdated
…ment

The four .parentElement reads in the new lifecycle tests pushed
testing-library/no-node-access to 712 against a 707 budget, failing
frontend-lint. The rows now carry data-testid="lifecycle-row" and the test
picks a row with within(), which keeps the assertion tied to the specific row
rather than the whole panel and takes the count back to 707.
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx Outdated
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptile

@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai review

@yuneng-berri
yuneng-berri merged commit 1dd4c13 into main Sep 17, 2026
83 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_fix_guardrail_lifecycle_untimed_entries branch September 17, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants