feat: eager vLLM warmup and smart page titles in paginate stage
- Add warmup() to LlmProvider interface for eager subprocess startup - ManagedVllmProvider.warmup() starts vLLM in background on project load - ProviderRegistry.warmupAll() triggers all managed providers - NamedProvider proxies warmup() to inner provider - paginate stage generates LLM-powered descriptive page titles when available, cached by content hash, falls back to generic "Page N" - project-mcp-endpoint calls warmupAll() on router creation so vLLM is loading while the session initializes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
114
src/mcplocal/tests/proxymodel-loader.test.ts
Normal file
114
src/mcplocal/tests/proxymodel-loader.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { loadProxyModels, getProxyModel } from '../src/proxymodel/loader.js';
|
||||
|
||||
describe('loadProxyModels', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'proxymodel-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads built-in models when directory is empty', async () => {
|
||||
const models = await loadProxyModels(tempDir);
|
||||
expect(models.has('default')).toBe(true);
|
||||
expect(models.has('subindex')).toBe(true);
|
||||
expect(models.get('default')!.source).toBe('built-in');
|
||||
});
|
||||
|
||||
it('loads built-in models when directory does not exist', async () => {
|
||||
const models = await loadProxyModels(join(tempDir, 'nonexistent'));
|
||||
expect(models.has('default')).toBe(true);
|
||||
expect(models.has('subindex')).toBe(true);
|
||||
});
|
||||
|
||||
it('loads local YAML files', async () => {
|
||||
const yaml = `kind: ProxyModel
|
||||
metadata:
|
||||
name: custom
|
||||
spec:
|
||||
stages:
|
||||
- type: passthrough
|
||||
`;
|
||||
await writeFile(join(tempDir, 'custom.yaml'), yaml);
|
||||
const models = await loadProxyModels(tempDir);
|
||||
expect(models.has('custom')).toBe(true);
|
||||
expect(models.get('custom')!.source).toBe('local');
|
||||
expect(models.get('custom')!.spec.stages[0]!.type).toBe('passthrough');
|
||||
});
|
||||
|
||||
it('loads .yml files too', async () => {
|
||||
const yaml = `metadata:
|
||||
name: alt
|
||||
spec:
|
||||
stages:
|
||||
- type: paginate
|
||||
config:
|
||||
pageSize: 4000
|
||||
`;
|
||||
await writeFile(join(tempDir, 'alt.yml'), yaml);
|
||||
const models = await loadProxyModels(tempDir);
|
||||
expect(models.has('alt')).toBe(true);
|
||||
expect(models.get('alt')!.spec.stages[0]!.config).toEqual({ pageSize: 4000 });
|
||||
});
|
||||
|
||||
it('local models override built-ins with same name', async () => {
|
||||
const yaml = `kind: ProxyModel
|
||||
metadata:
|
||||
name: default
|
||||
spec:
|
||||
controller: none
|
||||
stages:
|
||||
- type: passthrough
|
||||
cacheable: false
|
||||
`;
|
||||
await writeFile(join(tempDir, 'default.yaml'), yaml);
|
||||
const models = await loadProxyModels(tempDir);
|
||||
expect(models.get('default')!.source).toBe('local');
|
||||
expect(models.get('default')!.spec.controller).toBe('none');
|
||||
});
|
||||
|
||||
it('skips invalid YAML files without breaking', async () => {
|
||||
await writeFile(join(tempDir, 'valid.yaml'), `metadata:\n name: good\nspec:\n stages:\n - type: passthrough\n`);
|
||||
await writeFile(join(tempDir, 'invalid.yaml'), `metadata:\n name: \nspec:\n stages: []\n`);
|
||||
const models = await loadProxyModels(tempDir);
|
||||
expect(models.has('good')).toBe(true);
|
||||
expect(models.has('')).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores non-yaml files', async () => {
|
||||
await writeFile(join(tempDir, 'readme.md'), '# Readme');
|
||||
await writeFile(join(tempDir, 'notes.txt'), 'Notes');
|
||||
const models = await loadProxyModels(tempDir);
|
||||
// Only built-ins
|
||||
expect(models.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProxyModel', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'proxymodel-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns requested model by name', async () => {
|
||||
const model = await getProxyModel('subindex', tempDir);
|
||||
expect(model.metadata.name).toBe('subindex');
|
||||
});
|
||||
|
||||
it('falls back to default for unknown model', async () => {
|
||||
const model = await getProxyModel('nonexistent', tempDir);
|
||||
expect(model.metadata.name).toBe('default');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user