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()}