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:
Michal
2026-03-03 19:07:39 +00:00
parent 0427d7dc1a
commit 03827f11e4
147 changed files with 17561 additions and 2093 deletions

View File

@@ -4,6 +4,9 @@ import type { UpstreamConnection, JsonRpcRequest, JsonRpcResponse, JsonRpcNotifi
import type { McpdClient } from '../src/http/mcpd-client.js';
import { ProviderRegistry } from '../src/providers/registry.js';
import type { LlmProvider, CompletionResult } from '../src/providers/types.js';
import { createGatePlugin } from '../src/proxymodel/plugins/gate.js';
import { LLMProviderAdapter } from '../src/proxymodel/llm-adapter.js';
import { MemoryCache } from '../src/proxymodel/cache.js';
function mockUpstream(
name: string,
@@ -99,11 +102,20 @@ function setupGatedRouter(
providerRegistry.assignTier(mockProvider.name, 'heavy');
}
router.setGateConfig({
// Wire gate plugin via setPlugin
const gatePlugin = createGatePlugin({
gated: opts.gated !== false,
providerRegistry,
byteBudget: opts.byteBudget,
});
router.setPlugin(gatePlugin);
// Wire proxymodel services (needed for plugin context)
const llmAdapter = providerRegistry ? new LLMProviderAdapter(providerRegistry) : {
complete: async () => '',
available: () => false,
};
router.setProxyModel('default', llmAdapter, new MemoryCache());
return { router, mcpdClient };
}
@@ -146,6 +158,7 @@ describe('McpRouter gating', () => {
const names = tools.map((t) => t.name);
expect(names).toContain('ha/get_entities');
expect(names).toContain('read_prompts');
expect(names).toContain('propose_prompt');
expect(names).not.toContain('begin_session');
});
});
@@ -475,7 +488,7 @@ describe('McpRouter gating', () => {
});
describe('session cleanup', () => {
it('cleanupSession removes gate state', async () => {
it('cleanupSession removes gate state, re-creates on next access', async () => {
const { router } = setupGatedRouter();
await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
@@ -486,16 +499,17 @@ describe('McpRouter gating', () => {
);
expect((toolsRes.result as { tools: Array<{ name: string }> }).tools[0]!.name).toBe('begin_session');
// Cleanup
// Cleanup removes the context
router.cleanupSession('s1');
// After cleanup, session is treated as unknown (ungated)
// After cleanup, getOrCreatePluginContext creates a fresh context and
// calls onSessionCreate again → session is re-gated (gated=true config).
toolsRes = await router.route(
{ jsonrpc: '2.0', id: 3, method: 'tools/list' },
{ sessionId: 's1' },
);
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
expect(tools.map((t) => t.name)).not.toContain('begin_session');
expect(tools[0]!.name).toBe('begin_session');
});
});
@@ -710,8 +724,8 @@ describe('McpRouter gating', () => {
);
expect((toolsRes.result as { tools: Array<{ name: string }> }).tools[0]!.name).toBe('begin_session');
// Project config changes: gated → ungated
router.setGateConfig({ gated: false, providerRegistry: null });
// Project config changes: gated → ungated (new plugin replaces old)
router.setPlugin(createGatePlugin({ gated: false }));
// New session should be ungated
await router.route({ jsonrpc: '2.0', id: 3, method: 'initialize' }, { sessionId: 's2' });
@@ -738,7 +752,7 @@ describe('McpRouter gating', () => {
expect(names).toContain('ha/get_entities');
// Project config changes: ungated → gated
router.setGateConfig({ gated: true, providerRegistry: null });
router.setPlugin(createGatePlugin({ gated: true }));
// New session should be gated
await router.route({ jsonrpc: '2.0', id: 3, method: 'initialize' }, { sessionId: 's2' });
@@ -751,22 +765,26 @@ describe('McpRouter gating', () => {
expect(names[0]).toBe('begin_session');
});
it('existing sessions retain gate state after config change', async () => {
it('existing gated sessions become ungated when plugin changes to ungated', async () => {
const { router } = setupGatedRouter({ gated: true });
router.addUpstream(mockUpstream('ha', { tools: [{ name: 'get_entities' }] }));
// Session created while gated
await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
// Config changes to ungated
router.setGateConfig({ gated: false, providerRegistry: null });
// Config changes to ungated — new plugin replaces the old one
router.setPlugin(createGatePlugin({ gated: false }));
// Existing session s1 should STILL be gated (session state is immutable after creation)
// With plugin architecture, the new plugin's gate doesn't know about s1,
// so it treats it as ungated. This is correct behavior: when admin changes
// a project from gated to ungated, existing sessions should also become ungated.
const toolsRes = await router.route(
{ jsonrpc: '2.0', id: 2, method: 'tools/list' },
{ sessionId: 's1' },
);
expect((toolsRes.result as { tools: Array<{ name: string }> }).tools[0]!.name).toBe('begin_session');
const names = (toolsRes.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name);
expect(names).toContain('ha/get_entities');
expect(names).not.toContain('begin_session');
});
it('already-ungated sessions remain ungated after config changes to gated', async () => {
@@ -777,7 +795,7 @@ describe('McpRouter gating', () => {
await router.route({ jsonrpc: '2.0', id: 1, method: 'initialize' }, { sessionId: 's1' });
// Config changes to gated
router.setGateConfig({ gated: true, providerRegistry: null });
router.setPlugin(createGatePlugin({ gated: true }));
// Existing session s1 should remain ungated
const toolsRes = await router.route(
@@ -801,7 +819,7 @@ describe('McpRouter gating', () => {
);
// Config refreshes (still gated)
router.setGateConfig({ gated: true, providerRegistry: null });
router.setPlugin(createGatePlugin({ gated: true }));
// Session should remain ungated — begin_session already completed
const toolsRes = await router.route(