mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-25 04:35:37 -04:00
oc mini v2 (#38278)
This commit is contained in:
@@ -115,6 +115,7 @@ type RuntimeState = {
|
||||
shown: boolean
|
||||
aborting: boolean
|
||||
model: RunInput["model"]
|
||||
defaultModel: RunInput["model"]
|
||||
providers: RunProvider[]
|
||||
variants: string[]
|
||||
activeVariant: string | undefined
|
||||
@@ -212,6 +213,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
shown: !session.first,
|
||||
aborting: false,
|
||||
model: ctx.model ?? session.model,
|
||||
defaultModel: undefined,
|
||||
providers: [],
|
||||
variants: [],
|
||||
activeVariant: resolveVariant(ctx.variant, session.variant, savedVariant, []),
|
||||
@@ -286,17 +288,19 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
await settleForm(next.sessionID, next.formID)
|
||||
},
|
||||
onCycleVariant: () => {
|
||||
if (!state.model || state.variants.length === 0) {
|
||||
const model = state.model ?? state.defaultModel
|
||||
if (!model || state.variants.length === 0) {
|
||||
return {
|
||||
status: "no variants available",
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.model) state.model = model
|
||||
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
|
||||
void input.host.preferences.saveVariant(state.model, state.activeVariant)
|
||||
void input.host.preferences.saveVariant(model, state.activeVariant)
|
||||
return {
|
||||
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
|
||||
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
|
||||
modelLabel: formatModelLabel(model, state.activeVariant, state.providers),
|
||||
variant: state.activeVariant,
|
||||
}
|
||||
},
|
||||
@@ -335,7 +339,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
}
|
||||
},
|
||||
onVariantSelect: async (variant) => {
|
||||
if (!state.model || state.variants.length === 0) {
|
||||
const model = state.model ?? state.defaultModel
|
||||
if (!model || state.variants.length === 0) {
|
||||
return {
|
||||
status: "no variants available",
|
||||
}
|
||||
@@ -347,11 +352,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.model) state.model = model
|
||||
state.activeVariant = variant
|
||||
void input.host.preferences.saveVariant(state.model, state.activeVariant)
|
||||
void input.host.preferences.saveVariant(model, state.activeVariant)
|
||||
return {
|
||||
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
|
||||
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
|
||||
modelLabel: formatModelLabel(model, state.activeVariant, state.providers),
|
||||
variant: state.activeVariant,
|
||||
variants: state.variants,
|
||||
}
|
||||
@@ -602,7 +608,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
) {
|
||||
if (!currentClient(attempt)) return
|
||||
state.providers = info.providers
|
||||
state.variants = variantsFor(state.providers, state.model)
|
||||
const model = state.model ?? state.defaultModel
|
||||
state.variants = variantsFor(state.providers, model)
|
||||
state.activeVariant = boot
|
||||
? resolveVariant(ctx.variant, current, saved, state.variants)
|
||||
: current && !state.variants.includes(current)
|
||||
@@ -611,11 +618,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
if (footer.isClosed) return
|
||||
footer.event({ type: "models", providers: info.providers })
|
||||
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
||||
if (state.model)
|
||||
if (model)
|
||||
footer.event({
|
||||
type: "model",
|
||||
model: formatModelLabel(state.model, state.activeVariant, state.providers),
|
||||
selection: state.model,
|
||||
model: formatModelLabel(model, state.activeVariant, state.providers),
|
||||
selection: model,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -627,6 +634,50 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
task: Promise<void>
|
||||
}
|
||||
| undefined
|
||||
let defaultModelLoad: Promise<void> | undefined
|
||||
let defaultModelQueued = false
|
||||
const loadDefaultModel = (attempt: ClientAttempt) => {
|
||||
if (state.model || !currentClient(attempt)) return
|
||||
if (defaultModelLoad) {
|
||||
defaultModelQueued = true
|
||||
return
|
||||
}
|
||||
defaultModelQueued = false
|
||||
defaultModelLoad = attempt.sdk.model
|
||||
.default(
|
||||
{
|
||||
location: {
|
||||
directory: state.location.directory,
|
||||
workspace: state.location.workspaceID,
|
||||
},
|
||||
},
|
||||
{ signal: attempt.signal },
|
||||
)
|
||||
.then(async (result) => {
|
||||
if (!result.data || state.model || !currentClient(attempt)) return
|
||||
const model = { providerID: result.data.providerID, modelID: result.data.id }
|
||||
const changed =
|
||||
state.defaultModel?.providerID !== model.providerID || state.defaultModel.modelID !== model.modelID
|
||||
const saved = changed ? await input.host.preferences.resolveVariant(model) : undefined
|
||||
if (state.model || !currentClient(attempt)) return
|
||||
state.defaultModel = model
|
||||
state.variants = variantsFor(state.providers, model)
|
||||
if (changed)
|
||||
state.activeVariant = resolveVariant(ctx.variant, state.activeVariant, saved, state.variants)
|
||||
if (state.activeVariant) state.model = model
|
||||
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
||||
footer.event({
|
||||
type: "model",
|
||||
model: formatModelLabel(model, state.activeVariant, state.providers),
|
||||
selection: model,
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
defaultModelLoad = undefined
|
||||
if (defaultModelQueued) loadDefaultModel(clientAttempt())
|
||||
})
|
||||
}
|
||||
const requestCatalogRefresh = (signal?: AbortSignal): Promise<void> => {
|
||||
const attempt = clientAttempt(signal)
|
||||
if (!currentClient(attempt)) return Promise.resolve()
|
||||
@@ -658,6 +709,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
if (!currentClient(attempt)) return
|
||||
if (catalog) applyCatalog(catalog, attempt)
|
||||
if (info) applyModelInfo(info, state.activeVariant, attempt)
|
||||
loadDefaultModel(attempt)
|
||||
}
|
||||
})()
|
||||
refresh.task = task
|
||||
|
||||
@@ -380,7 +380,19 @@ async function resolveSelectedModel(
|
||||
.then((response) => response.model)
|
||||
if (session) return { ...session, variant: next.variant }
|
||||
|
||||
const fallback = await sdk.model.default(undefined, { signal: next.signal }).then((response) => response.data)
|
||||
const fallback = await sdk.model
|
||||
.default(
|
||||
input.location
|
||||
? {
|
||||
location: {
|
||||
directory: input.location.directory,
|
||||
workspace: input.location.workspaceID,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{ signal: next.signal },
|
||||
)
|
||||
.then((response) => response.data)
|
||||
if (!fallback) return
|
||||
return { providerID: fallback.providerID, id: fallback.id, variant: next.variant }
|
||||
}
|
||||
|
||||
@@ -48,6 +48,103 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe("run interactive runtime", () => {
|
||||
test("resolves the default model reactively without blocking catalog startup", async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const events: FooterEvent[] = []
|
||||
const ui = createFooterApiFixture({ events })
|
||||
const api = ui.api
|
||||
const selected = defer<Awaited<ReturnType<typeof sdk.model.default>>>()
|
||||
const catalogLoaded = defer<void>()
|
||||
const model = catalogModel({
|
||||
id: "resolved",
|
||||
providerID: "test",
|
||||
name: "Resolved Model",
|
||||
variants: ["low", "high"],
|
||||
})
|
||||
let lifecycle!: LifecycleInput
|
||||
let turnModel: { providerID: string; modelID: string } | undefined
|
||||
let refreshCatalog: (() => Promise<unknown>) | undefined
|
||||
stubCatalogLists(sdk, {
|
||||
providers: [catalogProvider("test", "Test Provider")],
|
||||
models: [model],
|
||||
})
|
||||
const defaultModel = spyOn(sdk.model, "default").mockImplementation(() => selected.promise)
|
||||
|
||||
const task = runInteractiveDeferredMode(
|
||||
{
|
||||
host: host(),
|
||||
sdk,
|
||||
directory: "/tmp",
|
||||
target: async () => ({
|
||||
sessionID: "ses_root",
|
||||
location: { directory: "/tmp", project: { id: "pro-1", directory: "/tmp" } },
|
||||
agent: "build",
|
||||
model: undefined,
|
||||
variant: undefined,
|
||||
resume: false,
|
||||
}),
|
||||
agent: "build",
|
||||
model: undefined,
|
||||
variant: undefined,
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
createRuntimeLifecycle: async (input) => {
|
||||
lifecycle = input
|
||||
return {
|
||||
footer: api,
|
||||
onResize: () => () => {},
|
||||
refreshTheme: () => {},
|
||||
setTitle: () => {},
|
||||
resetForReplay: () => Promise.resolve(),
|
||||
close: () => Promise.resolve(),
|
||||
}
|
||||
},
|
||||
streamTransport: Promise.resolve({
|
||||
createSessionTransport: async (input) => {
|
||||
refreshCatalog = () => Promise.resolve(input.onCatalogRefresh?.())
|
||||
await refreshCatalog()
|
||||
catalogLoaded.resolve()
|
||||
return {
|
||||
runPromptTurn: async (input) => {
|
||||
turnModel = input.model
|
||||
api.close()
|
||||
},
|
||||
queuePromptTurn: async () => {},
|
||||
waitForIdle: async () => {},
|
||||
interruptActiveTurn: async () => {},
|
||||
selectSubagent: () => {},
|
||||
replayOnResize: async () => false,
|
||||
close: async () => {},
|
||||
}
|
||||
},
|
||||
formatUnknownError: (error: unknown) => String(error),
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
await catalogLoaded.promise
|
||||
expect(events.some((event) => event.type === "model")).toBe(false)
|
||||
await refreshCatalog?.()
|
||||
expect(defaultModel).toHaveBeenCalledTimes(1)
|
||||
selected.resolve({
|
||||
location: { directory: "/tmp", project: { id: "pro-1", directory: "/tmp" } },
|
||||
data: model,
|
||||
})
|
||||
while (defaultModel.mock.calls.length < 2) await Bun.sleep(0)
|
||||
while (!events.some((event) => event.type === "model")) await Bun.sleep(0)
|
||||
expect(events).toContainEqual({
|
||||
type: "model",
|
||||
model: "Resolved Model · Test Provider",
|
||||
selection: { providerID: "test", modelID: "resolved" },
|
||||
})
|
||||
expect(lifecycle.onCycleVariant?.()).toMatchObject({ status: "variant low", variant: "low" })
|
||||
ui.submit("hello")
|
||||
while (!turnModel) await Bun.sleep(0)
|
||||
expect(turnModel).toEqual({ providerID: "test", modelID: "resolved" })
|
||||
await task
|
||||
})
|
||||
|
||||
test("routes form responses to their owners with global location and local settlement", async () => {
|
||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||
const api = footer()
|
||||
|
||||
@@ -2085,12 +2085,13 @@ describe("V2 mini transport", () => {
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: client,
|
||||
location: { directory: "/project", workspaceID: "wrk_1" },
|
||||
sessionID: "ses_1",
|
||||
thinking: false,
|
||||
footer: ui.api,
|
||||
})
|
||||
spyOn(client.session, "get").mockImplementation(() => ok({ model: undefined }) as never)
|
||||
spyOn(client.model, "default").mockImplementation(
|
||||
const defaultModel = spyOn(client.model, "default").mockImplementation(
|
||||
() =>
|
||||
ok({
|
||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
||||
@@ -2138,6 +2139,10 @@ describe("V2 mini transport", () => {
|
||||
{ sessionID: "ses_1", model: { providerID: "openai", id: "gpt-5", variant: "high" } },
|
||||
{ signal: undefined },
|
||||
)
|
||||
expect(defaultModel).toHaveBeenCalledWith(
|
||||
{ location: { directory: "/project", workspace: "wrk_1" } },
|
||||
{ signal: undefined },
|
||||
)
|
||||
await transport.close()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user