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:
@@ -161,9 +161,11 @@ describe('config setup wizard', () => {
|
||||
|
||||
describe('provider: anthropic', () => {
|
||||
it('prompts for API key and saves to secret store', async () => {
|
||||
// Answers: select provider, enter API key, select model
|
||||
// Flow: simple → anthropic → (no existing key) → whichBinary('claude') returns null →
|
||||
// log tip → password prompt → select model
|
||||
const deps = buildDeps({
|
||||
answers: ['simple', 'anthropic', 'sk-ant-new-key', 'claude-haiku-3-5-20241022'],
|
||||
whichBinary: vi.fn(async () => null),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
@@ -194,15 +196,84 @@ describe('config setup wizard', () => {
|
||||
|
||||
it('allows replacing existing key', async () => {
|
||||
// Answers: select provider, confirm change=true, enter new key, select model
|
||||
// Change=true → promptForAnthropicKey → whichBinary returns null → password prompt
|
||||
const deps = buildDeps({
|
||||
secrets: { 'anthropic-api-key': 'sk-ant-old' },
|
||||
answers: ['simple', 'anthropic', true, 'sk-ant-new', 'claude-haiku-3-5-20241022'],
|
||||
whichBinary: vi.fn(async () => null),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-new');
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('detects claude binary and prompts for OAuth token', async () => {
|
||||
// Flow: simple → anthropic → (no existing key) → whichBinary finds claude →
|
||||
// confirm OAuth=true → password prompt → select model
|
||||
const deps = buildDeps({
|
||||
answers: ['simple', 'anthropic', true, 'sk-ant-oat01-test-token', 'claude-haiku-3-5-20241022'],
|
||||
whichBinary: vi.fn(async () => '/usr/bin/claude'),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-oat01-test-token');
|
||||
expect(logs.some((l) => l.includes('Found Claude CLI at'))).toBe(true);
|
||||
expect(logs.some((l) => l.includes('claude setup-token'))).toBe(true);
|
||||
const config = readConfig();
|
||||
const llm = config.llm as Record<string, unknown>;
|
||||
expect(llm.provider).toBe('anthropic');
|
||||
expect(llm.model).toBe('claude-haiku-3-5-20241022');
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('falls back to API key when claude binary not found', async () => {
|
||||
// Flow: simple → anthropic → (no existing key) → whichBinary returns null →
|
||||
// password prompt (API key) → select model
|
||||
const deps = buildDeps({
|
||||
answers: ['simple', 'anthropic', 'sk-ant-api03-test', 'claude-sonnet-4-20250514'],
|
||||
whichBinary: vi.fn(async () => null),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-api03-test');
|
||||
expect(logs.some((l) => l.includes('Tip: Install Claude CLI'))).toBe(true);
|
||||
const config = readConfig();
|
||||
const llm = config.llm as Record<string, unknown>;
|
||||
expect(llm.model).toBe('claude-sonnet-4-20250514');
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('shows OAuth label when existing token is OAuth', async () => {
|
||||
// Flow: simple → anthropic → existing OAuth key → confirm change=false → select model
|
||||
const deps = buildDeps({
|
||||
secrets: { 'anthropic-api-key': 'sk-ant-oat01-existing-token' },
|
||||
answers: ['simple', 'anthropic', false, 'claude-haiku-3-5-20241022'],
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
// Should NOT have called set (kept existing key)
|
||||
expect(deps.secretStore.set).not.toHaveBeenCalled();
|
||||
// Confirm prompt should have received an OAuth label
|
||||
expect(deps.prompt.confirm).toHaveBeenCalledWith(
|
||||
expect.stringContaining('OAuth token stored'),
|
||||
false,
|
||||
);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('declines OAuth and enters API key instead', async () => {
|
||||
// Flow: simple → anthropic → (no existing key) → whichBinary finds claude →
|
||||
// confirm OAuth=false → password prompt (API key) → select model
|
||||
const deps = buildDeps({
|
||||
answers: ['simple', 'anthropic', false, 'sk-ant-api03-manual', 'claude-sonnet-4-20250514'],
|
||||
whichBinary: vi.fn(async () => '/usr/bin/claude'),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-api03-manual');
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider: vllm', () => {
|
||||
@@ -273,6 +344,44 @@ describe('config setup wizard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanced mode: duplicate names', () => {
|
||||
it('generates unique default name when same provider added to both tiers', async () => {
|
||||
// Flow: advanced →
|
||||
// add fast? yes → anthropic → name "anthropic" (default) → whichBinary null → key → model → add more? no →
|
||||
// add heavy? yes → anthropic → name "anthropic-2" (deduped default) → existing key, keep → model → add more? no
|
||||
const deps = buildDeps({
|
||||
answers: [
|
||||
'advanced',
|
||||
// fast tier
|
||||
true, // add fast?
|
||||
'anthropic', // fast provider type
|
||||
'anthropic', // provider name (default)
|
||||
'sk-ant-oat01-token', // API key (whichBinary returns null → password prompt)
|
||||
'claude-haiku-3-5-20241022', // model
|
||||
false, // add another fast?
|
||||
// heavy tier
|
||||
true, // add heavy?
|
||||
'anthropic', // heavy provider type
|
||||
'anthropic-2', // provider name (deduped default)
|
||||
false, // keep existing key
|
||||
'claude-opus-4-20250514', // model
|
||||
false, // add another heavy?
|
||||
],
|
||||
whichBinary: vi.fn(async () => null),
|
||||
});
|
||||
await runSetup(deps);
|
||||
|
||||
const config = readConfig();
|
||||
const llm = config.llm as { providers: Array<{ name: string; type: string; model: string; tier: string }> };
|
||||
expect(llm.providers).toHaveLength(2);
|
||||
expect(llm.providers[0].name).toBe('anthropic');
|
||||
expect(llm.providers[0].tier).toBe('fast');
|
||||
expect(llm.providers[1].name).toBe('anthropic-2');
|
||||
expect(llm.providers[1].tier).toBe('heavy');
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe('output messages', () => {
|
||||
it('shows restart instruction', async () => {
|
||||
const deps = buildDeps({ answers: ['simple', 'gemini-cli', 'gemini-2.5-flash'] });
|
||||
|
||||
Reference in New Issue
Block a user