Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton
84a1b0280e effect(cli): unwrap run/runtime.boot and variant.shared facades 2026-05-12 16:26:22 -04:00
5 changed files with 363 additions and 347 deletions

View File

@@ -9,7 +9,6 @@ import { Context, Effect, Layer } from "effect"
import { stringifyKeyStroke } from "@opentui/keymap"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { makeRuntime } from "@/effect/run-service"
import { reusePendingTask } from "./runtime.shared"
import { resolveSession, sessionHistory } from "./session.shared"
import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
@@ -28,6 +27,8 @@ const DEFAULT_KEYBINDS: FooterKeybinds = {
inputNewline: [{ key: "shift+return,ctrl+return,alt+return,ctrl+j" }],
}
export const defaultKeybinds: FooterKeybinds = DEFAULT_KEYBINDS
export type ModelInfo = {
providers: RunProvider[]
variants: string[]
@@ -41,7 +42,8 @@ export type SessionInfo = {
}
type Config = Awaited<ReturnType<typeof TuiConfig.get>>
type BootService = {
export interface Interface {
readonly resolveModelInfo: (
sdk: RunInput["sdk"],
directory: string,
@@ -58,13 +60,13 @@ type BootService = {
const configTask: { current?: Promise<Config> } = {}
class Service extends Context.Service<Service, BootService>()("@opencode/RunBoot") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/RunBoot") {}
function loadConfig() {
return reusePendingTask(configTask, () => TuiConfig.get())
}
function emptyModelInfo(): ModelInfo {
export function emptyModelInfo(): ModelInfo {
return {
providers: [],
variants: [],
@@ -72,7 +74,7 @@ function emptyModelInfo(): ModelInfo {
}
}
function emptySessionInfo(): SessionInfo {
export function emptySessionInfo(): SessionInfo {
return {
first: true,
history: [],
@@ -105,7 +107,7 @@ function footerKeybinds(config: Config | undefined): FooterKeybinds {
}
}
const layer = Layer.effect(
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined)))
@@ -192,31 +194,6 @@ const layer = Layer.effect(
}),
)
const runtime = makeRuntime(Service, layer)
export const defaultLayer = layer
// Fetches available variants and context limits for every provider/model pair.
export async function resolveModelInfo(
sdk: RunInput["sdk"],
directory: string,
model: RunInput["model"],
): Promise<ModelInfo> {
return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)).catch(() => emptyModelInfo())
}
// Fetches session messages to determine if this is the first turn and build prompt history.
export async function resolveSessionInfo(
sdk: RunInput["sdk"],
sessionID: string,
model: RunInput["model"],
): Promise<SessionInfo> {
return runtime.runPromise((svc) => svc.resolveSessionInfo(sdk, sessionID, model)).catch(() => emptySessionInfo())
}
// Reads keybind overrides from TUI config and merges them with defaults.
export async function resolveFooterKeybinds(): Promise<FooterKeybinds> {
return runtime.runPromise((svc) => svc.resolveFooterKeybinds()).catch(() => DEFAULT_KEYBINDS)
}
export async function resolveDiffStyle(): Promise<RunDiffStyle> {
return runtime.runPromise((svc) => svc.resolveDiffStyle()).catch(() => "auto")
}
export * as RunBoot from "./runtime.boot"

View File

@@ -14,13 +14,33 @@
// 4. runs the prompt queue until the footer closes.
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Effect, Layer } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { createRunDemo } from "./demo"
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot"
import {
RunBoot,
defaultKeybinds,
emptyModelInfo,
emptySessionInfo,
type ModelInfo,
type SessionInfo,
} from "./runtime.boot"
import { createRuntimeLifecycle } from "./runtime.lifecycle"
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
import { trace } from "./trace"
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
import type { RunInput, RunPrompt, RunProvider } from "./types"
import { Variant, cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared"
import type { RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
const BootLayer = Layer.mergeAll(RunBoot.defaultLayer, Variant.defaultLayer)
function persistVariant(model: RunInput["model"], variant: string | undefined) {
AppRuntime.runFork(
Effect.gen(function* () {
const variantSvc = yield* Variant.Service
yield* variantSvc.saveVariant(model, variant).pipe(Effect.orElseSucceed(() => undefined))
}).pipe(Effect.provide(BootLayer)),
)
}
/** @internal Exported for testing */
export { pickVariant, resolveVariant } from "./variant.shared"
@@ -169,25 +189,44 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
async (span) => {
const start = performance.now()
const log = trace()
const keybindTask = resolveFooterKeybinds()
const diffTask = resolveDiffStyle()
const earlyTask = AppRuntime.runPromise(
Effect.gen(function* () {
const boot = yield* RunBoot.Service
return yield* Effect.all(
{
keybinds: boot.resolveFooterKeybinds().pipe(Effect.orElseSucceed(() => defaultKeybinds)),
diffStyle: boot.resolveDiffStyle().pipe(Effect.orElseSucceed((): RunDiffStyle => "auto")),
},
{ concurrency: "unbounded" },
)
}).pipe(Effect.provide(BootLayer)),
)
const ctx = await input.boot()
const modelTask = resolveModelInfo(ctx.sdk, ctx.directory, ctx.model)
const sessionTask =
ctx.resume === true
? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
: Promise.resolve({
first: true,
history: [],
variant: undefined,
})
const savedTask = resolveSavedVariant(ctx.model)
const [keybinds, diffStyle, session, savedVariant] = await Promise.all([
keybindTask,
diffTask,
sessionTask,
savedTask,
])
const modelTask = AppRuntime.runPromise(
Effect.gen(function* () {
const boot = yield* RunBoot.Service
return yield* boot.resolveModelInfo(ctx.sdk, ctx.directory, ctx.model)
}).pipe(Effect.provide(BootLayer)),
).catch((): ModelInfo => emptyModelInfo())
const sessionAndSavedTask = AppRuntime.runPromise(
Effect.gen(function* () {
const boot = yield* RunBoot.Service
const variantSvc = yield* Variant.Service
return yield* Effect.all(
{
session:
ctx.resume === true
? boot
.resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
.pipe(Effect.orElseSucceed((): SessionInfo => emptySessionInfo()))
: Effect.succeed<SessionInfo>({ first: true, history: [], variant: undefined }),
savedVariant: variantSvc.resolveSavedVariant(ctx.model).pipe(Effect.orElseSucceed(() => undefined)),
},
{ concurrency: "unbounded" },
)
}).pipe(Effect.provide(BootLayer)),
)
const [{ keybinds, diffStyle }, { session, savedVariant }] = await Promise.all([earlyTask, sessionAndSavedTask])
const state: RuntimeState = {
shown: !session.first,
aborting: false,
@@ -280,7 +319,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
}
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
saveVariant(state.model, state.activeVariant)
persistVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
@@ -298,7 +337,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
state.model = model
state.activeVariant = undefined
state.variants = variantsFor(state.providers, model)
const switching = resolveSavedVariant(model).then((saved) => {
const switching = AppRuntime.runPromise(
Effect.gen(function* () {
const variantSvc = yield* Variant.Service
return yield* variantSvc.resolveSavedVariant(model).pipe(Effect.orElseSucceed(() => undefined))
}).pipe(Effect.provide(BootLayer)),
).then((saved) => {
const current = state.model
if (!current || current.providerID !== model.providerID || current.modelID !== model.modelID) {
return
@@ -343,7 +387,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
}
state.activeVariant = variant
saveVariant(state.model, state.activeVariant)
persistVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})

View File

@@ -9,7 +9,6 @@
import path from "path"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Context, Effect, Layer } from "effect"
import { makeRuntime } from "@/effect/run-service"
import { Global } from "@opencode-ai/core/global"
import { isRecord } from "@/util/record"
import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared"
@@ -20,16 +19,13 @@ const MODEL_FILE = path.join(Global.Path.state, "model.json")
type ModelState = Record<string, unknown> & {
variant?: Record<string, string | undefined>
}
type VariantService = {
export interface Interface {
readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect<string | undefined>
readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect<void>
}
type VariantRuntime = {
resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined>
saveVariant(model: RunInput["model"], variant: string | undefined): Promise<void>
}
class Service extends Context.Service<Service, VariantService>()("@opencode/RunVariant") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/RunVariant") {}
function modelKey(provider: string, model: string): string {
return `${provider}/${model}`
@@ -135,81 +131,62 @@ function state(value: unknown): ModelState {
}
}
function createLayer(fs = AppFileSystem.defaultLayer) {
return Layer.fresh(
Layer.effect(
Service,
Effect.gen(function* () {
const file = yield* AppFileSystem.Service
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const file = yield* AppFileSystem.Service
const read = Effect.fn("RunVariant.read")(function* () {
return yield* file.readJson(MODEL_FILE).pipe(
Effect.map(state),
Effect.catchCause(() => Effect.succeed(state(undefined))),
)
const read = Effect.fn("RunVariant.read")(function* () {
return yield* file.readJson(MODEL_FILE).pipe(
Effect.map(state),
Effect.catchCause(() => Effect.succeed(state(undefined))),
)
})
const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) {
if (!model) {
return undefined
}
return (yield* read()).variant?.[variantKey(model)]
})
const saveVariant = Effect.fn("RunVariant.saveVariant")(function* (
model: RunInput["model"],
variant: string | undefined,
) {
if (!model) {
return
}
const current = yield* read()
const next = {
...current.variant,
}
const key = variantKey(model)
if (variant) {
next[key] = variant
}
if (!variant) {
delete next[key]
}
yield* file
.writeJson(MODEL_FILE, {
...current,
variant: next,
})
.pipe(Effect.orElseSucceed(() => undefined))
})
const resolveSavedVariant = Effect.fn("RunVariant.resolveSavedVariant")(function* (model: RunInput["model"]) {
if (!model) {
return undefined
}
return Service.of({
resolveSavedVariant,
saveVariant,
})
}),
)
return (yield* read()).variant?.[variantKey(model)]
})
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const saveVariant = Effect.fn("RunVariant.saveVariant")(function* (
model: RunInput["model"],
variant: string | undefined,
) {
if (!model) {
return
}
const current = yield* read()
const next = {
...current.variant,
}
const key = variantKey(model)
if (variant) {
next[key] = variant
}
if (!variant) {
delete next[key]
}
yield* file
.writeJson(MODEL_FILE, {
...current,
variant: next,
})
.pipe(Effect.orElseSucceed(() => undefined))
})
return Service.of({
resolveSavedVariant,
saveVariant,
})
}),
).pipe(Layer.provide(fs)),
)
}
/** @internal Exported for testing. */
export function createVariantRuntime(fs = AppFileSystem.defaultLayer): VariantRuntime {
const runtime = makeRuntime(Service, createLayer(fs))
return {
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
}
}
const runtime = createVariantRuntime()
export async function resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined> {
return runtime.resolveSavedVariant(model)
}
export function saveVariant(model: RunInput["model"], variant: string | undefined): void {
void runtime.saveVariant(model, variant)
}
export * as Variant from "./variant.shared"

View File

@@ -1,12 +1,14 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import { afterEach, describe, expect, mock, spyOn } from "bun:test"
import type { KeyEvent, Renderable } from "@opentui/core"
import type { Binding } from "@opentui/keymap"
import { createBindingLookup } from "@opentui/keymap/extras"
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
import { Effect } from "effect"
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
import { formatBindings } from "@/cli/cmd/run/keymap.shared"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
import { RunBoot } from "@/cli/cmd/run/runtime.boot"
import { testEffect } from "../../lib/effect"
type RunBinding = Binding<Renderable, KeyEvent>
@@ -102,186 +104,206 @@ function config(input?: {
}
}
const it = testEffect(RunBoot.defaultLayer)
describe("run runtime boot", () => {
afterEach(() => {
mock.restore()
})
test("reads footer keybinds from resolved keybind config", async () => {
spyOn(TuiConfig, "get").mockResolvedValue(
config({
leader: "ctrl+g",
bindings: {
commandList: bindings("ctrl+p"),
variantCycle: bindings("ctrl+t", "alt+t"),
interrupt: bindings("ctrl+c"),
historyPrevious: bindings("k"),
historyNext: bindings("j"),
inputClear: bindings("ctrl+l"),
inputSubmit: bindings("ctrl+s"),
inputNewline: bindings("alt+return"),
},
}),
)
const result = await resolveFooterKeybinds()
expect(result.leader).toBe("ctrl+g")
expect(result.leaderTimeout).toBe(2000)
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t, alt+t")
expect(formatBindings(result.interrupt, result.leader)).toBe("ctrl+c")
expect(formatBindings(result.historyPrevious, result.leader)).toBe("k")
expect(formatBindings(result.historyNext, result.leader)).toBe("j")
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
})
test("falls back to default keybinds when config load fails", async () => {
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
const result = await resolveFooterKeybinds()
expect(result.leader).toBe("ctrl+x")
expect(result.leaderTimeout).toBe(2000)
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t")
expect(formatBindings(result.interrupt, result.leader)).toBe("esc")
expect(formatBindings(result.historyPrevious, result.leader)).toBe("up")
expect(formatBindings(result.historyNext, result.leader)).toBe("down")
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
})
test("reads diff style and falls back to auto", async () => {
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
await expect(resolveDiffStyle()).resolves.toBe("stacked")
mock.restore()
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
await expect(resolveDiffStyle()).resolves.toBe("auto")
})
test("prefers configured providers for model selector data", async () => {
const sdk = new OpencodeClient()
const data: {
all: Provider[]
default: Record<string, string>
connected: string[]
} = {
all: [
{
id: "openai",
name: "OpenAI",
source: "api",
env: [],
options: {},
models: {
"gpt-5": model("gpt-5", "openai", 128000, {
high: {},
minimal: {},
}),
it.live("reads footer keybinds from resolved keybind config", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockResolvedValue(
config({
leader: "ctrl+g",
bindings: {
commandList: bindings("ctrl+p"),
variantCycle: bindings("ctrl+t", "alt+t"),
interrupt: bindings("ctrl+c"),
historyPrevious: bindings("k"),
historyNext: bindings("j"),
inputClear: bindings("ctrl+l"),
inputSubmit: bindings("ctrl+s"),
inputNewline: bindings("alt+return"),
},
},
{
id: "anthropic",
name: "Anthropic",
source: "api",
env: [],
options: {},
models: {
sonnet: model("sonnet", "anthropic", 200000),
},
},
],
default: {},
connected: [],
}
const configured = {
providers: [data.all[0]!],
default: {},
}
const list = spyOn(sdk.provider, "list").mockImplementation(() =>
Promise.resolve({
data,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
spyOn(sdk.config, "providers").mockImplementation(() =>
Promise.resolve({
data: configured,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
}),
)
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
providers: configured.providers,
variants: ["high", "minimal"],
limits: {
"openai/gpt-5": 128000,
},
})
expect(list).not.toHaveBeenCalled()
})
const boot = yield* RunBoot.Service
const result = yield* boot.resolveFooterKeybinds()
test("falls back to provider list when configured providers are unavailable", async () => {
const sdk = new OpencodeClient()
const data: {
all: Provider[]
default: Record<string, string>
connected: string[]
} = {
all: [
{
id: "openai",
name: "OpenAI",
source: "api",
env: [],
options: {},
models: {
"gpt-5": model("gpt-5", "openai", 128000, {
high: {},
minimal: {},
}),
},
},
{
id: "anthropic",
name: "Anthropic",
source: "api",
env: [],
options: {},
models: {
sonnet: model("sonnet", "anthropic", 200000),
},
},
],
default: {},
connected: [],
}
spyOn(sdk.config, "providers").mockRejectedValue(new Error("boom"))
spyOn(sdk.provider, "list").mockImplementation(() =>
Promise.resolve({
data,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
expect(result.leader).toBe("ctrl+g")
expect(result.leaderTimeout).toBe(2000)
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t, alt+t")
expect(formatBindings(result.interrupt, result.leader)).toBe("ctrl+c")
expect(formatBindings(result.historyPrevious, result.leader)).toBe("k")
expect(formatBindings(result.historyNext, result.leader)).toBe("j")
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
}),
)
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({
providers: data.all,
variants: ["high", "minimal"],
limits: {
"openai/gpt-5": 128000,
"anthropic/sonnet": 200000,
},
})
})
it.live("falls back to default keybinds when config load fails", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
const boot = yield* RunBoot.Service
const result = yield* boot.resolveFooterKeybinds()
expect(result.leader).toBe("ctrl+x")
expect(result.leaderTimeout).toBe(2000)
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t")
expect(formatBindings(result.interrupt, result.leader)).toBe("esc")
expect(formatBindings(result.historyPrevious, result.leader)).toBe("up")
expect(formatBindings(result.historyNext, result.leader)).toBe("down")
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
}),
)
it.live("reads diff style and falls back to auto", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
const boot = yield* RunBoot.Service
expect(yield* boot.resolveDiffStyle()).toBe("stacked")
mock.restore()
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
expect(yield* boot.resolveDiffStyle()).toBe("auto")
}),
)
it.live("prefers configured providers for model selector data", () =>
Effect.gen(function* () {
const sdk = new OpencodeClient()
const data: {
all: Provider[]
default: Record<string, string>
connected: string[]
} = {
all: [
{
id: "openai",
name: "OpenAI",
source: "api",
env: [],
options: {},
models: {
"gpt-5": model("gpt-5", "openai", 128000, {
high: {},
minimal: {},
}),
},
},
{
id: "anthropic",
name: "Anthropic",
source: "api",
env: [],
options: {},
models: {
sonnet: model("sonnet", "anthropic", 200000),
},
},
],
default: {},
connected: [],
}
const configured = {
providers: [data.all[0]!],
default: {},
}
const list = spyOn(sdk.provider, "list").mockImplementation(() =>
Promise.resolve({
data,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
spyOn(sdk.config, "providers").mockImplementation(() =>
Promise.resolve({
data: configured,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
const boot = yield* RunBoot.Service
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
expect(result).toEqual({
providers: configured.providers,
variants: ["high", "minimal"],
limits: {
"openai/gpt-5": 128000,
},
})
expect(list).not.toHaveBeenCalled()
}),
)
it.live("falls back to provider list when configured providers are unavailable", () =>
Effect.gen(function* () {
const sdk = new OpencodeClient()
const data: {
all: Provider[]
default: Record<string, string>
connected: string[]
} = {
all: [
{
id: "openai",
name: "OpenAI",
source: "api",
env: [],
options: {},
models: {
"gpt-5": model("gpt-5", "openai", 128000, {
high: {},
minimal: {},
}),
},
},
{
id: "anthropic",
name: "Anthropic",
source: "api",
env: [],
options: {},
models: {
sonnet: model("sonnet", "anthropic", 200000),
},
},
],
default: {},
connected: [],
}
spyOn(sdk.config, "providers").mockRejectedValue(new Error("boom"))
spyOn(sdk.provider, "list").mockImplementation(() =>
Promise.resolve({
data,
error: undefined,
request: new Request("https://opencode.test"),
response: new Response(),
}),
)
const boot = yield* RunBoot.Service
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
expect(result).toEqual({
providers: data.all,
variants: ["high", "minimal"],
limits: {
"openai/gpt-5": 128000,
"anthropic/sonnet": 200000,
},
})
}),
)
})

View File

@@ -4,13 +4,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { describe, expect, test } from "bun:test"
import { Effect, FileSystem, Layer } from "effect"
import { Global } from "@opencode-ai/core/global"
import {
createVariantRuntime,
cycleVariant,
formatModelLabel,
pickVariant,
resolveVariant,
} from "@/cli/cmd/run/variant.shared"
import { Variant, cycleVariant, formatModelLabel, pickVariant, resolveVariant } from "@/cli/cmd/run/variant.shared"
import type { SessionMessages } from "@/cli/cmd/run/session.shared"
import type { RunProvider } from "@/cli/cmd/run/types"
import { testEffect } from "../../lib/effect"
@@ -171,26 +165,27 @@ describe("run variant shared", () => {
},
})
const svc = createVariantRuntime(remappedFs(root))
yield* Effect.gen(function* () {
const svc = yield* Variant.Service
yield* svc.saveVariant(model, "high")
expect(yield* svc.resolveSavedVariant(model)).toBe("high")
expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: {
"openai/gpt-4.1": "low",
"openai/gpt-5": "high",
},
})
yield* Effect.promise(() => svc.saveVariant(model, "high"))
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: {
"openai/gpt-4.1": "low",
"openai/gpt-5": "high",
},
})
yield* Effect.promise(() => svc.saveVariant(model, undefined))
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBeUndefined()
expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: {
"openai/gpt-4.1": "low",
},
})
yield* svc.saveVariant(model, undefined)
expect(yield* svc.resolveSavedVariant(model)).toBeUndefined()
expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: {
"openai/gpt-4.1": "low",
},
})
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
}),
)
@@ -203,15 +198,16 @@ describe("run variant shared", () => {
yield* filesys.writeFileString(file, "{")
const svc = createVariantRuntime(remappedFs(root))
yield* Effect.promise(() => svc.saveVariant(model, "high"))
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
expect(yield* fs.readJson(file)).toEqual({
variant: {
"openai/gpt-5": "high",
},
})
yield* Effect.gen(function* () {
const svc = yield* Variant.Service
yield* svc.saveVariant(model, "high")
expect(yield* svc.resolveSavedVariant(model)).toBe("high")
expect(yield* fs.readJson(file)).toEqual({
variant: {
"openai/gpt-5": "high",
},
})
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
}),
)
})