diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..965a1c2 --- /dev/null +++ b/logger.js @@ -0,0 +1,401 @@ +// --- START OF FILE logger.js --- + +import { writeFileSync, mkdirSync, existsSync, appendFileSync, readFileSync } from 'fs'; +import { join } from 'path'; +import settings from './settings.js'; // Import settings +import path from 'path'; // Needed for path operations + +// --- Configuration --- +const LOGS_DIR = './logs'; +const VISION_DATASET_DIR = join(LOGS_DIR, 'vision_dataset'); // HuggingFace dataset format +const VISION_IMAGES_DIR = join(VISION_DATASET_DIR, 'images'); // Images subdirectory + +// --- Log File Paths --- +const REASONING_LOG_FILE = join(LOGS_DIR, 'reasoning_logs.csv'); +const NORMAL_LOG_FILE = join(LOGS_DIR, 'normal_logs.csv'); +const VISION_METADATA_FILE = join(VISION_DATASET_DIR, 'metadata.jsonl'); // HF metadata format + +// --- Log Headers --- +const TEXT_LOG_HEADER = 'input,output\n'; + +// --- Log Counters --- +let logCounts = { + normal: 0, + reasoning: 0, + vision: 0, + total: 0, + skipped_disabled: 0, + skipped_empty: 0, + vision_images_saved: 0, +}; + +// --- Helper Functions --- +function ensureDirectoryExistence(dirPath) { + if (!existsSync(dirPath)) { + try { + mkdirSync(dirPath, { recursive: true }); + console.log(`[Logger] Created directory: ${dirPath}`); + } catch (error) { + console.error(`[Logger] Error creating directory ${dirPath}:`, error); + return false; + } + } + return true; +} + +function countLogEntries(logFile) { + if (!existsSync(logFile)) return 0; + try { + const data = readFileSync(logFile, 'utf8'); + const lines = data.split('\n').filter(line => line.trim()); + // Check if the first line looks like a header before subtracting + const hasHeader = lines.length > 0 && lines[0].includes(','); + return Math.max(0, hasHeader ? lines.length - 1 : lines.length); + } catch (err) { + console.error(`[Logger] Error reading log file ${logFile}:`, err); + return 0; + } +} + + +function ensureLogFile(logFile, header) { + if (!ensureDirectoryExistence(path.dirname(logFile))) return false; // Ensure parent dir exists + + if (!existsSync(logFile)) { + try { + writeFileSync(logFile, header); + console.log(`[Logger] Created log file: ${logFile}`); + } catch (error) { + console.error(`[Logger] Error creating log file ${logFile}:`, error); + return false; + } + } else { + try { + const content = readFileSync(logFile, 'utf-8'); + const headerLine = header.split('\n')[0]; + // If file is empty or header doesn't match, overwrite/create header + if (!content.trim() || !content.startsWith(headerLine)) { + // Attempt to prepend header if file has content but wrong/no header + if(content.trim() && !content.startsWith(headerLine)) { + console.warn(`[Logger] Log file ${logFile} seems to be missing or has an incorrect header. Prepending correct header.`); + writeFileSync(logFile, header + content); + } else { + // File is empty or correctly headed, just ensure header is there + writeFileSync(logFile, header); + } + console.log(`[Logger] Ensured header in log file: ${logFile}`); + } + } catch (error) { + console.error(`[Logger] Error checking/writing header for log file ${logFile}:`, error); + // Proceed cautiously, maybe log an error and continue? + } + } + return true; +} + + +function writeToLogFile(logFile, csvEntry) { + try { + appendFileSync(logFile, csvEntry); + // console.log(`[Logger] Logged data to ${logFile}`); // Keep console less noisy + } catch (error) { + console.error(`[Logger] Error writing to CSV log file ${logFile}:`, error); + } +} + +// --- Auto-Detection for Log Type (Based on Response Content) --- +function determineLogType(response) { + // Reasoning check: needs ... but ignore the specific 'undefined' placeholder + const isReasoning = response.includes('') && response.includes('') && !response.includes('\nundefined'); + + if (isReasoning) { + return 'reasoning'; + } else { + return 'normal'; + } +} + +function sanitizeForCsv(value) { + if (typeof value !== 'string') { + value = String(value); + } + // Escape double quotes by doubling them and enclose the whole string in double quotes + return `"${value.replace(/"/g, '""')}"`; +} + +// Helper function to clean reasoning markers from input +function cleanReasoningMarkers(input) { + if (typeof input !== 'string') { + return input; + } + + // Remove /think and /no_think markers + return input.replace(/\/think/g, '').replace(/\/no_think/g, '').trim(); +} + +// --- Main Logging Function (for text-based input/output) --- +export function log(input, response) { + const trimmedInputStr = input ? (typeof input === 'string' ? input.trim() : JSON.stringify(input)) : ""; + const trimmedResponse = response ? String(response).trim() : ""; // Ensure response is a string + + // Clean reasoning markers from input before logging + const cleanedInput = cleanReasoningMarkers(trimmedInputStr); + + // Basic filtering + if (!cleanedInput && !trimmedResponse) { + logCounts.skipped_empty++; + return; + } + if (cleanedInput === trimmedResponse) { + logCounts.skipped_empty++; + return; + } + // Avoid logging common error messages that aren't useful training data + const errorMessages = [ + "My brain disconnected, try again.", + "My brain just kinda stopped working. Try again.", + "I thought too hard, sorry, try again.", + "*no response*", + "No response received.", + "No response data.", + "Failed to send", // Broader match + "Error:", // Broader match + "Vision is only supported", + "Context length exceeded", + "Image input modality is not enabled", + "An unexpected error occurred", + // Add more generic errors/placeholders as needed + ]; + // Also check for responses that are just the input repeated (sometimes happens with errors) + if (errorMessages.some(err => trimmedResponse.includes(err)) || trimmedResponse === cleanedInput) { + logCounts.skipped_empty++; + // console.warn(`[Logger] Skipping log due to error/placeholder/repeat: "${trimmedResponse.substring(0, 70)}..."`); + return; + } + + + const logType = determineLogType(trimmedResponse); + let logFile; + let header; + let settingFlag; + + switch (logType) { + case 'reasoning': + logFile = REASONING_LOG_FILE; + header = TEXT_LOG_HEADER; + settingFlag = settings.log_reasoning_data; + break; + case 'normal': + default: + logFile = NORMAL_LOG_FILE; + header = TEXT_LOG_HEADER; + settingFlag = settings.log_normal_data; + break; + } + + // Check if logging for this type is enabled + if (!settingFlag) { + logCounts.skipped_disabled++; + return; + } + + // Ensure directory and file exist + if (!ensureLogFile(logFile, header)) return; // ensureLogFile now checks parent dir too + + // Prepare the CSV entry using the sanitizer with cleaned input + const safeInput = sanitizeForCsv(cleanedInput); + const safeResponse = sanitizeForCsv(trimmedResponse); + const csvEntry = `${safeInput},${safeResponse}\n`; + + // Write to the determined log file + writeToLogFile(logFile, csvEntry); + + // Update counts + logCounts[logType]++; + logCounts.total++; // Total here refers to text logs primarily + + // Display summary periodically (based on total text logs) + if (logCounts.normal + logCounts.reasoning > 0 && (logCounts.normal + logCounts.reasoning) % 20 === 0) { + printSummary(); + } +} + +// --- Enhanced Vision Logging Function for HuggingFace Dataset Format --- +export function logVision(conversationHistory, imageBuffer, response, visionMessage = null) { + if (!settings.log_vision_data) { + logCounts.skipped_disabled++; + return; + } + + const trimmedResponse = response ? String(response).trim() : ""; + + if (!conversationHistory || conversationHistory.length === 0 || !trimmedResponse || !imageBuffer) { + logCounts.skipped_empty++; + return; + } + + // Filter out error messages + const errorMessages = [ + "My brain disconnected, try again.", + "My brain just kinda stopped working. Try again.", + "I thought too hard, sorry, try again.", + "*no response*", + "No response received.", + "No response data.", + "Failed to send", + "Error:", + "Vision is only supported", + "Context length exceeded", + "Image input modality is not enabled", + "An unexpected error occurred", + ]; + + if (errorMessages.some(err => trimmedResponse.includes(err))) { + logCounts.skipped_empty++; + return; + } + + // Ensure directories exist + if (!ensureDirectoryExistence(VISION_DATASET_DIR)) return; + if (!ensureDirectoryExistence(VISION_IMAGES_DIR)) return; + + try { + // Generate unique filename for the image + const timestamp = Date.now(); + const randomSuffix = Math.random().toString(36).substring(2, 8); + const imageFilename = `vision_${timestamp}_${randomSuffix}.jpg`; + const imagePath = join(VISION_IMAGES_DIR, imageFilename); + const relativeImagePath = `images/${imageFilename}`; // Relative path for metadata + + // Save the image + writeFileSync(imagePath, imageBuffer); + logCounts.vision_images_saved++; + + // Extract the actual message sent with the image + // This is typically the vision prompt/instruction + let inputMessage = visionMessage; + if (!inputMessage && conversationHistory.length > 0) { + // Try to get the last user message or system message + const lastMessage = conversationHistory[conversationHistory.length - 1]; + if (typeof lastMessage.content === 'string') { + inputMessage = lastMessage.content; + } else if (Array.isArray(lastMessage.content)) { + // Find text content in the message + const textContent = lastMessage.content.find(c => c.type === 'text'); + inputMessage = textContent ? textContent.text : ''; + } + } + + // Fallback to conversation history if no specific message + if (!inputMessage) { + inputMessage = formatConversationInput(conversationHistory); + } + + // Create metadata entry in JSONL format for HuggingFace + const metadataEntry = { + file_name: relativeImagePath, + text: inputMessage, + response: trimmedResponse, + timestamp: timestamp + }; + + // Append to metadata JSONL file + const jsonlLine = JSON.stringify(metadataEntry) + '\n'; + appendFileSync(VISION_METADATA_FILE, jsonlLine); + + logCounts.vision++; + logCounts.total++; + + // Display summary periodically + if (logCounts.vision > 0 && logCounts.vision % 10 === 0) { + printSummary(); + } + + } catch (error) { + console.error(`[Logger] Error logging vision data:`, error); + } +} + +// Helper function to format conversation history as fallback +function formatConversationInput(conversationHistory) { + if (!conversationHistory || conversationHistory.length === 0) return ''; + + const formattedHistory = []; + + for (const turn of conversationHistory) { + const formattedTurn = { + role: turn.role || 'user', + content: [] + }; + + // Handle different content formats + if (typeof turn.content === 'string') { + formattedTurn.content.push({ + type: 'text', + text: turn.content + }); + } else if (Array.isArray(turn.content)) { + // Already in the correct format + formattedTurn.content = turn.content; + } else if (turn.content && typeof turn.content === 'object') { + // Convert object to array format + if (turn.content.text) { + formattedTurn.content.push({ + type: 'text', + text: turn.content.text + }); + } + if (turn.content.image) { + formattedTurn.content.push({ + type: 'image', + image: turn.content.image + }); + } + } + + formattedHistory.push(formattedTurn); + } + + return JSON.stringify(formattedHistory); +} + +function printSummary() { + const totalStored = logCounts.normal + logCounts.reasoning + logCounts.vision; + console.log('\n' + '='.repeat(60)); + console.log('LOGGER SUMMARY'); + console.log('-'.repeat(60)); + console.log(`Normal logs stored: ${logCounts.normal}`); + console.log(`Reasoning logs stored: ${logCounts.reasoning}`); + console.log(`Vision logs stored: ${logCounts.vision} (Images saved: ${logCounts.vision_images_saved})`); + console.log(`Skipped (disabled): ${logCounts.skipped_disabled}`); + console.log(`Skipped (empty/err): ${logCounts.skipped_empty}`); + console.log('-'.repeat(60)); + console.log(`Total logs stored: ${totalStored}`); + console.log('='.repeat(60) + '\n'); +} + +// Initialize counts at startup +function initializeCounts() { + logCounts.normal = countLogEntries(NORMAL_LOG_FILE); + logCounts.reasoning = countLogEntries(REASONING_LOG_FILE); + logCounts.vision = countVisionEntries(VISION_METADATA_FILE); + // Total count will be accumulated during runtime + console.log(`[Logger] Initialized log counts: Normal=${logCounts.normal}, Reasoning=${logCounts.reasoning}, Vision=${logCounts.vision}`); +} + +function countVisionEntries(metadataFile) { + if (!existsSync(metadataFile)) return 0; + try { + const data = readFileSync(metadataFile, 'utf8'); + const lines = data.split('\n').filter(line => line.trim()); + return lines.length; + } catch (err) { + console.error(`[Logger] Error reading vision metadata file ${metadataFile}:`, err); + return 0; + } +} + +// Initialize counts at startup +initializeCounts(); + +// --- END OF FILE logger.js --- \ No newline at end of file diff --git a/settings.js b/settings.js index 421ec56..f6713ae 100644 --- a/settings.js +++ b/settings.js @@ -46,7 +46,11 @@ const settings = { "verbose_commands": true, // show full command syntax "narrate_behavior": true, // chat simple automatic actions ('Picking up item!') "chat_bot_messages": true, // publicly chat messages to other bots - "log_all_prompts": false, // log ALL prompts to file + + "log_normal_data": false, + "log_reasoning_data": false, + "log_vision_data": false, + } // these environment variables override certain settings @@ -71,8 +75,5 @@ if (process.env.MAX_MESSAGES) { if (process.env.NUM_EXAMPLES) { settings.num_examples = process.env.NUM_EXAMPLES; } -if (process.env.LOG_ALL) { - settings.log_all_prompts = process.env.LOG_ALL; -} export default settings; diff --git a/src/models/claude.js b/src/models/claude.js index 16789da..50e5627 100644 --- a/src/models/claude.js +++ b/src/models/claude.js @@ -1,18 +1,16 @@ import Anthropic from '@anthropic-ai/sdk'; import { strictFormat } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; export class Claude { constructor(model_name, url, params) { this.model_name = model_name; this.params = params || {}; - let config = {}; if (url) config.baseURL = url; - config.apiKey = getKey('ANTHROPIC_API_KEY'); - this.anthropic = new Anthropic(config); this.supportsRawImageInput = true; } @@ -71,8 +69,7 @@ export class Claude { if (!this.params.max_tokens) { if (this.params.thinking?.budget_tokens) { - this.params.max_tokens = this.params.thinking.budget_tokens + 1000; - // max_tokens must be greater than thinking.budget_tokens + this.params.max_tokens = this.params.thinking.budget_tokens + 1000; // max_tokens must be greater } else { this.params.max_tokens = 4096; } @@ -83,9 +80,7 @@ export class Claude { messages: messages, // messages array is now potentially modified with image data ...(this.params || {}) }); - console.log('Received.') - // get first content of type text const textContent = resp.content.find(content => content.type === 'text'); if (textContent) { res = textContent.text; @@ -93,8 +88,7 @@ export class Claude { console.warn('No text content found in the response.'); res = 'No response from Claude.'; } - } - catch (err) { + } catch (err) { if (err.message.includes("does not support image input")) { res = "Vision is only supported by certain models."; } else { @@ -102,30 +96,34 @@ export class Claude { } console.log(err); } + const logMessagesForClaude = [{ role: "system", content: systemMessage }].concat(turns); + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(logMessagesForClaude), res); return res; } async sendVisionRequest(turns, systemMessage, imageBuffer) { - const imageMessages = [...turns]; - imageMessages.push({ - role: "user", - content: [ - { - type: "text", - text: systemMessage - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/jpeg", - data: imageBuffer.toString('base64') - } + const visionUserMessageContent = [ + { type: "text", text: systemMessage }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: imageBuffer.toString('base64') } - ] - }); + } + ]; + const turnsForAPIRequest = [...turns, { role: "user", content: visionUserMessageContent }]; - return this.sendRequest(imageMessages, systemMessage); + const res = await this.sendRequest(turnsForAPIRequest, systemMessage); + + if (imageBuffer && res) { + logVision(turns, imageBuffer, res, systemMessage); + } + return res; } async embed(text) { diff --git a/src/models/deepseek.js b/src/models/deepseek.js index 53793b2..ae0e552 100644 --- a/src/models/deepseek.js +++ b/src/models/deepseek.js @@ -1,17 +1,15 @@ import OpenAIApi from 'openai'; import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class DeepSeek { constructor(model_name, url, params) { this.model_name = model_name; this.params = params; - let config = {}; - config.baseURL = url || 'https://api.deepseek.com'; config.apiKey = getKey('DEEPSEEK_API_KEY'); - this.openai = new OpenAIApi(config); this.supportsRawImageInput = true; // Assuming DeepSeek models used can support this OpenAI-like format } @@ -78,18 +76,17 @@ export class DeepSeek { stop: stop_seq, ...(this.params || {}) }; - let res = null; try { - console.log('Awaiting deepseek api response...'); - // console.log('Formatted Messages for API:', JSON.stringify(messages, null, 2)); + + console.log('Awaiting deepseek api response...') + let completion = await this.openai.chat.completions.create(pack); if (completion.choices[0].finish_reason == 'length') throw new Error('Context length exceeded'); console.log('Received.'); res = completion.choices[0].message.content; - } - catch (err) { + } catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); @@ -98,6 +95,10 @@ export class DeepSeek { res = 'My brain disconnected, try again.'; } } + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); return res; } @@ -105,6 +106,3 @@ export class DeepSeek { throw new Error('Embeddings are not supported by Deepseek.'); } } - - - diff --git a/src/models/gemini.js b/src/models/gemini.js index a205753..3036ef5 100644 --- a/src/models/gemini.js +++ b/src/models/gemini.js @@ -1,6 +1,7 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; import { toSinglePrompt, strictFormat } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; export class Gemini { constructor(model_name, url, params) { @@ -8,28 +9,12 @@ export class Gemini { this.params = params; this.url = url; this.safetySettings = [ - { - "category": "HARM_CATEGORY_DANGEROUS", - "threshold": "BLOCK_NONE", - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE", - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE", - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE", - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE", - }, + { "category": "HARM_CATEGORY_DANGEROUS", "threshold": "BLOCK_NONE" }, + { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE" }, + { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE" }, + { "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE" }, + { "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" }, ]; - this.genAI = new GoogleGenerativeAI(getKey('GEMINI_API_KEY')); this.supportsRawImageInput = true; } @@ -41,20 +26,12 @@ export class Gemini { // systemInstruction does not work bc google is trash }; if (this.url) { - model = this.genAI.getGenerativeModel( - modelConfig, - { baseUrl: this.url }, - { safetySettings: this.safetySettings } - ); + model = this.genAI.getGenerativeModel(modelConfig, { baseUrl: this.url }, { safetySettings: this.safetySettings }); } else { - model = this.genAI.getGenerativeModel( - modelConfig, - { safetySettings: this.safetySettings } - ); + model = this.genAI.getGenerativeModel(modelConfig, { safetySettings: this.safetySettings }); } - console.log('Awaiting Google API response...'); - + const originalTurnsForLog = [{role: 'system', content: systemMessage}, ...turns]; turns.unshift({ role: 'system', content: systemMessage }); turns = strictFormat(turns); let contents = []; @@ -85,22 +62,12 @@ export class Gemini { const result = await model.generateContent({ contents, - generationConfig: { - ...(this.params || {}) - } + generationConfig: { ...(this.params || {}) } }); const response = await result.response; let text; - - // Handle "thinking" models since they smart if (this.model_name && this.model_name.includes("thinking")) { - if ( - response.candidates && - response.candidates.length > 0 && - response.candidates[0].content && - response.candidates[0].content.parts && - response.candidates[0].content.parts.length > 1 - ) { + if (response.candidates?.length > 0 && response.candidates[0].content?.parts?.length > 1) { text = response.candidates[0].content.parts[1].text; } else { console.warn("Unexpected response structure for thinking model:", response); @@ -109,34 +76,22 @@ export class Gemini { } else { text = response.text(); } - console.log('Received.'); - + if (typeof text === 'string') { + text = text.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(originalTurnsForLog), text); return text; } async sendVisionRequest(turns, systemMessage, imageBuffer) { let model; if (this.url) { - model = this.genAI.getGenerativeModel( - { model: this.model_name || "gemini-1.5-flash" }, - { baseUrl: this.url }, - { safetySettings: this.safetySettings } - ); + model = this.genAI.getGenerativeModel({ model: this.model_name || "gemini-1.5-flash" }, { baseUrl: this.url }, { safetySettings: this.safetySettings }); } else { - model = this.genAI.getGenerativeModel( - { model: this.model_name || "gemini-1.5-flash" }, - { safetySettings: this.safetySettings } - ); + model = this.genAI.getGenerativeModel({ model: this.model_name || "gemini-1.5-flash" }, { safetySettings: this.safetySettings }); } - - const imagePart = { - inlineData: { - data: imageBuffer.toString('base64'), - mimeType: 'image/jpeg' - } - }; - + const imagePart = { inlineData: { data: imageBuffer.toString('base64'), mimeType: 'image/jpeg' } }; const stop_seq = '***'; const prompt = toSinglePrompt(turns, systemMessage, stop_seq, 'model'); let res = null; @@ -146,6 +101,9 @@ export class Gemini { const response = await result.response; const text = response.text(); console.log('Received.'); + if (imageBuffer && text) { + logVision(turns, imageBuffer, text, prompt); + } if (!text.includes(stop_seq)) return text; const idx = text.indexOf(stop_seq); res = text.slice(0, idx); @@ -156,6 +114,11 @@ export class Gemini { } else { res = "An unexpected error occurred, please try again."; } + const loggedTurnsForError = [{role: 'system', content: systemMessage}, ...turns]; + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(loggedTurnsForError), res); } return res; } @@ -163,16 +126,10 @@ export class Gemini { async embed(text) { let model; if (this.url) { - model = this.genAI.getGenerativeModel( - { model: "text-embedding-004" }, - { baseUrl: this.url } - ); + model = this.genAI.getGenerativeModel({ model: "text-embedding-004" }, { baseUrl: this.url }); } else { - model = this.genAI.getGenerativeModel( - { model: "text-embedding-004" } - ); + model = this.genAI.getGenerativeModel({ model: "text-embedding-004" }); } - const result = await model.embedContent(text); return result.embedding.values; } diff --git a/src/models/glhf.js b/src/models/glhf.js index c7cbe0e..17fbea1 100644 --- a/src/models/glhf.js +++ b/src/models/glhf.js @@ -1,5 +1,6 @@ import OpenAIApi from 'openai'; import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; export class GLHF { constructor(model_name, url) { @@ -48,11 +49,13 @@ export class GLHF { continue; } // If there's a closing tag but no opening , prepend one. + if (res.includes("") && !res.includes("")) { res = "" + res; } finalRes = res.replace(/<\|separator\|>/g, '*no response*'); break; // Valid response obtained. + } catch (err) { if ((err.message === 'Context length exceeded' || err.code === 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); @@ -68,6 +71,11 @@ export class GLHF { if (finalRes === null) { finalRes = "I thought too hard, sorry, try again"; } + + if (typeof finalRes === 'string') { + finalRes = finalRes.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), finalRes); return finalRes; } diff --git a/src/models/gpt.js b/src/models/gpt.js index 154516d..4fd72fa 100644 --- a/src/models/gpt.js +++ b/src/models/gpt.js @@ -1,21 +1,18 @@ import OpenAIApi from 'openai'; import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class GPT { constructor(model_name, url, params) { this.model_name = model_name; this.params = params; - let config = {}; if (url) config.baseURL = url; - if (hasKey('OPENAI_ORG_ID')) config.organization = getKey('OPENAI_ORG_ID'); - config.apiKey = getKey('OPENAI_API_KEY'); - this.openai = new OpenAIApi(config); this.supportsRawImageInput = true; } @@ -65,19 +62,17 @@ export class GPT { if (this.model_name.includes('o1')) { delete pack.stop; } - let res = null; - try { + console.log('Awaiting openai api response from model', this.model_name); - // console.log('Formatted Messages for API:', JSON.stringify(messages, null, 2)); + let completion = await this.openai.chat.completions.create(pack); if (completion.choices[0].finish_reason == 'length') throw new Error('Context length exceeded'); console.log('Received.'); res = completion.choices[0].message.content; - } - catch (err) { + } catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); return await this.sendRequest(turns.slice(1), systemMessage, stop_seq); @@ -89,25 +84,32 @@ export class GPT { res = 'My brain disconnected, try again.'; } } + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); return res; } - async sendVisionRequest(messages, systemMessage, imageBuffer) { - const imageMessages = [...messages]; - imageMessages.push({ + async sendVisionRequest(original_turns, systemMessage, imageBuffer) { + const imageFormattedTurns = [...original_turns]; + imageFormattedTurns.push({ role: "user", content: [ { type: "text", text: systemMessage }, { type: "image_url", - image_url: { - url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` - } + image_url: { url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` } } ] }); - return this.sendRequest(imageMessages, systemMessage); + const res = await this.sendRequest(imageFormattedTurns, systemMessage); + + if (imageBuffer && res) { + logVision(original_turns, imageBuffer, res, systemMessage); + } + return res; } async embed(text) { @@ -120,8 +122,4 @@ export class GPT { }); return embedding.data[0].embedding; } - } - - - diff --git a/src/models/grok.js b/src/models/grok.js index 8afd643..79c956d 100644 --- a/src/models/grok.js +++ b/src/models/grok.js @@ -1,5 +1,6 @@ import OpenAIApi from 'openai'; import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; // xAI doesn't supply a SDK for their models, but fully supports OpenAI and Anthropic SDKs export class Grok { @@ -7,15 +8,12 @@ export class Grok { this.model_name = model_name; this.url = url; this.params = params; - let config = {}; if (url) config.baseURL = url; else config.baseURL = "https://api.x.ai/v1" - config.apiKey = getKey('XAI_API_KEY'); - this.openai = new OpenAIApi(config); // Direct image data in sendRequest is not supported by this wrapper for standard chat. // Grok may have specific vision capabilities, but this method assumes text-only. @@ -27,25 +25,21 @@ export class Grok { console.warn(`[Grok] Warning: imageData provided to sendRequest, but this method in grok.js does not support direct image data embedding for model ${this.model_name}. The image will be ignored.`); } let messages = [{'role': 'system', 'content': systemMessage}].concat(turns); - const pack = { model: this.model_name || "grok-beta", messages, stop: [stop_seq], ...(this.params || {}) }; - let res = null; try { console.log('Awaiting xai api response...') - ///console.log('Messages:', messages); let completion = await this.openai.chat.completions.create(pack); if (completion.choices[0].finish_reason == 'length') throw new Error('Context length exceeded'); console.log('Received.') res = completion.choices[0].message.content; - } - catch (err) { + } catch (err) { if ((err.message == 'Context length exceeded' || err.code == 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with shorter context.'); return await this.sendRequest(turns.slice(1), systemMessage, imageData, stop_seq); @@ -58,31 +52,36 @@ export class Grok { } } // sometimes outputs special token <|separator|>, just replace it - return res.replace(/<\|separator\|>/g, '*no response*'); + let finalResponseText = res ? res.replace(/<\|separator\|>/g, '*no response*') : (res === null ? "*no response*" : res); + if (typeof finalResponseText === 'string') { + finalResponseText = finalResponseText.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), finalResponseText); + return finalResponseText; } - async sendVisionRequest(messages, systemMessage, imageBuffer) { - const imageMessages = [...messages]; - imageMessages.push({ + async sendVisionRequest(original_turns, systemMessage, imageBuffer) { + const imageFormattedTurns = [...original_turns]; + imageFormattedTurns.push({ role: "user", content: [ { type: "text", text: systemMessage }, { type: "image_url", - image_url: { - url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` - } + image_url: { url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` } } ] }); - return this.sendRequest(imageMessages, systemMessage); + const res = await this.sendRequest(imageFormattedTurns, systemMessage); + + if (imageBuffer && res) { + logVision(original_turns, imageBuffer, res, systemMessage); + } + return res; } async embed(text) { throw new Error('Embeddings are not supported by Grok.'); } } - - - diff --git a/src/models/groq.js b/src/models/groq.js index 61b17a0..fefa8c7 100644 --- a/src/models/groq.js +++ b/src/models/groq.js @@ -1,14 +1,13 @@ import Groq from 'groq-sdk' import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; // THIS API IS NOT TO BE CONFUSED WITH GROK! // Go to grok.js for that. :) // Umbrella class for everything under the sun... That GroqCloud provides, that is. export class GroqCloudAPI { - constructor(model_name, url, params) { - this.model_name = model_name; this.url = url; this.params = params || {}; @@ -18,7 +17,6 @@ export class GroqCloudAPI { delete this.params.tools; // This is just a bit of future-proofing in case we drag Mindcraft in that direction. - // I'm going to do a sneaky ReplicateAPI theft for a lot of this, aren't I? if (this.url) console.warn("Groq Cloud has no implementation for custom URLs. Ignoring provided URL."); @@ -35,9 +33,7 @@ export class GroqCloudAPI { } // Construct messages array let messages = [{"role": "system", "content": systemMessage}].concat(turns); - let res = null; - try { console.log("Awaiting Groq response..."); @@ -47,7 +43,6 @@ export class GroqCloudAPI { this.params.max_completion_tokens = this.params.max_tokens; delete this.params.max_tokens; } - if (!this.params.max_completion_tokens) { this.params.max_completion_tokens = 4000; } @@ -60,11 +55,15 @@ export class GroqCloudAPI { ...(this.params || {}) }); - res = completion.choices[0].message; - - res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); - } - catch(err) { + let responseText = completion.choices[0].message.content; + if (typeof responseText === 'string') { + responseText = responseText.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), responseText); + // Original cleaning of tags for the *returned* response (not affecting log) + responseText = responseText.replace(/[\s\S]*?<\/think>/g, '').trim(); + return responseText; + } catch(err) { if (err.message.includes("content must be a string")) { res = "Vision is only supported by certain models."; } else { @@ -72,27 +71,33 @@ export class GroqCloudAPI { res = "My brain disconnected, try again."; } console.log(err); + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); + return res; } - return res; } - async sendVisionRequest(messages, systemMessage, imageBuffer) { - const imageMessages = messages.filter(message => message.role !== 'system'); + async sendVisionRequest(original_turns, systemMessage, imageBuffer) { + const imageMessages = [...original_turns]; imageMessages.push({ role: "user", content: [ { type: "text", text: systemMessage }, { type: "image_url", - image_url: { - url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` - } + image_url: { url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` } } ] }); - - // sendVisionRequest formats its own message array; sendRequest here should not process new imageData. - return this.sendRequest(imageMessages, systemMessage, null, stop_seq); + + const res = await this.sendRequest(imageMessages, systemMessage); + + if (imageBuffer && res) { + logVision(original_turns, imageBuffer, res, systemMessage); + } + return res; } async embed(_) { diff --git a/src/models/huggingface.js b/src/models/huggingface.js index cc0202e..cbc3abc 100644 --- a/src/models/huggingface.js +++ b/src/models/huggingface.js @@ -1,18 +1,16 @@ import { toSinglePrompt } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; import { HfInference } from "@huggingface/inference"; +import { log, logVision } from '../../logger.js'; export class HuggingFace { constructor(model_name, url, params) { - // Remove 'huggingface/' prefix if present this.model_name = model_name.replace('huggingface/', ''); this.url = url; this.params = params; - if (this.url) { console.warn("Hugging Face doesn't support custom urls!"); } - this.huggingface = new HfInference(getKey('HUGGINGFACE_API_KEY')); // Direct image data in sendRequest is not supported by this wrapper. // HuggingFace Inference API has other methods for vision tasks. @@ -24,14 +22,11 @@ export class HuggingFace { console.warn(`[HuggingFace] Warning: imageData provided to sendRequest, but this method in huggingface.js does not support direct image data embedding for model ${this.model_name}. The image will be ignored.`); } const stop_seq = '***'; - // Build a single prompt from the conversation turns const prompt = toSinglePrompt(turns, null, stop_seq); - // Fallback model if none was provided const model_name = this.model_name || 'meta-llama/Meta-Llama-3-8B'; - // Combine system message with the prompt - const input = systemMessage + "\n" + prompt; - - // We'll try up to 5 times in case of partial blocks for DeepSeek-R1 models. + const logInputMessages = [{role: 'system', content: systemMessage}, ...turns]; + const input = systemMessage + " +" + prompt; const maxAttempts = 5; let attempt = 0; let finalRes = null; @@ -41,7 +36,6 @@ export class HuggingFace { console.log(`Awaiting Hugging Face API response... (model: ${model_name}, attempt: ${attempt})`); let res = ''; try { - // Consume the streaming response chunk by chunk for await (const chunk of this.huggingface.chatCompletionStream({ model: model_name, messages: [{ role: "user", content: input }], @@ -52,36 +46,32 @@ export class HuggingFace { } catch (err) { console.log(err); res = 'My brain disconnected, try again.'; - // Break out immediately; we only retry when handling partial tags. break; } - // If the model is DeepSeek-R1, check for mismatched blocks. - const hasOpenTag = res.includes(""); - const hasCloseTag = res.includes(""); - - // If there's a partial mismatch, warn and retry the entire request. - if ((hasOpenTag && !hasCloseTag)) { - console.warn("Partial block detected. Re-generating..."); - continue; - } - - // If both tags are present, remove the block entirely. - if (hasOpenTag && hasCloseTag) { - res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); - } + const hasOpenTag = res.includes(""); + const hasCloseTag = res.includes(""); + if ((hasOpenTag && !hasCloseTag)) { + console.warn("Partial block detected. Re-generating..."); + if (attempt < maxAttempts) continue; + } + if (hasOpenTag && hasCloseTag) { + res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); + } finalRes = res; - break; // Exit loop if we got a valid response. + break; } - // If no valid response was obtained after max attempts, assign a fallback. if (finalRes == null) { - console.warn("Could not get a valid block or normal response after max attempts."); + console.warn("Could not get a valid response after max attempts."); finalRes = 'I thought too hard, sorry, try again.'; } console.log('Received.'); - console.log(finalRes); + if (typeof finalRes === 'string') { + finalRes = finalRes.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(logInputMessages), finalRes); return finalRes; } diff --git a/src/models/hyperbolic.js b/src/models/hyperbolic.js index 257755a..076c812 100644 --- a/src/models/hyperbolic.js +++ b/src/models/hyperbolic.js @@ -1,4 +1,5 @@ import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; export class Hyperbolic { constructor(modelName, apiUrl) { @@ -9,6 +10,7 @@ export class Hyperbolic { if (!this.apiKey) { throw new Error('HYPERBOLIC_API_KEY not found. Check your keys.js file.'); } + // Direct image data in sendRequest is not supported by this wrapper. this.supportsRawImageInput = false; } @@ -38,14 +40,13 @@ export class Hyperbolic { const maxAttempts = 5; let attempt = 0; let finalRes = null; + let rawCompletionContent = null; while (attempt < maxAttempts) { attempt++; console.log(`Awaiting Hyperbolic API response... (attempt: ${attempt})`); - // console.log('Messages:', messages); // Avoid logging full messages in production if sensitive - - let completionContent = null; + try { const response = await fetch(this.apiUrl, { method: 'POST', @@ -65,55 +66,57 @@ export class Hyperbolic { throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`); } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } const data = await response.json(); if (data?.choices?.[0]?.finish_reason === 'length') { throw new Error('Context length exceeded'); } - completionContent = data?.choices?.[0]?.message?.content || ''; + rawCompletionContent = data?.choices?.[0]?.message?.content || ''; console.log('Received response from Hyperbolic.'); } catch (err) { - if ( - (err.message.includes('Context length exceeded') || err.code === 'context_length_exceeded') && // Adjusted to check includes for message - turns.length > 1 - ) { + if ((err.message === 'Context length exceeded' || err.code === 'context_length_exceeded') && turns.length > 1) { console.log('Context length exceeded, trying again with a shorter context...'); - return await this.sendRequest(turns.slice(1), systemMessage, imageData, stopSeq); // Pass imageData + return await this.sendRequest(turns.slice(1), systemMessage, imageData, stopSeq); } else { console.error(err); - completionContent = 'My brain disconnected, try again.'; - // No break here, let it be set and then break after the think block logic + rawCompletionContent = 'My brain disconnected, try again.'; + finalRes = rawCompletionContent; + break; } } - const hasOpenTag = completionContent.includes(""); - const hasCloseTag = completionContent.includes(""); + let processedContent = rawCompletionContent; + const hasOpenTag = processedContent.includes(""); + const hasCloseTag = processedContent.includes(""); if ((hasOpenTag && !hasCloseTag)) { console.warn("Partial block detected. Re-generating..."); - if (attempt >= maxAttempts) { // If this was the last attempt - finalRes = "I thought too hard and got stuck in a loop, sorry, try again."; - break; - } - continue; + if (attempt < maxAttempts) continue; } - if (hasCloseTag && !hasOpenTag) { - completionContent = '' + completionContent; + processedContent = '' + processedContent; } - if (hasOpenTag && hasCloseTag) { - completionContent = completionContent.replace(/[\s\S]*?<\/think>/g, '').trim(); + processedContent = processedContent.replace(/[\s\S]*?<\/think>/g, '').trim(); + } + finalRes = processedContent.replace(/<\|separator\|>/g, '*no response*'); + if (!(hasOpenTag && !hasCloseTag && attempt < maxAttempts)) { + break; } - - finalRes = completionContent.replace(/<\|separator\|>/g, '*no response*'); - break; } - if (finalRes == null) { // This condition might be hit if all attempts fail and continue - console.warn("Could not get a valid block or normal response after max attempts."); - finalRes = 'I thought too hard, sorry, try again.'; + if (finalRes == null) { + finalRes = rawCompletionContent || 'I thought too hard, sorry, try again.'; + finalRes = finalRes.replace(/<\|separator\|>/g, '*no response*'); } + + if (typeof finalRes === 'string') { + finalRes = finalRes.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), finalRes); return finalRes; } diff --git a/src/models/local.js b/src/models/local.js index cf6a808..c199df8 100644 --- a/src/models/local.js +++ b/src/models/local.js @@ -1,4 +1,5 @@ import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class Local { constructor(model_name, url, params) { @@ -38,7 +39,6 @@ export class Local { } } - // We'll attempt up to 5 times for models with deepseek-r1-esk reasoning if the tags are mismatched. const maxAttempts = 5; let attempt = 0; let finalRes = null; @@ -48,14 +48,14 @@ export class Local { console.log(`Awaiting local response... (model: ${model}, attempt: ${attempt})`); let res = null; try { - res = await this.send(this.chat_endpoint, { + let apiResponse = await this.send(this.chat_endpoint, { model: model, messages: messages, stream: false, ...(this.params || {}) }); - if (res) { - res = res['message']['content']; + if (apiResponse) { + res = apiResponse['message']['content']; } else { res = 'No response data.'; } @@ -67,38 +67,33 @@ export class Local { console.log(err); res = 'My brain disconnected, try again.'; } - } - // If the model name includes "deepseek-r1" or "Andy-3.5-reasoning", then handle the block. - const hasOpenTag = res.includes(""); - const hasCloseTag = res.includes(""); - - // If there's a partial mismatch, retry to get a complete response. - if ((hasOpenTag && !hasCloseTag)) { - console.warn("Partial block detected. Re-generating..."); - continue; - } - - // If is present but is not, prepend - if (hasCloseTag && !hasOpenTag) { - res = '' + res; - } - // Changed this so if the model reasons, using and but doesn't start the message with , ges prepended to the message so no error occur. - - // If both tags appear, remove them (and everything inside). - if (hasOpenTag && hasCloseTag) { - res = res.replace(/[\s\S]*?<\/think>/g, ''); - } + const hasOpenTag = res.includes(""); + const hasCloseTag = res.includes(""); + if ((hasOpenTag && !hasCloseTag)) { + console.warn("Partial block detected. Re-generating..."); + if (attempt < maxAttempts) continue; + } + if (hasCloseTag && !hasOpenTag) { + res = '' + res; + } + if (hasOpenTag && hasCloseTag) { + res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); + } finalRes = res; - break; // Exit the loop if we got a valid response. + break; } if (finalRes == null) { - console.warn("Could not get a valid block or normal response after max attempts."); + console.warn("Could not get a valid response after max attempts."); finalRes = 'I thought too hard, sorry, try again.'; } + if (typeof finalRes === 'string') { + finalRes = finalRes.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), finalRes); return finalRes; } diff --git a/src/models/mistral.js b/src/models/mistral.js index 762b7ec..58f0e46 100644 --- a/src/models/mistral.js +++ b/src/models/mistral.js @@ -1,19 +1,17 @@ import { Mistral as MistralClient } from '@mistralai/mistralai'; import { getKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class Mistral { #client; - constructor(model_name, url, params) { this.model_name = model_name; this.params = params; if (typeof url === "string") { console.warn("Mistral does not support custom URL's, ignoring!"); - } - if (!getKey("MISTRAL_API_KEY")) { throw new Error("Mistral API Key missing, make sure to set MISTRAL_API_KEY in settings.json") } @@ -26,13 +24,8 @@ export class Mistral { this.supportsRawImageInput = false; // Standard chat completions may not support raw images for all models. - // Prevents the following code from running when model not specified - if (typeof this.model_name === "undefined") return; - - // get the model name without the "mistral" or "mistralai" prefix - // e.g "mistral/mistral-large-latest" -> "mistral-large-latest" - if (typeof model_name.split("/")[1] !== "undefined") { - this.model_name = model_name.split("/")[1]; + if (typeof this.model_name === "string" && typeof this.model_name.split("/")[1] !== "undefined") { + this.model_name = this.model_name.split("/")[1]; } } @@ -43,22 +36,16 @@ export class Mistral { } let result; - + const model = this.model_name || "mistral-large-latest"; + const messages = [{ role: "system", content: systemMessage }]; + messages.push(...strictFormat(turns)); try { - const model = this.model_name || "mistral-large-latest"; - - const messages = [ - { role: "system", content: systemMessage } - ]; - messages.push(...strictFormat(turns)); - console.log('Awaiting mistral api response...') const response = await this.#client.chat.complete({ model, messages, ...(this.params || {}) }); - result = response.choices[0].message.content; } catch (err) { if (err.message.includes("A request containing images has been given to a model which does not have the 'vision' capability.")) { @@ -68,24 +55,28 @@ export class Mistral { } console.log(err); } - + if (typeof result === 'string') { + result = result.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), result); return result; } - async sendVisionRequest(messages, systemMessage, imageBuffer) { - const imageMessages = [...messages]; - imageMessages.push({ - role: "user", - content: [ - { type: "text", text: systemMessage }, - { - type: "image_url", - imageUrl: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` - } - ] + async sendVisionRequest(original_turns, systemMessage, imageBuffer) { + const imageFormattedTurns = [...original_turns]; + const userMessageContent = [{ type: "text", text: systemMessage }]; + userMessageContent.push({ + type: "image_url", + imageUrl: `data:image/jpeg;base64,${imageBuffer.toString('base64')}` }); + imageFormattedTurns.push({ role: "user", content: userMessageContent }); - return this.sendRequest(imageMessages, systemMessage); + const res = await this.sendRequest(imageFormattedTurns, systemMessage); + + if (imageBuffer && res) { + logVision(original_turns, imageBuffer, res, systemMessage); + } + return res; } async embed(text) { diff --git a/src/models/novita.js b/src/models/novita.js index 65a5eab..a07bfe1 100644 --- a/src/models/novita.js +++ b/src/models/novita.js @@ -1,6 +1,7 @@ import OpenAIApi from 'openai'; import { getKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; // llama, mistral export class Novita { @@ -54,17 +55,26 @@ export class Novita { res = 'My brain disconnected, try again.'; } } - if (res.includes('')) { - let start = res.indexOf(''); - let end = res.indexOf('') + 8; - if (start != -1) { - if (end != -1) { - res = res.substring(0, start) + res.substring(end); - } else { - res = res.substring(0, start+7); + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); // Log transformed res + + // Existing stripping logic for tags + if (res && typeof res === 'string' && res.includes('')) { + let start = res.indexOf(''); + let end = res.indexOf('') + 8; // length of '' + if (start !== -1) { // Ensure '' was found + if (end !== -1 && end > start + 7) { // Ensure '' was found and is after '' + res = res.substring(0, start) + res.substring(end); + } else { + // Malformed or missing end tag, strip from '' onwards or handle as error + // Original code: res = res.substring(0, start+7); This would leave "" + // Let's assume we strip from start if end is not valid. + res = res.substring(0, start); + } } - } - res = res.trim(); + res = res.trim(); } return res; } diff --git a/src/models/openrouter.js b/src/models/openrouter.js index 8b44966..838f4a3 100644 --- a/src/models/openrouter.js +++ b/src/models/openrouter.js @@ -1,22 +1,18 @@ import OpenAIApi from 'openai'; import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class OpenRouter { constructor(model_name, url) { this.model_name = model_name; - let config = {}; config.baseURL = url || 'https://openrouter.ai/api/v1'; - const apiKey = getKey('OPENROUTER_API_KEY'); if (!apiKey) { console.error('Error: OPENROUTER_API_KEY not found. Make sure it is set properly.'); } - - // Pass the API key to OpenAI compatible Api - config.apiKey = apiKey; - + config.apiKey = apiKey; this.openai = new OpenAIApi(config); // OpenRouter is a router; individual models might support vision. // This generic sendRequest does not format for vision. Use sendVisionRequest or specific model logic. @@ -30,32 +26,79 @@ export class OpenRouter { let messages = [{ role: 'system', content: systemMessage }, ...turns]; messages = strictFormat(messages); - // Choose a valid model from openrouter.ai (for example, "openai/gpt-4o") const pack = { model: this.model_name, messages, - stop: stop_seq + include_reasoning: true, + // stop: stop_seq // Commented out since some API providers on Openrouter do not support a stop sequence, such as Grok 3 }; - let res = null; - try { - console.log('Awaiting openrouter api response...'); - let completion = await this.openai.chat.completions.create(pack); - if (!completion?.choices?.[0]) { - console.error('No completion or choices returned:', completion); - return 'No response received.'; + const maxAttempts = 5; + let attempt = 0; + let finalRes = null; + + while (attempt < maxAttempts) { + attempt++; + console.info(`Awaiting openrouter API response... (attempt: ${attempt})`); + let res = null; + try { + let completion = await this.openai.chat.completions.create(pack); + if (!completion?.choices?.[0]) { + console.error('No completion or choices returned:', completion); + return 'No response received.'; + } + + const logMessages = [{ role: "system", content: processedSystemMessage }].concat(turns); + + if (completion.choices[0].finish_reason === 'length') { + throw new Error('Context length exceeded'); + } + + if (completion.choices[0].message.reasoning) { + try{ + const reasoning = '\n' + completion.choices[0].message.reasoning + '\n'; + const content = completion.choices[0].message.content; + + // --- VISION LOGGING --- + if (visionImageBuffer) { + logVision(turns, visionImageBuffer, reasoning + "\n" + content, visionMessage); + } else { + log(JSON.stringify(logMessages), reasoning + "\n" + content); + } + res = content; + } catch {} + } else { + try { + res = completion.choices[0].message.content; + if (visionImageBuffer) { + logVision(turns, visionImageBuffer, res, visionMessage); + } else { + log(JSON.stringify(logMessages), res); + } + } catch { + console.warn("Unable to log due to unknown error!"); + } + } + // Trim blocks from the final response if present. + if (res && res.includes("") && res.includes("")) { + res = res.replace(/[\s\S]*?<\/think>/g, '').trim(); + } + + console.info('Received.'); + } catch (err) { + console.error('Error while awaiting response:', err); + res = 'My brain disconnected, try again.'; } - if (completion.choices[0].finish_reason === 'length') { - throw new Error('Context length exceeded'); - } - console.log('Received.'); - res = completion.choices[0].message.content; - } catch (err) { - console.error('Error while awaiting response:', err); - // If the error indicates a context-length problem, we can slice the turns array, etc. - res = 'My brain disconnected, try again.'; + + finalRes = res; + break; // Exit loop once a valid response is obtained. } - return res; + + if (finalRes == null) { + console.warn("Could not get a valid block or normal response after max attempts."); + finalRes = 'I thought too hard, sorry, try again.'; + } + return finalRes; } async sendVisionRequest(messages, systemMessage, imageBuffer) { @@ -76,9 +119,10 @@ export class OpenRouter { // sendVisionRequest formats its own message array; sendRequest here should not process new imageData. // Pass systemMessage and stop_seq as originally intended by sendRequest. return this.sendRequest(imageMessages, systemMessage, null, stop_seq); + } async embed(text) { throw new Error('Embeddings are not supported by Openrouter.'); } -} \ No newline at end of file +} diff --git a/src/models/prompter.js b/src/models/prompter.js index 1da0a8c..9b4b70f 100644 --- a/src/models/prompter.js +++ b/src/models/prompter.js @@ -465,8 +465,26 @@ export class Prompter { } async _saveLog(prompt, messages, generation, tag) { - if (!settings.log_all_prompts) - return; + // NEW LOGIC STARTS + switch (tag) { + case 'conversation': + case 'coding': // Assuming coding logs fall under normal data + case 'memSaving': + if (!settings.log_normal_data) return; + break; + // Add case for 'vision' if prompter.js starts logging vision prompts/responses via _saveLog + // case 'vision': + // if (!settings.log_vision_data) return; + // break; + default: + // If it's an unknown tag, perhaps log it if general logging is on, or ignore. + // For safety, let's assume if it's not specified, it doesn't get logged unless a general flag is on. + // However, the goal is to use specific flags. So, if a new tag appears, this logic should be updated. + // For now, if it doesn't match known tags that map to a setting, it won't log. + return; + } + // NEW LOGIC ENDS + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); let logEntry; let task_id = this.agent.task.task_id; diff --git a/src/models/qwen.js b/src/models/qwen.js index d3d7abd..f37a4ef 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -1,6 +1,7 @@ import OpenAIApi from 'openai'; import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class Qwen { constructor(model_name, url, params) { @@ -81,6 +82,10 @@ export class Qwen { res = 'My brain disconnected, try again.'; } } + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); return res; } diff --git a/src/models/replicate.js b/src/models/replicate.js index 92979b9..e0f1c16 100644 --- a/src/models/replicate.js +++ b/src/models/replicate.js @@ -1,6 +1,7 @@ import Replicate from 'replicate'; import { toSinglePrompt } from '../utils/text.js'; import { getKey } from '../utils/keys.js'; +import { log, logVision } from '../../logger.js'; // llama, mistral export class ReplicateAPI { @@ -29,6 +30,7 @@ export class ReplicateAPI { const prompt = toSinglePrompt(turns, null, stop_seq); let model_name = this.model_name || 'meta/meta-llama-3-70b-instruct'; + const logInputMessages = [{role: 'system', content: systemMessage}, ...turns]; const input = { prompt, system_prompt: systemMessage, @@ -51,6 +53,10 @@ export class ReplicateAPI { console.log(err); res = 'My brain disconnected, try again.'; } + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(logInputMessages), res); console.log('Received.'); return res; } diff --git a/src/models/vllm.js b/src/models/vllm.js index d5aae34..894269a 100644 --- a/src/models/vllm.js +++ b/src/models/vllm.js @@ -1,9 +1,13 @@ // This code uses Dashscope and HTTP to ensure the latest support for the Qwen model. // Qwen is also compatible with the OpenAI API format; +// This code uses Dashscope and HTTP to ensure the latest support for the Qwen model. +// Qwen is also compatible with the OpenAI API format; + import OpenAIApi from 'openai'; import { getKey, hasKey } from '../utils/keys.js'; import { strictFormat } from '../utils/text.js'; +import { log, logVision } from '../../logger.js'; export class VLLM { constructor(model_name, url) { @@ -59,6 +63,10 @@ export class VLLM { res = 'My brain disconnected, try again.'; } } + if (typeof res === 'string') { + res = res.replace(//g, '').replace(/<\/thinking>/g, ''); + } + log(JSON.stringify(messages), res); return res; }