Skip to content

feat: add Codex fast service tier toggle#780

Open
NightWatcher314 wants to merge 2 commits into
tiann:mainfrom
NightWatcher314:codex-fast-service-tier
Open

feat: add Codex fast service tier toggle#780
NightWatcher314 wants to merge 2 commits into
tiann:mainfrom
NightWatcher314:codex-fast-service-tier

Conversation

@NightWatcher314
Copy link
Copy Markdown
Contributor

Summary

  • add persisted serviceTier session config for Codex and propagate it through spawn/resume, keepalive, RPC config updates, and Codex app-server thread/turn params
  • expose Codex model service tiers from model/list and add a web Fast toggle for remote Codex sessions
  • update status bar to reflect the actual Codex service tier instead of inferring “fast” from model/reasoning heuristics

Tests

  • bun typecheck
  • bun test hub/src/web/routes/sessions.test.ts hub/src/sync/sessionModel.test.ts
  • cd cli && bun test src/codex/utils/appServerConfig.test.ts

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Local Codex launches never receive the selected service tier — --service-tier is consumed into options.serviceTier, but the local launcher only forwards Codex CLI argv/model-reasoning config to the spawned codex process. Local hapi codex --service-tier priority and local resume of a persisted fast tier will show/persist Fast in HAPI while the actual local Codex process runs without that tier. Evidence: cli/src/commands/codex.ts:79.
    Suggested fix:
    // cli/src/codex/codexLocalLauncher.ts
    await codexLocal({
        path: session.path,
        sessionId: resumeSessionId,
        modelReasoningEffort: (session.getModelReasoningEffort() ?? undefined) as ReasoningEffort | undefined,
        serviceTier: session.getServiceTier() ?? undefined,
        onSessionFound: handleSessionFound,
        abort: abortSignal,
        codexArgs,
        mcpServers,
        sessionHook: { port: hookServer.port, token: hookServer.token }
    })
    
    // cli/src/codex/codexLocal.ts
    serviceTier?: string | null;
    
    if (opts.serviceTier) {
        args.push('--service-tier', opts.serviceTier);
    }
  • [Major] Changing Codex models can leave an unsupported Fast tier stuck on the session — the new tier lookup is model-specific, but handleModelChange only updates model; if the prior serviceTier is still priority and the new model has no matching tier, the toggle becomes disabled while StatusBar still shows Fast and subsequent turns still send serviceTier: priority. Evidence: web/src/components/SessionChat.tsx:162.
    Suggested fix:
    const supportsServiceTier = useCallback((modelId: string | null, tierId: string | null) => {
        if (!tierId) return true
        const selectedModel = modelId ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
        const modelSummary = codexModelsState.models.find((codexModel) => codexModel.id === selectedModel)
        return (modelSummary?.serviceTiers ?? []).some((tier) => tier.id === tierId)
    }, [codexModelsState.models])
    
    const handleModelChange = useCallback(async (model: string | null) => {
        try {
            await setModel(model)
            if (agentFlavor === 'codex' && !supportsServiceTier(model, props.session.serviceTier)) {
                await setServiceTier(null)
            }
            haptic.notification('success')
            props.onRefresh()
        } catch (e) {
            haptic.notification('error')
            console.error('Failed to set model:', e)
        }
    }, [setModel, setServiceTier, agentFlavor, supportsServiceTier, props.session.serviceTier, props.onRefresh, haptic])

Summary
Review mode: initial
Two issues found: local Codex service-tier propagation is incomplete, and model changes can leave an invalid/stale Fast tier active. No questions.

Testing
Not run (automation)

HAPI Bot

Comment thread cli/src/commands/codex.ts
if (!tier) {
throw new Error('Missing --service-tier value')
}
options.serviceTier = tier
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Local Codex launches never receive the selected service tier. This parser consumes --service-tier into HAPI state, but the local launcher spawns codex from codexArgs/explicit local options; unlike --model, this value is not forwarded to the actual local Codex process. That makes hapi codex --service-tier priority and local resume of a persisted tier show/persist Fast in HAPI while local Codex runs with the default tier.

Suggested fix:

// cli/src/codex/codexLocalLauncher.ts
await codexLocal({
    path: session.path,
    sessionId: resumeSessionId,
    modelReasoningEffort: (session.getModelReasoningEffort() ?? undefined) as ReasoningEffort | undefined,
    serviceTier: session.getServiceTier() ?? undefined,
    onSessionFound: handleSessionFound,
    abort: abortSignal,
    codexArgs,
    mcpServers,
    sessionHook: { port: hookServer.port, token: hookServer.token }
})

// cli/src/codex/codexLocal.ts
serviceTier?: string | null;

if (opts.serviceTier) {
    args.push('--service-tier', opts.serviceTier);
}

