Add DeepSeek and LMStudio providers

- Integrate DeepSeek and LMStudio AI providers
- Add message processing utilities for improved handling
- Implement reasoning panel for message actions
- Add logging functionality to UI
- Update configurations and dependencies
This commit is contained in:
haddadrm
2025-02-25 08:53:53 +04:00
parent 4d24d73161
commit a6e4402616
18 changed files with 8270 additions and 592 deletions

251
src/lib/deepseekChat.ts Normal file
View File

@ -0,0 +1,251 @@
import { BaseChatModel, BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
import { AIMessage, AIMessageChunk, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
import { ChatResult, ChatGenerationChunk } from '@langchain/core/outputs';
import axios from 'axios';
import { BaseChatModelParams } from '@langchain/core/language_models/chat_models';
interface DeepSeekChatParams extends BaseChatModelParams {
apiKey: string;
baseURL: string;
modelName: string;
temperature?: number;
max_tokens?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
export class DeepSeekChat extends BaseChatModel<BaseChatModelCallOptions & { stream?: boolean }> {
private apiKey: string;
private baseURL: string;
private modelName: string;
private temperature: number;
private maxTokens: number;
private topP: number;
private frequencyPenalty: number;
private presencePenalty: number;
constructor(params: DeepSeekChatParams) {
super(params);
this.apiKey = params.apiKey;
this.baseURL = params.baseURL;
this.modelName = params.modelName;
this.temperature = params.temperature ?? 0.7;
this.maxTokens = params.max_tokens ?? 8192;
this.topP = params.top_p ?? 1;
this.frequencyPenalty = params.frequency_penalty ?? 0;
this.presencePenalty = params.presence_penalty ?? 0;
}
async _generate(
messages: BaseMessage[],
options: this['ParsedCallOptions'],
runManager?: CallbackManagerForLLMRun
): Promise<ChatResult> {
const formattedMessages = messages.map(msg => ({
role: this.getRole(msg),
content: msg.content.toString(),
}));
const response = await this.callDeepSeekAPI(formattedMessages, options.stream);
if (options.stream) {
return this.processStreamingResponse(response, messages, options, runManager);
} else {
const choice = response.data.choices[0];
let content = choice.message.content || '';
if (choice.message.reasoning_content) {
content = `<think>\n${choice.message.reasoning_content}\n</think>\n\n${content}`;
}
// Report usage stats if available
if (response.data.usage && runManager) {
runManager.handleLLMEnd({
generations: [],
llmOutput: {
tokenUsage: {
completionTokens: response.data.usage.completion_tokens,
promptTokens: response.data.usage.prompt_tokens,
totalTokens: response.data.usage.total_tokens
}
}
});
}
return {
generations: [
{
text: content,
message: new AIMessage(content),
},
],
};
}
}
private getRole(msg: BaseMessage): string {
if (msg instanceof SystemMessage) return 'system';
if (msg instanceof HumanMessage) return 'user';
if (msg instanceof AIMessage) return 'assistant';
return 'user'; // Default to user
}
private async callDeepSeekAPI(messages: Array<{ role: string; content: string }>, streaming?: boolean) {
return axios.post(
`${this.baseURL}/chat/completions`,
{
messages,
model: this.modelName,
stream: streaming,
temperature: this.temperature,
max_tokens: this.maxTokens,
top_p: this.topP,
frequency_penalty: this.frequencyPenalty,
presence_penalty: this.presencePenalty,
response_format: { type: 'text' },
...(streaming && {
stream_options: {
include_usage: true
}
})
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
responseType: streaming ? 'text' : 'json',
}
);
}
public async *_streamResponseChunks(messages: BaseMessage[], options: this['ParsedCallOptions'], runManager?: CallbackManagerForLLMRun) {
const response = await this.callDeepSeekAPI(messages.map(msg => ({
role: this.getRole(msg),
content: msg.content.toString(),
})), true);
let thinkState = -1; // -1: not started, 0: thinking, 1: answered
let currentContent = '';
// Split the response into lines
const lines = response.data.split('\n');
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const jsonStr = line.slice(6);
if (jsonStr === '[DONE]') break;
try {
console.log('Received chunk:', jsonStr);
const chunk = JSON.parse(jsonStr);
const delta = chunk.choices[0].delta;
console.log('Parsed delta:', delta);
// Handle usage stats in final chunk
if (chunk.usage && !chunk.choices?.length) {
runManager?.handleLLMEnd?.({
generations: [],
llmOutput: {
tokenUsage: {
completionTokens: chunk.usage.completion_tokens,
promptTokens: chunk.usage.prompt_tokens,
totalTokens: chunk.usage.total_tokens
}
}
});
continue;
}
// Handle reasoning content
if (delta.reasoning_content) {
if (thinkState === -1) {
thinkState = 0;
const startTag = '<think>\n';
currentContent += startTag;
console.log('Emitting think start:', startTag);
runManager?.handleLLMNewToken(startTag);
const chunk = new ChatGenerationChunk({
text: startTag,
message: new AIMessageChunk(startTag),
generationInfo: {}
});
yield chunk;
}
currentContent += delta.reasoning_content;
console.log('Emitting reasoning:', delta.reasoning_content);
runManager?.handleLLMNewToken(delta.reasoning_content);
const chunk = new ChatGenerationChunk({
text: delta.reasoning_content,
message: new AIMessageChunk(delta.reasoning_content),
generationInfo: {}
});
yield chunk;
}
// Handle regular content
if (delta.content) {
if (thinkState === 0) {
thinkState = 1;
const endTag = '\n</think>\n\n';
currentContent += endTag;
console.log('Emitting think end:', endTag);
runManager?.handleLLMNewToken(endTag);
const chunk = new ChatGenerationChunk({
text: endTag,
message: new AIMessageChunk(endTag),
generationInfo: {}
});
yield chunk;
}
currentContent += delta.content;
console.log('Emitting content:', delta.content);
runManager?.handleLLMNewToken(delta.content);
const chunk = new ChatGenerationChunk({
text: delta.content,
message: new AIMessageChunk(delta.content),
generationInfo: {}
});
yield chunk;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to parse chunk';
console.error(`Streaming error: ${errorMessage}`);
if (error instanceof Error && error.message.includes('DeepSeek API Error')) {
throw error;
}
}
}
// Handle any unclosed think block
if (thinkState === 0) {
const endTag = '\n</think>\n\n';
currentContent += endTag;
runManager?.handleLLMNewToken(endTag);
const chunk = new ChatGenerationChunk({
text: endTag,
message: new AIMessageChunk(endTag),
generationInfo: {}
});
yield chunk;
}
}
private async processStreamingResponse(response: any, messages: BaseMessage[], options: this['ParsedCallOptions'], runManager?: CallbackManagerForLLMRun): Promise<ChatResult> {
let accumulatedContent = '';
for await (const chunk of this._streamResponseChunks(messages, options, runManager)) {
accumulatedContent += chunk.message.content;
}
return {
generations: [
{
text: accumulatedContent,
message: new AIMessage(accumulatedContent),
},
],
};
}
_llmType(): string {
return 'deepseek';
}
}

View File

@ -0,0 +1,69 @@
import { DeepSeekChat } from '../deepseekChat';
import logger from '../../utils/logger';
import { getDeepseekApiKey } from '../../config';
import axios from 'axios';
interface DeepSeekModel {
id: string;
object: string;
owned_by: string;
}
interface ModelListResponse {
object: 'list';
data: DeepSeekModel[];
}
interface ChatModelConfig {
displayName: string;
model: DeepSeekChat;
}
const MODEL_DISPLAY_NAMES: Record<string, string> = {
'deepseek-reasoner': 'DeepSeek R1',
'deepseek-chat': 'DeepSeek V3'
};
export const loadDeepSeekChatModels = async (): Promise<Record<string, ChatModelConfig>> => {
const deepSeekEndpoint = 'https://api.deepseek.com';
const apiKey = getDeepseekApiKey();
if (!apiKey) return {};
if (!deepSeekEndpoint || !apiKey) {
logger.debug('DeepSeek endpoint or API key not configured, skipping');
return {};
}
try {
const response = await axios.get<{ data: DeepSeekModel[] }>(`${deepSeekEndpoint}/models`, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
});
const deepSeekModels = response.data.data;
const chatModels = deepSeekModels.reduce<Record<string, ChatModelConfig>>((acc, model) => {
// Only include models we have display names for
if (model.id in MODEL_DISPLAY_NAMES) {
acc[model.id] = {
displayName: MODEL_DISPLAY_NAMES[model.id],
model: new DeepSeekChat({
apiKey,
baseURL: deepSeekEndpoint,
modelName: model.id,
temperature: 0.7,
}),
};
}
return acc;
}, {});
return chatModels;
} catch (err) {
logger.error(`Error loading DeepSeek models: ${String(err)}`);
return {};
}
};

View File

@ -4,6 +4,8 @@ import { loadOpenAIChatModels, loadOpenAIEmbeddingsModels } from './openai';
import { loadAnthropicChatModels } from './anthropic';
import { loadTransformersEmbeddingsModels } from './transformers';
import { loadGeminiChatModels, loadGeminiEmbeddingsModels } from './gemini';
import { loadDeepSeekChatModels } from './deepseek';
import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio';
import {
getCustomOpenaiApiKey,
getCustomOpenaiApiUrl,
@ -17,6 +19,8 @@ const chatModelProviders = {
ollama: loadOllamaChatModels,
anthropic: loadAnthropicChatModels,
gemini: loadGeminiChatModels,
deepseek: loadDeepSeekChatModels,
lm_studio: loadLMStudioChatModels,
};
const embeddingModelProviders = {
@ -24,6 +28,7 @@ const embeddingModelProviders = {
local: loadTransformersEmbeddingsModels,
ollama: loadOllamaEmbeddingsModels,
gemini: loadGeminiEmbeddingsModels,
lm_studio: loadLMStudioEmbeddingsModels,
};
export const getAvailableChatModelProviders = async () => {

View File

@ -0,0 +1,96 @@
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { getLMStudioApiEndpoint, getKeepAlive } from '../../config';
import logger from '../../utils/logger';
import axios from 'axios';
interface LMStudioModel {
id: string;
name?: string;
}
const ensureV1Endpoint = (endpoint: string): string =>
endpoint.endsWith('/v1') ? endpoint : `${endpoint}/v1`;
const checkServerAvailability = async (endpoint: string): Promise<boolean> => {
try {
const keepAlive = getKeepAlive();
await axios.get(`${ensureV1Endpoint(endpoint)}/models`, {
timeout: parseInt(keepAlive) * 1000 || 5000,
headers: { 'Content-Type': 'application/json' },
});
return true;
} catch {
return false;
}
};
export const loadLMStudioChatModels = async () => {
const endpoint = getLMStudioApiEndpoint();
const keepAlive = getKeepAlive();
if (!endpoint) return {};
if (!await checkServerAvailability(endpoint)) return {};
try {
const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, {
timeout: parseInt(keepAlive) * 1000 || 5000,
headers: { 'Content-Type': 'application/json' },
});
const chatModels = response.data.data.reduce((acc: Record<string, any>, model: LMStudioModel) => {
acc[model.id] = {
displayName: model.name || model.id,
model: new ChatOpenAI({
openAIApiKey: 'lm-studio',
configuration: {
baseURL: ensureV1Endpoint(endpoint),
},
modelName: model.id,
temperature: 0.7,
streaming: true,
maxRetries: 3
}),
};
return acc;
}, {});
return chatModels;
} catch (err) {
logger.error(`Error loading LM Studio models: ${err}`);
return {};
}
};
export const loadLMStudioEmbeddingsModels = async () => {
const endpoint = getLMStudioApiEndpoint();
const keepAlive = getKeepAlive();
if (!endpoint) return {};
if (!await checkServerAvailability(endpoint)) return {};
try {
const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, {
timeout: parseInt(keepAlive) * 1000 || 5000,
headers: { 'Content-Type': 'application/json' },
});
const embeddingsModels = response.data.data.reduce((acc: Record<string, any>, model: LMStudioModel) => {
acc[model.id] = {
displayName: model.name || model.id,
model: new OpenAIEmbeddings({
openAIApiKey: 'lm-studio',
configuration: {
baseURL: ensureV1Endpoint(endpoint),
},
modelName: model.id,
}),
};
return acc;
}, {});
return embeddingsModels;
} catch (err) {
logger.error(`Error loading LM Studio embeddings model: ${err}`);
return {};
}
};