From 491792c2a4a7911f165ef289d1b5bc99cc3d6a65 Mon Sep 17 00:00:00 2001 From: Akshat Khandelwal Date: Sat, 21 Feb 2026 11:10:42 +0530 Subject: [PATCH] feat(model selector): Add cost of the models on the right side --- .../src/components/dialog-select-model.tsx | 56 +++++++++++++++---- packages/app/src/components/model-tooltip.tsx | 13 +++++ packages/opencode/src/cli/cmd/models.ts | 10 ++++ .../cli/cmd/tui/component/dialog-model.tsx | 10 +++- .../src/cli/cmd/tui/ui/dialog-select.tsx | 17 +++++- packages/ui/src/components/list.tsx | 4 ++ 6 files changed, 94 insertions(+), 16 deletions(-) diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 9f7afb8cd27d..05e87d9f6530 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -18,6 +18,14 @@ import { useLanguage } from "@/context/language" const isFree = (provider: string, cost: { input: number } | undefined) => provider === "opencode" && (!cost || cost.input === 0) +function formatCost(cost?: { input: number; output: number }) { + if (!cost || (cost.input === 0 && cost.output === 0)) return undefined + return { + input: `$${cost.input.toFixed(2)}`, + output: `$${cost.output.toFixed(2)}`, + } +} + const ModelList: Component<{ provider?: string class?: string @@ -39,6 +47,14 @@ const ModelList: Component<{ class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`} search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }} emptyMessage={language.t("dialog.model.empty")} + header={ +
+ Model + + Input / Output /1M tok + +
+ } key={(x) => `${x.provider.id}:${x.id}`} items={models} current={local.model.current()} @@ -69,17 +85,35 @@ const ModelList: Component<{ props.onSelect() }} > - {(i) => ( -
- {i.name} - - {language.t("model.tag.free")} - - - {language.t("model.tag.latest")} - -
- )} + {(i) => { + const price = () => formatCost(i.cost) + return ( +
+ {i.name} + + {language.t("model.tag.latest")} + + + + {language.t("model.tag.free")} + + } + > + {(p) => ( + <> + {p().input} + / + {p().output} + + )} + + +
+ ) + }} ) } diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx index 53164dae85e2..a1cba6c05448 100644 --- a/packages/app/src/components/model-tooltip.tsx +++ b/packages/app/src/components/model-tooltip.tsx @@ -18,6 +18,10 @@ type ModelInfo = { input: Array } reasoning?: boolean + cost?: { + input: number + output: number + } limit: { context: number } @@ -73,6 +77,12 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free? : language.t("model.tooltip.reasoning.none") } const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() }) + const pricing = () => { + if (props.free) return "Free" + const cost = props.model.cost + if (!cost || (cost.input === 0 && cost.output === 0)) return undefined + return `$${cost.input.toFixed(2)} / $${cost.output.toFixed(2)} /1M tokens` + } return (
@@ -86,6 +96,9 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
{reasoning()}
{context()}
+ + {(price) =>
{price()}
} +
) } diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index 156dae91c676..20825466af8a 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -36,11 +36,21 @@ export const ModelsCommand = cmd({ async fn() { const providers = await Provider.list() + function formatCost(cost: { input: number; output: number }) { + if (cost.input === 0 && cost.output === 0) return UI.Style.TEXT_DIM + "Free" + UI.Style.TEXT_NORMAL + return ( + UI.Style.TEXT_DIM + + `$${cost.input.toFixed(2)} / $${cost.output.toFixed(2)} /1M tokens` + + UI.Style.TEXT_NORMAL + ) + } + function printModels(providerID: string, verbose?: boolean) { const provider = providers[providerID] const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b)) for (const [modelID, model] of sortedModels) { process.stdout.write(`${providerID}/${modelID}`) + process.stdout.write(" " + formatCost(model.cost)) process.stdout.write(EOL) if (verbose) { process.stdout.write(JSON.stringify(model, null, 2)) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index c30b8d12a933..55f0f667281b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -15,6 +15,11 @@ export function useConnected() { ) } +function formatCost(cost?: { input: number; output: number }) { + if (!cost || (cost.input === 0 && cost.output === 0)) return "Free".padStart(15) + return `${`$${cost.input.toFixed(2)}`.padStart(6)} / ${`$${cost.output.toFixed(2)}`.padStart(6)}` +} + export function DialogModel(props: { providerID?: string }) { const local = useLocal() const sync = useSync() @@ -48,7 +53,7 @@ export function DialogModel(props: { providerID?: string }) { description: provider.name, category, disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: formatCost(model.cost), onSelect: () => { dialog.clear() local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true }) @@ -86,7 +91,7 @@ export function DialogModel(props: { providerID?: string }) { : undefined, category: connected() ? provider.name : undefined, disabled: provider.id === "opencode" && model.includes("-nano"), - footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: formatCost(info.cost), onSelect() { dialog.clear() local.model.set({ providerID: provider.id, modelID: model }, { recent: true }) @@ -159,6 +164,7 @@ export function DialogModel(props: { providerID?: string }) { flat={true} skipFilter={true} title={title()} + columnHeader="Input / Output /1M tok" current={local.model.current()} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 151f73cf7c0a..a625d5e2b835 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -21,6 +21,7 @@ export interface DialogSelectProps { onFilter?: (query: string) => void onSelect?: (option: DialogSelectOption) => void skipFilter?: boolean + columnHeader?: string keybind?: { keybind?: Keybind.Info title: string @@ -263,6 +264,16 @@ export function DialogSelect(props: DialogSelectProps) { /> + + + + Model + + + {props.columnHeader} + + + 0} fallback={ @@ -320,9 +331,9 @@ export function DialogSelect(props: DialogSelectProps) { gap={1} > + +
{props.header}
+
0 || showAdd()}