|
|
@@ -1,12 +1,15 @@
|
|
|
import type { StatusBadgeProps } from '@/components/status-badge'
|
|
|
import {
|
|
|
BILLING_PRICING_VARS,
|
|
|
+ normalizeTierLabel,
|
|
|
parseTiersFromExpr,
|
|
|
type ParsedTier,
|
|
|
} from '@/features/pricing/lib/billing-expr'
|
|
|
import type { UsageLog } from '../data/schema'
|
|
|
import type { LogOtherData } from '../types'
|
|
|
|
|
|
+export { normalizeTierLabel }
|
|
|
+
|
|
|
const PARAM_OVERRIDE_ACTION_MAP: Record<string, string> = {
|
|
|
set: 'Set',
|
|
|
delete: 'Delete',
|
|
|
@@ -36,8 +39,8 @@ const PARAM_OVERRIDE_ACTION_MAP: Record<string, string> = {
|
|
|
* Get localized label for a param override action
|
|
|
*/
|
|
|
export function getParamOverrideActionLabel(
|
|
|
- action: string,
|
|
|
- t: (key: string) => string
|
|
|
+ action: string,
|
|
|
+ t: (key: string) => string
|
|
|
): string {
|
|
|
const key = PARAM_OVERRIDE_ACTION_MAP[action.toLowerCase()]
|
|
|
return key ? t(key) : action
|
|
|
@@ -47,7 +50,7 @@ export function getParamOverrideActionLabel(
|
|
|
* Parse a param override audit line into action and content
|
|
|
*/
|
|
|
export function parseAuditLine(
|
|
|
- line: string
|
|
|
+ line: string
|
|
|
): { action: string; content: string } | null {
|
|
|
if (typeof line !== 'string') return null
|
|
|
const firstSpace = line.indexOf(' ')
|
|
|
@@ -64,9 +67,9 @@ export function parseAuditLine(
|
|
|
export function isViolationFeeLog(other: LogOtherData | null): boolean {
|
|
|
if (!other) return false
|
|
|
return (
|
|
|
- other.violation_fee === true ||
|
|
|
- Boolean(other.violation_fee_code) ||
|
|
|
- Boolean(other.violation_fee_marker)
|
|
|
+ other.violation_fee === true ||
|
|
|
+ Boolean(other.violation_fee_code) ||
|
|
|
+ Boolean(other.violation_fee_marker)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -88,7 +91,7 @@ export function parseLogOther(other: string): LogOtherData | null {
|
|
|
* Get time color based on duration (in seconds)
|
|
|
*/
|
|
|
export function getTimeColor(
|
|
|
- seconds: number
|
|
|
+ seconds: number
|
|
|
): 'success' | 'warning' | 'danger' {
|
|
|
if (seconds < 10) return 'success'
|
|
|
if (seconds < 30) return 'warning'
|
|
|
@@ -99,7 +102,7 @@ export function getTimeColor(
|
|
|
* Get first-response-token color based on latency (in seconds)
|
|
|
*/
|
|
|
export function getFirstResponseTimeColor(
|
|
|
- seconds: number
|
|
|
+ seconds: number
|
|
|
): 'success' | 'warning' | 'danger' {
|
|
|
if (seconds < 5) return 'success'
|
|
|
if (seconds < 10) return 'warning'
|
|
|
@@ -110,7 +113,7 @@ export function getFirstResponseTimeColor(
|
|
|
* Get throughput color based on generated tokens per second
|
|
|
*/
|
|
|
export function getThroughputColor(
|
|
|
- tokensPerSecond: number
|
|
|
+ tokensPerSecond: number
|
|
|
): 'success' | 'warning' | 'danger' {
|
|
|
if (tokensPerSecond >= 30) return 'success'
|
|
|
if (tokensPerSecond >= 15) return 'warning'
|
|
|
@@ -121,8 +124,8 @@ export function getThroughputColor(
|
|
|
* Get response color using throughput only when enough output tokens exist.
|
|
|
*/
|
|
|
export function getResponseTimeColor(
|
|
|
- seconds: number,
|
|
|
- completionTokens: number
|
|
|
+ seconds: number,
|
|
|
+ completionTokens: number
|
|
|
): 'success' | 'warning' | 'danger' {
|
|
|
if (completionTokens < 100 || seconds <= 0) return getTimeColor(seconds)
|
|
|
return getThroughputColor(completionTokens / seconds)
|
|
|
@@ -138,9 +141,9 @@ export function formatModelName(log: UsageLog): {
|
|
|
} {
|
|
|
const other = parseLogOther(log.other)
|
|
|
const isMapped = !!(
|
|
|
- other?.is_model_mapped &&
|
|
|
- other?.upstream_model_name &&
|
|
|
- other.upstream_model_name !== ''
|
|
|
+ other?.is_model_mapped &&
|
|
|
+ other?.upstream_model_name &&
|
|
|
+ other.upstream_model_name !== ''
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
@@ -157,7 +160,25 @@ export function formatModelName(log: UsageLog): {
|
|
|
export function decodeBillingExprB64(exprB64: string | undefined): string {
|
|
|
if (!exprB64) return ''
|
|
|
try {
|
|
|
- return atob(exprB64)
|
|
|
+ const binaryString =
|
|
|
+ typeof window !== 'undefined'
|
|
|
+ ? window.atob(exprB64)
|
|
|
+ : Buffer.from(exprB64, 'base64').toString('binary')
|
|
|
+ const bytes = new Uint8Array(binaryString.length)
|
|
|
+
|
|
|
+ for (let i = 0; i < binaryString.length; i++) {
|
|
|
+ bytes[i] = binaryString.charCodeAt(i)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof TextDecoder !== 'undefined') {
|
|
|
+ return new TextDecoder().decode(bytes)
|
|
|
+ }
|
|
|
+
|
|
|
+ return decodeURIComponent(
|
|
|
+ Array.prototype.map
|
|
|
+ .call(bytes, (byte: number) => '%' + byte.toString(16).padStart(2, '0'))
|
|
|
+ .join('')
|
|
|
+ )
|
|
|
} catch {
|
|
|
return ''
|
|
|
}
|
|
|
@@ -165,19 +186,21 @@ export function decodeBillingExprB64(exprB64: string | undefined): string {
|
|
|
|
|
|
/**
|
|
|
* Resolve which parsed tier corresponds to the matched_tier label in a log
|
|
|
- * entry. Falls back to the first tier when the label is missing or unknown,
|
|
|
- * which mirrors the behaviour of the classic frontend renderer.
|
|
|
+ * entry. Missing or unknown labels do not fall back to another tier because
|
|
|
+ * that would display guessed unit prices.
|
|
|
*/
|
|
|
export function resolveMatchedTier(
|
|
|
- tiers: ParsedTier[],
|
|
|
- matchedLabel: string | undefined
|
|
|
+ tiers: ParsedTier[],
|
|
|
+ matchedLabel: string | undefined
|
|
|
): ParsedTier | null {
|
|
|
if (tiers.length === 0) return null
|
|
|
- if (matchedLabel) {
|
|
|
- const found = tiers.find((tier) => tier.label === matchedLabel)
|
|
|
- if (found) return found
|
|
|
- }
|
|
|
- return tiers[0]
|
|
|
+ if (!matchedLabel) return null
|
|
|
+ const found = tiers.find((tier) => {
|
|
|
+ const l1 = normalizeTierLabel(tier.label)
|
|
|
+ const l2 = normalizeTierLabel(matchedLabel)
|
|
|
+ return l1 === l2 && l1 !== ''
|
|
|
+ })
|
|
|
+ return found || null
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -197,19 +220,19 @@ export interface TieredBillingSummary {
|
|
|
* not exercise the cache path (mirrors the classic frontend behaviour).
|
|
|
*/
|
|
|
export function hasAnyCacheTokens(
|
|
|
- other: LogOtherData | null | undefined
|
|
|
+ other: LogOtherData | null | undefined
|
|
|
): boolean {
|
|
|
if (!other) return false
|
|
|
return (
|
|
|
- (other.cache_tokens || 0) > 0 ||
|
|
|
- (other.cache_creation_tokens || 0) > 0 ||
|
|
|
- (other.cache_creation_tokens_5m || 0) > 0 ||
|
|
|
- (other.cache_creation_tokens_1h || 0) > 0
|
|
|
+ (other.cache_tokens || 0) > 0 ||
|
|
|
+ (other.cache_creation_tokens || 0) > 0 ||
|
|
|
+ (other.cache_creation_tokens_5m || 0) > 0 ||
|
|
|
+ (other.cache_creation_tokens_1h || 0) > 0
|
|
|
)
|
|
|
}
|
|
|
|
|
|
export function getTieredBillingSummary(
|
|
|
- other: LogOtherData | null
|
|
|
+ other: LogOtherData | null
|
|
|
): TieredBillingSummary | null {
|
|
|
if (!other || other.billing_mode !== 'tiered_expr') return null
|
|
|
const exprStr = decodeBillingExprB64(other.expr_b64)
|
|
|
@@ -244,16 +267,16 @@ export function getTieredBillingSummary(
|
|
|
* @param unit - Unit of the timestamps ('seconds' or 'milliseconds')
|
|
|
*/
|
|
|
export function formatDuration(
|
|
|
- submitTime?: number,
|
|
|
- finishTime?: number,
|
|
|
- unit: 'seconds' | 'milliseconds' = 'milliseconds'
|
|
|
+ submitTime?: number,
|
|
|
+ finishTime?: number,
|
|
|
+ unit: 'seconds' | 'milliseconds' = 'milliseconds'
|
|
|
): { durationSec: number; variant: StatusBadgeProps['variant'] } | null {
|
|
|
if (!submitTime || !finishTime) return null
|
|
|
|
|
|
const durationSec =
|
|
|
- unit === 'milliseconds'
|
|
|
- ? (finishTime - submitTime) / 1000
|
|
|
- : finishTime - submitTime
|
|
|
+ unit === 'milliseconds'
|
|
|
+ ? (finishTime - submitTime) / 1000
|
|
|
+ : finishTime - submitTime
|
|
|
|
|
|
return { durationSec, variant: durationSec > 60 ? 'red' : 'green' }
|
|
|
}
|