diff --git a/ui/src/components/AIModelConfigurationV2Editor.tsx b/ui/src/components/AIModelConfigurationV2Editor.tsx index 281f8dac..ca6e160c 100644 --- a/ui/src/components/AIModelConfigurationV2Editor.tsx +++ b/ui/src/components/AIModelConfigurationV2Editor.tsx @@ -22,6 +22,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { VoiceSelectorModal } from "@/components/VoiceSelectorModal"; import { LANGUAGE_DISPLAY_NAMES } from "@/constants/languages"; +import { formatRoundingPolicy } from "@/lib/billingDisplay"; type ModelMode = "realtime" | "dograh" | "byok"; @@ -296,6 +297,25 @@ function formatPricePerMinute(price: ModelConfigurationMetricPrice): string { }).format(price.price_per_minute); } +function MetricPrice({ + label, + price, +}: { + label: string; + price: ModelConfigurationMetricPrice; +}) { + return ( +
+

+ {label}: {formatPricePerMinute(price)}/{price.unit} +

+

+ {formatRoundingPolicy(price.rounding_policy)} +

+
+ ); +} + function PricingSummary({ pricing, includeDograhModel, @@ -314,21 +334,16 @@ function PricingSummary({

Usage pricing

{platformPrice && ( -

- Platform usage: {formatPricePerMinute(platformPrice)}/{platformPrice.unit} -

+ )} {dograhModelPrice && ( -

- Dograh model usage: {formatPricePerMinute(dograhModelPrice)}/{dograhModelPrice.unit} -

+ )} {thirdPartyModels && (

Your selected model provider may charge separately for its usage.

)} -

Usage is rounded up to whole minutes per call.

); diff --git a/ui/src/lib/billingDisplay.test.ts b/ui/src/lib/billingDisplay.test.ts new file mode 100644 index 00000000..77ec1837 --- /dev/null +++ b/ui/src/lib/billingDisplay.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; + +import { formatRoundingPolicy } from "@/lib/billingDisplay"; + +describe("formatRoundingPolicy", () => { + it.each([ + ["ceil_minute", "Rounded up to whole minutes per call."], + ["ceil_1_minute", "Rounded up to whole minutes per call."], + ["ceil_15_seconds", "Rounded up in 15-second increments per call."], + ["ceil_30_second", "Rounded up in 30-second increments per call."], + ["ceil_2_minutes", "Rounded up in 2-minute increments per call."], + ["none", "Billed using exact measured usage."], + ["custom_contract", "Billing policy: custom contract."], + ["", "Billing policy details are unavailable."], + ])("formats %s", (policy, expected) => { + expect(formatRoundingPolicy(policy)).toBe(expected); + }); +}); diff --git a/ui/src/lib/billingDisplay.ts b/ui/src/lib/billingDisplay.ts new file mode 100644 index 00000000..8a493a6e --- /dev/null +++ b/ui/src/lib/billingDisplay.ts @@ -0,0 +1,24 @@ +export function formatRoundingPolicy(roundingPolicy: string): string { + const normalizedPolicy = roundingPolicy.trim().toLowerCase(); + if (!normalizedPolicy) { + return "Billing policy details are unavailable."; + } + if (normalizedPolicy === "ceil_minute" || normalizedPolicy === "ceil_1_minute") { + return "Rounded up to whole minutes per call."; + } + if (normalizedPolicy === "none") { + return "Billed using exact measured usage."; + } + + const secondsMatch = normalizedPolicy.match(/^ceil_(\d+)_seconds?$/); + if (secondsMatch) { + return `Rounded up in ${Number(secondsMatch[1])}-second increments per call.`; + } + + const minutesMatch = normalizedPolicy.match(/^ceil_(\d+)_minutes?$/); + if (minutesMatch) { + return `Rounded up in ${Number(minutesMatch[1])}-minute increments per call.`; + } + + return `Billing policy: ${roundingPolicy.replaceAll("_", " ")}.`; +}