if (agentFlavor !== 'codex') {
return null
}
const selectedModel = props.session.model ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] This derives Fast support from the selected model, but changing the model never clears an already-persisted serviceTier. If a user enables Fast on one model, then switches to a model without that tier, the toggle becomes disabled while props.session.serviceTier remains priority; StatusBar still shows Fast and the next Codex turn still sends serviceTier: priority for an unsupported model.

Suggested fix:

const supportsServiceTier = useCallback((modelId: string | null, tierId: string | null) => {
    if (!tierId) return true
    const selectedModel = modelId ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
    const modelSummary = codexModelsState.models.find((codexModel) => codexModel.id === selectedModel)
    return (modelSummary?.serviceTiers ?? []).some((tier) => tier.id === tierId)
}, [codexModelsState.models])

const handleModelChange = useCallback(async (model: string | null) => {
    try {
        await setModel(model)
        if (agentFlavor === 'codex' && !supportsServiceTier(model, props.session.serviceTier)) {
            await setServiceTier(null)
        }
        haptic.notification('success')
        props.onRefresh()
    } catch (e) {
        haptic.notification('error')
        console.error('Failed to set model:', e)
    }
}, [setModel, setServiceTier, agentFlavor, supportsServiceTier, props.session.serviceTier, props.onRefresh, haptic])

@NightWatcher314
Copy link
Copy Markdown
Contributor Author

Thanks for the review. I pushed 490065b to fix the local launch/test coverage issue: serviceTier now propagates through codexLocalLauncher/codexLocal and the affected CLI mocks are covered by tests.\n\nFor the model-switch behavior: the current behavior is intentional for this first pass. HAPI stores serviceTier as user intent rather than an effective runtime tier; if a selected model does not advertise Fast, the UI disables the Fast control while still showing the persisted requested tier, and the app-server remains the final validator on the next turn. I can change this to clear unsupported tiers on model switch if maintainers prefer that policy.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Changing Codex models can leave an unsupported Fast tier active — the Fast tier lookup is now model-specific, but the model-change path still only persists model; an existing serviceTier is not cleared when the newly selected model has no matching Fast tier. After enabling Fast on one model and switching to an unsupported model, the toggle becomes disabled while the session still sends the stale tier on future Codex turns. Evidence: web/src/components/SessionChat.tsx:162.
    Suggested fix:
    const supportsServiceTier = useCallback((modelId: string | null, tierId: string | null) => {
        if (!tierId) return true
        const selectedModel = modelId ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
        const modelSummary = codexModelsState.models.find((codexModel) => codexModel.id === selectedModel)
        return (modelSummary?.serviceTiers ?? []).some((tier) => tier.id === tierId)
    }, [codexModelsState.models])
    
    const handleModelChange = useCallback(async (model: string | null) => {
        try {
            await setModel(model)
            if (agentFlavor === 'codex' && !supportsServiceTier(model, props.session.serviceTier)) {
                await setServiceTier(null)
            }
            haptic.notification('success')
            props.onRefresh()
        } catch (e) {
            haptic.notification('error')
            console.error('Failed to set model:', e)
        }
    }, [setModel, setServiceTier, agentFlavor, supportsServiceTier, props.session.serviceTier, props.onRefresh, haptic])

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • The local Codex launcher propagation issue from the previous review was addressed. The stale service-tier-on-model-change issue remains.

Testing

  • Not run (automation)

HAPI Bot

if (agentFlavor !== 'codex') {
return null
}
const selectedModel = props.session.model ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Changing Codex models can leave an unsupported Fast tier active. This Fast tier lookup is model-specific, but the model-change handler still only persists model; it does not clear an existing serviceTier when the newly selected model has no matching Fast tier. After enabling Fast on one model and switching to an unsupported model, the toggle becomes disabled while the session still sends the stale tier on future Codex turns.

Suggested fix:

const supportsServiceTier = useCallback((modelId: string | null, tierId: string | null) => {
    if (!tierId) return true
    const selectedModel = modelId ?? codexModelsState.models.find((model) => model.isDefault)?.id ?? null
    const modelSummary = codexModelsState.models.find((codexModel) => codexModel.id === selectedModel)
    return (modelSummary?.serviceTiers ?? []).some((tier) => tier.id === tierId)
}, [codexModelsState.models])

const handleModelChange = useCallback(async (model: string | null) => {
    try {
        await setModel(model)
        if (agentFlavor === 'codex' && !supportsServiceTier(model, props.session.serviceTier)) {
            await setServiceTier(null)
        }
        haptic.notification('success')
        props.onRefresh()
    } catch (e) {
        haptic.notification('error')
        console.error('Failed to set model:', e)
    }
}, [setModel, setServiceTier, agentFlavor, supportsServiceTier, props.session.serviceTier, props.onRefresh, haptic])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant