diff --git a/src/backend/src/modules/puterai/DeepSeekService.js b/src/backend/src/modules/puterai/DeepSeekService.js new file mode 100644 index 000000000..697f42412 --- /dev/null +++ b/src/backend/src/modules/puterai/DeepSeekService.js @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// METADATA // {"ai-commented":{"service":"claude"}} +const BaseService = require("../../services/BaseService"); +const { whatis, nou } = require("../../util/langutil"); +const { PassThrough } = require("stream"); +const { TypedValue } = require("../../services/drivers/meta/Runtime"); +const { TeePromise } = require('@heyputer/putility').libs.promise; + +const PUTER_PROMPT = ` + You are running on an open-source platform called Puter, + as the DeepSeek implementation for a driver interface + called puter-chat-completion. + + The following JSON contains system messages from the + user of the driver interface (typically an app on Puter): +`.replace('\n', ' ').trim(); + + +/** +* DeepSeekService class - Provides integration with X.AI's API for chat completions +* Extends BaseService to implement the puter-chat-completion interface. +* Handles model management, message adaptation, streaming responses, +* and usage tracking for X.AI's language models like Grok. +* @extends BaseService +*/ +class DeepSeekService extends BaseService { + static MODULES = { + openai: require('openai'), + } + + + /** + * Gets the system prompt used for AI interactions + * @returns {string} The base system prompt that identifies the AI as running on Puter + */ + get_system_prompt () { + return PUTER_PROMPT; + } + + adapt_model (model) { + return model; + } + + + /** + * Initializes the XAI service by setting up the OpenAI client and registering with the AI chat provider + * @private + * @returns {Promise} Resolves when initialization is complete + */ + async _init () { + this.openai = new this.modules.openai.OpenAI({ + apiKey: this.global_config.services.deepseek.apiKey, + baseURL: 'https://api.deepseek.com', + }); + + const svc_aiChat = this.services.get('ai-chat'); + svc_aiChat.register_provider({ + service_name: this.service_name, + alias: true, + }); + } + + + /** + * Returns the default model identifier for the XAI service + * @returns {string} The default model ID 'grok-beta' + */ + get_default_model () { + return 'grok-beta'; + } + + static IMPLEMENTS = { + ['puter-chat-completion']: { + /** + * Returns a list of available models and their details. + * See AIChatService for more information. + * + * @returns Promise> Array of model details + */ + async models () { + return await this.models_(); + }, + /** + * Returns a list of available model names including their aliases + * @returns {Promise} Array of model identifiers and their aliases + * @description Retrieves all available model IDs and their aliases, + * flattening them into a single array of strings that can be used for model selection + */ + async list () { + const models = await this.models_(); + const model_names = []; + for ( const model of models ) { + model_names.push(model.id); + if ( model.aliases ) { + model_names.push(...model.aliases); + } + } + return model_names; + }, + + /** + * AI Chat completion method. + * See AIChatService for more details. + */ + async complete ({ messages, stream, model }) { + model = this.adapt_model(model); + const adapted_messages = []; + + const system_prompts = []; + let previous_was_user = false; + for ( const message of messages ) { + if ( typeof message.content === 'string' ) { + message.content = { + type: 'text', + text: message.content, + }; + } + if ( whatis(message.content) !== 'array' ) { + message.content = [message.content]; + } + if ( ! message.role ) message.role = 'user'; + if ( message.role === 'user' && previous_was_user ) { + const last_msg = adapted_messages[adapted_messages.length-1]; + last_msg.content.push( + ...(Array.isArray ? message.content : [message.content]) + ); + continue; + } + if ( message.role === 'system' ) { + system_prompts.push(...message.content); + continue; + } + adapted_messages.push(message); + if ( message.role === 'user' ) { + previous_was_user = true; + } + } + + adapted_messages.unshift({ + role: 'system', + content: this.get_system_prompt() + + JSON.stringify(system_prompts), + }) + + const completion = await this.openai.chat.completions.create({ + messages: adapted_messages, + model: model ?? this.get_default_model(), + max_tokens: 1000, + stream, + ...(stream ? { + stream_options: { include_usage: true }, + } : {}), + }); + + if ( stream ) { + let usage_promise = new TeePromise(); + + const stream = new PassThrough(); + const retval = new TypedValue({ + $: 'stream', + content_type: 'application/x-ndjson', + chunked: true, + }, stream); + (async () => { + let last_usage = null; + for await ( const chunk of completion ) { + if ( chunk.usage ) last_usage = chunk.usage; + // if ( + // event.type !== 'content_block_delta' || + // event.delta.type !== 'text_delta' + // ) continue; + // const str = JSON.stringify({ + // text: event.delta.text, + // }); + // stream.write(str + '\n'); + if ( chunk.choices.length < 1 ) continue; + if ( nou(chunk.choices[0].delta.content) ) continue; + const str = JSON.stringify({ + text: chunk.choices[0].delta.content + }); + stream.write(str + '\n'); + } + usage_promise.resolve({ + input_tokens: last_usage.prompt_tokens, + output_tokens: last_usage.completion_tokens, + }); + stream.end(); + })(); + + return new TypedValue({ $: 'ai-chat-intermediate' }, { + stream: true, + response: retval, + usage_promise: usage_promise, + }); + } + + const ret = completion.choices[0]; + ret.usage = { + input_tokens: completion.usage.prompt_tokens, + output_tokens: completion.usage.completion_tokens, + }; + return ret; + } + } + } + + + /** + * Retrieves available AI models and their specifications + * @returns {Promise} Array of model objects containing: + * - id: Model identifier string + * - name: Human readable model name + * - context: Maximum context window size + * - cost: Pricing information object with currency and rates + * @private + */ + async models_ () { + return [ + { + id: 'deepseek-chat', + name: 'DeepSeek Chat', + context: 64000, + cost: { + currency: 'usd-cents', + tokens: 1_000_000, + input: 14, + output: 28, + }, + }, + { + id: 'deepseek-reasoner', + name: 'DeepSeek Reasoner', + context: 64000, + cost: { + currency: 'usd-cents', + tokens: 1_000_000, + input: 55, + output: 219, + }, + } + ]; + } +} + +module.exports = { + DeepSeekService, +}; + diff --git a/src/backend/src/modules/puterai/PuterAIModule.js b/src/backend/src/modules/puterai/PuterAIModule.js index 04247342b..b3ec8d48f 100644 --- a/src/backend/src/modules/puterai/PuterAIModule.js +++ b/src/backend/src/modules/puterai/PuterAIModule.js @@ -92,6 +92,14 @@ class PuterAIModule extends AdvancedBase { // services.registerService('claude', ClaudeEnoughService); } + if ( !! config?.services?.['deepseek'] ) { + const { DeepSeekService } = require('./DeepSeekService'); + services.registerService('deepseek', DeepSeekService); + + // const { ClaudeEnoughService } = require('./ClaudeEnoughService'); + // services.registerService('claude', ClaudeEnoughService); + } + const { AIChatService } = require('./AIChatService'); services.registerService('ai-chat', AIChatService);