diff --git a/.env.example b/.env.example index 4c30a1c..205f5d7 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # Deepseek API Configuration DEEPSEEK_API_KEY=your_deepseek_api_key_here -DEEPSEEK_API_BASE_URL=https://api.deepseek.com/v1 -DEEPSEEK_MODEL=deepseek-coder +DEEPSEEK_API_BASE_URL=https://api.deepseek.com +DEEPSEEK_MODEL=deepseek-chat DEEPSEEK_MAX_RETRIES=3 DEEPSEEK_TIMEOUT=30000 diff --git a/package-lock.json b/package-lock.json index 016b47a..b3f7511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^22.10.10", "axios": "^1.7.9", "dotenv": "^16.4.7", + "openai": "^4.80.1", "typescript": "^5.7.3" } }, @@ -41,6 +42,40 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -118,6 +153,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventsource": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz", @@ -173,6 +217,25 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -189,6 +252,15 @@ "node": ">= 0.8" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -228,6 +300,96 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai": { + "version": "4.80.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.80.1.tgz", + "integrity": "sha512-+6+bbXFwbIE88foZsBEt36bPkgZPdyFN82clAXG61gnHb2gXdZApDyRrcAHqEtpYICywpqaNo57kOm9dtnb7Cw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.74", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz", + "integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -279,6 +441,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -307,6 +475,31 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/zod": { "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", diff --git a/package.json b/package.json index 8dee2d9..0b59049 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/node": "^22.10.10", "axios": "^1.7.9", "dotenv": "^16.4.7", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "openai": "^4.80.1" } } diff --git a/src/api/deepseek/deepseek.ts b/src/api/deepseek/deepseek.ts index 387433a..5bc1e21 100644 --- a/src/api/deepseek/deepseek.ts +++ b/src/api/deepseek/deepseek.ts @@ -1,41 +1,8 @@ -import axios, { AxiosError, AxiosInstance } from 'axios'; +import OpenAI from 'openai'; import type { LLMResponse } from '../../types/index.js'; import { config } from '../../config.js'; import { sanitizeInput } from '../../utils/prompt.js'; -/** - * Interface for Deepseek API message format - */ -interface DeepseekMessage { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -/** - * Interface for Deepseek API response format - */ -interface DeepseekResponse { - choices: Array<{ - message: { - content: string; - }; - finish_reason?: string; - }>; - usage?: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -/** - * Interface for API error response - */ -interface ApiErrorResponse { - message?: string; - error?: string; -} - /** * Rate limiter implementation using token bucket algorithm */ @@ -75,61 +42,24 @@ class RateLimiter { } /** - * Deepseek API client class + * Deepseek API client class using OpenAI SDK */ class DeepseekClient { - private readonly axiosInstance: AxiosInstance; + private readonly client: OpenAI; private readonly rateLimiter: RateLimiter; constructor() { - this.axiosInstance = axios.create({ - baseURL: config.api.baseUrl, - timeout: config.api.timeout, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.api.apiKey}`, - }, + this.client = new OpenAI({ + baseURL: config.api.baseUrl || 'https://api.deepseek.com', + apiKey: config.api.apiKey, + defaultQuery: { model: config.api.model || 'deepseek-chat' }, + defaultHeaders: { 'api-key': config.api.apiKey } }); this.rateLimiter = new RateLimiter( 50, // max 50 requests 10 // refill 10 tokens per second ); - - // Add response interceptor for error handling - this.axiosInstance.interceptors.response.use( - response => response, - this.handleApiError.bind(this) - ); - } - - /** - * Handles API errors and transforms them into appropriate responses - */ - private async handleApiError(error: AxiosError): Promise { - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - const status = error.response.status; - const message = error.response.data?.message || error.response.data?.error || error.message; - - switch (status) { - case 401: - throw new Error('Authentication failed: Invalid API key'); - case 429: - throw new Error('Rate limit exceeded. Please try again later.'); - case 500: - throw new Error('Deepseek API server error. Please try again later.'); - default: - throw new Error(`API error: ${message}`); - } - } else if (error.request) { - // The request was made but no response was received - throw new Error('No response received from Deepseek API'); - } else { - // Something happened in setting up the request - throw new Error(`Error setting up request: ${error.message}`); - } } /** @@ -147,7 +77,7 @@ class DeepseekClient { return await operation(); } catch (error) { lastError = error as Error; - if (error instanceof AxiosError && error.response?.status === 429) { + if (error instanceof OpenAI.APIError && error.status === 429) { const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); continue; @@ -180,37 +110,38 @@ class DeepseekClient { const sanitizedPrompt = sanitizeInput(prompt); const sanitizedSystemPrompt = sanitizeInput(systemPrompt); - const messages: DeepseekMessage[] = [ - { - role: 'system', - content: sanitizedSystemPrompt, - }, - { - role: 'user', - content: sanitizedPrompt, - }, - ]; - const response = await this.retryWithExponentialBackoff(async () => { - const result = await this.axiosInstance.post('/chat/completions', { - model: config.api.model, - messages, + const completion = await this.client.chat.completions.create({ + messages: [ + { role: 'system', content: sanitizedSystemPrompt }, + { role: 'user', content: sanitizedPrompt } + ], + model: config.api.model || 'deepseek-chat', temperature: 0.7, - max_tokens: 2048, + max_tokens: 2048 }); - return result; + + return completion; }); return { - text: response.data.choices[0].message.content, + text: response.choices[0]?.message?.content || '', isError: false, }; } catch (error) { console.error('Deepseek API error:', error); + let errorMessage = 'Unknown error occurred'; + + if (error instanceof OpenAI.APIError) { + errorMessage = error.message; + } else if (error instanceof Error) { + errorMessage = error.message; + } + return { text: '', isError: true, - errorMessage: error instanceof Error ? error.message : 'Unknown error occurred', + errorMessage }; } } diff --git a/src/config.ts b/src/config.ts index 9ad7764..0cf9925 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,8 +14,8 @@ function requireEnv(name: string): string { const apiConfig: APIConfig = { apiKey: requireEnv('DEEPSEEK_API_KEY'), - baseUrl: process.env.DEEPSEEK_API_BASE_URL || 'https://api.deepseek.com/v1', - model: process.env.DEEPSEEK_MODEL || 'deepseek-coder', + baseUrl: process.env.DEEPSEEK_API_BASE_URL || 'https://api.deepseek.com', + model: process.env.DEEPSEEK_MODEL || 'deepseek-chat', maxRetries: parseInt(process.env.DEEPSEEK_MAX_RETRIES || '3', 10), timeout: parseInt(process.env.DEEPSEEK_TIMEOUT || '30000', 10), };