mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* feat(native-chat): offer Codex Ultrafast as a speed choice Codex lists Ultrafast as a third service tier for eligible models and accounts (GPT-6.1 Sol). Codex chats saved Fast as on/off, which cannot hold a third tier. The Codex speed is now a Standard/Fast/Ultrafast choice built from the tiers the model lists; the tier id is still resolved from the catalog at each send. Older clients keep their Fast toggle: the host still reports Fast on/off and accepts it as a speed. * refactor(native-chat): save the Codex tier id instead of a speed name The chat now keeps the tier id Codex lists (serviceTier: default, priority, ultrafast) and sends it as-is on turn/start; Codex drops a tier the model no longer lists with its own warning. This removes the saved per-model tier map, the send-time lookup and the reconcile code. The unused fastModeTierByModel catalog field is deleted. Older clients still get Fast on/off, derived from the model's tier named Fast. Also drops the pnpm-lock.yaml churn the first commit picked up. * test(pi): drop the removed fastModeTierByModel catalog field * fix(native-chat): review round 1 for Codex service tiers - Accept an older client's fastMode write through the Codex adapter; it was refused before reaching the tier alias. - Bump the saved model catalog schema so catalogs from before serviceTiers re-list instead of offering a Fast toggle with no tier. - Keep only the reported tier on the record, not the Fast it implies. - Drop a reported tier, not only a picked one, when the next model does not list it. - Localize Standard inside the speed choice only. - Return the options answer directly instead of a one-field wrapper. * fix(native-chat): review round 2 for Codex service tiers Keep a tier Codex reports for the thread (it accepts unlisted ones such as flex); read the legacy Fast tier beside an empty serviceTiers list. * fix(native-chat): review round 3 for Codex service tiers A reported tier the old model listed and the new one does not resets to Standard on a model switch, and the reported tier itself stays pickable. * fix(native-chat): keep a re-picked reported Codex tier The thread's reported tier is never reset by the unlisted-tier drop, so a re-picked Flex survives the next read, a model switch, and the turn. * refactor(native-chat): one rule for which Codex tier a model offers Standard, a tier the model lists, or the thread's reported tier that no model lists. The setter and the unlisted-tier reset share it, replacing the separate reported-tier and model-switch guards. * test(native-chat): drop the removed fastModeTierByModel field from main's new catalog test
188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
clearNativeChatSessionOptionModel,
|
|
narrowStructuredLaunchSeedOptions,
|
|
resolveNativeChatSessionOptionDefaults,
|
|
resolveStructuredLaunchSeedOptions,
|
|
updateNativeChatSessionOptionDefaults
|
|
} from './native-chat-session-option-defaults'
|
|
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
|
|
|
|
const persistedGrok = (
|
|
model: string | undefined,
|
|
valuesByModel: Record<string, Record<string, string>> = {}
|
|
): PersistedNativeChatSessionOptions => ({
|
|
grok: { ...(model ? { model } : {}), valuesByModel }
|
|
})
|
|
|
|
describe('clearNativeChatSessionOptionModel', () => {
|
|
it('drops the model a retired id would otherwise launch as -m', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
persistedGrok('grok-build', { 'grok-build': { effort: 'low' } }),
|
|
'grok'
|
|
)
|
|
expect(cleared.grok?.model).toBeUndefined()
|
|
// Resolution keys off `model`, so clearing it is what stops the flag going out.
|
|
expect(resolveNativeChatSessionOptionDefaults(cleared, 'grok')).toBeUndefined()
|
|
})
|
|
|
|
it('keeps the per-model values so a reselect restores the old effort', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
persistedGrok('grok-build', { 'grok-build': { effort: 'low' } }),
|
|
'grok'
|
|
)
|
|
expect(cleared.grok?.valuesByModel).toEqual({ 'grok-build': { effort: 'low' } })
|
|
const reselected = updateNativeChatSessionOptionDefaults({
|
|
persisted: cleared,
|
|
agent: 'grok',
|
|
modelId: 'grok-build',
|
|
optionId: 'model',
|
|
value: 'grok-build'
|
|
})
|
|
expect(resolveNativeChatSessionOptionDefaults(reselected, 'grok')).toEqual({
|
|
model: 'grok-build',
|
|
effort: 'low'
|
|
})
|
|
})
|
|
|
|
it('leaves every other agent untouched', () => {
|
|
const cleared = clearNativeChatSessionOptionModel(
|
|
{ ...persistedGrok('grok-build'), claude: { model: 'opus', valuesByModel: {} } },
|
|
'grok'
|
|
)
|
|
expect(cleared.claude).toEqual({ model: 'opus', valuesByModel: {} })
|
|
})
|
|
|
|
it('is a no-op when nothing is persisted for the agent', () => {
|
|
expect(clearNativeChatSessionOptionModel(undefined, 'grok')).toEqual({})
|
|
expect(clearNativeChatSessionOptionModel({}, 'grok')).toEqual({})
|
|
const untouched = persistedGrok(undefined, { 'grok-4.5': { effort: 'high' } })
|
|
expect(clearNativeChatSessionOptionModel(untouched, 'grok')).toEqual(untouched)
|
|
})
|
|
})
|
|
|
|
describe('resolveNativeChatSessionOptionDefaults', () => {
|
|
it('emits nothing until a model is explicitly picked, preserving the CLI default', () => {
|
|
expect(resolveNativeChatSessionOptionDefaults(undefined, 'grok')).toBeUndefined()
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok(undefined), 'grok')).toBeUndefined()
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok(' '), 'grok')).toBeUndefined()
|
|
})
|
|
|
|
it('returns a stale id verbatim, which is why retirement happens upstream', () => {
|
|
// Nothing here validates the id against the host; a retired one still resolves
|
|
// and becomes `-m <id>`. Only clearing the persisted value prevents that.
|
|
expect(resolveNativeChatSessionOptionDefaults(persistedGrok('grok-build'), 'grok')).toEqual({
|
|
model: 'grok-build'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveStructuredLaunchSeedOptions', () => {
|
|
const persistedCodex = (
|
|
valuesByModel: Record<string, Record<string, string | boolean>>
|
|
): PersistedNativeChatSessionOptions =>
|
|
({ codex: { model: 'gpt-5.6-sol', valuesByModel } }) as PersistedNativeChatSessionOptions
|
|
|
|
it('seeds the saved model and effort a structured create must apply', () => {
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: 'medium' } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
|
|
})
|
|
|
|
it('encodes Fast mode while dropping ids the providers only accept mid-session', () => {
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({
|
|
'gpt-5.6-sol': { effort: 'high', fastMode: true, personality: 'concise' }
|
|
}),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol', effort: 'high', fastMode: 'true' })
|
|
})
|
|
|
|
it('seeds a saved service tier', () => {
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { serviceTier: 'ultrafast' } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol', serviceTier: 'ultrafast' })
|
|
})
|
|
|
|
it('drops a seeded id whose persisted value is not a usable string', () => {
|
|
// settings.json is user-writable, so a non-string `effort` must not reach a
|
|
// record typed Record<string, string> and be emitted as a turn option.
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: true } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol' })
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(
|
|
persistedCodex({ 'gpt-5.6-sol': { effort: ' ' } }),
|
|
'codex'
|
|
)
|
|
).toEqual({ model: 'gpt-5.6-sol' })
|
|
})
|
|
|
|
it('seeds nothing when the stored values empty the model out', () => {
|
|
// `valuesByModel` is merged over the resolved model, so a stored `model` key
|
|
// can blank it. Emitting `{ model: '' }` fails the record's bounded-string
|
|
// guard, and that throw is not a wire refusal code — it escapes as a raw
|
|
// error the client reads as unknown, stranding the launch with no fallback.
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions(persistedCodex({ 'gpt-5.6-sol': { model: '' } }), 'codex')
|
|
).toBeUndefined()
|
|
})
|
|
|
|
it('seeds nothing until a model is picked, so the CLI default survives', () => {
|
|
expect(resolveStructuredLaunchSeedOptions(undefined, 'codex')).toBeUndefined()
|
|
expect(
|
|
resolveStructuredLaunchSeedOptions({ codex: { valuesByModel: {} } }, 'codex')
|
|
).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('narrowStructuredLaunchSeedOptions', () => {
|
|
it('keeps the seedable ids an explicit selection names', () => {
|
|
expect(
|
|
narrowStructuredLaunchSeedOptions({ model: 'opus', effort: 'high', fastMode: false })
|
|
).toEqual({
|
|
model: 'opus',
|
|
effort: 'high',
|
|
fastMode: 'false'
|
|
})
|
|
})
|
|
|
|
// A never-picked Fast preference must stay absent, not default on: seeding `true`
|
|
// would silently move a user onto the faster tier they never chose.
|
|
it('seeds no fastMode at all when the user never picked one', () => {
|
|
expect(narrowStructuredLaunchSeedOptions({ model: 'opus', effort: 'high' })).toEqual({
|
|
model: 'opus',
|
|
effort: 'high'
|
|
})
|
|
})
|
|
|
|
it('drops ids no structured create may seed', () => {
|
|
expect(narrowStructuredLaunchSeedOptions({ model: 'opus', mode: 'plan' })).toEqual({
|
|
model: 'opus'
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['nothing at all', {}],
|
|
['whitespace only', { model: ' ', effort: '\t' }],
|
|
['no usable string', { model: 5, effort: null }],
|
|
['undefined', undefined]
|
|
])('resolves %s to undefined rather than {}', (_name, values) => {
|
|
// An empty map fails the durable record's bounded-string guard, and
|
|
// `agent_session_options_invalid` is not a wire refusal code: the throw escapes as a raw
|
|
// error the client reads as unknown, stranding the launch with no fallback.
|
|
expect(narrowStructuredLaunchSeedOptions(values as never)).toBeUndefined()
|
|
})
|
|
})
|