feat(llm-pi-ai): per-model reasoningEfforts and reasoning-dispatch compat switches

A model entry's reasoningEfforts dict declares its selectable thinking
levels — key = offered level, value = the wire spelling dispatch sends;
only off may leave the value empty (supported, send nothing). false
strips reasoning from a catalog model; every level is materialized
explicitly into pi-ai's thinkingLevelMap so nobody has to know pi-ai's
asymmetric absent-key defaulting. compat.thinkingFormat and
compat.supportsReasoningEffort become configurable on the route and per
model (model > route > catalog entry > pi-ai's URL-derived guess),
openai-completions only, so a private gateway speaking the DeepSeek
reasoning dialect no longer depends on its URL being recognizable.
Record-typed drift gates pin both enums to pi-ai's, and an unserviceable
declaration is refused at the write that produced it, naming route,
model, and level.
This commit is contained in:
Yichen Jiang
2026-08-08 00:51:54 +08:00
parent 698d65de2d
commit 0490f8bb06
14 changed files with 806 additions and 23 deletions
@@ -400,6 +400,149 @@ describe('provider profile lifecycle', () => {
.resolves.toMatchObject({ reasoning: { defaultEffort: ReasoningEffortId('off') } })
})
it('serves declared reasoning efforts to selectors and honours the profile default', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKey: 'gw-key',
api: 'openai-completions',
baseURL: 'https://acme.test/v1',
reasoning: 'high',
models: [{
id: 'acme-think',
contextWindow: 65_536,
maxTokens: 4096,
reasoningEfforts: { off: null, low: 'low', high: 'high' },
}],
},
},
})
// Declared levels reach the same seam catalog metadata does, so the
// effort picker works for a model pi-ai has never heard of.
await expect(ctx.llm.resolveModelInfo('acme-gateway', 'acme-think')).resolves.toMatchObject({
reasoning: {
efforts: [
{ id: ReasoningEffortId('off'), name: 'Off' },
{ id: ReasoningEffortId('low'), name: 'Low' },
{ id: ReasoningEffortId('high'), name: 'High' },
],
defaultEffort: ReasoningEffortId('high'),
},
})
})
it('sends the declared wire spelling and refuses undeclared levels before network I/O', async () => {
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKey: 'gw-key',
api: 'openai-completions',
baseURL: `${server.url}/v1`,
models: [{
id: 'acme-think',
contextWindow: 65_536,
maxTokens: 4096,
reasoningEfforts: { off: null, high: 'ultra' },
}],
},
},
})
await assemble(ctx, {
provider: 'acme-gateway',
model: 'acme-think',
reasoningEffort: ReasoningEffortId('high'),
messages: [],
})
// The declared value, not the canonical level name, goes on the wire.
expect(server.requests[0]).toMatchObject({ reasoning_effort: 'ultra' })
const undeclared = await assemble(ctx, {
provider: 'acme-gateway',
model: 'acme-think',
reasoningEffort: ReasoningEffortId('max'),
messages: [],
})
expect(undeclared.finish).toMatchObject({
kind: 'error',
failure: { code: 'UNSUPPORTED_REASONING_EFFORT' },
})
expect(server.requests).toHaveLength(1)
})
it('dispatches the compat-switched dialect on a declared route', async () => {
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKey: 'gw-key',
api: 'openai-completions',
baseURL: `${server.url}/v1`,
// Without the switch pi-ai guesses the dialect from the endpoint
// URL, and a private gateway's URL says nothing.
compat: { thinkingFormat: 'deepseek' },
models: [{
id: 'acme-think',
contextWindow: 65_536,
maxTokens: 4096,
reasoningEfforts: { off: null, high: 'high' },
}],
},
},
})
const prompt = (effort: string): Promise<unknown> => assemble(ctx, {
provider: 'acme-gateway',
model: 'acme-think',
reasoningEffort: ReasoningEffortId(effort),
messages: [],
})
await prompt('high')
expect(server.requests[0]).toMatchObject({ thinking: { type: 'enabled' }, reasoning_effort: 'high' })
await prompt('off')
expect(server.requests[1]).toMatchObject({ thinking: { type: 'disabled' } })
expect(server.requests[1]).not.toHaveProperty('reasoning_effort')
})
it('holds back reasoning_effort when the endpoint cannot take it', async () => {
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKey: 'gw-key',
api: 'openai-completions',
baseURL: `${server.url}/v1`,
compat: { supportsReasoningEffort: false },
models: [{
id: 'acme-think',
contextWindow: 65_536,
maxTokens: 4096,
reasoningEfforts: { off: null, high: 'high' },
}],
},
},
})
await assemble(ctx, {
provider: 'acme-gateway',
model: 'acme-think',
reasoningEffort: ReasoningEffortId('high'),
messages: [],
})
expect(server.requests[0]).not.toHaveProperty('reasoning_effort')
})
it('accepts absent credentials for pi-ai ambient authentication', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
const server = await mockServer([{ events: textEvents }])