UNPKG

@neuroequalityorg/knightcode

Version:

Knightcode CLI - Your local AI coding assistant using Ollama, LM Studio, and more

286 lines (248 loc) 7.87 kB
/** * LM Studio Client * * Handles interaction with LM Studio's local API for text completion * and code assistance features. */ import { logger } from '../utils/logger.js'; import { createUserError } from '../errors/formatter.js'; import { ErrorCategory } from '../errors/types.js'; import { withTimeout, withRetry } from '../utils/async.js'; import { AIProvider, Message, CompletionOptions, CompletionResponse, StreamEvent } from './provider.js'; interface CompletionRequest { model: string; messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>; temperature?: number; max_tokens?: number; top_p?: number; top_k?: number; stop_sequences?: string[]; stream?: boolean; system?: string; } interface ApiError { error?: { message?: string; }; } // Default API configuration const DEFAULT_CONFIG = { apiBaseUrl: 'http://localhost:1234', timeout: 60000, // 60 seconds retryOptions: { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000 }, defaultModel: 'default', // LM Studio uses 'default' for the loaded model defaultMaxTokens: 4096, defaultTemperature: 0.7 }; /** * LM Studio client for interacting with local LM Studio API */ export class LMStudioClient implements AIProvider { private config: typeof DEFAULT_CONFIG; private model: string; constructor(config: Partial<typeof DEFAULT_CONFIG> = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.model = this.config.defaultModel; } /** * Test connection to LM Studio service */ async testConnection(): Promise<boolean> { try { logger.debug('Testing connection to LM Studio service'); const timeoutFetch = withTimeout( () => fetch(`${this.config.apiBaseUrl}/v1/models`), this.config.timeout ); const response = await timeoutFetch(); if (!response.ok) { logger.debug('LM Studio service responded with non-OK status:', response.status); return false; } const models = await response.json(); logger.debug('Available LM Studio models:', models); return true; } catch (error) { logger.debug('Failed to connect to LM Studio service:', error); return false; } } /** * Get available models from LM Studio */ async getModels(): Promise<string[]> { try { const timeoutFetch = withTimeout( () => fetch(`${this.config.apiBaseUrl}/v1/models`), this.config.timeout ); const response = await timeoutFetch(); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json() as any; return data.data?.map((model: any) => model.id) || []; } catch (error) { logger.error('Failed to get LM Studio models:', error); throw createUserError('Failed to get available models', { cause: error, category: ErrorCategory.CONNECTION, resolution: 'Check if LM Studio is running and accessible.' }); } } /** * Set the model to use */ setModel(model: string): void { this.model = model; logger.debug('LM Studio model set to:', model); } /** * Get the current model */ getModel(): string { return this.model; } /** * Complete text using LM Studio */ async complete(options: CompletionOptions): Promise<CompletionResponse> { const { temperature = this.config.defaultTemperature, maxTokens = this.config.defaultMaxTokens, topP = 1.0, topK = 40, stopSequences = [], stream = false, system = '' } = options; const messages = system ? [{ role: 'system' as const, content: system }, ...options.messages] : options.messages; const requestBody: CompletionRequest = { model: this.model, messages, temperature, max_tokens: maxTokens, top_p: topP, top_k: topK, stop_sequences: stopSequences, stream }; try { logger.debug('Sending completion request to LM Studio:', { model: this.model, messageCount: messages.length, temperature, maxTokens }); const timeoutFetch = withTimeout( () => fetch(`${this.config.apiBaseUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }), this.config.timeout ); const retryFetch = withRetry(timeoutFetch, this.config.retryOptions); const response = await retryFetch(); if (!response.ok) { const errorText = await response.text(); logger.error(`LM Studio API error: ${response.status} - ${errorText}`); throw createUserError('LM Studio API error', { cause: new Error(`HTTP ${response.status}: ${errorText}`), category: ErrorCategory.API, resolution: 'Check LM Studio logs and ensure the model is loaded.' }); } const data = await response.json() as any; // Transform LM Studio response to match our interface const completionResponse: CompletionResponse = { id: data.id || `lmstudio_${Date.now()}`, model: data.model || this.model, usage: { input_tokens: data.usage?.prompt_tokens || 0, output_tokens: data.usage?.completion_tokens || 0 }, content: data.choices?.[0]?.message?.content ? [{ type: 'text', text: data.choices[0].message.content }] : [], stop_reason: data.choices?.[0]?.finish_reason || 'stop' }; logger.debug('LM Studio completion successful:', { id: completionResponse.id, model: completionResponse.model, inputTokens: completionResponse.usage.input_tokens, outputTokens: completionResponse.usage.output_tokens }); return completionResponse; } catch (error) { logger.error('LM Studio completion failed:', error); if (error instanceof Error && error.message.includes('LM Studio API error')) { throw error; } throw createUserError('Failed to complete text with LM Studio', { cause: error, category: ErrorCategory.API, resolution: 'Check if LM Studio is running and the model is loaded.' }); } } /** * Stream text completion using LM Studio */ async *completeStream(options: CompletionOptions): AsyncGenerator<StreamEvent> { const streamOptions = { ...options, stream: true }; try { const response = await this.complete(streamOptions); // For now, return the complete response as a single event // In a full implementation, this would handle actual streaming yield { type: 'message_start', message: { id: response.id, model: response.model, content: response.content, stop_reason: response.stop_reason } }; yield { type: 'message_stop', message: { id: response.id, model: response.model, content: response.content, stop_reason: response.stop_reason } }; } catch (error) { logger.error('LM Studio streaming failed:', error); throw error; } } /** * Get configuration information */ getConfig(): typeof DEFAULT_CONFIG { return { ...this.config }; } /** * Update configuration */ updateConfig(newConfig: Partial<typeof DEFAULT_CONFIG>): void { this.config = { ...this.config, ...newConfig }; logger.debug('LM Studio client configuration updated:', this.config); } /** * Get provider name */ getProviderName(): string { return 'lmstudio'; } }