mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
* test: reproduce issue #898 (Codex fast mode service tier) * fix(codex): add Fast mode (service tier) toggle and /fast command (closes #898) * feat(codex+web): Fast mode UI toggle with full persistence Wires the Codex Fast mode (service tier) end-to-end so it can be toggled from the web composer and survives reload/handoff: - shared: serviceTier on Session/SessionPatch, session-alive payload, resume target, and a SessionServiceTierRequest schema - cli: AgentSessionBase carries serviceTier through keepAlive; runCodex syncs it to the session instance - hub: service_tier column (schema v10 + migration), store setter, sessionCache + syncEngine plumbing, POST /sessions/:id/service-tier - web: api.setServiceTier + mutation, a Fast/Standard toggle in the composer settings (gated to Codex GPT-5.5/5.4), and StatusBar now reflects the real tier instead of the effort heuristic Refs #898 * fix(codex): preserve unset/persisted service tier on startup keepalive Addresses HAPI Bot [Major] on PR #904: applyCurrentConfigToSession ran setServiceTier(currentServiceTier ?? null) on wrapper-ready, collapsing the untouched `undefined` state into explicit Standard. The immediate setCollaborationMode keepalive then persisted serviceTier: null, silently downgrading resumed Fast sessions and disabling account-default Fast. - Seed currentServiceTier from the persisted session (sessionInfo.serviceTier), so a resumed Fast thread keeps running Fast. - Only call setServiceTier when the tier is explicit (!== undefined), preserving the three-state omit semantics at the keepalive boundary. - Add regression tests: persisted Fast is re-asserted; untouched omits the tier. * feat(codex+web): gate Fast toggle on catalog-advertised service tier The Fast toggle was gated on a model-name regex (gpt-5.5/5.4), which still showed a no-op control to API-key users — Fast credits only apply with ChatGPT login. Codex's model/list catalog advertises the service tiers actually available for each model in the current auth/plan context, so gate on that instead: - cli: capture serviceTiers (ids) per model in ModelListItem + normalizeModel - shared: CodexModelSummary.serviceTiers (flows through the existing getSessionCodexModels pass-through; no hub change needed) - web: codexModelAdvertisesFastTier(sessionModel, models) replaces the regex; SessionChat gates the toggle on it (hidden while the catalog is loading/errored). The toggle now only appears when toggling it will actually take effect. Refs #898 * fix(codex): make explicit Standard service tier sticky across resume Addresses HAPI Bot [Major] (round 2): a single persisted null conflated "untouched" with "explicit Standard". A user who turned Fast off persisted null, but startup mapped null -> undefined (untouched) and omitted serviceTier, so an account/thread-default Fast could silently return after restart/resume. Introduce a distinct stored representation: - 'fast' / 'standard' are explicit user choices; null/undefined = untouched. - Translate 'standard' -> Codex app-server serviceTier: null ONLY when building thread/turn params (toAppServerServiceTier); untouched omits the field. - /fast off now stores 'standard'; the web Standard option sends 'standard'. - Tighten SessionServiceTierRequest to enum(['fast','standard']) so stray tier strings are never forwarded. Tests: sticky-Standard-on-resume regression; turn/thread params translate 'standard'->null and omit on untouched; hub route applies fast/standard and rejects unsupported values + local sessions. Refs #898 * fix(codex): recognize real Fast tier (id 'priority', name 'Fast') in catalog gate Live E2E against an authed Codex session revealed the model catalog advertises the Fast tier with id 'priority' and display name 'Fast' (not id 'fast'), so the /fast/i gate — which only saw tier ids — wrongly hid the toggle for valid ChatGPT users on gpt-5.5/gpt-5.4. Capture both the tier id and name as lowercased tokens so the existing name-based match recognizes 'Fast'. The sent value stays 'fast' (the documented service_tier value / raw additionalSpeedTiers request tier). Verified end-to-end: gpt-5.5/gpt-5.4 gate on, gpt-5.4-mini off. Refs #898 * fix(codex): preserve service tier across session resume Resuming a Codex session spawns a fresh session (serviceTier null) and merges the old one in. Unlike model/effort/permissionMode, serviceTier was neither threaded through the resume spawn nor preserved in mergeSessionData, so a resumed Fast (or explicit Standard) session silently reverted to the account default. Thread serviceTier through the spawn path like its siblings: - hub: resumeSession passes session.serviceTier to spawnSession; rpcGateway + syncEngine carry it in the spawn RPC payload; mergeSessionData preserves it old->new (safety net). - cli: SpawnSessionOptions.serviceTier; apiMachine forwards it; buildCliArgs emits --service-tier for codex; the codex command parses it; runCodex seeds currentServiceTier from the spawn override first (opts.serviceTier ?? sessionInfo.serviceTier), so a resumed thread immediately runs the right tier. Verified end-to-end: set Fast -> kill process -> reopen -> resumed session (new id) still runs Fast. Tests: buildCliArgs --service-tier (codex only), runCodex spawn-override seed, mergeSessionData service-tier preservation. Refs #898 * fix(codex): send advertised 'priority' tier id for Fast, not 'fast' The model catalog advertises the Fast tier with request id 'priority' (display name 'Fast'), and OpenAI docs confirm service_tier='fast' maps to the request value 'priority'. The app-server serviceTier override is a raw request value that does not validate unknown strings (a live probe accepted 'bogus-xyz'), so sending 'fast' risks being silently ignored — no Fast applied. Translate the stored 'fast' state to app-server 'priority' at the thread/turn param boundary (toAppServerServiceTier); the stored/UI/command representation stays 'fast'/'standard'. Verified live: a turn with serviceTier='priority' runs and consumes the Fast-tier rate budget. Addresses HAPI Bot [Major]. Refs #898 * fix(codex): validate --service-tier CLI value (fast|standard) Addresses HAPI Bot [Minor]: the internal --service-tier spawn arg accepted any non-empty string, unlike the web /service-tier enum, so a malformed value could be seeded into currentServiceTier and persisted via keepalive. Parse it to 'fast'|'standard' and reject anything else, matching the web endpoint. Refs #898
This commit is contained in:
@@ -518,6 +518,13 @@ export class ApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
async setServiceTier(sessionId: string, serviceTier: string | null): Promise<void> {
|
||||
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/service-tier`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ serviceTier })
|
||||
})
|
||||
}
|
||||
|
||||
async approvePermission(
|
||||
sessionId: string,
|
||||
requestId: string,
|
||||
|
||||
@@ -101,6 +101,10 @@ export function HappyComposer(props: {
|
||||
onModelEffortChange?: (wireId: string | null) => void
|
||||
onModelReasoningEffortChange?: (modelReasoningEffort: string | null) => void
|
||||
onEffortChange?: (effort: string | null) => void
|
||||
/** Codex Fast mode (service tier): current value ('fast' or null/standard). */
|
||||
serviceTier?: string | null
|
||||
/** When provided, a Fast-mode toggle renders (Codex GPT-5.5 / GPT-5.4 only). */
|
||||
onServiceTierChange?: (serviceTier: string | null) => void
|
||||
onSwitchToRemote?: () => void
|
||||
onTerminal?: () => void
|
||||
terminalUnsupported?: boolean
|
||||
@@ -158,6 +162,8 @@ export function HappyComposer(props: {
|
||||
onModelEffortChange,
|
||||
onModelReasoningEffortChange,
|
||||
onEffortChange,
|
||||
serviceTier: rawServiceTier,
|
||||
onServiceTierChange,
|
||||
onSwitchToRemote,
|
||||
onTerminal,
|
||||
terminalUnsupported = false,
|
||||
@@ -180,6 +186,7 @@ export function HappyComposer(props: {
|
||||
const model = rawModel ?? null
|
||||
const modelReasoningEffort = rawModelReasoningEffort ?? null
|
||||
const effort = rawEffort ?? null
|
||||
const serviceTier = rawServiceTier ?? null
|
||||
|
||||
const api = useAssistantApi()
|
||||
const { composerEnterBehavior } = useComposerEnterBehavior()
|
||||
@@ -615,6 +622,20 @@ export function HappyComposer(props: {
|
||||
haptic('light')
|
||||
}, [onEffortChange, controlsDisabled, haptic])
|
||||
|
||||
const handleServiceTierChange = useCallback((nextServiceTier: string | null) => {
|
||||
if (!onServiceTierChange || controlsDisabled) return
|
||||
onServiceTierChange(nextServiceTier)
|
||||
setShowSettings(false)
|
||||
haptic('light')
|
||||
}, [onServiceTierChange, controlsDisabled, haptic])
|
||||
|
||||
// 'standard' (not null) is the explicit Fast-off choice so it persists
|
||||
// distinctly from an untouched/account-default session.
|
||||
const fastModeOptions: Array<{ value: string; label: string }> = useMemo(() => [
|
||||
{ value: 'standard', label: t('misc.fastModeStandard') },
|
||||
{ value: 'fast', label: t('misc.fastModeFast') }
|
||||
], [t])
|
||||
|
||||
const showCollaborationSettings = Boolean(onCollaborationModeChange && collaborationModeOptions.length > 0)
|
||||
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
|
||||
const showModelSettings = Boolean(onModelChange && supportsModelChange(agentFlavor) && modelOptions.length > 0)
|
||||
@@ -625,6 +646,7 @@ export function HappyComposer(props: {
|
||||
)
|
||||
const showModelReasoningEffortSettings = Boolean(onModelReasoningEffortChange && codexReasoningEffortOptions.length > 0)
|
||||
const showEffortSettings = Boolean(onEffortChange && supportsEffort(agentFlavor))
|
||||
const showFastModeSettings = Boolean(onServiceTierChange)
|
||||
const showSettingsButton = Boolean(
|
||||
showCollaborationSettings
|
||||
|| showPermissionSettings
|
||||
@@ -632,6 +654,7 @@ export function HappyComposer(props: {
|
||||
|| showModelEffortSettings
|
||||
|| showModelReasoningEffortSettings
|
||||
|| showEffortSettings
|
||||
|| showFastModeSettings
|
||||
)
|
||||
const showAbortButton = true
|
||||
const voiceEnabled = Boolean(onVoiceToggle)
|
||||
@@ -651,7 +674,7 @@ export function HappyComposer(props: {
|
||||
}, [api])
|
||||
|
||||
const overlays = useMemo(() => {
|
||||
if (showSettings && (showCollaborationSettings || showPermissionSettings || showModelSettings || showModelEffortSettings || showModelReasoningEffortSettings || showEffortSettings)) {
|
||||
if (showSettings && (showCollaborationSettings || showPermissionSettings || showModelSettings || showModelEffortSettings || showModelReasoningEffortSettings || showEffortSettings || showFastModeSettings)) {
|
||||
return (
|
||||
<div className="absolute bottom-[100%] mb-2 w-full">
|
||||
<FloatingOverlay maxHeight={320}>
|
||||
@@ -901,6 +924,47 @@ export function HappyComposer(props: {
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(showModelReasoningEffortSettings || showEffortSettings) && showFastModeSettings ? (
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
) : null}
|
||||
|
||||
{showFastModeSettings ? (
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
{t('misc.fastMode')}
|
||||
</div>
|
||||
{fastModeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value ?? 'standard'}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handleServiceTierChange(option.value)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
serviceTier === option.value
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{serviceTier === option.value && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={serviceTier === option.value ? 'text-[var(--app-link)]' : ''}>
|
||||
{option.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</FloatingOverlay>
|
||||
</div>
|
||||
)
|
||||
@@ -932,9 +996,11 @@ export function HappyComposer(props: {
|
||||
selectedModelVariant,
|
||||
showModelReasoningEffortSettings,
|
||||
showEffortSettings,
|
||||
showFastModeSettings,
|
||||
modelOptions,
|
||||
codexReasoningEffortOptions,
|
||||
claudeEffortOptions,
|
||||
fastModeOptions,
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
controlsDisabled,
|
||||
@@ -943,6 +1009,7 @@ export function HappyComposer(props: {
|
||||
model,
|
||||
modelReasoningEffort,
|
||||
effort,
|
||||
serviceTier,
|
||||
collaborationModeOptions,
|
||||
permissionModeOptions,
|
||||
handleCollaborationChange,
|
||||
@@ -950,6 +1017,7 @@ export function HappyComposer(props: {
|
||||
handleModelChange,
|
||||
handleModelReasoningEffortChange,
|
||||
handleEffortChange,
|
||||
handleServiceTierChange,
|
||||
handleSuggestionSelect,
|
||||
t
|
||||
])
|
||||
@@ -971,6 +1039,7 @@ export function HappyComposer(props: {
|
||||
contextWindow={contextWindow}
|
||||
model={model}
|
||||
modelReasoningEffort={modelReasoningEffort}
|
||||
serviceTier={serviceTier}
|
||||
permissionMode={permissionMode}
|
||||
collaborationMode={collaborationMode}
|
||||
threadGoal={threadGoal}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { AgentState, CodexCollaborationMode, PermissionMode } from '@/types
|
||||
import type { ConversationStatus } from '@/realtime/types'
|
||||
import type { ThreadGoal } from '@/types/api'
|
||||
import { getContextBudgetTokens } from '@/chat/modelConfig'
|
||||
import { isFastServiceTier } from './codexFastMode'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
// Vibing messages for thinking state
|
||||
@@ -154,6 +155,7 @@ export function StatusBar(props: {
|
||||
contextWindow?: number | null
|
||||
model?: string | null
|
||||
modelReasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
permissionMode?: PermissionMode
|
||||
collaborationMode?: CodexCollaborationMode
|
||||
threadGoal?: ThreadGoal | null
|
||||
@@ -213,8 +215,12 @@ export function StatusBar(props: {
|
||||
const codexReasoningLabel = (props.agentFlavor === 'codex' || props.agentFlavor === 'opencode')
|
||||
? formatCodexReasoningLabel(props.modelReasoningEffort)
|
||||
: null
|
||||
// Prefer the explicit service tier (the real Fast-mode toggle) when set;
|
||||
// fall back to the effort/model heuristic only when the tier is unknown.
|
||||
const codexFastMode = props.agentFlavor === 'codex'
|
||||
? isCodexFastMode(props.model, props.modelReasoningEffort)
|
||||
? (props.serviceTier != null
|
||||
? isFastServiceTier(props.serviceTier)
|
||||
: isCodexFastMode(props.model, props.modelReasoningEffort))
|
||||
: false
|
||||
const goalLabel = props.agentFlavor === 'codex' && props.threadGoal
|
||||
? props.threadGoal.status === 'active'
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { codexModelAdvertisesFastTier, isFastServiceTier } from './codexFastMode'
|
||||
|
||||
// Mirrors the real Codex catalog: the Fast tier's id is 'priority' and its
|
||||
// display name is 'Fast', so the CLI captures both as lowercased tokens
|
||||
// (['priority','fast']). Models without Fast advertise no such token.
|
||||
const models = [
|
||||
{ id: 'gpt-5.5', isDefault: true, serviceTiers: ['priority', 'fast'] },
|
||||
{ id: 'gpt-5.4-mini', isDefault: false, serviceTiers: [] },
|
||||
{ id: 'o3', isDefault: false }
|
||||
]
|
||||
|
||||
describe('codexModelAdvertisesFastTier', () => {
|
||||
it('is true when the active model advertises a fast tier (real id=priority, name=Fast)', () => {
|
||||
expect(codexModelAdvertisesFastTier('gpt-5.5', models)).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to the catalog default model when session model is auto/null', () => {
|
||||
// default model (gpt-5.5) advertises fast
|
||||
expect(codexModelAdvertisesFastTier(null, models)).toBe(true)
|
||||
expect(codexModelAdvertisesFastTier(undefined, models)).toBe(true)
|
||||
expect(codexModelAdvertisesFastTier(' ', models)).toBe(true)
|
||||
})
|
||||
|
||||
it('is false when the active model does not advertise a fast tier', () => {
|
||||
expect(codexModelAdvertisesFastTier('gpt-5.4-mini', models)).toBe(false)
|
||||
expect(codexModelAdvertisesFastTier('o3', models)).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when the model is unknown or the catalog is empty', () => {
|
||||
expect(codexModelAdvertisesFastTier('gpt-9', models)).toBe(false)
|
||||
expect(codexModelAdvertisesFastTier('gpt-5.5', [])).toBe(false)
|
||||
})
|
||||
|
||||
it('matches fast tokens case-insensitively', () => {
|
||||
expect(codexModelAdvertisesFastTier('m', [{ id: 'm', isDefault: true, serviceTiers: ['Fast'] }])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFastServiceTier', () => {
|
||||
it('detects the fast tier regardless of casing/spacing', () => {
|
||||
expect(isFastServiceTier('fast')).toBe(true)
|
||||
expect(isFastServiceTier(' Fast ')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats null/standard as not fast', () => {
|
||||
expect(isFastServiceTier(null)).toBe(false)
|
||||
expect(isFastServiceTier(undefined)).toBe(false)
|
||||
expect(isFastServiceTier('standard')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
// Codex Fast mode (service tier) availability is advertised per-model by the
|
||||
// Codex app-server `model/list` catalog, which is resolved server-side from the
|
||||
// user's account/auth/plan. An API-key session (no Fast credits) or a model
|
||||
// without Fast support simply won't list a `fast` service tier. Gating the UI on
|
||||
// this catalog signal — rather than a model-name heuristic — means the toggle
|
||||
// only appears when toggling it will actually do something.
|
||||
|
||||
type CodexModelCatalogEntry = {
|
||||
id: string
|
||||
isDefault: boolean
|
||||
serviceTiers?: string[]
|
||||
}
|
||||
|
||||
function isFastTierId(tierId: string): boolean {
|
||||
return /fast/i.test(tierId.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the catalog entry for the session's active model. A null/empty
|
||||
* session model means "auto" — the catalog's default model is active.
|
||||
*/
|
||||
function findActiveModel<T extends CodexModelCatalogEntry>(
|
||||
sessionModel: string | null | undefined,
|
||||
models: ReadonlyArray<T>
|
||||
): T | undefined {
|
||||
const normalized = sessionModel?.trim().toLowerCase()
|
||||
if (normalized) {
|
||||
return models.find((model) => model.id.trim().toLowerCase() === normalized)
|
||||
}
|
||||
return models.find((model) => model.isDefault)
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the session's active Codex model advertises a Fast service tier in
|
||||
* the current auth/plan context. Returns false when the catalog is empty/not
|
||||
* yet loaded so the toggle stays hidden until we have an authoritative answer.
|
||||
*/
|
||||
export function codexModelAdvertisesFastTier(
|
||||
sessionModel: string | null | undefined,
|
||||
models: ReadonlyArray<CodexModelCatalogEntry>
|
||||
): boolean {
|
||||
if (models.length === 0) {
|
||||
return false
|
||||
}
|
||||
const active = findActiveModel(sessionModel, models)
|
||||
return Boolean(active?.serviceTiers?.some(isFastTierId))
|
||||
}
|
||||
|
||||
export function isFastServiceTier(serviceTier?: string | null): boolean {
|
||||
return serviceTier?.trim().toLowerCase() === 'fast'
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { buildVisibleChatBlocks, isToolGroupBlock, type ToolGroupBlock } from '@
|
||||
import { isQueuedForInvocation, mergeMessages } from '@/lib/messages'
|
||||
import { inactiveSessionCanResume } from '@/lib/sessionResume'
|
||||
import { HappyComposer, type ComposerSendError } from '@/components/AssistantChat/HappyComposer'
|
||||
import { codexModelAdvertisesFastTier } from '@/components/AssistantChat/codexFastMode'
|
||||
import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
|
||||
import { resolvePendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
|
||||
import { HappyThread } from '@/components/AssistantChat/HappyThread'
|
||||
@@ -559,7 +560,8 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
setCollaborationMode,
|
||||
setModel,
|
||||
setModelReasoningEffort,
|
||||
setEffort
|
||||
setEffort,
|
||||
setServiceTier
|
||||
} = useSessionActions(
|
||||
props.api,
|
||||
props.session.id,
|
||||
@@ -864,6 +866,17 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
}
|
||||
}, [setEffort, props.onRefresh, haptic])
|
||||
|
||||
const handleServiceTierChange = useCallback(async (serviceTier: string | null) => {
|
||||
try {
|
||||
await setServiceTier(serviceTier)
|
||||
haptic.notification('success')
|
||||
props.onRefresh()
|
||||
} catch (e) {
|
||||
haptic.notification('error')
|
||||
console.error('Failed to set service tier:', e)
|
||||
}
|
||||
}, [setServiceTier, props.onRefresh, haptic])
|
||||
|
||||
// Abort handler
|
||||
const handleAbort = useCallback(async () => {
|
||||
await abortSession()
|
||||
@@ -1162,6 +1175,16 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
: undefined
|
||||
}
|
||||
onEffortChange={handleEffortChange}
|
||||
serviceTier={agentFlavor === 'codex' ? props.session.serviceTier : undefined}
|
||||
onServiceTierChange={
|
||||
agentFlavor === 'codex'
|
||||
&& props.session.active
|
||||
&& !controlledByUser
|
||||
&& !codexModelsState.error
|
||||
&& codexModelAdvertisesFastTier(props.session.model, codexModelsState.models)
|
||||
? handleServiceTierChange
|
||||
: undefined
|
||||
}
|
||||
onSwitchToRemote={handleSwitchToRemote}
|
||||
onTerminal={props.session.active && terminalSupported ? handleViewTerminal : undefined}
|
||||
terminalUnsupported={props.session.active && !terminalSupported}
|
||||
|
||||
@@ -22,6 +22,7 @@ export function useSessionActions(
|
||||
setModel: (model: string | null) => Promise<void>
|
||||
setModelReasoningEffort: (modelReasoningEffort: string | null) => Promise<void>
|
||||
setEffort: (effort: string | null) => Promise<void>
|
||||
setServiceTier: (serviceTier: string | null) => Promise<void>
|
||||
renameSession: (name: string) => Promise<void>
|
||||
deleteSession: () => Promise<void>
|
||||
isPending: boolean
|
||||
@@ -150,6 +151,22 @@ export function useSessionActions(
|
||||
onSuccess: () => void invalidateSession(),
|
||||
})
|
||||
|
||||
const serviceTierMutation = useMutation({
|
||||
mutationFn: async (serviceTier: string | null) => {
|
||||
if (!api || !sessionId) {
|
||||
throw new Error('Session unavailable')
|
||||
}
|
||||
if (agentFlavor !== 'codex') {
|
||||
throw new Error('Fast mode is only supported for Codex sessions')
|
||||
}
|
||||
if (!codexCollaborationModeSupported) {
|
||||
throw new Error('Fast mode is only supported for remote sessions')
|
||||
}
|
||||
await api.setServiceTier(sessionId, serviceTier)
|
||||
},
|
||||
onSuccess: () => void invalidateSession(),
|
||||
})
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: async (name: string) => {
|
||||
if (!api || !sessionId) {
|
||||
@@ -185,6 +202,7 @@ export function useSessionActions(
|
||||
setModel: modelMutation.mutateAsync,
|
||||
setModelReasoningEffort: modelReasoningEffortMutation.mutateAsync,
|
||||
setEffort: effortMutation.mutateAsync,
|
||||
setServiceTier: serviceTierMutation.mutateAsync,
|
||||
renameSession: renameMutation.mutateAsync,
|
||||
deleteSession: deleteMutation.mutateAsync,
|
||||
isPending: abortMutation.isPending
|
||||
@@ -196,6 +214,7 @@ export function useSessionActions(
|
||||
|| modelMutation.isPending
|
||||
|| modelReasoningEffortMutation.isPending
|
||||
|| effortMutation.isPending
|
||||
|| serviceTierMutation.isPending
|
||||
|| renameMutation.isPending
|
||||
|| deleteMutation.isPending,
|
||||
}
|
||||
|
||||
@@ -627,6 +627,9 @@ export default {
|
||||
'misc.model': 'Model',
|
||||
'misc.reasoningEffort': 'Reasoning Effort',
|
||||
'misc.effort': 'Effort',
|
||||
'misc.fastMode': 'Fast Mode',
|
||||
'misc.fastModeStandard': 'Standard',
|
||||
'misc.fastModeFast': 'Fast',
|
||||
'misc.variant': 'Variant',
|
||||
'misc.loading': 'Loading…',
|
||||
'misc.loadOlder': 'Load older',
|
||||
|
||||
@@ -631,6 +631,9 @@ export default {
|
||||
'misc.model': '模型',
|
||||
'misc.reasoningEffort': '推理强度',
|
||||
'misc.effort': '思考强度',
|
||||
'misc.fastMode': '快速模式',
|
||||
'misc.fastModeStandard': '标准',
|
||||
'misc.fastModeFast': '快速',
|
||||
'misc.variant': '变体',
|
||||
'misc.loading': '加载中…',
|
||||
'misc.loadOlder': '加载更早的',
|
||||
|
||||
@@ -28,6 +28,7 @@ function makeExport(messages: HapiSessionExport['messages']): HapiSessionExport
|
||||
model: null,
|
||||
modelReasoningEffort: null,
|
||||
effort: null,
|
||||
serviceTier: null,
|
||||
permissionMode: 'default',
|
||||
collaborationMode: 'default'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user