oc mini v2 (#38278)

This commit is contained in:
Simon Klee
2026-07-22 11:50:21 +02:00
committed by GitHub
parent dc9fd126a0
commit 58d18be590
4 changed files with 178 additions and 12 deletions
+62 -10
View File
@@ -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
+13 -1
View File
@@ -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 }
}
+97
View File
@@ -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()
})