fix(app): show free models alongside their paid twins by default (#55712)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-17 00:32:48 +00:00
committed by GitHub
Unverified
parent 543b42ec2c
commit a70eaa759b
4 changed files with 17 additions and 17 deletions
+7 -3
View File
@@ -17,6 +17,9 @@ function modelKey(model: ModelKey) {
return `${model.providerID}:${model.modelID}`
}
export const isFreeModel = (model: { provider: { id: string }; cost?: { input: number } }) =>
model.provider.id === "opencode" && (!model.cost || model.cost.input === 0)
const createModelsController = (directory: Accessor<string | undefined>) => {
const providers = useProviders(() => directory())
const models = useGlobal().models
@@ -54,7 +57,8 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
mapValues((models) =>
pipe(
models,
groupBy((x) => x.family),
// A free model shares its family and release date with its paid twin, so group them apart.
groupBy((x) => `${x.family}:${isFreeModel(x)}`),
values(),
(groups) =>
groups.flatMap((g) => {
@@ -161,7 +165,7 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
name: "Models",
gate: false,
init: (props: { directory?: string | Accessor<string | undefined> } = {}) => {
return createModelsController(() => (typeof props.directory === "function" ? props.directory() : props.directory))
init: (props: { directory?: string } = {}) => {
return createModelsController(() => props.directory)
},
})
@@ -3,6 +3,7 @@ import { Component, ComponentProps, createEffect, createMemo, For, JSX, Show, Su
import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media"
import { useLocal, type ModelSelection } from "@/providers/models/selection"
import { isFreeModel } from "@/providers/models/models"
import { useDialog } from "@opencode/ui/context/dialog"
import { popularProviders } from "@/providers/catalog/providers"
import { Button } from "@opencode/ui/button"
@@ -40,9 +41,6 @@ const MobilePanelDrawer = lazy(async () => {
return { default: MobilePanelDrawer }
})
const isFree = (provider: string, cost: { input: number } | undefined) =>
provider === "opencode" && (!cost || cost.input === 0)
type ModelState = ModelSelection
type ModelItem = ReturnType<ModelState["list"]>[number]
@@ -158,12 +156,12 @@ const ModelList: Component<{
gutter={12}
openDelay={0}
value={
<ModelTooltip model={item} latest={item.latest} free={isFree(item.provider.id, item.cost)} v2 />
<ModelTooltip model={item} latest={item.latest} free={isFreeModel(item)} v2 />
}
>
<span class="min-w-0 truncate">{item.name}</span>
</Tooltip>
<Show when={isFree(item.provider.id, item.cost)}>
<Show when={isFreeModel(item)}>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
</Show>
<Show when={item.latest}>
@@ -728,7 +726,7 @@ export function ModelSelectorPopoverView(props: {
<ModelTooltip
model={item}
latest={item.latest}
free={isFree(item.provider.id, item.cost)}
free={isFreeModel(item)}
v2
/>
}
@@ -746,7 +744,7 @@ export function ModelSelectorPopoverView(props: {
onSelect={() => selectModel(item)}
>
<span class="min-w-0 truncate leading-5">{item.name}</span>
<Show when={isFree(item.provider.id, item.cost)}>
<Show when={isFreeModel(item)}>
<Badge class="shrink-0">{language.t("model.tag.free")}</Badge>
</Show>
<Show when={item.latest}>
+4 -6
View File
@@ -7,6 +7,7 @@ import { useDialog } from "@opencode/ui/context/dialog"
import { useTheme } from "@opencode/ui/theme"
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
import { useLocal, type ModelSelection } from "@/providers/models/selection"
import { isFreeModel } from "@/providers/models/models"
import { useIntegrations } from "@/providers/catalog/integrations"
import { decode64 } from "@/runtime/persistence/base64"
import { useLanguage } from "@/runtime/i18n/language"
@@ -34,13 +35,10 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
return c ? `${c.provider.id}:${c.id}` : undefined
})
const isFree = (item: ReturnType<ModelState["list"]>[number]) =>
item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)
const providerName = (provider: { id: string; name: string }) =>
provider.id === "opencode" ? language.t("provider.connect.opencode.name") : provider.name
const freeModels = createMemo(() => model.list().filter(isFree))
const freeModels = createMemo(() => model.list().filter(isFreeModel))
const openProviders = (provider?: string) => {
void import("@/providers/connect/dialog").then((x) => {
@@ -66,7 +64,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
const buttons = Array.from(listEl.querySelectorAll<HTMLButtonElement>("button"))
if (buttons.length === 0) return
const index = buttons.indexOf(document.activeElement as HTMLButtonElement)
const index = buttons.findIndex((button) => button === document.activeElement)
const next =
index < 0 ? (e.key === "ArrowDown" ? 0 : buttons.length - 1) : index + (e.key === "ArrowDown" ? 1 : -1)
@@ -108,7 +106,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
<ModelTooltip
model={{ ...item, name: displayModelName(item.name) }}
latest={item.latest}
free={isFree(item)}
free={isFreeModel(item)}
v2
/>
}
+1 -1
View File
@@ -39,7 +39,7 @@ export function TargetSessionRouteContent() {
return (
<>
<MarkSessionViewed sessionID={() => params.id} />
<ModelsProvider directory={directory}>
<ModelsProvider directory={directory()}>
<TargetSessionSettingsCommand />
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
<Show when={tabs.pendingSession(server.key, params.id)} fallback={<ResolvedTargetSessionRoute />}>