From cac8e168bbc56a15140b7f9b51c4c8403304fb17 Mon Sep 17 00:00:00 2001 From: zszz3 <91608029+zszz3@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:40:30 +0800 Subject: [PATCH 1/3] feat(session): add /fast for OpenAI's priority service tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatGPT subscribers and OpenAI API users can ask for a faster priority tier (Responses `service_tier: "priority"`), which costs more plan limits or a higher per-token price. PI-Desktop never requested a tier. Model settings now own whether a model may use it: a per-model "Fast mode" opt-in, enabled for Responses wires, stored in the existing binding JSON. The cost is a per-conversation choice, so `/fast` toggles a persisted session switch (schema v25, additive). On a model that is not configured, `/fast` stores nothing and points to Settings → Models → Advanced. Launch sends Fast mode only when both are on, and only main turns carry the field; subagents and compaction keep the default tier. closes #1657 --- .../desktop/electron/main/builtin-commands.ts | 1 + apps/desktop/electron/main/ipc/session-ipc.ts | 1 + .../electron/main/remote/remote-backend.ts | 9 +- .../electron/main/runtime/session-launch.ts | 3 + apps/desktop/src/components/Composer.tsx | 2 + apps/desktop/src/components/icons.tsx | 2 + .../settings/ModelSelectionPanes.tsx | 31 ++++ .../src/components/settings/provider-copy.ts | 1 + .../chat/composer/ComposerModelPicker.tsx | 15 +- .../chat/composer/ComposerToolbar.tsx | 4 + apps/desktop/src/lib/api.ts | 2 +- apps/desktop/src/lib/commands.ts | 4 + apps/desktop/src/lib/fast-mode-command.ts | 78 +++++++++ apps/desktop/src/lib/session-thinking.ts | 1 + apps/desktop/src/stores/app-state.ts | 12 +- .../stores/runtime/session-coordination.ts | 1 + .../src/stores/runtime/session-runtime.ts | 2 +- .../src/stores/slices/session-slice.ts | 1 + apps/desktop/src/styles/composer-menus.css | 8 + apps/desktop/test/builtin-commands.test.mjs | 5 +- apps/desktop/test/fast-mode-command.test.mjs | 155 ++++++++++++++++++ .../test/fast-mode-session-launch.test.mjs | 89 ++++++++++ .../test/model-advanced-capabilities.test.mjs | 12 ++ crates/host-core/src/db.rs | 4 +- crates/host-core/src/db/repositories.rs | 7 + crates/host-core/src/db/schema.rs | 1 + .../src/db/session_fast_mode_migration.rs | 33 ++++ crates/host-core/src/db/tests.rs | 37 +++++ crates/host-core/src/plugins/providers.rs | 1 + .../host-core/src/plugins/providers/tests.rs | 8 +- crates/host-core/src/providers/catalog.rs | 3 + crates/host-core/src/providers/model.rs | 4 + crates/host-core/src/providers/repository.rs | 1 + crates/host-core/src/providers/tests.rs | 8 + crates/host-core/src/rpc/mod.rs | 72 +++++++- crates/host-core/src/rpc/scheduled_rpc.rs | 2 + .../src/session_collaboration/mod.rs | 2 + crates/host-core/src/session_search.rs | 3 +- .../src/session_worktrees/delegate/tests.rs | 1 + .../src/session_worktrees/lifecycle/tests.rs | 2 + crates/host-core/src/sessions.rs | 128 ++++++++++++++- crates/host-core/src/turn_queue.rs | 2 +- docs/adr/0332-openai-fast-mode.md | 69 ++++++++ docs/adr/README.md | 1 + docs/project/unreleased.md | 6 + docs/spec/03-runtime/01-ipc-protocol.md | 2 + docs/spec/03-runtime/04-data-storage.md | 15 +- docs/spec/03-runtime/06-host-rpc-protocol.md | 6 +- .../03-runtime/11-provider-model-system.md | 12 ++ docs/spec/04-ux/04-builtin-commands.md | 23 ++- docs/spec/06-delivery/04-e2e-test-plan.md | 31 ++++ docs/zh-CN/spec/03-runtime/01-ipc-protocol.md | 2 + docs/zh-CN/spec/03-runtime/04-data-storage.md | 13 +- .../spec/03-runtime/06-host-rpc-protocol.md | 6 +- .../03-runtime/11-provider-model-system.md | 9 + docs/zh-CN/spec/04-ux/04-builtin-commands.md | 18 +- .../spec/06-delivery/04-e2e-test-plan.md | 18 ++ packages/agent-runtime/src/fast-mode.test.ts | 75 +++++++++ packages/agent-runtime/src/fast-mode.ts | 34 ++++ packages/agent-runtime/src/runtime.ts | 35 +++- packages/agent-runtime/src/sidecar.ts | 4 + packages/i18n/src/locales/de/index.ts | 11 ++ packages/i18n/src/locales/en/index.ts | 11 ++ packages/i18n/src/locales/es/index.ts | 11 ++ packages/i18n/src/locales/fr/index.ts | 11 ++ packages/i18n/src/locales/ko/index.ts | 11 ++ packages/i18n/src/locales/pt-BR/index.ts | 11 ++ packages/i18n/src/locales/tr/index.ts | 11 ++ packages/i18n/src/locales/zh-CN/index.ts | 11 ++ packages/i18n/src/locales/zh-TW/index.ts | 11 ++ packages/shared/src/fast-mode.test.ts | 19 +++ packages/shared/src/fast-mode.ts | 26 +++ packages/shared/src/index.ts | 1 + packages/shared/src/model-config-import.ts | 3 + packages/shared/src/provider-sync.ts | 4 +- packages/shared/src/types/models.ts | 6 + packages/shared/src/types/sessions.ts | 6 + 77 files changed, 1244 insertions(+), 57 deletions(-) create mode 100644 apps/desktop/src/lib/fast-mode-command.ts create mode 100644 apps/desktop/test/fast-mode-command.test.mjs create mode 100644 apps/desktop/test/fast-mode-session-launch.test.mjs create mode 100644 crates/host-core/src/db/session_fast_mode_migration.rs create mode 100644 docs/adr/0332-openai-fast-mode.md create mode 100644 packages/agent-runtime/src/fast-mode.test.ts create mode 100644 packages/agent-runtime/src/fast-mode.ts create mode 100644 packages/shared/src/fast-mode.test.ts create mode 100644 packages/shared/src/fast-mode.ts diff --git a/apps/desktop/electron/main/builtin-commands.ts b/apps/desktop/electron/main/builtin-commands.ts index 28ca880c80..e84cf36a3a 100644 --- a/apps/desktop/electron/main/builtin-commands.ts +++ b/apps/desktop/electron/main/builtin-commands.ts @@ -15,6 +15,7 @@ export const BUILTIN_COMMANDS: BuiltinCommandDef[] = [ { id: "builtin.mode.agent", title: "Switch to Agent mode", category: "Session", keywords: ["mode", "agent"], source: "builtin", slash: "agent-mode" }, { id: "builtin.mode.plan", title: "Switch to Plan mode", category: "Session", keywords: ["mode", "plan", "planning"], source: "builtin", slash: "plan-mode" }, { id: "builtin.mode.goal", title: "Switch to Goal mode", category: "Session", keywords: ["mode", "goal", "objective", "autonomous"], source: "builtin", slash: "goal-mode" }, + { id: "builtin.session.fast", title: "Toggle Fast mode", category: "Session", keywords: ["fast", "speed", "priority", "service tier"], source: "builtin", slash: "fast" }, ]; /** Palette-shaped items (no slash field leaks into the palette contract). */ diff --git a/apps/desktop/electron/main/ipc/session-ipc.ts b/apps/desktop/electron/main/ipc/session-ipc.ts index f62be5b1e4..1fb56753a7 100644 --- a/apps/desktop/electron/main/ipc/session-ipc.ts +++ b/apps/desktop/electron/main/ipc/session-ipc.ts @@ -603,6 +603,7 @@ export function registerSessionIpc({ modelId?: string; thinkingLevel?: SessionThinkingLevel; permissionMode?: "inherit" | "ask" | "accept-edits" | "auto"; + fastMode?: boolean; }, ) => { rejectNativeMutation(id, "configuration"); diff --git a/apps/desktop/electron/main/remote/remote-backend.ts b/apps/desktop/electron/main/remote/remote-backend.ts index 0fc5d91c75..dd17b5029c 100644 --- a/apps/desktop/electron/main/remote/remote-backend.ts +++ b/apps/desktop/electron/main/remote/remote-backend.ts @@ -269,9 +269,14 @@ export function createRemoteBackend(options: RemoteBackendOptions): RemoteBacken case IPC.invoke.sessionConfigure: { const remoteSessionId = remoteIdFor(args); const config = (args[1] ?? {}) as Partial< - Pick + Pick >; - if (config.providerId !== undefined || config.modelId !== undefined || config.thinkingLevel !== undefined) { + if ( + config.providerId !== undefined || + config.modelId !== undefined || + config.thinkingLevel !== undefined || + config.fastMode !== undefined + ) { throw capabilityUnavailable("remote sessions use the Host model configuration"); } const { session } = await client.request<{ session: RacpSession }>("session/configure", { diff --git a/apps/desktop/electron/main/runtime/session-launch.ts b/apps/desktop/electron/main/runtime/session-launch.ts index 3461c2e709..0b82b13da2 100644 --- a/apps/desktop/electron/main/runtime/session-launch.ts +++ b/apps/desktop/electron/main/runtime/session-launch.ts @@ -706,6 +706,9 @@ export function createSessionLaunchRuntime({ ...(overrides.turnId ? { turnId: overrides.turnId } : {}), thinkingLevel, infiniteProviderRetry: settings.infiniteProviderRetry === true, + // The session `/fast` switch only reaches a model the user allowed it + // for in model settings (ADR 0332). + fastMode: session.fastMode === true && storedModel?.fastModeAllowed === true, ...(jevApiKey ? { jevApiKey } : {}), commandShell, scratchDir: join(dataDir, "scratch", sessionId), diff --git a/apps/desktop/src/components/Composer.tsx b/apps/desktop/src/components/Composer.tsx index c9baf395d8..10f9b244de 100644 --- a/apps/desktop/src/components/Composer.tsx +++ b/apps/desktop/src/components/Composer.tsx @@ -25,6 +25,7 @@ import { isActivePlanExecution } from "../lib/plan-mode-state"; import { headAsk, queuedAskCount } from "../lib/pending-asks"; import type { QueuedPrompt } from "../lib/queued-prompts"; import { composerModelDisplayName, sameComposerModelId } from "../lib/composer-models"; +import { sessionFastModeActive } from "../lib/fast-mode-command"; import { resolveComposerThinkingProvider, } from "../lib/session-thinking"; @@ -626,6 +627,7 @@ export function Composer({ modelMenu={modelMenu} modelLabel={modelLabel} thinkingLabel={thinkingLabel} + modelFastMode={sessionFastModeActive(provider, modelId, activeSession ?? draftConfiguration)} contextUsage={composerContextUsage ?? null} value={value} modelReady={modelReady} diff --git a/apps/desktop/src/components/icons.tsx b/apps/desktop/src/components/icons.tsx index 13f39d92c9..08337551f8 100644 --- a/apps/desktop/src/components/icons.tsx +++ b/apps/desktop/src/components/icons.tsx @@ -101,6 +101,7 @@ import { Workflow, Wrench, X, + Zap, type LucideIcon, type LucideProps, } from "lucide-react"; @@ -192,6 +193,7 @@ export const IconDatabase = icon(Database); export const IconCheck = icon(Check); export const IconBell = icon(Bell); export const IconBot = icon(Bot); +export const IconZap = icon(Zap); export const IconCheckCheck = icon(CheckCheck); export const IconShield = icon(Shield); export const IconChevronDown = icon(ChevronDown); diff --git a/apps/desktop/src/components/settings/ModelSelectionPanes.tsx b/apps/desktop/src/components/settings/ModelSelectionPanes.tsx index 668a838b26..8b0192d9a4 100644 --- a/apps/desktop/src/components/settings/ModelSelectionPanes.tsx +++ b/apps/desktop/src/components/settings/ModelSelectionPanes.tsx @@ -16,6 +16,7 @@ import { bindingForCustomModel, bindingForCustomModelInfo, bindingFromModelInfo, + fastModeBillingOn, formatTokenCount, modelMatchesFilter, nativeWebSearchSupportedOn, @@ -307,6 +308,14 @@ export function ModelSelectionPanes({ // Use the same published endpoint routing as the runtime. A disabled control // means this connection is not integrated, not that the vendor cannot search. const nativeWebSearchWireCapable = nativeWebSearchSupportedOn(apiStyle, lookupContext?.baseUrl); + // Fast mode rides the Responses `service_tier` field; the hint names what + // the priority tier costs on this wire (ADR 0332). + const fastModeBilling = fastModeBillingOn(apiStyle); + const fastModeHint = fastModeBilling === "subscription" + ? "settings.fastModeAllowedSubscriptionHint" + : fastModeBilling === "usage" + ? "settings.fastModeAllowedUsageHint" + : "settings.fastModeAllowedUnsupported"; /** * The chosen list narrows with the discovered list's rule plus the binding's @@ -1032,6 +1041,28 @@ export function ModelSelectionPanes({ + + + + + + diff --git a/apps/desktop/src/components/settings/provider-copy.ts b/apps/desktop/src/components/settings/provider-copy.ts index 54b06748bd..eb3805547a 100644 --- a/apps/desktop/src/components/settings/provider-copy.ts +++ b/apps/desktop/src/components/settings/provider-copy.ts @@ -47,6 +47,7 @@ export function copyProviderConfiguration(provider: ProviderPublic, name: string ...(model.supportsDocuments !== undefined ? { supportsDocuments: model.supportsDocuments } : {}), ...(model.availableForSubagents !== undefined ? { availableForSubagents: model.availableForSubagents } : {}), ...(model.nativeWebSearch !== undefined ? { nativeWebSearch: model.nativeWebSearch } : {}), + ...(model.fastModeAllowed !== undefined ? { fastModeAllowed: model.fastModeAllowed } : {}), })), }; } diff --git a/apps/desktop/src/features/chat/composer/ComposerModelPicker.tsx b/apps/desktop/src/features/chat/composer/ComposerModelPicker.tsx index 05a89f8158..d9e9431981 100644 --- a/apps/desktop/src/features/chat/composer/ComposerModelPicker.tsx +++ b/apps/desktop/src/features/chat/composer/ComposerModelPicker.tsx @@ -2,7 +2,7 @@ import type { TFunction } from "i18next"; import type { ReactNode } from "react"; import { ComposerModelList } from "./ComposerModelList"; import { AnchoredMenu } from "../../../components/settings/AnchoredMenu"; -import { IconBot, IconChevronDown } from "../../../components/icons"; +import { IconBot, IconChevronDown, IconZap } from "../../../components/icons"; import { TooltipButton } from "../../../components/ui"; import type { useComposerModelMenu } from "./hooks/useComposerModelMenu"; import { ThinkingLevelSlider } from "./ThinkingLevelSlider"; @@ -15,6 +15,8 @@ export type ComposerModelPickerProps = { modelLabel: string; thinkingLabel: string; thinkingLevel: string; + /** The selected model asks for OpenAI's priority tier (ADR 0332). */ + fastMode?: boolean; selectedProviderId?: string; selectedModelId?: string; controlsBlocked: boolean; @@ -29,6 +31,7 @@ export function ComposerModelPicker({ modelLabel, thinkingLabel, thinkingLevel, + fastMode = false, selectedProviderId, selectedModelId, controlsBlocked, @@ -54,6 +57,7 @@ export function ComposerModelPicker({ commitThinkingLevel, onMenuKeyDown, } = controller; + const fastSuffix = fastMode ? ` · ${t("chat.fastModeBadge")}` : ""; return ( {thinkingLabel} ) : null} + {fastMode ? ( + + ) : null}