mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
feat(ui): surface batch results on the logs page
This commit is contained in:
parent
1c14ded0e4
commit
2a2c49ad4c
9 changed files with 418 additions and 3 deletions
|
|
@ -113,6 +113,84 @@ describe("LogDetailContent", () => {
|
|||
expect(screen.getAllByText("$0.00200000").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("shows reasoning tokens in Metrics when the usage breakout carries them", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={createLogEntry({
|
||||
metadata: {
|
||||
status: "success",
|
||||
usage_object: { completion_tokens_details: { text_tokens: 32, reasoning_tokens: 224 } },
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Reasoning Tokens")).toBeInTheDocument();
|
||||
expect(screen.getByText("224")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the reasoning metric when the breakout is absent or zero", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={createLogEntry({
|
||||
metadata: {
|
||||
status: "success",
|
||||
usage_object: { completion_tokens_details: { text_tokens: 32, reasoning_tokens: 0 } },
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Reasoning Tokens")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Batch Results section", () => {
|
||||
const batchCostEntry = (metadata: Record<string, unknown>) =>
|
||||
createLogEntry({
|
||||
request_id: "batch_abc123_batch_cost",
|
||||
call_type: "aretrieve_batch",
|
||||
metadata: { status: "success", ...metadata },
|
||||
});
|
||||
|
||||
it("renders batch id, per-request outcome counts, and batch models for a batch cost row", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={batchCostEntry({
|
||||
batch_models: ["gemini-2.5-flash"],
|
||||
batch_successful_requests: 2,
|
||||
batch_failed_requests: 1,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const section = screen.getByText("Batch Results").closest('[data-slot="card"]') as HTMLElement;
|
||||
expect(within(section).getByText("batch_abc123")).toBeInTheDocument();
|
||||
expect(within(section).getByText("2")).toBeInTheDocument();
|
||||
expect(within(section).getByText("1")).toBeInTheDocument();
|
||||
expect(within(section).getByText("gemini-2.5-flash")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("still renders the batch id when a legacy row carries no counts", () => {
|
||||
render(<LogDetailContent logEntry={batchCostEntry({})} />);
|
||||
|
||||
expect(screen.getByText("Batch Results")).toBeInTheDocument();
|
||||
expect(screen.getByText("batch_abc123")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Successful Requests")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("never renders for a non-batch call type", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
logEntry={createLogEntry({
|
||||
metadata: { status: "success", batch_successful_requests: 2, batch_failed_requests: 1 },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Batch Results")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show Input Tokens and Output Tokens for anthropic_messages when uncached text_tokens exist", () => {
|
||||
render(
|
||||
<LogDetailContent
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
|
|||
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
|
||||
import GuardrailViewer from "../GuardrailViewer/GuardrailViewer";
|
||||
import EvalViewer from "../EvalViewer/EvalViewer";
|
||||
import {
|
||||
getBatchIdFromRequestId,
|
||||
getBatchModels,
|
||||
getBatchRequestCounts,
|
||||
getReasoningTokens,
|
||||
isBatchCallType,
|
||||
} from "../batchLogUtils";
|
||||
import { CostBreakdownViewer } from "../CostBreakdownViewer";
|
||||
import { ConfigInfoMessage } from "../ConfigInfoMessage";
|
||||
import { VectorStoreViewer } from "../VectorStoreViewer";
|
||||
|
|
@ -150,6 +157,9 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Batch Results */}
|
||||
{isBatchCallType(logEntry.call_type) && <BatchResultsSection logEntry={logEntry} metadata={metadata} />}
|
||||
|
||||
{/* Routing */}
|
||||
<RoutingDecisionCard decision={metadata?.routing_decision as RoutingDecision | undefined} />
|
||||
|
||||
|
|
@ -374,6 +384,53 @@ function MetricLabel({ label, tooltip, docsUrl }: { label: string; tooltip: stri
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate per-request outcomes for a batch cost row: batch id, success/failure counts
|
||||
* from the parsed output and error files, and the models the batch actually ran on.
|
||||
*/
|
||||
function BatchResultsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record<string, any> }) {
|
||||
const counts = getBatchRequestCounts(metadata);
|
||||
const batchId = getBatchIdFromRequestId(logEntry.request_id);
|
||||
const batchModels = getBatchModels(metadata);
|
||||
if (!counts && !batchId && !batchModels) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||
<Card size="sm" style={{ marginBottom: 0 }}>
|
||||
<CardHeader>
|
||||
<CardTitle>Batch Results</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DescriptionList>
|
||||
{batchId && (
|
||||
<DescriptionItem label="Batch ID">
|
||||
<TruncatedValue value={batchId} />
|
||||
</DescriptionItem>
|
||||
)}
|
||||
{counts && (
|
||||
<>
|
||||
<DescriptionItem label="Successful Requests">
|
||||
{formatNumberWithCommas(counts.successful)}
|
||||
</DescriptionItem>
|
||||
<DescriptionItem label="Failed Requests">
|
||||
{counts.failed > 0 ? (
|
||||
<Badge variant="secondary" className="bg-destructive/15 text-destructive">
|
||||
{formatNumberWithCommas(counts.failed)}
|
||||
</Badge>
|
||||
) : (
|
||||
formatNumberWithCommas(counts.failed)
|
||||
)}
|
||||
</DescriptionItem>
|
||||
</>
|
||||
)}
|
||||
{batchModels && <DescriptionItem label="Models">{batchModels.join(", ")}</DescriptionItem>}
|
||||
</DescriptionList>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record<string, any> }) {
|
||||
const completionStartTime = logEntry.completionStartTime;
|
||||
const ttftMs =
|
||||
|
|
@ -391,6 +448,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
|||
const uncachedInputTokens = getUncachedInputTextTokens(metadata);
|
||||
const showAnthropicMessagesInputOutput =
|
||||
logEntry.call_type === "anthropic_messages" && uncachedInputTokens !== undefined;
|
||||
const reasoningTokens = getReasoningTokens(metadata);
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||
|
|
@ -416,6 +474,9 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
|||
/>
|
||||
</DescriptionItem>
|
||||
)}
|
||||
{reasoningTokens !== undefined && reasoningTokens > 0 && (
|
||||
<DescriptionItem label="Reasoning Tokens">{formatNumberWithCommas(reasoningTokens)}</DescriptionItem>
|
||||
)}
|
||||
<DescriptionItem label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</DescriptionItem>
|
||||
<DescriptionItem label="Duration">
|
||||
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
|
||||
|
|
|
|||
|
|
@ -134,6 +134,59 @@ describe("Type column", () => {
|
|||
|
||||
expect(screen.getByText("MCP")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks a batch cost row with the Batch badge instead of LLM", () => {
|
||||
renderRows([logEntry({ request_id: "batch_1_batch_cost", call_type: "aretrieve_batch" })]);
|
||||
|
||||
expect(screen.getByText("Batch")).toBeInTheDocument();
|
||||
expect(screen.queryByText("LLM")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("batch rows", () => {
|
||||
const batchRow = (overrides: Partial<LogEntry>): LogEntry =>
|
||||
logEntry({
|
||||
request_id: "batch_abc123_batch_cost",
|
||||
call_type: "aretrieve_batch",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("rolls partial failures into the status badge instead of reporting blanket Success", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderRows([batchRow({ metadata: { batch_successful_requests: 2, batch_failed_requests: 1 } })]);
|
||||
|
||||
expect(screen.queryByText("Success")).not.toBeInTheDocument();
|
||||
await user.hover(screen.getByText("2/3 succeeded"));
|
||||
expect(await screen.findByText("1 of 3 batch requests failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the Success badge when every batch request succeeded", () => {
|
||||
renderRows([batchRow({ metadata: { batch_successful_requests: 3, batch_failed_requests: 0 } })]);
|
||||
|
||||
expect(screen.getByText("Success")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the Failure badge when the batch row itself failed, whatever the counts say", () => {
|
||||
renderRows([batchRow({ metadata: { status: "failure", batch_successful_requests: 2, batch_failed_requests: 1 } })]);
|
||||
|
||||
expect(screen.getByText("Failure")).toBeInTheDocument();
|
||||
expect(screen.queryByText("2/3 succeeded")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the provider batch id, not the synthetic _batch_cost request id", () => {
|
||||
renderRows([batchRow({ metadata: { batch_successful_requests: 1, batch_failed_requests: 0 } })]);
|
||||
|
||||
expect(screen.getByText("batch_abc123")).toBeInTheDocument();
|
||||
expect(screen.queryByText("batch_abc123_batch_cost")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("batch cost")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("leaves ordinary request ids untouched", () => {
|
||||
renderRows([logEntry({ request_id: "chatcmpl-42" })]);
|
||||
|
||||
expect(screen.getByText("chatcmpl-42")).toBeInTheDocument();
|
||||
expect(screen.queryByText("batch cost")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Model column", () => {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import { CellTooltip, DateCell, IdCell, MoneyCell, StatusBadge } from "@/compone
|
|||
import { getSpendString } from "@/utils/dataUtils";
|
||||
|
||||
import { getProviderLogoAndName } from "../provider_info_helpers";
|
||||
import { getBatchIdFromRequestId, getBatchRequestCounts, isBatchCallType } from "./batchLogUtils";
|
||||
import type { LogEntry } from "./columns";
|
||||
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants";
|
||||
import { AgentBadge, AgentIcon, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges";
|
||||
import { AgentBadge, AgentIcon, BatchBadge, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges";
|
||||
|
||||
export interface RequestLogsTableColumnsDeps {
|
||||
onKeyHashClick: (keyHash: string) => void;
|
||||
|
|
@ -66,6 +67,7 @@ export const getRequestLogsTableColumns = ({
|
|||
if (sessionCount <= 1) {
|
||||
if (isMcp) return <McpBadge />;
|
||||
if (isAgent) return <AgentBadge />;
|
||||
if (isBatchCallType(log.call_type)) return <BatchBadge />;
|
||||
return <LlmBadge />;
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +108,17 @@ export const getRequestLogsTableColumns = ({
|
|||
cell: ({ row }) => {
|
||||
const status = readMetaString(row.original.metadata, "status") ?? "Success";
|
||||
const isSuccess = status.toLowerCase() !== "failure";
|
||||
const batchCounts = isSuccess ? getBatchRequestCounts(row.original.metadata) : undefined;
|
||||
if (batchCounts && batchCounts.failed > 0) {
|
||||
const total = batchCounts.successful + batchCounts.failed;
|
||||
return (
|
||||
<StatusBadge
|
||||
tone="warning"
|
||||
label={`${batchCounts.successful}/${total} succeeded`}
|
||||
tooltip={`${batchCounts.failed} of ${total} batch requests failed`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <StatusBadge tone={isSuccess ? "success" : "error"} label={isSuccess ? "Success" : "Failure"} />;
|
||||
},
|
||||
},
|
||||
|
|
@ -122,7 +135,19 @@ export const getRequestLogsTableColumns = ({
|
|||
accessorKey: "request_id",
|
||||
header: "Request ID",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <IdCell value={row.original.request_id} variant="plain" />,
|
||||
cell: ({ row }) => {
|
||||
const log = row.original;
|
||||
const batchId = isBatchCallType(log.call_type) ? getBatchIdFromRequestId(log.request_id) : undefined;
|
||||
if (batchId) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<IdCell value={batchId} variant="plain" copyable tooltip={`Batch ${batchId} (row: ${log.request_id})`} />
|
||||
<span className="text-[10px] text-muted-foreground">batch cost</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <IdCell value={log.request_id} variant="plain" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "spend",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LlmBadge, McpBadge, AgentBadge } from "./TypeBadges";
|
||||
import { LlmBadge, McpBadge, AgentBadge, BatchBadge } from "./TypeBadges";
|
||||
|
||||
describe("TypeBadges", () => {
|
||||
describe("LlmBadge", () => {
|
||||
|
|
@ -43,4 +43,16 @@ describe("TypeBadges", () => {
|
|||
expect(screen.getByText("12")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("BatchBadge", () => {
|
||||
it("should render with default 'Batch' text when no count is provided", () => {
|
||||
render(<BatchBadge />);
|
||||
expect(screen.getByText("Batch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the count when provided", () => {
|
||||
render(<BatchBadge count={4} />);
|
||||
expect(screen.getByText("4")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,6 +57,25 @@ export const AgentIcon = ({ size = 12 }: { size?: number }) => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
/** Stacked-layers icon for Batch API call types (Lucide Layers-style). */
|
||||
export const LayersIcon = ({ size = 12 }: { size?: number }) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="shrink-0"
|
||||
>
|
||||
<path d="M12 2 2 7l10 5 10-5-10-5z" />
|
||||
<path d="m2 17 10 5 10-5" />
|
||||
<path d="m2 12 10 5 10-5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LlmBadge = ({ count }: { count?: number }) => (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-info/10 text-info border border-info/20 rounded-full text-[11px] font-medium whitespace-nowrap">
|
||||
<SparkleIcon />
|
||||
|
|
@ -77,3 +96,10 @@ export const AgentBadge = ({ count }: { count?: number }) => (
|
|||
{count != null ? count : "Agent"}
|
||||
</span>
|
||||
);
|
||||
|
||||
export const BatchBadge = ({ count }: { count?: number }) => (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-teal-50 text-teal-700 border border-teal-200 rounded-full text-[11px] font-medium whitespace-nowrap dark:bg-teal-950 dark:text-teal-300 dark:border-teal-800">
|
||||
<LayersIcon />
|
||||
{count != null ? count : "Batch"}
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getBatchIdFromRequestId,
|
||||
getBatchModels,
|
||||
getBatchRequestCounts,
|
||||
getReasoningTokens,
|
||||
isBatchCallType,
|
||||
} from "./batchLogUtils";
|
||||
|
||||
/** Metadata shape the batch cost poller writes on an aretrieve_batch spend row. */
|
||||
const batchCostMetadata = {
|
||||
batch_models: ["gemini-2.5-flash"],
|
||||
batch_successful_requests: 2,
|
||||
batch_failed_requests: 1,
|
||||
usage_object: {
|
||||
total_tokens: 270,
|
||||
prompt_tokens: 14,
|
||||
completion_tokens: 256,
|
||||
completion_tokens_details: { text_tokens: 32, reasoning_tokens: 224 },
|
||||
},
|
||||
};
|
||||
|
||||
describe("isBatchCallType", () => {
|
||||
it("recognizes the poller's aretrieve_batch and the create call types", () => {
|
||||
for (const callType of ["aretrieve_batch", "retrieve_batch", "acreate_batch", "create_batch"]) {
|
||||
expect(isBatchCallType(callType)).toBe(true);
|
||||
}
|
||||
expect(isBatchCallType("acompletion")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBatchRequestCounts", () => {
|
||||
it("reads both counts off a batch cost row", () => {
|
||||
expect(getBatchRequestCounts(batchCostMetadata)).toEqual({ successful: 2, failed: 1 });
|
||||
});
|
||||
|
||||
it("returns undefined for a non-batch row and for null counts, so no rollup renders", () => {
|
||||
expect(getBatchRequestCounts({ status: "success" })).toBeUndefined();
|
||||
expect(getBatchRequestCounts({ batch_successful_requests: null, batch_failed_requests: null })).toBeUndefined();
|
||||
expect(getBatchRequestCounts(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats a lone present count as the other being 0, for rows logged mid-rollout", () => {
|
||||
expect(getBatchRequestCounts({ batch_successful_requests: 3 })).toEqual({ successful: 3, failed: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBatchIdFromRequestId", () => {
|
||||
it("strips the poller's synthetic _batch_cost suffix down to the provider batch id", () => {
|
||||
expect(getBatchIdFromRequestId("batch_abc123_batch_cost")).toBe("batch_abc123");
|
||||
});
|
||||
|
||||
it("returns undefined for ordinary request ids and a bare suffix", () => {
|
||||
expect(getBatchIdFromRequestId("chatcmpl-123")).toBeUndefined();
|
||||
expect(getBatchIdFromRequestId("_batch_cost")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBatchModels", () => {
|
||||
it("returns the model list from metadata.batch_models", () => {
|
||||
expect(getBatchModels(batchCostMetadata)).toEqual(["gemini-2.5-flash"]);
|
||||
});
|
||||
|
||||
it("returns undefined when absent, null, or empty", () => {
|
||||
expect(getBatchModels({})).toBeUndefined();
|
||||
expect(getBatchModels({ batch_models: null })).toBeUndefined();
|
||||
expect(getBatchModels({ batch_models: [] })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReasoningTokens", () => {
|
||||
it("reads reasoning tokens from usage_object on a batch cost row", () => {
|
||||
expect(getReasoningTokens(batchCostMetadata)).toBe(224);
|
||||
});
|
||||
|
||||
it("prefers additional_usage_values, which per-request rows carry", () => {
|
||||
const metadata = {
|
||||
additional_usage_values: { completion_tokens_details: { reasoning_tokens: 40 } },
|
||||
usage_object: { completion_tokens_details: { reasoning_tokens: 999 } },
|
||||
};
|
||||
expect(getReasoningTokens(metadata)).toBe(40);
|
||||
});
|
||||
|
||||
it("returns undefined when the breakout is null or missing", () => {
|
||||
expect(getReasoningTokens({ usage_object: { completion_tokens_details: null } })).toBeUndefined();
|
||||
expect(getReasoningTokens({})).toBeUndefined();
|
||||
expect(getReasoningTokens(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Helpers for reading batch-specific fields off a spend log row.
|
||||
*
|
||||
* The proxy's batch cost poller (CheckBatchCost) writes one spend log per completed batch
|
||||
* with request_id "<batch_id>_batch_cost" and call_type "aretrieve_batch", carrying
|
||||
* batch_models / batch_successful_requests / batch_failed_requests in metadata
|
||||
* (see litellm/proxy/spend_tracking/spend_tracking_utils.py).
|
||||
*/
|
||||
|
||||
import { BATCH_CALL_TYPES } from "./constants";
|
||||
|
||||
export const BATCH_COST_REQUEST_ID_SUFFIX = "_batch_cost";
|
||||
|
||||
export interface BatchRequestCounts {
|
||||
successful: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export const isBatchCallType = (callType: string): boolean => BATCH_CALL_TYPES.includes(callType);
|
||||
|
||||
const readMetaNumber = (metadata: Record<string, unknown> | undefined, key: string): number | undefined => {
|
||||
const value = metadata?.[key];
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-request outcome counts of a batch cost row. Undefined when the row carries neither
|
||||
* count (a non-batch row, or a batch logged before counts were tracked).
|
||||
*/
|
||||
export const getBatchRequestCounts = (
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
): BatchRequestCounts | undefined => {
|
||||
const successful = readMetaNumber(metadata, "batch_successful_requests");
|
||||
const failed = readMetaNumber(metadata, "batch_failed_requests");
|
||||
if (successful === undefined && failed === undefined) return undefined;
|
||||
return { successful: successful ?? 0, failed: failed ?? 0 };
|
||||
};
|
||||
|
||||
/** The provider batch id behind a poller-written "<batch_id>_batch_cost" spend row. */
|
||||
export const getBatchIdFromRequestId = (requestId: string): string | undefined =>
|
||||
requestId.endsWith(BATCH_COST_REQUEST_ID_SUFFIX) && requestId.length > BATCH_COST_REQUEST_ID_SUFFIX.length
|
||||
? requestId.slice(0, -BATCH_COST_REQUEST_ID_SUFFIX.length)
|
||||
: undefined;
|
||||
|
||||
/** The models the batch's requests actually ran on, from metadata.batch_models. */
|
||||
export const getBatchModels = (metadata: Record<string, unknown> | undefined): string[] | undefined => {
|
||||
const models = metadata?.["batch_models"];
|
||||
if (!Array.isArray(models)) return undefined;
|
||||
const names = models.filter((model): model is string => typeof model === "string" && model !== "");
|
||||
return names.length > 0 ? names : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reasoning tokens aggregated across the row's completion usage. Read from the same two
|
||||
* metadata containers the drawer already uses for prompt-token details: per-request rows
|
||||
* carry additional_usage_values, batch cost rows carry usage_object.
|
||||
*/
|
||||
export const getReasoningTokens = (metadata: Record<string, unknown> | undefined): number | undefined => {
|
||||
const readDetails = (container: unknown): number | undefined => {
|
||||
if (typeof container !== "object" || container === null) return undefined;
|
||||
const details = (container as Record<string, unknown>)["completion_tokens_details"];
|
||||
if (typeof details !== "object" || details === null) return undefined;
|
||||
const reasoning = (details as Record<string, unknown>)["reasoning_tokens"];
|
||||
return typeof reasoning === "number" && Number.isFinite(reasoning) ? reasoning : undefined;
|
||||
};
|
||||
return readDetails(metadata?.["additional_usage_values"]) ?? readDetails(metadata?.["usage_object"]);
|
||||
};
|
||||
|
|
@ -21,6 +21,9 @@ export const MCP_CALL_TYPES = ["call_mcp_tool", "list_mcp_tools"];
|
|||
/** Call types that represent agent/A2A requests (e.g. asend_message). */
|
||||
export const AGENT_CALL_TYPES = ["asend_message"];
|
||||
|
||||
/** Call types that represent Batch API operations (creation and retrieval, sync and async). */
|
||||
export const BATCH_CALL_TYPES = ["acreate_batch", "create_batch", "aretrieve_batch", "retrieve_batch"];
|
||||
|
||||
export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [
|
||||
{ label: "Last Minute", value: 1, unit: "minutes" },
|
||||
{ label: "Last 15 Minutes", value: 15, unit: "minutes" },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue