feat: enrich assistant request telemetry

This commit is contained in:
ajnart committed 2026-08-02 08:51:48 +02:00
1 parent d562706c8b
commit 04520cf67c
10 files changed
+798 -39

No files matched your search

+100 -22
View File
@@ -119,21 +119,33 @@ const createProviderTelemetryExtractor = (): MetadataExtractor => {
if (!body) return;
const usage = asRecord(body.usage);
const costDetails = asRecord(usage?.cost_details);
const routerMetadata = asRecord(body.openrouter_metadata);
const endpoints = asRecord(routerMetadata?.endpoints);
const selectedEndpoint = Array.isArray(endpoints?.available)
? endpoints.available.map(asRecord).find((endpoint) => endpoint?.selected === true)
: undefined;
const generationId = typeof body.id === "string" ? body.id : undefined;
const routedProvider =
typeof body.provider === "string"
? body.provider
: typeof usage?.provider === "string"
? usage.provider
: undefined;
: typeof selectedEndpoint?.provider === "string"
? selectedEndpoint.provider
: undefined;
const cost = asFiniteNumber(usage?.cost);
const upstreamCost = asFiniteNumber(costDetails?.upstream_inference_cost);
const routerAttempt = asFiniteNumber(routerMetadata?.attempt);
metadata = {
...metadata,
...(generationId ? { generationId } : {}),
...(routedProvider ? { routedProvider } : {}),
...(cost !== undefined ? { cost } : {}),
...(upstreamCost !== undefined ? { upstreamCost } : {}),
...(routerAttempt !== undefined ? { fallbackCount: Math.max(routerAttempt - 1, 0) } : {}),
...(typeof routerMetadata?.strategy === "string" ? { routerStrategy: routerMetadata.strategy } : {}),
...(typeof routerMetadata?.region === "string" ? { routerRegion: routerMetadata.region } : {}),
...(typeof routerMetadata?.is_byok === "boolean" ? { isByok: routerMetadata.is_byok ? 1 : 0 } : {}),
};
};
const build = () => (Object.keys(metadata).length > 0 ? { homarrTelemetry: metadata } : undefined);
@@ -252,6 +264,16 @@ const toUsageMetadata = (usage: {
: {}),
});
const sumCompleteStepMetric = (
steps: AssistantRequestStep[],
getValue: (step: AssistantRequestStep) => number | undefined,
) => {
if (steps.length === 0) return undefined;
const values = steps.map(getValue);
if (values.some((value) => value === undefined)) return undefined;
return (values as number[]).reduce((sum, value) => sum + value, 0);
};
const safeToolError = (error: unknown) => {
if (error instanceof Error && "code" in error) {
switch ((error as Error & { code: string }).code) {
@@ -428,14 +450,20 @@ export async function POST(request: Request) {
: {};
const providerHeaders = {
...(configuration.provider === "openrouter"
? { "HTTP-Referer": "https://homarr.dev", "X-Title": "Homarr Assistant" }
? {
"HTTP-Referer": "https://homarr.dev",
"X-Title": "Homarr Assistant",
"X-OpenRouter-Metadata": "enabled",
}
: {}),
...customHeaders,
};
const providerApiKey = configuration.encryptedApiKey ? decryptSecret(configuration.encryptedApiKey) : undefined;
const providerName = `homarr-${configuration.provider}`;
const provider = createOpenAICompatible({
name: `homarr-${configuration.provider}`,
name: providerName,
baseURL: configuration.baseUrl,
apiKey: configuration.encryptedApiKey ? decryptSecret(configuration.encryptedApiKey) : undefined,
apiKey: providerApiKey,
headers: providerHeaders,
includeUsage: true,
metadataExtractor: createProviderTelemetryExtractor(),
@@ -464,6 +492,8 @@ export async function POST(request: Request) {
timeout: { totalMs: 55_000, stepMs: 30_000, toolMs: 30_000 },
maxRetries: 2,
reasoning: parsed.data.reasoning === "auto" ? undefined : parsed.data.reasoning,
providerOptions:
configuration.provider === "openrouter" ? { [providerName]: { usage: { include: true } } } : undefined,
toolApproval,
experimental_toolApprovalSecret: getToolApprovalSecret(),
onChunk: ({ chunk }) => {
@@ -484,17 +514,13 @@ export async function POST(request: Request) {
},
onStepFinish: ({ performance, providerMetadata, response, usage }) => {
const telemetry = asRecord(providerMetadata?.homarrTelemetry);
const cost = asFiniteNumber(telemetry?.cost);
const stepUpstreamCost = asFiniteNumber(telemetry?.upstreamCost);
if (cost !== undefined) {
reportedCost += cost;
hasReportedCost = true;
}
if (stepUpstreamCost !== undefined) {
upstreamCost += stepUpstreamCost;
hasUpstreamCost = true;
}
requestSteps.push({
const generationId =
typeof telemetry?.generationId === "string"
? telemetry.generationId
: typeof response.id === "string"
? response.id
: undefined;
const step: AssistantRequestStep = {
index: requestSteps.length + 1,
durationMs: performance.stepTimeMs,
modelDurationMs: performance.responseTimeMs,
@@ -507,11 +533,28 @@ export async function POST(request: Request) {
: {}),
...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
...(cost !== undefined ? { cost } : {}),
...(stepUpstreamCost !== undefined ? { upstreamCost: stepUpstreamCost } : {}),
...(typeof response.id === "string" ? { generationId: response.id } : {}),
...(asFiniteNumber(telemetry?.cost) !== undefined ? { cost: asFiniteNumber(telemetry?.cost) } : {}),
...(asFiniteNumber(telemetry?.upstreamCost) !== undefined
? { upstreamCost: asFiniteNumber(telemetry?.upstreamCost) }
: {}),
...(generationId ? { generationId } : {}),
...(typeof telemetry?.routedProvider === "string" ? { routedProvider: telemetry.routedProvider } : {}),
});
...(asFiniteNumber(telemetry?.fallbackCount) !== undefined
? { fallbackCount: asFiniteNumber(telemetry?.fallbackCount) }
: {}),
...(typeof telemetry?.routerStrategy === "string" ? { routerStrategy: telemetry.routerStrategy } : {}),
...(typeof telemetry?.routerRegion === "string" ? { routerRegion: telemetry.routerRegion } : {}),
...(telemetry?.isByok === 0 || telemetry?.isByok === 1 ? { isByok: telemetry.isByok === 1 } : {}),
};
requestSteps.push(step);
if (step.cost !== undefined) {
reportedCost += step.cost;
hasReportedCost = true;
}
if (step.upstreamCost !== undefined) {
upstreamCost += step.upstreamCost;
hasUpstreamCost = true;
}
},
onError: ({ error }) => {
logger.error("Assistant response stream failed", {
@@ -552,8 +595,29 @@ export async function POST(request: Request) {
}
const completedAt = Date.now();
const usage = toUsageMetadata(part.totalUsage);
const contextUsed = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
const aiUsage = toUsageMetadata(part.totalUsage);
const providerInputTokens = sumCompleteStepMetric(requestSteps, (step) => step.inputTokens);
const providerOutputTokens = sumCompleteStepMetric(requestSteps, (step) => step.outputTokens);
const providerCachedInputTokens = sumCompleteStepMetric(requestSteps, (step) => step.cachedInputTokens);
const providerReasoningTokens = sumCompleteStepMetric(requestSteps, (step) => step.reasoningTokens);
const inputTokens = providerInputTokens ?? aiUsage.inputTokens;
const outputTokens = providerOutputTokens ?? aiUsage.outputTokens;
const usage: AssistantUsage = {
...aiUsage,
...(inputTokens !== undefined ? { inputTokens } : {}),
...(outputTokens !== undefined ? { outputTokens } : {}),
...(inputTokens !== undefined && outputTokens !== undefined
? { totalTokens: inputTokens + outputTokens }
: {}),
...(providerCachedInputTokens !== undefined ? { cachedInputTokens: providerCachedInputTokens } : {}),
...(providerReasoningTokens !== undefined ? { reasoningTokens: providerReasoningTokens } : {}),
};
const latestStep = requestSteps.at(-1);
const latestContextUsed =
latestStep?.inputTokens !== undefined && latestStep.outputTokens !== undefined
? latestStep.inputTokens + latestStep.outputTokens
: undefined;
const contextUsed = latestContextUsed ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
const promptPrice = asFiniteNumber(selectedModel?.promptPrice);
const completionPrice = asFiniteNumber(selectedModel?.completionPrice);
const estimatedCost =
@@ -564,6 +628,16 @@ export async function POST(request: Request) {
const generatedSeconds = requestSteps.reduce((sum, step) => sum + step.modelDurationMs, 0) / 1000;
const outputTokensPerSecond =
usage.outputTokens !== undefined && generatedSeconds > 0 ? usage.outputTokens / generatedSeconds : undefined;
const providerGenerationTimeMs = sumCompleteStepMetric(requestSteps, (step) => step.generationTimeMs);
const providerGeneratedTokens = sumCompleteStepMetric(requestSteps, (step) => step.outputTokens);
const providerOutputTokensPerSecond =
providerGenerationTimeMs !== undefined &&
providerGenerationTimeMs > 0 &&
providerGeneratedTokens !== undefined
? providerGeneratedTokens / (providerGenerationTimeMs / 1000)
: undefined;
const cacheDiscount = sumCompleteStepMetric(requestSteps, (step) => step.cacheDiscount);
const fallbackCount = sumCompleteStepMetric(requestSteps, (step) => step.fallbackCount);
return {
usage,
@@ -574,13 +648,17 @@ export async function POST(request: Request) {
durationMs: completedAt - requestStartedAt,
...(firstOutputAt !== undefined ? { timeToFirstOutputMs: firstOutputAt - requestStartedAt } : {}),
...(outputTokensPerSecond !== undefined ? { outputTokensPerSecond } : {}),
...(providerOutputTokensPerSecond !== undefined ? { providerOutputTokensPerSecond } : {}),
...(providerGenerationTimeMs !== undefined ? { generationTimeMs: providerGenerationTimeMs } : {}),
...(contextUsed > 0 ? { contextUsed } : {}),
...(selectedModel?.contextLength && contextUsed > 0
? { contextUtilization: Math.min(contextUsed / selectedModel.contextLength, 1) }
: {}),
...(cost !== undefined ? { cost, costType: hasReportedCost ? "reported" : "estimated" } : {}),
...(hasUpstreamCost ? { upstreamCost } : {}),
finishReason: part.finishReason,
...(cacheDiscount !== undefined ? { cacheDiscount } : {}),
...(fallbackCount !== undefined ? { fallbackCount } : {}),
finishReason: latestStep?.finishReason ?? part.finishReason,
},
},
};
@@ -19,7 +19,35 @@ describe("getAssistantTelemetry", () => {
contextUtilization: 0.015625,
cost: 0.001,
costType: "reported",
steps: [{ index: 1, durationMs: 900, modelDurationMs: 800, toolDurationMs: 100 }],
providerOutputTokensPerSecond: 37.9,
generationTimeMs: 1451,
cacheDiscount: 0.00003,
fallbackCount: 1,
steps: [
{
index: 1,
durationMs: 900,
modelDurationMs: 800,
toolDurationMs: 100,
providerLatencyMs: 964,
generationTimeMs: 1451,
providerOutputTokensPerSecond: 37.9,
inputTokens: 12_777,
outputTokens: 55,
normalizedInputTokens: 12_799,
normalizedOutputTokens: 62,
cachedInputTokens: 2048,
reasoningTokens: 7,
cacheDiscount: 0.00003,
fallbackCount: 1,
routedProvider: "Alibaba",
serviceTier: "default",
dataRegion: "US",
isByok: false,
streamed: true,
cancelled: false,
},
],
},
},
}),
@@ -29,6 +57,19 @@ describe("getAssistantTelemetry", () => {
durationMs: 1000,
contextLength: 128_000,
costType: "reported",
providerOutputTokensPerSecond: 37.9,
generationTimeMs: 1451,
cacheDiscount: 0.00003,
fallbackCount: 1,
steps: [
expect.objectContaining({
providerLatencyMs: 964,
inputTokens: 12_777,
cachedInputTokens: 2048,
routedProvider: "Alibaba",
serviceTier: "default",
}),
],
});
});
@@ -16,12 +16,32 @@ export type AssistantRequestStep = {
toolDurationMs: number;
timeToFirstOutputMs?: number;
outputTokensPerSecond?: number;
providerLatencyMs?: number;
generationTimeMs?: number;
moderationLatencyMs?: number;
providerOutputTokensPerSecond?: number;
inputTokens?: number;
outputTokens?: number;
normalizedInputTokens?: number;
normalizedOutputTokens?: number;
cachedInputTokens?: number;
reasoningTokens?: number;
cost?: number;
upstreamCost?: number;
cacheDiscount?: number;
generationId?: string;
routedProvider?: string;
finishReason?: string;
nativeFinishReason?: string;
serviceTier?: string;
dataRegion?: string;
routerStrategy?: string;
routerRegion?: string;
fallbackCount?: number;
fallbackLatencyMs?: number;
isByok?: boolean;
streamed?: boolean;
cancelled?: boolean;
};
export type AssistantRequestTelemetry = {
@@ -33,11 +53,16 @@ export type AssistantRequestTelemetry = {
durationMs?: number;
timeToFirstOutputMs?: number;
outputTokensPerSecond?: number;
providerOutputTokensPerSecond?: number;
generationTimeMs?: number;
contextLength?: number;
contextUsed?: number;
contextUtilization?: number;
cost?: number;
upstreamCost?: number;
cacheDiscount?: number;
fallbackCount?: number;
fallbackLatencyMs?: number;
costType?: "reported" | "estimated";
finishReason?: string;
steps: AssistantRequestStep[];
@@ -124,20 +149,64 @@ export const getAssistantTelemetry = (metadata: unknown): AssistantRequestTeleme
...(getFiniteNonNegativeNumber(candidate.outputTokensPerSecond) !== undefined
? { outputTokensPerSecond: getFiniteNonNegativeNumber(candidate.outputTokensPerSecond) }
: {}),
...(getFiniteNonNegativeNumber(candidate.providerLatencyMs) !== undefined
? { providerLatencyMs: getFiniteNonNegativeNumber(candidate.providerLatencyMs) }
: {}),
...(getFiniteNonNegativeNumber(candidate.generationTimeMs) !== undefined
? { generationTimeMs: getFiniteNonNegativeNumber(candidate.generationTimeMs) }
: {}),
...(getFiniteNonNegativeNumber(candidate.moderationLatencyMs) !== undefined
? { moderationLatencyMs: getFiniteNonNegativeNumber(candidate.moderationLatencyMs) }
: {}),
...(getFiniteNonNegativeNumber(candidate.providerOutputTokensPerSecond) !== undefined
? { providerOutputTokensPerSecond: getFiniteNonNegativeNumber(candidate.providerOutputTokensPerSecond) }
: {}),
...(getFiniteNonNegativeNumber(candidate.inputTokens) !== undefined
? { inputTokens: getFiniteNonNegativeNumber(candidate.inputTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.outputTokens) !== undefined
? { outputTokens: getFiniteNonNegativeNumber(candidate.outputTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.normalizedInputTokens) !== undefined
? { normalizedInputTokens: getFiniteNonNegativeNumber(candidate.normalizedInputTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.normalizedOutputTokens) !== undefined
? { normalizedOutputTokens: getFiniteNonNegativeNumber(candidate.normalizedOutputTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.cachedInputTokens) !== undefined
? { cachedInputTokens: getFiniteNonNegativeNumber(candidate.cachedInputTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.reasoningTokens) !== undefined
? { reasoningTokens: getFiniteNonNegativeNumber(candidate.reasoningTokens) }
: {}),
...(getFiniteNonNegativeNumber(candidate.cost) !== undefined
? { cost: getFiniteNonNegativeNumber(candidate.cost) }
: {}),
...(getFiniteNonNegativeNumber(candidate.upstreamCost) !== undefined
? { upstreamCost: getFiniteNonNegativeNumber(candidate.upstreamCost) }
: {}),
...(getFiniteNonNegativeNumber(candidate.cacheDiscount) !== undefined
? { cacheDiscount: getFiniteNonNegativeNumber(candidate.cacheDiscount) }
: {}),
...(typeof candidate.generationId === "string" ? { generationId: candidate.generationId } : {}),
...(typeof candidate.routedProvider === "string" ? { routedProvider: candidate.routedProvider } : {}),
...(typeof candidate.finishReason === "string" ? { finishReason: candidate.finishReason } : {}),
...(typeof candidate.nativeFinishReason === "string"
? { nativeFinishReason: candidate.nativeFinishReason }
: {}),
...(typeof candidate.serviceTier === "string" ? { serviceTier: candidate.serviceTier } : {}),
...(typeof candidate.dataRegion === "string" ? { dataRegion: candidate.dataRegion } : {}),
...(typeof candidate.routerStrategy === "string" ? { routerStrategy: candidate.routerStrategy } : {}),
...(typeof candidate.routerRegion === "string" ? { routerRegion: candidate.routerRegion } : {}),
...(getFiniteNonNegativeNumber(candidate.fallbackCount) !== undefined
? { fallbackCount: getFiniteNonNegativeNumber(candidate.fallbackCount) }
: {}),
...(getFiniteNonNegativeNumber(candidate.fallbackLatencyMs) !== undefined
? { fallbackLatencyMs: getFiniteNonNegativeNumber(candidate.fallbackLatencyMs) }
: {}),
...(typeof candidate.isByok === "boolean" ? { isByok: candidate.isByok } : {}),
...(typeof candidate.streamed === "boolean" ? { streamed: candidate.streamed } : {}),
...(typeof candidate.cancelled === "boolean" ? { cancelled: candidate.cancelled } : {}),
},
];
})
@@ -158,6 +227,12 @@ export const getAssistantTelemetry = (metadata: unknown): AssistantRequestTeleme
...(getFiniteNonNegativeNumber(value.outputTokensPerSecond) !== undefined
? { outputTokensPerSecond: getFiniteNonNegativeNumber(value.outputTokensPerSecond) }
: {}),
...(getFiniteNonNegativeNumber(value.providerOutputTokensPerSecond) !== undefined
? { providerOutputTokensPerSecond: getFiniteNonNegativeNumber(value.providerOutputTokensPerSecond) }
: {}),
...(getFiniteNonNegativeNumber(value.generationTimeMs) !== undefined
? { generationTimeMs: getFiniteNonNegativeNumber(value.generationTimeMs) }
: {}),
...(getFiniteNonNegativeNumber(value.contextLength) !== undefined
? { contextLength: getFiniteNonNegativeNumber(value.contextLength) }
: {}),
@@ -171,6 +246,15 @@ export const getAssistantTelemetry = (metadata: unknown): AssistantRequestTeleme
...(getFiniteNonNegativeNumber(value.upstreamCost) !== undefined
? { upstreamCost: getFiniteNonNegativeNumber(value.upstreamCost) }
: {}),
...(getFiniteNonNegativeNumber(value.cacheDiscount) !== undefined
? { cacheDiscount: getFiniteNonNegativeNumber(value.cacheDiscount) }
: {}),
...(getFiniteNonNegativeNumber(value.fallbackCount) !== undefined
? { fallbackCount: getFiniteNonNegativeNumber(value.fallbackCount) }
: {}),
...(getFiniteNonNegativeNumber(value.fallbackLatencyMs) !== undefined
? { fallbackLatencyMs: getFiniteNonNegativeNumber(value.fallbackLatencyMs) }
: {}),
...(value.costType === "reported" || value.costType === "estimated" ? { costType: value.costType } : {}),
...(typeof value.finishReason === "string" ? { finishReason: value.finishReason } : {}),
};
@@ -614,8 +614,8 @@ const formatDuration = (milliseconds: number) =>
const formatCost = (cost: number) => {
if (cost === 0) return "$0";
if (cost < 0.0001) return `<$0.0001`;
return `$${cost.toFixed(cost < 0.01 ? 4 : 3)}`;
if (cost < 0.00001) return `<$0.00001`;
return `$${cost.toFixed(cost < 0.001 ? 5 : cost < 0.01 ? 4 : 3)}`;
};
const getContextColor = (percentage: number) => {
@@ -628,9 +628,101 @@ const RequestTelemetry = () => {
const t = useScopedI18n("common.assistant");
const [opened, setOpened] = useState(false);
const metadata = useAuiState((state) => state.message.metadata);
const telemetry = getAssistantTelemetry(metadata);
const usage = getAssistantUsage(metadata);
if (!telemetry) return null;
const messageId = useAuiState((state) => state.message.id);
const threadId = useAuiState((state) => state.threadListItem.remoteId);
const persistedTelemetry = getAssistantTelemetry(metadata);
const persistedUsage = getAssistantUsage(metadata);
const generationQuery = clientApi.assistant.getGenerationTelemetry.useQuery(
{ threadId: threadId ?? "", messageId },
{
enabled: opened && persistedTelemetry?.provider === "openrouter" && Boolean(threadId),
refetchInterval: (query) =>
query.state.data?.complete === false && query.state.dataUpdateCount < 6 ? 1500 : false,
retry: 2,
staleTime: (query) => (query.state.data?.complete ? 5 * 60_000 : 0),
},
);
if (!persistedTelemetry) return null;
const generationById = new Map(
generationQuery.data?.generations.flatMap((generation) =>
generation.generationId ? [[generation.generationId, generation] as const] : [],
) ?? [],
);
const steps = persistedTelemetry.steps.map((step) => ({
...step,
...(step.generationId ? generationById.get(step.generationId) : undefined),
}));
const sumCompleteMetric = (getValue: (step: (typeof steps)[number]) => number | undefined) => {
if (steps.length === 0) return undefined;
const values = steps.map(getValue);
return values.some((value) => value === undefined)
? undefined
: (values as number[]).reduce((sum, value) => sum + value, 0);
};
const providerInputTokens = sumCompleteMetric((step) => step.inputTokens);
const providerOutputTokens = sumCompleteMetric((step) => step.outputTokens);
const providerGenerationTimeMs = sumCompleteMetric((step) => step.generationTimeMs);
const providerOutputTokensPerSecond =
providerOutputTokens !== undefined && providerGenerationTimeMs !== undefined && providerGenerationTimeMs > 0
? providerOutputTokens / (providerGenerationTimeMs / 1000)
: undefined;
const latestStep = steps.at(-1);
const providerContextUsed =
latestStep?.inputTokens !== undefined && latestStep.outputTokens !== undefined
? latestStep.inputTokens + latestStep.outputTokens
: undefined;
const usage = {
...persistedUsage,
...(generationQuery.data?.complete && providerInputTokens !== undefined
? { inputTokens: providerInputTokens }
: {}),
...(generationQuery.data?.complete && providerOutputTokens !== undefined
? { outputTokens: providerOutputTokens }
: {}),
...(generationQuery.data?.complete && providerInputTokens !== undefined && providerOutputTokens !== undefined
? { totalTokens: providerInputTokens + providerOutputTokens }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.cachedInputTokens) !== undefined
? { cachedInputTokens: sumCompleteMetric((step) => step.cachedInputTokens) }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.reasoningTokens) !== undefined
? { reasoningTokens: sumCompleteMetric((step) => step.reasoningTokens) }
: {}),
};
const telemetry = {
...persistedTelemetry,
steps,
...(generationQuery.data?.complete && providerGenerationTimeMs !== undefined
? { generationTimeMs: providerGenerationTimeMs }
: {}),
...(generationQuery.data?.complete && providerOutputTokensPerSecond !== undefined
? { providerOutputTokensPerSecond }
: {}),
...(generationQuery.data?.complete && providerContextUsed !== undefined
? {
contextUsed: providerContextUsed,
...(persistedTelemetry.contextLength
? { contextUtilization: Math.min(providerContextUsed / persistedTelemetry.contextLength, 1) }
: {}),
}
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.cost) !== undefined
? { cost: sumCompleteMetric((step) => step.cost), costType: "reported" as const }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.upstreamCost) !== undefined
? { upstreamCost: sumCompleteMetric((step) => step.upstreamCost) }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.cacheDiscount) !== undefined
? { cacheDiscount: sumCompleteMetric((step) => step.cacheDiscount) }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.fallbackCount) !== undefined
? { fallbackCount: sumCompleteMetric((step) => step.fallbackCount) }
: {}),
...(generationQuery.data?.complete && sumCompleteMetric((step) => step.fallbackLatencyMs) !== undefined
? { fallbackLatencyMs: sumCompleteMetric((step) => step.fallbackLatencyMs) }
: {}),
};
const contextLength = telemetry.contextLength ?? 0;
const contextUsed = telemetry.contextUsed ?? 0;
@@ -645,6 +737,21 @@ const RequestTelemetry = () => {
const contextLabel = hasContextWindow
? `${t("usage.contextWindow")}: ${contextUsed.toLocaleString()} / ${contextLength.toLocaleString()} (${contextPercentage}%)`
: `${t("usage.contextWindow")}: ${t("usage.notReported")}`;
const displayedThroughput = telemetry.providerOutputTokensPerSecond ?? telemetry.outputTokensPerSecond;
const hasProviderMetrics =
generationQuery.isFetching ||
telemetry.providerOutputTokensPerSecond !== undefined ||
telemetry.generationTimeMs !== undefined ||
telemetry.cacheDiscount !== undefined ||
telemetry.fallbackCount !== undefined ||
telemetry.fallbackLatencyMs !== undefined ||
telemetry.steps.some(
(step) =>
step.providerLatencyMs !== undefined ||
step.moderationLatencyMs !== undefined ||
step.serviceTier !== undefined ||
step.dataRegion !== undefined,
);
return (
<Box className={classes.telemetry}>
@@ -661,9 +768,9 @@ const RequestTelemetry = () => {
{formatDuration(telemetry.durationMs)}
</Text>
)}
{telemetry.outputTokensPerSecond !== undefined && (
{displayedThroughput !== undefined && (
<Text size="xs" c="dimmed">
{telemetry.outputTokensPerSecond.toFixed(1)} tok/s
{displayedThroughput.toFixed(1)} tok/s
</Text>
)}
{telemetry.cost !== undefined && (
@@ -676,7 +783,7 @@ const RequestTelemetry = () => {
<Popover
opened={opened}
onChange={setOpened}
width="min(20rem, calc(100vw - 1.5rem))"
width="min(23rem, calc(100vw - 1.5rem))"
position="bottom-end"
shadow="md"
withArrow
@@ -803,7 +910,18 @@ const RequestTelemetry = () => {
</Box>
<Divider />
<Group gap="lg" align="flex-start">
<Text size="xs" fw={700} tt="uppercase" c="dimmed">
{t("usage.homarrTiming")}
</Text>
<Box className={classes.usageGrid}>
<div>
<Text size="xs" c="dimmed">
{t("usage.endToEnd")}
</Text>
<Text size="sm" fw={600}>
{telemetry.durationMs !== undefined ? formatDuration(telemetry.durationMs) : t("usage.notReported")}
</Text>
</div>
<div>
<Text size="xs" c="dimmed">
{t("usage.firstOutput")}
@@ -814,6 +932,16 @@ const RequestTelemetry = () => {
: t("usage.notReported")}
</Text>
</div>
<div>
<Text size="xs" c="dimmed">
{t("usage.observedThroughput")}
</Text>
<Text size="sm" fw={600}>
{telemetry.outputTokensPerSecond !== undefined
? `${telemetry.outputTokensPerSecond.toFixed(1)} tok/s`
: t("usage.notReported")}
</Text>
</div>
<div>
<Text size="xs" c="dimmed">
{t("usage.cost")}
@@ -847,7 +975,74 @@ const RequestTelemetry = () => {
</Text>
</div>
)}
</Group>
</Box>
{hasProviderMetrics && (
<>
<Divider />
<Text size="xs" fw={700} tt="uppercase" c="dimmed">
{t("usage.providerTiming")}
</Text>
{generationQuery.isFetching && generationQuery.data?.complete !== true && (
<Group gap="xs">
<Loader size="xs" type="bars" />
<Text size="xs" c="dimmed">
{t("usage.loadingProviderDetails")}
</Text>
</Group>
)}
<Box className={classes.usageGrid}>
<div>
<Text size="xs" c="dimmed">
{t("usage.generationTime")}
</Text>
<Text size="sm" fw={600}>
{telemetry.generationTimeMs !== undefined
? formatDuration(telemetry.generationTimeMs)
: t("usage.notReported")}
</Text>
</div>
<div>
<Text size="xs" c="dimmed">
{t("usage.providerThroughput")}
</Text>
<Text size="sm" fw={600}>
{telemetry.providerOutputTokensPerSecond !== undefined
? `${telemetry.providerOutputTokensPerSecond.toFixed(1)} tok/s`
: t("usage.notReported")}
</Text>
</div>
<div>
<Text size="xs" c="dimmed">
{t("usage.fallbacks")}
</Text>
<Text size="sm" fw={600}>
{telemetry.fallbackCount?.toLocaleString() ?? t("usage.notReported")}
</Text>
</div>
{telemetry.fallbackLatencyMs !== undefined && (
<div>
<Text size="xs" c="dimmed">
{t("usage.fallbackLatency")}
</Text>
<Text size="sm" fw={600}>
{formatDuration(telemetry.fallbackLatencyMs)}
</Text>
</div>
)}
{telemetry.cacheDiscount !== undefined && (
<div>
<Text size="xs" c="dimmed">
{t("usage.cacheDiscount")}
</Text>
<Text size="sm" fw={600}>
{formatCost(telemetry.cacheDiscount)}
</Text>
</div>
)}
</Box>
</>
)}
{telemetry.steps.length > 0 && (
<Stack gap={4}>
@@ -863,12 +1058,86 @@ const RequestTelemetry = () => {
</Text>
<Text size="xs" c="dimmed">
{formatDuration(step.durationMs)}
{step.outputTokensPerSecond !== undefined
? ` · ${step.outputTokensPerSecond.toFixed(1)} tok/s`
: ""}
{step.providerOutputTokensPerSecond !== undefined
? ` · ${step.providerOutputTokensPerSecond.toFixed(1)} tok/s`
: step.outputTokensPerSecond !== undefined
? ` · ${step.outputTokensPerSecond.toFixed(1)} tok/s`
: ""}
{step.cost !== undefined ? ` · ${formatCost(step.cost)}` : ""}
</Text>
</Group>
{(step.providerLatencyMs !== undefined ||
step.generationTimeMs !== undefined ||
step.moderationLatencyMs !== undefined ||
step.fallbackCount !== undefined ||
step.fallbackLatencyMs !== undefined) && (
<Text size="xs" c="dimmed">
{[
step.providerLatencyMs !== undefined
? `${t("usage.providerLatency")} ${formatDuration(step.providerLatencyMs)}`
: undefined,
step.generationTimeMs !== undefined
? `${t("usage.generationTime")} ${formatDuration(step.generationTimeMs)}`
: undefined,
step.moderationLatencyMs !== undefined
? `${t("usage.moderationLatency")} ${formatDuration(step.moderationLatencyMs)}`
: undefined,
step.fallbackCount !== undefined
? `${t("usage.fallbacks")} ${step.fallbackCount.toLocaleString()}`
: undefined,
step.fallbackLatencyMs !== undefined
? `${t("usage.fallbackLatency")} ${formatDuration(step.fallbackLatencyMs)}`
: undefined,
]
.filter(Boolean)
.join(" · ")}
</Text>
)}
{(step.inputTokens !== undefined || step.outputTokens !== undefined) && (
<Text size="xs" c="dimmed">
{[
step.inputTokens !== undefined
? `${step.inputTokens.toLocaleString()} ${t("usage.inputShort")}`
: undefined,
step.outputTokens !== undefined
? `${step.outputTokens.toLocaleString()} ${t("usage.outputShort")}`
: undefined,
step.cachedInputTokens !== undefined
? `${step.cachedInputTokens.toLocaleString()} ${t("usage.cachedShort")}`
: undefined,
step.reasoningTokens !== undefined
? `${step.reasoningTokens.toLocaleString()} ${t("usage.reasoningShort")}`
: undefined,
]
.filter(Boolean)
.join(" · ")}
</Text>
)}
{(step.serviceTier ||
step.dataRegion ||
step.routerStrategy ||
step.routerRegion ||
step.isByok) && (
<Text size="xs" c="dimmed">
{[
step.routerStrategy,
step.routerRegion,
step.serviceTier,
step.dataRegion,
step.isByok ? t("usage.byok") : undefined,
]
.filter(Boolean)
.join(" · ")}
</Text>
)}
{(step.normalizedInputTokens !== undefined || step.normalizedOutputTokens !== undefined) &&
(step.normalizedInputTokens !== step.inputTokens ||
step.normalizedOutputTokens !== step.outputTokens) && (
<Text size="xs" c="dimmed">
{t("usage.normalizedTokens")}: {step.normalizedInputTokens?.toLocaleString() ?? "–"} /{" "}
{step.normalizedOutputTokens?.toLocaleString() ?? "–"}
</Text>
)}
{step.generationId && (
<Text size="xs" c="dimmed" className={classes.generationId} title={step.generationId}>
{t("usage.generation")}: {step.generationId}