Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	docs/module-graph.md
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/index.ts
#	packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 17:56:53 +08:00
841 changed files with 13085 additions and 5978 deletions

View File

@@ -481,6 +481,185 @@ 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': {
apiKeyEnv: 'PI_TEST_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 () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKeyEnv: 'PI_TEST_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 () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKeyEnv: 'PI_TEST_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('sends a declared off value as the effort parameter instead of omitting it', async () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKeyEnv: 'PI_TEST_KEY',
api: 'openai-completions',
baseURL: `${server.url}/v1`,
models: [{
id: 'acme-think',
contextWindow: 65_536,
maxTokens: 4096,
reasoningEfforts: { off: 'none', high: 'high' },
}],
},
},
})
// The adapter strips a selected Off to "no reasoning option", and pi-ai's
// dispatch reads thinkingLevelMap.off exactly then — so the declared value
// still reaches the wire, which is the README's promise for `off: none`.
await assemble(ctx, {
provider: 'acme-gateway',
model: 'acme-think',
reasoningEffort: ReasoningEffortId('off'),
messages: [],
})
expect(server.requests[0]).toMatchObject({ reasoning_effort: 'none' })
})
it('holds back reasoning_effort when the endpoint cannot take it', async () => {
vi.stubEnv('PI_TEST_KEY', 'test-key')
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: {
'acme-gateway': {
apiKeyEnv: 'PI_TEST_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 }])

View File

@@ -10,8 +10,8 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
import { createModels } from '@earendil-works/pi-ai'
import type { Api, Model, Provider } from '@earendil-works/pi-ai'
import { createModels, getSupportedThinkingLevels } from '@earendil-works/pi-ai'
import type { Api, Model, OpenAICompletionsCompat, Provider } from '@earendil-works/pi-ai'
import { resolveProfiles } from '../src/config.ts'
import { buildProvider, supportedProtocols } from '../src/provider.ts'
import { assemble } from './assemble.ts'
@@ -484,6 +484,248 @@ describe('catalog routes with per-model configuration', () => {
})
})
describe('per-model reasoning efforts', () => {
/** One hand-declared route holding exactly the given models. */
function declared(models: LlmPiAi.PiAiModelProfile[]): Record<string, LlmPiAi.PiAiProviderProfile> {
return { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models } }
}
/** The first materialized model of one route, or throw. */
function modelOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>, route = 'acme-gateway'): Model<Api> {
const [model] = resolveProfiles(providers).get(route)?.piProvider.getModels() ?? []
if (model === undefined) throw new Error(`route "${route}" resolved no models`)
return model
}
it('declares selectable levels with their wire spellings on a hand-declared model', () => {
const model = modelOf(declared([{
id: 'acme-think',
reasoningEfforts: { off: null, low: 'low', high: 'high', max: 'ultra' },
}]))
expect(model.reasoning).toBe(true)
// Undeclared levels are pinned null rather than left to pi-ai's own
// defaulting, which is asymmetric: an absent key means "supported" for the
// five base levels but "unsupported" for xhigh/max. A profile author
// should not need to know that. Declared `off` with no value stays absent
// from the map — supported, send nothing.
expect(model.thinkingLevelMap).toEqual({
minimal: null,
medium: null,
xhigh: null,
low: 'low',
high: 'high',
max: 'ultra',
})
expect(getSupportedThinkingLevels(model)).toEqual(['off', 'low', 'high', 'max'])
})
it('keeps a declared off value in the map for dispatch to send', () => {
const model = modelOf(declared([{ id: 'm', reasoningEfforts: { off: 'none', high: 'high' } }]))
expect(model.thinkingLevelMap?.off).toBe('none')
expect(getSupportedThinkingLevels(model)).toEqual(['off', 'high'])
})
it('offers exactly the declared keys: leaving off out makes thinking mandatory', () => {
const model = modelOf(declared([{ id: 'm', reasoningEfforts: { high: 'high' } }]))
expect(getSupportedThinkingLevels(model)).toEqual(['high'])
})
it('narrows a catalog model’s levels in place', () => {
const [catalogModel] = getBuiltinModels('deepseek')
if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model')
expect(getSupportedThinkingLevels(catalogModel as Model<Api>)).toEqual(['off', 'high', 'max'])
const model = modelOf({
deepseek: { models: [{ id: catalogModel.id, reasoningEfforts: { off: null, high: 'high' } }] },
}, 'deepseek')
expect(getSupportedThinkingLevels(model)).toEqual(['off', 'high'])
// Only the reasoning fields change; identity and capacities stay catalog.
expect(model.name).toBe(catalogModel.name)
expect(model.contextWindow).toBe(catalogModel.contextWindow)
})
it('strips reasoning from a catalog model with false', () => {
const [catalogModel] = getBuiltinModels('deepseek')
if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model')
expect(catalogModel.reasoning).toBe(true)
const model = modelOf({ deepseek: { models: [{ id: catalogModel.id, reasoningEfforts: false }] } }, 'deepseek')
expect(model.reasoning).toBe(false)
expect(getSupportedThinkingLevels(model)).toEqual(['off'])
})
it('inherits the catalog capability when the field is absent', () => {
const [catalogModel] = getBuiltinModels('deepseek')
if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model')
const model = modelOf({ deepseek: { models: [{ id: catalogModel.id }] } }, 'deepseek')
expect(model.reasoning).toBe(catalogModel.reasoning)
expect(model.thinkingLevelMap).toEqual(catalogModel.thinkingLevelMap)
})
it('rejects a declaration that offers nothing or spells a level it cannot send', () => {
const declare = (efforts: NonNullable<LlmPiAi.PiAiModelProfile['reasoningEfforts']>): (() => unknown) =>
() => resolveProfiles(declared([{ id: 'm', reasoningEfforts: efforts }]))
expect(declare({})).toThrow(/empty reasoningEfforts/)
// A YAML `reasoningEfforts:` left valueless arrives as null through the
// schema union; it declares nothing and is not a spelling of "inherit".
expect(declare(null as never)).toThrow(/empty reasoningEfforts/)
expect(declare({ off: null })).toThrow(/offers no level beyond "off"/)
expect(declare({ off: 'none' })).toThrow(/offers no level beyond "off"/)
expect(declare({ high: null })).toThrow(/only "off" may leave it empty/)
expect(declare({ high: '' })).toThrow(/must not be an empty string/)
})
})
describe('modelOverrides', () => {
const deepseekModel = (): Model<Api> => {
const [model] = getBuiltinModels('deepseek')
if (model === undefined) throw new Error('the installed catalog ships no deepseek model')
return model
}
it('reshapes one catalog model while the rest of the catalog keeps serving', () => {
const catalogSize = getBuiltinModels('deepseek').length
const target = deepseekModel()
const resolved = resolveProfiles({
deepseek: {
modelOverrides: {
[target.id]: {
name: 'DeepSeek (proxied)',
maxTokens: 4096,
reasoningEfforts: { off: null, high: 'high' },
},
},
},
})
const models = resolved.get('deepseek')?.piProvider.getModels() ?? []
const reshaped = models.find(model => model.id === target.id)
if (reshaped === undefined) throw new Error('the overridden model vanished from the route')
// The whole catalog still serves — that is the difference from `models`,
// which replaces it.
expect(models).toHaveLength(catalogSize)
expect(reshaped.name).toBe('DeepSeek (proxied)')
expect(getSupportedThinkingLevels(reshaped)).toEqual(['off', 'high'])
// An override's cap is explicit configuration, so it becomes the request
// default exactly as a models entry's would.
expect(resolved.get('deepseek')?.configuredMaxTokens.get(target.id)).toBe(4096)
// A sibling the overrides do not name is byte-identical to the catalog.
const sibling = models.find(model => model.id !== target.id)
expect(sibling?.maxTokens).toBe(getBuiltinModels('deepseek').find(model => model.id === sibling?.id)?.maxTokens)
})
it('refuses every override that lands nowhere instead of skipping it', () => {
expect(() => resolveProfiles({
deepseek: { modelOverrides: { 'no-such-model': { name: 'ghost' } } },
})).toThrow(/which the installed catalog does not describe/)
expect(() => resolveProfiles({
'acme-gateway': {
api: 'openai-completions',
baseURL: 'https://acme.test',
models: [{ id: 'm' }],
modelOverrides: { m: { name: 'renamed' } },
},
})).toThrow(/a declared route spells every model out/)
const declaredOnly = deepseekModel()
expect(() => resolveProfiles({
deepseek: {
models: [{ id: declaredOnly.id }],
modelOverrides: { [declaredOnly.id]: { name: 'renamed' } },
},
})).toThrow(/models already replaces the served catalog/)
expect(() => resolveProfiles({
deepseek: { modelOverrides: { '': { name: 'nameless' } } },
})).toThrow(/empty model id/)
// The dict key is the id; a value smuggling its own would quietly rename
// the model it meant to customize. The schema passes unknown keys
// through, so resolution is the boundary that refuses it — the variable
// indirection mirrors that boundary by sidestepping the literal check.
const smuggled = { name: 'x', id: 'other' }
expect(() => resolveProfiles({
deepseek: { modelOverrides: { [deepseekModel().id]: smuggled } },
})).toThrow(/sets "id", which is the dict key/)
})
})
describe('reasoning-dispatch compat switches', () => {
/** The materialized models of one route, keyed by id. */
function modelsOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>, route: string): Map<string, Model<Api>> {
const models = resolveProfiles(providers).get(route)?.piProvider.getModels() ?? []
return new Map(models.map(model => [model.id, model]))
}
it('applies route switches to every openai-completions model, entries winning per field', () => {
const models = modelsOf({
'acme-gateway': {
api: 'openai-completions',
baseURL: 'https://acme.test',
compat: { thinkingFormat: 'deepseek' },
models: [
{ id: 'dialect-default', reasoningEfforts: { off: null, high: 'high' } },
{ id: 'dialect-odd', compat: { thinkingFormat: 'openai', supportsReasoningEffort: false } },
],
},
}, 'acme-gateway')
expect(models.get('dialect-default')?.compat).toEqual({ thinkingFormat: 'deepseek' })
expect(models.get('dialect-odd')?.compat).toEqual({ thinkingFormat: 'openai', supportsReasoningEffort: false })
})
it('merges the switches over the catalog entry’s own compat instead of replacing it', () => {
const [catalogModel] = getBuiltinModels('deepseek')
if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model')
const inherited = catalogModel.compat as OpenAICompletionsCompat
expect(inherited.requiresReasoningContentOnAssistantMessages).toBe(true)
const models = modelsOf({
deepseek: { models: [{ id: catalogModel.id, compat: { thinkingFormat: 'openai' } }] },
}, 'deepseek')
// The one switched field changes; the catalog's other quirks survive,
// because configuration has no way to restate them.
expect(models.get(catalogModel.id)?.compat).toEqual({ ...inherited, thinkingFormat: 'openai' })
})
it('skips models of other protocols on a mixed route instead of failing them', () => {
// xai ships both completions and responses models, so a route-level switch
// must land on the former without invalidating the latter.
const catalog = getBuiltinModels('xai') as readonly Model<Api>[]
const completions = catalog.find(model => model.api === 'openai-completions')
const responses = catalog.find(model => model.api === 'openai-responses')
if (completions === undefined || responses === undefined) throw new Error('xai no longer ships a mixed catalog')
const models = modelsOf({
xai: {
compat: { supportsReasoningEffort: false },
models: [{ id: completions.id }, { id: responses.id }],
},
}, 'xai')
expect((models.get(completions.id)?.compat as OpenAICompletionsCompat).supportsReasoningEffort).toBe(false)
expect(models.get(responses.id)?.compat).toEqual(responses.compat)
})
it('rejects a model-level switch on a protocol that has no such field', () => {
expect(() => resolveProfiles({
anthropic: {
models: [{ id: 'claude-sonnet-4-5', compat: { thinkingFormat: 'openai' } }],
},
})).toThrow(/exist only on openai-completions/)
})
it('rejects route switches no model on the route can take', () => {
expect(() => resolveProfiles({
anthropic: { compat: { thinkingFormat: 'openai' } },
})).toThrow(/no model on the route speaks openai-completions/)
})
})
describe('resolution snapshots', () => {
it('finishes an in-flight request under the configuration it started with', async () => {
const server = await mockServer([{ events: textEvents }])

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { Config } from '../src/config.ts'
describe('reasoning schema boundary', () => {
const configWith = (model: Record<string, unknown>): (() => unknown) =>
() => Config({
providers: {
'acme-gateway': {
api: 'openai-completions',
baseURL: 'https://acme.test',
models: [{ id: 'm', ...model }],
},
},
})
it('rejects a level pi-ai does not know at the write that produced it', () => {
expect(configWith({ reasoningEfforts: { ultra: 'x' } })).toThrow(/"off"/)
expect(configWith({ reasoningEfforts: { high: 42 } })).toThrow()
})
it('keeps false distinguishable from an absent declaration', () => {
type Materialized = { providers: Record<string, { models?: { reasoningEfforts?: unknown }[] }> }
const withFalse = configWith({ reasoningEfforts: false })() as Materialized
expect(withFalse.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBe(false)
const absent = configWith({})() as Materialized
expect(absent.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBeUndefined()
})
it('rejects a thinking format outside the offered set', () => {
expect(configWith({ compat: { thinkingFormat: 'quantum' } })).toThrow(/expected/)
})
})