From c0ac72ddd70fcd1913c0e4f4b0442fd2c4ce3999 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:42:14 +1000 Subject: [PATCH] fix(app): always list OpenCode free models in the model picker --- packages/app/e2e/regression/composer.spec.ts | 19 +++++++++++++++++++ packages/app/e2e/utils/mock-server.ts | 3 ++- packages/app/src/providers/models/models.tsx | 13 ++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/regression/composer.spec.ts b/packages/app/e2e/regression/composer.spec.ts index f9a1ecb01f06..806fb3aa1c11 100644 --- a/packages/app/e2e/regression/composer.spec.ts +++ b/packages/app/e2e/regression/composer.spec.ts @@ -889,6 +889,25 @@ test("keeps focus in a dialog that replaces the model dialog and returns it to t await expect(editor).toHaveText("keep, me") }) +test("lists OpenCode's free models in the model picker however old they are", async ({ page }) => { + await page.clock.setFixedTime(T0) + const year = { released: T0 - 365 * 24 * 60 * 60 * 1000 } + await openSession(page, { + name: "ComposerFreeModels", + provider: provider( + { id: "new-paid", name: "New Paid", cost: { input: 1, output: 1 }, time: { released: T0 } }, + { id: "old-paid", name: "Old Paid", cost: { input: 1, output: 1 }, time: year }, + { id: "big-pickle", name: "Big Pickle", cost: { input: 0, output: 0 }, time: year }, + ), + }) + + await page.locator('[data-component="composer"] [data-action="composer-model"]').click() + const models = page.getByRole("dialog", { name: "Select model", exact: true }) + await expect(models.locator('[data-option-key="opencode:new-paid"]')).toBeVisible() + await expect(models.locator('[data-option-key="opencode:big-pickle"]')).toBeVisible() + await expect(models.locator('[data-option-key="opencode:old-paid"]')).toHaveCount(0) +}) + test("lists slash commands in their built-in order", async ({ page }) => { const { editor } = await openSession(page, { name: "ComposerSlash", diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index db17839517c8..5bdbcd450460 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -1554,7 +1554,7 @@ function currentModels(catalog: MockProviderCatalog) { variants: Object.entries(model.variants ?? {}).map(([id, settings]) => Predicate.isObject(settings) ? { id, settings } : { id }, ), - time: { released: Date.now() }, + time: model.time ?? { released: Date.now() }, cost: [{ input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, cache: { read: 0, write: 0 } }], status: "active", enabled: true, @@ -1687,6 +1687,7 @@ const decodeModel = Schema.decodeUnknownOption( cost: lenient(Schema.Struct({ input: lenient(Schema.Number), output: lenient(Schema.Number) })), capabilities: lenient(Model.Capabilities), variants: lenient(Schema.Record(Schema.String, Schema.Unknown)), + time: lenient(Schema.Struct({ released: Schema.Number })), }), ) diff --git a/packages/app/src/providers/models/models.tsx b/packages/app/src/providers/models/models.tsx index 91978c31442d..cd8937453790 100644 --- a/packages/app/src/providers/models/models.tsx +++ b/packages/app/src/providers/models/models.tsx @@ -84,6 +84,17 @@ const createModelsController = (directory: Accessor) => { const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x)))) + // OpenCode's free models (Big Pickle) stay on offer long after release, so they are listed regardless of age or a + // newer free model in the same family. + const freeSet = createMemo( + () => + new Set( + available() + .filter(isFreeModel) + .map((x) => modelKey({ providerID: x.provider.id, modelID: x.id })), + ), + ) + const visibility = createMemo(() => { const map = new Map() @@ -122,7 +133,7 @@ const createModelsController = (directory: Accessor) => { if (state === "show") return true - if (latestSet().has(key)) return true + if (latestSet().has(key) || freeSet().has(key)) return true // Models without a parseable release date stay visible. return !Number.isFinite(release().get(key) ?? NaN)