opencode / bot.js
abbasyk60's picture
Update to Qwen2.5-Coder-3B
a69619e
Raw
History Blame Contribute Delete
7.39 kB
const { Bot } = require("grammy");
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
const ZEN_API_KEY = process.env.OPENCODE_ZEN_KEY;
const ROUTER_URL = process.env.ROUTER_URL || "http://127.0.0.1:7861";
if (!BOT_TOKEN) { console.error("[ERROR] TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
if (!ZEN_API_KEY) { console.error("[ERROR] OPENCODE_ZEN_KEY not set"); process.exit(1); }
console.log("[CONFIG] CHAT_ID filter:", CHAT_ID || "ALL");
console.log("[CONFIG] Zen Key:", ZEN_API_KEY.substring(0, 12) + "...");
console.log("[CONFIG] Router:", ROUTER_URL);
const ZEN_BASE = "https://opencode.ai/zen/v1";
const LOCAL_MODEL = "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF";
const LOCAL_API = "https://abbasyk60-ai-telegram-bot.hf.space/v1/chat/completions";
const FREE_MODELS = ["deepseek-v4-flash-free", "mimo-v2.5-free", "big-pickle", "laguna-s-2.1-free", "ling-3.0-flash-free", "north-mini-code-free", "nemotron-3-ultra-free"];
const bot = new Bot(BOT_TOKEN);
const userSessions = new Map();
async function callAPI(messages, modelId, url, headers, bodyExtra = {}) {
const body = { model: modelId, messages, max_tokens: 2048, ...bodyExtra };
const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
if (!response.ok) {
const err = await response.text();
throw new Error(`API ${response.status}: ${err.substring(0, 200)}`);
}
const data = await response.json();
if (data.choices?.[0]?.message?.content?.trim()) return data.choices[0].message.content;
if (data.choices?.[0]?.message?.reasoning_content?.trim()) return data.choices[0].message.reasoning_content;
throw new Error("Empty response");
}
async function callWithFallback(messages, preferredModel) {
// If user wants local model, try it first
if (preferredModel === LOCAL_MODEL || preferredModel === "deepseek-r1-7b") {
try {
const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
console.log("[OK] Local model responded");
return response;
} catch (e) {
console.log("[SKIP] Local failed:", e.message.substring(0, 80));
}
}
// Try preferred model on Zen
const modelsToTry = preferredModel === LOCAL_MODEL ? FREE_MODELS : [preferredModel, ...FREE_MODELS.filter((m) => m !== preferredModel)];
for (const model of modelsToTry) {
// Try Zen API
try {
const response = await callAPI(messages, model, `${ZEN_BASE}/chat/completions`, {
Authorization: `Bearer ${ZEN_API_KEY}`,
"Content-Type": "application/json",
});
console.log("[OK] Zen responded with:", model);
return response;
} catch (e) {
console.log("[SKIP] Zen failed:", model, e.message.substring(0, 80));
}
// Try local model as fallback
try {
const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
console.log("[OK] Local model responded (fallback)");
return response;
} catch (e) {
console.log("[SKIP] Local fallback failed:", e.message.substring(0, 80));
}
}
// Last resort: try local model alone
try {
const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
console.log("[OK] Local model responded (last resort)");
return response;
} catch (e) {
console.log("[SKIP] Local last resort failed:", e.message.substring(0, 80));
}
throw new Error("All models failed. Try again in a minute.");
}
bot.command("start", async (ctx) => {
const chatId = ctx.chat.id;
if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
await ctx.reply(
"Welcome to OpenCode AI Agent!\n\n" +
"Commands:\n" +
"/model <name> - Switch model\n" +
"/models - List available models\n" +
"/new - New conversation\n" +
"/help - Show help\n\n" +
"Default model: DeepSeek V4 Flash (FREE)\n" +
"Local model: DeepSeek R1 7B (free, runs on your server)"
);
});
bot.command("help", async (ctx) => {
await ctx.reply(
"Send any message and I'll respond using AI.\n\n" +
"Commands:\n" +
"/start - Initialize bot\n" +
"/models - List models\n" +
"/model <name> - Switch model\n" +
"/new - Clear history"
);
});
bot.command("models", async (ctx) => {
await ctx.reply(
"FREE Models (Zen API):\n" +
"- deepseek-v4-flash-free\n" +
"- mimo-v2.5-free\n" +
"- big-pickle\n" +
"- laguna-s-2.1-free\n" +
"- ling-3.0-flash-free\n" +
"- north-mini-code-free\n" +
"- nemotron-3-ultra-free\n\n" +
"LOCAL Model (Your Server):\n" +
"- deepseek-r1-7b (DeepSeek R1 7B)\n\n" +
"Paid: gpt-5.5, gpt-5.4, claude-sonnet-5, gemini-3.6-flash, etc.\n\n" +
"/model <model-id>"
);
});
bot.command("model", async (ctx) => {
const chatId = ctx.chat.id;
if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
const args = ctx.message.text.split(" ").slice(1);
if (args.length === 0) {
const session = userSessions.get(chatId);
await ctx.reply("Current: " + (session?.model || "deepseek-v4-flash-free"));
return;
}
let modelId = args[0].toLowerCase();
if (modelId === "deepseek-r1-7b" || modelId === "local") modelId = LOCAL_MODEL;
const session = userSessions.get(chatId) || { history: [] };
userSessions.set(chatId, { model: modelId, history: [] });
await ctx.reply("Switched to: " + modelId);
});
bot.command("new", async (ctx) => {
const chatId = ctx.chat.id;
if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
const session = userSessions.get(chatId);
userSessions.set(chatId, { model: session?.model || "deepseek-v4-flash-free", history: [] });
await ctx.reply("New conversation started!");
});
bot.on("message:text", async (ctx) => {
const chatId = ctx.chat.id;
const message = ctx.message.text;
if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) {
console.log("[BLOCKED] Chat", chatId);
return;
}
console.log("[MSG]", chatId, ":", message.substring(0, 50));
await ctx.replyWithChatAction("typing");
if (!userSessions.has(chatId)) {
userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
}
const session = userSessions.get(chatId);
const messages = [...session.history, { role: "user", content: message }];
try {
const response = await callWithFallback(messages, session.model);
session.history.push({ role: "user", content: message });
session.history.push({ role: "assistant", content: response });
if (session.history.length > 20) session.history = session.history.slice(-20);
if (response.length > 4000) {
const chunks = response.match(/.{1,4000}/gs) || [response];
for (const chunk of chunks) await ctx.reply(chunk);
} else {
await ctx.reply(response);
}
console.log("[REPLY]", response.length, "chars");
} catch (error) {
console.error("[ERROR]", error.message);
await ctx.reply("Error: " + error.message);
}
});
bot.catch((err) => console.error("[BOT ERROR]", err));
console.log("[OK] Starting Telegram bot...");
bot.start({
onStart: () => {
console.log("[OK] Telegram bot running!");
console.log("[OK] Chat ID:", CHAT_ID || "none");
},
drop_pending_updates: true,
});