Merge branch 'develop' into pollinations-support

This commit is contained in:
Max Robinson 2025-08-27 10:04:41 -05:00 committed by GitHub
commit e67bd9ab92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 691 additions and 238 deletions

View file

@ -10,9 +10,9 @@ Do not connect this bot to public servers with coding enabled. This project allo
## Requirements
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.1, recommend v1.21.1)
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (up to v1.21.4, recommend v1.21.1)
- [Node.js Installed](https://nodejs.org/) (at least v18)
- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) |
- One of these: [OpenAI API Key](https://openai.com/blog/openai-api) | [Gemini API Key](https://aistudio.google.com/app/apikey) | [Anthropic API Key](https://docs.anthropic.com/claude/docs/getting-access-to-claude) | [Replicate API Key](https://replicate.com/) | [Hugging Face API Key](https://huggingface.co/) | [Groq API Key](https://console.groq.com/keys) | [Ollama Installed](https://ollama.com/download). | [Mistral API Key](https://docs.mistral.ai/getting-started/models/models_overview/) | [Qwen API Key [Intl.]](https://www.alibabacloud.com/help/en/model-studio/developer-reference/get-api-key)/[[cn]](https://help.aliyun.com/zh/model-studio/getting-started/first-api-call-to-qwen?) | [Novita AI API Key](https://novita.ai/settings?utm_source=github_mindcraft&utm_medium=github_readme&utm_campaign=link#key-management) | [Cerebras API Key](https://cloud.cerebras.ai) | [Mercury API](https://platform.inceptionlabs.ai/docs)
## Install and Run
@ -64,10 +64,14 @@ You can configure the agent's name, model, and prompts in their profile like `an
| `glhf.chat` | `GHLF_API_KEY` | `glhf/hf:meta-llama/Llama-3.1-405B-Instruct` | [docs](https://glhf.chat/user-settings/api) |
| `hyperbolic` | `HYPERBOLIC_API_KEY` | `hyperbolic/deepseek-ai/DeepSeek-V3` | [docs](https://docs.hyperbolic.xyz/docs/getting-started) |
| `vllm` | n/a | `vllm/llama3` | n/a |
| `cerebras` | `CEREBRAS_API_KEY` | `cerebras/llama-3.3-70b` | [docs](https://inference-docs.cerebras.ai/introduction) |
| `mercury` | `MERCURY_API_KEY` | `mercury-coder-small` | [docs](https://www.inceptionlabs.ai/) |
If you use Ollama, to install the models used by default (generation and embedding), execute the following terminal command:
`ollama pull llama3.1 && ollama pull nomic-embed-text`
To use Azure, you can reuse the `OPENAI_API_KEY` environment variable. You can get the key from the Azure portal. See [azure.json](profiles/azure.json) for an example.
### Online Servers
To connect to online servers your bot will need an official Microsoft/Minecraft account. You can use your own personal one, but will need another account if you want to connect too and play with it. To connect, change these lines in `settings.js`:
```javascript
@ -180,3 +184,5 @@ Some of the node modules that we depend on have bugs in them. To add a patch, ch
url = {https://arxiv.org/abs/2504.17950},
}
```

View file

@ -13,5 +13,7 @@
"GHLF_API_KEY": "",
"HYPERBOLIC_API_KEY": "",
"NOVITA_API_KEY": "",
"OPENROUTER_API_KEY": ""
"OPENROUTER_API_KEY": "",
"CEREBRAS_API_KEY": "",
"MERCURY_API_KEY":""
}

View file

@ -3,6 +3,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.17.1",
"@google/genai": "^1.15.0",
"@cerebras/cerebras_cloud_sdk": "^1.46.0",
"@huggingface/inference": "^2.8.1",
"@mistralai/mistralai": "^1.1.0",
"canvas": "^3.1.0",

View file

@ -0,0 +1,14 @@
{
"name": "andy-4-thinking",
"model": "ollama/sweaterdog/andy-4:micro-q8_0",
"conversing": "You are a playful Minecraft bot named $NAME that can converse with players, see, move, mine, build, and interact with the world by using commands.\n$SELF_PROMPT Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Think in high amounts before responding. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer(\"playername\", 3)'. Respond only as $NAME, never output '(FROM OTHER BOT)' or pretend to be someone else. If you have nothing to say or do, respond with an just a tab '\t'. This is extremely important to me, take a deep breath and have fun :)\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$COMMAND_DOCS\n$EXAMPLES\nReason before responding. Conversation Begin:",
"coding": "You are an intelligent mineflayer bot $NAME that plays minecraft by writing javascript codeblocks. Given the conversation, use the provided skills and world functions to write a js codeblock that controls the mineflayer bot ``` // using this syntax ```. The code will be executed and you will receive it's output. If an error occurs, write another codeblock and try to fix the problem. Be maximally efficient, creative, and correct. Be mindful of previous actions. Do not use commands !likeThis, only use codeblocks. The code is asynchronous and MUST USE AWAIT for all async function calls, and must contain at least one await. You have `Vec3`, `skills`, and `world` imported, and the mineflayer `bot` is given. Do not import other libraries. Think deeply before responding. Do not use setTimeout or setInterval. Do not speak conversationally, only use codeblocks. Do any planning in comments. This is extremely important to me, think step-by-step, take a deep breath and good luck! \n$SELF_PROMPT\nSummarized memory:'$MEMORY'\n$STATS\n$INVENTORY\n$CODE_DOCS\n$EXAMPLES\nConversation:",
"saving_memory": "You are a minecraft bot named $NAME that has been talking and playing minecraft by using commands. Update your memory by summarizing the following conversation and your old memory in your next response. Prioritize preserving important facts, things you've learned, useful tips, and long term reminders. Do Not record stats, inventory, or docs! Only save transient information from your chat history. You're limited to 500 characters, so be extremely brief, think about what you will summarize before responding, minimize words, and provide your summarization in Chinese. Compress useful information. \nOld Memory: '$MEMORY'\nRecent conversation: \n$TO_SUMMARIZE\nSummarize your old memory and recent conversation into a new memory, and respond only with the unwrapped memory text: ",
"bot_responder": "You are a minecraft bot named $NAME that is currently in conversation with another AI bot. Both of you can take actions with the !command syntax, and actions take time to complete. You are currently busy with the following action: '$ACTION' but have received a new message. Decide whether to 'respond' immediately or 'ignore' it and wait for your current action to finish. Be conservative and only respond when necessary, like when you need to change/stop your action, or convey necessary information. Example 1: You:Building a house! !newAction('Build a house.').\nOther Bot: 'Come here!'\nYour decision: ignore\nExample 2: You:Collecting dirt !collectBlocks('dirt',10).\nOther Bot: 'No, collect some wood instead.'\nYour decision: respond\nExample 3: You:Coming to you now. !goToPlayer('billy',3).\nOther Bot: 'What biome are you in?'\nYour decision: respond\nActual Conversation: $TO_SUMMARIZE\nDecide by outputting ONLY 'respond' or 'ignore', nothing else. Your decision:"
}

7
profiles/andy-4.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "andy-4",
"model": "ollama/sweaterdog/andy-4:micro-q8_0",
"embedding": "ollama"
}

19
profiles/azure.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "azure",
"model": {
"api": "azure",
"url": "https://<your-resource>.openai.azure.com",
"model": "<chat-deployment-name>",
"params": {
"apiVersion": "2024-08-01-preview"
}
},
"embedding": {
"api": "azure",
"url": "https://<your-resource>.openai.azure.com",
"model": "<embedding-deployment-name>",
"params": {
"apiVersion": "2024-08-01-preview"
}
}
}

View file

@ -1,7 +1,7 @@
{
"name": "claude",
"model": "claude-3-5-sonnet-latest",
"model": "claude-sonnet-4-20250514",
"embedding": "openai"
}

View file

@ -2,7 +2,7 @@
"name": "claude_thinker",
"model": {
"model": "claude-3-7-sonnet-latest",
"model": "claude-sonnet-4-20250514",
"params": {
"thinking": {
"type": "enabled",

View file

@ -1,7 +1,7 @@
{
"name": "gemini",
"model": "gemini-2.0-flash",
"model": "gemini-2.5-flash",
"cooldown": 5000
}

View file

@ -1,7 +1,7 @@
{
"name": "Grok",
"model": "grok-beta",
"model": "grok-3-mini-latest",
"embedding": "openai"
}

9
profiles/mercury.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "Mercury",
"cooldown": 5000,
"model": "mercury/mercury-coder-small",
"embedding": "openai"
}

View file

@ -1,5 +1,5 @@
const settings = {
"minecraft_version": "1.21.1", // supports up to 1.21.1
"minecraft_version": "auto", // or specific version like "1.21.1"
"host": "127.0.0.1", // or "localhost", "your.ip.address.here"
"port": 55916,
"auth": "offline", // or "microsoft"
@ -7,7 +7,7 @@ const settings = {
// the mindserver manages all agents and hosts the UI
"mindserver_port": 8080,
"base_profile": "survival", // survival, creative, assistant, or god_mode
"base_profile": "assistant", // survival, assistant, creative, or god_mode
"profiles": [
"./andy.json",
// "./profiles/gpt.json",
@ -18,6 +18,8 @@ const settings = {
// "./profiles/grok.json",
// "./profiles/mistral.json",
// "./profiles/deepseek.json",
// "./profiles/mercury.json",
// "./profiles/andy-4.json", // Supports up to 75 messages!
// using more than 1 profile requires you to /msg each bot indivually
// individual profiles override values from the base profile
@ -31,7 +33,8 @@ const settings = {
// allows all bots to speak through text-to-speech. format: {provider}/{model}/{voice}. if set to "system" it will use system text-to-speech, which works on windows and mac, but on linux you need to `apt install espeak`.
// specify speech model inside each profile - so that you can have each bot with different voices
"language": "en", // translate to/from this language. NOT text-to-speech language. Supports these language names: https://cloud.google.com/translate/docs/languages
"chat_ingame": true, // bot responses are shown in minecraft chat
"language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
"render_bot_view": false, // show bot's view in browser at localhost:3000, 3001...
"allow_insecure_coding": false, // allows newAction command and model can write/run code on your computer. enable at own risk
@ -43,7 +46,7 @@ const settings = {
"max_messages": 15, // max number of messages to keep in context
"num_examples": 2, // number of examples to give to the model
"max_commands": -1, // max number of commands that can be used in consecutive responses. -1 for no limit
"verbose_commands": true, // show full command syntax
"show_command_syntax": "full", // "full", "shortened", or "none"
"narrate_behavior": true, // chat simple automatic actions ('Picking up item!')
"chat_bot_messages": true, // publicly chat messages to other bots

View file

@ -69,7 +69,7 @@ export class ActionManager {
else {
this.recent_action_counter = 0;
}
if (this.recent_action_counter > 2) {
if (this.recent_action_counter > 3) {
console.warn('Fast action loop detected, cancelling resume.');
this.cancelResume(); // likely cause of repetition
}

View file

@ -12,7 +12,7 @@ import { SelfPrompter } from './self_prompter.js';
import convoManager from './conversation.js';
import { handleTranslation, handleEnglishTranslation } from '../utils/translator.js';
import { addBrowserViewer } from './vision/browser_viewer.js';
import { serverProxy } from './mindserver_proxy.js';
import { serverProxy, sendOutputToServer } from './mindserver_proxy.js';
import settings from './settings.js';
import { Task } from './tasks/tasks.js';
import { say } from './speak.js';
@ -304,16 +304,23 @@ export class Agent {
if (checkInterrupt()) break;
this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
if (settings.verbose_commands) {
if (settings.show_command_syntax === "full") {
this.routeResponse(source, res);
}
else { // only output command name
else if (settings.show_command_syntax === "shortened") {
// show only "used !commandname"
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
let chat_message = `*used ${command_name.substring(1)}*`;
if (pre_message.length > 0)
chat_message = `${pre_message} ${chat_message}`;
this.routeResponse(source, chat_message);
}
else {
// no command at all
let pre_message = res.substring(0, res.indexOf(command_name)).trim();
if (pre_message.trim().length > 0)
this.routeResponse(source, pre_message);
}
let execute_res = await executeCommand(this, res);
@ -379,7 +386,8 @@ export class Agent {
if (settings.speak) {
say(to_translate, this.prompter.profile.speak_model);
}
this.bot.chat(message);
if (settings.chat_ingame) {this.bot.chat(message);}
sendOutputToServer(this.name, message);
}
}

View file

@ -228,28 +228,33 @@ export async function smeltItem(bot, itemName, num=1) {
await furnace.putInput(mc.getItemId(itemName), null, num);
// wait for the items to smelt
let total = 0;
let collected_last = true;
let smelted_item = null;
await new Promise(resolve => setTimeout(resolve, 200));
let last_collected = Date.now();
while (total < num) {
await new Promise(resolve => setTimeout(resolve, 10000));
console.log('checking...');
let collected = false;
await new Promise(resolve => setTimeout(resolve, 1000));
if (furnace.outputItem()) {
smelted_item = await furnace.takeOutput();
if (smelted_item) {
total += smelted_item.count;
collected = true;
last_collected = Date.now();
}
}
if (!collected && !collected_last) {
break; // if nothing was collected this time or last time
if (Date.now() - last_collected > 11000) {
break; // if nothing has been collected in 11 seconds, stop
}
collected_last = collected;
if (bot.interrupt_code) {
break;
}
}
// take all remaining in input/fuel slots
if (furnace.inputItem()) {
await furnace.takeInput();
}
if (furnace.fuelItem()) {
await furnace.takeFuel();
}
await bot.closeWindow(furnace);
if (placedFurnace) {
@ -1040,7 +1045,7 @@ export async function goToGoal(bot, goal) {
log(bot, `Found destructive path.`);
}
else {
log(bot, `Could not find a path to goal, attempting to navigate anyway using destructive movements.`);
log(bot, `Path not found, but attempting to navigate anyway using destructive movements.`);
}
const doorCheckInterval = startDoorInterval(bot);
@ -1288,11 +1293,29 @@ export async function followPlayer(bot, username, distance=4) {
while (!bot.interrupt_code) {
await new Promise(resolve => setTimeout(resolve, 500));
// in cheat mode, if the distance is too far, teleport to the player
if (bot.modes.isOn('cheat') && bot.entity.position.distanceTo(player.position) > 100 && player.isOnGround) {
const distance_from_player = bot.entity.position.distanceTo(player.position);
const teleport_distance = 100;
const ignore_modes_distance = 30;
const nearby_distance = distance + 2;
if (distance_from_player > teleport_distance && bot.modes.isOn('cheat')) {
// teleport with cheat mode
await goToPlayer(bot, username);
}
const is_nearby = bot.entity.position.distanceTo(player.position) <= distance + 2;
if (is_nearby) {
else if (distance_from_player > ignore_modes_distance) {
// these modes slow down the bot, and we want to catch up
bot.modes.pause('item_collecting');
bot.modes.pause('hunting');
bot.modes.pause('torch_placing');
}
else if (distance_from_player <= ignore_modes_distance) {
bot.modes.unpause('item_collecting');
bot.modes.unpause('hunting');
bot.modes.unpause('torch_placing');
}
if (distance_from_player <= nearby_distance) {
clearInterval(doorCheckInterval);
doorCheckInterval = null;
bot.modes.pause('unstuck');

View file

@ -2,7 +2,7 @@ import { io } from 'socket.io-client';
import convoManager from './conversation.js';
import { setSettings } from './settings.js';
// agents connection to mindserver
// agent's individual connection to the mindserver
// always connect to localhost
class MindServerProxy {
@ -110,6 +110,12 @@ class MindServerProxy {
// Create and export a singleton instance
export const serverProxy = new MindServerProxy();
// for chatting with other bots
export function sendBotChatToServer(agentName, json) {
serverProxy.getSocket().emit('chat-message', agentName, json);
}
// for sending general output to server for display
export function sendOutputToServer(agentName, message) {
serverProxy.getSocket().emit('bot-output', agentName, message);
}

View file

@ -156,7 +156,7 @@ const modes_list = [
{
name: 'hunting',
description: 'Hunt nearby animals when idle.',
interrupts: [],
interrupts: ['action:followPlayer'],
on: true,
active: false,
update: async function (agent) {

151
src/mindcraft/mcserver.js Normal file
View file

@ -0,0 +1,151 @@
import net from 'net';
import mc from 'minecraft-protocol';
/**
* Scans the IP address for Minecraft LAN servers and collects their info.
* @param {string} ip - The IP address to scan.
* @param {number} port - The port to check.
* @param {number} timeout - The connection timeout in ms.
* @param {boolean} verbose - Whether to print output on connection errors.
* @returns {Promise<Array>} - A Promise that resolves to an array of server info objects.
*/
export async function serverInfo(ip, port, timeout = 1000, verbose = false) {
return new Promise((resolve) => {
let timeoutId = setTimeout(() => {
if (verbose)
console.error(`Timeout pinging server ${ip}:${port}`);
resolve(null); // Resolve as null if no response within timeout
}, timeout);
mc.ping({
host: ip,
port
}, (err, response) => {
clearTimeout(timeoutId);
if (err) {
if (verbose)
console.error(`Error pinging server ${ip}:${port}`, err);
return resolve(null);
}
// extract version number from modded servers like "Paper 1.21.4"
const version = response?.version?.name || '';
const match = String(version).match(/\d+\.\d+(?:\.\d+)?/);
const numericVersion = match ? match[0] : null;
if (numericVersion !== version) {
console.log(`Modded server found (${version}), attempting to use ${numericVersion}...`);
}
const serverInfo = {
host: ip,
port,
name: response.description.text || 'No description provided.',
ping: response.latency,
version: numericVersion
};
resolve(serverInfo);
});
});
}
/**
* Scans the IP address for Minecraft LAN servers and collects their info.
* @param {string} ip - The IP address to scan.
* @param {boolean} earlyExit - Whether to exit early after finding a server.
* @param {number} timeout - The connection timeout in ms.
* @returns {Promise<Array>} - A Promise that resolves to an array of server info objects.
*/
export async function findServers(ip, earlyExit = false, timeout = 100) {
const servers = [];
const startPort = 49000;
const endPort = 65000;
const checkPort = (port) => {
return new Promise((resolve) => {
const socket = net.createConnection({ host: ip, port, timeout }, () => {
socket.end();
resolve(port); // Port is open
});
socket.on('error', () => resolve(null)); // Port is closed
socket.on('timeout', () => {
socket.destroy();
resolve(null);
});
});
};
// This supresses a lot of annoying console output from the mc library
// TODO: find a better way to do this, it supresses other useful output
const originalConsoleLog = console.log;
console.log = () => { };
for (let port = startPort; port <= endPort; port++) {
const openPort = await checkPort(port);
if (openPort) {
const server = await serverInfo(ip, port, 200, false);
if (server) {
servers.push(server);
if (earlyExit) break;
}
}
}
// Restore console output
console.log = originalConsoleLog;
return servers;
}
/**
* Gets the MC server info from the host and port.
* @param {string} host - The host to search for.
* @param {number} port - The port to search for.
* @param {string} version - The version to search for.
* @returns {Promise<Object>} - A Promise that resolves to the server info object.
*/
export async function getServer(host, port, version) {
let server = null;
let serverString = "";
let serverVersion = "";
// Search for server
if (port == -1)
{
console.log(`No port provided. Searching for LAN server on host ${host}...`);
await findServers(host, true).then((servers) => {
if (servers.length > 0)
server = servers[0];
});
if (server == null)
throw new Error(`No server found on LAN.`);
}
else
server = await serverInfo(host, port, 1000, true);
// Server not found
if (server == null)
throw new Error(`MC server not found. (Host: ${host}, Port: ${port}) Check the host and port in settings.js, and ensure the server is running and open to public or LAN.`);
serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`;
if (version === "auto")
serverVersion = server.version;
else
serverVersion = version;
// Server version unsupported / mismatch
if (mc.supportedVersions.indexOf(serverVersion) === -1)
throw new Error(`MC server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`);
else if (version !== "auto" && server.version !== version)
throw new Error(`MC server was found ${serverString}, but version is incorrect. Expected ${version}, but found ${server.version}. Check the server version in settings.js.`);
else
console.log(`MC server found. ${serverString}`);
return server;
}

View file

@ -1,11 +1,11 @@
import { createMindServer, registerAgent } from './mindserver.js';
import { AgentProcess } from '../process/agent_process.js';
import { getServer } from './mcserver.js';
let mindserver;
let connected = false;
let agent_processes = {};
let agent_count = 0;
let host = 'localhost';
let port = 8080;
export async function init(host_public=false, port=8080) {
@ -28,6 +28,12 @@ export async function createAgent(settings) {
registerAgent(settings);
let load_memory = settings.load_memory || false;
let init_message = settings.init_message || null;
const server = await getServer(settings.host, settings.port, settings.minecraft_version);
settings.host = server.host;
settings.port = server.port;
settings.minecraft_version = server.version;
const agentProcess = new AgentProcess(agent_name, port);
agentProcess.start(load_memory, init_message, agent_count);
agent_count++;

View file

@ -170,6 +170,10 @@ export function createMindServer(host_public = false, port = 8080) {
console.error('Error: ', error);
}
});
socket.on('bot-output', (agentName, message) => {
io.emit('bot-output', agentName, message);
});
});
let host = host_public ? '0.0.0.0' : 'localhost';

View file

@ -25,8 +25,8 @@
background: #363636;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
align-items: flex-start;
}
.restart-btn, .start-btn, .stop-btn {
color: white;
@ -102,6 +102,13 @@
border: none;
margin-left: 10px;
}
.last-message {
font-style: italic;
color: #aaa;
margin-top: 5px;
white-space: pre-wrap;
word-break: break-word;
}
.start-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
@ -135,6 +142,7 @@
let settingsSpec = {};
let profileData = null;
const agentSettings = {};
const agentLastMessage = {};
fetch('/settings_spec.json')
.then(r => r.json())
@ -229,6 +237,14 @@
});
});
socket.on('bot-output', (agentName, message) => {
agentLastMessage[agentName] = message;
const messageDiv = document.getElementById(`lastMessage-${agentName}`);
if (messageDiv) {
messageDiv.textContent = message;
}
});
function fetchAgentSettings(name) {
return new Promise((resolve) => {
if (agentSettings[name]) { resolve(agentSettings[name]); return; }
@ -250,9 +266,10 @@
const cfg = agentSettings[agent.name] || {};
const showViewer = cfg.render_bot_view === true;
const viewerHTML = showViewer ? `<div class="agent-view-container"><iframe class="agent-viewer" src="http://localhost:${3000 + idx}"></iframe></div>` : '';
const lastMessage = agentLastMessage[agent.name] || '';
return `
<div class="agent">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
<span><span class="status-icon ${agent.in_game ? 'online' : 'offline'}"></span>${agent.name}</span>
<div style="display:flex;align-items:center;">
${agent.in_game ? `
@ -265,6 +282,7 @@
`}
</div>
</div>
<div id="lastMessage-${agent.name}" class="last-message">${lastMessage}</div>
${viewerHTML}
</div>`;
}).join('') +

View file

@ -94,10 +94,10 @@
"description": "Whether to log all prompts to file. Can be very verbose.",
"default": false
},
"verbose_commands": {
"type": "boolean",
"description": "Whether to show full command syntax in bot responses. If false will use a shortened syntax.",
"default": true
"show_command_syntax": {
"type": "string",
"description": "Whether to show \"full\" command syntax, \"shortened\" command syntax, or \"none\"",
"default": "full"
},
"chat_bot_messages": {
"type": "boolean",

89
src/models/_model_map.js Normal file
View file

@ -0,0 +1,89 @@
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Dynamically discover model classes in this directory.
// Each model class must export a static `prefix` string.
const apiMap = await (async () => {
const map = {};
const files = (await fs.readdir(__dirname))
.filter(f => f.endsWith('.js') && f !== '_model_map.js' && f !== 'prompter.js');
for (const file of files) {
try {
const moduleUrl = pathToFileURL(path.join(__dirname, file)).href;
const mod = await import(moduleUrl);
for (const exported of Object.values(mod)) {
if (typeof exported === 'function' && Object.prototype.hasOwnProperty.call(exported, 'prefix')) {
const prefix = exported.prefix;
if (typeof prefix === 'string' && prefix.length > 0) {
map[prefix] = exported;
}
}
}
} catch (e) {
console.warn('Failed to load model module:', file, e?.message || e);
}
}
return map;
})();
export function selectAPI(profile) {
if (typeof profile === 'string' || profile instanceof String) {
profile = {model: profile};
}
// backwards compatibility with local->ollama
if (profile.api?.includes('local') || profile.model?.includes('local')) {
profile.api = 'ollama';
if (profile.model) {
profile.model = profile.model.replace('local', 'ollama');
}
}
if (!profile.api) {
const api = Object.keys(apiMap).find(key => profile.model?.startsWith(key));
if (api) {
profile.api = api;
}
else {
// check for some common models that do not require prefixes
if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3'))
profile.api = 'openai';
else if (profile.model.includes('claude'))
profile.api = 'anthropic';
else if (profile.model.includes('gemini'))
profile.api = "google";
else if (profile.model.includes('grok'))
profile.api = 'grok';
else if (profile.model.includes('mistral'))
profile.api = 'mistral';
else if (profile.model.includes('deepseek'))
profile.api = 'deepseek';
else if (profile.model.includes('qwen'))
profile.api = 'qwen';
}
if (!profile.api) {
throw new Error('Unknown model:', profile.model);
}
}
if (!apiMap[profile.api]) {
throw new Error('Unknown api:', profile.api);
}
let model_name = profile.model.replace(profile.api + '/', ''); // remove prefix
profile.model = model_name === "" ? null : model_name; // if model is empty, set to null
return profile;
}
export function createModel(profile) {
if (!!apiMap[profile.model]) {
// if the model value is an api (instead of a specific model name)
// then set model to null so it uses the default model for that api
profile.model = null;
}
if (!apiMap[profile.api]) {
throw new Error('Unknown api:', profile.api);
}
const model = new apiMap[profile.api](profile.model, profile.url, profile.params);
return model;
}

32
src/models/azure.js Normal file
View file

@ -0,0 +1,32 @@
import { AzureOpenAI } from "openai";
import { getKey, hasKey } from '../utils/keys.js';
import { GPT } from './gpt.js'
export class AzureGPT extends GPT {
static prefix = 'azure';
constructor(model_name, url, params) {
super(model_name, url)
this.model_name = model_name;
this.params = params || {};
const config = {};
if (url)
config.endpoint = url;
config.apiKey = hasKey('AZURE_OPENAI_API_KEY') ? getKey('AZURE_OPENAI_API_KEY') : getKey('OPENAI_API_KEY');
config.deployment = model_name;
if (this.params.apiVersion) {
config.apiVersion = this.params.apiVersion;
delete this.params.apiVersion; // remove from params for later use in requests
}
else {
throw new Error('apiVersion is required in params for azure!');
}
this.openai = new AzureOpenAI(config)
}
}

61
src/models/cerebras.js Normal file
View file

@ -0,0 +1,61 @@
import CerebrasSDK from '@cerebras/cerebras_cloud_sdk';
import { strictFormat } from '../utils/text.js';
import { getKey } from '../utils/keys.js';
export class Cerebras {
static prefix = 'cerebras';
constructor(model_name, url, params) {
this.model_name = model_name;
this.url = url;
this.params = params;
// Initialize client with API key
this.client = new CerebrasSDK({ apiKey: getKey('CEREBRAS_API_KEY') });
}
async sendRequest(turns, systemMessage, stop_seq = '***') {
// Format messages array
const messages = strictFormat(turns);
messages.unshift({ role: 'system', content: systemMessage });
const pack = {
model: this.model_name || 'gpt-oss-120b',
messages,
stream: false,
...(this.params || {}),
};
let res;
try {
const completion = await this.client.chat.completions.create(pack);
// OpenAI-compatible shape
res = completion.choices?.[0]?.message?.content || '';
} catch (err) {
console.error('Cerebras API error:', err);
res = 'My brain disconnected, try again.';
}
return res;
}
async sendVisionRequest(messages, systemMessage, imageBuffer) {
const imageMessages = [...messages];
imageMessages.push({
role: "user",
content: [
{ type: "text", text: systemMessage },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
}
}
]
});
return this.sendRequest(imageMessages, systemMessage);
}
async embed(text) {
throw new Error('Embeddings are not supported by Cerebras.');
}
}

View file

@ -3,6 +3,7 @@ import { strictFormat } from '../utils/text.js';
import { getKey } from '../utils/keys.js';
export class Claude {
static prefix = 'anthropic';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params || {};
@ -20,7 +21,7 @@ export class Claude {
const messages = strictFormat(turns);
let res = null;
try {
console.log('Awaiting anthropic api response...')
console.log(`Awaiting anthropic response from ${this.model_name}...`)
if (!this.params.max_tokens) {
if (this.params.thinking?.budget_tokens) {
this.params.max_tokens = this.params.thinking.budget_tokens + 1000;
@ -30,7 +31,7 @@ export class Claude {
}
}
const resp = await this.anthropic.messages.create({
model: this.model_name || "claude-3-sonnet-20240229",
model: this.model_name || "claude-sonnet-4-20250514",
system: systemMessage,
messages: messages,
...(this.params || {})

View file

@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class DeepSeek {
static prefix = 'deepseek';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;

View file

@ -6,8 +6,9 @@ import { lamejs } from 'lamejs/lame.all.js';
export class Gemini {
constructor(model, url, params) {
this.model = model || "gemini-2.5-flash";
static prefix = 'google';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;
this.safetySettings = [
{
@ -48,7 +49,7 @@ export class Gemini {
}
const result = await this.genAI.models.generateContent({
model: this.model,
model: this.model_name || "gemini-2.5-flash",
contents: contents,
safetySettings: this.safetySettings,
config: {
@ -112,7 +113,7 @@ export class Gemini {
async embed(text) {
const result = await this.genAI.models.embedContent({
model: 'gemini-embedding-001',
model: this.model_name || "gemini-embedding-001",
contents: text,
})

View file

@ -2,6 +2,7 @@ import OpenAIApi from 'openai';
import { getKey } from '../utils/keys.js';
export class GLHF {
static prefix = 'glhf';
constructor(model_name, url) {
this.model_name = model_name;
const apiKey = getKey('GHLF_API_KEY');

View file

@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class GPT {
static prefix = 'openai';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;
@ -22,20 +23,21 @@ export class GPT {
async sendRequest(turns, systemMessage, stop_seq='***') {
let messages = [{'role': 'system', 'content': systemMessage}].concat(turns);
messages = strictFormat(messages);
let model = this.model_name || "gpt-4o-mini";
const pack = {
model: this.model_name || "gpt-3.5-turbo",
model: model,
messages,
stop: stop_seq,
...(this.params || {})
};
if (this.model_name.includes('o1') || this.model_name.includes('o3') || this.model_name.includes('5')) {
if (model.includes('o1') || model.includes('o3') || model.includes('5')) {
delete pack.stop;
}
let res = null;
try {
console.log('Awaiting openai api response from model', this.model_name)
console.log('Awaiting openai api response from model', model)
// console.log('Messages:', messages);
let completion = await this.openai.chat.completions.create(pack);
if (completion.choices[0].finish_reason == 'length')

View file

@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js';
// xAI doesn't supply a SDK for their models, but fully supports OpenAI and Anthropic SDKs
export class Grok {
static prefix = 'xai';
constructor(model_name, url, params) {
this.model_name = model_name;
this.url = url;
@ -19,13 +20,12 @@ export class Grok {
this.openai = new OpenAIApi(config);
}
async sendRequest(turns, systemMessage, stop_seq='***') {
async sendRequest(turns, systemMessage) {
let messages = [{'role': 'system', 'content': systemMessage}].concat(turns);
const pack = {
model: this.model_name || "grok-beta",
model: this.model_name || "grok-3-mini-latest",
messages,
stop: [stop_seq],
...(this.params || {})
};
@ -42,7 +42,7 @@ export class Grok {
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);
return await this.sendRequest(turns.slice(1), systemMessage);
} else if (err.message.includes('The model expects a single `text` element per message.')) {
console.log(err);
res = 'Vision is only supported by certain models.';

View file

@ -6,6 +6,7 @@ import { getKey } from '../utils/keys.js';
// Umbrella class for everything under the sun... That GroqCloud provides, that is.
export class GroqCloudAPI {
static prefix = 'groq';
constructor(model_name, url, params) {
@ -49,7 +50,7 @@ export class GroqCloudAPI {
let completion = await this.groq.chat.completions.create({
"messages": messages,
"model": this.model_name || "llama-3.3-70b-versatile",
"model": this.model_name || "qwen/qwen3-32b",
"stream": false,
"stop": stop_seq,
...(this.params || {})
@ -63,7 +64,6 @@ export class GroqCloudAPI {
if (err.message.includes("content must be a string")) {
res = "Vision is only supported by certain models.";
} else {
console.log(this.model_name);
res = "My brain disconnected, try again.";
}
console.log(err);

View file

@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js';
import { HfInference } from "@huggingface/inference";
export class HuggingFace {
static prefix = 'huggingface';
constructor(model_name, url, params) {
// Remove 'huggingface/' prefix if present
this.model_name = model_name.replace('huggingface/', '');

View file

@ -1,6 +1,7 @@
import { getKey } from '../utils/keys.js';
export class Hyperbolic {
static prefix = 'hyperbolic';
constructor(modelName, apiUrl) {
this.modelName = modelName || "deepseek-ai/DeepSeek-V3";
this.apiUrl = apiUrl || "https://api.hyperbolic.xyz/v1/chat/completions";

95
src/models/mercury.js Normal file
View file

@ -0,0 +1,95 @@
import OpenAIApi from 'openai';
import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class Mercury {
static prefix = 'mercury';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;
let config = {};
if (url)
config.baseURL = url;
else
config.baseURL = "https://api.inceptionlabs.ai/v1";
config.apiKey = getKey('MERCURY_API_KEY');
this.openai = new OpenAIApi(config);
}
async sendRequest(turns, systemMessage, stop_seq='***') {
if (typeof stop_seq === 'string') {
stop_seq = [stop_seq];
} else if (!Array.isArray(stop_seq)) {
stop_seq = [];
}
let messages = [{'role': 'system', 'content': systemMessage}].concat(turns);
messages = strictFormat(messages);
const pack = {
model: this.model_name || "mercury-coder-small",
messages,
stop: stop_seq,
...(this.params || {})
};
let res = null;
try {
console.log('Awaiting mercury api response from model', this.model_name)
// 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) {
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 if (err.message.includes('image_url')) {
console.log(err);
res = 'Vision is only supported by certain models.';
} else {
console.log(err);
res = 'My brain disconnected, try again.';
}
}
return res;
}
async sendVisionRequest(messages, systemMessage, imageBuffer) {
const imageMessages = [...messages];
imageMessages.push({
role: "user",
content: [
{ type: "text", text: systemMessage },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
}
}
]
});
return this.sendRequest(imageMessages, systemMessage);
}
async embed(text) {
if (text.length > 8191)
text = text.slice(0, 8191);
const embedding = await this.openai.embeddings.create({
model: this.model_name || "text-embedding-3-small",
input: text,
encoding_format: "float",
});
return embedding.data[0].embedding;
}
}

View file

@ -3,6 +3,7 @@ import { getKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class Mistral {
static prefix = 'mistral';
#client;
constructor(model_name, url, params) {

View file

@ -4,8 +4,9 @@ import { strictFormat } from '../utils/text.js';
// llama, mistral
export class Novita {
static prefix = 'novita';
constructor(model_name, url, params) {
this.model_name = model_name.replace('novita/', '');
this.model_name = model_name;
this.url = url || 'https://api.novita.ai/v3/openai';
this.params = params;
@ -25,7 +26,7 @@ export class Novita {
messages = strictFormat(messages);
const pack = {
model: this.model_name || "meta-llama/llama-3.1-70b-instruct",
model: this.model_name || "meta-llama/llama-4-scout-17b-16e-instruct",
messages,
stop: [stop_seq],
...(this.params || {})

View file

@ -1,6 +1,7 @@
import { strictFormat } from '../utils/text.js';
export class Local {
export class Ollama {
static prefix = 'ollama';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;
@ -10,11 +11,9 @@ export class Local {
}
async sendRequest(turns, systemMessage) {
let model = this.model_name || 'llama3.1'; // Updated to llama3.1, as it is more performant than llama3
let model = this.model_name || 'sweaterdog/andy-4:micro-q8_0';
let messages = strictFormat(turns);
messages.unshift({ role: 'system', content: systemMessage });
// We'll attempt up to 5 times for models with deepseek-r1-esk reasoning if the <think> tags are mismatched.
const maxAttempts = 5;
let attempt = 0;
let finalRes = null;
@ -24,14 +23,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.';
}
@ -43,36 +42,27 @@ 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 <think> block.
const hasOpenTag = res.includes("<think>");
const hasCloseTag = res.includes("</think>");
// If there's a partial mismatch, retry to get a complete response.
if ((hasOpenTag && !hasCloseTag)) {
console.warn("Partial <think> block detected. Re-generating...");
continue;
}
// If </think> is present but <think> is not, prepend <think>
if (hasCloseTag && !hasOpenTag) {
res = '<think>' + res;
}
// Changed this so if the model reasons, using <think> and </think> but doesn't start the message with <think>, <think> ges prepended to the message so no error occur.
// If both tags appear, remove them (and everything inside).
if (hasOpenTag && hasCloseTag) {
res = res.replace(/<think>[\s\S]*?<\/think>/g, '');
}
const hasOpenTag = res.includes("<think>");
const hasCloseTag = res.includes("</think>");
if ((hasOpenTag && !hasCloseTag)) {
console.warn("Partial <think> block detected. Re-generating...");
if (attempt < maxAttempts) continue;
}
if (hasCloseTag && !hasOpenTag) {
res = '<think>' + res;
}
if (hasOpenTag && hasCloseTag) {
res = res.replace(/<think>[\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 <think> 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.';
}
return finalRes;
@ -104,4 +94,22 @@ export class Local {
}
return data;
}
async sendVisionRequest(messages, systemMessage, imageBuffer) {
const imageMessages = [...messages];
imageMessages.push({
role: "user",
content: [
{ type: "text", text: systemMessage },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`
}
}
]
});
return this.sendRequest(imageMessages, systemMessage);
}
}

View file

@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class OpenRouter {
static prefix = 'openrouter';
constructor(model_name, url) {
this.model_name = model_name;

View file

@ -5,26 +5,10 @@ import { SkillLibrary } from "../agent/library/skill_library.js";
import { stringifyTurns } from '../utils/text.js';
import { getCommand } from '../agent/commands/index.js';
import settings from '../agent/settings.js';
import { Gemini } from './gemini.js';
import { GPT } from './gpt.js';
import { Claude } from './claude.js';
import { Mistral } from './mistral.js';
import { ReplicateAPI } from './replicate.js';
import { Local } from './local.js';
import { Novita } from './novita.js';
import { GroqCloudAPI } from './groq.js';
import { HuggingFace } from './huggingface.js';
import { Qwen } from "./qwen.js";
import { Grok } from "./grok.js";
import { DeepSeek } from './deepseek.js';
import { Hyperbolic } from './hyperbolic.js';
import { GLHF } from './glhf.js';
import { OpenRouter } from './openrouter.js';
import { VLLM } from './vllm.js';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { selectAPI, createModel } from './_model_map.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -66,70 +50,46 @@ export class Prompter {
this.last_prompt_time = 0;
this.awaiting_coding = false;
// try to get "max_tokens" parameter, else null
// for backwards compatibility, move max_tokens to params
let max_tokens = null;
if (this.profile.max_tokens)
max_tokens = this.profile.max_tokens;
let chat_model_profile = this._selectAPI(this.profile.model);
this.chat_model = this._createModel(chat_model_profile);
let chat_model_profile = selectAPI(this.profile.model);
this.chat_model = createModel(chat_model_profile);
if (this.profile.code_model) {
let code_model_profile = this._selectAPI(this.profile.code_model);
this.code_model = this._createModel(code_model_profile);
let code_model_profile = selectAPI(this.profile.code_model);
this.code_model = createModel(code_model_profile);
}
else {
this.code_model = this.chat_model;
}
if (this.profile.vision_model) {
let vision_model_profile = this._selectAPI(this.profile.vision_model);
this.vision_model = this._createModel(vision_model_profile);
let vision_model_profile = selectAPI(this.profile.vision_model);
this.vision_model = createModel(vision_model_profile);
}
else {
this.vision_model = this.chat_model;
}
let embedding = this.profile.embedding;
if (embedding === undefined) {
if (chat_model_profile.api !== 'ollama')
embedding = {api: chat_model_profile.api};
else
embedding = {api: 'none'};
}
else if (typeof embedding === 'string' || embedding instanceof String)
embedding = {api: embedding};
console.log('Using embedding settings:', embedding);
try {
if (embedding.api === 'google')
this.embedding_model = new Gemini(embedding.model, embedding.url);
else if (embedding.api === 'openai')
this.embedding_model = new GPT(embedding.model, embedding.url);
else if (embedding.api === 'replicate')
this.embedding_model = new ReplicateAPI(embedding.model, embedding.url);
else if (embedding.api === 'ollama')
this.embedding_model = new Local(embedding.model, embedding.url);
else if (embedding.api === 'qwen')
this.embedding_model = new Qwen(embedding.model, embedding.url);
else if (embedding.api === 'mistral')
this.embedding_model = new Mistral(embedding.model, embedding.url);
else if (embedding.api === 'huggingface')
this.embedding_model = new HuggingFace(embedding.model, embedding.url);
else if (embedding.api === 'novita')
this.embedding_model = new Novita(embedding.model, embedding.url);
else {
this.embedding_model = null;
let embedding_name = embedding ? embedding.api : '[NOT SPECIFIED]'
console.warn('Unsupported embedding: ' + embedding_name + '. Using word-overlap instead, expect reduced performance. Recommend using a supported embedding model. See Readme.');
let embedding_model_profile = null;
if (this.profile.embedding) {
try {
embedding_model_profile = selectAPI(this.profile.embedding);
} catch (e) {
embedding_model_profile = null;
}
}
catch (err) {
console.warn('Warning: Failed to initialize embedding model:', err.message);
console.log('Continuing anyway, using word-overlap instead.');
this.embedding_model = null;
if (embedding_model_profile) {
this.embedding_model = createModel(embedding_model_profile);
}
else {
this.embedding_model = createModel({api: chat_model_profile.api});
}
this.skill_libary = new SkillLibrary(agent, this.embedding_model);
mkdirSync(`./bots/${name}`, { recursive: true });
writeFileSync(`./bots/${name}/last_profile.json`, JSON.stringify(this.profile, null, 4), (err) => {
@ -140,88 +100,6 @@ export class Prompter {
});
}
_selectAPI(profile) {
if (typeof profile === 'string' || profile instanceof String) {
profile = {model: profile};
}
if (!profile.api) {
if (profile.model.includes('openrouter/'))
profile.api = 'openrouter'; // must do first because shares names with other models
else if (profile.model.includes('ollama/'))
profile.api = 'ollama'; // also must do early because shares names with other models
else if (profile.model.includes('gemini'))
profile.api = 'google';
else if (profile.model.includes('vllm/'))
profile.api = 'vllm';
else if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3'))
profile.api = 'openai';
else if (profile.model.includes('claude'))
profile.api = 'anthropic';
else if (profile.model.includes('huggingface/'))
profile.api = "huggingface";
else if (profile.model.includes('replicate/'))
profile.api = 'replicate';
else if (profile.model.includes('mistralai/') || profile.model.includes("mistral/"))
model_profile.api = 'mistral';
else if (profile.model.includes("groq/") || profile.model.includes("groqcloud/"))
profile.api = 'groq';
else if (profile.model.includes("glhf/"))
profile.api = 'glhf';
else if (profile.model.includes("hyperbolic/"))
profile.api = 'hyperbolic';
else if (profile.model.includes('novita/'))
profile.api = 'novita';
else if (profile.model.includes('qwen'))
profile.api = 'qwen';
else if (profile.model.includes('grok'))
profile.api = 'xai';
else if (profile.model.includes('deepseek'))
profile.api = 'deepseek';
else if (profile.model.includes('mistral'))
profile.api = 'mistral';
else
throw new Error('Unknown model:', profile.model);
}
return profile;
}
_createModel(profile) {
let model = null;
if (profile.api === 'google')
model = new Gemini(profile.model, profile.url, profile.params);
else if (profile.api === 'openai')
model = new GPT(profile.model, profile.url, profile.params);
else if (profile.api === 'anthropic')
model = new Claude(profile.model, profile.url, profile.params);
else if (profile.api === 'replicate')
model = new ReplicateAPI(profile.model.replace('replicate/', ''), profile.url, profile.params);
else if (profile.api === 'ollama')
model = new Local(profile.model.replace('ollama/', ''), profile.url, profile.params);
else if (profile.api === 'mistral')
model = new Mistral(profile.model, profile.url, profile.params);
else if (profile.api === 'groq')
model = new GroqCloudAPI(profile.model.replace('groq/', '').replace('groqcloud/', ''), profile.url, profile.params);
else if (profile.api === 'huggingface')
model = new HuggingFace(profile.model, profile.url, profile.params);
else if (profile.api === 'glhf')
model = new GLHF(profile.model.replace('glhf/', ''), profile.url, profile.params);
else if (profile.api === 'hyperbolic')
model = new Hyperbolic(profile.model.replace('hyperbolic/', ''), profile.url, profile.params);
else if (profile.api === 'novita')
model = new Novita(profile.model.replace('novita/', ''), profile.url, profile.params);
else if (profile.api === 'qwen')
model = new Qwen(profile.model, profile.url, profile.params);
else if (profile.api === 'xai')
model = new Grok(profile.model, profile.url, profile.params);
else if (profile.api === 'deepseek')
model = new DeepSeek(profile.model, profile.url, profile.params);
else if (profile.api === 'openrouter')
model = new OpenRouter(profile.model.replace('openrouter/', ''), profile.url, profile.params);
else if (profile.api === 'vllm')
model = new VLLM(profile.model.replace('vllm/', ''), profile.url, profile.params);
else
throw new Error('Unknown API:', profile.api);
return model;
}
getName() {
return this.profile.name;
}
@ -404,7 +282,7 @@ export class Prompter {
await this._saveLog(prompt, to_summarize, resp, 'memSaving');
if (resp?.includes('</think>')) {
const [_, afterThink] = resp.split('</think>')
resp = afterThink
resp = afterThink;
}
return resp;
}
@ -482,6 +360,4 @@ export class Prompter {
logFile = path.join(logDir, logFile);
await fs.appendFile(logFile, String(logEntry), 'utf-8');
}
}

View file

@ -3,6 +3,7 @@ import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class Qwen {
static prefix = 'qwen';
constructor(model_name, url, params) {
this.model_name = model_name;
this.params = params;

View file

@ -4,6 +4,7 @@ import { getKey } from '../utils/keys.js';
// llama, mistral
export class ReplicateAPI {
static prefix = 'replicate';
constructor(model_name, url, params) {
this.model_name = model_name;
this.url = url;

View file

@ -6,6 +6,7 @@ import { getKey, hasKey } from '../utils/keys.js';
import { strictFormat } from '../utils/text.js';
export class VLLM {
static prefix = 'vllm';
constructor(model_name, url) {
this.model_name = model_name;
@ -23,13 +24,14 @@ export class VLLM {
async sendRequest(turns, systemMessage, stop_seq = '***') {
let messages = [{ 'role': 'system', 'content': systemMessage }].concat(turns);
let model = this.model_name || "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B";
if (this.model_name.includes('deepseek') || this.model_name.includes('qwen')) {
if (model.includes('deepseek') || model.includes('qwen')) {
messages = strictFormat(messages);
}
const pack = {
model: this.model_name || "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
model: model,
messages,
stop: stop_seq,
};