import { BaseHandler } from './BaseHandler'; import { IPromptOptions, IPromptGenericConfig, ModuleOptions } from '../../types'; import { libx } from 'libx.js/build/bundles/essentials.js'; import { Streams } from 'libx.js/build/modules/Streams'; export class DeepSeekHandler extends BaseHandler { private baseUrl = 'https://api.deepseek.com/v1'; constructor(options: ModuleOptions) { super(options); } async execute(isStream: boolean, options: IPromptOptions): Promise { const isPlain = options.config.plain ?? false; try { const config = this.getConfig(options.config); const apiKey = this.getApiKey('deepseek'); if (!apiKey) { throw new Error('DeepSeek API key not configured'); } const messages = this.prepareMessages(options); const payload: any = { model: config.model, messages, stream: isStream, }; if (config.temperature !== undefined) payload.temperature = config.temperature; if (config.top_p !== undefined) payload.top_p = config.top_p; if (config.max_tokens !== undefined) payload.max_tokens = config.max_tokens; if (config.stop) payload.stop = config.stop; if (config.frequency_penalty !== undefined) payload.frequency_penalty = config.frequency_penalty; if (config.presence_penalty !== undefined) payload.presence_penalty = config.presence_penalty; if (isStream) { return await this.executeStream(payload, apiKey, isPlain); } else { return await this.executeSync(payload, apiKey, isPlain); } } catch (error) { return this.handleError(error, isPlain); } } getConfig(generalConfig: IPromptGenericConfig): any { const rawModel = generalConfig.model || 'deepseek-chat'; const model = this.normalizeModelName(rawModel, 'deepseek'); const maxTokens = generalConfig.maxTokens || 4096; // DeepSeek R1 (reasoner) has specific temperature requirements const isReasonerModel = model.includes('reasoner') || model.includes('-r1'); return { model, temperature: isReasonerModel ? 1 : (generalConfig.temperature ?? 0.7), max_tokens: maxTokens, top_p: generalConfig.topP, frequency_penalty: generalConfig.frequencyPenalty || 0, presence_penalty: generalConfig.presencePenalty || 0, stop: generalConfig.stopSequences, }; } private prepareMessages(options: IPromptOptions): any[] { const messages: any[] = []; if (options.systemPrompt) { messages.push({ role: 'system', content: options.systemPrompt }); } if (options.messages) { for (const msg of options.messages) { const message: any = { role: msg.role, content: msg.content, }; if (msg.name) message.name = msg.name; messages.push(message); } } return messages; } private async executeStream(payload: any, apiKey: string, isPlain: boolean): Promise { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); const url = `${this.baseUrl}/chat/completions`; libx.log.v('DeepSeekHandler: streaming request to', url); let faultyChunk = ''; const promise = Streams.getStream( url, async (eventChunk: string) => { try { const rows = eventChunk.split('\n').filter((x) => x.trim() != ''); for (let row of rows) { if (!row.startsWith('data:')) { row = faultyChunk + row; faultyChunk = ''; } const dataStr = row.slice(5).trim(); if (dataStr === '[DONE]') continue; try { const data = JSON.parse(dataStr); const delta = data.choices?.[0]?.delta?.content; if (delta) { await this.writeStreamChunk(writer, encoder, delta, isPlain); } } catch (e) { faultyChunk = row; } } } catch (err) { libx.log.w('DeepSeekHandler: chunk processing error', err); } }, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: payload, encoding: 'utf-8', useEventBuffering: false, } ); promise.then(() => { libx.log.v('DeepSeekHandler: stream completed'); if (!isPlain) { writer.write(encoder.encode('data: [DONE]\n\n')); } writer.close(); }).catch((error) => { libx.log.e('DeepSeekHandler: stream error', error); if (isPlain) { writer.write(encoder.encode(`\nError: ${error.message}`)); } else { writer.write(encoder.encode(`data: ${JSON.stringify({ error: error.message })}\n\n`)); } writer.close(); }); return readable; } private async executeSync(payload: any, apiKey: string, isPlain: boolean): Promise { const url = `${this.baseUrl}/chat/completions`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.text(); throw new Error(`DeepSeek API error: ${error}`); } const data = await response.json(); const content = data.choices?.[0]?.message?.content || ''; const metadata = { model: data.model, usage: data.usage, finish_reason: data.choices?.[0]?.finish_reason }; return this.writeSyncResponse(content, metadata, isPlain); } }