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:
@@ -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 }])
|
||||
|
||||
Reference in New Issue
Block a user