fix(ui): keep the request log model cell to one model (#49619)
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Cost map pins recheck / rerun-cost-map-guard (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
LiteLLM Rust DB / db-queries (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
unit / rust-bridge (push) Waiting to run
unit / assert-shard-coverage (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / proxy-server (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / OpenAI 3.0.0 compatibility (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / docs (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Blocked by required conditions
unit / OpenAI 3.25.0 compatibility (push) Blocked by required conditions
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run

* fix(ui): keep the request log model cell to one model

A session row rendered every model the conversation ever used, comma
joined. A long conversation produced a huge string that the auto-layout
table turned into a very wide Model column, pushing the rest of the
columns off screen.

Render only the model of the row's own call (the newest call in the
session) and show an overflow count. The full model list stays in the
hover popup, including a note when the server capped the list.

Co-Authored-By: AaronHowell <111272993+AaronHowell@users.noreply.github.com>

* fix(ui): use the shared Badge and an i18n key in the request log model cell

The "+N" overflow marker now uses the shadcn Badge instead of a hand-styled span, and the
"More models not shown" note reads from en.json so the dashboard's i18next/no-literal-string
lint rule passes.

---------

Co-authored-by: AaronHowell <111272993+AaronHowell@users.noreply.github.com>
Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
This commit is contained in:
Rick 2026-11-10 16:05:02 +08:00 • committed by GitHub
parent 52cd9ef0e4
commit f700e21e32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 25 deletions

View file

@ -368,7 +368,24 @@ describe("Request ID column", () => {
});
describe("Model column", () => {
it("lists every model used across a conversation, not only the representative call's model", () => {
it("keeps a long conversation's inline cell to the latest call's model", () => {
const conversationCall: Partial<LogEntry> = {
request_id: "req-session",
model: "gpt-5.6",
session_id: "sess-1",
session_total_count: 4,
session_models: ["claude-sonnet-5", "gemini-3-pro", "gpt-5.6", "o4-mini"],
};
renderRows([logEntry(conversationCall)]);
expect(screen.getByText("gpt-5.6")).toBeInTheDocument();
for (const otherModel of ["claude-sonnet-5", "gemini-3-pro", "o4-mini"]) {
expect(screen.queryByText(otherModel)).not.toBeInTheDocument();
}
});
it("reveals the conversation's other models in the hover popup", async () => {
const user = userEvent.setup();
const conversationCall: Partial<LogEntry> = {
request_id: "req-session",
model: "gpt-5.6",
@ -378,12 +395,14 @@ describe("Model column", () => {
};
renderRows([logEntry(conversationCall)]);
expect(screen.getByText("claude-sonnet-5, gpt-5.6")).toBeInTheDocument();
expect(screen.queryByText("gpt-5.6")).not.toBeInTheDocument();
await user.hover(screen.getByText("+1"));
expect(await screen.findByText("claude-sonnet-5")).toBeInTheDocument();
});
it("marks a conversation whose model list was capped by the server", () => {
const cappedCall = {
it("flags a conversation whose model list was capped by the server", async () => {
const user = userEvent.setup();
const cappedCall: Partial<LogEntry> = {
request_id: "req-capped",
model: "gpt-5.6",
session_id: "sess-2",
@ -393,13 +412,16 @@ describe("Model column", () => {
};
renderRows([logEntry(cappedCall)]);
expect(screen.getByText("claude-sonnet-5, gpt-5.6, ...")).toBeInTheDocument();
await user.hover(screen.getByText("+1"));
expect(await screen.findByText("More models not shown")).toBeInTheDocument();
});
it("keeps a single call's own model", () => {
it("keeps a single call's own model and shows no overflow marker", () => {
renderRows([logEntry({ request_id: "req-single", model: "gpt-5.6" })]);
expect(screen.getByText("gpt-5.6")).toBeInTheDocument();
expect(screen.queryByText(/^\+\d+$/)).not.toBeInTheDocument();
});
});

View file

@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { DataTableSortHeader } from "@/components/shared/DataTable";
import { CellTooltip, DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells";
import { Badge } from "@/components/ui/badge";
import i18n from "@/i18n/i18n";
import { copyToClipboard, getSpendString } from "@/utils/dataUtils";
@ -329,28 +330,42 @@ export const getRequestLogsTableColumns = (
enableSorting: true,
cell: ({ row }) => {
const log = row.original;
const provider = log.custom_llm_provider;
const sessionModels = log.session_models ?? [];
const modelNames = sessionModels.length > 0 ? sessionModels : [log.model ?? ""];
const modelLabel = log.session_models_truncated ? `${modelNames.join(", ")}, ...` : modelNames.join(", ");
const isSingleModel = modelNames.length === 1;
const latestModel = log.model || sessionModels[0] || "";
const otherModels = sessionModels.filter((model) => model !== latestModel);
const models = [latestModel, ...otherModels].filter((model) => model !== "");
if (models.length === 0) return <span className="text-muted-foreground">-</span>;
return (
<div className="flex items-center space-x-2">
{provider && isSingleModel && (
<img
src={getLogoUrl(log, provider)}
alt=""
className="w-4 h-4"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
)}
<div className="flex min-w-0 items-center">
<CellTooltip
content={modelLabel}
content={
<div className="flex max-w-[280px] flex-col gap-0.5">
{models.map((model) => (
<span key={model}>{model}</span>
))}
{log.session_models_truncated === true && (
<span className="text-muted-foreground">{t("logs.request.columns.moreModelsNotShown")}</span>
)}
</div>
}
trigger={
<span className={isSingleModel ? "max-w-[15ch] truncate block" : "min-w-0 truncate block"}>
{modelLabel}
<span className="inline-flex min-w-0 items-center gap-1.5">
{log.custom_llm_provider && (
<img
src={getLogoUrl(log, log.custom_llm_provider)}
alt=""
className="w-4 h-4 shrink-0"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
)}
<span className="max-w-[22ch] truncate">{latestModel}</span>
{otherModels.length > 0 && (
<Badge variant="outline" className="text-muted-foreground">
+{otherModels.length}
</Badge>
)}
</span>
}
/>

View file

@ -14887,6 +14887,7 @@
"keyHash": "Key Hash",
"keyAlias": "Key Alias",
"model": "Model",
"moreModelsNotShown": "More models not shown",
"tokens": "Tokens",
"internalUser": "Internal User",
"endUser": "End User",