@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
362 lines (303 loc) • 13.2 kB
text/typescript
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}`);
}
}