opencode/packages/core/test/model-free.test.ts

90 lines
4.5 KiB
TypeScript

import { expect } from "bun:test"
import { DateTime, Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Location } from "@opencode/core/location"
import { Model } from "@opencode/core/model"
import { ModelResolver } from "@opencode/core/model-resolver"
import { Project } from "@opencode/core/project"
import { Provider } from "@opencode/core/provider"
import { AbsolutePath } from "@opencode/core/schema"
import { Session } from "@opencode/core/session"
import { SessionRunnerModel } from "@opencode/core/session/runner/model"
import { Money } from "@opencode/schema/money"
import { withEnv } from "./fixture/env"
import { location } from "./fixture/location"
import { it } from "./lib/effect"
const ref = location({ directory: AbsolutePath.make("test") })
const layer = AppNodeBuilder.build(
LayerNode.group([Provider.node, Model.node, ModelResolver.node, SessionRunnerModel.node]),
[Location.node.replace(Layer.succeed(Location.Service, Location.Service.of(ref)))],
)
const free = Model.ID.make("big-pickle")
const paid = Model.ID.make("paid")
const local = Model.ID.make("local")
const other = Provider.ID.make("other")
for (const flag of [undefined, "0", "1", "true"]) {
for (const providerID of [Provider.ID.opencode, Provider.ID.make("opencode-go")])
it.effect(`${providerID} catalog with OPENCODE_DISABLE_FREE_MODELS=${flag}`, () =>
withEnv({ OPENCODE_DISABLE_FREE_MODELS: flag }, () =>
Effect.gen(function* () {
const providers = yield* Provider.Service
const models = yield* Model.Service
const runner = yield* SessionRunnerModel.Service
const resolver = yield* ModelResolver.Service
yield* providers.transform((editor) => {
editor.update(providerID, () => {})
editor.models.update(providerID, free, (model) => {
model.time.released = 3000
model.family = Model.Family.make("gpt-luna")
model.capabilities = { ...model.capabilities, input: ["text"], output: ["text"] }
})
editor.models.update(providerID, paid, (model) => {
model.time.released = 2000
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.zero,
cache: { read: Money.USDPerMillionTokens.zero, write: Money.USDPerMillionTokens.zero },
},
]
})
editor.update(other, () => {})
editor.models.update(other, local, () => {})
})
yield* models.transform((editor) => editor.default.set(providerID, free))
const disabled = flag === "1" || flag === "true"
expect((yield* models.all()).map((model) => model.id)).toEqual(disabled ? [paid, local] : [free, paid, local])
expect(yield* models.available()).toEqual(yield* models.all())
expect((yield* models.default())?.id).toBe(disabled ? paid : free)
expect((yield* models.get(providerID, free))?.id).toBe(disabled ? undefined : free)
expect((yield* models.small(providerID))?.id).toBe(disabled ? undefined : free)
if (!disabled) return
const session = Session.Info.make({
id: Session.ID.make("ses_free_model"),
projectID: Project.ID.global,
title: "test",
model: { id: free, providerID },
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: ref,
})
const failure = yield* runner.resolve(session, models.available).pipe(Effect.flip)
expect(failure.message).toBe(`Model unavailable: ${providerID}/big-pickle`)
expect(yield* resolver.resolve(Model.Ref.make({ providerID, id: free }))).toBeUndefined()
// Later transforms cannot restore a free model, even when all paid models are removed.
yield* models.transform((editor) => {
editor.update(providerID, free, (model) => {
model.enabled = true
})
editor.remove(providerID, paid)
})
expect((yield* models.default())?.providerID).toBe(other)
expect(yield* models.get(providerID, free)).toBeUndefined()
}).pipe(Effect.provide(layer)),
),
)
}