feat: enrich assistant request telemetry
This commit is contained in:
1 parent
d562706c8b
commit
04520cf67c
10 files changed
+798
-39
No files matched your search
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user