mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
Withholding unlisted ids regressed Codex. On main the reader fabricated a row for an unlisted `current.model`, so the pill named it; dropping that fabrication left the name to provenance, and Codex reported none — `readCodexStructuredSessionOptions` emitted no `confirmed` ids, so `result.current.confirmed ?? []` recorded even a model the thread was genuinely running as `dispatched`. Probed against origin/main: a resumed thread on `gpt-unlisted` rendered `gpt-unlisted` there and `Model` here. That is the bug this branch fixes for Claude, recreated for Codex. `input.current` cannot answer it alone: it merges two sources. `session.options` holds a pick restored from a previous session or a write of ours the thread has not echoed, while `session.reportedOptions` has one writer — the opened or resumed thread — and is the agent's own state. Only `readLiveCodexSessionOptions` knows which answered, so it now says, and the reader passes it on. `confirmed` is an existing optional wire field whose absence already means "unconfirmed", so a host that predates this sends nothing and old clients read exactly what they read now. A model the reader substitutes because nothing was current is never confirmed: that id is our choice, not a report.
242 lines
7.8 KiB
TypeScript
242 lines
7.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { CodexAppServerConnection } from './codex-app-server-connection'
|
|
import { CodexAcquisitionWindow } from './codex-structured-acquisition-window'
|
|
import {
|
|
applyCodexStructuredSessionOption,
|
|
readCodexStructuredSessionOptions,
|
|
readLiveCodexSessionOptions,
|
|
reportedCodexThreadOptions,
|
|
restoredCodexSessionOptions
|
|
} from './codex-structured-session-options'
|
|
import { CodexBackgroundTaskTracker } from './codex-background-task-tracker'
|
|
import type { CodexSession } from './codex-structured-session-state'
|
|
|
|
function optionSession(request: CodexAppServerConnection['request']): CodexSession {
|
|
return {
|
|
connection: {
|
|
pid: 1,
|
|
closed: false,
|
|
request,
|
|
notify: () => {},
|
|
respond: () => {},
|
|
respondWithError: () => {},
|
|
close: async () => true
|
|
},
|
|
backgroundTasks: new CodexBackgroundTaskTracker('thread-1'),
|
|
ended: false,
|
|
requestedClose: false,
|
|
fence: 1,
|
|
acquisitionGeneration: 'generation-1',
|
|
threadId: 'thread-1',
|
|
historyPath: null,
|
|
prompts: new CodexAcquisitionWindow().prompts,
|
|
options: new Map(),
|
|
reportedOptions: { model: 'gpt-live', effort: 'high' },
|
|
turnIdWaiters: [],
|
|
translator: null
|
|
}
|
|
}
|
|
|
|
describe('structured Codex session options', () => {
|
|
it('filters restored records to recognized turn options', () => {
|
|
expect(
|
|
Object.fromEntries(
|
|
restoredCodexSessionOptions({
|
|
model: 'gpt-live',
|
|
effort: 'high',
|
|
threadId: 'thread-injected',
|
|
input: 'input-injected'
|
|
})
|
|
)
|
|
).toEqual({ model: 'gpt-live', effort: 'high' })
|
|
})
|
|
|
|
it('hydrates paged provider models and their supported efforts', async () => {
|
|
const request = vi.fn(async (_method: string, params?: Record<string, unknown>) =>
|
|
params?.cursor
|
|
? {
|
|
data: [
|
|
{
|
|
model: 'gpt-second',
|
|
displayName: 'GPT Second',
|
|
description: 'Fast',
|
|
hidden: false,
|
|
supportedReasoningEfforts: [
|
|
{ reasoningEffort: 'low', description: 'Quick reasoning' }
|
|
],
|
|
defaultReasoningEffort: 'low',
|
|
isDefault: false
|
|
}
|
|
],
|
|
nextCursor: null
|
|
}
|
|
: {
|
|
data: [
|
|
{
|
|
model: 'gpt-live',
|
|
displayName: 'GPT Live',
|
|
hidden: false,
|
|
supportedReasoningEfforts: [
|
|
{ reasoningEffort: 'medium', description: 'Balanced' },
|
|
{ reasoningEffort: 'high', description: 'Deep reasoning' }
|
|
],
|
|
defaultReasoningEffort: 'medium',
|
|
isDefault: true
|
|
}
|
|
],
|
|
nextCursor: 'page-2'
|
|
}
|
|
)
|
|
|
|
await expect(
|
|
readCodexStructuredSessionOptions({
|
|
connection: { request } as never,
|
|
current: { model: 'gpt-live', effort: 'medium' }
|
|
})
|
|
).resolves.toEqual({
|
|
models: [
|
|
{
|
|
id: 'gpt-live',
|
|
label: 'GPT Live',
|
|
isDefault: true,
|
|
defaultEffort: 'medium',
|
|
efforts: [
|
|
{ value: 'medium', label: 'Medium', description: 'Balanced' },
|
|
{ value: 'high', label: 'High', description: 'Deep reasoning' }
|
|
]
|
|
},
|
|
{
|
|
id: 'gpt-second',
|
|
label: 'GPT Second',
|
|
description: 'Fast',
|
|
isDefault: false,
|
|
defaultEffort: 'low',
|
|
efforts: [{ value: 'low', label: 'Low', description: 'Quick reasoning' }]
|
|
}
|
|
],
|
|
current: { model: 'gpt-live', effort: 'medium' }
|
|
})
|
|
expect(request).toHaveBeenNthCalledWith(
|
|
2,
|
|
'model/list',
|
|
{ limit: 100, includeHidden: false, cursor: 'page-2' },
|
|
{ timeoutMs: undefined }
|
|
)
|
|
})
|
|
|
|
it('reports an unlisted current model without offering it as a choice', async () => {
|
|
// Was: a fabricated `{ id, label: id, efforts: [] }` row, which offered a raw launch
|
|
// id in the picker as though the account were entitled to it.
|
|
const request = vi.fn(async () => ({
|
|
data: [{ model: 'gpt-live', displayName: 'GPT Live', isDefault: true }],
|
|
nextCursor: null
|
|
}))
|
|
|
|
await expect(
|
|
readCodexStructuredSessionOptions({
|
|
connection: { request } as never,
|
|
current: { model: 'gpt-unlisted' }
|
|
})
|
|
).resolves.toEqual({
|
|
models: [{ id: 'gpt-live', label: 'GPT Live', isDefault: true, efforts: [] }],
|
|
current: { model: 'gpt-unlisted' }
|
|
})
|
|
})
|
|
|
|
it('confirms a model the thread reported, but not a pick we restored', async () => {
|
|
// The display rule names an unlisted model only when the agent reported it, so the
|
|
// reader has to say which of the two answered. `session.options` is a restored pick
|
|
// or an unechoed write of ours; `reportedOptions` is the opened thread's own state.
|
|
const request = vi.fn(async () => ({
|
|
data: [{ model: 'gpt-live', displayName: 'GPT Live', isDefault: true }],
|
|
nextCursor: null
|
|
}))
|
|
const session = (
|
|
options: Map<string, string>,
|
|
reported: Record<string, string>
|
|
): CodexSession =>
|
|
({ connection: { request }, options, reportedOptions: reported }) as unknown as CodexSession
|
|
|
|
await expect(
|
|
readLiveCodexSessionOptions(session(new Map(), { model: 'gpt-unlisted' }), undefined)
|
|
).resolves.toMatchObject({ current: { model: 'gpt-unlisted', confirmed: ['model'] } })
|
|
|
|
const restored = await readLiveCodexSessionOptions(
|
|
session(new Map([['model', 'gpt-stale']]), {}),
|
|
undefined
|
|
)
|
|
expect(restored.current).toEqual({ model: 'gpt-stale' })
|
|
expect(restored.current.confirmed).toBeUndefined()
|
|
})
|
|
|
|
it('does not confirm a model it substituted rather than read', async () => {
|
|
// With nothing current the reader picks the default; that is our choice, not a report.
|
|
const request = vi.fn(async () => ({
|
|
data: [{ model: 'gpt-live', displayName: 'GPT Live', isDefault: true }],
|
|
nextCursor: null
|
|
}))
|
|
|
|
await expect(
|
|
readCodexStructuredSessionOptions({
|
|
connection: { request } as never,
|
|
current: {},
|
|
confirmed: ['model']
|
|
})
|
|
).resolves.toEqual({
|
|
models: [{ id: 'gpt-live', label: 'GPT Live', isDefault: true, efforts: [] }],
|
|
current: { model: 'gpt-live' }
|
|
})
|
|
})
|
|
|
|
it('hydrates current values from thread start or resume', () => {
|
|
expect(
|
|
reportedCodexThreadOptions({
|
|
threadId: 'thread-1',
|
|
historyPath: null,
|
|
model: 'gpt-live',
|
|
effort: 'high'
|
|
})
|
|
).toEqual({ model: 'gpt-live', effort: 'high' })
|
|
})
|
|
|
|
it('reconciles an incompatible effort when only the model changes', async () => {
|
|
const session = optionSession(
|
|
vi.fn(async () => ({
|
|
data: [
|
|
{
|
|
model: 'gpt-live',
|
|
supportedReasoningEfforts: [{ reasoningEffort: 'high' }],
|
|
defaultReasoningEffort: 'high'
|
|
},
|
|
{
|
|
model: 'gpt-fast',
|
|
supportedReasoningEfforts: [{ reasoningEffort: 'low' }],
|
|
defaultReasoningEffort: 'low'
|
|
}
|
|
],
|
|
nextCursor: null
|
|
}))
|
|
)
|
|
|
|
await expect(
|
|
applyCodexStructuredSessionOption(session, 'model', 'gpt-fast', undefined)
|
|
).resolves.toEqual({ model: 'gpt-fast', effort: 'low' })
|
|
})
|
|
|
|
it('rejects values absent from the provider catalog', async () => {
|
|
const session = optionSession(
|
|
vi.fn(async () => ({
|
|
data: [{ model: 'gpt-live', supportedReasoningEfforts: [] }],
|
|
nextCursor: null
|
|
}))
|
|
)
|
|
|
|
await expect(
|
|
applyCodexStructuredSessionOption(session, 'model', 'not-entitled', undefined)
|
|
).rejects.toThrow('does not offer model not-entitled')
|
|
await expect(
|
|
applyCodexStructuredSessionOption(session, 'effort', 'high', undefined)
|
|
).rejects.toThrow('does not support high')
|
|
})
|
|
})
|