Skip to content

Commit 77a0fd6

Browse files
authored
fix(app): always list OpenCode free models in the model picker (#60047)
1 parent 0e10290 commit 77a0fd6

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎packages/app/e2e/regression/composer.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,25 @@ test("keeps focus in a dialog that replaces the model dialog and returns it to t
889889
await expect(editor).toHaveText("keep, me")
890890
})
891891

892+
test("lists OpenCode's free models in the model picker however old they are", async ({ page }) => {
893+
await page.clock.setFixedTime(T0)
894+
const year = { released: T0 - 365 * 24 * 60 * 60 * 1000 }
895+
await openSession(page, {
896+
name: "ComposerFreeModels",
897+
provider: provider(
898+
{ id: "new-paid", name: "New Paid", cost: { input: 1, output: 1 }, time: { released: T0 } },
899+
{ id: "old-paid", name: "Old Paid", cost: { input: 1, output: 1 }, time: year },
900+
{ id: "big-pickle", name: "Big Pickle", cost: { input: 0, output: 0 }, time: year },
901+
),
902+
})
903+
904+
await page.locator('[data-component="composer"] [data-action="composer-model"]').click()
905+
const models = page.getByRole("dialog", { name: "Select model", exact: true })
906+
await expect(models.locator('[data-option-key="opencode:new-paid"]')).toBeVisible()
907+
await expect(models.locator('[data-option-key="opencode:big-pickle"]')).toBeVisible()
908+
await expect(models.locator('[data-option-key="opencode:old-paid"]')).toHaveCount(0)
909+
})
910+
892911
test("lists slash commands in their built-in order", async ({ page }) => {
893912
const { editor } = await openSession(page, {
894913
name: "ComposerSlash",

‎packages/app/e2e/utils/mock-server.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1554,7 +1554,7 @@ function currentModels(catalog: MockProviderCatalog) {
15541554
variants: Object.entries(model.variants ?? {}).map(([id, settings]) =>
15551555
Predicate.isObject(settings) ? { id, settings } : { id },
15561556
),
1557-
time: { released: Date.now() },
1557+
time: model.time ?? { released: Date.now() },
15581558
cost: [{ input: model.cost?.input ?? 0, output: model.cost?.output ?? 0, cache: { read: 0, write: 0 } }],
15591559
status: "active",
15601560
enabled: true,
@@ -1687,6 +1687,7 @@ const decodeModel = Schema.decodeUnknownOption(
16871687
cost: lenient(Schema.Struct({ input: lenient(Schema.Number), output: lenient(Schema.Number) })),
16881688
capabilities: lenient(Model.Capabilities),
16891689
variants: lenient(Schema.Record(Schema.String, Schema.Unknown)),
1690+
time: lenient(Schema.Struct({ released: Schema.Number })),
16901691
}),
16911692
)
16921693

‎packages/app/src/providers/models/models.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
8484

8585
const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x))))
8686

87+
// OpenCode's free models (Big Pickle) stay on offer long after release, so they are listed regardless of age or a
88+
// newer free model in the same family.
89+
const freeSet = createMemo(
90+
() =>
91+
new Set(
92+
available()
93+
.filter(isFreeModel)
94+
.map((x) => modelKey({ providerID: x.provider.id, modelID: x.id })),
95+
),
96+
)
97+
8798
const visibility = createMemo(() => {
8899
const map = new Map<string, Visibility>()
89100

@@ -122,7 +133,7 @@ const createModelsController = (directory: Accessor<string | undefined>) => {
122133

123134
if (state === "show") return true
124135

125-
if (latestSet().has(key)) return true
136+
if (latestSet().has(key) || freeSet().has(key)) return true
126137

127138
// Models without a parseable release date stay visible.
128139
return !Number.isFinite(release().get(key) ?? NaN)

0 commit comments

Comments
 (0)