// Model variant resolution and persistence. // // Variants are provider-specific reasoning effort levels (e.g., "high", "max"). // Resolution priority: CLI --variant flag > saved preference > session history. // // The saved variant persists across sessions in ~/.local/state/opencode/model.json // so your last-used variant sticks. Cycling (ctrl+t) updates both the active // variant and the persisted file. import path from "path" import { FSUtil } from "@opencode-ai/core/fs-util" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Context, Effect, Layer } from "effect" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/core/effect/app-node" import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Global } from "@opencode-ai/core/global" import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared" import type { RunInput, RunProvider } from "./types" const MODEL_FILE = path.join(Global.Path.state, "model.json") type ModelState = Record & { variant?: Record } type VariantService = { readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect } type VariantRuntime = { resolveSavedVariant(model: RunInput["model"]): Promise saveVariant(model: RunInput["model"], variant: string | undefined): Promise } function isRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value) } class Service extends Context.Service()("@opencode/RunVariant") {} function modelKey(provider: string, model: string): string { return `${provider}/${model}` } function variantKey(model: NonNullable): string { return modelKey(model.providerID, model.modelID) } export function modelInfo(providers: RunProvider[] | undefined, model: NonNullable) { const provider = providers?.find((item) => item.id === model.providerID) return { provider: provider?.name ?? model.providerID, model: provider?.models[model.modelID]?.name ?? model.modelID, } } export function formatModelLabel( model: NonNullable, variant: string | undefined, providers?: RunProvider[], ): string { const names = modelInfo(providers, model) const label = variant ? ` · ${variant}` : "" return `${names.model} · ${names.provider}${label}` } export function cycleVariant(current: string | undefined, variants: string[]): string | undefined { if (variants.length === 0) { return undefined } if (!current) { return variants[0] } const idx = variants.indexOf(current) if (idx === -1 || idx === variants.length - 1) { return undefined } return variants[idx + 1] } export function pickVariant(model: RunInput["model"], input: RunSession | SessionMessages): string | undefined { return sessionVariant(Array.isArray(input) ? createSession(input) : input, model) } function fitVariant(value: string | undefined, variants: string[]): string | undefined { if (!value) { return undefined } if (variants.length === 0 || variants.includes(value)) { return value } return undefined } // Picks the active variant. CLI flag wins, then saved preference, then session // history. fitVariant() checks saved and session values against the available // variants list -- if the provider doesn't offer a variant, it drops. export function resolveVariant( input: string | undefined, session: string | undefined, saved: string | undefined, variants: string[], ): string | undefined { if (input !== undefined) { return input } const fallback = fitVariant(saved, variants) const current = fitVariant(session, variants) if (current !== undefined) { return current } return fallback } function state(value: unknown): ModelState { if (!isRecord(value)) { return {} } const variant = isRecord(value.variant) ? Object.fromEntries( Object.entries(value.variant).flatMap(([key, item]) => { if (typeof item !== "string") { return [] } return [[key, item] as const] }), ) : undefined return { ...value, variant, } } const layer = Layer.fresh( Layer.effect( Service, Effect.gen(function* () { const file = yield* FSUtil.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 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)) }) return Service.of({ resolveSavedVariant, saveVariant, }) }), ), ) const node = makeGlobalNode({ service: Service, layer, deps: [FSUtil.node] }) /** @internal Exported for testing. */ export function createVariantRuntime(replacements?: readonly LayerNode.Replacement[]): VariantRuntime { const runtime = makeRuntime(Service, AppNodeBuilder.build(node, replacements)) 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 { return runtime.resolveSavedVariant(model) } export function saveVariant(model: RunInput["model"], variant: string | undefined): void { void runtime.saveVariant(model, variant) }