feat: use managed model capabilities for reasoning and image input

This commit is contained in:
2026-09-12 21:37:18 +08:00
parent e7701d1f2c
commit 26cbb29aa9
29 changed files with 652 additions and 271 deletions

View File

@@ -28,6 +28,7 @@ import type {
PiRpcResponse,
} from '../../electron/coding-runtime/pi/rpc-client';
import { PI_084_TEXT_TURN } from '../fixtures/pi-0.84.2-projector-fixtures';
import { unknownManagedModelCapability, type ManagedModelRequest } from '../../shared/managed-model-capabilities';
const roots: string[] = [];
const NOW = '2026-08-22T15:00:00.000Z';
@@ -152,9 +153,11 @@ class RuntimeFakeWorker implements PiConversationWorker {
}
class TrackingExtensionHost extends PiManagedExtensionHost {
managedRequest?: ManagedModelRequest;
readonly runs = new Map<string, { generation: number; runId: string }>();
override async bindRun(conversationId: string, generation: number, runId: string): Promise<void> {
override async bindRun(conversationId: string, generation: number, runId: string, request?: ManagedModelRequest): Promise<void> {
this.managedRequest = request;
this.runs.set(conversationId, { generation, runId });
}
@@ -168,6 +171,57 @@ afterEach(async () => {
});
describe('Pi Conversation runtime', () => {
it('persists native managed choices and rejects removed effort or unsupported images before prompt', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-managed-runtime-'));
roots.push(projectPath);
const projectStore = createCodingProjectStore(createMemoryCodingProjectStorage(), {
createId: () => 'project-managed', now: () => NOW,
});
await createLocalCodingProject({ projectPath, now: NOW }, projectStore);
const model = { accountId: 'niancode-user-models', modelId: 'unfamiliar-model', thinkingLevel: 'high' as const };
await createCodingProjectAgent(projectPath, {
id: 'agent-a', avatarId: 'avatar-01', roleName: 'Implementer', name: 'Agent A',
model, modelResolution: 'resolved',
responsibility: { mission: 'Implement', owns: [], boundaries: [], collaborators: [], principles: [] },
}, { now: NOW });
const store = createCodingConversationStore(projectPath);
const conversation = await store.create({ agentId: 'agent-a', title: 'Managed', model, modelResolution: 'resolved' });
let worker: RuntimeFakeWorker;
const pool = new PiWorkerPool({ openWorker: async ({generation}) => {
worker = new RuntimeFakeWorker('managed-worker', generation);
return { worker, session: { piSessionId: 'session', sessionKey: 'key' } };
} });
const capability = unknownManagedModelCapability();
capability.inputModalities = ['text'];
capability.reasoning = { supported: true, canDisable: true, defaultEnabled: true,
controlFormat: 'qwen', effortValues: ['xhigh', 'future-native'], defaultEffort: 'xhigh', budget: null };
const host = new TrackingExtensionHost();
const runtime = new PiConversationRuntime({
pool, registry: new PiSessionRegistry({ projectStore }), extensionHost: host,
resolveImages: async () => [],
resolveModel: async candidate => ({ ...candidate, input: ['text'],
runtimeProviderId: 'managed', managedCapability: structuredClone(capability) }),
});
try {
await runtime.prepare({ conversationId: conversation.id, projectId: 'project-managed', agentId: 'agent-a',
title: conversation.title, model: { model, modelResolution: 'resolved' } });
expect((await runtime.getSnapshot(conversation.id)).conversation.model.model?.reasoningChoice).toEqual({ mode: 'default' });
await runtime.setThinking({ conversationId: conversation.id, thinkingLevel: 'off',
reasoningChoice: { mode: 'enabled', effort: 'future-native' } });
expect((await store.get(conversation.id))?.model?.reasoningChoice).toEqual({ mode: 'enabled', effort: 'future-native' });
capability.reasoning.effortValues = ['xhigh'];
const prompt = { conversationId: conversation.id, clientRequestId: 'request-managed', mode: 'prompt' as const, text: 'Hello', attachments: [] };
await expect(runtime.prompt(prompt)).rejects.toThrow('思考选项已不可用');
expect(worker!.requests.filter(r => r.type === 'prompt')).toHaveLength(0);
await runtime.setThinking({ conversationId: conversation.id, thinkingLevel: 'off', reasoningChoice: { mode: 'default' } });
await expect(runtime.prompt({ ...prompt, attachments: [{ attachmentId: 'image' }] })).rejects.toThrow('图片输入');
await runtime.prompt(prompt);
expect(host.managedRequest).toEqual({ modelId: 'unfamiliar-model', choice: { mode: 'default' }, reasoningFields: {} });
for (const event of PI_084_TEXT_TURN.events) worker!.emit(structuredClone(event));
await runtime.getSnapshot(conversation.id);
} finally { await runtime.shutdown(); }
});
it('separates RPC acceptance from settle and changes only the target Conversation model', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-pi-runtime-'));
roots.push(projectPath);