From 1b5a64043ab0bc39916f2d315b95cb640eb636b9 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Mon, 17 Feb 2025 18:46:48 -0500 Subject: [PATCH] dev: add openrouter implementation --- .../src/modules/puterai/OpenRouterService.js | 197 ++++++++++++++++++ .../src/modules/puterai/PuterAIModule.js | 4 + src/puter-js/src/modules/AI.js | 3 + 3 files changed, 204 insertions(+) create mode 100644 src/backend/src/modules/puterai/OpenRouterService.js diff --git a/src/backend/src/modules/puterai/OpenRouterService.js b/src/backend/src/modules/puterai/OpenRouterService.js new file mode 100644 index 00000000..23761615 --- /dev/null +++ b/src/backend/src/modules/puterai/OpenRouterService.js @@ -0,0 +1,197 @@ +/* + * 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 OpenAIUtil = require("./lib/OpenAIUtil"); + +const PUTER_PROMPT = ` + You are running on an open-source platform called Puter, + under the OpenRouter implementation for a driver interface + called puter-chat-completion. +`.replace('\n', ' ').trim(); + + +/** +* XAIService 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 OpenRouterService extends BaseService { + static MODULES = { + openai: require('openai'), + kv: globalThis.kv, + uuidv4: require('uuid').v4, + axios: require('axios'), + } + + + /** + * 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.api_base_url = 'https://openrouter.ai/api/v1'; + this.openai = new this.modules.openai.OpenAI({ + apiKey: this.config.apiKey, + baseURL: this.api_base_url, + }); + this.kvkey = this.modules.uuidv4(); + + 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); + } + return model_names; + }, + + /** + * AI Chat completion method. + * See AIChatService for more details. + */ + async complete ({ messages, stream, model, tools }) { + model = this.adapt_model(model); + + if ( model.startsWith('openrouter:') ) { + model = model.slice('openrouter:'.length); + } + + messages = await OpenAIUtil.process_input_messages(messages); + + messages.unshift({ + role: 'system', + content: this.get_system_prompt() + }) + + const completion = await this.openai.chat.completions.create({ + messages, + model: model ?? this.get_default_model(), + ...(tools ? { tools } : {}), + max_tokens: 1000, + stream, + ...(stream ? { + stream_options: { include_usage: true }, + } : {}), + }); + + return OpenAIUtil.handle_completion_output({ + usage_calculator: OpenAIUtil.create_usage_calculator({ + model_details: (await this.models_()).find(m => m.id === 'openrouter:' + model), + }), + stream, completion, + }); + } + } + } + + + /** + * 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_ () { + const axios = this.require('axios'); + + const cached_models = this.modules.kv.get(`${this.kvkey}:models`); + if ( cached_models ) { + return cached_models; + } + const resp = await axios.request({ + method: 'GET', + url: this.api_base_url + '/models', + }); + const resp_models = resp.data.data; + const coerced_models = []; + for ( const model of resp_models ) { + coerced_models.push({ + id: 'openrouter:' + model.id, + name: model.name + ' (OpenRouter)', + cost: { + currency: 'usd-cents', + tokens: 1_000_000, + input: model.pricing.prompt * 1000000, + output: model.pricing.completion * 1000000, + } + }); + } + this.modules.kv.set(`${this.kvkey}:models`, coerced_models); + return coerced_models; + } +} + +module.exports = { + OpenRouterService, +}; + diff --git a/src/backend/src/modules/puterai/PuterAIModule.js b/src/backend/src/modules/puterai/PuterAIModule.js index 0ede2391..b6fa45a1 100644 --- a/src/backend/src/modules/puterai/PuterAIModule.js +++ b/src/backend/src/modules/puterai/PuterAIModule.js @@ -103,6 +103,10 @@ class PuterAIModule extends AdvancedBase { const { GeminiService } = require('./GeminiService'); services.registerService('gemini', GeminiService); } + if ( !! config?.services?.['openrouter'] ) { + const { OpenRouterService } = require('./OpenRouterService'); + services.registerService('openrouter', OpenRouterService); + } const { AIChatService } = require('./AIChatService'); services.registerService('ai-chat', AIChatService); diff --git a/src/puter-js/src/modules/AI.js b/src/puter-js/src/modules/AI.js index ee63e252..c0aaf597 100644 --- a/src/puter-js/src/modules/AI.js +++ b/src/puter-js/src/modules/AI.js @@ -285,6 +285,9 @@ class AI{ ){ driver = 'gemini'; } + else if ( options.model.startsWith('openrouter:') ) { + driver = 'openrouter'; + } // stream flag from settings if(settings.stream !== undefined && typeof settings.stream === 'boolean'){