refactor: update Deepseek API implementation

- Switch to OpenAI SDK for Deepseek API calls
- Add openai package dependency
- Update configuration for Deepseek API
- Update environment variable examples
- Improve error handling for API responses

This change improves reliability and maintainability by using the official SDK
with proper typing and error handling.
This commit is contained in:
cyanheads
2025-01-25 19:09:41 -08:00
parent 7639395c85
commit a6edebbf8e
5 changed files with 227 additions and 102 deletions

View File

@@ -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

193
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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<ApiErrorResponse>): Promise<never> {
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<DeepseekResponse>('/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
};
}
}

View File

@@ -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),
};