feat(model): add provider fast mode toggle

This commit is contained in:
Rhys Sullivan 2026-03-16 11:46:41 -07:00
commit e73ec6d2d7
19 changed files with 337 additions and 9 deletions

View file

@ -1023,6 +1023,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const variants = createMemo(() => ["default", ...local.model.variant.list()])
const fast = createMemo(() => local.model.fast.available())
const fastLabel = createMemo(() =>
language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
)
const accepting = createMemo(() => {
const id = params.id
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
@ -1534,6 +1538,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
</TooltipKeybind>
</div>
<Show when={fast()}>
<Tooltip placement="top" gutter={8} value={fastLabel()}>
<Button
data-action="prompt-fast"
variant="ghost"
onClick={() => local.model.fast.toggle()}
class="h-7 px-2 shrink-0 text-13-medium"
classList={{
"text-text-base": !local.model.fast.current(),
"text-icon-warning-base bg-surface-warning-base": local.model.fast.current(),
}}
style={control()}
aria-label={fastLabel()}
aria-pressed={local.model.fast.current()}
>
{language.t("command.model.fast.label")}
</Button>
</Tooltip>
</Show>
<TooltipKeybind
placement="top"
gutter={8}

View file

@ -34,6 +34,7 @@ export type FollowupDraft = {
agent: string
model: { providerID: string; modelID: string }
variant?: string
fast?: boolean
}
type FollowupSendInput = {
@ -88,6 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
agent: input.draft.agent,
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
variant: input.draft.variant,
fast: input.draft.fast,
parts: images.map((attachment) => ({
id: Identifier.ascending("part"),
type: "file" as const,
@ -122,6 +124,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
agent: input.draft.agent,
model: input.draft.model,
variant: input.draft.variant,
fast: input.draft.fast,
}
const add = () =>
@ -156,6 +159,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
messageID,
parts: requestParts,
variant: input.draft.variant,
fast: input.draft.fast,
})
return true
} catch (err) {
@ -297,6 +301,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const currentModel = local.model.current()
const currentAgent = local.agent.current()
const variant = local.model.variant.current()
const fast = local.model.fast.current()
if (!currentModel || !currentAgent) {
showToast({
title: language.t("prompt.toast.modelAgentRequired.title"),
@ -398,6 +403,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
agent,
model,
variant,
fast,
}
const clearInput = () => {
@ -461,6 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
agent,
model: `${model.providerID}/${model.modelID}`,
variant,
fast,
parts: images.map((attachment) => ({
id: Identifier.ascending("part"),
type: "file" as const,

View file

@ -8,6 +8,7 @@ import { useProviders } from "@/hooks/use-providers"
import { modelEnabled, modelProbe } from "@/testing/model-selection"
import { Persist, persisted } from "@/utils/persist"
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
import * as Fast from "./model-fast"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
@ -17,6 +18,7 @@ type State = {
agent?: string
model?: ModelKey
variant?: string | null
fast?: boolean
}
type Saved = {
@ -79,10 +81,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
current?: string
draft?: State
last?: {
type: "agent" | "model" | "variant"
type: "agent" | "model" | "variant" | "fast"
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
}>({
current: list()[0]?.name,
@ -191,11 +194,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: item.name,
model: item.model,
variant: item.variant ?? null,
fast: scope()?.fast,
})
const next = {
agent: item.name,
model: item.model,
variant: item.variant,
fast: scope()?.fast,
} satisfies State
const session = id()
if (session) {
@ -249,6 +254,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : undefined,
variant: selected(),
fast: !!scope()?.fast && Fast.enabled(model),
} satisfies State
}
@ -296,6 +302,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: item ?? null,
variant: selected(),
fast: model.fast.current(),
})
write({ model: item })
if (!item) return
@ -333,6 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
variant: value ?? null,
fast: !!scope()?.fast && Fast.enabled(model),
})
write({ variant: value ?? null })
})
@ -349,6 +357,34 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
)
},
},
fast: {
selected() {
return scope()?.fast === true
},
current() {
return this.selected() && this.available()
},
available() {
return Fast.enabled(current())
},
set(value: boolean) {
if (value && !this.available()) return
const model = current()
batch(() => {
setStore("last", {
type: "fast",
agent: agent.current()?.name,
model: model ? { providerID: model.provider.id, modelID: model.id } : null,
variant: selected(),
fast: value,
})
write({ fast: value || undefined })
})
},
toggle() {
this.set(!this.current())
},
},
}
const result = {
@ -372,7 +408,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
handoff.set(handoffKey(dir, session), next)
setStore("draft", undefined)
},
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string; fast?: boolean }) {
const session = id()
if (!session) return
if (msg.sessionID !== session) return
@ -383,6 +419,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: msg.agent,
model: msg.model,
variant: msg.variant ?? null,
fast: msg.fast === true,
})
},
},
@ -405,6 +442,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
: undefined,
variant: result.model.variant.current() ?? null,
fast: result.model.fast.current(),
selected: result.model.variant.selected(),
configured: result.model.variant.configured(),
pick: scope(),

