Default model: let the picker choose a reasoning effort
The header switcher only offered provider and model, so catalog efforts a brand-new Session could inherit were reachable solely by typing effort into the Eval and Execution forms. Rows whose catalog entry advertises efforts now expand a second tier — the model default plus each effort, with descriptions and a check on the route in effect — the switch validates the effort against the catalog, treats an effort-only change as a real switch, and forwards the branded id to selectModel. Docs name the re-pointing that dee76b1 added.
This commit is contained in:
@@ -11,10 +11,17 @@ const MASTER = 'master-1'
|
||||
/** One model route as the catalog reports it. */
|
||||
const selection = (provider: string, model: string, reasoningEffort?: string): ModelSelection =>
|
||||
({ provider, model, ...reasoningEffort ? { reasoningEffort } : {} })
|
||||
/** A catalog whose default is `current` and whose routable providers offer exactly `models`, keyed by provider id. */
|
||||
function catalog(current: ModelSelection, models: Record<string, string[]> = { host: ['default', 'next'] }): ModelCatalog {
|
||||
/** Reasoning metadata offered by an exact `provider/model` route, keyed by `${provider}/${model}`. */
|
||||
type ReasoningByRoute = Record<string, { efforts: { id: string; name: string; description?: string }[]; defaultEffort?: string }>
|
||||
/** A catalog whose default is `current` and whose routable providers offer exactly `models`, keyed by provider id;
|
||||
* `reasoning` attaches selectable-effort metadata to the exact `provider/model` routes it names. */
|
||||
function catalog(current: ModelSelection, models: Record<string, string[]> = { host: ['default', 'next'] },
|
||||
reasoning: ReasoningByRoute = {}): ModelCatalog {
|
||||
return { default: current, routableProviders: Object.keys(models), failures: [],
|
||||
groups: Object.entries(models).map(([id, ids]) => ({ id, name: id, models: ids.map(model => ({ id: model, name: model })) })) }
|
||||
groups: Object.entries(models).map(([id, ids]) => ({ id, name: id, models: ids.map(model => {
|
||||
const route = `${id}/${model}`
|
||||
return reasoning[route] ? { id: model, name: model, reasoning: reasoning[route] } : { id: model, name: model }
|
||||
}) })) }
|
||||
}
|
||||
/**
|
||||
* A fake Host context: `modelCatalog` keeps reporting `initial` until `selectModel` runs, when it starts reporting `saved`
|
||||
@@ -88,13 +95,60 @@ describe('default model switch', () => {
|
||||
})
|
||||
|
||||
it('forwards reasoning effort as a branded id and reports the switch saved only when it matches', async () => {
|
||||
const host = controller(catalog(selection('host', 'default')), catalog(selection('host', 'next', 'high')))
|
||||
const efforts = [{ id: 'high', name: 'High' }]
|
||||
const host = controller(catalog(selection('host', 'default'), { host: ['default', 'next'] }, { 'host/next': { efforts } }),
|
||||
catalog(selection('host', 'next', 'high'), { host: ['default', 'next'] }, { 'host/next': { efforts } }))
|
||||
await expect(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next', reasoningEffort: 'high' }))
|
||||
.resolves.toEqual(selection('host', 'next', 'high'))
|
||||
const request = host.selectModel.mock.calls[0]?.[0]
|
||||
expect(request?.reasoningEffort).toBe(ReasoningEffortId('high'))
|
||||
expect(String(request?.reasoningEffort)).toBe('high')
|
||||
})
|
||||
|
||||
it('switches onto a model with a supported effort, branding the selectModel request', async () => {
|
||||
const efforts = [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }]
|
||||
const host = controller(catalog(selection('host', 'default'), { host: ['default', 'next'] }, { 'host/next': { efforts } }),
|
||||
catalog(selection('host', 'next', 'low')))
|
||||
await expect(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next', reasoningEffort: 'low' }))
|
||||
.resolves.toEqual(selection('host', 'next', 'low'))
|
||||
expect(host.selectModel).toHaveBeenCalledOnce()
|
||||
expect(host.selectModel).toHaveBeenCalledWith({ sessionId: MASTER, provider: 'host', model: 'next', reasoningEffort: ReasoningEffortId('low') })
|
||||
})
|
||||
|
||||
it('refuses an effort the model does not offer, without touching the Session selection', async () => {
|
||||
const efforts = [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }]
|
||||
const host = controller(catalog(selection('host', 'default'), { host: ['default', 'next'] }, { 'host/next': { efforts } }))
|
||||
const failure = await refused(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next', reasoningEffort: 'ultra' }))
|
||||
expect(failure.reason).toBe('unknownEffort')
|
||||
expect(host.selectModel).not.toHaveBeenCalled()
|
||||
expect(host.modelCatalog).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('refuses a reasoning effort for a model that offers none', async () => {
|
||||
const host = controller(catalog(selection('host', 'default')))
|
||||
const failure = await refused(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next', reasoningEffort: 'low' }))
|
||||
expect(failure.reason).toBe('unknownEffort')
|
||||
expect(host.selectModel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats an effort change alone as a valid switch away from the model default', async () => {
|
||||
const efforts = [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }]
|
||||
const host = controller(catalog(selection('host', 'next', 'high'), { host: ['default', 'next'] }, { 'host/next': { efforts, defaultEffort: 'high' } }),
|
||||
catalog(selection('host', 'next', 'low')))
|
||||
await expect(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next', reasoningEffort: 'low' }))
|
||||
.resolves.toEqual(selection('host', 'next', 'low'))
|
||||
expect(host.selectModel).toHaveBeenCalledTimes(1)
|
||||
expect(host.selectModel).toHaveBeenCalledWith({ sessionId: MASTER, provider: 'host', model: 'next', reasoningEffort: ReasoningEffortId('low') })
|
||||
})
|
||||
|
||||
it('refuses an unchanged route whose implicit effort settles on the current default', async () => {
|
||||
const efforts = [{ id: 'low', name: 'Low' }, { id: 'high', name: 'High' }]
|
||||
const host = controller(catalog(selection('host', 'next', 'high'), { host: ['default', 'next'] }, { 'host/next': { efforts, defaultEffort: 'high' } }))
|
||||
const failure = await refused(switchDefaultModel(host.ctx, MASTER, { provider: 'host', model: 'next' }))
|
||||
expect(failure.reason).toBe('unchanged')
|
||||
expect(host.selectModel).not.toHaveBeenCalled()
|
||||
expect(host.modelCatalog).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('default model refusal', () => {
|
||||
@@ -105,6 +159,11 @@ describe('default model refusal', () => {
|
||||
expect(defaultModelRefusal(t, failure)).toBe('[defaultModelError.unknownProvider]')
|
||||
})
|
||||
|
||||
it('localizes a refusal the host tagged with an unknownEffort code', () => {
|
||||
const failure = new ApiError('Model "host/next" does not offer effort "ultra"', 400, { code: 'defaultModel.unknownEffort' })
|
||||
expect(defaultModelRefusal(t, failure)).toBe('[defaultModelError.unknownEffort]')
|
||||
})
|
||||
|
||||
it('returns error text that names no defaultModel code unchanged', () => {
|
||||
const failure = new Error('Provider "ghost" has no available models')
|
||||
expect(defaultModelRefusal(t, failure)).toBe('Provider "ghost" has no available models')
|
||||
|
||||
Reference in New Issue
Block a user