| import * as d3 from 'd3'; |
| import type { TextStats } from './textStatistics'; |
| import { tr } from '../lang/i18n-lite'; |
|
|
| |
| export type ApiTokenUsage = { |
| prompt_tokens?: number; |
| completion_tokens?: number; |
| total_tokens?: number; |
| }; |
|
|
| function formatApiUsageLine(usage: ApiTokenUsage | null | undefined): string | null { |
| if (!usage) return null; |
| const parts: string[] = []; |
| if (typeof usage.prompt_tokens === 'number' && Number.isFinite(usage.prompt_tokens)) { |
| parts.push(`prompt ${usage.prompt_tokens} tokens`); |
| } |
| if (typeof usage.completion_tokens === 'number' && Number.isFinite(usage.completion_tokens)) { |
| parts.push(`completion ${usage.completion_tokens} tokens`); |
| } |
| if (typeof usage.total_tokens === 'number' && Number.isFinite(usage.total_tokens)) { |
| parts.push(`total ${usage.total_tokens} tokens`); |
| } |
| return parts.length > 0 ? parts.join('<br/>') : null; |
| } |
|
|
| |
| export function updateApiUsageDisplay( |
| metricUsage: d3.Selection<any, unknown, any, any>, |
| usage: ApiTokenUsage | null | undefined |
| ): void { |
| const line = formatApiUsageLine(usage ?? null); |
| if (line) { |
| metricUsage.html(`<span class="text-metrics-api-usage">${line}</span>`); |
| } else { |
| metricUsage.text(''); |
| } |
| } |
|
|
| |
| export function updateChatCompletionMetrics( |
| metricUsage: d3.Selection<any, unknown, any, any>, |
| metricModel: d3.Selection<any, unknown, any, any>, |
| modelName: string | null | undefined, |
| usage: ApiTokenUsage | null | undefined |
| ): void { |
| if (!validateMetricsElements(metricUsage, metricModel)) { |
| return; |
| } |
| updateApiUsageDisplay(metricUsage, usage ?? null); |
| updateModel(metricModel, modelName); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function updateBasicMetrics( |
| metricBytes: d3.Selection<any, unknown, any, any>, |
| metricChars: d3.Selection<any, unknown, any, any>, |
| metricTokens: d3.Selection<any, unknown, any, any>, |
| stats: TextStats, |
| apiUsage?: ApiTokenUsage | null |
| ): void { |
| metricBytes.text(`${stats.byteCount} B`); |
| metricChars.text(`${stats.charCount} ${tr('chars')}`); |
| const tokensText = `${stats.tokenCount} ${tr('tokens')}`; |
| let primaryLine: string; |
| if (stats.tokenCount > 0 && stats.byteCount > 0) { |
| const bytesPerToken = stats.byteCount / stats.tokenCount; |
| primaryLine = `${tokensText} (${bytesPerToken.toFixed(2)} B/t)`; |
| } else { |
| primaryLine = tokensText; |
| } |
|
|
| const usageLine = formatApiUsageLine(apiUsage ?? null); |
| if (usageLine) { |
| metricTokens.html(`${primaryLine}<br/><span class="text-metrics-api-usage">${usageLine}</span>`); |
| } else { |
| metricTokens.text(primaryLine); |
| } |
| } |
|
|
| |
| |
| |
| export type DiffModeConfig = { |
| delta: number | null; |
| baseTotalSurprisal: number | null; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function updateTotalSurprisal( |
| metricTotalSurprisal: d3.Selection<any, unknown, any, any>, |
| stats: TextStats, |
| totalSurprisalFormat: (value: number | null) => string, |
| diffMode?: DiffModeConfig |
| ): void { |
| |
| if (diffMode) { |
| const { delta, baseTotalSurprisal } = diffMode; |
| if (delta !== null && Number.isFinite(delta)) { |
| if (baseTotalSurprisal !== null && Number.isFinite(baseTotalSurprisal) && baseTotalSurprisal !== 0) { |
| |
| const percentage = (delta / baseTotalSurprisal) * 100; |
| const sign = percentage >= 0 ? '+' : ''; |
| metricTotalSurprisal.text(`Δ${tr('total information')} = ${sign}${percentage.toFixed(2)}%`); |
| } else { |
| |
| metricTotalSurprisal.text(`Δ${tr('total information')} = --%`); |
| } |
| } else { |
| metricTotalSurprisal.text(`Δ${tr('total information')} = --%`); |
| } |
| return; |
| } |
|
|
| |
| if (stats.totalSurprisal !== null && Number.isFinite(stats.totalSurprisal)) { |
| const totalSurprisalText = `${tr('total information')} = ${totalSurprisalFormat(stats.totalSurprisal)} bits`; |
| |
| if (stats.byteCount > 0 && stats.tokenCount > 0) { |
| const bitsPerByte = stats.totalSurprisal / stats.byteCount; |
| const bitsPerToken = stats.totalSurprisal / stats.tokenCount; |
| metricTotalSurprisal.html(`${totalSurprisalText}<br>${totalSurprisalFormat(bitsPerByte)} bits/Byte, ${totalSurprisalFormat(bitsPerToken)} bits/token`); |
| } else if (stats.byteCount > 0) { |
| const bitsPerByte = stats.totalSurprisal / stats.byteCount; |
| metricTotalSurprisal.html(`${totalSurprisalText}<br>${totalSurprisalFormat(bitsPerByte)} bits/Byte`); |
| } else { |
| metricTotalSurprisal.text(totalSurprisalText); |
| } |
| } else { |
| metricTotalSurprisal.text(`${tr('total information')} = -- bits`); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function updateModel( |
| metricModel: d3.Selection<any, unknown, any, any>, |
| modelName?: string | null | undefined |
| ): void { |
| metricModel.text(`${tr('model')}: ${modelName}`); |
| } |
|
|
| |
| |
| |
| |
| |
| export function validateMetricsElements( |
| ...elements: d3.Selection<any, unknown, any, any>[] |
| ): boolean { |
| return elements.every(el => !el.empty()); |
| } |
|
|