mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(chat): retain unlisted reported Codex models (#32068)
This commit is contained in:
@@ -157,6 +157,31 @@ describe('useMobileNativeChatSessionOptions', () => {
|
||||
expect(onAgentPicker).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it.each([null, 'gpt-5.6-terra'])(
|
||||
'tracks an unlisted Codex report after %s without inventing effort choices',
|
||||
async (initialModel) => {
|
||||
mount({ agent: 'codex', reportedModel: initialModel })
|
||||
update({ reportedModel: 'gpt-6.1-sol' })
|
||||
expect(api!.snapshot).toHaveLength(1)
|
||||
expect(api!.snapshot[0]).toMatchObject({
|
||||
valueSource: 'reported',
|
||||
action: { type: 'agent-picker' },
|
||||
kind: {
|
||||
currentValue: 'gpt-6.1-sol',
|
||||
choices: expect.arrayContaining([{ value: 'gpt-6.1-sol', label: 'gpt-6.1-sol' }])
|
||||
}
|
||||
})
|
||||
const snapshot = api!.snapshot
|
||||
update({ reportedModel: null })
|
||||
update({ reportedModel: 'gpt-6.1-sol' })
|
||||
expect(api!.snapshot).toBe(snapshot)
|
||||
await act(async () => {
|
||||
await api!.invokeAction('model')
|
||||
})
|
||||
expect(dispatchCommand).toHaveBeenCalledWith('/model', { delivery: 'type' })
|
||||
}
|
||||
)
|
||||
|
||||
it('types the Codex effort picker command', async () => {
|
||||
mount({ agent: 'codex', reportedModel: 'gpt-5.5' })
|
||||
await act(async () => {
|
||||
|
||||
@@ -110,7 +110,11 @@ export function useMobileNativeChatSessionOptions(args: {
|
||||
}
|
||||
// OMP reports exact selectors, including models absent from cached discovery.
|
||||
const matched =
|
||||
agent === 'omp' ? reportedModel.trim() : matchNativeChatCatalogModelId(catalog, reportedModel)
|
||||
agent === 'omp'
|
||||
? reportedModel.trim()
|
||||
: matchNativeChatCatalogModelId(catalog, reportedModel, {
|
||||
allowUnlisted: agent === 'codex'
|
||||
})
|
||||
if (!matched) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,6 +63,52 @@ describe('useNativeChatSessionOptions model reporting', () => {
|
||||
Object.defineProperty(window, 'api', { configurable: true, value: undefined })
|
||||
})
|
||||
|
||||
it.each([null, 'gpt-5.6-terra'])(
|
||||
'tracks an unlisted Codex report after %s without inventing effort choices',
|
||||
async (initialModel) => {
|
||||
discoverModels.mockResolvedValue(null)
|
||||
const paneKey = `codex-unlisted-${initialModel}:leaf`
|
||||
const dispatchCommand = vi.fn()
|
||||
storeState.agentStatusByPaneKey[paneKey] = initialModel ? { model: initialModel } : {}
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useNativeChatSessionOptions({
|
||||
agent: 'codex',
|
||||
terminalTabId: paneKey,
|
||||
targetPtyId: paneKey,
|
||||
dispatchCommand,
|
||||
paneKey
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(modelDescriptor(result.current.snapshot).currentValue).toBe(
|
||||
initialModel ?? undefined
|
||||
)
|
||||
)
|
||||
storeState.agentStatusByPaneKey[paneKey] = { model: 'gpt-6.1-sol' }
|
||||
rerender()
|
||||
await waitFor(() =>
|
||||
expect(modelDescriptor(result.current.snapshot).currentValue).toBe('gpt-6.1-sol')
|
||||
)
|
||||
expect(result.current.snapshot).toHaveLength(1)
|
||||
expect(result.current.snapshot[0]).toMatchObject({
|
||||
valueSource: 'reported',
|
||||
action: { type: 'agent-picker' }
|
||||
})
|
||||
expect(modelDescriptor(result.current.snapshot).choices).toContainEqual({
|
||||
value: 'gpt-6.1-sol',
|
||||
label: 'gpt-6.1-sol'
|
||||
})
|
||||
const snapshot = result.current.snapshot
|
||||
storeState.agentStatusByPaneKey[paneKey] = {}
|
||||
rerender()
|
||||
storeState.agentStatusByPaneKey[paneKey] = { model: 'gpt-6.1-sol' }
|
||||
rerender()
|
||||
expect(result.current.snapshot).toBe(snapshot)
|
||||
await result.current.surface?.invokeAction('model')
|
||||
expect(dispatchCommand).toHaveBeenCalledWith('/model', { delivery: 'type' })
|
||||
}
|
||||
)
|
||||
|
||||
it('names an OMP session by the model its hook reports, before and after discovery', async () => {
|
||||
// Why: OMP seeds no models and has no terminal frame to read; the hook's
|
||||
// `provider/id` stamp is the only way the pill can name the running model.
|
||||
|
||||
@@ -216,7 +216,9 @@ export function useNativeChatSessionOptions(args: {
|
||||
const matched =
|
||||
agent === 'omp'
|
||||
? reportedModel.trim()
|
||||
: matchNativeChatCatalogModelId({ ...catalog, models }, reportedModel)
|
||||
: matchNativeChatCatalogModelId({ ...catalog, models }, reportedModel, {
|
||||
allowUnlisted: agent === 'codex'
|
||||
})
|
||||
if (!matched || appliedReportedModelRef.current === matched) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,6 +48,35 @@ describe('applyNativeChatReportedSessionOptions', () => {
|
||||
})
|
||||
|
||||
describe('matchNativeChatCatalogModelId', () => {
|
||||
it.each(['gpt-6.1-sol', 'gpt-5.5-custom', 'custom/Model-v2'])(
|
||||
'keeps the exact unlisted selector %s only when requested',
|
||||
(reported) => {
|
||||
expect(matchNativeChatCatalogModelId(CODEX_SESSION_OPTION_CATALOG, reported)).not.toBe(
|
||||
reported
|
||||
)
|
||||
expect(
|
||||
matchNativeChatCatalogModelId(CODEX_SESSION_OPTION_CATALOG, reported, {
|
||||
allowUnlisted: true
|
||||
})
|
||||
).toBe(reported)
|
||||
}
|
||||
)
|
||||
|
||||
it('retains known labels and rejects blank or free text as unlisted selectors', () => {
|
||||
expect(
|
||||
matchNativeChatCatalogModelId(CODEX_SESSION_OPTION_CATALOG, ' GPT-5.5 ', {
|
||||
allowUnlisted: true
|
||||
})
|
||||
).toBe('gpt-5.5')
|
||||
for (const reported of ['', ' ', 'unknown model', 'unknown\nmodel']) {
|
||||
expect(
|
||||
matchNativeChatCatalogModelId(CODEX_SESSION_OPTION_CATALOG, reported, {
|
||||
allowUnlisted: true
|
||||
})
|
||||
).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('prefers the longest contained id and keeps catalog order for ties', () => {
|
||||
const catalog = {
|
||||
...CLAUDE_SESSION_OPTION_CATALOG,
|
||||
|
||||
@@ -153,10 +153,11 @@ export function applyNativeChatReportedSessionOptions(
|
||||
|
||||
/** Resolves a hook-reported model string to a catalog id: exact id, then label,
|
||||
* then the longest id the report contains. A catalog that seeds no models keeps
|
||||
* the report itself. Null when nothing matches. */
|
||||
* the report itself. Callers may retain unlisted selectors without fuzzy id matching. */
|
||||
export function matchNativeChatCatalogModelId(
|
||||
catalog: AgentSessionOptionCatalog,
|
||||
reported: string
|
||||
reported: string,
|
||||
options?: { allowUnlisted?: boolean }
|
||||
): string | null {
|
||||
const normalized = reported.trim().toLowerCase()
|
||||
if (!normalized) {
|
||||
@@ -175,6 +176,9 @@ export function matchNativeChatCatalogModelId(
|
||||
if (byLabel) {
|
||||
return byLabel.id
|
||||
}
|
||||
if (options?.allowUnlisted && !/\s/.test(normalized)) {
|
||||
return reported.trim()
|
||||
}
|
||||
let containingId: string | null = null
|
||||
for (const model of catalog.models) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user