oc mini v2 (#38278)
This commit is contained in:
parent
dc9fd126a0
commit
58d18be590
4 changed files with 178 additions and 12 deletions
|
|
@ -115,6 +115,7 @@ type RuntimeState = {
|
||||||
shown: boolean
|
shown: boolean
|
||||||
aborting: boolean
|
aborting: boolean
|
||||||
model: RunInput["model"]
|
model: RunInput["model"]
|
||||||
|
defaultModel: RunInput["model"]
|
||||||
providers: RunProvider[]
|
providers: RunProvider[]
|
||||||
variants: string[]
|
variants: string[]
|
||||||
activeVariant: string | undefined
|
activeVariant: string | undefined
|
||||||
|
|
@ -212,6 +213,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
shown: !session.first,
|
shown: !session.first,
|
||||||
aborting: false,
|
aborting: false,
|
||||||
model: ctx.model ?? session.model,
|
model: ctx.model ?? session.model,
|
||||||
|
defaultModel: undefined,
|
||||||
providers: [],
|
providers: [],
|
||||||
variants: [],
|
variants: [],
|
||||||
activeVariant: resolveVariant(ctx.variant, session.variant, savedVariant, []),
|
activeVariant: resolveVariant(ctx.variant, session.variant, savedVariant, []),
|
||||||
|
|
@ -286,17 +288,19 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
await settleForm(next.sessionID, next.formID)
|
await settleForm(next.sessionID, next.formID)
|
||||||
},
|
},
|
||||||
onCycleVariant: () => {
|
onCycleVariant: () => {
|
||||||
if (!state.model || state.variants.length === 0) {
|
const model = state.model ?? state.defaultModel
|
||||||
|
if (!model || state.variants.length === 0) {
|
||||||
return {
|
return {
|
||||||
status: "no variants available",
|
status: "no variants available",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.model) state.model = model
|
||||||
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
|
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 {
|
return {
|
||||||
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
|
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,
|
variant: state.activeVariant,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -335,7 +339,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onVariantSelect: async (variant) => {
|
onVariantSelect: async (variant) => {
|
||||||
if (!state.model || state.variants.length === 0) {
|
const model = state.model ?? state.defaultModel
|
||||||
|
if (!model || state.variants.length === 0) {
|
||||||
return {
|
return {
|
||||||
status: "no variants available",
|
status: "no variants available",
|
||||||
}
|
}
|
||||||
|
|
@ -347,11 +352,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.model) state.model = model
|
||||||
state.activeVariant = variant
|
state.activeVariant = variant
|
||||||
void input.host.preferences.saveVariant(state.model, state.activeVariant)
|
void input.host.preferences.saveVariant(model, state.activeVariant)
|
||||||
return {
|
return {
|
||||||
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
|
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,
|
variant: state.activeVariant,
|
||||||
variants: state.variants,
|
variants: state.variants,
|
||||||
}
|
}
|
||||||
|
|
@ -602,7 +608,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
) {
|
) {
|
||||||
if (!currentClient(attempt)) return
|
if (!currentClient(attempt)) return
|
||||||
state.providers = info.providers
|
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
|
state.activeVariant = boot
|
||||||
? resolveVariant(ctx.variant, current, saved, state.variants)
|
? resolveVariant(ctx.variant, current, saved, state.variants)
|
||||||
: current && !state.variants.includes(current)
|
: current && !state.variants.includes(current)
|
||||||
|
|
@ -611,11 +618,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
if (footer.isClosed) return
|
if (footer.isClosed) return
|
||||||
footer.event({ type: "models", providers: info.providers })
|
footer.event({ type: "models", providers: info.providers })
|
||||||
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
footer.event({ type: "variants", variants: state.variants, current: state.activeVariant })
|
||||||
if (state.model)
|
if (model)
|
||||||
footer.event({
|
footer.event({
|
||||||
type: "model",
|
type: "model",
|
||||||
model: formatModelLabel(state.model, state.activeVariant, state.providers),
|
model: formatModelLabel(model, state.activeVariant, state.providers),
|
||||||
selection: state.model,
|
selection: model,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,6 +634,50 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
task: Promise<void>
|
task: Promise<void>
|
||||||
}
|
}
|
||||||
| undefined
|
| 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 requestCatalogRefresh = (signal?: AbortSignal): Promise<void> => {
|
||||||
const attempt = clientAttempt(signal)
|
const attempt = clientAttempt(signal)
|
||||||
if (!currentClient(attempt)) return Promise.resolve()
|
if (!currentClient(attempt)) return Promise.resolve()
|
||||||
|
|
@ -658,6 +709,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
if (!currentClient(attempt)) return
|
if (!currentClient(attempt)) return
|
||||||
if (catalog) applyCatalog(catalog, attempt)
|
if (catalog) applyCatalog(catalog, attempt)
|
||||||
if (info) applyModelInfo(info, state.activeVariant, attempt)
|
if (info) applyModelInfo(info, state.activeVariant, attempt)
|
||||||
|
loadDefaultModel(attempt)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
refresh.task = task
|
refresh.task = task
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,19 @@ async function resolveSelectedModel(
|
||||||
.then((response) => response.model)
|
.then((response) => response.model)
|
||||||
if (session) return { ...session, variant: next.variant }
|
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
|
if (!fallback) return
|
||||||
return { providerID: fallback.providerID, id: fallback.id, variant: next.variant }
|
return { providerID: fallback.providerID, id: fallback.id, variant: next.variant }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,103 @@ afterEach(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("run interactive runtime", () => {
|
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 () => {
|
test("routes form responses to their owners with global location and local settlement", async () => {
|
||||||
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
|
||||||
const api = footer()
|
const api = footer()
|
||||||
|
|
|
||||||
|
|
@ -2085,12 +2085,13 @@ describe("V2 mini transport", () => {
|
||||||
const ui = footer()
|
const ui = footer()
|
||||||
const transport = await createSessionTransport({
|
const transport = await createSessionTransport({
|
||||||
sdk: client,
|
sdk: client,
|
||||||
|
location: { directory: "/project", workspaceID: "wrk_1" },
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
thinking: false,
|
thinking: false,
|
||||||
footer: ui.api,
|
footer: ui.api,
|
||||||
})
|
})
|
||||||
spyOn(client.session, "get").mockImplementation(() => ok({ model: undefined }) as never)
|
spyOn(client.session, "get").mockImplementation(() => ok({ model: undefined }) as never)
|
||||||
spyOn(client.model, "default").mockImplementation(
|
const defaultModel = spyOn(client.model, "default").mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
ok({
|
ok({
|
||||||
location: { directory: "/tmp", project: { id: "proj_1", directory: "/tmp" } },
|
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" } },
|
{ sessionID: "ses_1", model: { providerID: "openai", id: "gpt-5", variant: "high" } },
|
||||||
{ signal: undefined },
|
{ signal: undefined },
|
||||||
)
|
)
|
||||||
|
expect(defaultModel).toHaveBeenCalledWith(
|
||||||
|
{ location: { directory: "/project", workspace: "wrk_1" } },
|
||||||
|
{ signal: undefined },
|
||||||
|
)
|
||||||
await transport.close()
|
await transport.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue