Files
makelore/tests/unit/managed-model-capabilities.test.ts

113 lines
6.1 KiB
TypeScript

// @vitest-environment node
import { createServer } from 'node:http';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { describe, expect, it, vi } from 'vitest';
import { streamSimple } from '@earendil-works/pi-ai/api/openai-completions';
import type { Model } from '@earendil-works/pi-ai';
import {
buildManagedModelRequest, managedPiThinkingLevel, managedReasoningOptions,
unknownManagedModelCapability,
type ManagedModelCapability, type ManagedReasoningChoice,
} from '../../shared/managed-model-capabilities';
import { materializeMakelorePiExtension } from '../../electron/coding-runtime/pi/extensions/makelore-runtime';
function capability(format: 'qwen' | 'deepseek'): ManagedModelCapability {
return {
...unknownManagedModelCapability(),
inputModalities: ['text', 'image'], outputModalities: ['text'], resolutionStatus: 'ready',
reasoning: {
supported: true, canDisable: true, defaultEnabled: true,
effortValues: ['low', 'medium', 'xhigh', 'max', 'future-native'],
defaultEffort: 'low', controlFormat: format, budget: null,
},
};
}
describe('managed model reasoning', () => {
it('distinguishes unknown, switch-only and always-on capabilities', () => {
expect(managedReasoningOptions(unknownManagedModelCapability()).map((x) => x.value)).toEqual(['default']);
const profile = capability('qwen');
profile.reasoning.effortValues = [];
expect(managedReasoningOptions(profile).map((x) => x.value)).toEqual(['default', 'disabled', 'enabled']);
profile.reasoning.canDisable = false;
profile.reasoning.effortValues = ['max'];
expect(managedReasoningOptions(profile).map((x) => x.value)).toEqual(['default', 'effort:max']);
expect(buildManagedModelRequest('model', { mode: 'enabled', effort: 'max' }, profile).reasoningFields)
.toEqual({ reasoning_effort: 'max' });
expect(() => buildManagedModelRequest('model', { mode: 'disabled' }, profile)).toThrow('不可用');
expect(() => buildManagedModelRequest('model', { mode: 'enabled', effort: 'high' }, profile)).toThrow('不可用');
});
it.each(['qwen', 'deepseek'] as const)('sends exact %s controls through Pi and the bundled extension', async (format) => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-managed-wire-'));
const bodies: Record<string, unknown>[] = [];
const server = createServer(async (request, response) => {
let raw = '';
for await (const chunk of request) raw += String(chunk);
bodies.push(JSON.parse(raw));
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end('data: ' + JSON.stringify({
id: 'test-response', object: 'chat.completion.chunk', created: 0, model: 'catalog-model',
choices: [{ index: 0, delta: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}) + '\n\ndata: [DONE]\n\n');
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
try {
const address = server.address();
if (!address || typeof address === 'string') throw new Error('No test server address');
const contextFile = path.join(root, 'worker.json');
const profile = capability(format);
await writeFile(contextFile, JSON.stringify({ runId: 'test-run', tools: [], allowedToolNames: [] }));
const extensionPath = await materializeMakelorePiExtension(root);
const extension = await import(/* @vite-ignore */ pathToFileURL(extensionPath).href);
const hooks = new Map<string, (event: { payload: unknown }) => Promise<unknown>>();
await extension.createMakeloreRuntime({ contextFile })({
registerFlag: vi.fn(), registerTool: vi.fn(),
on: (name: string, handler: (event: { payload: unknown }) => Promise<unknown>) => hooks.set(name, handler),
});
const beforeRequest = hooks.get('before_provider_request');
expect(beforeRequest).toBeTypeOf('function');
const model: Model<'openai-completions'> = {
id: 'catalog-model', name: 'Catalog model', api: 'openai-completions',
provider: 'managed-test', baseUrl: 'http://127.0.0.1:' + address.port + '/v1',
reasoning: true, input: ['text', 'image'], contextWindow: 4096, maxTokens: 512,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
thinkingLevelMap: { off: 'off', low: 'low', medium: 'medium', xhigh: 'xhigh', max: 'max' },
compat: { thinkingFormat: format, supportsReasoningEffort: true },
};
const choices: ManagedReasoningChoice[] = [
{ mode: 'default' }, { mode: 'disabled' }, { mode: 'enabled' },
...['low', 'medium', 'xhigh', 'max', 'future-native'].map((effort) => ({ mode: 'enabled' as const, effort })),
];
for (const choice of choices) {
const prepared = buildManagedModelRequest(model.id, choice, profile);
await writeFile(contextFile, JSON.stringify({
runId: 'test-run', tools: [], allowedToolNames: [], managedModelRequest: prepared,
}));
const result = await streamSimple(model, {
messages: [{ role: 'user', content: 'test', timestamp: 0 }],
}, {
apiKey: 'synthetic-key', reasoning: managedPiThinkingLevel(choice),
onPayload: (payload) => beforeRequest!({ payload }),
}).result();
expect(result.stopReason, result.errorMessage).not.toBe('error');
const body = bodies.at(-1)!;
for (const field of ['thinking', 'enable_thinking', 'reasoning_effort', 'thinking_budget']) {
expect(body[field], field + ' for ' + JSON.stringify(choice)).toEqual(prepared.reasoningFields[field]);
}
expect(body.model).toBe(model.id);
expect(body.messages).toEqual([{ role: 'user', content: 'test' }]);
}
expect(bodies).toHaveLength(choices.length);
} finally {
server.closeAllConnections();
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
await rm(root, { recursive: true, force: true });
}
});
});