From a70eaa759b7857f89241c6262dd2cab00fbc3844 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:32:48 +0000 Subject: [PATCH] fix(app): show free models alongside their paid twins by default (#55712) Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> --- packages/app/src/providers/models/models.tsx | 10 +++++++--- packages/app/src/providers/models/select-dialog.tsx | 12 +++++------- packages/app/src/providers/models/unpaid.tsx | 10 ++++------ packages/app/src/session/route.tsx | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/app/src/providers/models/models.tsx b/packages/app/src/providers/models/models.tsx index b872852b099..33b67a9d520 100644 --- a/packages/app/src/providers/models/models.tsx +++ b/packages/app/src/providers/models/models.tsx @@ -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) => { const providers = useProviders(() => directory()) const models = useGlobal().models @@ -54,7 +57,8 @@ const createModelsController = (directory: Accessor) => { 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) => { export const { use: useModels, provider: ModelsProvider } = createSimpleContext({ name: "Models", gate: false, - init: (props: { directory?: string | Accessor } = {}) => { - return createModelsController(() => (typeof props.directory === "function" ? props.directory() : props.directory)) + init: (props: { directory?: string } = {}) => { + return createModelsController(() => props.directory) }, }) diff --git a/packages/app/src/providers/models/select-dialog.tsx b/packages/app/src/providers/models/select-dialog.tsx index 929f4385f35..aaad7bc2299 100644 --- a/packages/app/src/providers/models/select-dialog.tsx +++ b/packages/app/src/providers/models/select-dialog.tsx @@ -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[number] @@ -158,12 +156,12 @@ const ModelList: Component<{ gutter={12} openDelay={0} value={ - + } > {item.name} - + {language.t("model.tag.free")} @@ -728,7 +726,7 @@ export function ModelSelectorPopoverView(props: { } @@ -746,7 +744,7 @@ export function ModelSelectorPopoverView(props: { onSelect={() => selectModel(item)} > {item.name} - + {language.t("model.tag.free")} diff --git a/packages/app/src/providers/models/unpaid.tsx b/packages/app/src/providers/models/unpaid.tsx index ae9aebaea00..ed011c17c66 100644 --- a/packages/app/src/providers/models/unpaid.tsx +++ b/packages/app/src/providers/models/unpaid.tsx @@ -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[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("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 } diff --git a/packages/app/src/session/route.tsx b/packages/app/src/session/route.tsx index cfd75e3fa14..ec8d008e868 100644 --- a/packages/app/src/session/route.tsx +++ b/packages/app/src/session/route.tsx @@ -39,7 +39,7 @@ export function TargetSessionRouteContent() { return ( <> params.id} /> - + }>