@unified-llm/core
Version:
Unified LLM interface (in-memory).
346 lines • 14.7 kB
JavaScript
import { validateChatRequest } from '../../utils/validation';
import BaseProvider from '../base-provider';
export class DeepSeekProvider extends BaseProvider {
constructor({ apiKey, model, tools }) {
super({ model: model || 'deepseek-chat', tools });
this.baseUrl = 'https://api.deepseek.com/v1';
this.apiKey = apiKey;
}
async chat(request) {
validateChatRequest(request);
try {
const deepseekRequest = this.convertToDeepSeekFormat(request);
let response = await this.makeAPICall('/chat/completions', deepseekRequest);
let messages = [...deepseekRequest.messages];
// Handle tool calls if present
while (response.choices[0].finish_reason === 'tool_calls' && this.tools) {
const toolCalls = response.choices[0].message.tool_calls;
const toolResults = [];
if (toolCalls) {
for (const toolCall of toolCalls) {
if (toolCall.type === 'function') {
const customFunction = this.tools.find(func => func.function.name === toolCall.function.name);
if (customFunction) {
try {
const mergedArgs = {
...(customFunction.args || {}),
...JSON.parse(toolCall.function.arguments)
};
const result = await customFunction.handler(mergedArgs);
toolResults.push({
role: 'tool',
content: typeof result === 'string' ? result : JSON.stringify(result),
tool_call_id: toolCall.id,
});
}
catch (error) {
toolResults.push({
role: 'tool',
content: error instanceof Error ? error.message : 'Unknown error',
tool_call_id: toolCall.id,
});
}
}
}
}
}
// Make follow-up request with tool results
if (toolResults.length > 0) {
messages = [
...messages,
response.choices[0].message,
...toolResults,
];
const followUpRequest = {
...deepseekRequest,
messages,
};
response = await this.makeAPICall('/chat/completions', followUpRequest);
}
else {
break;
}
}
return this.convertFromDeepSeekFormat(response);
}
catch (error) {
throw this.handleError(error);
}
}
async *stream(request) {
var _a;
validateChatRequest(request);
const deepseekRequest = this.convertToDeepSeekFormat(request);
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
...deepseekRequest,
stream: true,
}),
});
if (!response.ok) {
const error = await response.json();
throw this.handleError(error);
}
const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
if (!reader)
throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done)
break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]')
continue;
try {
const chunk = JSON.parse(data);
yield this.convertStreamChunk(chunk);
}
catch (_e) {
// Ignore parse errors
}
}
}
}
}
async makeAPICall(endpoint, payload) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
let error;
try {
error = JSON.parse(errorText);
}
catch (parseError) {
error = { message: errorText };
}
throw this.handleError(error);
}
return response.json();
}
convertToDeepSeekFormat(request) {
var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m;
const model = request.model || this.model;
if (!model) {
throw new Error('Model is required for DeepSeek chat completions');
}
const messages = request.messages.map(msg => {
const content = this.normalizeContent(msg.content);
// Handle tool result messages
if (msg.role === 'tool' || content.some(c => c.type === 'tool_result')) {
const toolResults = content.filter(c => c.type === 'tool_result');
if (toolResults.length > 0) {
return toolResults.map(tr => ({
role: 'tool',
content: Array.isArray(tr.content)
? tr.content.map(item => item.type === 'text' ? item.text : '[Non-text content]').join('\n')
: '[Tool result]',
tool_call_id: tr.toolUseId,
}));
}
}
// Handle system messages
if (msg.role === 'system') {
return {
role: 'system',
content: content.length === 1 && content[0].type === 'text'
? content[0].text
: content.filter(c => c.type === 'text').map(c => c.text).join('\n') || '[System message]',
};
}
// Handle simple text messages
if (content.length === 1 && content[0].type === 'text') {
return {
role: msg.role,
content: content[0].text,
name: msg.name,
};
}
// Handle tool use content
const toolUseContents = content.filter(c => c.type === 'tool_use');
if (toolUseContents.length > 0) {
const textContent = content.filter(c => c.type === 'text').map(c => c.text).join('\n');
return {
role: msg.role,
content: textContent || null,
tool_calls: toolUseContents.map(toolUse => ({
id: toolUse.id,
type: 'function',
function: {
name: toolUse.name,
arguments: JSON.stringify(toolUse.input)
}
})),
name: msg.name,
};
}
// Handle multimodal content
const deepseekContent = content.map(c => {
switch (c.type) {
case 'text':
return { type: 'text', text: c.text };
case 'image':
return {
type: 'image_url',
image_url: {
url: c.source.url || `data:${c.source.mediaType};base64,${c.source.data}`,
},
};
default:
return { type: 'text', text: '[Unsupported content type]' };
}
});
return {
role: msg.role,
content: deepseekContent,
name: msg.name,
};
}).flat();
return {
model: model,
messages,
temperature: (_a = request.generationConfig) === null || _a === void 0 ? void 0 : _a.temperature,
max_tokens: (_b = request.generationConfig) === null || _b === void 0 ? void 0 : _b.max_tokens,
top_p: (_c = request.generationConfig) === null || _c === void 0 ? void 0 : _c.top_p,
frequencyPenalty: (_d = request.generationConfig) === null || _d === void 0 ? void 0 : _d.frequencyPenalty,
presencePenalty: (_f = request.generationConfig) === null || _f === void 0 ? void 0 : _f.presencePenalty,
stop: (_g = request.generationConfig) === null || _g === void 0 ? void 0 : _g.stopSequences,
tools: [
...(((_h = request.tools) === null || _h === void 0 ? void 0 : _h.map(tool => ({
type: 'function',
function: tool.function,
}))) || []),
...(((_j = this.tools) === null || _j === void 0 ? void 0 : _j.map(func => ({
type: 'function',
function: func.function,
}))) || []),
].length > 0 ? [
...(((_k = request.tools) === null || _k === void 0 ? void 0 : _k.map(tool => ({
type: 'function',
function: tool.function,
}))) || []),
...(((_l = this.tools) === null || _l === void 0 ? void 0 : _l.map(func => ({
type: 'function',
function: func.function,
}))) || []),
] : undefined,
tool_choice: request.tool_choice,
responseFormat: (_m = request.generationConfig) === null || _m === void 0 ? void 0 : _m.responseFormat,
};
}
convertFromDeepSeekFormat(response) {
const choice = response.choices[0];
const message = choice.message;
const content = [];
if (message.content) {
content.push({ type: 'text', text: message.content });
}
if (message.tool_calls) {
message.tool_calls.forEach((toolCall) => {
if (toolCall.type === 'function') {
content.push({
type: 'tool_use',
id: toolCall.id,
name: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
});
}
});
}
const unifiedMessage = {
id: this.generateMessageId(),
role: message.role,
content,
createdAt: new Date(),
};
const usage = response.usage ? {
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
totalTokens: response.usage.total_tokens,
} : undefined;
// Extract text for convenience field
const contentArray = Array.isArray(unifiedMessage.content) ? unifiedMessage.content : [{ type: 'text', text: unifiedMessage.content }];
const textContent = contentArray.find((c) => c.type === 'text');
return {
id: response.id,
model: response.model,
provider: 'deepseek',
message: unifiedMessage,
text: (textContent === null || textContent === void 0 ? void 0 : textContent.text) || '',
usage,
finish_reason: choice.finish_reason,
createdAt: new Date(response.created * 1000),
rawResponse: response,
};
}
convertStreamChunk(chunk) {
const choice = chunk.choices[0];
const delta = choice.delta;
const content = [];
if (delta.content) {
content.push({ type: 'text', text: delta.content });
}
const unifiedMessage = {
id: this.generateMessageId(),
role: delta.role || 'assistant',
content,
createdAt: new Date(),
};
// Extract text for convenience field
const contentArray = Array.isArray(unifiedMessage.content) ? unifiedMessage.content : [{ type: 'text', text: unifiedMessage.content }];
const textContent = contentArray.find((c) => c.type === 'text');
return {
id: chunk.id,
model: chunk.model,
provider: 'deepseek',
message: unifiedMessage,
text: (textContent === null || textContent === void 0 ? void 0 : textContent.text) || '',
finish_reason: choice.finish_reason,
createdAt: new Date(chunk.created * 1000),
rawResponse: chunk,
};
}
handleError(error) {
var _a, _b;
return {
code: ((_a = error.error) === null || _a === void 0 ? void 0 : _a.code) || 'deepseek_error',
message: ((_b = error.error) === null || _b === void 0 ? void 0 : _b.message) || error.message || 'Unknown error occurred',
type: this.mapErrorType(error.statusCode || error.status),
statusCode: error.statusCode || error.status,
provider: 'deepseek',
details: error,
};
}
mapErrorType(status) {
if (!status)
return 'api_error';
if (status === 429)
return 'rate_limit';
if (status === 401)
return 'authentication';
if (status >= 400 && status < 500)
return 'invalid_request';
if (status >= 500)
return 'server_error';
return 'api_error';
}
}
//# sourceMappingURL=provider.js.map