diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index ab3d0968925..301781d1321 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -442,6 +442,20 @@ function App() {
local.model.cycleFavorite(-1)
},
},
+ {
+ title: "Switch compaction model",
+ value: "compaction_model.list",
+ keybind: "compaction_model_list",
+ category: "Agent",
+ slash: {
+ name: "compaction-models",
+ aliases: ["compaction-model"],
+ },
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ },
+
{
title: "Switch agent",
value: "agent.list",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index c30b8d12a93..ab3c5ed2d02 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -15,7 +15,7 @@ export function useConnected() {
)
}
-export function DialogModel(props: { providerID?: string }) {
+export function DialogModel(props: { providerID?: string; target?: "session" | "compaction" }) {
const local = useLocal()
const sync = useSync()
const dialog = useDialog()
@@ -25,14 +25,41 @@ export function DialogModel(props: { providerID?: string }) {
const connected = useConnected()
const providers = createDialogProviderOptions()
+ const isCompaction = props.target === "compaction"
+
const showExtra = createMemo(() => connected() && !props.providerID)
+ function onModelSelect(model: { providerID: string; modelID: string }) {
+ dialog.clear()
+ if (isCompaction) {
+ local.model.compaction.set(model)
+ return
+ }
+ local.model.set(model, { recent: true })
+ }
+
const options = createMemo(() => {
const needle = query().trim()
const showSections = showExtra() && needle.length === 0
const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()
+ // "Use session model (default)" option only shown in compaction mode
+ const defaultOption = isCompaction
+ ? [
+ {
+ value: { providerID: "", modelID: "" },
+ title: "Use session model (default)",
+ description: "Compaction will use the same model as the session",
+ category: showSections ? "Default" : undefined,
+ onSelect: () => {
+ dialog.clear()
+ local.model.compaction.clear()
+ },
+ },
+ ]
+ : []
+
function toOptions(items: typeof favorites, category: string) {
if (!showSections) return []
return items.flatMap((item) => {
@@ -49,10 +76,7 @@ export function DialogModel(props: { providerID?: string }) {
category,
disabled: provider.id === "opencode" && model.id.includes("-nano"),
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect: () => {
- dialog.clear()
- local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
- },
+ onSelect: () => onModelSelect({ providerID: provider.id, modelID: model.id }),
},
]
})
@@ -87,10 +111,7 @@ export function DialogModel(props: { providerID?: string }) {
category: connected() ? provider.name : undefined,
disabled: provider.id === "opencode" && model.includes("-nano"),
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect() {
- dialog.clear()
- local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
- },
+ onSelect: () => onModelSelect({ providerID: provider.id, modelID: model }),
})),
filter((x) => {
if (!showSections) return true
@@ -121,19 +142,22 @@ export function DialogModel(props: { providerID?: string }) {
if (needle) {
return [
+ ...defaultOption,
...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
]
}
- return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
+ return [...defaultOption, ...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
})
const provider = createMemo(() =>
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
)
- const title = createMemo(() => provider()?.name ?? "Select model")
+ const title = createMemo(() => (isCompaction ? "Select compaction model" : (provider()?.name ?? "Select model")))
+
+ const current = createMemo(() => (isCompaction ? local.model.compaction.current() : local.model.current()))
return (
[number]["value"]>
@@ -151,7 +175,10 @@ export function DialogModel(props: { providerID?: string }) {
title: "Favorite",
disabled: !connected(),
onTrigger: (option) => {
- local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
+ const val = option.value as { providerID: string; modelID: string }
+ if (val.providerID && val.modelID) {
+ local.model.toggleFavorite(val)
+ }
},
},
]}
@@ -159,7 +186,7 @@ export function DialogModel(props: { providerID?: string }) {
flat={true}
skipFilter={true}
title={title()}
- current={local.model.current()}
+ current={current()}
/>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index d63c248fb83..d3a7d4f279c 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -1012,6 +1012,10 @@ export function Prompt(props: PromptProps) {
{local.model.variant.current()}
+
+ ยท
+ compact: {local.model.compaction.parsed().model}
+
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index d93079f12a4..77197135ea5 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -13,6 +13,7 @@ import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
+import { useKV } from "./kv"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -20,6 +21,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
+ const kv = useKV()
function isModelValid(model: { providerID: string; modelID: string }) {
const provider = sync.data.provider.find((x) => x.id === model.providerID)
@@ -320,6 +322,44 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
save()
})
},
+ compaction: iife(() => {
+ const key = "compaction_model"
+ const [get] = kv.signal<{ providerID: string; modelID: string } | undefined>(key, undefined)
+ return {
+ current() {
+ return get() as { providerID: string; modelID: string } | undefined
+ },
+ parsed: createMemo(() => {
+ const value = get() as { providerID: string; modelID: string } | undefined
+ if (!value) {
+ return {
+ provider: undefined,
+ model: "Using session model",
+ }
+ }
+ const provider = sync.data.provider.find((x) => x.id === value.providerID)
+ const info = provider?.models[value.modelID]
+ return {
+ provider: provider?.name ?? value.providerID,
+ model: info?.name ?? value.modelID,
+ }
+ }),
+ set(model: { providerID: string; modelID: string }) {
+ if (!isModelValid(model)) {
+ toast.show({
+ message: `Model ${model.providerID}/${model.modelID} is not valid`,
+ variant: "warning",
+ duration: 3000,
+ })
+ return
+ }
+ kv.set(key, { ...model })
+ },
+ clear() {
+ kv.set(key, undefined)
+ },
+ }
+ }),
variant: {
current() {
const m = currentModel()
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index f5a7f6f6ca4..aa816d45f60 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -430,10 +430,12 @@ export function Session() {
})
return
}
+ const compactionModel = local.model.compaction.current()
sdk.client.session.summarize({
sessionID: route.sessionID,
modelID: selectedModel.modelID,
providerID: selectedModel.providerID,
+ compactionModel: compactionModel ?? undefined,
})
dialog.clear()
},
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index aad0fd76c4b..cbdaa2a2571 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -828,6 +828,7 @@ export namespace Config {
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
+ compaction_model_list: z.string().optional().default("none").describe("List available compaction models"),
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
agent_list: z.string().optional().default("a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
index 1195529e06a..39b8a1702d4 100644
--- a/packages/opencode/src/server/routes/session.ts
+++ b/packages/opencode/src/server/routes/session.ts
@@ -511,6 +511,12 @@ export const SessionRoutes = lazy(() =>
providerID: z.string(),
modelID: z.string(),
auto: z.boolean().optional().default(false),
+ compactionModel: z
+ .object({
+ providerID: z.string(),
+ modelID: z.string(),
+ })
+ .optional(),
}),
),
async (c) => {
@@ -535,6 +541,7 @@ export const SessionRoutes = lazy(() =>
modelID: body.modelID,
},
auto: body.auto,
+ compactionModel: body.compactionModel,
})
await SessionPrompt.loop({ sessionID })
return c.json(true)
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 9245426057c..3ad507a463f 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -104,12 +104,15 @@ export namespace SessionCompaction {
sessionID: string
abort: AbortSignal
auto: boolean
+ compactionModel?: { providerID: string; modelID: string }
}) {
const userMessage = input.messages.findLast((m) => m.info.id === input.parentID)!.info as MessageV2.User
const agent = await Agent.get("compaction")
- const model = agent.model
- ? await Provider.getModel(agent.model.providerID, agent.model.modelID)
- : await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
+ const model = input.compactionModel
+ ? await Provider.getModel(input.compactionModel.providerID, input.compactionModel.modelID)
+ : agent.model
+ ? await Provider.getModel(agent.model.providerID, agent.model.modelID)
+ : await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
const msg = (await Session.updateMessage({
id: Identifier.ascending("message"),
role: "assistant",
@@ -237,6 +240,12 @@ When constructing the summary, try to stick to this template:
modelID: z.string(),
}),
auto: z.boolean(),
+ compactionModel: z
+ .object({
+ providerID: z.string(),
+ modelID: z.string(),
+ })
+ .optional(),
}),
async (input) => {
const msg = await Session.updateMessage({
@@ -255,6 +264,7 @@ When constructing the summary, try to stick to this template:
sessionID: msg.sessionID,
type: "compaction",
auto: input.auto,
+ compactionModel: input.compactionModel,
})
},
)
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 178751a2227..00dfba2d64e 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -196,6 +196,12 @@ export namespace MessageV2 {
export const CompactionPart = PartBase.extend({
type: z.literal("compaction"),
auto: z.boolean(),
+ compactionModel: z
+ .object({
+ providerID: z.string(),
+ modelID: z.string(),
+ })
+ .optional(),
}).meta({
ref: "CompactionPart",
})
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 75bd3c9dfac..781223a13aa 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -533,6 +533,7 @@ export namespace SessionPrompt {
abort,
sessionID,
auto: task.auto,
+ compactionModel: task.compactionModel,
})
if (result === "stop") break
continue
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index b4848e60540..0df8148fb1f 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -1442,6 +1442,10 @@ export class Session2 extends HeyApiClient {
providerID?: string
modelID?: string
auto?: boolean
+ compactionModel?: {
+ providerID: string
+ modelID: string
+ }
},
options?: Options,
) {
@@ -1455,6 +1459,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "providerID" },
{ in: "body", key: "modelID" },
{ in: "body", key: "auto" },
+ { in: "body", key: "compactionModel" },
],
},
],
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 4050ef15738..87424d53993 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -505,6 +505,10 @@ export type CompactionPart = {
messageID: string
type: "compaction"
auto: boolean
+ compactionModel?: {
+ providerID: string
+ modelID: string
+ }
}
export type Part =
@@ -1167,6 +1171,10 @@ export type KeybindsConfig = {
* Previous favorite model
*/
model_cycle_favorite_reverse?: string
+ /**
+ * List available compaction models
+ */
+ compaction_model_list?: string
/**
* List available commands
*/
@@ -3432,6 +3440,10 @@ export type SessionSummarizeData = {
providerID: string
modelID: string
auto?: boolean
+ compactionModel?: {
+ providerID: string
+ modelID: string
+ }
}
path: {
/**
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 2741c2362ec..8a431f0cc54 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -2303,6 +2303,18 @@
"auto": {
"default": false,
"type": "boolean"
+ },
+ "compactionModel": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
}
},
"required": ["providerID", "modelID"]
@@ -7368,6 +7380,18 @@
},
"auto": {
"type": "boolean"
+ },
+ "compactionModel": {
+ "type": "object",
+ "properties": {
+ "providerID": {
+ "type": "string"
+ },
+ "modelID": {
+ "type": "string"
+ }
+ },
+ "required": ["providerID", "modelID"]
}
},
"required": ["id", "sessionID", "messageID", "type", "auto"]
@@ -8870,6 +8894,11 @@
"default": "none",
"type": "string"
},
+ "compaction_model_list": {
+ "description": "List available compaction models",
+ "default": "none",
+ "type": "string"
+ },
"command_list": {
"description": "List available commands",
"default": "ctrl+p",