mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
fix(codex): validate restored structured models
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import type { CodexAppServerConnection } from './codex-app-server-connection'
|
||||
import {
|
||||
codexCatalogAdmitsModel,
|
||||
readCodexStructuredSessionOptions,
|
||||
reportedCodexThreadOptions,
|
||||
restoredCodexSessionOptions,
|
||||
type CodexModelCatalog
|
||||
} from './codex-structured-session-options'
|
||||
import type { CodexSession } from './codex-structured-session-state'
|
||||
import type { CodexOpenedThread } from './codex-structured-thread-open'
|
||||
|
||||
export type CodexAcquiredSessionOptions = {
|
||||
options: Map<string, string>
|
||||
reportedOptions: CodexSession['reportedOptions']
|
||||
notice?: string
|
||||
}
|
||||
|
||||
const NOTICE_MODEL_ID_LIMIT = 160
|
||||
|
||||
function displayedModelId(modelId: string): string {
|
||||
const bounded =
|
||||
modelId.length > NOTICE_MODEL_ID_LIMIT
|
||||
? `${modelId.slice(0, NOTICE_MODEL_ID_LIMIT - 1)}…`
|
||||
: modelId
|
||||
return JSON.stringify(bounded)
|
||||
}
|
||||
|
||||
function rejectedModelDescription(input: {
|
||||
restored: string | undefined
|
||||
reported: string | undefined
|
||||
}): string {
|
||||
if (input.restored && input.reported && input.restored !== input.reported) {
|
||||
return `restored model ${displayedModelId(input.restored)} and provider-reported model ${displayedModelId(input.reported)}`
|
||||
}
|
||||
return `model ${displayedModelId(input.restored ?? input.reported ?? 'unknown')}`
|
||||
}
|
||||
|
||||
function catalogCurrent(input: {
|
||||
options: Readonly<Record<string, string>> | undefined
|
||||
opened: CodexOpenedThread
|
||||
}): { model?: string; effort?: string } {
|
||||
const restoredModel = input.options?.model || undefined
|
||||
const restoredEffort = input.options?.effort || undefined
|
||||
const model = restoredModel ?? input.opened.model
|
||||
const effort = restoredEffort ?? input.opened.effort
|
||||
return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) }
|
||||
}
|
||||
|
||||
async function readCatalog(input: {
|
||||
connection: Pick<CodexAppServerConnection, 'request'>
|
||||
options: Readonly<Record<string, string>> | undefined
|
||||
opened: CodexOpenedThread
|
||||
timeoutMs: number | undefined
|
||||
}): Promise<CodexModelCatalog> {
|
||||
return readCodexStructuredSessionOptions({
|
||||
connection: input.connection,
|
||||
current: catalogCurrent(input),
|
||||
timeoutMs: input.timeoutMs
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
export async function resolveCodexAcquiredSessionOptions(input: {
|
||||
connection: Pick<CodexAppServerConnection, 'request'>
|
||||
options: Readonly<Record<string, string>> | undefined
|
||||
opened: CodexOpenedThread
|
||||
timeoutMs: number | undefined
|
||||
}): Promise<CodexAcquiredSessionOptions> {
|
||||
const catalog = await readCatalog(input)
|
||||
const restoredModel = input.options?.model || undefined
|
||||
const reportedModel = input.opened.model
|
||||
const restoredRefused = Boolean(restoredModel && !codexCatalogAdmitsModel(catalog, restoredModel))
|
||||
const reportedRefused = Boolean(reportedModel && !codexCatalogAdmitsModel(catalog, reportedModel))
|
||||
const options = restoredCodexSessionOptions(input.options, catalog)
|
||||
const reportedOptions = reportedCodexThreadOptions(input.opened, catalog)
|
||||
|
||||
if (!restoredRefused && !reportedRefused) {
|
||||
return { options, reportedOptions }
|
||||
}
|
||||
|
||||
const rejected = rejectedModelDescription({
|
||||
restored: restoredRefused ? restoredModel : undefined,
|
||||
reported: reportedRefused ? reportedModel : undefined
|
||||
})
|
||||
if (restoredModel && !restoredRefused && reportedRefused) {
|
||||
return {
|
||||
options,
|
||||
reportedOptions,
|
||||
notice: `Codex no longer lists ${rejected}. Orca will use the existing restored choice ${displayedModelId(restoredModel)} for this session.`
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = catalog?.models.filter((model) => model.isDefault) ?? []
|
||||
if (defaults.length !== 1) {
|
||||
return {
|
||||
options: restoredCodexSessionOptions(input.options, null),
|
||||
reportedOptions: reportedCodexThreadOptions(input.opened, null),
|
||||
notice: `Codex no longer lists ${rejected}, but did not identify a unique provider default. Orca left the model unchanged; choose an available model to continue.`
|
||||
}
|
||||
}
|
||||
|
||||
const replacement = defaults[0]
|
||||
options.set('model', replacement.id)
|
||||
options.delete('effort')
|
||||
if (
|
||||
replacement.defaultEffort &&
|
||||
replacement.efforts.some((effort) => effort.value === replacement.defaultEffort)
|
||||
) {
|
||||
options.set('effort', replacement.defaultEffort)
|
||||
}
|
||||
return {
|
||||
options,
|
||||
reportedOptions,
|
||||
notice: `Codex no longer lists ${rejected}. Orca selected the provider-listed default ${displayedModelId(replacement.id)} for this session.`
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,7 @@ import {
|
||||
closeCodexPublishedSession,
|
||||
handleCodexSessionExit
|
||||
} from './codex-structured-session-close'
|
||||
import {
|
||||
reportedCodexThreadOptions,
|
||||
restoredCodexSessionOptions
|
||||
} from './codex-structured-session-options'
|
||||
import { resolveCodexAcquiredSessionOptions } from './codex-structured-model-restoration'
|
||||
import {
|
||||
codexSessionLifecycle,
|
||||
mintCodexAcquisitionGeneration,
|
||||
@@ -169,6 +166,13 @@ export async function acquireCodexStructuredSession(input: {
|
||||
acquisitions.assertCurrent(sessionId, attempt)
|
||||
const opened = await openCodexThread(connection, launch, deps.requestTimeoutMs)
|
||||
acquisitions.assertCurrent(sessionId, attempt)
|
||||
const resolvedOptions = await resolveCodexAcquiredSessionOptions({
|
||||
connection,
|
||||
options: acquireInput.options,
|
||||
opened,
|
||||
timeoutMs: deps.requestTimeoutMs
|
||||
})
|
||||
acquisitions.assertCurrent(sessionId, attempt)
|
||||
primaryThreadId = opened.threadId
|
||||
const restoreAdmission = translator?.restoreThread(opened.threadId, opened.thread ?? {})
|
||||
if (restoreAdmission && !restoreAdmission.accepted) {
|
||||
@@ -205,8 +209,8 @@ export async function acquireCodexStructuredSession(input: {
|
||||
historyMode: opened.historyMode,
|
||||
activeTurnIds: new Set(),
|
||||
prompts: acquisition.prompts,
|
||||
options: restoredCodexSessionOptions(acquireInput.options),
|
||||
reportedOptions: reportedCodexThreadOptions(opened),
|
||||
options: resolvedOptions.options,
|
||||
reportedOptions: resolvedOptions.reportedOptions,
|
||||
turnIdWaiters: [],
|
||||
translator,
|
||||
backgroundTasks: new CodexBackgroundTaskTracker(opened.threadId, subagentExecutions),
|
||||
@@ -221,6 +225,16 @@ export async function acquireCodexStructuredSession(input: {
|
||||
}
|
||||
turnCancellation.register(session)
|
||||
sessions.set(sessionId, session)
|
||||
if (resolvedOptions.notice && acquireInput.events) {
|
||||
acquireInput.events.appendItem(
|
||||
{
|
||||
provider: 'orca',
|
||||
clientMessageId: `codex-model-restoration:${acquired.acquisitionGeneration}`
|
||||
},
|
||||
{ kind: 'status', text: resolvedOptions.notice, tone: 'warning' }
|
||||
)
|
||||
acquireInput.events.publish()
|
||||
}
|
||||
for (const event of acquisition.drain()) {
|
||||
event()
|
||||
}
|
||||
|
||||
@@ -12,10 +12,23 @@ import { AgentSessionOptionRejectedError } from '../native-chat/agent-session-wi
|
||||
const MODEL_PAGE_LIMIT = 100
|
||||
const MAX_MODEL_PAGES = 20
|
||||
|
||||
export type CodexModelCatalog = Pick<AgentSessionOptionsResult, 'models'> | null | undefined
|
||||
|
||||
export function restoredCodexSessionOptions(
|
||||
options: Readonly<Record<string, string>> | undefined
|
||||
options: Readonly<Record<string, string>> | undefined,
|
||||
catalog: CodexModelCatalog
|
||||
): Map<string, string> {
|
||||
return new Map(Object.entries(options ?? {}).filter(([key]) => isCodexTurnOptionKey(key)))
|
||||
const restored = new Map(
|
||||
Object.entries(options ?? {}).filter(
|
||||
([key, value]) => isCodexTurnOptionKey(key) && (key !== 'model' || value.length > 0)
|
||||
)
|
||||
)
|
||||
const model = restored.get('model')
|
||||
if (model && !codexCatalogAdmitsModel(catalog, model)) {
|
||||
restored.delete('model')
|
||||
restored.delete('effort')
|
||||
}
|
||||
return restored
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
@@ -102,13 +115,8 @@ export async function readCodexStructuredSessionOptions(input: {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (input.current.model && !models.some((model) => model.id === input.current.model)) {
|
||||
models.push({
|
||||
id: input.current.model,
|
||||
label: input.current.model,
|
||||
isDefault: false,
|
||||
efforts: []
|
||||
})
|
||||
if (cursor) {
|
||||
throw new Error(`codex app-server model enumeration exceeded ${MAX_MODEL_PAGES} pages`)
|
||||
}
|
||||
const model = input.current.model ?? models.find((entry) => entry.isDefault)?.id ?? models[0]?.id
|
||||
if (!model) {
|
||||
@@ -120,12 +128,23 @@ export async function readCodexStructuredSessionOptions(input: {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the final admission decision, including unavailable evidence's permissive case. */
|
||||
export function codexCatalogAdmitsModel(catalog: CodexModelCatalog, modelId: string): boolean {
|
||||
const models = catalog?.models ?? []
|
||||
// An empty list identifies no model, so it is not evidence against one — a
|
||||
// CLI unable to answer model/list must not have every model refused under it.
|
||||
// Do not turn this into a refusal.
|
||||
return models.length === 0 || models.some((model) => model.id === modelId)
|
||||
}
|
||||
|
||||
export function reportedCodexThreadOptions(
|
||||
opened: CodexOpenedThread
|
||||
opened: CodexOpenedThread,
|
||||
catalog: CodexModelCatalog
|
||||
): CodexSession['reportedOptions'] {
|
||||
const modelAdmitted = !opened.model || codexCatalogAdmitsModel(catalog, opened.model)
|
||||
return {
|
||||
...(opened.model ? { model: opened.model } : {}),
|
||||
...(opened.effort ? { effort: opened.effort } : {})
|
||||
...(opened.model && modelAdmitted ? { model: opened.model } : {}),
|
||||
...(opened.effort && modelAdmitted ? { effort: opened.effort } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user