UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

362 lines (303 loc) 13.2 kB
import axios from "axios"; import { configService, logger } from "./config-service"; import { preprocessText } from "../utils/text-utils"; import { withRetry } from "../utils/retry-utils"; // Rate limiting state const requestTimestamps: number[] = []; async function waitForRateLimit(): Promise<void> { const now = Date.now(); const rpmLimit = configService.DEEPSEEK_RPM_LIMIT; while (requestTimestamps.length > 0 && requestTimestamps[0] < now - 60000) { requestTimestamps.shift(); } if (requestTimestamps.length >= rpmLimit) { const timeToWait = (requestTimestamps[0] + 60000) - now; if (timeToWait > 0) { logger.info(`DeepSeek rate limit nearly reached (${requestTimestamps.length}/${rpmLimit} requests in last minute). Delaying next request for ${timeToWait}ms.`); await new Promise(resolve => setTimeout(resolve, timeToWait)); } } requestTimestamps.push(now); } /** * Check if DeepSeek API key is configured * @returns boolean - True if API key is configured, false otherwise */ export function checkDeepSeekApiKey(): boolean { // ConfigService handles loading the API key from file and environment. // It also updates process.env.DEEPSEEK_API_KEY. // This function now primarily serves to check if the key (from any source) is valid/present. const apiKey = configService.DEEPSEEK_API_KEY; if (!apiKey) { logger.error("DeepSeek API key is not configured. Set DEEPSEEK_API_KEY environment variable or run 'npm run set-deepseek-key'."); return false; } // process.env.DEEPSEEK_API_KEY is updated by configService. // Global scope setting is not strictly necessary if configService is the source of truth. logger.info(`DeepSeek API key configured (via ConfigService). Length: ${apiKey.length}`); return true; } /** * Test DeepSeek API connection * @returns Promise<boolean> - True if connection is successful, false otherwise */ export async function testDeepSeekConnection(): Promise<boolean> { try { const apiKey = configService.DEEPSEEK_API_KEY; if (!apiKey) { logger.error("DeepSeek API key is not configured (via ConfigService). Set DEEPSEEK_API_KEY environment variable or use ~/.codecompass/deepseek-config.json."); return false; } // process.env.DEEPSEEK_API_KEY is managed by configService. logger.info(`Testing DeepSeek API connection with key length: ${apiKey.length}, key prefix: ${apiKey.substring(0, 5)}...`); const apiUrl = configService.DEEPSEEK_API_URL; // process.env.DEEPSEEK_API_URL is managed by configService. logger.info(`Using DeepSeek API URL: ${apiUrl}`); try { const modelToTest = configService.DEEPSEEK_MODEL; logger.info(`Sending test request to DeepSeek API at ${apiUrl}`); logger.info(`Using model: ${modelToTest}`); logger.debug(`DeepSeek test request payload: ${JSON.stringify({ model: modelToTest, messages: [{ role: "user", content: "Hello" }], max_tokens: 10 })}`); const response = await axios.post( apiUrl, { model: modelToTest, messages: [{ role: "user", content: "Hello" }], max_tokens: 10 }, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, timeout: 15000 // Specific timeout for this test is fine } ); let dataForLogging: string; try { // response.data is 'any' from Axios. Stringify it safely for logging. dataForLogging = JSON.stringify(response.data); } catch { dataForLogging = "[Unserializable data in response]"; } logger.debug(`DeepSeek API response: ${JSON.stringify({ status: response.status, statusText: response.statusText, headers: response.headers, data: dataForLogging })}`); logger.info(`DeepSeek API test request to ${apiUrl} completed with status: ${response.status}`); if (response.status === 200) { logger.info("DeepSeek API connection successful"); return true; } logger.warn(`DeepSeek API test failed with status: ${response.status}`); return false; } catch (requestError: unknown) { const err: Error = requestError instanceof Error ? requestError : new Error(String(requestError)); interface DeepSeekErrorLogPayload { message: string; code?: string; response?: { status: number; statusText: string; data: string; } | string; request?: string; } const logPayload: DeepSeekErrorLogPayload = { message: err.message }; if (axios.isAxiosError(requestError)) { logPayload.code = requestError.code; logPayload.request = requestError.request ? 'Request present' : 'No request data'; if (requestError.response) { let responseDataString: string; try { responseDataString = typeof requestError.response.data === 'string' ? requestError.response.data : JSON.stringify(requestError.response.data); } catch { responseDataString = "[Unserializable response data]"; } logPayload.response = { status: requestError.response.status, statusText: requestError.response.statusText, data: responseDataString, }; } else { logPayload.response = 'No response data'; } // Check for specific error types using the narrowed requestError if (requestError.code === 'ECONNREFUSED') { logger.error("Connection refused. Check if the DeepSeek API endpoint is correct and accessible."); } else if (requestError.response && requestError.response.status === 401) { logger.error("Authentication failed. Check your DeepSeek API key."); } else if (requestError.response && requestError.response.status === 404) { logger.error("API endpoint not found. Check the DeepSeek API URL."); } } else { // For non-Axios errors logPayload.response = 'No response data (not an Axios error)'; logPayload.request = 'No request data (not an Axios error)'; } logger.error("DeepSeek API connection test failed", logPayload); return false; } } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); let errorCode: string | undefined; let errorResponseData: unknown; let errorResponseStatus: number | undefined; if (axios.isAxiosError(error)) { errorCode = error.code; if (error.response) { errorResponseData = error.response.data; errorResponseStatus = error.response.status; } } logger.error("DeepSeek API connection test failed (outer catch)", { message: err.message, code: errorCode, response: errorResponseStatus !== undefined ? { status: errorResponseStatus, data: errorResponseData } : null, }); return false; } } /** * Generate text with DeepSeek API * @param prompt - The prompt to generate text from * @returns Promise<string> - The generated text */ export async function generateWithDeepSeek(prompt: string): Promise<string> { try { const apiKey = configService.DEEPSEEK_API_KEY; if (!apiKey) { logger.error("DeepSeek API key not configured (via ConfigService)."); throw new Error("DeepSeek API key not configured"); } logger.info(`DeepSeek API key is configured with length: ${apiKey.length}`); await waitForRateLimit(); logger.info(`Generating with DeepSeek for prompt (length: ${prompt.length})`); const apiUrl = configService.DEEPSEEK_API_URL; const model = configService.DEEPSEEK_MODEL; logger.debug(`Using DeepSeek API URL: ${apiUrl}`); logger.debug(`Using model: ${model}`); interface DeepSeekChoice { message: { content: string; }; } interface DeepSeekChatResponse { choices: DeepSeekChoice[]; // Add other fields if necessary, e.g., usage } const response = await withRetry(async () => { logger.debug(`Sending request to DeepSeek API at ${apiUrl} with model ${model}`); logger.debug(`Request payload: ${JSON.stringify({ model: model, messages: [{ role: "user", content: `${prompt.substring(0, 50)}...` }], temperature: 0.7, max_tokens: 2048 })}`); const res = await axios.post<DeepSeekChatResponse>( apiUrl, { model: model, messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 2048 }, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, timeout: configService.REQUEST_TIMEOUT * 2 } ); if (!res.data.choices || res.data.choices.length === 0) { logger.error(`DeepSeek API request to ${apiUrl} failed with status ${res.status}: Invalid response structure. Response data: ${JSON.stringify(res.data)}`); throw new Error("Invalid response from DeepSeek API"); } logger.info(`DeepSeek API request to ${apiUrl} (generateText) completed with status: ${res.status}`); return res.data.choices[0].message.content; }); return response; } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); const axiosError = error as import('axios').AxiosError<{ message?: string }>; logger.error("DeepSeek API error", { message: err.message, code: axiosError.code, response: axiosError.response ? { status: axiosError.response.status, data: axiosError.response.data } : null, promptLength: prompt.length, promptSnippet: prompt.slice(0, 100) + (prompt.length > 100 ? '...' : '') }); throw new Error(`Failed to generate with DeepSeek: ${err.message}`); } } export async function generateEmbeddingWithDeepSeek(text: string): Promise<number[]> { try { const apiKey = configService.DEEPSEEK_API_KEY; if (!apiKey) { logger.error("DeepSeek API key not configured (via ConfigService)."); throw new Error("DeepSeek API key not configured"); } logger.info(`DeepSeek API key is configured with length: ${apiKey.length}`); const processedText = preprocessText(text); await waitForRateLimit(); const embeddingUrl = configService.DEEPSEEK_API_URL.includes("api.deepseek.com") ? "https://api.deepseek.com/embeddings" : configService.DEEPSEEK_API_URL.replace("/chat/completions", "/embeddings"); logger.info(`Generating embedding with DeepSeek for text (length: ${processedText.length})`); logger.debug(`Using DeepSeek embedding URL: ${embeddingUrl}`); interface DeepSeekEmbeddingData { embedding: number[]; // Add other fields if necessary } interface DeepSeekEmbeddingResponse { data: DeepSeekEmbeddingData[]; // Add other fields if necessary, e.g., usage } const response = await withRetry(async () => { logger.debug(`Sending embedding request to DeepSeek API with model: deepseek-embedding`); const res = await axios.post<DeepSeekEmbeddingResponse>( embeddingUrl, { model: "deepseek-embedding", input: processedText }, { headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, timeout: configService.REQUEST_TIMEOUT * 1.5 } ); if (!res.data.data || !res.data.data[0].embedding) { logger.error(`DeepSeek API request to ${embeddingUrl} failed with status ${res.status}: Invalid embedding response structure. Response data: ${JSON.stringify(res.data)}`); throw new Error("Invalid embedding response from DeepSeek API"); } logger.info(`DeepSeek API request to ${embeddingUrl} (generateEmbedding) completed with status: ${res.status}`); return res.data.data[0].embedding; }); return response; } catch (error: unknown) { const err = error instanceof Error ? error : new Error(String(error)); const axiosError = error as import('axios').AxiosError<{ message?: string }>; logger.error("DeepSeek embedding error", { message: err.message, code: axiosError.code, response: axiosError.response ? { status: axiosError.response.status, data: axiosError.response.data } : null, textLength: text.length, textSnippet: text.slice(0, 100) + (text.length > 100 ? '...' : '') }); throw new Error(`Failed to generate embedding with DeepSeek: ${err.message}`); } }