From 500407c6b160d20436cfe2d8c56445bb72da28fc Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Mon, 27 Jul 2026 07:38:24 +0800 Subject: [PATCH] fix(codex): show catalog-default Fast tier (#1179) * fix(codex): show catalog-default Fast tier * fix(web): show inherited Fast tier in header --- cli/src/modules/common/codexModels.ts | 1 + shared/src/apiTypes.ts | 1 + .../AssistantChat/codexFastMode.test.ts | 13 +++++ .../components/AssistantChat/codexFastMode.ts | 11 +++- web/src/components/SessionChat.tsx | 12 ++++- web/src/components/SessionHeader.test.tsx | 50 +++++++++++++++++++ web/src/components/SessionHeader.tsx | 3 +- 7 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 web/src/components/SessionHeader.test.tsx diff --git a/cli/src/modules/common/codexModels.ts b/cli/src/modules/common/codexModels.ts index 27d41859..fa51d052 100644 --- a/cli/src/modules/common/codexModels.ts +++ b/cli/src/modules/common/codexModels.ts @@ -74,6 +74,7 @@ function normalizeModel(entry: unknown): CodexModelSummary | null { displayName: asNonEmptyString(record.displayName) ?? id, isDefault: record.isDefault === true, defaultReasoningEffort: asNonEmptyString(record.defaultReasoningEffort), + defaultServiceTier: asNonEmptyString(record.defaultServiceTier), supportedReasoningEfforts: normalizeSupportedReasoningEfforts(record.supportedReasoningEfforts), serviceTiers: normalizeServiceTiers(record.serviceTiers) }; diff --git a/shared/src/apiTypes.ts b/shared/src/apiTypes.ts index ff06fabb..5a754dc5 100644 --- a/shared/src/apiTypes.ts +++ b/shared/src/apiTypes.ts @@ -433,6 +433,7 @@ export type CodexModelSummary = { displayName: string isDefault: boolean defaultReasoningEffort?: string | null + defaultServiceTier?: string | null supportedReasoningEfforts?: string[] /** Service tier ids advertised for this model in the current auth/plan context (e.g. 'fast'). */ serviceTiers?: string[] diff --git a/web/src/components/AssistantChat/codexFastMode.test.ts b/web/src/components/AssistantChat/codexFastMode.test.ts index f7f661bf..1e79456f 100644 --- a/web/src/components/AssistantChat/codexFastMode.test.ts +++ b/web/src/components/AssistantChat/codexFastMode.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { codexModelAdvertisesFastTier, + getEffectiveCodexServiceTier, getDisplayedCodexServiceTier, isFastServiceTier } from './codexFastMode' @@ -45,6 +46,7 @@ describe('isFastServiceTier', () => { it('detects the fast tier regardless of casing/spacing', () => { expect(isFastServiceTier('fast')).toBe(true) expect(isFastServiceTier(' Fast ')).toBe(true) + expect(isFastServiceTier('priority')).toBe(true) }) it('treats null/standard as not fast', () => { @@ -54,6 +56,17 @@ describe('isFastServiceTier', () => { }) }) +describe('getEffectiveCodexServiceTier', () => { + it('uses the active model default only when the session has no override', () => { + const modelsWithDefault = models.map((model) => ( + model.id === 'gpt-5.5' ? { ...model, defaultServiceTier: 'priority' } : model + )) + + expect(getEffectiveCodexServiceTier(null, null, modelsWithDefault)).toBe('priority') + expect(getEffectiveCodexServiceTier('standard', null, modelsWithDefault)).toBe('standard') + }) +}) + describe('getDisplayedCodexServiceTier', () => { it('displays untouched and explicit non-fast tiers as standard', () => { expect(getDisplayedCodexServiceTier(null)).toBe('standard') diff --git a/web/src/components/AssistantChat/codexFastMode.ts b/web/src/components/AssistantChat/codexFastMode.ts index 7b859990..d9486957 100644 --- a/web/src/components/AssistantChat/codexFastMode.ts +++ b/web/src/components/AssistantChat/codexFastMode.ts @@ -8,6 +8,7 @@ type CodexModelCatalogEntry = { id: string isDefault: boolean + defaultServiceTier?: string | null serviceTiers?: string[] } @@ -47,7 +48,15 @@ export function codexModelAdvertisesFastTier( } export function isFastServiceTier(serviceTier?: string | null): boolean { - return serviceTier?.trim().toLowerCase() === 'fast' + return /^(fast|priority)$/i.test(serviceTier?.trim() ?? '') +} + +export function getEffectiveCodexServiceTier( + serviceTier: string | null | undefined, + sessionModel: string | null | undefined, + models: ReadonlyArray +): string | null | undefined { + return serviceTier ?? findActiveModel(sessionModel, models)?.defaultServiceTier } /** diff --git a/web/src/components/SessionChat.tsx b/web/src/components/SessionChat.tsx index 6687b3ff..13bca4c0 100644 --- a/web/src/components/SessionChat.tsx +++ b/web/src/components/SessionChat.tsx @@ -26,7 +26,7 @@ import { supportsCodexReasoningEffort } from '@/lib/codexModelCapabilities' import { HappyComposer, type ComposerSendError } from '@/components/AssistantChat/HappyComposer' -import { codexModelAdvertisesFastTier } from '@/components/AssistantChat/codexFastMode' +import { codexModelAdvertisesFastTier, getEffectiveCodexServiceTier } from '@/components/AssistantChat/codexFastMode' import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker' import { resolvePendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker' import { HappyThread } from '@/components/AssistantChat/HappyThread' @@ -543,6 +543,13 @@ function SessionChatInner(props: SessionChatProps) { sessionId: props.session.id, enabled: agentFlavor === 'codex' && props.session.active && !controlledByUser }) + const effectiveCodexServiceTier = agentFlavor === 'codex' + ? getEffectiveCodexServiceTier( + props.session.serviceTier, + props.session.model, + codexModelsState.models + ) + : undefined const codexModelOptions = useMemo(() => { if (agentFlavor !== 'codex') { return undefined @@ -1199,6 +1206,7 @@ function SessionChatInner(props: SessionChatProps) {
cleanup()) + +describe('SessionHeader', () => { + it('shows an inherited catalog-default Fast tier', () => { + const session: Session = { + id: 'session-1', + namespace: 'default', + seq: 0, + createdAt: 0, + updatedAt: 0, + active: true, + activeAt: 0, + metadata: { flavor: 'codex', path: '/repo', host: 'machine' }, + metadataVersion: 0, + agentState: null, + agentStateVersion: 0, + thinking: false, + thinkingAt: 0, + model: null, + modelReasoningEffort: null, + effort: null, + serviceTier: null + } + + render( + + + + + + + + ) + + expect(screen.getByText('fast')).toBeInTheDocument() + }) +}) diff --git a/web/src/components/SessionHeader.tsx b/web/src/components/SessionHeader.tsx index 2142a756..5882a3b5 100644 --- a/web/src/components/SessionHeader.tsx +++ b/web/src/components/SessionHeader.tsx @@ -90,6 +90,7 @@ function MoreVerticalIcon(props: { className?: string }) { export function SessionHeader(props: { session: Session + serviceTier?: string | null onBack: () => void onToggleFiles?: () => void filesActive?: boolean @@ -113,7 +114,7 @@ export function SessionHeader(props: { ? formatCodexReasoningLabel(session.modelReasoningEffort) : null // Match expected Fast badge semantics (#1004): only explicit service tier, no effort/model heuristics. - const showFastBadge = agentFlavor === 'codex' && isFastServiceTier(session.serviceTier) + const showFastBadge = agentFlavor === 'codex' && isFastServiceTier(props.serviceTier ?? session.serviceTier) const codexSessionId = session.metadata?.flavor === 'codex' ? session.metadata.codexSessionId?.trim() || null : null