Files
extension/src/cli-tools/config/local-llm-config.ts
T
Hanzo Dev a12d455b02 feat: Add Dev CLI with multi-agent orchestration and comprehensive testing
- Implement multi-agent orchestration system for parallel AI tasks
- Add role-based AI assignment (coder, reviewer, critic, architect, etc)
- Create predefined workflows (code-review, implement-feature, optimize, debug)
- Add local LLM support with auto-detection (Ollama, LocalAI, etc)
- Implement async job management with idle timeout
- Add authentication system with OAuth2 PKCE flow
- Create comprehensive test suite with integration tests
- Add headless Chrome testing for auth flows
- Create mock AI server for testing all endpoints
- Add visual test runners and demo scripts
- Update CI/CD pipeline for all services
- Rename CLI from 'hanzo-dev' to 'dev' for simplicity
- Update repository references to github.com/hanzoai/dev

Key features:
- Parallel execution of multiple AI agents
- Git worktree support for isolated development
- Universal context sync across tools
- Secure API key management
- Comprehensive workflow system
2025-07-08 18:33:08 -04:00

299 lines
9.6 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export interface LocalLLMProvider {
name: string;
endpoint: string;
models: string[];
defaultModel: string;
apiFormat: 'openai' | 'ollama' | 'custom';
headers?: Record<string, string>;
authRequired?: boolean;
}
export interface LocalLLMConfig {
providers: LocalLLMProvider[];
defaultProvider: string;
autoDetect: boolean;
}
export class LocalLLMManager {
private configPath: string;
private config: LocalLLMConfig;
constructor(configPath?: string) {
this.configPath = configPath || path.join(os.homedir(), '.dev', 'local-llm.json');
this.config = this.loadConfig();
if (this.config.autoDetect) {
this.autoDetectProviders();
}
}
private loadConfig(): LocalLLMConfig {
const defaultConfig: LocalLLMConfig = {
providers: [
{
name: 'ollama',
endpoint: 'http://localhost:11434',
models: ['llama2', 'mistral', 'codellama', 'llama3', 'phi3', 'gemma2'],
defaultModel: 'llama3',
apiFormat: 'ollama'
},
{
name: 'llm-server',
endpoint: 'http://localhost:8080',
models: ['gpt-j', 'gpt-neox', 'bloom'],
defaultModel: 'gpt-j',
apiFormat: 'openai'
},
{
name: 'text-generation-webui',
endpoint: 'http://localhost:5000',
models: ['model'],
defaultModel: 'model',
apiFormat: 'custom'
},
{
name: 'localai',
endpoint: 'http://localhost:8000',
models: ['ggml-model'],
defaultModel: 'ggml-model',
apiFormat: 'openai'
},
{
name: 'llamacpp',
endpoint: 'http://localhost:8081',
models: ['model'],
defaultModel: 'model',
apiFormat: 'custom'
}
],
defaultProvider: 'ollama',
autoDetect: true
};
try {
if (fs.existsSync(this.configPath)) {
const userConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
return { ...defaultConfig, ...userConfig };
}
} catch (error) {
console.error('Failed to load local LLM config:', error);
}
return defaultConfig;
}
private async autoDetectProviders(): Promise<void> {
for (const provider of this.config.providers) {
try {
const isAvailable = await this.checkProviderAvailability(provider);
if (isAvailable) {
console.log(`✓ Detected ${provider.name} at ${provider.endpoint}`);
// Try to get available models
const models = await this.getAvailableModels(provider);
if (models.length > 0) {
provider.models = models;
}
}
} catch (error) {
// Provider not available
}
}
}
private async checkProviderAvailability(provider: LocalLLMProvider): Promise<boolean> {
try {
const healthEndpoint = provider.apiFormat === 'ollama'
? `${provider.endpoint}/api/tags`
: `${provider.endpoint}/health`;
const response = await fetch(healthEndpoint, {
method: 'GET',
signal: AbortSignal.timeout(3000)
});
return response.ok;
} catch {
return false;
}
}
private async getAvailableModels(provider: LocalLLMProvider): Promise<string[]> {
try {
if (provider.apiFormat === 'ollama') {
const response = await fetch(`${provider.endpoint}/api/tags`);
if (response.ok) {
const data = await response.json();
return data.models?.map((m: any) => m.name) || [];
}
} else if (provider.apiFormat === 'openai') {
const response = await fetch(`${provider.endpoint}/v1/models`);
if (response.ok) {
const data = await response.json();
return data.data?.map((m: any) => m.id) || [];
}
}
} catch (error) {
console.error(`Failed to get models for ${provider.name}:`, error);
}
return provider.models;
}
async callLocalLLM(
prompt: string,
options: {
provider?: string;
model?: string;
temperature?: number;
maxTokens?: number;
systemPrompt?: string;
} = {}
): Promise<string> {
const providerName = options.provider || this.config.defaultProvider;
const provider = this.config.providers.find(p => p.name === providerName);
if (!provider) {
throw new Error(`Provider ${providerName} not found`);
}
const model = options.model || provider.defaultModel;
switch (provider.apiFormat) {
case 'ollama':
return this.callOllama(provider, prompt, model, options);
case 'openai':
return this.callOpenAIFormat(provider, prompt, model, options);
case 'custom':
return this.callCustomFormat(provider, prompt, model, options);
default:
throw new Error(`Unknown API format: ${provider.apiFormat}`);
}
}
private async callOllama(
provider: LocalLLMProvider,
prompt: string,
model: string,
options: any
): Promise<string> {
const response = await fetch(`${provider.endpoint}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...provider.headers
},
body: JSON.stringify({
model,
prompt: options.systemPrompt
? `${options.systemPrompt}\n\n${prompt}`
: prompt,
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2048,
stream: false
})
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.statusText}`);
}
const data = await response.json();
return data.response;
}
private async callOpenAIFormat(
provider: LocalLLMProvider,
prompt: string,
model: string,
options: any
): Promise<string> {
const messages = [];
if (options.systemPrompt) {
messages.push({ role: 'system', content: options.systemPrompt });
}
messages.push({ role: 'user', content: prompt });
const response = await fetch(`${provider.endpoint}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...provider.headers
},
body: JSON.stringify({
model,
messages,
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2048
})
});
if (!response.ok) {
throw new Error(`OpenAI format error: ${response.statusText}`);
}
const data = await response.json();
return data.choices[0].message.content;
}
private async callCustomFormat(
provider: LocalLLMProvider,
prompt: string,
model: string,
options: any
): Promise<string> {
// Generic format for custom providers
const response = await fetch(`${provider.endpoint}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...provider.headers
},
body: JSON.stringify({
prompt,
model,
temperature: options.temperature || 0.7,
max_length: options.maxTokens || 2048,
system_prompt: options.systemPrompt
})
});
if (!response.ok) {
throw new Error(`Custom provider error: ${response.statusText}`);
}
const data = await response.json();
return data.text || data.response || data.generated_text || '';
}
getProviders(): LocalLLMProvider[] {
return this.config.providers;
}
getProvider(name: string): LocalLLMProvider | undefined {
return this.config.providers.find(p => p.name === name);
}
addProvider(provider: LocalLLMProvider): void {
const existing = this.config.providers.findIndex(p => p.name === provider.name);
if (existing >= 0) {
this.config.providers[existing] = provider;
} else {
this.config.providers.push(provider);
}
this.saveConfig();
}
private saveConfig(): void {
try {
fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
} catch (error) {
console.error('Failed to save local LLM config:', error);
}
}
}