View file

@ -0,0 +1,28 @@
type Model = {
id: string
provider: {
id: string
}
}
function lower(model: Model) {
return model.id.toLowerCase()
}
export function kind(model: Model | undefined) {
if (!model) return
const id = lower(model)
if (
model.provider.id === "anthropic" &&
(id.includes("claude-opus-4-6") || id.includes("claude-opus-4.6") || id.includes("opus-4-6"))
) {
return "claude"
}
if (model.provider.id === "openai" && id.includes("gpt-5.4")) {
return "codex"
}
}
export function enabled(model: Model | undefined) {
return !!kind(model)
}

View file

@ -67,6 +67,10 @@ export const dict = {
"command.agent.cycle.description": "Switch to the next agent",
"command.agent.cycle.reverse": "Cycle agent backwards",
"command.agent.cycle.reverse.description": "Switch to the previous agent",
"command.model.fast.label": "Fast",
"command.model.fast.enable": "Enable fast mode",
"command.model.fast.disable": "Disable fast mode",
"command.model.fast.description": "Toggle provider fast mode for supported Claude and Codex models",
"command.model.variant.cycle": "Cycle thinking effort",
"command.model.variant.cycle.description": "Switch to the next effort level",
"command.prompt.mode.shell": "Shell",

View file

@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant" | "fast">>) =>
({
id: "msg",
sessionID: "session",
@ -31,6 +31,24 @@ describe("syncSessionModel", () => {
expect(calls).toEqual([message({ variant: "high" })])
})
test("restores fast mode from the last message", () => {
const calls: unknown[] = []
syncSessionModel(
{
session: {
restore(value) {
calls.push(value)
},
reset() {},
},
},
message({ fast: true }),
)
expect(calls).toEqual([message({ fast: true })])
})
})
describe("resetSessionModel", () => {

View file

@ -353,6 +353,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
slash: "model",
onSelect: () => dialog.show(() => <DialogSelectModel model={local.model} />),
}),
modelCommand({
id: "model.fast.toggle",
title: language.t(local.model.fast.current() ? "command.model.fast.disable" : "command.model.fast.enable"),
description: language.t("command.model.fast.description"),
slash: "fast",
disabled: !local.model.fast.available(),
onSelect: () => local.model.fast.toggle(),
}),
mcpCommand({
id: "mcp.toggle",
title: language.t("command.mcp.toggle"),

View file

@ -7,20 +7,23 @@ type State = {
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
export type ModelProbeState = {
dir?: string
sessionID?: string
last?: {
type: "agent" | "model" | "variant"
type: "agent" | "model" | "variant" | "fast"
agent?: string
model?: ModelKey | null
variant?: string | null
fast?: boolean
}
agent?: string
model?: (ModelKey & { name?: string }) | undefined
variant?: string | null
fast?: boolean
selected?: string | null
configured?: string
pick?: State