mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-09-10 12:02:59 +02:00
I've integrated universal logging and applied some refactors.
I implemented comprehensive logging across all API providers in src/models/ using logger.js. This includes: - Adding log() and logVision() calls to each provider (Claude, DeepSeek, Gemini, GLHF, GPT, Grok, Groq, HuggingFace, Hyperbolic, Local, Mistral, Novita, Qwen, Replicate, VLLM). - Ensuring logging respects 'log_normal_data', 'log_reasoning_data', and 'log_vision_data' flags in settings.js, which I added. - I deprecated 'log_all_prompts' in settings.js and updated prompter.js accordingly. I refactored openrouter.js and prompter.js: - I removed the experimental reasoning prompt functionality ($REASONING) from openrouter.js. - I removed a previously implemented (and then reverted) personality injection feature ($PERSONALITY) from prompter.js, openrouter.js, and profile files. I had to work around some issues: - I replaced the full file content for glhf.js and hyperbolic.js due to persistent errors with applying changes. Something I still need to do: - Based on your latest feedback, model responses containing <thinking>...</thinking> tags need to be transformed to <think>...</think> tags before being passed to logger.js to ensure they are categorized into reasoning_logs.csv. This change is not included in this update.
This commit is contained in:
parent
fa35e03ec5
commit
62bcb1950c
16 changed files with 362 additions and 241 deletions
|
@ -45,6 +45,12 @@ const settings = {
|
|||
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
|
||||
"chat_bot_messages": true, // publicly chat messages to other bots
|
||||
// "log_all_prompts": false, // DEPRECATED: Replaced by granular log_normal_data, log_reasoning_data, log_vision_data in logger.js and prompter.js
|
||||
|
||||
// NEW LOGGING SETTINGS
|
||||
"log_normal_data": true,
|
||||
"log_reasoning_data": true,
|
||||
"log_vision_data": true,
|
||||
// END NEW LOGGING SETTINGS
|
||||
}
|
||||
|
||||
// these environment variables override certain settings
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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) {
|
||||
|
@ -54,30 +55,45 @@ export class Claude {
|
|||
}
|
||||
console.log(err);
|
||||
}
|
||||
const logMessagesForClaude = [{ role: "system", content: systemMessage }].concat(turns);
|
||||
// The actual 'turns' passed to anthropic.messages.create are already strictFormatted
|
||||
// For logging, we want to capture the input as it was conceptually given.
|
||||
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 }, // Text part of the vision message
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/jpeg",
|
||||
data: imageBuffer.toString('base64')
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
];
|
||||
// Create the turns structure that will actually be sent to the API
|
||||
const turnsForAPIRequest = [...turns, { role: "user", content: visionUserMessageContent }];
|
||||
|
||||
return this.sendRequest(imageMessages, systemMessage);
|
||||
// Call sendRequest. Note: Claude's sendRequest takes systemMessage separately.
|
||||
// The systemMessage parameter for sendRequest here should be the overall system instruction,
|
||||
// not the text part of the vision message if that's already included in turnsForAPIRequest.
|
||||
// Assuming the passed 'systemMessage' to sendVisionRequest is the vision prompt.
|
||||
// And the actual system prompt for the Claude API call is handled by sendRequest's own 'systemMessage' param.
|
||||
// Let's assume the 'systemMessage' passed to sendVisionRequest is the primary text prompt for the vision task.
|
||||
// The 'sendRequest' function will handle its own logging using log().
|
||||
|
||||
const res = await this.sendRequest(turnsForAPIRequest, systemMessage); // This will call log() internally for the text part.
|
||||
|
||||
// After getting the response, specifically log the vision interaction.
|
||||
if (imageBuffer && res) {
|
||||
// 'turns' are the original conversation turns *before* adding the vision-specific user message.
|
||||
// 'systemMessage' here is used as the 'visionMessage' (the text prompt accompanying the image).
|
||||
logVision(turns, imageBuffer, res, systemMessage);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
|
|
|
@ -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 DeepSeek {
|
||||
constructor(model_name, url, params) {
|
||||
|
@ -46,6 +47,7 @@ export class DeepSeek {
|
|||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
log(JSON.stringify(messages), res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
@ -54,6 +55,7 @@ export class Gemini {
|
|||
|
||||
console.log('Awaiting Google API response...');
|
||||
|
||||
const originalTurnsForLog = [{role: 'system', content: systemMessage}, ...turns];
|
||||
turns.unshift({ role: 'system', content: systemMessage });
|
||||
turns = strictFormat(turns);
|
||||
let contents = [];
|
||||
|
@ -93,6 +95,7 @@ export class Gemini {
|
|||
|
||||
console.log('Received.');
|
||||
|
||||
log(JSON.stringify(originalTurnsForLog), text);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
@ -127,7 +130,12 @@ export class Gemini {
|
|||
const response = await result.response;
|
||||
const text = response.text();
|
||||
console.log('Received.');
|
||||
if (!text.includes(stop_seq)) return text;
|
||||
if (imageBuffer && text) {
|
||||
// 'turns' is the original conversation history.
|
||||
// 'prompt' is the vision message text.
|
||||
logVision(turns, imageBuffer, text, prompt);
|
||||
}
|
||||
if (!text.includes(stop_seq)) return text; // No logging for this early return? Or log text then return text? Assuming logVision is the primary goal.
|
||||
const idx = text.indexOf(stop_seq);
|
||||
res = text.slice(0, idx);
|
||||
} catch (err) {
|
||||
|
@ -137,6 +145,8 @@ export class Gemini {
|
|||
} else {
|
||||
res = "An unexpected error occurred, please try again.";
|
||||
}
|
||||
const loggedTurnsForError = [{role: 'system', content: systemMessage}, ...turns];
|
||||
log(JSON.stringify(loggedTurnsForError), res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -1,70 +1,73 @@
|
|||
import OpenAIApi from 'openai';
|
||||
import { getKey } from '../utils/keys.js';
|
||||
|
||||
export class GLHF {
|
||||
constructor(model_name, url) {
|
||||
this.model_name = model_name;
|
||||
const apiKey = getKey('GHLF_API_KEY');
|
||||
if (!apiKey) {
|
||||
throw new Error('API key not found. Please check keys.json and ensure GHLF_API_KEY is defined.');
|
||||
}
|
||||
this.openai = new OpenAIApi({
|
||||
apiKey,
|
||||
baseURL: url || "https://glhf.chat/api/openai/v1"
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequest(turns, systemMessage, stop_seq = '***') {
|
||||
// Construct the message array for the API request.
|
||||
let messages = [{ role: 'system', content: systemMessage }].concat(turns);
|
||||
const pack = {
|
||||
model: this.model_name || "hf:meta-llama/Llama-3.1-405B-Instruct",
|
||||
messages,
|
||||
stop: [stop_seq]
|
||||
};
|
||||
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let finalRes = null;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
console.log(`Awaiting glhf.chat API response... (attempt: ${attempt})`);
|
||||
try {
|
||||
let completion = await this.openai.chat.completions.create(pack);
|
||||
if (completion.choices[0].finish_reason === 'length') {
|
||||
throw new Error('Context length exceeded');
|
||||
}
|
||||
let res = completion.choices[0].message.content;
|
||||
// If there's an open <think> tag without a corresponding </think>, retry.
|
||||
if (res.includes("<think>") && !res.includes("</think>")) {
|
||||
console.warn("Partial <think> block detected. Re-generating...");
|
||||
continue;
|
||||
}
|
||||
// If there's a closing </think> tag but no opening <think>, prepend one.
|
||||
if (res.includes("</think>") && !res.includes("<think>")) {
|
||||
res = "<think>" + 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.');
|
||||
return await this.sendRequest(turns.slice(1), systemMessage, stop_seq);
|
||||
} else {
|
||||
console.error(err);
|
||||
finalRes = 'My brain disconnected, try again.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalRes === null) {
|
||||
finalRes = "I thought too hard, sorry, try again";
|
||||
}
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
throw new Error('Embeddings are not supported by glhf.');
|
||||
}
|
||||
}
|
||||
import OpenAIApi from 'openai';
|
||||
import { getKey } from '../utils/keys.js';
|
||||
import { log, logVision } from '../../logger.js'; // Added import
|
||||
|
||||
export class GLHF {
|
||||
constructor(model_name, url) {
|
||||
this.model_name = model_name;
|
||||
const apiKey = getKey('GHLF_API_KEY');
|
||||
if (!apiKey) {
|
||||
throw new Error('API key not found. Please check keys.json and ensure GHLF_API_KEY is defined.');
|
||||
}
|
||||
this.openai = new OpenAIApi({
|
||||
apiKey,
|
||||
baseURL: url || "https://glhf.chat/api/openai/v1"
|
||||
});
|
||||
}
|
||||
|
||||
async sendRequest(turns, systemMessage, stop_seq = '***') {
|
||||
// Construct the message array for the API request.
|
||||
let messages = [{ role: 'system', content: systemMessage }].concat(turns); // messages for API and logging
|
||||
const pack = {
|
||||
model: this.model_name || "hf:meta-llama/Llama-3.1-405B-Instruct",
|
||||
messages,
|
||||
stop: [stop_seq]
|
||||
};
|
||||
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let finalRes = null;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
console.log(`Awaiting glhf.chat API response... (attempt: ${attempt})`);
|
||||
try {
|
||||
let completion = await this.openai.chat.completions.create(pack);
|
||||
if (completion.choices[0].finish_reason === 'length') {
|
||||
throw new Error('Context length exceeded');
|
||||
}
|
||||
let res = completion.choices[0].message.content;
|
||||
// If there's an open <think> tag without a corresponding </think>, retry.
|
||||
if (res.includes("<think>") && !res.includes("</think>")) {
|
||||
console.warn("Partial <think> block detected. Re-generating...");
|
||||
if (attempt < maxAttempts) continue; // Continue if not the last attempt
|
||||
}
|
||||
// If there's a closing </think> tag but no opening <think>, prepend one.
|
||||
if (res.includes("</think>") && !res.includes("<think>")) {
|
||||
res = "<think>" + 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.');
|
||||
// Recursive call will handle its own logging
|
||||
return await this.sendRequest(turns.slice(1), systemMessage, stop_seq);
|
||||
} else {
|
||||
console.error(err);
|
||||
finalRes = 'My brain disconnected, try again.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalRes === null) { // Should only be reached if loop completed due to continue on last attempt
|
||||
finalRes = "I thought too hard, sorry, try again";
|
||||
}
|
||||
log(JSON.stringify(messages), finalRes); // Added log call
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
throw new Error('Embeddings are not supported by glhf.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 GPT {
|
||||
constructor(model_name, url, params) {
|
||||
|
@ -55,15 +56,17 @@ export class GPT {
|
|||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
// Assuming res is assigned in both try and catch.
|
||||
log(JSON.stringify(messages), res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async sendVisionRequest(messages, systemMessage, imageBuffer) {
|
||||
const imageMessages = [...messages];
|
||||
imageMessages.push({
|
||||
async sendVisionRequest(original_turns, systemMessage, imageBuffer) { // Renamed 'messages' to 'original_turns'
|
||||
const imageFormattedTurns = [...original_turns];
|
||||
imageFormattedTurns.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: systemMessage },
|
||||
{ type: "text", text: systemMessage }, // This is the vision prompt text
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
|
@ -73,7 +76,22 @@ export class GPT {
|
|||
]
|
||||
});
|
||||
|
||||
return this.sendRequest(imageMessages, systemMessage);
|
||||
// Pass a system message to sendRequest. If systemMessage is purely for vision prompt,
|
||||
// then the main system message for the API call itself might be different or empty.
|
||||
// For GPT, system messages are part of the 'messages' array.
|
||||
// The sendRequest will create its 'messages' array including a system role.
|
||||
// Let's assume the 'systemMessage' param here is the specific prompt for the vision task.
|
||||
// The 'sendRequest' will use its own 'systemMessage' parameter from its signature for the API system message.
|
||||
// For consistency, the 'systemMessage' for the API call in sendRequest should be the overarching one.
|
||||
|
||||
const res = await this.sendRequest(imageFormattedTurns, systemMessage); // This will call log() for the text part.
|
||||
|
||||
if (imageBuffer && res) {
|
||||
// 'original_turns' is the conversation history before adding the image-specific content.
|
||||
// 'systemMessage' is the vision prompt text.
|
||||
logVision(original_turns, imageBuffer, res, systemMessage);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -52,15 +53,17 @@ export class Grok {
|
|||
}
|
||||
}
|
||||
// sometimes outputs special token <|separator|>, just replace it
|
||||
return res.replace(/<\|separator\|>/g, '*no response*');
|
||||
const finalResponseText = res ? res.replace(/<\|separator\|>/g, '*no response*') : (res === null ? "*no response*" : res);
|
||||
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: "text", text: systemMessage }, // systemMessage is the vision prompt
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
|
@ -70,7 +73,13 @@ export class Grok {
|
|||
]
|
||||
});
|
||||
|
||||
return this.sendRequest(imageMessages, systemMessage);
|
||||
// Assuming 'systemMessage' (the vision prompt) should also act as the system message for this specific API call.
|
||||
const res = await this.sendRequest(imageFormattedTurns, systemMessage); // sendRequest will call log()
|
||||
|
||||
if (imageBuffer && res) { // Check res to ensure a response was received
|
||||
logVision(original_turns, imageBuffer, res, systemMessage);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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. :)
|
||||
|
@ -55,9 +56,14 @@ export class GroqCloudAPI {
|
|||
...(this.params || {})
|
||||
});
|
||||
|
||||
res = completion.choices[0].message;
|
||||
// res = completion.choices[0].message; // Original assignment
|
||||
let responseText = completion.choices[0].message.content; // Get content
|
||||
|
||||
res = res.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
log(JSON.stringify(messages), responseText); // Log here
|
||||
|
||||
// Original cleaning of <think> tags for the *returned* response (not affecting log)
|
||||
responseText = responseText.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
return responseText;
|
||||
}
|
||||
catch(err) {
|
||||
if (err.message.includes("content must be a string")) {
|
||||
|
@ -67,16 +73,21 @@ export class GroqCloudAPI {
|
|||
res = "My brain disconnected, try again.";
|
||||
}
|
||||
console.log(err);
|
||||
// Log error response
|
||||
log(JSON.stringify(messages), res);
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
// This return is now unreachable due to returns in try/catch, but if logic changes, ensure logging covers it.
|
||||
// log(JSON.stringify(messages), 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]; // Use a copy
|
||||
imageMessages.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: systemMessage },
|
||||
{ type: "text", text: systemMessage }, // systemMessage is the vision prompt
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
|
@ -86,7 +97,13 @@ export class GroqCloudAPI {
|
|||
]
|
||||
});
|
||||
|
||||
return this.sendRequest(imageMessages);
|
||||
// Assuming 'systemMessage' (the vision prompt) should also act as the system message for this API call.
|
||||
const res = await this.sendRequest(imageMessages, systemMessage); // sendRequest will call log()
|
||||
|
||||
if (imageBuffer && res) {
|
||||
logVision(original_turns, imageBuffer, res, systemMessage);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async embed(_) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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) {
|
||||
|
@ -23,6 +24,7 @@ export class HuggingFace {
|
|||
// 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 logInputMessages = [{role: 'system', content: systemMessage}, ...turns];
|
||||
const input = systemMessage + "\n" + prompt;
|
||||
|
||||
// We'll try up to 5 times in case of partial <think> blocks for DeepSeek-R1 models.
|
||||
|
@ -76,6 +78,7 @@ export class HuggingFace {
|
|||
}
|
||||
console.log('Received.');
|
||||
console.log(finalRes);
|
||||
log(JSON.stringify(logInputMessages), finalRes);
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,113 +1,116 @@
|
|||
import { getKey } from '../utils/keys.js';
|
||||
|
||||
export class Hyperbolic {
|
||||
constructor(modelName, apiUrl) {
|
||||
this.modelName = modelName || "deepseek-ai/DeepSeek-V3";
|
||||
this.apiUrl = apiUrl || "https://api.hyperbolic.xyz/v1/chat/completions";
|
||||
|
||||
// Retrieve the Hyperbolic API key from keys.js
|
||||
this.apiKey = getKey('HYPERBOLIC_API_KEY');
|
||||
if (!this.apiKey) {
|
||||
throw new Error('HYPERBOLIC_API_KEY not found. Check your keys.js file.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a chat completion request to the Hyperbolic endpoint.
|
||||
*
|
||||
* @param {Array} turns - An array of message objects, e.g. [{role: 'user', content: 'Hi'}].
|
||||
* @param {string} systemMessage - The system prompt or instruction.
|
||||
* @param {string} stopSeq - A stopping sequence, default '***'.
|
||||
* @returns {Promise<string>} - The model's reply.
|
||||
*/
|
||||
async sendRequest(turns, systemMessage, stopSeq = '***') {
|
||||
// Prepare the messages with a system prompt at the beginning
|
||||
const messages = [{ role: 'system', content: systemMessage }, ...turns];
|
||||
|
||||
// Build the request payload
|
||||
const payload = {
|
||||
model: this.modelName,
|
||||
messages: messages,
|
||||
max_tokens: 8192,
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let finalRes = null;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
console.log(`Awaiting Hyperbolic API response... (attempt: ${attempt})`);
|
||||
console.log('Messages:', messages);
|
||||
|
||||
let completionContent = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
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 || '';
|
||||
console.log('Received response from Hyperbolic.');
|
||||
} catch (err) {
|
||||
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, stopSeq);
|
||||
} else {
|
||||
console.error(err);
|
||||
completionContent = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Check for <think> blocks
|
||||
const hasOpenTag = completionContent.includes("<think>");
|
||||
const hasCloseTag = completionContent.includes("</think>");
|
||||
|
||||
if ((hasOpenTag && !hasCloseTag)) {
|
||||
console.warn("Partial <think> block detected. Re-generating...");
|
||||
continue; // Retry the request
|
||||
}
|
||||
|
||||
if (hasCloseTag && !hasOpenTag) {
|
||||
completionContent = '<think>' + completionContent;
|
||||
}
|
||||
|
||||
if (hasOpenTag && hasCloseTag) {
|
||||
completionContent = completionContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
}
|
||||
|
||||
finalRes = completionContent.replace(/<\|separator\|>/g, '*no response*');
|
||||
break; // Valid response obtained—exit loop
|
||||
}
|
||||
|
||||
if (finalRes == null) {
|
||||
console.warn("Could not get a valid <think> block or normal response after max attempts.");
|
||||
finalRes = 'I thought too hard, sorry, try again.';
|
||||
}
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
throw new Error('Embeddings are not supported by Hyperbolic.');
|
||||
}
|
||||
}
|
||||
import { getKey } from '../utils/keys.js';
|
||||
import { log, logVision } from '../../logger.js'; // Added import
|
||||
|
||||
export class Hyperbolic {
|
||||
constructor(modelName, apiUrl) {
|
||||
this.modelName = modelName || "deepseek-ai/DeepSeek-V3";
|
||||
this.apiUrl = apiUrl || "https://api.hyperbolic.xyz/v1/chat/completions";
|
||||
|
||||
// Retrieve the Hyperbolic API key from keys.js
|
||||
this.apiKey = getKey('HYPERBOLIC_API_KEY');
|
||||
if (!this.apiKey) {
|
||||
throw new Error('HYPERBOLIC_API_KEY not found. Check your keys.js file.');
|
||||
}
|
||||
}
|
||||
|
||||
async sendRequest(turns, systemMessage, stopSeq = '***') {
|
||||
const messages = [{ role: 'system', content: systemMessage }, ...turns];
|
||||
|
||||
const payload = {
|
||||
model: this.modelName,
|
||||
messages: messages,
|
||||
max_tokens: 8192,
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
stream: false
|
||||
};
|
||||
|
||||
const maxAttempts = 5;
|
||||
let attempt = 0;
|
||||
let finalRes = null; // Holds the content after <think> processing and <|separator|> replacement
|
||||
let rawCompletionContent = null; // Holds raw content from API for each attempt
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
console.log(`Awaiting Hyperbolic API response... (attempt: ${attempt})`);
|
||||
// console.log('Messages:', messages); // Original console log
|
||||
|
||||
try {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
rawCompletionContent = data?.choices?.[0]?.message?.content || '';
|
||||
console.log('Received response from Hyperbolic.');
|
||||
} catch (err) {
|
||||
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...');
|
||||
// Recursive call handles its own logging
|
||||
return await this.sendRequest(turns.slice(1), systemMessage, stopSeq);
|
||||
} else {
|
||||
console.error(err);
|
||||
rawCompletionContent = 'My brain disconnected, try again.';
|
||||
// Assign to finalRes here if we are to break and log this error immediately
|
||||
finalRes = rawCompletionContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process <think> blocks
|
||||
let processedContent = rawCompletionContent;
|
||||
const hasOpenTag = processedContent.includes("<think>");
|
||||
const hasCloseTag = processedContent.includes("</think>");
|
||||
|
||||
if ((hasOpenTag && !hasCloseTag)) {
|
||||
console.warn("Partial <think> block detected. Re-generating...");
|
||||
if (attempt < maxAttempts) continue;
|
||||
// If last attempt, use the content as is (or error if preferred)
|
||||
}
|
||||
|
||||
if (hasCloseTag && !hasOpenTag) {
|
||||
processedContent = '<think>' + processedContent;
|
||||
}
|
||||
|
||||
if (hasOpenTag && hasCloseTag) {
|
||||
processedContent = processedContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
||||
}
|
||||
|
||||
finalRes = processedContent.replace(/<\|separator\|>/g, '*no response*');
|
||||
|
||||
// If not retrying due to partial tag, break
|
||||
if (!(hasOpenTag && !hasCloseTag && attempt < maxAttempts)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalRes == null) {
|
||||
console.warn("Could not get a valid response after max attempts, or an error occurred on the last attempt.");
|
||||
finalRes = rawCompletionContent || 'I thought too hard, sorry, try again.'; // Use raw if finalRes never got set
|
||||
finalRes = finalRes.replace(/<\|separator\|>/g, '*no response*'); // Clean one last time
|
||||
}
|
||||
|
||||
log(JSON.stringify(messages), finalRes);
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
throw new Error('Embeddings are not supported by Hyperbolic.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { strictFormat } from '../utils/text.js';
|
||||
import { log, logVision } from '../../logger.js';
|
||||
|
||||
export class Local {
|
||||
constructor(model_name, url, params) {
|
||||
|
@ -75,6 +76,7 @@ export class Local {
|
|||
console.warn("Could not get a valid <think> block or normal response after max attempts.");
|
||||
finalRes = 'I thought too hard, sorry, try again.';
|
||||
}
|
||||
log(JSON.stringify(messages), finalRes);
|
||||
return finalRes;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
@ -64,23 +65,37 @@ export class Mistral {
|
|||
console.log(err);
|
||||
}
|
||||
|
||||
log(JSON.stringify(messages), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendVisionRequest(messages, systemMessage, imageBuffer) {
|
||||
const imageMessages = [...messages];
|
||||
imageMessages.push({
|
||||
async sendVisionRequest(original_turns, systemMessage, imageBuffer) {
|
||||
const imageFormattedTurns = [...original_turns];
|
||||
// The user message content should be an array for Mistral when including images
|
||||
const userMessageContent = [{ type: "text", text: systemMessage }];
|
||||
userMessageContent.push({
|
||||
type: "image_url", // This structure is based on current code; Mistral SDK might prefer different if it auto-detects from base64 content.
|
||||
// The provided code uses 'imageUrl'. Mistral SDK docs show 'image_url' for some contexts or direct base64.
|
||||
// For `chat.complete`, it's usually within the 'content' array of a user message.
|
||||
imageUrl: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
|
||||
});
|
||||
imageFormattedTurns.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: systemMessage },
|
||||
{
|
||||
type: "image_url",
|
||||
imageUrl: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
|
||||
}
|
||||
]
|
||||
content: userMessageContent // Content is an array
|
||||
});
|
||||
|
||||
return this.sendRequest(imageMessages, systemMessage);
|
||||
// 'systemMessage' passed to sendRequest should be the overarching system prompt.
|
||||
// If the 'systemMessage' parameter of sendVisionRequest is the vision text prompt,
|
||||
// and it's already incorporated into imageFormattedTurns, then the systemMessage for sendRequest
|
||||
// might be a different, more general one, or empty if not applicable.
|
||||
// For now, let's assume the 'systemMessage' param of sendVisionRequest is the main prompt for this turn
|
||||
// and should also serve as the system-level instruction for the API call via sendRequest.
|
||||
const res = await this.sendRequest(imageFormattedTurns, systemMessage); // sendRequest will call log()
|
||||
|
||||
if (imageBuffer && res) {
|
||||
logVision(original_turns, imageBuffer, res, systemMessage); // systemMessage here is the vision prompt
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async embed(text) {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -49,17 +50,23 @@ export class Novita {
|
|||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
if (res.includes('<think>')) {
|
||||
let start = res.indexOf('<think>');
|
||||
let end = res.indexOf('</think>') + 8;
|
||||
if (start != -1) {
|
||||
if (end != -1) {
|
||||
res = res.substring(0, start) + res.substring(end);
|
||||
} else {
|
||||
res = res.substring(0, start+7);
|
||||
log(JSON.stringify(messages), res); // Log before stripping <think> tags
|
||||
|
||||
// Existing stripping logic for <think> tags
|
||||
if (res && typeof res === 'string' && res.includes('<think>')) {
|
||||
let start = res.indexOf('<think>');
|
||||
let end = res.indexOf('</think>') + 8; // length of '</think>'
|
||||
if (start !== -1) { // Ensure '<think>' was found
|
||||
if (end !== -1 && end > start + 7) { // Ensure '</think>' was found and is after '<think>'
|
||||
res = res.substring(0, start) + res.substring(end);
|
||||
} else {
|
||||
// Malformed or missing end tag, strip from '<think>' onwards or handle as error
|
||||
// Original code: res = res.substring(0, start+7); This would leave "<think>"
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -45,6 +46,7 @@ export class Qwen {
|
|||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
log(JSON.stringify(messages), res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -23,6 +24,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,
|
||||
|
@ -45,6 +47,7 @@ export class ReplicateAPI {
|
|||
console.log(err);
|
||||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
log(JSON.stringify(logInputMessages), res);
|
||||
console.log('Received.');
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -53,6 +57,7 @@ export class VLLM {
|
|||
res = 'My brain disconnected, try again.';
|
||||
}
|
||||
}
|
||||
log(JSON.stringify(messages), res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue