fix(codex): show catalog-default Fast tier (#1179)

* fix(codex): show catalog-default Fast tier

* fix(web): show inherited Fast tier in header
This commit is contained in:
SSU-WEI HUANG
2026-07-27 07:38:24 +08:00
committed by GitHub
parent deb5d63695
commit 500407c6b1
7 changed files with 87 additions and 4 deletions
+1
View File
@@ -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)
};
+1
View File
@@ -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[]
@@ -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')
@@ -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<CodexModelCatalogEntry>
): string | null | undefined {
return serviceTier ?? findActiveModel(sessionModel, models)?.defaultServiceTier
}
/**
+10 -2
View File
@@ -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) {
<div className="flex h-full min-h-0 flex-col">
<SessionHeader
session={props.session}
serviceTier={effectiveCodexServiceTier}
onBack={props.onBack}
onToggleFiles={props.session.metadata?.path ? handleToggleFiles : undefined}
filesActive={false}
@@ -1431,7 +1439,7 @@ function SessionChatInner(props: SessionChatProps) {
: undefined)
: handleEffortChange
}
serviceTier={agentFlavor === 'codex' ? props.session.serviceTier : undefined}
serviceTier={effectiveCodexServiceTier}
onServiceTierChange={
agentFlavor === 'codex'
&& props.session.active
+50
View File
@@ -0,0 +1,50 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Session } from '@/types/api'
import { I18nProvider } from '@/lib/i18n-context'
import { ToastProvider } from '@/lib/toast-context'
import { SessionHeader } from './SessionHeader'
afterEach(() => 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(
<QueryClientProvider client={new QueryClient()}>
<ToastProvider>
<I18nProvider>
<SessionHeader
session={session}
serviceTier="priority"
onBack={vi.fn()}
api={null}
/>
</I18nProvider>
</ToastProvider>
</QueryClientProvider>
)
expect(screen.getByText('fast')).toBeInTheDocument()
})
})
+2 -1
View File
@@ -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