feat(ui): OpenAI Native service tier dropdown + tiered pricing table; hide redundant price rows

This commit is contained in:
Hannes Rudolph 2025-09-03 16:10:38 -06:00
parent c25cfdeaef
commit 5d6fc17e13
3 changed files with 128 additions and 16 deletions

View file

@ -496,7 +496,11 @@ const ApiOptions = ({
)}
{selectedProvider === "openai-native" && (
<OpenAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
<OpenAI
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
selectedModelInfo={selectedModelInfo}
/>
)}
{selectedProvider === "mistral" && (

View file

@ -25,7 +25,14 @@ export const ModelInfoView = ({
}: ModelInfoViewProps) => {
const { t } = useAppTranslation()
const infoItems = [
// Show tiered pricing table for OpenAI Native when model supports non-standard tiers
const allowedTiers =
(modelInfo?.allowedServiceTiers || []).filter((tier) => tier === "flex" || tier === "priority") ?? []
const tierPricing = modelInfo?.serviceTierPricing
const shouldShowTierPricingTable = apiProvider === "openai-native" && allowedTiers.length > 0 && !!tierPricing
const fmt = (n?: number) => (typeof n === "number" ? `${formatPrice(n)}` : "—")
const baseInfoItems = [
typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.contextWindow")}</span>{" "}
@ -53,6 +60,21 @@ export const ModelInfoView = ({
supportsLabel={t("settings:modelInfo.supportsPromptCache")}
doesNotSupportLabel={t("settings:modelInfo.noPromptCache")}
/>,
apiProvider === "gemini" && (
<span className="italic">
{selectedModelId.includes("pro-preview")
? t("settings:modelInfo.gemini.billingEstimate")
: t("settings:modelInfo.gemini.freeRequests", {
count: selectedModelId && selectedModelId.includes("flash") ? 15 : 2,
})}{" "}
<VSCodeLink href="https://ai.google.dev/pricing" className="text-sm">
{t("settings:modelInfo.gemini.pricingDetails")}
</VSCodeLink>
</span>
),
].filter(Boolean)
const priceInfoItems = [
modelInfo?.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.inputPrice")}:</span>{" "}
@ -77,20 +99,10 @@ export const ModelInfoView = ({
{formatPrice(modelInfo.cacheWritesPrice || 0)} / 1M tokens
</>
),
apiProvider === "gemini" && (
<span className="italic">
{selectedModelId.includes("pro-preview")
? t("settings:modelInfo.gemini.billingEstimate")
: t("settings:modelInfo.gemini.freeRequests", {
count: selectedModelId && selectedModelId.includes("flash") ? 15 : 2,
})}{" "}
<VSCodeLink href="https://ai.google.dev/pricing" className="text-sm">
{t("settings:modelInfo.gemini.pricingDetails")}
</VSCodeLink>
</span>
),
].filter(Boolean)
const infoItems = shouldShowTierPricingTable ? baseInfoItems : [...baseInfoItems, ...priceInfoItems]
return (
<>
{modelInfo?.description && (
@ -106,6 +118,62 @@ export const ModelInfoView = ({
<div key={index}>{item}</div>
))}
</div>
{shouldShowTierPricingTable && (
<div className="mt-2">
<div className="text-xs text-vscode-descriptionForeground mb-1">
Pricing by service tier (price per 1M tokens)
</div>
<div className="border border-vscode-dropdown-border rounded-xs overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-vscode-dropdown-background">
<tr>
<th className="text-left px-3 py-1.5">Tier</th>
<th className="text-right px-3 py-1.5">Input</th>
<th className="text-right px-3 py-1.5">Output</th>
<th className="text-right px-3 py-1.5">Cache reads</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-vscode-dropdown-border/60">
<td className="px-3 py-1.5">Standard</td>
<td className="px-3 py-1.5 text-right">{fmt(modelInfo?.inputPrice)}</td>
<td className="px-3 py-1.5 text-right">{fmt(modelInfo?.outputPrice)}</td>
<td className="px-3 py-1.5 text-right">{fmt(modelInfo?.cacheReadsPrice)}</td>
</tr>
{allowedTiers.includes("flex") && (
<tr className="border-t border-vscode-dropdown-border/60">
<td className="px-3 py-1.5">Flex</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.flex?.inputPrice ?? modelInfo?.inputPrice)}
</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.flex?.outputPrice ?? modelInfo?.outputPrice)}
</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.flex?.cacheReadsPrice ?? modelInfo?.cacheReadsPrice)}
</td>
</tr>
)}
{allowedTiers.includes("priority") && (
<tr className="border-t border-vscode-dropdown-border/60">
<td className="px-3 py-1.5">Priority</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.priority?.inputPrice ?? modelInfo?.inputPrice)}
</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.priority?.outputPrice ?? modelInfo?.outputPrice)}
</td>
<td className="px-3 py-1.5 text-right">
{fmt(tierPricing?.priority?.cacheReadsPrice ?? modelInfo?.cacheReadsPrice)}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</>
)
}

View file

@ -2,19 +2,21 @@ import { useCallback, useState } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
import type { ModelInfo, ProviderSettings } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
type OpenAIProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
selectedModelInfo?: ModelInfo
}
export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIProps) => {
export const OpenAI = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: OpenAIProps) => {
const { t } = useAppTranslation()
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
@ -72,6 +74,44 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIPro
{t("settings:providers.getOpenAiApiKey")}
</VSCodeButtonLink>
)}
{(() => {
const allowedTiers = (selectedModelInfo?.allowedServiceTiers || []).filter(
(t) => t === "flex" || t === "priority",
)
if (allowedTiers.length === 0) return null
return (
<div className="flex flex-col gap-1 mt-2" data-testid="openai-service-tier">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">Service tier</label>
<StandardTooltip content="For faster processing of API requests, try the priority processing service tier. For lower prices with higher latency, try the flex processing tier.">
<i className="codicon codicon-info text-vscode-descriptionForeground text-xs" />
</StandardTooltip>
</div>
<Select
value={apiConfiguration.openAiNativeServiceTier || "default"}
onValueChange={(value) =>
setApiConfigurationField(
"openAiNativeServiceTier",
value as ProviderSettings["openAiNativeServiceTier"],
)
}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Standard</SelectItem>
{allowedTiers.includes("flex") && <SelectItem value="flex">Flex</SelectItem>}
{allowedTiers.includes("priority") && (
<SelectItem value="priority">Priority</SelectItem>
)}
</SelectContent>
</Select>
</div>
)
})()}
</>
)
}