feat(setup): add model provider setup flows
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createInterface } from 'readline/promises';
|
||||
import { EventEmitter } from 'events';
|
||||
import { createPrompter } from './prompts.js';
|
||||
import { ConfigBuilder } from './config.js';
|
||||
import { setupProviders } from './providers.js';
|
||||
|
||||
function mockReadline(inputs: string[]) {
|
||||
let questionIdx = 0;
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
return {
|
||||
async question(query: string) {
|
||||
const answer = inputs[questionIdx++];
|
||||
return answer ?? '';
|
||||
},
|
||||
|
||||
close() {
|
||||
// no-op
|
||||
},
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
|
||||
async next() {
|
||||
return { done: true };
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('setupProviders', () => {
|
||||
it('configures anthropic as default provider', async () => {
|
||||
const rl = mockReadline(['1', 'sk-ant-test123', '', 'n']);
|
||||
const p = createPrompter(rl);
|
||||
const builder = new ConfigBuilder();
|
||||
await setupProviders(p, builder);
|
||||
const config = builder.build();
|
||||
expect(config.models.default.provider).toBe('anthropic');
|
||||
expect(config.models.default.api_key).toBe('sk-ant-test123');
|
||||
expect(config.models.default.model).toBe('claude-sonnet-4-20250514');
|
||||
});
|
||||
|
||||
it('configures ollama as default provider', async () => {
|
||||
const rl = mockReadline(['3', '', '', 'n']);
|
||||
const p = createPrompter(rl);
|
||||
const builder = new ConfigBuilder();
|
||||
await setupProviders(p, builder);
|
||||
const config = builder.build();
|
||||
expect(config.models.default.provider).toBe('ollama');
|
||||
expect(config.models.default.endpoint).toBe('http://localhost:11434');
|
||||
});
|
||||
|
||||
it('configures anthropic with fast tier', async () => {
|
||||
const rl = mockReadline(['1', 'sk-ant-test123', '', 'y', '']);
|
||||
const p = createPrompter(rl);
|
||||
const builder = new ConfigBuilder();
|
||||
await setupProviders(p, builder);
|
||||
const config = builder.build();
|
||||
expect(config.models.default.provider).toBe('anthropic');
|
||||
expect(config.models.fast).toBeDefined();
|
||||
expect(config.models.fast.provider).toBe('anthropic');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Prompter } from './prompts.js';
|
||||
import type { ConfigBuilder } from './config.js';
|
||||
|
||||
interface ProviderDef {
|
||||
name: string;
|
||||
provider: string;
|
||||
defaultModel: string;
|
||||
fastModel?: string;
|
||||
needsApiKey: boolean;
|
||||
needsEndpoint: boolean;
|
||||
defaultEndpoint?: string;
|
||||
apiKeyLabel?: string;
|
||||
}
|
||||
|
||||
const TOP_TIER: ProviderDef[] = [
|
||||
{ name: 'Anthropic', provider: 'anthropic', defaultModel: 'claude-sonnet-4-20250514', fastModel: 'claude-haiku-4-5-20251001', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'Anthropic API key' },
|
||||
{ name: 'OpenAI', provider: 'openai', defaultModel: 'gpt-4.1', fastModel: 'gpt-4.1-mini', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'OpenAI API key' },
|
||||
{ name: 'Ollama (local)', provider: 'ollama', defaultModel: 'llama3.3', fastModel: 'llama3.2:3b', needsApiKey: false, needsEndpoint: true, defaultEndpoint: 'http://localhost:11434' },
|
||||
];
|
||||
|
||||
const SECOND_TIER: ProviderDef[] = [
|
||||
{ name: 'Gemini', provider: 'gemini', defaultModel: 'gemini-2.5-flash', fastModel: 'gemini-2.0-flash-lite', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'Gemini API key' },
|
||||
{ name: 'OpenRouter', provider: 'openrouter', defaultModel: 'anthropic/claude-sonnet-4', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'OpenRouter API key' },
|
||||
{ name: 'xAI (Grok)', provider: 'xai', defaultModel: 'grok-3', fastModel: 'grok-3-mini', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'xAI API key' },
|
||||
{ name: 'Amazon Bedrock', provider: 'bedrock', defaultModel: 'anthropic.claude-sonnet-4-20250514-v1:0', needsApiKey: false, needsEndpoint: false },
|
||||
{ name: 'GitHub Models', provider: 'github', defaultModel: 'claude-sonnet-4-20250514', needsApiKey: false, needsEndpoint: false },
|
||||
];
|
||||
|
||||
async function configureProvider(p: Prompter, def: ProviderDef): Promise<{
|
||||
provider: string; model: string; api_key?: string; endpoint?: string;
|
||||
}> {
|
||||
const config: Record<string, string> = { provider: def.provider };
|
||||
if (def.needsApiKey) config.api_key = await p.password(def.apiKeyLabel ?? 'API key');
|
||||
if (def.needsEndpoint) config.endpoint = await p.ask('Host', def.defaultEndpoint);
|
||||
config.model = await p.ask('Model', def.defaultModel);
|
||||
return config as { provider: string; model: string; api_key?: string; endpoint?: string };
|
||||
}
|
||||
|
||||
export async function setupProviders(p: Prompter, builder: ConfigBuilder): Promise<void> {
|
||||
const allOptions = [
|
||||
...TOP_TIER.map(d => ({ label: d.name, value: d })),
|
||||
{ label: 'More providers...', value: null as ProviderDef | null },
|
||||
];
|
||||
|
||||
let chosen: ProviderDef;
|
||||
const selection = await p.choose('Model provider:', allOptions);
|
||||
|
||||
if (selection === null) {
|
||||
const secondOptions = SECOND_TIER.map(d => ({ label: d.name, value: d }));
|
||||
chosen = await p.choose('Model provider:', secondOptions);
|
||||
} else {
|
||||
chosen = selection;
|
||||
}
|
||||
|
||||
p.println();
|
||||
const cfg = await configureProvider(p, chosen);
|
||||
builder.setProvider('default', cfg);
|
||||
|
||||
if (chosen.fastModel) {
|
||||
p.println();
|
||||
const wantFast = await p.confirm('Configure a fast tier for compaction/delegation?', false);
|
||||
if (wantFast) {
|
||||
const fastModel = await p.ask('Fast model', chosen.fastModel);
|
||||
builder.setProvider('fast', { ...cfg, model: fastModel });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user